Skip to content

Native analytical backends: ClickHouse and Databricks

The shared precis_mcp.analytical interface provides lazy ClickHouse and native Databricks SQL connections. Main reporting, inspection, hierarchy discovery, scenario permission reads and MCP instructions now select the configured client. ClickHouse remains the default. Native Databricks reporting is implemented and accepted on the reference deployment. Native startup requires validated customer inputs and a stored Précis scenario registry. Commercial planning workflows are outside the open package; basic SQL execution does not add planning tools to the external MCP surface.

For required reporting-view columns, dimension keys, scenario-table types and hierarchy inputs, see the Databricks schema contract.

Install the optional native driver with pip install 'precis-finance-mcp[databricks-primary]' from a build containing this increment. The existing databricks extra remains Ibis federation. Both image definitions accept PRECIS_EXTRAS=databricks-primary to install the native dependencies; this is not an integrated backend deployment profile. ClickHouse-only installs do not need the new dependencies.

Backend choice and indicative timings

For interactive reporting, local ClickHouse can return a small financial report in well under a second at the tool level. Databricks can take several seconds to tens of seconds for the same report, even with an already-running warehouse. These are observed orders of magnitude from the development deployment, not service commitments or limits of either database.

The following examples were observed on 2026-10-25 with the same demo data and report selections: a P&L with 19 displayed lines, five scenario/comparison columns and six financial SQL statements. All displayed results matched. ClickHouse ran on the application host; Databricks was a remote SQL warehouse. The Databricks observations used connection reuse and up to three concurrent financial queries. The ClickHouse observation preceded those changes and its hierarchy materialization. This is a small single-user sample from different implementation stages, not a controlled comparison or a fixed slowdown ratio.

Measurement Local ClickHouse, one observed request Databricks, first / repeated report
Sum of database-reported durations for six financial SQL statements 0.37 s 37.2 / 11.2 s
Elapsed window covering those financial statements Not separately recorded 14.3 / 4.0 s
Complete run_statement tool call 0.43 s 16.0 / 4.6 s
Report payload emitted, measured from agent-turn start 3.9 s 20.0 / 6.8 s
Complete agent turn, including model calls 7.7 s 24.6 / 9.1 s

Database-reported statement durations include compilation and execution; they are not pure computation time. Concurrent statements overlap, so their summed durations can exceed elapsed wall time and must not be read as CPU usage or compute cost. Tool time additionally includes metadata reads, query submission, result fetching and report assembly. Agent-turn time includes model calls; payload emission does not measure browser rendering or authentication before the trace starts. An open MCP consumer's own agent adds its own latency, so the agent-turn examples are illustrative rather than intrinsic MCP response times.

The first Databricks report had no financial-query result-cache hits; the repeat hit the result cache for all six statements. The warehouse was already running, and no provisioning or overload queue time was recorded: first does not mean cold warehouse here. A stopped warehouse can add startup time. Warehouse size, region, data volume, query shape, cache state and concurrent demand can change these timings substantially; these observations do not establish production capacity.

Choose local ClickHouse when low interactive latency is a priority and operating the bundled database fits the deployment. Databricks lets an organization keep its existing warehouse and customer-managed pipelines, at the cost of higher observed latency for these small reports. Evaluate representative first and repeat reports on the intended deployment, measuring both tool wall time and database statement timings; SQL execution alone does not describe the user's wait. Commercial planning/write timings are outside this open-package example.

Docker deployment with Databricks

The open multi-user bundle supports an explicit provider overlay. Use Docker Compose 2.24.4 or newer: the overlay replaces provider-specific profiles using the documented !override merge rule. The single-user ClickHouse demo remains unchanged.

Use a source export/build containing this implementation; changing runtime variables does not install drivers in an existing published core image. Build with PRECIS_EXTRAS=databricks-primary. The databricks extra remains the separate Ibis federation option.

Configure DATABRICKS_HOST, DATABRICKS_HTTP_PATH, DATABRICKS_CLIENT_ID, DATABRICKS_CATALOG and DATABRICKS_CLIENT_SECRET_FILE in the deployment environment. The last is an absolute host path to the OAuth secret file. Keep that file outside the Docker build context or inside the Docker-ignored secrets/ directory; arbitrary files under the checkout can be copied into an image. Local Compose file-secret mounts preserve host ownership/permissions: make it readable by the application's numeric UID 10001, with restricted permissions; a root-owned mode-0600 file is unreadable by that application. Compose mounts the file read-only into the serving container. No CLI profile or secret value goes into the image or catalogue. Retain the usual PostgreSQL, identity, artifact-signing and public-origin configuration.

