@shivaedev/sql 0.23.0 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +185 -170
  3. package/dist/ChangeDeclarations.d.ts +5 -0
  4. package/dist/ChangeDeclarations.d.ts.map +1 -0
  5. package/dist/ChangeDeclarations.js +3 -0
  6. package/dist/ChangeDeclarations.js.map +1 -0
  7. package/dist/batchWrites.d.ts +1 -1
  8. package/dist/batchWrites.d.ts.map +1 -1
  9. package/dist/batchWrites.js +6 -3
  10. package/dist/batchWrites.js.map +1 -1
  11. package/dist/compositeRepository.d.ts +2 -2
  12. package/dist/compositeRepository.d.ts.map +1 -1
  13. package/dist/compositeRepository.js +1 -8
  14. package/dist/compositeRepository.js.map +1 -1
  15. package/dist/conditionalUpsert.d.ts +11 -4
  16. package/dist/conditionalUpsert.d.ts.map +1 -1
  17. package/dist/conditionalUpsert.js +7 -3
  18. package/dist/conditionalUpsert.js.map +1 -1
  19. package/dist/conflictWrites.d.ts +14 -1
  20. package/dist/conflictWrites.d.ts.map +1 -1
  21. package/dist/conflictWrites.js +5 -3
  22. package/dist/conflictWrites.js.map +1 -1
  23. package/dist/insertFromSelect.js +1 -1
  24. package/dist/insertFromSelect.js.map +1 -1
  25. package/dist/mapSqlError.d.ts +6 -0
  26. package/dist/mapSqlError.d.ts.map +1 -0
  27. package/dist/mapSqlError.js +22 -0
  28. package/dist/mapSqlError.js.map +1 -0
  29. package/dist/repository.js +1 -1
  30. package/dist/repository.js.map +1 -1
  31. package/dist/repositoryChanges.d.ts +4 -3
  32. package/dist/repositoryChanges.d.ts.map +1 -1
  33. package/dist/repositoryChanges.js +46 -13
  34. package/dist/repositoryChanges.js.map +1 -1
  35. package/dist/repositoryWrites.d.ts.map +1 -1
  36. package/dist/repositoryWrites.js +5 -5
  37. package/dist/repositoryWrites.js.map +1 -1
  38. package/dist/testing.d.ts +3 -2
  39. package/dist/testing.d.ts.map +1 -1
  40. package/dist/testing.js +25 -3
  41. package/dist/testing.js.map +1 -1
  42. package/dist/transact.js +1 -1
  43. package/dist/transact.js.map +1 -1
  44. package/dist/upsertResult.d.ts +13 -0
  45. package/dist/upsertResult.d.ts.map +1 -0
  46. package/dist/upsertResult.js +9 -0
  47. package/dist/upsertResult.js.map +1 -0
  48. package/dist/versionedRepository.js +6 -6
  49. package/dist/versionedRepository.js.map +1 -1
  50. package/dist/versionedStatements.d.ts +0 -1
  51. package/dist/versionedStatements.d.ts.map +1 -1
  52. package/dist/versionedStatements.js +1 -2
  53. package/dist/versionedStatements.js.map +1 -1
  54. package/package.json +12 -11
  55. package/src/ChangeDeclarations.ts +5 -0
  56. package/src/batchWrites.ts +6 -2
  57. package/src/compositeRepository.ts +3 -9
  58. package/src/conditionalUpsert.ts +11 -7
  59. package/src/conflictWrites.ts +5 -3
  60. package/src/insertFromSelect.ts +1 -1
  61. package/src/mapSqlError.ts +31 -0
  62. package/src/repository.ts +1 -1
  63. package/src/repositoryChanges.ts +56 -19
  64. package/src/repositoryWrites.ts +7 -5
  65. package/src/testing.ts +30 -5
  66. package/src/transact.ts +1 -1
  67. package/src/upsertResult.ts +18 -0
  68. package/src/versionedRepository.ts +6 -6
  69. package/src/versionedStatements.ts +1 -2
