--- name: databricks description: Databricks REST API and SQL reference for Rocky's warehouse adapter. Use when implementing SQL execution, Unity Catalog management, workspace bindings, authentication, permission reconciliation, or any Databricks integration in the rocky-databricks crate. --- # Databricks API Reference for Rocky ## SQL Statement Execution API Primary execution path for all SQL. No SDK needed — pure REST. ### Submit Statement ``` POST https://{host}/api/2.0/sql/statements Authorization: Bearer {token} Content-Type: application/json { "warehouse_id": "{warehouse_id}", "statement": "SELECT * FROM catalog.schema.table LIMIT 10", "wait_timeout": "30s", "disposition": "INLINE", "format": "JSON_ARRAY" } ``` `warehouse_id` is extracted from the HTTP path: `/sql/1.0/warehouses/{warehouse_id}` Response (immediate if fast): ```json { "statement_id": "abc-123", "status": { "state": "SUCCEEDED" }, "manifest": { "schema": { "columns": [ { "name": "col1", "type_name": "STRING", "position": 0 } ] }, "total_row_count": 10 }, "result": { "data_array": [["value1"], ["value2"]] } } ``` ### Poll Statement (if not immediately complete) ``` GET https://{host}/api/2.0/sql/statements/{statement_id} Authorization: Bearer {token} ``` States: `PENDING` → `RUNNING` → `SUCCEEDED` | `FAILED` | `CANCELED` | `CLOSED` Poll strategy: 100ms → 200ms → 500ms → 1s → 2s (exponential backoff, cap at 2s) ### Cancel Statement ``` POST https://{host}/api/2.0/sql/statements/{statement_id}/cancel ``` ### Important Notes - `wait_timeout: "0s"` returns immediately with `PENDING` — useful for fire-and-forget - `wait_timeout: "30s"` waits up to 30s inline before returning — avoids polling for fast queries - `disposition: "INLINE"` returns data in response body (good for small results) - `disposition: "EXTERNAL_LINKS"` returns presigned URLs for large results (future: Arrow Flight) - Max statement size: 100KB - Max concurrent statements per warehouse: varies by warehouse size ## Authentication ### PAT (Personal Access Token) ``` Authorization: Bearer {DATABRICKS_TOKEN} ``` ### OAuth M2M (Service Principal) Token request: ``` POST https://{host}/oidc/v1/token Content-Type: application/x-www-form-urlencoded grant_type=client_credentials& client_id={DATABRICKS_CLIENT_ID}& client_secret={DATABRICKS_CLIENT_SECRET}& scope=all-apis ``` Response: ```json { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600 } ``` Implementation: - Cache the token - Refresh when `expires_in` is within 60s of expiry - Use the access_token as `Authorization: Bearer {access_token}` ### Auto-Detection Logic ``` if DATABRICKS_TOKEN is set and non-empty: use PAT auth else if DATABRICKS_CLIENT_ID and DATABRICKS_CLIENT_SECRET are set: use OAuth M2M else: error: no auth configured ``` ## Unity Catalog APIs ### Catalog Isolation ``` PATCH https://{host}/api/2.1/unity-catalog/catalogs/{catalog_name} Authorization: Bearer {token} Content-Type: application/json { "isolation_mode": "ISOLATED" } ``` ### Workspace Bindings Get current bindings: ``` GET https://{host}/api/2.1/unity-catalog/bindings/catalog/{catalog_name} ``` Response: ```json { "bindings": [ { "workspace_id": 12345, "binding_type": "BINDING_TYPE_READ_WRITE" } ] } ``` Update bindings (add/remove): ``` PATCH https://{host}/api/2.1/unity-catalog/bindings/catalog/{catalog_name} Content-Type: application/json { "add": [ { "workspace_id": 67890, "binding_type": "BINDING_TYPE_READ_WRITE" } ], "remove": [ { "workspace_id": 11111 } ] } ``` ## SQL Statements Rocky Must Generate ### Catalog Lifecycle ```sql -- Create CREATE CATALOG IF NOT EXISTS -- Tag (keys/values come from [governance.tags] in rocky.toml; 'managed_by' is always set) ALTER CATALOG SET TAGS ( 'managed_by' = '' -- plus any tags declared under [governance.tags] ) -- Inspect DESCRIBE CATALOG -- Returns rows: (info_name, info_value) — check for 'Catalog Name' row -- Discover managed catalogs SELECT catalog_name FROM system.information_schema.catalog_tags WHERE tag_name = 'managed_by' AND tag_value = '' ``` ### Schema Lifecycle ```sql -- Create CREATE SCHEMA IF NOT EXISTS . -- Tag (keys/values come from [governance.tags]; Rocky always sets 'managed_by') ALTER SCHEMA . SET TAGS ( 'layer' = 'raw', 'connector' = '', 'managed_by' = '' -- plus any tags declared under [governance.tags] ) -- List schemas SHOW SCHEMAS IN ``` ### Incremental Copy (Core Operation) Rocky reads the prior watermark (`MAX()` from the previous run) out of its redb state store and threads it into the generated SQL **as a literal** — it does **not** subquery the target table. See `rocky-core/src/sql_gen.rs`; a regression test asserts the generated SQL does not contain `SELECT COALESCE(MAX(...))`. ```sql -- Full refresh SELECT * FROM .. -- Incremental (append rows newer than the stored watermark) SELECT * FROM ..
WHERE _fivetran_synced > TIMESTAMP '' ``` After the copy, Rocky re-queries `MAX()` from the source and records it as the next watermark. ### Schema Drift Detection ```sql -- Get column info for comparison DESCRIBE TABLE ..
-- Returns rows: (col_name, data_type, comment) -- On a SAFE type widening (e.g. INT -> BIGINT): evolve in place ALTER TABLE ..
ALTER COLUMN TYPE -- On an UNSAFE type change: drop, then full refresh on next run DROP TABLE IF EXISTS ..
``` The safe-vs-unsafe decision is `drift.rs::is_safe_type_widening()`. ### Permission Reconciliation ```sql -- Inspect current grants SHOW GRANTS ON CATALOG -- Returns rows: (principal, action_type, object_type, object_name) SHOW GRANTS ON SCHEMA . -- Grant (principal always backtick-quoted) GRANT BROWSE ON CATALOG TO `` GRANT USE CATALOG ON CATALOG TO `` GRANT SELECT ON CATALOG TO `` GRANT USE SCHEMA ON SCHEMA . TO `` -- Revoke REVOKE BROWSE ON CATALOG FROM `` -- Supported permission types for reconciliation: -- BROWSE, USE CATALOG, USE SCHEMA, SELECT, MANAGE, MODIFY -- Skip these (non-managed): OWNERSHIP, ALL PRIVILEGES, CREATE SCHEMA ``` ### Data Quality Checks ```sql -- Single table row count SELECT COUNT(*) FROM ..
-- Batched row counts (UNION ALL, batches of 200) SELECT 'cat1' AS c, 'sch1' AS s, 'tbl1' AS t, COUNT(*) AS cnt FROM cat1.sch1.tbl1 UNION ALL SELECT 'cat1' AS c, 'sch1' AS s, 'tbl2' AS t, COUNT(*) AS cnt FROM cat1.sch1.tbl2 UNION ALL ... -- Column introspection (batched by schema) SELECT lower(table_schema), lower(table_name), lower(column_name) FROM .information_schema.columns WHERE table_schema IN ('schema1', 'schema2', ...) ORDER BY table_schema, table_name, ordinal_position ``` ## Validation Rules **SQL identifiers** (catalogs, schemas, tables): ``` ^[a-zA-Z0-9_]+$ ``` Reject anything that doesn't match. Never use `format!()` with unvalidated strings. **Principal names** (for GRANT/REVOKE): ``` ^[a-zA-Z0-9_ \-\.@]+$ ``` Always wrap in backticks: `` `principal_name` `` ## Error Handling Common Databricks errors to handle: - `TEMPORARILY_UNAVAILABLE` (503) — Retry with exponential backoff - `INVALID_PARAMETER_VALUE` — Bad SQL or missing object - `RESOURCE_DOES_NOT_EXIST` — Table/catalog/schema not found - `PERMISSION_DENIED` — Missing privileges - `InvalidOperationHandle` — Statement expired, re-submit - Rate limiting — Warehouse concurrency limit reached, back off Retry strategy: 3 attempts, exponential backoff (1s → 3s → 9s), only on transient errors (503, rate limit, InvalidOperationHandle).