From the exported repository root:

docker compose --env-file deploy/.env \
  -f deploy/docker-compose.yml -f deploy/docker-compose.databricks.yml \
  up -d --build

Configure the usual auth/ingress profiles. The overlay selects databricks and customer_managed, excludes bundled CH and ingestion daemons, and mounts the M2M secret only into the serving application. PostgreSQL migrations still run. The warehouse, semantic inputs, scenarios, hierarchy projections and grants must already exist; startup validates them without provisioning source data. ClickHouse credentials are not required. Selecting an overlay does not stop already-running CH/ingestion containers from an earlier deployment; stop those old services explicitly during an accepted cutover, retaining their volumes.

The remote helper supports scripts/deploy-mcp.sh --data-mode databricks. It selects the overlay, retains configured optional drivers while adding databricks-primary, builds the image and skips CH provisioning. Repeat the data-mode flag on later deployments; omitting it on a configured Databricks deployment is rejected. --data-only cannot provision a customer-managed warehouse. Configure the M2M file/settings on the target before invoking it. Existing CH deployment presets retain their behavior.

Backups use the same backend selector and require no warehouse credentials. The optional backup profile uses UID 10001 for the Databricks sidecar, allowing private managed-file reads without changing artifact permissions. See backup ownership. Configuration rendering and component checks establish neither image nor deployment acceptance; verify the chosen image and platform restore on the target before relying on its recovery coverage.

Credentials and configuration

Production uses explicit service-principal OAuth M2M:

Variable Purpose
DATABRICKS_HOST HTTPS workspace origin, without a path, query or credentials
DATABRICKS_HTTP_PATH /sql/1.0/warehouses/<warehouse-id>
DATABRICKS_AUTH_TYPE Defaults to oauth-m2m; developer CLI access requires explicit databricks-cli
DATABRICKS_CLIENT_ID Service-principal application ID for M2M
DATABRICKS_CLIENT_SECRET Service-principal OAuth secret for M2M
DATABRICKS_CLIENT_SECRET_FILE Optional mounted secret, resolved by the existing shared startup loader
DATABRICKS_CONFIG_PROFILE Optional named profile for explicit developer CLI authentication only
PRECIS_ANALYTICAL_BACKEND Deployment's primary analytical backend: clickhouse (default) or databricks
PRECIS_INGESTION_MODE managed (CH default) or customer_managed (Databricks default and currently required there)
DATABRICKS_CATALOG Catalog for native shared-engine SQL
PRECIS_DATABRICKS_SEMANTIC_SCHEMA Physical schema for logical semantic objects; default semantic
PRECIS_DATABRICKS_PLANNING_SCHEMA Physical schema for logical planning objects; default precis_planning
PRECIS_DATABRICKS_ACTUALS_SCHEMA Physical schema for logical live/actuals objects; default actuals
PRECIS_DATABRICKS_SOCKET_TIMEOUT Positive per-RPC socket timeout in seconds; default 30, maximum 300
PRECIS_DATABRICKS_STATEMENT_TIMEOUT Server SQL execution timeout in seconds; default 60, range 1–172800

Secrets follow the same plain-environment, *_FILE and operator-vault-wrapper conventions as other platform credentials. The factory activates the shared startup loader before reading settings; it does not load a dotenv file itself. Plain values take precedence over mounted files. Restart after file rotation. The M2M path requires both values and does not fall back to CLI credentials. It requests the sql OAuth scope explicitly.

For local development, explicitly select databricks-cli and authenticate the Databricks CLI beforehand. The executable must be on PATH. No password or OAuth token needs to be added to .env; the SDK obtains refreshed credentials through the CLI for the configured workspace. This mode is not for service deployment. Do not enable verbose SDK/connector logging on credential-bearing production traffic: those libraries own their diagnostic output. The native client redacts URLs from CloudFetch download warnings and exception text while retaining their operational context. Signed result-download links must be treated as temporary credentials.

Create service credentials for a native server

