Skip to content

#224: Databricks deterministic sampling + materialise_sample + get_row_count - #257

Merged
wjduenow merged 16 commits into
devfrom
feature/224-databricks-sampling
Jun 25, 2026
Merged

#224: Databricks deterministic sampling + materialise_sample + get_row_count#257
wjduenow merged 16 commits into
devfrom
feature/224-databricks-sampling

Conversation

@wjduenow

@wjduenow wjduenow commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Summary

Super plan for #224 — Databricks adapter sampling surface (sample_rows, get_row_count, run_test_sql, materialise_sample, column_stats). Epic #219; follows #221/#222/#223; models on Snowflake #122/#139/#140.

Phase: complete (implemented + reviewed; live cert deferred to #226)
Stories: 5 implementation + Quality Gate + Patterns & Memory
Decisions: 12 (DEC-001 … DEC-012)

Key decisions

  • materialise_sample → true materialisation via qualified CREATE TEMPORARY TABLE mirroring Snowflake feat: Snowflake deterministic sampling (sample_rows HASH-mod) + materialise_sample (TEMP TABLE) #122 (reuses TableRef+compiler path, zero model churn). Qualified-temp validity + connector session persistence are Databricks: test harness + gated live e2e + ops docs #226 live-cert blockers.
  • get_row_countSELECT COUNT(*) (the ticket's DESCRIBE DETAIL → numRows column does not exist; numRows lives in the statistics map only after ANALYZE).
  • TableRef.project → relaxed to accept SQL identifiers (unblocks short Unity Catalog catalogs like main; fixes latent short-Snowflake-DB case).
  • Scope → also implements column_stats (ahead of Snowflake); follow-up issue to decide Snowflake parity.
  • Sampling → inline-predicate shape (Databricks has no HASH(*) predicate restriction); no DATABRICKS_DIALECT change.

Plan document

See plans/super/224-databricks-sampling.md.

Next steps

  • Review the plan in this PR
  • Approve in Claude Code to proceed to devolve (beads creation)

Implementation landed (Ralph epic bd_1-scaffolding-v4o). All 5 stories + Quality Gate + Patterns & Memory merged; full suite green (4383 passed). Shipped: sample_rows / get_row_count (COUNT(*) sizing) / materialise_sample (qualified CREATE TEMPORARY TABLE) / run_test_sql / column_stats, validate_catalog_or_project for Unity Catalog catalogs, fakes + ungated sqlglot databricks parse-guard. Live validity (qualified-temp acceptance, connector session persistence, to_json capture marshalling, column_stats complex-type MIN/MAX) is the #226 live-cert ledger. Snowflake column_stats parity → #258.

Summary by CodeRabbit

  • New Features

    • Added full Databricks support for deterministic sampling, row-count sizing, temporary sample materialization, test-query execution (with optional failure capture), and column profiling.
    • Updated table/project handling to accept either catalog-style identifiers or GCP-style project identifiers (with stricter injection rejection).
  • Bug Fixes

    • Expanded Databricks exception mapping into clearer error categories (auth, not found, unresolved columns, SQL/syntax issues).
    • Strengthened adapter session cleanup with safer correlation logging and more reliable cleanup behavior.
  • Tests

    • Added/extended offline Databricks adapter tests, SQL dialect parse-guards, and validation coverage for identifier safety and Databricks sampling SQL.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7668e6a2-cd8c-4489-abaa-642e90d577bb

📥 Commits

Reviewing files that changed from the base of the PR and between 535993d and 377da1d.

📒 Files selected for processing (5)
  • .claude/rules/warehouse-adapters.md
  • plans/super/224-databricks-sampling.md
  • src/signalforge/warehouse/adapters/databricks.py
  • tests/warehouse/_fake_databricks.py
  • tests/warehouse/test_databricks_adapter.py
✅ Files skipped from review due to trivial changes (2)
  • plans/super/224-databricks-sampling.md
  • .claude/rules/warehouse-adapters.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/warehouse/_fake_databricks.py
  • tests/warehouse/test_databricks_adapter.py
  • src/signalforge/warehouse/adapters/databricks.py

📝 Walkthrough

Walkthrough

This change relaxes TableRef.project validation, implements Databricks exception mapping and sampling/materialisation/test-SQL/column-stats flows, expands adapter test coverage, and updates the related docs and sampling plan.

Changes

Databricks adapter validation and sampling

Layer / File(s) Summary
Project validation contract
src/signalforge/warehouse/_sql_safety.py, src/signalforge/warehouse/models.py, tests/warehouse/test_sql_safety.py, tests/warehouse/test_models.py, .claude/rules/warehouse-adapters.md
validate_catalog_or_project accepts identifier-shaped or project-id-shaped values, TableRef.project uses it, and tests cover accepted and rejected project strings.
Session cleanup and error mapping
src/signalforge/warehouse/adapters/_databricks_client.py, tests/warehouse/_fake_databricks.py, tests/warehouse/test_databricks_adapter.py, tests/warehouse/test_databricks_stub.py
Databricks connector errors map to warehouse errors, connection cleanup reads session ids defensively, and the fake connection plus tests cover queued SQL, cleanup logging, and the remaining degrade-method stub contract.
Sampling and SQL execution
src/signalforge/warehouse/adapters/databricks.py
The Databricks adapter now renders sample SQL, materialises temp tables, computes row counts, aggregates column stats, and runs test SQL with captured failures.
Adapter SQL-shape tests
tests/warehouse/test_databricks_adapter.py, tests/warehouse/test_databricks_sql_parse.py
The adapter tests and parse-guard tests assert emitted Databricks SQL, result shaping, and failure handling for sampling, materialisation, test SQL, and column stats.
Documentation and plan
.claude/rules/warehouse-adapters.md, docs/warehouse-adapter-ops.md, plans/super/224-databricks-sampling.md
The Databricks rules, adapter ops docs, and new sampling plan describe the implemented surface and the remaining deferred methods.

Sequence Diagram(s)

sequenceDiagram
  participant DatabricksAdapter
  participant FakeDatabricksConnection
  participant map_databricks_exception
  DatabricksAdapter->>FakeDatabricksConnection: execute COUNT(*), sample SQL, or CTAS SQL
  FakeDatabricksConnection-->>DatabricksAdapter: rows or exception
  DatabricksAdapter->>map_databricks_exception: classify connector failure
  map_databricks_exception-->>DatabricksAdapter: WarehouseError subtype
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • Issue 227: The changes overlap with the Databricks follow-up work for column_stats and materialise_sample.
  • Issue 226: The fake connection, exception taxonomy, parse-guard, and ops-doc updates align closely with the Databricks test harness work.
  • Issue 219: The Databricks sampling, materialisation, and row-count work aligns with the broader Databricks adapter workstream.
  • Issue 224: This PR implements the deterministic sampling, materialise_sample, get_row_count, and run_test_sql work described there.

Possibly related PRs

  • wjduenow/SignalForge#30: The main PR implements Databricks deterministic sampling and temp-table materialisation, matching the earlier materialise_sample seam work.
  • wjduenow/SignalForge#143: This PR adds the concrete get_row_count implementation used by the shared row-count seam.
  • wjduenow/SignalForge#254: Both PRs modify the Databricks adapter stack, and this one extends it into deterministic sampling and profiling logic.

Poem

A bunny typed with twitchy paws,
and SQL sprang forth without a pause.
The temp tables hopped, the counts came true,
and grumpy errors lost their chew.
Hop, hop—Databricks, you’re tidy now! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.66% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the core Databricks sampling, materialise_sample, and get_row_count work and is specific and concise.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Comment @coderabbitai help to get the list of available commands.

wjduenow added 7 commits June 25, 2026 12:22
…g catalog names

Add validate_catalog_or_project to _sql_safety.py composing the existing
validate_identifier and validate_project_id, and route TableRef.__post_init__'s
project check through it (DEC-005 of #224). Short Unity Catalog catalogs (main),
default catalogs (workspace), and short Snowflake databases now construct, while
injection-shaped project values still raise InvalidIdentifierError. validate_project_id
is unchanged; TableRef stays dialect-neutral (no new field).
… cleanup + error mapping

#224 US-002 — the Databricks analogue of the Snowflake #122/#124 connection /
cleanup / error-mapping work.

- Add tests/warehouse/_fake_databricks.py (FakeDatabricksConnection) mirroring
  the Snowflake fake: injectable session_id, close_raises, a cursor with
  execute/fetchall/description/close, expect_execute/assert_all_expectations_met,
  and close_call_count.
- DatabricksAdapter._cleanup_active_session now reads the connection's opaque
  session id (defensive: session_id attr first, get_session_id_hex() fallback),
  emits the raw id ONLY in the swallow-and-warn failure WARNING (no manual
  cleanup command, no auto-expire countdown — temp objects are session-local),
  and the hashed id (_hash_session_id) in the success INFO. State resets in
  finally for idempotency; _connection is never nulled so a second __exit__ is a
  clean no-op.
- Extend map_databricks_exception to the full taxonomy mirroring
  map_snowflake_exception: auth -> WarehouseAuthError; table-not-found ->
  TableNotFoundError; unresolved-column -> ColumnNotFoundError; residual Spark/SQL
  error -> QuerySyntaxError; else passthrough. The Table/Column/Syntax split is
  scoped to ServerOperationError/ProgrammingError so a transient OperationalError
  is not mis-mapped. The databricks.sql.exc import stays lazy + confined to the
  shim. No new WarehouseError subclass.
- tests/warehouse/test_databricks_adapter.py covers the fake, the connection
  seam, fail-soft cleanup (raw-id WARNING / hashed-id INFO / idempotent second
  exit), repr redaction, and the error-mapper taxonomy (built from the
  connector's own databricks.sql.exc classes).
…ws (#224 US-003)

Implement the first real DatabricksAdapter warehouse I/O, mirroring the
Snowflake adapter (DEC-002 / DEC-003 / DEC-008 of #224):

- _quote / _fold / _quote_identifier: fold-then-quote per component using
  DATABRICKS_DIALECT (identifier_case='lower' + backtick,
  quote_qualified_per_component=True), identical folding to the prune
  compiler so CREATE-vs-REFERENCE never diverge (the #124 lesson).
- _execute / _execute_to_dicts / _rows_to_dicts: connection-bound cursor
  execution, error mapping via map_databricks_exception, dict-row shaping
  via cursor.description (no dict-cursor dependency).
- get_row_count: SELECT COUNT(*) (DESCRIBE DETAIL has no reliable numRows;
  COUNT is metadata-cheap on Delta) -> int, WarehouseError -> None;
  overrides the ABC RowCountNotSupportedError degrade.
- _resolve_sample_bucket: shared fail-loud sizing (UnknownTableSizeError /
  SamplingRequiresPartitionFilterError / bucket=1000 fallback / bucket math).
- sample_rows: n<=0 ValueError; inline-predicate shape via
  render_sample_select(order_by_hash=True) (Databricks has no Snowflake-style
  HASH(*) predicate restriction; sample_hash_in_projection=False); hash
  expression read from the dialect, never hard-coded.
- _render_partition_filter: datetime/date/str via dialect literal templates +
  escape, column fold-then-quoted.

Tests: extend test_databricks_adapter.py (determinism, all three sizing
branches, get_row_count happy/dict/error->None/empty/passthrough, dict-row
shaping, partition-filter rendering, project=None two-part quoting, SDK
error mapping); new ungated sqlglot databricks-dialect parse-guard
(test_databricks_sql_parse.py) over every emitted statement plus a
planted-violation self-check. Update the two now-stale stub tests in
test_databricks_stub.py (sample_rows + get_row_count graduated).
@wjduenow
wjduenow requested a review from Copilot June 25, 2026 20:05
@wjduenow
wjduenow marked this pull request as ready for review June 25, 2026 20:05
wjduenow added 2 commits June 25, 2026 13:06
…224 US-004)

Implements the materialise + test-execution surface on DatabricksAdapter,
mirroring the Snowflake adapter and reusing the US-003 helpers.

- materialise_sample: overrides the ABC MaterialisationNotSupportedError
  degrade with a qualified `CREATE TEMPORARY TABLE <cat>.<sch>._sf_sample_<run_id>
  AS <inline-predicate sample body>`. run_id via the shared _compute_run_id
  recipe (byte-identical to BigQuery/Snowflake); sample body via the shared
  render_sample_select (inline shape, sample_hash_in_projection=False). Returns
  the qualified temp TableRef; pins the connection so a follow-up run_test_sql
  reaches the session-scoped temp table. Cursor errors route through
  map_databricks_exception then wrap in MaterialisationFailedError(cause=...);
  one INFO log with the hashed session id + table + sample_rows + run_id.
- run_test_sql: overrides the NotImplementedError stub. Count via
  `SELECT COUNT(*) AS failures FROM (<sql>) AS t`; capture via a second
  per-row `to_json(struct(*)) ... LIMIT k` query, json.loads-ing each JSON
  string individually (not a single outer decode like Snowflake's VARIANT).
  Case-insensitive column resolution via cursor.description.
- Extends the ungated sqlglot databricks parse-guard over the emitted CTAS +
  COUNT wrap + to_json capture SQL; adds the #116 custom_sql {{ this }}
  substitution test (source name -> quoted temp ref, no full-scan leakage).
- Retires the two now-obsolete stub assertions in test_databricks_stub.py.

#226 live-cert caveats (shape-only here, noted in test docstrings): qualified
temp-table-name acceptance, connector session persistence across queries, and
the per-row to_json capture marshalling.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a “super plan” document for issue #224, outlining the intended Databricks adapter implementation for deterministic sampling, sample materialisation, and row-count sizing, including decisions (DEC-001…DEC-012) and a story-by-story breakdown to drive beads task creation and later implementation PRs.

Changes:

  • Introduces a detailed plan for implementing sample_rows, get_row_count, materialise_sample, run_test_sql, and column_stats for the Databricks adapter.
  • Records key architectural decisions and testing strategy (fakes + sqlglot parse guards + gated live cert in #226).
  • Documents a beads task breakdown and links the Snowflake column_stats parity follow-up.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plans/super/224-databricks-sampling.md Outdated
Comment thread plans/super/224-databricks-sampling.md Outdated
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":500,"request":{"method":"PATCH","url":"https://api.github.com/repos/wjduenow/SignalForge/issues/comments/4803375313","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- review_stack_entry_start -->\n\n[![Review Change Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/wjduenow/SignalForge/pull/257?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)\n\n<!-- review_stack_entry_end -->\n<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->\n\n> [!NOTE]\n> Currently processing new changes in this PR. This may take a few minutes, please wait...\n> \n> <details>\n> <summary>⚙️ Run configuration</summary>\n> \n> **Configuration used**: Organization UI\n> \n> **Review profile**: CHILL\n> \n> **Plan**: Pro\n> \n> **Run ID**: `45afa783-366d-4689-8d8b-bd12f38e5245`\n> \n> </details>\n> \n> <details>\n> <summary>📥 Commits</summary>\n> \n> Reviewing files that changed from the base of the PR and between eeaa77ad5d8589fcbf8a8712c3740e24382b3a45 and 3c6a41a409944ffdabdce1c0bff2f9bfbf54af52.\n> \n> </details>\n> \n> <details>\n> <summary>📒 Files selected for processing (1)</summary>\n> \n> * `plans/super/224-databricks-sampling.md`\n> \n> </details>\n> \n> ```ascii\n>  _____________________________________________________________________________________________________\n> < Care about your craft. Why spend your life developing software unless you care about doing it well? >\n>  -----------------------------------------------------------------------------------------------------\n>   \\\n>    \\   (\\__/)\n>        (•ㅅ•)\n>        /   づ\n> ```\n\n<!-- end of auto-generated comment: review in progress by coderabbit.ai -->\n\n<!-- tips_start -->\n\n---\n\n\n\n\n<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>\n\n<!-- tips_end -->"},"request":{"retryCount":3,"signal":{},"retries":3,"retryAfter":16}}}

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

wjduenow added 4 commits June 25, 2026 13:18
…224 US-005)

Implement DatabricksAdapter.column_stats, overriding the v0.x
NotImplementedError stub. Databricks ships aggregate-only profiling AHEAD
of Snowflake (which stubs it); the Snowflake-parity decision is tracked as
GitHub issue #258.

A single aggregate query over the fold-then-quoted table computes the
ColumnStats contract for the fold-then-quoted column, mirroring
BigQueryAdapter.column_stats semantics in Spark SQL:
- count   = COUNT(<col>)            (non-null count)
- distinct= COUNT(DISTINCT <col>)
- nulls   = COUNT_IF(<col> IS NULL)
- min/max = MIN(<col>) / MAX(<col>)
- data_type = MAX(typeof(<col>))    (NULL on empty table -> "")

Reuses the US-003 helpers (_quote / _quote_identifier / _execute_to_dicts);
no context-manager batching (single per-call round-trip, mirroring
sample_rows / run_test_sql / get_row_count). Errors route through
map_databricks_exception via _execute_to_dicts; the column is validated by
validate_identifier before reaching SQL (DEC-013).

Tests: ColumnStats shape against FakeDatabricksConnection (every field),
identifier fold-to-lower, case-insensitive alias resolution, empty-table
data_type coercion, two-part quoting, error mapping, identifier validation.
Adds column_stats coverage to the ungated sqlglot databricks parse-guard.
Removes the obsolete column_stats NotImplementedError stub test.

Traces to DEC-001, DEC-011 (plans/super/224-databricks-sampling.md).
… fix stale skeleton docstrings, document column_stats complex-type MIN/MAX divergence (#226), add partition-filter run_id test
…on in warehouse-adapters.md, ops-doc sampling surface + #226 live-cert ledger
@wjduenow wjduenow changed the title #224: Databricks deterministic sampling + materialise_sample + get_row_count (plan) #224: Databricks deterministic sampling + materialise_sample + get_row_count Jun 25, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/signalforge/warehouse/models.py (1)

391-396: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale class docstring vs. relaxed contract.

The docstring still reads "Fully-qualified BigQuery table identity" and describes project purely as a BigQuery default-project deferral, but the validation is now dialect-neutral (BigQuery project / Snowflake database / Unity Catalog catalog). Consider refreshing the class docstring so the public contract matches the relaxed validate_catalog_or_project behavior introduced below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/signalforge/warehouse/models.py` around lines 391 - 396, Update the class
docstring for the table identity model so it matches the relaxed,
dialect-neutral contract enforced by validate_catalog_or_project. Replace the
BigQuery-only wording with language that covers the supported catalog/project
behavior across backends, and adjust the description of the optional top-level
identifier so it no longer implies only BigQuery default-project deferral.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/signalforge/warehouse/adapters/databricks.py`:
- Around line 385-393: The cursor handling in Databricks query helpers is
leaking resources because `_execute`, `_execute_to_dicts`, and
`materialise_sample` open cursors via `self._get_connection().cursor()` without
guaranteeing cleanup. Update each of these methods to wrap cursor use in a
try/finally block so the cursor is always closed after `cursor.execute(...)`,
`fetchall()`, or exception mapping, and keep the existing exception behavior in
`map_databricks_exception` intact while ensuring release on both success and
failure.

---

Nitpick comments:
In `@src/signalforge/warehouse/models.py`:
- Around line 391-396: Update the class docstring for the table identity model
so it matches the relaxed, dialect-neutral contract enforced by
validate_catalog_or_project. Replace the BigQuery-only wording with language
that covers the supported catalog/project behavior across backends, and adjust
the description of the optional top-level identifier so it no longer implies
only BigQuery default-project deferral.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7ff16ede-adf5-45ba-8408-8569597814ce

📥 Commits

Reviewing files that changed from the base of the PR and between eeaa77a and 535993d.

📒 Files selected for processing (13)
  • .claude/rules/warehouse-adapters.md
  • docs/warehouse-adapter-ops.md
  • plans/super/224-databricks-sampling.md
  • src/signalforge/warehouse/_sql_safety.py
  • src/signalforge/warehouse/adapters/_databricks_client.py
  • src/signalforge/warehouse/adapters/databricks.py
  • src/signalforge/warehouse/models.py
  • tests/warehouse/_fake_databricks.py
  • tests/warehouse/test_databricks_adapter.py
  • tests/warehouse/test_databricks_sql_parse.py
  • tests/warehouse/test_databricks_stub.py
  • tests/warehouse/test_models.py
  • tests/warehouse/test_sql_safety.py

Comment thread src/signalforge/warehouse/adapters/databricks.py
…CodeRabbit), reconcile plan phase to complete
@wjduenow

Copy link
Copy Markdown
Owner Author

PR Review Summary

Fixed (1 item)

File Issue Commit
src/signalforge/warehouse/adapters/databricks.py (CodeRabbit, Major) _execute / _execute_to_dicts / materialise_sample opened a cursor via conn.cursor() without closing it → server-side cursor-handle leak on repeated queries. Wrapped each in try/finally cursor cleanup (closing on both success and failure; _execute_to_dicts closes only after _rows_to_dicts reads cursor.description). Added 4 regression tests asserting cursors close on success, on failure, after row-shaping, and after the materialise CTAS. 81b18dc

Reconciled (2 items)

File Issue Resolution
plans/super/224-databricks-sampling.md (Copilot ×2) Plan phase (devolved) conflicted with the PR description's phase (detailing (awaiting approval)). Implementation has landed and been reviewed, so both the plan metadata and the PR description now read complete.

All 7 stories merged; full suite green (4387 passed); pyright 0 errors. No deferrals.

@wjduenow
wjduenow merged commit 7d1c9a1 into dev Jun 25, 2026
7 checks passed
@wjduenow
wjduenow deleted the feature/224-databricks-sampling branch June 25, 2026 21:49
wjduenow added a commit that referenced this pull request Jul 2, 2026
…_execute_to_dicts/run_test_sql)

Wrap the execute/fetch in try/finally cursor.close() for the three
cursor-opening methods that previously leaked server-side handles, mirroring
_execute_scalar (the one already-correct path) and the Databricks PR #257 shape.

_execute_to_dicts uses an OUTER try/finally (close) wrapping an INNER try/except
(exception mapping) so the cursor closes AFTER _rows_to_dicts reads
cursor.description. run_test_sql likewise closes only after description + rows
are captured into locals.

TDD: extend FakeSnowflakeConnection to track vended cursors (.cursors) and
expose per-cursor .closed, then six new tests in tests/warehouse/
test_snowflake_adapter.py assert closed on both success and failure paths.

Traces to DEC-007 of plans/super/258-snowflake-column-stats.md.
wjduenow added a commit that referenced this pull request Jul 3, 2026
* Add super plan for #258: Snowflake column_stats parity

* Mark #258 plan published (PR #262)

* Devolve #258 plan to beads (epic bd_1-scaffolding-om0)

* bd_1-scaffolding-om0.1: fix Snowflake adapter cursor leaks (_execute/_execute_to_dicts/run_test_sql)

Wrap the execute/fetch in try/finally cursor.close() for the three
cursor-opening methods that previously leaked server-side handles, mirroring
_execute_scalar (the one already-correct path) and the Databricks PR #257 shape.

_execute_to_dicts uses an OUTER try/finally (close) wrapping an INNER try/except
(exception mapping) so the cursor closes AFTER _rows_to_dicts reads
cursor.description. run_test_sql likewise closes only after description + rows
are captured into locals.

TDD: extend FakeSnowflakeConnection to track vended cursors (.cursors) and
expose per-cursor .closed, then six new tests in tests/warehouse/
test_snowflake_adapter.py assert closed on both success and failure paths.

Traces to DEC-007 of plans/super/258-snowflake-column-stats.md.

* bd_1-scaffolding-om0.2: implement Snowflake column_stats (full-batch, catalog pre-filter)

Replace the NotImplementedError stub with a BigQuery-style context-manager
batched column_stats (issue #258 US-002; DEC-001..DEC-006):

- __enter__/__exit__ open/reset per-table pending + results caches, coexisting
  with the connection-session state.
- column_stats: validate identifier -> DEC-025 with-block guard -> cache hit ->
  queue + flush.
- _flush_column_stats_batch: one INFORMATION_SCHEMA.COLUMNS catalog lookup
  (mirroring _get_num_rows escaping) for the data_type field + MIN/MAX skip
  decision, then ONE aggregate for every queued column (index-suffixed aliases;
  COUNT(*)-COUNT null count per DEC-005; MIN/MAX gated per DEC-003).
- _get_column_types keyed by lower(COLUMN_NAME); a requested column absent from
  the catalog raises ColumnNotFoundError before the aggregate.
- _COMPLEX_SNOWFLAKE_TYPES {ARRAY,OBJECT,VARIANT,GEOGRAPHY,GEOMETRY} +
  _is_complex_snowflake_type (BINARY orderable); Decimal->float min/max (DEC-004).

Full default-suite unit coverage via the hand-fake (no drift detector -
ColumnStats is frozen/in-process). Drops the obsolete stub NotImplementedError
test.

* bd_1-scaffolding-om0.5: flip Snowflake column_stats parity docs (ops/README/CHANGELOG)

* bd_1-scaffolding-om0.4: gated live cert for Snowflake column_stats (scalar + complex skip-set + aggregate-only smoke)

* bd_1-scaffolding-om0.3: fakesnow offline execution tests for Snowflake column_stats

Add five fakesnow-executed offline tests driving the real SnowflakeAdapter
column_stats path end-to-end against DuckDB (US-003, DEC-008):

- scalar NUMBER column: COUNT / COUNT(DISTINCT) / COUNT(*)-COUNT(col) null
  count / MIN / MAX execute; Decimal min/max coerced to float (DEC-004)
- INFORMATION_SCHEMA.COLUMNS catalog lookup (_get_column_types) round-trip —
  fakesnow returns consumable Snowflake DATA_TYPE strings, so this is a full
  EXECUTION assertion (no parse-only degrade needed)
- VARCHAR string MIN/MAX bounds pass through unchanged
- VARIANT complex-type MIN/MAX skip decision (DEC-003) executes; live cert
  owns the analysis-time-rejection validation (#227 lesson)
- all-NULL scalar column: null-count arithmetic + empty MIN/MAX None path

Document in the module docstring that column_stats is fully executable under
fakesnow (no parse-only degrade) and that ColumnStats needs NO drift detector
(frozen, in-process, never read back from disk).

* bd_1-scaffolding-om0.6: Quality gate — coerce out-of-union min/max (BINARY/TIME), doc GEOGRAPHY DISTINCT limit

QG review (2 independent lenses) found BINARY (bytearray) and TIME (datetime.time)
column MIN/MAX return types are outside ColumnStats.min/max's union, raising a
non-WarehouseError ValidationError that fails the whole batch at the panic tier.
Fix: _coerce_min_max nulls any out-of-union value (return-type analogue of the
skip-set). Add BINARY/TIME coercion tests (offline + gated live columns). Document
the pre-existing GEOGRAPHY/GEOMETRY COUNT(DISTINCT) limitation.

* bd_1-scaffolding-om0.7: Patterns & Memory — flip Snowflake column_stats to shipped (#258)

Update warehouse-adapters.md: flip the 'column_stats stays NotImplementedError' /
'parity is the open follow-up #258' statements to shipped; add the § 'Snowflake
column_stats (issue #258)' subsection capturing the two-tier MIN/MAX defence
(SQL-raise skip-set vs out-of-union return-type coercion net), the GEOGRAPHY
DISTINCT limitation, and the deferred live cert.

* #258: Address PR review feedback (CodeRabbit + Copilot)

- CRITICAL (CodeRabbit): drain column_stats pending batch UP FRONT so a failed
  column (ColumnNotFoundError / GEOGRAPHY COUNT(DISTINCT)) no longer poisons
  every subsequent column of the same table within the with-block. + regression test.
- Live test: move CREATE/INSERT inside the outer try/finally (no orphaned table on setup failure).
- Docs/README/CHANGELOG: qualify aggregate-only with the GEOGRAPHY/GEOMETRY
  COUNT(DISTINCT) limitation; soften 'certified'/'confirmed' to maintainer-run gated cert.
- MD018: reflow lines so #258 is not at a wrapped-line start (ops doc + README).
- warehouse-adapters.md: update the stale Snowflake cursor-leak note (only materialise_sample leaks now).

* #258: wire key-pair (JWT) auth through make_real_client (enables live cert on MFA accounts)

The #120 profile model + from_profile + SnowflakeAdapter.__init__ already parsed
and stored private_key_path / private_key_passphrase / authenticator, but
make_real_client dropped them at the connect() call (they were never used). Thread
them through: key-pair auth when private_key_path is set (private_key_file [+ _pwd]),
else password; authenticator passed through when set. This is the non-interactive
path MFA-enforced accounts require. The gated live cert (_make_adapter + _skip_reason)
now accepts key-pair OR password auth.

* #258: flip column_stats live cert to live-certified (passed via key-pair auth)

Ran the gated column_stats live cert against a real Snowflake warehouse (key-pair
/ JWT auth — the restored account enforces MFA, which the headless password path
can't satisfy). Result: scalar min/max populate; ARRAY/OBJECT/VARIANT return
min=max=None without raising (DEC-003 skip-set confirmed complete); BINARY/TIME
coerce to None. Since the cert profiles ARRAY/OBJECT/VARIANT (emitting
COUNT(DISTINCT) on them) and passed, COUNT(DISTINCT) on semi-structured types
does NOT raise — only GEOGRAPHY/GEOMETRY remain the documented limit. Docs/rules
updated from 'pending a maintainer run' to 'live-certified'.

* #258: fix Snowflake connection reuse across with-blocks + wire key-pair into e2e smoke

Exposed by the aggregate-only e2e: generate with safety=aggregate-only uses one
adapter across two 'with adapter:' blocks (safety-aggregate column_stats, then
prune_tests). Snowflake's __exit__ closes the connection (the connection IS the
session) but #122 didn't null self._connection, so the 2nd block reused the
closed connection -> 250002 (08003): Connection is closed. Fix: track
self._owns_connection (True only on lazy build); cleanup nulls _connection after
close only when owned, so the next block rebuilds; injected fakes stay intact.
+ 2 lifecycle regression tests. Also wire key-pair (JWT) auth into the e2e smoke
profiles.yml (SNOWFLAKE_PRIVATE_KEY_PATH), so the aggregate-only generate e2e is
now live-certified against a real MFA-enforced Snowflake account.
@coderabbitai coderabbitai Bot mentioned this pull request Jul 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants