You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Repo:tazama-lf/admin-service (branch dev) Follow-up to:#436 (atomic batch POST), #450 (consistent batch reply shaping). Surfaced while testing the rule/typology batch POST against a remote database.
Problem
The atomic batch (array) POST for rule and typology (#436) inserts items one row at a time: the handler pins a single transaction client and loops repo.create(item, tenantId, client) serially (see buildCrudPlugin in src/utils/crud-schema.ts and TypologyConfigRepo.create in src/repositories/configuration/typology.config.repository.ts). Because a single pg connection serialises queries, an N-item batch costs roughly N database round-trips. Over a remote database link this is the dominant latency - a modest typology batch can approach multi-second response times even though each individual insert is cheap.
Naive parallelism does not fix this:
Promise.all of client.query(...) on the same pinned client gives no concurrency - pg queues queries on one connection and runs them sequentially.
Collapse the per-item inserts into a single parameterised multi-row INSERT per batch, so an N-item batch is one round-trip instead of N, while preserving atomicity and the existing response contract.
INSERT INTO typology (configuration) VALUES ($1),($2),...,($n) RETURNING configuration
A single statement is inherently atomic, so the all-or-nothing guarantee holds (a constraint violation on any row aborts the whole statement) without relying on the explicit transaction wrapper for correctness.
Approach (locked after pre-implementation review - see the "Resolved questions" comment below)
Add a createMany(payloads, tenantId, client?) method to the batch-enabled repositories (rule, typology), duplicated per repo with fully literal SQL, matching the existing get/create/update/remove duplication (Q4). It:
stamps tenantId, creDtTm, updDtTm on each payload exactly as create does today, using one shared timestamp for the whole batch (Q1);
builds one parameterised INSERT INTO <table> (configuration) VALUES ($1),($2),...,($n) RETURNING configuration - placeholders generated from the payload count, values bound positionally, no string interpolation of data;
returns the created entities in input order (Postgres emits RETURNING rows in insertion order for a plain non-parallel multi-row VALUES - deterministic, stable behaviour, accepted reliance);
has no empty-array guard: the schema's minItems: 1 rejects at the boundary and the batch handler is createMany's only caller.
Type system (Q3): createMany? is an optional method on the CrudRepository interface, following the existing optional-capability precedent (activate?/deactivate?/getActive?), with a fail-fast guard at plugin registration when batch is configured but the repo lacks createMany.
The generic batch handler in buildCrudPlugin calls createManyonce for the whole array instead of looping create, keeping the injected runInTransaction wrapper (Q2): the single statement is atomic on its own; the wrapper is retained for interface stability and to future-proof multi-statement batches (noted with a one-line comment).
Respect the existing maxItems cap (default 200, a startup-time constant frozen into the compiled schema - not a runtime knob) and the per-item validation that reports item[<index>] on the first invalid element (Q5).
Acceptance criteria
rule and typology batch POST insert the whole array in a single multi-row INSERT (one database round-trip for the rows), not N serial inserts.
Atomicity is preserved: a failure on any row (e.g. unique-constraint violation) inserts none of the batch - all-or-nothing, asserted by test.
Per-item validation and the maxItems cap behave exactly as today (first invalid item -> 400 item[<index>]: ...; empty array and over-cap -> 400).
Values are bound positionally; no batch data is ever string-interpolated into SQL (no injection surface) - asserted structurally: exact-equality on the statement text as a pure function of the item count, data present only in values, plus a hostile-payload probe.
Red-tests-first: failing unit tests are written first and reviewed before any production code. They must cover: single multi-row statement is issued (one query/round-trip for N rows), input-order preservation in the response, all-or-nothing rollback on a mid-batch failure, validation/cap behaviour unchanged, and the response shape/content-type contract.
Docs: update the README "Configuration POST contract (single or batch)" section to describe the single multi-row INSERT and its atomicity, and update any affected OpenAPI/Swagger notes for explainability and maintainability.
Out of scope
Cross-entity batching (each entity type keeps its own statement/table).
Streaming/COPY-based ingestion or chunking beyond the existing maxItems cap.
Configurable maxItems overrides (env var, per-request, or per-route): the fixed startup-time cap of 200 is significant enough for now.
DB-error-to-HTTP mapping (e.g. unique violation 23505 -> 409): none exists today - a mid-batch failure surfaces as Fastify's default 500 and the single statement preserves that contract unchanged. Adding a mapping is a separate issue.
Cross-cutting
Exact-RC pins; red-first TDD with the database/transport mocked at the unit level; minimal, explanatory-only comments; update the README and Swagger docs.
Repo:
tazama-lf/admin-service(branchdev)Follow-up to: #436 (atomic batch POST), #450 (consistent batch reply shaping). Surfaced while testing the rule/typology batch POST against a remote database.
Problem
The atomic batch (array) POST for
ruleandtypology(#436) inserts items one row at a time: the handler pins a single transaction client and loopsrepo.create(item, tenantId, client)serially (seebuildCrudPlugininsrc/utils/crud-schema.tsandTypologyConfigRepo.createinsrc/repositories/configuration/typology.config.repository.ts). Because a singlepgconnection serialises queries, an N-item batch costs roughly N database round-trips. Over a remote database link this is the dominant latency - a modest typology batch can approach multi-second response times even though each individual insert is cheap.Naive parallelism does not fix this:
Promise.allofclient.query(...)on the same pinned client gives no concurrency -pgqueues queries on one connection and runs them sequentially.BEGIN/COMMIT, losing the all-or-nothing atomicity guarantee that the batch feature exists to provide (feat: allow batch (array) submission of configurations on POST endpoints #436).Goal
Collapse the per-item inserts into a single parameterised multi-row INSERT per batch, so an N-item batch is one round-trip instead of N, while preserving atomicity and the existing response contract.
A single statement is inherently atomic, so the all-or-nothing guarantee holds (a constraint violation on any row aborts the whole statement) without relying on the explicit transaction wrapper for correctness.
Approach (locked after pre-implementation review - see the "Resolved questions" comment below)
createMany(payloads, tenantId, client?)method to the batch-enabled repositories (rule,typology), duplicated per repo with fully literal SQL, matching the existingget/create/update/removeduplication (Q4). It:tenantId,creDtTm,updDtTmon each payload exactly ascreatedoes today, using one shared timestamp for the whole batch (Q1);INSERT INTO <table> (configuration) VALUES ($1),($2),...,($n) RETURNING configuration- placeholders generated from the payload count, values bound positionally, no string interpolation of data;VALUES- deterministic, stable behaviour, accepted reliance);minItems: 1rejects at the boundary and the batch handler iscreateMany's only caller.createMany?is an optional method on theCrudRepositoryinterface, following the existing optional-capability precedent (activate?/deactivate?/getActive?), with a fail-fast guard at plugin registration whenbatchis configured but the repo lackscreateMany.buildCrudPlugincallscreateManyonce for the whole array instead of loopingcreate, keeping the injectedrunInTransactionwrapper (Q2): the single statement is atomic on its own; the wrapper is retained for interface stability and to future-proof multi-statement batches (noted with a one-line comment).201JSON array in input order, each element serialised through the route'sEntityserialiser (consistent with a single create), andContent-Type: application/json.maxItemscap (default 200, a startup-time constant frozen into the compiled schema - not a runtime knob) and the per-item validation that reportsitem[<index>]on the first invalid element (Q5).Acceptance criteria
ruleandtypologybatch POST insert the whole array in a single multi-row INSERT (one database round-trip for the rows), not N serial inserts.201, a JSON array in input order, each element schema-serialised identically to a single-object create,Content-Type: application/json.maxItemscap behave exactly as today (first invalid item ->400 item[<index>]: ...; empty array and over-cap ->400).values, plus a hostile-payload probe.query/round-trip for N rows), input-order preservation in the response, all-or-nothing rollback on a mid-batch failure, validation/cap behaviour unchanged, and the response shape/content-type contract.Out of scope
network_mapbatch (still deliberately single-object only, feat: allow batch (array) submission of configurations on POST endpoints #436).maxItemscap.maxItemsoverrides (env var, per-request, or per-route): the fixed startup-time cap of 200 is significant enough for now.Cross-cutting
Exact-RC pins; red-first TDD with the database/transport mocked at the unit level; minimal, explanatory-only comments; update the README and Swagger docs.