In workspace Settings → Identity and access → Service principals, create a dedicated principal for the Précis deployment. Databricks assigns its application/client ID. Open its Secrets tab and generate an OAuth secret restricted to the sql API scope. Store its value when it is displayed; it is shown only once. See Databricks OAuth setup.

Grant the reporting access described below. A valid OAuth secret does not grant warehouse or data access, and creating a principal grants no Précis user role.

On the native host, as the account running Précis:

install -d -m 700 ~/.config/precis/secrets
umask 077
nano ~/.config/precis/secrets/databricks_client_secret
chmod 600 ~/.config/precis/secrets/databricks_client_secret

The file contains only the OAuth secret. Set DATABRICKS_CLIENT_ID and DATABRICKS_CLIENT_SECRET_FILE in the process environment/configuration, using an absolute path to that file. The runtime account must be able to read it. A developer CLI login on another machine is not server authentication. Keep the backend selector unchanged until the warehouse contracts, optional dependencies and deployment assembly have been accepted. Credentials alone do not provision the warehouse objects or establish deployment readiness.

Runtime permissions

The standalone open MCP serving principal needs workspace access, the applicable Databricks SQL entitlement and CAN USE on the selected SQL warehouse. It also needs USE CATALOG on the configured catalog, USE SCHEMA on each accessed schema, and SELECT on required semantic views/tables, including semantic.scenarios. Warehouse access and Unity Catalog data privileges are separate; see warehouse ACLs and Unity Catalog access.

Derive the relation list from the instance's native domain views, dimension masters, hierarchy projections and scenario registry. Resolve their physical names through DATABRICKS_CATALOG and the configured schema roles. An omitted domain/hierarchy does not need grants merely because another instance uses it. Access to a view does not establish direct access to its base tables; the view's owner/dependencies must remain valid. Keep the serving principal read-only.

Operator provisioning and customer-pipeline hierarchy refresh run with separate credentials. Creation needs namespace usage, source SELECT and CREATE TABLE on target schemas; creating a schema also needs CREATE SCHEMA on its catalog. Use an authorized owner of the Précis-owned targets for replacement operations. Scenario seeding requires target read/write access. These are operator powers, not additional requirements for ordinary reporting. Recheck serving grants after view replacement. The renderer itself needs no warehouse credentials.

Interface and limits

Use get_analytical_client("databricks") as a context manager. The default provider is ClickHouse; PRECIS_ANALYTICAL_BACKEND selects the shared reporting and permission-read path. The returned object has query(sql, parameters), execute(sql, parameters) and close(). DatabricksConfig may also be supplied explicitly, with credential fields excluded from its representation.

SQL is trusted compiler/operator input, not an arbitrary SQL tool exposed to agents. Databricks SQL uses native :name mapping parameters or ? positional parameters. ClickHouse retains its typed named markers. SQL dialect translation is not implemented by the client: the shared engine constructs SQL through SqlDialect helpers. Basic execute is trusted low-level SQL transport, not an agent tool or an application mutation framework. Native execution accepts success only after the connector confirms SUCCEEDED; StatementError reports failed/cancelled/unknown/not-submitted feedback and an available statement ID. Précis does not resubmit failed commands. Unknown outcomes require reconciliation before retrying. ClickHouse retains its existing driver's internal transport behaviour; this wrapper does not establish exactly-once execution. Results provide column_names, result_rows, provider-native column_types and an optional query ID, preserving native Python values.

The factory returns a request-owned handle backed by a lazy process-local pool of at most three native clients for matching connection settings. Queries borrow a client exclusively through fetching and cursor cleanup; context exit closes the request handle while healthy sessions remain reusable. Checkout waits at most 30 seconds; idle sessions expire lazily after five minutes. Databricks reuses the SDK credential provider and token renewal, with fresh cursors and UTC session time. CH retains its shared HTTP pool and distinct sessions for concurrent loans. Warehouse type metadata remains request-scoped. Directly constructed native clients and operator provisioning retain their independent session lifecycle.

The retriever submits independent native financial queries across scenarios to three shared workers and merges results in plan order. The pool cap applies across reports in the process; multiple processes each own a pool. No per-user or distributed SQL scheduler is introduced. Worker trace context is explicitly copied. Failed sessions are discarded without silent SQL replay; a required query failure fails the whole report after pending cancellation and worker drain. Databricks cancel() targets only that handle's active reads; its return does not prove terminal execution on the server. A connection is never returned while its query/cursor is still in use. Socket, checkout and SQL execution timeouts do not constitute a total request deadline. No query keepalive prevents warehouse auto-stop. Server lifespan shutdown drains workers and closes pooled resources; one-off users can call close_analytical_resources() explicitly.

