Refuse an empty key list before delete-many renders IN ()
Table of Contents
This page is a capture in the inbox bucket of the product backlog — a pre-sprint idea, not yet pulled into a sprint as a story.
1. What
A batch removal that resolves no rows must not reach the store. The generated
service checks that the request carries removals, then resolves each key to a
row and collects the ids it found. When the request carries a key that names no
row, the collected id list is empty, the check has already passed, and the
repository is called with {}. The generated repository renders the ids with
sqlgen, so the statement reads ... AND ("id" IN ())) AND ... and Postgres
refuses it:
Repository error: Query execution failed: ERROR: syntax error at or near ")"
LINE 1: ...61d4a-aaaf-440d-857f-5e69365ffa06') AND ("id" IN ())) AND ("...
The repository should refuse an empty id list with a verdict of its own, and the service should treat "nothing resolved" as the empty batch its own comment already promises: a row that is already gone is not a failure.
2. Why
Found on 2026-09-26 during the ores.analytics clean pass, in the live V04 run
of the 40 generated scripts against the Acme tenant. The trigger is narrow and
was measured directly: the command fails when its key resolves no row, and
answers ok when one matches. Both branches were observed on the same base with
the same command, pricing_model_configs delete-many:
| Run | State when it ran | Verdict |
|---|---|---|
| Sweep | the add before it had inserted a row named __none__ |
ok |
| Alone | no such row in the tenant | internal_error, syntax error at ) |
pricing_model_product_parameters delete-many fails in the sweep because its
add aborts in the client and leaves no row behind, so the empty branch is the
one it always takes. pricing_engine_types delete-many answers ok because that
entity is keyed by text and its key always resolves to a non-empty predicate.
So the condition is "no row matched", not "the entity is uuid-keyed": a caller reaches it whenever the row is already gone, which is exactly the case the service's own comment calls a success.
The failure is worth its own capture because it is not the sentinel defect: the sentinel decides which key arrives, but a caller can reach this path with a key for a row that is already gone, which is the case the service explicitly treats as a success. A caller cannot tell an empty batch from a broken server, and the answer names an internal error rather than the batch. It is generated code on both sides of the join, so the fix belongs on a codegen story with a regression test.
3. References
projects/ores.analytics/core/src/service/pricing_model_config_service.cpp—delete_many_pricing_model_configs: guardsrequest.removals.empty(), not an empty resolved set.projects/ores.analytics/core/src/repository/pricing_model_config_repository.cpp:335 —where("tenant_id"_c =tid && "id"_c.in(ids) && …)=, whichsqlgenrenders as =IN () = for an empty vector.projects/ores.analytics/modeling/ores.analytics.pricing_model_config.organd the two sibling uuid-keyed models.
4. See also
- Fix the shell recipe sentinel for optional columns — the other client-side abort from the same live run.
- Bring ores.analytics to the clean standard — where this was found, and the V04 note it is recorded in.