Skip to content

feat: reject empty-input-only SQL tests and fix SQL test helper scope - #803

Merged
kvlonge merged 12 commits into
mainfrom
fix/sql-test-helpers-cursor-window
Sep 24, 2026
Merged

kvlonge merged 12 commits into
mainfrom
fix/sql-test-helpers-cursor-window

Conversation

@kvlonge

@kvlonge kvlonge commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Why

SQL unit tests could not use helper CTEs in assertions or expected rows, and models using cursor intrinsics could not be tested. Compile hid SQL test planning errors whenever a rule failed. Audit lifecycle events arrived in one end-of-run burst, so slow sinks silently lost them. Empty-input-only filler tests counted as coverage, and dupscore did not point at concrete duplicated code.

Changes

  • SQL tests: helper CTEs are scoped into __expected__/__assert__ SQL, including mocks reached through helpers; cursor intrinsics render a wide typed window, overridable per test with TEST(cursor_start …, cursor_end …).
  • Compile reports SQL test planning errors alongside rule errors.
  • audit_completed is published as audits finish; new sinks.lifecycle.shutdown_timeout; stderr warning when lifecycle deliveries are dropped or fail (exit code unchanged).
  • New SQBRTEST203 empty-input-only-test rule with allowed_tests; SQBRTEST202 no longer counts such tests.
  • dupscore clones: function-level clone detection for Python and Rust, with --since.
  • Shared SQL-test scanning/scope helpers, marker replacement, cursor-bound rendering and fixed-duration config parsing.

Upgrade note: projects relying on empty-input filler tests to meet min_tests_per_model will see SQBRTEST202/SQBRTEST203 errors; replace the filler or list reviewed cases in allowed_tests.

Verification

  • DuckDB E2E through real sqb compile/sqb test/sqb audit/sqb build for helper scope, cursor windows, rule-gated diagnostics, incremental audit events, sink drain and warning, and SQBRTEST202/203.
  • Rust and Python unit tests; make test; make check-ci.
  • Compile performance and fingerprint suites; large-project compile time unchanged.

Helper CTEs are now in scope for __expected__ and __assert__ SQL in both
the SQL-analysis and textual planner paths, for comparison and difference
sample queries. Referenced helpers, the helpers they read, and mocks they
read by CTE name are emitted once at top level in dependency order.

Models using __cursor_start()/__cursor_end() are rendered for tests with
a wide typed window (1900-01-01 to 2999-12-31, or +/-1e15 for integer
cursors) using the adapter's execution bound literal. TEST headers accept
cursor_start (inclusive) and cursor_end (exclusive), validated against
each intrinsic-using chain model's cursor_type and cursor_grain.
When rule errors withhold compile artifacts, still report ERROR-severity SQL test planning diagnostics in text and JSON output. The background prepared-artifacts path reuses its staged planning result without publishing; the inline path plans uncached tests without writing artifacts or cache records. Planning input errors become diagnostics so rule errors are not lost.
audit_completed events were published from the end-of-run audit batch
projection, so a run emitted every audit event in one burst after the last
audit finished. A slow lifecycle sink then lost the tail at the fixed
two-second exit drain, and nothing on the console reported the loss.

- Publish each audit_completed once as its outcome is confirmed: per audit
  for sqb audit (serial and concurrent), source audits and end audits, and
  per model result for model audits in build. Payloads, result ids,
  envelope correlation and persisted audit-result rows are unchanged; the
  batch projection reuses the already published records and still writes
  history once.
- Add sinks.lifecycle.shutdown_timeout (fixed duration "0s".."10m",
  default "2s") to configure the exit drain for lifecycle sinks. The
  per-invocation timeout and queue priorities are unchanged.
- Print one stderr warning when the final export summary reports dropped
  or failed lifecycle deliveries, stating when the command itself
  succeeded. Exit codes and machine-readable stdout are unchanged.
Helper scope is now the transitive closure over helper and mock CTEs, where a mock body's references to helpers or other mocks count as dependencies. Generated mocks in the analysis path and inlined mocks in the textual path bring the mocks they read into top-level scope once, in dependency order, and those mocks count as reachable. Marker replacement helpers move to their own module.
Share quote- and comment-aware scanning (sql_scan) between SQL-test extraction and marker resolution, and CTE assembly (cte_sql) between planning and rendering. Every mock CTE body now comes from the test's ScopeGraph in analysis generation, textual inlining and helper scope; helper injection into mock and direct-test bodies uses one function. Test and execution cursor intrinsics share one adapter bound renderer, and the compile writer shares planning diagnostic conversion with the rules-failed path. Compiled test SQL is unchanged.
Add SQBRTEST203 (empty-input-only-test), enabled by default, which flags
model-mode SQL tests whose mocks are all empty and whose expected outputs
and assertions can only prove that no rows are produced. Reviewed
exceptions live in the typed allowed_tests rule option, and stale entries
are reported.

The native engine classifies each test once; SQBRTEST203 and the
SQBRTEST202 min_tests_per_model count both read that result, so filler
never satisfies the minimum whether or not SQBRTEST203 is selected.
Allowlisted tests still count.
Add a default `clones` mode that extracts Python functions and methods and
Rust fn items, normalises their token streams, finds candidates with
winnowed k-gram fingerprints, scores them with a token-sequence ratio, and
reports exact, renamed, and near-miss clusters with path:start-end members.

Supports --since (clusters touching lines changed since a revision,
including uncommitted and untracked files), --lang, --path, --min-tokens,
--min-similarity, --include-tests, --json, and a [[clone_allowlist]] with
required reasons. `make dupscore` now runs the clone mode; `report` and
`pair` are unchanged. Helpers are regrouped into package_pairs, clones, and
inputs buckets to keep module fan-out within fensu limits.
An assertion whose projection or ORDER BY calls a function (for example an
UNNEST over an aggregate check) can verify concrete output, and a WHERE FALSE
fixture that calls a function anywhere (for example ORDER BY COUNT(*)) can
still produce a row. Treat both as not empty-input-only.
@kvlonge
kvlonge enabled auto-merge (squash) September 24, 2026 10:18
@kvlonge
kvlonge merged commit b1410f6 into main Sep 24, 2026
28 checks passed
@kvlonge
kvlonge deleted the fix/sql-test-helpers-cursor-window branch September 24, 2026 10:31
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.

1 participant