The repository's scripts/check_analytical_client.py --catalog <catalog> checks bound values, native types and an existing semantic view using reads only. It may automatically start a stopped warehouse. It does not demonstrate integrated finance-engine parity, concurrency capacity or production M2M grants.

Shared-engine reads and SQL rules

The common engine accepts an injected native client without a second catalogue, resolver, comparison policy or formatter. Its SQL helpers qualify logical role.table references, bind list elements separately and preserve grouping masks with native GROUPING_ID. Filter/scope and scenario metadata read builders use the same seam. Stored report refresh also replays the existing financial tools through their permission gate instead of reconstructing an independent engine request. Trusted low-level statement execution does not add commercial planning workflows to the open package.

No extra read-contract file is required. The existing catalogue supplies the logical names and required columns; startup and explicit reload project those columns with LIMIT 0 and validate stored scenario vocabulary. A failed reload retains the previous catalogue. These checks may start compute but perform no DDL, seeding or ingestion. They do not compare the warehouse with an external expected-schema declaration.

The SQL adapter applies one SUM rule on both backends: no matching rows returns zero; a nonempty group containing only null values returns null. In particular, the former CH nullable-empty SUM result is normalized to zero. Average/min/max and division-by-zero semantics are unchanged. The native driver preserves Decimal values without float conversion. Integer flags and Boolean columns use their actual warehouse types. Nullable dimension attributes remain null; unmatched nonnullable members use the type default. Those lookups use a small cache owned by each client, discarded with that client. No operator metadata registry or backend configuration snapshot framework is involved.

Native readiness reports the databricks_config check without issuing SQL; it does not claim warehouse reachability. Backend and credential changes take effect on restart, using the existing environment/secret conventions. The removed prototype's PRECIS_ANALYTICAL_READ_CONTRACT setting is no longer read.

Customer-managed mode omits Précis ingestion tools and upload/load/configuration routes on either backend. External Ibis source declarations remain separate. Legacy direct CH calls are rejected in a Databricks deployment; they cannot silently use another financial store. Explicit operator CH commissioning via the analytical factory remains possible.

Précis-owned scenario and hierarchy objects

Scenario metadata reads share an in-process registry cache on both backends. PRECIS_SCENARIO_REGISTRY_TTL defaults to 60 seconds. Refresh is on demand after expiry, with no expired fallback on failure; scenario mutations and catalogue reload invalidate locally. Other processes and external updates rely on expiry. Statement/metric tools use one registry snapshot across all columns and engine execution. The cache contains metadata, not caller permissions or financial results: user/profile grants remain fresh PostgreSQL reads and scenario discovery still filters visibility per caller. Write-target resolution, status/lock governance checks and startup/reload validation retain direct database reads.

semantic.scenarios remains the same logical Précis registry. Its column definition is shared by CH and native renderers; PY/variance scenarios remain generated by ScenarioRegistry. Customer actuals/master-data pipelines do not own scenario lifecycle policy. The open MCP surface exposes scenario reads; operators provision the stored registry separately.

scripts/render_databricks_contract.py --catalog <catalog> --output <bundle.json> renders a local operator bundle using the configured catalogue and scenario seed file. It contains the Précis schema/table DDL, bound seed-if-absent MERGE and materialized hierarchy mappings with stable projection views. It requires no warehouse credentials and applies nothing. Review it, validate existing/proposed aliases and IDs, and serialize provisioning with scenario writers before applying with provisioning credentials. MERGE does not guarantee uniqueness under competing writers. Existing scenario rows are never updated by seed SQL; seeds supply every column and do not depend on omitted-column defaults. Customer actuals and master-data tables/views are not created or repaired. scripts/check_databricks_contract.py runs the same read-only validator as startup against the configured endpoint and catalogue, including stored scenarios; it performs no DDL, seeding or ingestion.

Generated and provided hierarchy projections share one closure algorithm, including cycle guards and diamond deduplication. Refresh computes the closure into semantic.dim_<leaf>_<hierarchy>_rollup_materialized; the existing *_rollup view reads those stored pairs, so reporting and scope resolution do not repeat the recursive walk. ClickHouse uses MergeTree storage and Databricks uses Delta. Refresh SQL uses recursive CTEs; the warehouse must support them. Databricks's default recursion and result-size bounds can reject oversized hierarchies; this increment does not silently truncate them or claim unbounded closure support.

