| title | ztd-cli SSSQL Authoring |
|---|---|
| outline | deep |
ztd-cli does not execute application SQL by itself, but it is part of the authoring loop that decides what kind of SQL gets saved under src/sql/.
When the request is "add an optional filter" or "make this condition optional", prefer SSSQL before falling back to string-built SQL assembly outside the SQL file.
In a ZTD project, the authoring loop usually looks like this:
- Update the SQL asset under
src/sql/ - Regenerate or verify the QuerySpec with
ztd model-gen - Run ZTD tests
If the change is a routine optional predicate, the SQL asset should stay truthful:
select
p.product_id,
p.product_name
from public.products p
where (:brand_name is null or p.brand_name = :brand_name)
and (:category_name is null or exists (
select 1
from public.product_categories pc
join public.categories c
on c.category_id = pc.category_id
where pc.product_id = p.product_id
and c.category_name = :category_name
))Then keep the runtime pruning intent explicit in application code:
const query = builder.buildQuery(sql, {
optionalConditionParameters: {
brand_name: input.brandName ?? null,
category_name: input.categoryName ?? null,
},
});When the optional branch is already authored, the CLI can help inspect or undo it:
ztd query sssql list src/sql/products/list_products.sql
ztd query sssql remove src/sql/products/list_products.sql --parameter category_name --previewIf the current rewrite would drop existing SQL comments, the command should fail fast instead of silently writing a damaged file.
Reach for SSSQL first when the prompt sounds like:
- "add an optional filter"
- "make
brand_nameoptional" - "support search-by-category when the value is present"
- "keep one SQL file instead of branching queries in code"
These requests are usually about preserving a single truthful SQL asset, not about inventing a new SQL-construction layer.
For routine optional predicates, avoid:
- building
WHEREfragments with string concatenation - adding
WHERE 1 = 1sentinels only to make later concatenation easier - splitting one readable query into multiple near-duplicate query files
- hiding ordinary optional-filter logic in imperative branching before the SQL is even parsed
Use ztd-cli to keep the SQL-first loop tight:
ztd model-gen --probe-mode ztdvalidates the saved SQL asset against your DDL snapshotztd lintchecks structural issues in authored SQL files- ZTD tests keep the spec, fixtures, and DDL snapshot aligned
ztd-cli should help validate the authored SQL, not push the project toward non-SQL-first assembly for ordinary optional filters.
Use a single SQL asset as the source of truth:
-- src/sql/products/list_products.sql
select
p.product_id,
p.product_name,
p.brand_name
from public.products p
where (:brand_name is null or p.brand_name = :brand_name)
and (:category_name is null or exists (
select 1
from public.product_categories pc
join public.categories c
on c.category_id = pc.category_id
where pc.product_id = p.product_id
and c.category_name = :category_name
))
order by p.product_nameThen run the normal ZTD loop:
ztd model-gen src/features/products/persistence/list_products.sql \
--probe-mode ztd \
--out src/features/products/persistence/list_products.spec.ts
ztd lint src/features/products/persistence/list_products.sql
vitest runIf the runtime layer uses rawsql-ts, keep the pruning contract explicit instead of inventing a second SQL assembly path:
const query = builder.buildQuery(sql, {
optionalConditionParameters: {
brand_name: input.brandName ?? null,
category_name: input.categoryName ?? null,
},
});This is the intended flow for prompts such as "add an optional filter to src/sql/products/list_products.sql" or "make category search optional without splitting the SQL file".
When an AI or scripted caller receives requests like these, the first response should stay on the SQL-first path:
- "Add an optional filter to
src/sql/products/list_products.sql." - "Make
brand_nameoptional without branching the repository method." - "Support category filtering only when the value is present."
A good response shape is:
- edit the SQL asset with truthful optional predicates
- use
ztd query sssql scaffold,ztd query sssql list, orztd query sssql remove --previewwhen that shortens the authoring loop safely - rerun
ztd model-gen --probe-mode ztdif the contract changed - rerun
ztd lintand tests - wire
optionalConditionParametersonly for branch pruning
Do not steer these requests toward string-built WHERE concatenation or redundant LEFT JOIN scaffolding plus later cleanup.
Do not suggest runtime optional predicate injection as if DynamicQueryBuilder still synthesizes new filter branches from filter.