Skip to content

Dynamic SQL Necessity Audit

Decision record

Overall status: done as an evidence-bounded audit; no product prototype was adopted.

The question is not whether a library exposes a dynamic API. It is which business requirement needs runtime SQL syntax at all, and what the least powerful adequate mechanism is: value binding, subtraction, finite closed-world construction, separate canonical SQL, or an open-ended builder. This audit actively includes cases where an open-ended builder is the better fit. It is not a prevalence survey and does not claim a universal winner.

Baseline: c116761957b6b6ac89e7f7db49961bef87940822 (PR #52 merge). The isolated worktree was clean before audit artifacts. The first pnpm verify attempt was blocked by the worktree dependency-preparation permission; after a lockfile install, the recorded full pnpm verify exit code was 0. Focused package and live PostgreSQL evidence is listed below; the documentation build also passed after the report change.

Evidence discipline

  • Observed is checked-in source/test evidence or an executed isolated probe. It is not a claim about frequency across all applications.
  • Inference is a bounded design conclusion from observed behavior.
  • Hypothesis is a candidate needing a matched application or Fresh-Agent experiment.
  • Repository evidence is reviewable source, tests, and this report. Supplementary evidence is the disposable probe output used to test narrow behavior; it cannot by itself promote an application-wide guarantee.

Acceptance items

Acceptance itemStatusEvidenceGap
Baseline and requirement corpusdonec116761, isolated clean status, a 28-row corpus spanning eight requested domains and all minimum requirements.Corpus counts are composition, not market prevalence.
Five-stage taxonomy and security/reviewability analysisdoneEvery row considered the same ordered mechanisms and has a support placement.Several rows are inference/hypothesis, not production measurements.
Focused PostgreSQL probesdone12/12 disposable PostgreSQL 16 probes passed, including binding, sort, nullable keyset, variants, joins, and EXPLAIN.Fixture plans are not universal performance claims.
Raw SQL / Ashiba / Drizzle / Kysely comparisondoneCompile-only probe ran actual Drizzle 0.45.2 and Kysely 0.29.5 over six disputed cases.No live DB, schema-drift, or transaction comparison for those packages.
Fresh-Agent comparisonpartial82 current adapter tests passed; prior controlled construction evidence is explicitly reused.No independent controlled Fresh-Agent runner, so no new natural-adoption, retry, or human-intervention rate is claimed.
Decision, Constitution, and prototype gatedoneThis decision record narrows adoption boundaries; no candidate met every prototype gate.Dynamic keyset/grouping and governed analytics catalog need follow-up.

Requirement corpus and taxonomy

The corpus covers business search, admin/list, report/export, batch, dashboard, workflow queue, pagination-heavy API, and analytics-like screens. The table reports the first adequate current recommendation, not the only possible implementation. Common means a recurring enough bounded pattern to be useful for Ashiba rules/proof; it does not automatically authorize a new feature.

RequirementEvidenceFirst adequate mechanismPlacementWhy lower mechanisms stop
optional scalar predicateObservedBindingCommona null/scalar value does not change syntax
multiple optional predicatesObservedSubtractionCommonknown branches can be removed from one reviewed maximum shape
IN / list inputObservedBindingCommonPostgreSQL = ANY(array) keeps list values as data
date rangeInferenceBindingCommonbounds are values
LIMIT / OFFSETObservedBindingCommoncounts are values; application still validates range
dynamic sort keyObservedClosed-worldCommonidentifier is syntax, but a finite reviewed key map is sufficient
sort directionObservedClosed-worldCommondirection is syntax, but a finite map is sufficient
NULLS FIRST/LASTObservedSeparate canonical SQLApplicationcurrent Safe Sort rejects these terms; finite expansion is possible but unmeasured in product
multi-column sortObservedClosed-worldCommonone finite profile can select reviewed ordered terms
fixed-sort keysetHypothesisBindingCommoncursor values bind when order/predicate are fixed
dynamic-sort keysetHypothesisInsufficient evidenceApplicationdirection changes predicate and order; finite count/maintenance needs matched evidence
exact/prefix/contains modeInferenceClosed-worldCommonmode is finite syntax/operator choice
optional JOINInferenceSeparate canonical SQLApplicationjoin cardinality and plan/contract can diverge
optional projectionInferenceSeparate canonical SQLApplicationprojection changes result/authorization contract
screen versus export projectionInferenceSeparate canonical SQLCommonowned contracts, PII, and tuning are clearer separately
finite aggregate choiceInferenceSeparate canonical SQLApplicationaggregate/result semantics differ materially
GROUP BY variationInferenceInsufficient evidenceApplicationfinite report modes and open grouping have different boundaries
optional HAVINGInferenceSubtractionApplicationknown aggregate condition can be removed; semantic divergence may split queries
finite status/business modeObservedClosed-worldCommonenum-like modes map to reviewed predicates/expressions
arbitrary nested boolean treeHypothesisOpen-ended BuilderExplicitly out of scopetree shape is unbounded syntax
arbitrary JOIN graphHypothesisOpen-ended BuilderExplicitly out of scoperelation graph is unbounded syntax/cost/authorization
user-selected columnsHypothesisOpen-ended BuilderExplicitly out of scopeidentifiers and result surface are unbounded
user-selected aggregatesHypothesisOpen-ended BuilderExplicitly out of scopefunctions/expressions and cost are unbounded
user-selected groupingHypothesisOpen-ended BuilderExplicitly out of scopegroup shape is unbounded
batch scope and chunkInferenceBindingCommonIDs, cursor and batch size are data; workflow remains app-owned
fixed dashboard metricsInferenceSeparate canonical SQLCommondistinct grain/plan/metric meaning warrants separate resources
queue claim / lockObservedSeparate canonical SQLApplicationlocking and transaction boundary must remain explicit
PostgreSQL array/JSON/searchObservedBindingCommonfixed operator/expression with bound values needs no builder

Corpus composition: Binding 7, Subtraction 2, Closed-world Construction 5, Separate Canonical SQL 7, Open-ended Builder 5, Insufficient Evidence 2. Those numbers describe the deliberately bounded corpus, not the outside world.

PostgreSQL observations

The supplementary fixture created and removed a private schema in a local PostgreSQL 16 instance. All 12 probes passed in 0.304 seconds.

Binding and its hard boundary

  • ANY($1::bigint[]), LIMIT $2, and OFFSET $3 executed with array/page values bound as values. A negative LIMIT was rejected by PostgreSQL, so policy validation remains application responsibility.
  • ORDER BY $1 accepted the value name, but it ordered by a constant expression, not by the name identifier. This is an observed reason not to mistake parameter binding for identifier/direction binding.
  • Fixed PostgreSQL full-text, JSON, array, or trigram-style expressions can retain their syntax in canonical SQL while search terms bind.

Dynamic Sort

The fixture executed finite mappings for a simple column, multi-column order, ASC/DESC, explicit NULL placement, lower(name), COLLATE "C", and a CASE expression. Every executable term was source-defined; no request text became SQL syntax. This validates the pattern:

text
runtime input -> reviewed key -> predefined complete ordering term

It does not prove that current Ashiba Safe Sort already supports the whole pattern. Current Safe Sort correctly rejects explicit NULLS FIRST/LAST rather than silently losing their semantics. A future finite-null policy is a candidate only after a separate before/after Agent and product experiment; none is adopted here.

Keyset pagination

A fixed nullable order used explicit null handling and an id tiebreaker; its cursor values were parameters. Switching ASC to DESC changed both the row comparison and ORDER BY, producing two inspectable variants. Therefore:

  • fixed policy + bound cursor: Binding;
  • a small known strategy catalogue: potentially Closed-world or separate SQL;
  • a growing cross-product of key, direction, null policy, cursor encoding, and tiebreaker: not yet demonstrated as practically maintainable.

For example, 2 sort keys × 2 directions × 2 null policies × 3 cursor modes is 24 strategies before application business modes. Finite is not automatically simple; the actual variant/test/review count must be measured before product support is proposed.

JOIN, projection, and plans

Two canonical variants (users_only, with_order_total) passed with distinct row contracts. This is clearer than a generic projection/join string for a small owned include set, while it does not scale to arbitrary user analytics.

The fixture's EXPLAIN facts are deliberately narrow:

QueryObserved planCost
indexed name ASC NULLS LAST, id ASCLimit -> Index Only Scan1.83
lower(name) without expression indexLimit -> Sort -> Seq Scan252.60
offset 4000Limit -> Index Only Scan311.73
keyset after cursorLimit -> Index Only Scan2.51

They demonstrate that a finite expression choice can have a real plan consequence and that a keyset policy can outperform deep offset in this fixture. They do not choose a universal pagination or sort architecture.

Open-ended analytics counterexample

A fixed date_trunc($1, created_at) bucket and time bounds were bound values. The request “arbitrary dimensions country/device/plan plus sum/p95/distinct-count” has unbounded identifiers, aggregate expressions, GROUP BY, result type, authorization, and cost. It cannot safely be one free-form canonical SQL string. Its honest choices are a finite semantic catalog, separate approved reports, or a governed builder/analytics engine.

Security and canonical-SQL reviewability

MechanismCan runtime value become syntax?Capability relative to canonical SQLReview/client/EXPLAIN outcome
Bindingnounchangedone independently executable SQL resource
Subtractionno, if rewrite only removes reviewed branchesequal or smallermaximum predicate surface remains visible; branch tests required
Closed-world mapnot after input -> key -> fixed fragmentfinite, bounded by reviewed mapall allowed terms must be source-visible and complete
Separate canonical SQLnoone reviewed contract per resourcestrongest direct SQL-client and tuning round-trip
Open-ended buildervalues can still bind; identifiers/shape varybroader than any one canonical resourcecompiled/logged SQL and domain policy become required review artifacts

Raw APIs remain a boundary in Drizzle and Kysely too: their normal builders compiled values as $1… parameters, but sql.raw emitted the supplied string verbatim in the probe. That is useful for database syntax, not an automatic allowlist. Kysely dynamic.ref quoted an identifier for a genuinely dynamic analytics dimension; this is a legitimate capability with a deliberately broader runtime surface.

Raw SQL, Ashiba, Drizzle, and Kysely

The supplementary compile-only probe used Drizzle 0.45.2 and Kysely 0.29.5. It compiled six representative requirements; it did not execute them against a database.

ProblemRaw SQL / Ashiba rulesAshiba current mechanismDrizzle/Kysely resultDecision
optional search + finite sort + pagecanonical SQL and valuesbinding + finite Safe Sorttyped builder with bound placeholdersall fit; choose canonical SQL when direct review/execution matters
dynamic listPostgreSQL array/explicit driver conventionno general list-expansion policy observedinArray / in expanded bound placeholdersbuilders have convenience; Ashiba should document adapter policy before feature work
free-form sortunsafe interpolationintentionally unavailableraw escape hatches emitted verbatim stringretain finite map; do not add free sort
subtractive optional predicatetwo reviewed shapesoptional-condition compressionconditional compositionall fit; no builder necessity
PostgreSQL full-text expressioncanonical expression + bound termcanonical SQLraw expression with bound valueboth viable; standalone SQL improves client/tuning reuse
open analytics dimensionfinite catalog or separate reportsintentional non-goalKysely dynamic.ref directly composed identifier; Drizzle minimal probe required rawbuilder/analytics system wins when truly open

Drizzle documents .$dynamic() so reusable functions may add clauses or joins; Kysely documents composable expressions and dynamic references. Those are composition strengths, not evidence that ordinary bounded search needs them. Neither normal builder path makes arbitrary request-provided sort text safe; the application still needs finite maps or a policy layer.

Fresh-Agent and natural-selection result

Status: partial. This worktree did not expose a controlled independent Fresh-Agent runner or model allocation. No agent selection was simulated.

The direct package check passed 82 tests across the core and PostgreSQL adapters, including unknown/duplicate/SQL-like sort rejection, invalid directions, stale/missing model metadata, optional-condition composition, and execution shaping. Existing construction evidence remains relevant context: Tool Available agents did not naturally select Ashiba commands in the prior Greenfield/Brownfield cells; Tool Required scaffolds needed repair or were discarded. It is not a new dynamic-SQL tool-adoption rate.

Consequently, final correctness, injection safety, canonical reviewability, and architecture fit have direct mechanism evidence, but natural approach selection, LLM retries, false repair, human intervention, files/bytes read, and tokens remain unmeasured for this five-case comparison.

Adoption boundaries

Ashiba boundary

Ashiba is a natural fit when the application owns a finite query contract: business search, queues, bounded report modes, fixed dashboards, batch scope, and finite sort/search choices. Its distinctive value is not that it can compose every query: canonical SQL remains executable, tunable, and the reviewable maximum capability while bindings and finite profiles cannot silently add syntax.

Builder boundary

Choose a query builder or dedicated analytics system when the product actually requires an evolving user-defined query language: arbitrary predicate trees, join graphs, output columns, aggregate expressions, grouping dimensions, or a large enough strategy catalogue that every supposed finite variant becomes an unreviewable copy/branch matrix. The builder must then be accompanied by a semantic catalog/authorization/resource policy; parameterization alone is not that policy.

Ashiba support candidates

  • No new Dynamic SQL feature is adopted.
  • Binding, optional-condition subtraction, source-visible finite sorting, and explicit separate SQL are already the relevant patterns.
  • Documenting PostgreSQL list-array policy and measuring finite null-ordering or dynamic-keyset ergonomics are experiments, not feature commitments.

Explicitly out of scope

Generic query builder/fluent API/runtime AST, arbitrary joins/projections, arbitrary grouping/aggregate/filter tree, repository redesign, ORM, migration framework, and new MCP remain out of scope.

Constitution update and remaining hypotheses

The evidence supports retaining, not upgrading to proven invariants:

  • Subtraction-first dynamic behavior: Strong hypothesis. It fit known optional predicate/HAVING branches, but optional joins/projections often became clearer as separate SQL.
  • Closed-world construction: Strong hypothesis. The PostgreSQL probe shows the pattern works for complete finite sort terms, yet variant explosion and current NULLS limitations prevent a universal rule.
  • Runtime capability must not silently exceed canonical SQL: Strong hypothesis. Binding/subtraction preserve it; a finite map preserves it only when all complete terms are reviewed; builders deliberately broaden it.

Open hypotheses: practical dynamic-keyset variant threshold; governed analytics semantic catalog versus builder; Fresh-Agent natural selection and repair burden; current Safe Sort support for finite null-ordering.

Prototype decision

No prototype adopted or reverted. No candidate met all gates: high frequency demonstrated in a real application, inability of bind/subtract/ separate SQL, safe finite design, small implementation, and measured Agent/application benefit. Implementing a safe pagination or null-ordering API now would mistake a plausible experiment for evidence.

Run a controlled five-case Fresh-Agent experiment with identical requirements under Raw SQL/Ashiba rules, Ashiba Tool Available, and Drizzle/Kysely Tool Available. Record discovery, commands, retries, false repairs, final live behavior, review edits, and generated/canonical surface. Prioritize dynamic keyset, finite null ordering, optional projection/join, and one deliberately open analytics workload. Do not implement a builder before that result.

Self review

Consistency review

  • Each acceptance item has status, evidence, and gap; observed, inference, and hypothesis are not collapsed.
  • Supplementary PostgreSQL/compiler output is not presented as a prevalence or universal performance claim.
  • No local filesystem path is used in GitHub-facing text.

Human acceptance review

  • The adoption boundary, non-adoption of a product feature, and the two strongest technical limits are visible before implementation detail.
  • Follow-up: controlled Fresh-Agent cells and a finite-keyset/null-ordering maintenance experiment.
  • Nit: the corpus is deliberately broad enough that some classifications remain inference rather than live application observations.
  • Review readiness: ready as an evidence-bounded no-product-change audit.

What the human should decide next: accept the boundary and run the controlled Fresh-Agent follow-up, or request that bounded experiment before accepting any Dynamic SQL support candidate.