package/README.md CHANGED
@@ -23,7 +23,7 @@ const rows = yield* lines.findMany({
23
23
 
24
24
  The application owns the database schema and its native Effect `SqlClient` layer. Configure new clients with `clientOptions` to map camelCase model fields to snake_case PostgreSQL columns. A **model** describes decoded row fields and the variants used for insertion, updating and JSON. A **repository** combines native `SqlModel` CRUD with a small `findMany` query whose filters and selected results use those fields.
25
25
 
26
- Repository writes announce their table and returned row keys automatically. A single write returning more than 1,000 rows across all its chunks announces only the table and `<table>:*`; writes returning up to 1,000 rows keep their item keys. Effect-contract item reads also react to the collection-wide key. Each write uses `transact`; start a surrounding multi-write boundary with `transact` too, so keys leave only after its owning database commits. Bare native transactions are refused because their commit cannot be observed. The change ownership comes from `changes`; this package supplies the SQL boundary and the Reactivity publisher.
26
+ Repository writes announce their table and returned row keys automatically. Every written row keeps its record key, including in bulk writes. Fields marked with `bucket` also announce their old and new bucket keys. Each write uses `transact`; start a surrounding multi-write boundary with `transact` too, so keys leave only after its owning database commits. Bare native transactions are refused because their commit cannot be observed. The change ownership comes from `changes`; this package supplies the SQL boundary and the Reactivity publisher.
27
27
 
28
28
  Migrations are ordinary Effects loaded by native Migrator. `migratePostgres` adds coordination for PostgreSQL runners sharing a ledger. It does not derive a table from a model: migration SQL and row codecs must agree.
29
29
 
@@ -36,14 +36,16 @@ import * as PgClient from "@effect/sql-pg/PgClient";
36
36
  import { Redacted } from "effect";
37
37
  import { clientOptions } from "@shivaedev/sql/clientOptions.ts";
38
38
 
39
- const database = PgClient.layer(clientOptions({
40
- url: Redacted.make("postgresql://localhost/example"),
41
- }));
39
+ const database = PgClient.layer(
40
+ clientOptions({
41
+ url: Redacted.make("postgresql://localhost/example"),
42
+ }),
43
+ );
42
44
  ```
43
45
 
44
46
  `clientOptions` enables Effect SQL's `transformQueryNames` with Effect's `String.camelToSnake` and `transformResultNames` with `String.snakeToCamel` by default. A model field `createdAt` writes the migration-created `created_at` column, and returned rows contain `createdAt`. Inserts, selects, filters, ordering, updates and raw SQL share the client's transforms; column aliases are unnecessary. Write literal migration SQL with snake_case columns. In tagged SQL, interpolated identifiers such as `sql("createdAt")` are transformed; literal SQL text stays as written.
45
47
 
46
- To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `platform`'s `appHost` or `appDatabase`, use `clientOptions` on their `config` object; the host forwards those native transforms to its client.
48
+ To opt out for an existing database with quoted camelCase columns or models with snake_case fields, use `PgClient.layer(clientOptions(options, { snakeCase: false }))`. Existing native clients are unchanged until configured with this helper. Native `transformQueryNames` and `transformResultNames` supplied in `options` take precedence. For `appHost` or `appDatabase` from `@shivaedev/server/appHost.ts`, use `clientOptions` on their `config` object; the host forwards those native transforms to its client.
47
49
 
48
50
  ### 1. Once per model: fields and writable variants
49
51
 
@@ -53,20 +55,20 @@ import { Model } from "effect/schema";
53
55
  import { makeRepository } from "@shivaedev/sql/repository.ts";
54
56
 
55
57
  class InvoiceLine extends Model.Class<InvoiceLine>("InvoiceLine")({
56
- amount: Schema.NumberFromString,
57
- id: Model.Field({
58
- select: Schema.Number,
59
- update: Schema.Number,
60
- json: Schema.Number,
61
- }),
62
- name: Schema.String,
63
- note: Schema.NullOr(Schema.String),
58
+ amount: Schema.NumberFromString,
59
+ id: Model.Field({
60
+ select: Schema.Number,
61
+ update: Schema.Number,
62
+ json: Schema.Number,
63
+ }),
64
+ name: Schema.String,
65
+ note: Schema.NullOr(Schema.String),
64
66
  }) {}
65
67
 
66
68
  const makeInvoiceLines = makeRepository(InvoiceLine, {
67
- tableName: "invoice_line",
68
- idColumn: "id",
69
- spanPrefix: "InvoiceLine",
69
+ tableName: "invoice_line",
70
+ idColumn: "id",
71
+ spanPrefix: "InvoiceLine",
70
72
  });
71
73
  ```
72
74
 
@@ -78,8 +80,8 @@ A generated creation time can have a different database and JSON representation:
78
80
 
79
81
  ```ts
80
82
  const createdAt = Model.Field({
81
- select: Schema.Date,
82
- json: Schema.DateFromString,
83
+ select: Schema.Date,
84
+ json: Schema.DateFromString,
83
85
  });
84
86
  ```
85
87
 
@@ -93,17 +95,15 @@ import { Result, SchemaGetter, SchemaParser } from "effect";
93
95
 
94
96
  const Integers = Schema.Array(Schema.Int);
95
97
  const StoredIntegers = Schema.Unknown.pipe(
96
- Schema.decodeTo(Integers, {
97
- decode: SchemaGetter.transformEffect((value) =>
98
- SchemaParser.decodeUnknownEffect(Integers)(PgTypes.isParameter(value) ? value.value : value),
99
- ),
100
- encode: SchemaGetter.transform((values) => Result.getOrThrow(PgTypes.array(values, PgTypes.OID.int4))),
101
- }),
98
+ Schema.decodeTo(Integers, {
99
+ decode: SchemaGetter.transformEffect((value) => SchemaParser.decodeUnknownEffect(Integers)(PgTypes.isParameter(value) ? value.value : value)),
100
+ encode: SchemaGetter.transform((values) => Result.getOrThrow(PgTypes.array(values, PgTypes.OID.int4))),
101
+ }),
102
102
  );
103
103
 
104
104
  class Sample extends Model.Class<Sample>("Sample")({
105
- id: Schema.Int,
106
- values: Model.Field({ insert: StoredIntegers, select: StoredIntegers, update: StoredIntegers, json: Integers }),
105
+ id: Schema.Int,
106
+ values: Model.Field({ insert: StoredIntegers, select: StoredIntegers, update: StoredIntegers, json: Integers }),
107
107
  }) {}
108
108
  ```
109
109
 
@@ -113,20 +113,20 @@ The decoded value is an integer array in every variant. The database variants en
113
113
 
114
114
  ```ts
115
115
  const reviseInvoiceLine = Effect.gen(function* () {
116
- const lines = yield* makeInvoiceLines;
117
- const inserted = yield* lines.insert({
118
- amount: 52,
119
- name: "Consulting",
120
- note: null,
121
- });
122
- const updated = yield* lines.update({
123
- ...inserted,
124
- amount: 55,
125
- name: "Onsite consulting",
126
- });
127
- const found = yield* lines.findById(updated.id);
128
- yield* lines.delete(found.id);
129
- return yield* lines.findMany({ where: { id: found.id } });
116
+ const lines = yield* makeInvoiceLines;
117
+ const inserted = yield* lines.insert({
118
+ amount: 52,
119
+ name: "Consulting",
120
+ note: null,
121
+ });
122
+ const updated = yield* lines.update({
123
+ ...inserted,
124
+ amount: 55,
125
+ name: "Onsite consulting",
126
+ });
127
+ const found = yield* lines.findById(updated.id);
128
+ yield* lines.delete(found.id);
129
+ return yield* lines.findMany({ where: { id: found.id } });
130
130
  });
131
131
  ```
132
132
 
@@ -144,15 +144,15 @@ For lists, choose the fields the caller needs:
144
144
 
145
145
  ```ts
146
146
  const listInvoiceLines = (name: string | undefined) =>
147
- Effect.gen(function* () {
148
- const lines = yield* makeInvoiceLines;
149
- return yield* lines.findMany({
150
- where: { amount: 52, name, note: null },
151
- select: ["name", "amount"],
152
- orderBy: { field: "name", direction: "desc" },
153
- limit: 20,
154
- });
155
- });
147
+ Effect.gen(function* () {
148
+ const lines = yield* makeInvoiceLines;
149
+ return yield* lines.findMany({
150
+ where: { amount: 52, name, note: null },
151
+ select: ["name", "amount"],
152
+ orderBy: { field: "name", direction: "desc" },
153
+ limit: 20,
154
+ });
155
+ });
156
156
  ```
157
157
 
158
158
  Filter entries are joined with `AND`. A field value uses equality; a null filter matches SQL `NULL`, and an omitted or `undefined` value leaves the field unconstrained. Filters use decoded field values, so `amount` takes a number. Selected results are decoded records with the selected fields' types; this function returns `Array<{ readonly name: string; readonly amount: number }>`.
@@ -267,34 +267,30 @@ Here `archive` is another unversioned `InvoiceLine` repository with its own tabl
267
267
 
268
268
  A repository configured with `conflictColumns` also exposes `insertFromSelectOrIgnore(source, columns)`. It uses the same conflict columns and optional `conflictWhere` predicate as `insertOrIgnore`, skips existing keys, and returns only inserted rows. An empty source returns `[]`. Both writes join `transact` and roll back with it. Source filter encoding services and target result decoding services remain in the write's Effect environment.
269
269
 
270
-
271
270
  ### Composite keys and idempotent writes
272
271
 
273
272
  A generated app can ingest the same reading more than once. Declare the table's primary key with an `idColumn` tuple and its unique conflict key with `conflictColumns`:
274
273
 
275
274
  ```ts
276
275
  class Reading extends Model.Class<Reading>("Reading")({
277
- source: Schema.String,
278
- sequence: Schema.NumberFromString,
279
- amount: Schema.NumberFromString,
280
- label: Schema.String,
276
+ source: Schema.String,
277
+ sequence: Schema.NumberFromString,
278
+ amount: Schema.NumberFromString,
279
+ label: Schema.String,
281
280
  }) {}
282
281
 
283
282
  const ingest = Effect.gen(function* () {
284
- const readings = yield* makeRepository(Reading, {
285
- tableName: "readings",
286
- idColumn: ["source", "sequence"],
287
- conflictColumns: ["source", "sequence"],
288
- spanPrefix: "Reading",
289
- });
290
- const input = { source: "meter", sequence: 1, amount: 52, label: "Imported" };
291
- const inserted = yield* readings.insertOrIgnore(input);
292
- const saved = yield* readings.upsert(
293
- { ...input, amount: 60 },
294
- { updateColumns: ["amount"] },
295
- );
296
- const found = yield* readings.findById({ source: "meter", sequence: 1 });
297
- return { inserted, saved, found };
283
+ const readings = yield* makeRepository(Reading, {
284
+ tableName: "readings",
285
+ idColumn: ["source", "sequence"],
286
+ conflictColumns: ["source", "sequence"],
287
+ spanPrefix: "Reading",
288
+ });
289
+ const input = { source: "meter", sequence: 1, amount: 52, label: "Imported" };
290
+ const inserted = yield* readings.insertOrIgnore(input);
291
+ const saved = yield* readings.upsert({ ...input, amount: 60 }, { updateColumns: ["amount"] });
292
+ const found = yield* readings.findById({ source: "meter", sequence: 1 });
293
+ return { inserted, saved, found };
298
294
  });
299
295
  ```
300
296
 
@@ -302,12 +298,12 @@ The application creates `readings` with `PRIMARY KEY (source, sequence)`; `seque
302
298
 
303
299
  `conflictColumns` is a nonempty tuple of inserted model fields backed by a database unique constraint or primary key. It can differ from the primary key, including a unique external key beside a generated ID. Declaring it adds these methods:
304
300
 
305
- | Method | Input | Result |
306
- | --- | --- | --- |
307
- | `insertOrIgnore` | Model insert input | `Option.some` of the decoded inserted model, or `Option.none` when the declared key conflicts. |
308
- | `upsert` | Model insert input and `{ updateColumns: [...] }` | The decoded inserted or updated model. |
309
- | `insertManyOrIgnore` | Readonly array of model insert inputs | Decoded inserted models; conflicting rows are omitted. |
310
- | `upsertMany` | Readonly array of model insert inputs and `{ updateColumns: [...] }` | Decoded inserted or updated models. |
301
+ | Method | Input | Result |
302
+ | -------------------- | -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
303
+ | `insertOrIgnore` | Model insert input | `Option.some` of the decoded inserted model, or `Option.none` when the declared key conflicts. |
304
+ | `upsert` | Model insert input and `{ updateColumns: [...] }` | `{ row, inserted }`, with the decoded model and a boolean identifying an insert. |
305
+ | `insertManyOrIgnore` | Readonly array of model insert inputs | Decoded inserted models; conflicting rows are omitted. |
306
+ | `upsertMany` | Readonly array of model insert inputs and `{ updateColumns: [...] }` | An array of `{ row, inserted }` results. |
311
307
 
312
308
  All four methods encode with the model's insert schema and retain its codec services. `upsert` and `upsertMany` require a nonempty list of inserted, updateable fields; primary-key, conflict-key and generated fields are excluded. Only those columns take their incoming encoded values on conflict. Other stored values stay intact. A selected optional field must be present in every encoded input. Unknown, empty, protected or absent update columns fail with `SchemaError`. A violation of another unique constraint remains a native `SqlError`.
313
309
 
@@ -337,7 +333,9 @@ const result = yield* pullRequests.upsert(incoming, {
337
333
 
338
334
  `updateWhere` uses the repository filter shape: equality, `in`, ranges and text patterns combine with AND. A field names the stored column; `{ excluded: "field" }` refers to an incoming column of the same decoded type, in equality or range operands. Literal values pass through their field codecs. The condition becomes `DO UPDATE SET ... WHERE ...`, separate from the index predicate `conflictWhere`, and does not restrict inserts. PostgreSQL evaluates it while holding the conflicting row lock, so an older or equal incoming timestamp cannot overwrite a newer stored revision.
339
335
 
340
- With `updateWhere`, the typed result is `{ _tag: "Inserted", row }`, `{ _tag: "Updated", row }` or `{ _tag: "Unchanged" }`. Changed rows are decoded models; unchanged results carry no row and announce no change keys. Omitting `updateWhere` keeps the model result. `UpsertFilter` and `UpsertResult` are exported from `@shivaedev/sql/conditionalUpsert.ts`.
336
+ On PostgreSQL, `upsert` returns `{ row, inserted }`: `row` is the decoded model and `inserted` is `true` for an insert and `false` for an update. Read the model from `saved.row` or destructure `{ row }`. `upsertMany` returns the same shape for each returned row. `UpsertRow` is exported from `@shivaedev/sql/upsertResult.ts`.
337
+
338
+ With `updateWhere`, the typed result is `{ _tag: "Inserted", row, inserted: true }`, `{ _tag: "Updated", row, inserted: false }` or `{ _tag: "Unchanged" }`. Unchanged results carry no row and announce no change keys. `UpsertFilter` and `UpsertResult` are exported from `@shivaedev/sql/conditionalUpsert.ts`.
341
339
 
342
340
  `upsertMany(inputs, { updateColumns, updateWhere })` accepts the same condition and returns one tagged result per input, in input order. It executes one statement per input inside one transaction, so a later failure rolls back earlier writes. This uses more database round trips than an unconditional batch. Other batch calls use the chunked statements described below.
343
341
 
@@ -345,7 +343,7 @@ Versioned repositories with `conflictColumns` offer `insertOrIgnore`, including
345
343
 
346
344
  ### Batch writes
347
345
 
348
- Every unversioned repository also offers `insertMany(inputs)`, without requiring `conflictColumns`. Each batch method accepts a readonly array of decoded model insert inputs. Unconditional batch methods return an array of decoded models; conditional upserts return the tagged outcomes described above. Empty inputs return `[]` without writing.
346
+ Every unversioned repository also offers `insertMany(inputs)`, without requiring `conflictColumns`. Each batch method accepts a readonly array of decoded model insert inputs. `insertMany` and `insertManyOrIgnore` return decoded models; `upsertMany` returns `{ row, inserted }` results, or the tagged outcomes described above when conditional. Empty inputs return `[]` without writing.
349
347
 
350
348
  ```ts
351
349
  const saved = yield* readings.upsertMany(
@@ -367,27 +365,27 @@ Declare a numeric version field in the model's select and update variants, leave
367
365
 
368
366
  ```ts
369
367
  class Document extends Model.Class<Document>("Document")({
370
- id: Schema.String,
371
- ownerId: Schema.String,
372
- name: Schema.String,
373
- lockVersion: Model.Field({ select: Schema.Number, update: Schema.Number, json: Schema.Number }),
368
+ id: Schema.String,
369
+ ownerId: Schema.String,
370
+ name: Schema.String,
371
+ lockVersion: Model.Field({ select: Schema.Number, update: Schema.Number, json: Schema.Number }),
374
372
  }) {}
375
373
 
376
374
  const makeDocuments = makeRepository(Document, {
377
- tableName: "documents",
378
- idColumn: ["id", "ownerId"],
379
- conflictColumns: ["id"],
380
- spanPrefix: "Document",
381
- versionColumn: "lockVersion",
375
+ tableName: "documents",
376
+ idColumn: ["id", "ownerId"],
377
+ conflictColumns: ["id"],
378
+ spanPrefix: "Document",
379
+ versionColumn: "lockVersion",
382
380
  });
383
381
 
384
382
  const reviseDocument = Effect.gen(function* () {
385
- const documents = yield* makeDocuments;
386
- const key = { id: crypto.randomUUID(), ownerId: "current-owner" };
387
- yield* documents.insertOrIgnore({ ...key, name: "Draft" });
388
- const original = yield* documents.findById(key);
389
- const saved = yield* documents.update({ ...original, name: "Published" });
390
- yield* documents.delete(key, saved.lockVersion);
383
+ const documents = yield* makeDocuments;
384
+ const key = { id: crypto.randomUUID(), ownerId: "current-owner" };
385
+ yield* documents.insertOrIgnore({ ...key, name: "Draft" });
386
+ const original = yield* documents.findById(key);
387
+ const saved = yield* documents.update({ ...original, name: "Published" });
388
+ yield* documents.delete(key, saved.lockVersion);
391
389
  });
392
390
  ```
393
391
 
@@ -406,27 +404,44 @@ import { Data } from "effect";
406
404
  import { transact } from "@shivaedev/sql/transact.ts";
407
405
 
408
406
  class StorageUnavailable extends Data.TaggedError("StorageUnavailable")<{
409
- readonly reason: string;
407
+ readonly reason: string;
410
408
  }> {}
411
409
 
412
410
  const saveInvoiceLine = (input: typeof InvoiceLine.update.Type) =>
413
- Effect.gen(function* () {
414
- const lines = yield* makeInvoiceLines;
415
- const saved = yield* lines.update(input);
416
- return saved;
417
- }).pipe(
418
- transact({
419
- onSqlError: (error) => new StorageUnavailable({ reason: error.message }),
420
- }),
421
- );
411
+ Effect.gen(function* () {
412
+ const lines = yield* makeInvoiceLines;
413
+ const saved = yield* lines.update(input);
414
+ return saved;
415
+ }).pipe(
416
+ transact({
417
+ onSqlError: (error) => new StorageUnavailable({ reason: error.message }),
418
+ }),
419
+ );
422
420
  ```
423
421
 
424
422
  `transact` wraps native `sql.withTransaction`. It maps typed `SqlError` failures through `onSqlError` and keeps other typed failures and service requirements. `invalidateOnCommit` records derived reader keys that a repository or command declaration does not already name; after the owning outermost commit, they are deduplicated and passed to native `Reactivity.invalidate` once. The record `{ invoiceLines: [1] }` marks `"invoiceLines"` and `"invoiceLines:1"`. An array such as `["invoiceLines:1"]` marks only those keys.
425
423
 
426
- Repository writes returning more than 1,000 rows announce the table key and `<table>:*` instead of per-row item keys, counted across all chunks of each call. Calls returning up to 1,000 rows keep their table and item keys. Effect-contract item reads register the collection-wide key automatically; code registering raw `<table>:<id>` Reactivity keys must also register `<table>:*`. Command invalidation keys are unchanged.
424
+ Mark bucket fields on the model with `bucket` from `@shivaedev/changes/buckets.ts`:
427
425
 
428
- The transaction rules follow the database owner:
426
+ ```ts
427
+ import { bucket } from "@shivaedev/changes/buckets.ts";
428
+ import { collection, invalidationKeys } from "@shivaedev/changes/keys.ts";
429
+
430
+ class Note extends Model.Class<Note>("Note")({
431
+ id: Schema.Number,
432
+ listId: bucket(Schema.Number),
433
+ owner: bucket(Schema.String),
434
+ title: Schema.String,
435
+ }) {}
436
+
437
+ const notes = collection("notes", Note.fields.id, { listId: Note.fields.listId, owner: Note.fields.owner });
438
+ ```
429
439
 
440
+ Use the repository's table name as the collection name in query read keys. Every inserted, updated or deleted row announces its record key and each declared bucket from the old and new stored values. An insert belongs only to its new buckets; a move announces both sides; a delete announces the old buckets. Bucket codecs decode stored values before producing keys, so a `bucket(Schema.NumberFromString)` field matches a numeric query bucket even when stored as text. Nullable and optional bucket values contribute no key when absent. Bucket codecs must need no services and decode to strings, numbers or absent values.
441
+
442
+ Bucket writes require PostgreSQL 18 or later: native `RETURNING old` supplies old values atomically, including for conflict updates. Repositories without bucket fields keep their existing SQL dialect support. Bulk writes keep every record and bucket key; the PostgreSQL publisher splits large batches into NOTIFY payloads.
443
+
444
+ The transaction rules follow the database owner:
430
445
  - A typed failure or interruption before commit rolls back and announces nothing. A failed deferred `COMMIT` fails through `onSqlError` and also announces nothing.
431
446
  - A nested `transact` on the same client uses a native savepoint. Its successful keys join the outer set; a rolled-back savepoint's keys are discarded.
432
447
  - A transaction on a second client commits independently. Its keys publish when that client commits, even if the enclosing client's transaction later rolls back.
@@ -446,14 +461,15 @@ Serialize cooperating jobs inside `transact`:
446
461
  ```ts
447
462
  import { advisoryXactLock } from "@shivaedev/sql/advisoryXactLock.ts";
448
463
 
449
- const serializeInvoiceJob = (input: typeof InvoiceLine.update.Type) => Effect.gen(function* () {
450
- yield* advisoryXactLock("jobs:invoice-import");
451
- yield* saveInvoiceLine(input);
452
- }).pipe(
453
- transact({
454
- onSqlError: (error) => new StorageUnavailable({ reason: error.message }),
455
- }),
456
- );
464
+ const serializeInvoiceJob = (input: typeof InvoiceLine.update.Type) =>
465
+ Effect.gen(function* () {
466
+ yield* advisoryXactLock("jobs:invoice-import");
467
+ yield* saveInvoiceLine(input);
468
+ }).pipe(
469
+ transact({
470
+ onSqlError: (error) => new StorageUnavailable({ reason: error.message }),
471
+ }),
472
+ );
457
473
  ```
458
474
 
459
475
  `advisoryXactLock(key: bigint | string)` waits for PostgreSQL's `pg_advisory_xact_lock` on the current client's transaction. A bigint is passed as a signed 64-bit PostgreSQL key without converting it to a JavaScript number. A string uses PostgreSQL's `hashtextextended(key, 0)` as its bigint key; use the same string in cooperating callers. Hash collisions share a lock. Different keys proceed independently. The lock lasts until commit or rollback, including rollback when the holding fiber is interrupted. Outside a transaction, the function fails with typed `AdvisoryLockOutsideTransaction` from the same module. SQL failures remain typed `SqlError` and follow the surrounding `transact` mapper.
@@ -467,8 +483,8 @@ import { Effect } from "effect";
467
483
  import { SqlClient } from "effect/sql";
468
484
 
469
485
  export default Effect.gen(function* () {
470
- const sql = yield* SqlClient.SqlClient;
471
- yield* sql`create table orders (name text primary key)`;
486
+ const sql = yield* SqlClient.SqlClient;
487
+ yield* sql`create table orders (name text primary key)`;
472
488
  });
473
489
  ```
474
490
 
@@ -481,9 +497,9 @@ import { migrationsFromDirectory } from "@shivaedev/sql/migrationsFromDirectory.
481
497
  import { migratePostgres } from "@shivaedev/sql/migrations.ts";
482
498
 
483
499
  const migrateOrders = migratePostgres({
484
- table: "app_migrations",
485
- lockTimeout: "5 seconds",
486
- loader: migrationsFromDirectory(process.cwd()).pipe(Effect.provide(NodeServices.layer)),
500
+ table: "app_migrations",
501
+ lockTimeout: "5 seconds",
502
+ loader: migrationsFromDirectory(process.cwd()).pipe(Effect.provide(NodeServices.layer)),
487
503
  });
488
504
  ```
489
505
 
@@ -497,7 +513,7 @@ Use sequential numbers, not timestamps. Effect's Migrator skips any migration wh
497
513
 
498
514
  `migratePostgres` takes a transaction-scoped advisory lock before creating the ledger and running native Migrator. Concurrent runners on a fresh or existing ledger wait, then see the committed ledger and avoid repeating the migrations. Every runner of that ledger must use this helper and the same ledger name. The required `lockTimeout` bounds PostgreSQL lock waits; `Duration.infinity` sets the transaction's lock timeout to zero. Choose deployment and execution timeouts in the application.
499
515
 
500
- A failed batch rolls back its DDL, data and ledger entries, retaining previously committed migrations. A failed first batch also rolls back the bootstrapped ledger. A lock timeout fails with typed `SqlError` and reason `LockTimeoutError`; a migration-body SQL failure is a native `MigrationError` defect. Inspect startup completion with `Effect.exit` when both typed failures and defects matter. Keep nontransactional DDL in schema components; external side effects, crash recovery and deployment compatibility need their own migration policy.
516
+ A failed batch rolls back its DDL, data and ledger entries, retaining previously committed migrations. A failed first batch also rolls back the bootstrapped ledger. A lock timeout fails with typed `SqlError` and reason `LockTimeoutError`; a migration-body SQL failure is a typed native `MigrationError`. Inspect startup completion with `Effect.exit` when both typed failures and defects matter. Keep nontransactional DDL in schema components; external side effects, crash recovery and deployment compatibility need their own migration policy.
501
517
 
502
518
  Schema components describe storage owned by another engine alongside the numbered `loader`: pass `components: [{ name, apply, pending }]` to `migratePostgres` and `assertPostgresMigrations`. `apply` runs after the ledger transaction commits, outside that transaction, on every migration invocation; it must support repeated execution. `pending` returns a description when work remains and `undefined` when current. Components are never recorded as numbered ledger entries. Component failures become `MigrationError` (`kind: "Failed"`) with the component name and original cause; their failure leaves the application ledger committed.
503
519
 
@@ -507,8 +523,7 @@ For a structured readiness readout, use `pendingPostgresMigrations({ loader, tab
507
523
 
508
524
  Pass an optional `release` string to `migratePostgres` to record the owning release after all ledger migrations and schema components succeed. The final `postgresRelease(release, table?)` schema component owns a one-row `<ledger>_release` table with typed `release` and UTC `recorded_at timestamptz` columns, plus a `<ledger>_workers` table with typed process identity, release and UTC heartbeat columns. `readPostgresRelease(table?)` from `postgresRelease.ts` returns the recorded release and `recordedAt` Date, or `undefined` for a missing marker. `pendingPostgresMigrations({ loader, release, table? })` reports a missing or different release as a pending component. Without `release`, migration and readiness calls neither write nor read these tables.
509
525
 
510
- Server's `appDatabase` supplies the optional `WorkerRelease` Context service in migration mode `"wait"` with a release. Startup waits for that marker within `waitTimeout`; subsequent checks run every `pollInterval`. Each worker database Layer claims a process row only when its release owns the marker and no live row of another release remains. Heartbeats continue every poll while jobs drain; completed drain and clean shutdown remove the row. Rows older than five polling intervals count as gone, bounding takeover after a crash (five seconds at the default one-second poll). Reserve SQL pool capacity for heartbeats alongside active jobs and the change listener. Release equality decides ownership, so rolling back the marker resumes the earlier worker. jobs reads the service automatically; applications do not connect a signal themselves. Web's `"check"` mode ignores the marker.
511
-
526
+ `appDatabase` from `@shivaedev/server/appHost.ts` supplies the optional `WorkerRelease` Context service in migration mode `"wait"` with a release. Startup waits for that marker within `waitTimeout`; subsequent checks run every `pollInterval`. Each worker database Layer claims a process row only when its release owns the marker and no live row of another release remains. Heartbeats continue every poll while jobs drain; completed drain and clean shutdown remove the row. Rows older than five polling intervals count as gone, bounding takeover after a crash (five seconds at the default one-second poll). Reserve SQL pool capacity for heartbeats alongside active jobs and the change listener. Release equality decides ownership, so rolling back the marker resumes the earlier worker. `@shivaedev/jobs` reads the service automatically; applications do not connect a signal themselves. Web's `"check"` mode ignores the marker.
512
527
 
513
528
  ### PostgreSQL delivery between processes
514
529
 
@@ -516,9 +531,9 @@ Server's `appDatabase` supplies the optional `WorkerRelease` Context service in
516
531
 
517
532
  ```ts
518
533
  const delivery = postgresChanges({
519
- channel: "application_changes",
520
- client: pg,
521
- resync: hints.resync,
534
+ channel: "application_changes",
535
+ client: pg,
536
+ resync: hints.resync,
522
537
  });
523
538
  ```
524
539
 
@@ -531,26 +546,29 @@ NOTIFY is a wake-up signal, not a durable log. Transaction payloads contain only
531
546
  ### API
532
547
 
533
548
  Import the module that defines the name; there is no root entry.
534
-
535
- | Module | Export | Input or role |
536
- | --- | --- | --- |
537
- | `@shivaedev/sql/clientOptions.ts` | `clientOptions(options, { snakeCase? })`, `SqlNameTransforms` | Add default native name transforms to driver or app host options; opt out with `snakeCase: false`. |
538
- | `@shivaedev/sql/repository.ts` | `makeRepository(model, options)` | `tableName`, scalar or composite `idColumn`, `spanPrefix`, optional `versionColumn`, `conflictColumns` and `conflictWhere`; requires native `SqlClient`. |
539
- | `@shivaedev/sql/repository.ts` | `OptimisticConcurrencyError` | Typed stale or missing-row failure for versioned writes, with `tableName`, `id`, `expectedVersion`, `reason: "missing" | "stale"`. |
540
- | `@shivaedev/sql/transact.ts` | `transact(effect, options)` or `effect.pipe(transact(options))` | Required `onSqlError` mapper; requires `SqlClient` and `Reactivity`. |
541
- | `@shivaedev/sql/transact.ts` | `invalidateOnCommit(keys)` | Records keys for its client, or invalidates immediately outside a transaction. |
542
- | `@shivaedev/sql/advisoryXactLock.ts` | `advisoryXactLock(key)`, `AdvisoryLockOutsideTransaction` | PostgreSQL transaction lock on a bigint or string hashed with `hashtextextended(key, 0)`; requires an active transaction on the current `SqlClient`. |
543
- | `@shivaedev/sql/migrationConvention.ts` | `MIGRATIONS_DIRECTORY`, `MIGRATION_FILE_PATTERN` | Application folder and four-digit sequential snake_case TypeScript filename pattern. |
544
- | `@shivaedev/sql/migrationsFromDirectory.ts` | `migrationsFromDirectory(root)` | Discover migrations under the application root with native `FileSystem` and `Path`. |
545
- | `@shivaedev/sql/migrations.ts` | `migratePostgres(options)` | Native loader, required `lockTimeout`, optional `components` and ledger `table`, default `effect_sql_migrations`. |
546
- | `@shivaedev/sql/assertPostgresMigrations.ts` | `assertPostgresMigrations({ loader, table?, components? })` | Read-only readiness check of required migration IDs and names in the native ledger and registered schema components. |
547
- | `@shivaedev/sql/pendingPostgresMigrations.ts` | `pendingPostgresMigrations({ loader, table?, components? })` | Pending ledger migrations as ID and name, and pending schema components as name and detail. |
548
- | `@shivaedev/sql/postgresChanges.ts` | `postgresChanges(options)` | Scoped PostgreSQL LISTEN/NOTIFY Layer for commit-bound invalidation. |
549
+ - `@shivaedev/sql/clientOptions.ts`: `clientOptions(options, { snakeCase? })`, `SqlNameTransforms` — Add default native name transforms to driver or app host options; opt out with `snakeCase: false`.
550
+ - `@shivaedev/sql/repository.ts`: `makeRepository(model, options)` — `tableName`, scalar or composite `idColumn`, `spanPrefix`, optional `versionColumn`, `conflictColumns` and `conflictWhere`; requires native `SqlClient`.
551
+ - `@shivaedev/sql/repository.ts`: `OptimisticConcurrencyError` — Typed stale or missing-row failure for versioned writes, with `tableName`, `id`, `expectedVersion`, `reason: "missing" | "stale"`.
552
+ - `@shivaedev/sql/tupleIn.ts`: `tupleIn(model, fields, tuples)` — Filter entry matching two or more fields together against a list of tuples, for `where` in reads and unversioned deletes.
553
+ - `@shivaedev/sql/upsertResult.ts`: `UpsertRow` — The `{ row, inserted }` result of an unconditional upsert.
554
+ - `@shivaedev/sql/conditionalUpsert.ts`: `UpsertFilter`, `UpsertResult` — The `updateWhere` filter shape and the tagged `Inserted`, `Updated` or `Unchanged` result of a conditional upsert.
555
+ - `@shivaedev/sql/mapSqlError.ts`: `mapSqlError({ constraints, fallback })` — Mapper from named constraint violations to application errors, with a fallback for every other `SqlError`.
556
+ - `@shivaedev/sql/transact.ts`: `transact(effect, options)` or `effect.pipe(transact(options))` — Required `onSqlError` mapper; requires `SqlClient` and `Reactivity`.
557
+ - `@shivaedev/sql/transact.ts`: `invalidateOnCommit(keys)` — Records keys for its client, or invalidates immediately outside a transaction.
558
+ - `@shivaedev/sql/advisoryXactLock.ts`: `advisoryXactLock(key)`, `AdvisoryLockOutsideTransaction` — PostgreSQL transaction lock on a bigint or string hashed with `hashtextextended(key, 0)`; requires an active transaction on the current `SqlClient`.
559
+ - `@shivaedev/sql/migrationConvention.ts`: `MIGRATIONS_DIRECTORY`, `MIGRATION_FILE_PATTERN` — Application folder and four-digit sequential snake_case TypeScript filename pattern.
560
+ - `@shivaedev/sql/migrationsFromDirectory.ts`: `migrationsFromDirectory(root)` — Discover migrations under the application root with native `FileSystem` and `Path`.
561
+ - `@shivaedev/sql/migrations.ts`: `migratePostgres(options)` — Native loader, required `lockTimeout`, optional `components`, `release` and ledger `table`, default `effect_sql_migrations`.
562
+ - `@shivaedev/sql/assertPostgresMigrations.ts`: `assertPostgresMigrations({ loader, table?, components? })` — Read-only readiness check of required migration IDs and names in the native ledger and registered schema components.
563
+ - `@shivaedev/sql/pendingPostgresMigrations.ts`: `pendingPostgresMigrations({ loader, table?, components? })` — Pending ledger migrations as ID and name, and pending schema components as name and detail.
564
+ - `@shivaedev/sql/postgresRelease.ts`: `postgresRelease(release, table?)`, `readPostgresRelease(table?)` — Release marker schema component and its reader.
565
+ - `@shivaedev/sql/postgresChanges.ts`: `postgresChanges(options)` — Scoped PostgreSQL LISTEN/NOTIFY Layer for commit-bound invalidation.
566
+ - `@shivaedev/sql/testing.ts`: `withChangeCoverage(body, { covers })`, `expectPublished(keys)`, `UnannouncedWrites`, `PublishedKeysMismatch` — Test assertions that every write announced its keys and that the expected keys were published.
549
567
 
550
568
  ### Install and database setup
551
569
 
552
570
  ```sh
553
- pnpm add @shivaedev/sql effect@4.0.5 @effect/sql-pg@4.0.5
571
+ pnpm add @shivaedev/sql effect @effect/sql-pg
554
572
  ```
555
573
 
556
574
  `effect` is a peer. Install the native driver your application uses; this command chooses PostgreSQL. Provide its layer, and provide native Reactivity when using `transact` or `invalidateOnCommit`:
@@ -561,16 +579,13 @@ import { Layer, Redacted } from "effect";
561
579
  import * as Reactivity from "effect/reactivity/Reactivity";
562
580
  import { clientOptions } from "@shivaedev/sql/clientOptions.ts";
563
581
 
564
- const database = Layer.merge(
565
- PgClient.layer(clientOptions({ url: Redacted.make("postgresql://localhost/example") })),
566
- Reactivity.layer,
567
- );
582
+ const database = Layer.merge(PgClient.layer(clientOptions({ url: Redacted.make("postgresql://localhost/example") })), Reactivity.layer);
568
583
 
569
584
  const save = saveInvoiceLine({
570
- id: 1,
571
- name: "Consulting",
572
- amount: 52,
573
- note: null,
585
+ id: 1,
586
+ name: "Consulting",
587
+ amount: 52,
588
+ note: null,
574
589
  }).pipe(Effect.provide(database));
575
590
  ```
576
591
 
@@ -580,29 +595,27 @@ The application's startup runs migrations and keeps its database scope alive for
580
595
 
581
596
  Query decoding and invalid limits can fail with `SchemaError`; SQL statement failures remain `SqlError`. `findById` adds `NoSuchElementError` for a missing row. Unversioned `update` defects if it returns no row unless called with `{ onMissing: "fail" }`, which keeps `NoSuchElementError` in the error channel. Versioned updates and deletes instead fail with typed `OptimisticConcurrencyError` when their ID and expected version match no row. A failed `COMMIT` is a typed `SqlError` like any other statement failure.
582
597
 
583
- Map a known constraint at the operation that understands it, after the transaction has rolled back:
598
+ Map named constraints at the operation that understands them. `mapSqlError` returns a mapper for `transact`'s `onSqlError` or `Effect.mapError` on an Effect whose error is `SqlError`:
584
599
 
585
600
  ```ts
586
- import type { SqlError } from "effect/sql/SqlError";
587
-
588
- class NameTaken extends Data.TaggedError("NameTaken") {}
589
-
590
- const knownNameConflict = (error: SqlError): Effect.Effect<never, NameTaken | SqlError> =>
591
- error.reason._tag === "UniqueViolation" &&
592
- error.reason.constraint === "invoice_line_name_key"
593
- ? Effect.fail(new NameTaken())
594
- : Effect.fail(error);
595
-
596
- const insertNamedLine = Effect.gen(function* () {
597
- const sql = yield* SqlClient.SqlClient;
598
- const lines = yield* makeInvoiceLines;
599
- return yield* sql
600
- .withTransaction(lines.insert({ amount: 52, name: "Consulting", note: null }))
601
- .pipe(Effect.catchTag("SqlError", knownNameConflict));
601
+ import { mapSqlError } from "@shivaedev/sql/mapSqlError.ts";
602
+ import { transact } from "@shivaedev/sql/transact.ts";
603
+
604
+ const onSqlError = mapSqlError({
605
+ constraints: {
606
+ "invoice_line_name_key": () => new NameTaken(),
607
+ "invoice_line_invoice_fkey": () => new InvoiceMissing(),
608
+ "invoice_line_amount_check": () => new InvalidAmount(),
609
+ },
610
+ fallback: (error) => new StorageUnavailable({ reason: error.message }),
602
611
  });
612
+
613
+ const insertNamedLine = lines.insert({ amount: 52, name: "Consulting", note: null }).pipe(transact({ onSqlError }));
603
614
  ```
604
615
 
605
- The migration owns the constraint name `invoice_line_name_key`. The mapper recognizes that named unique violation and keeps other failures intact. Put recovery outside the transaction so earlier writes in the operation roll back too. Domain error presentation belongs to the application.
616
+ The mapper handles named unique violations and PostgreSQL foreign-key and check violations. Every other `SqlError`, including unknown constraints, goes to `fallback`. Each callback receives the original error. The inferred error type contains exactly the mapped errors and the fallback; `transact` also preserves other typed failures from the operation. Constraint names belong to the application's migration; error types and presentation belong to the application.
617
+
618
+ Recover outside `transact` so earlier writes roll back before handling a mapped domain error. A losing unique-key insert can then catch `NameTaken` and read the winner's row without a private transaction error. See the [constraint-failures guide](https://github.com/ShivaeDev/platform/blob/main/docs/framework/constraint-errors.md).
606
619
 
607
620
  For the native PostgreSQL driver's default representations, use precise numeric strings with `Schema.BigDecimalFromString`, `int8` values with `Schema.BigInt`, `date` values as `YYYY-MM-DD` strings with `Schema.String`, instants with `Schema.Date`, nullable text and generated values. A persisted value that violates its schema fails decoding; a write and read inside the enclosing transaction roll back together when that failure escapes.
608
621
 
@@ -612,18 +625,20 @@ JSON and JSONB columns are not supported. A repository write of a plain object t
612
625
 
613
626
  The package's PostgreSQL tests use a disposable database through `PLATFORM_SQL_TEST_DATABASE_URL`. Without the URL they skip locally; under `CI`, collection fails at the shared gate.
614
627
 
615
- The sql story kit exposes `withChangeCoverage` and `expectPublished`, also exported from `@shivaedev/sql/testing.ts`. `withChangeCoverage(body, { covers })` checks PostgreSQL transaction write counters before commit. Each written table must have its table key or at least one declared covering key recorded; otherwise `UnannouncedWrites` fails the transaction. Declare a module's domain read keys by table, including for raw SQL writes:
628
+ The sql story kit exposes `withChangeCoverage` and `expectPublished`, also exported from `@shivaedev/sql/testing.ts`. `withChangeCoverage(body, { covers })` checks PostgreSQL transaction write counters before commit and requires every declared record and bucket key to be recorded. Repository writes supply these declarations automatically. Raw writes must declare their affected keys through `covers` and record them through the same after-commit path; a table key alone covers nothing. Missing announcements fail with `UnannouncedWrites` and roll back the transaction.
616
629
 
617
630
  ```ts
618
- const coverage = { covers: { orders: ["dashboard:orders"] } };
631
+ const changed = [notes.item(id), notes.bucket.listId(oldList), notes.bucket.listId(newList), notes.bucket.owner(owner)];
619
632
 
620
633
  yield* withChangeCoverage(
621
634
  Effect.gen(function* () {
622
- yield* sql`update orders set status = 'closed' where id = ${id}`;
623
- yield* invalidateOnCommit(["dashboard:orders"]);
635
+ yield* sql`update notes set "listId" = ${newList} where id = ${id}`;
636
+ yield* invalidateOnCommit(invalidationKeys(changed));
624
637
  }),
625
- coverage,
626
- ).pipe(expectPublished(["dashboard:orders"]));
638
+ { covers: { notes: changed } },
639
+ );
627
640
  ```
628
641
 
642
+ The coverage check verifies the declarations, not the contents of arbitrary SQL. Raw writers must declare every affected record and bucket, including unchanged buckets. Transaction statistics retain writes from rolled-back savepoints, so those writes still need declarations during a coverage check.
643
+
629
644
  `body.pipe(expectPublished(keys))` compares the distinct published keys, regardless of order, and preserves the body's result. Put it outside the committing transaction. It observes the existing change channel and leaves delivery intact; recorded keys from rolled-back writes do not count. A mismatch fails with `PublishedKeysMismatch`, whose `expected`, `actual` and message show both key sets. The assertion happens after the body, so a mismatch does not roll back a committed write. See [automatic change delivery](https://github.com/ShivaeDev/platform/blob/main/docs/framework/changes.md) for host defaults and coverage limits.
@@ -0,0 +1,5 @@
1
+ import { Context, Effect } from "effect";
2
+ export declare const ChangeDeclarations: Context.Reference<{
3
+ declare: (table: string, keys: readonly string[], rows: number) => Effect.Effect<void>;
4
+ }>;
5
+ //# sourceMappingURL=ChangeDeclarations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChangeDeclarations.d.ts","sourceRoot":"","sources":["../src/ChangeDeclarations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,eAAO,MAAM,kBAAkB;aACrB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;EACU,CAAC"}
@@ -0,0 +1,3 @@
1
+ import { Context, Effect } from "effect";
2
+ export const ChangeDeclarations = Context.Reference("@shivaedev/sql/ChangeDeclarations", { defaultValue: () => ({ declare: () => Effect.void }) });
3
+ //# sourceMappingURL=ChangeDeclarations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ChangeDeclarations.js","sourceRoot":"","sources":["../src/ChangeDeclarations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEzC,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC,SAAS,CAEhD,mCAAmC,EAAE,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC"}
@@ -12,5 +12,5 @@ export declare function makeInsertMany<TServices = never>(model: Model.Any, sql:
12
12
  readonly tableName: string;
13
13
  readonly spanPrefix: string;
14
14
  readonly changes: RepositoryChanges;
15
- }, operation: string, prepare?: (row: Readonly<Record<string, unknown>>) => Effect.Effect<Fragment, Schema.SchemaError, TServices>): (inputs: readonly unknown[]) => Effect.Effect<unknown[], Schema.SchemaError | SqlError, unknown>;
15
+ }, operation: string, prepare?: (row: Readonly<Record<string, unknown>>) => Effect.Effect<Fragment, Schema.SchemaError, TServices>, upsert?: boolean): (inputs: readonly unknown[]) => Effect.Effect<unknown[], Schema.SchemaError | SqlError, unknown>;
16
16
  //# sourceMappingURL=batchWrites.d.ts.map