Explicit hierarchy provisioning and refresh

The semantic namespace and compatible customer-owned dimension, node and edge input relations must already exist. --provision installs only Précis-owned hierarchy objects; it does not create the namespace or ingest customer inputs.

On either backend, the host-side operator command uses the existing selected backend configuration and *_FILE secret loading. Run it in an operator/pipeline environment with source-read and hierarchy-provisioning permissions. The regular reporting identity and Précis user profiles do not need refresh privileges. Customer-managed refresh credentials are separate from the reporting service; managed CH ingestion continues to use its existing write-capable ingestion client.

# First installation, migration from recursive rollup views, or catalogue hierarchy-definition changes:
python -m precis_mcp.admin_cli hierarchy refresh --provision

# After changing hierarchy leaf data or parent-child relationships:
python -m precis_mcp.admin_cli hierarchy refresh

--catalogue <directory> overrides PRECIS_CATALOGUE_ROOT; otherwise the CLI uses the existing instance-directory resolver. Initial provisioning installs the edge/node views, computes the backing tables and switches the canonical rollup views to those tables. Normal refresh changes backing tables only. Programmatic callers can use analytical.provisioning.refresh_hierarchies(catalogue, execute, dialect) with their own operator connection; provision=True installs definitions as well. The function stops at the first error and performs no automatic retry.

For Databricks, adding --hierarchy-refresh-only to the existing render script produces only the table-replacement SQL for a customer pipeline, without needing a scenario seed file. The bundle is generated SQL, not an additional maintained instance file. Its statements can instead be executed by the customer's own SQL runner. Application startup, readiness and catalogue reload never apply this DDL.

Customer-managed publication contract (CH and Databricks). The customer must invoke refresh after changing leaf IDs, hierarchy ancestor columns or provided edges; initial setup, hierarchy-definition changes and recovery also require provisioning/rebuild before serving the affected reports. No scheduled refresh or change-detection service is installed. Serialize hierarchy updates and keep affected reporting unavailable during source changes and mapping publication. Replacement is atomic for one backing table, not across source tables and all hierarchies. Failed refresh retains that table's previous version, but earlier source updates or mapping replacements may already be visible. Keep reporting unavailable until those states are reconciled. Stale membership can affect authorization scopes; retaining old mappings is not a safe availability fallback. Précis does not automatically enforce the maintenance window or detect external source/closure drift.

Managed CH refreshes all catalogue hierarchies after snapshot ingestion and before marking the load successful. Hierarchy master/edge inputs are expected to use snapshot bindings; period fact loads do not trigger refresh. A refresh failure marks the load failed_swap, retaining the source-swap timestamp when publication already happened; it does not roll back the source. The same reporting-quiescence requirement applies. Direct writes outside ingestion must invoke the explicit refresh. Rebuild these derived mappings after recovery when their sources or catalogue definitions have changed.

The per-table replacement choice follows ClickHouse REPLACE TABLE (Atomic/Replicated databases) and Databricks table replacement. Neither provides a transaction covering this whole publication workflow.

Embedding applications that register register_scenario_query_extension retain their existing two-argument SQL factory on CH. A factory used with a native Databricks client must additionally accept the dialect keyword and construct its markers through that dialect; opaque SQL strings are never translated.

scripts/check_databricks_engine.py is a read-only shared-engine diagnostic requiring the reference-fixture contract: full_pnl, the gl inspection domain, the named revenue/cost/margin metrics, actuals, division/period breakdowns and cost-centre scopes. The operator supplies compatible catalogue/scenario fixtures; it is not a general customer-catalogue validator. It checks an actuals/PY/YoY statement, two-axis grains, independent SQL control totals, empty scopes and synthetic null/default contract violations. It does not test application authentication, stored scenario governance or commercial writes, and its total duration is not a performance benchmark. Optional --benchmark-runs separately measures warmed shared-report calls, including all SQL reads performed inside execute_report. Catalogue, registry are preloaded; MCP/auth, agent and consumer rendering are excluded, and caches are uncontrolled. These are pipeline timings, not complete product latency or a concurrency envelope.