@shivaedev/sql 0.24.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.25.0 - 2026-10-30
4
+
5
+ ### Changed
6
+
7
+ - Require the Effect 4.0.6 stack.
8
+
9
+ ### Fixed
10
+
11
+ - Publish committed SQL invalidations immediately for their owning database, including when another database has an open transaction.
12
+
13
+ ### Breaking
14
+
15
+ - `@shivaedev/sql`: migration-body SQL failures now propagate as native typed `MigrationError` failures. Handle them with `Effect.catchTag("MigrationError", ...)` instead of defect recovery; migration-body defects remain defects.
16
+ - `@shivaedev/http`: bounded request-body reads use the native reader and its connection cleanup. Do not assume a rejected upload can drain an arbitrarily large unread body and reuse its connection; allow the HTTP client to open a new connection. The optional Node platform peer is no longer required by this package.
17
+
3
18
  ## 0.24.0 - 2026-10-28
4
19
 
5
20
  ### Added
package/README.md CHANGED
@@ -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: [...] }` | `{ row, inserted }`, with the decoded model and a boolean identifying an insert. |
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: [...] }` | An array of `{ row, inserted }` results. |
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
 
@@ -369,27 +365,27 @@ Declare a numeric version field in the model's select and update variants, leave
369
365
 
370
366
  ```ts
371
367
  class Document extends Model.Class<Document>("Document")({
372
- id: Schema.String,
373
- ownerId: Schema.String,
374
- name: Schema.String,
375
- 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 }),
376
372
  }) {}
377
373
 
378
374
  const makeDocuments = makeRepository(Document, {
379
- tableName: "documents",
380
- idColumn: ["id", "ownerId"],
381
- conflictColumns: ["id"],
382
- spanPrefix: "Document",
383
- versionColumn: "lockVersion",
375
+ tableName: "documents",
376
+ idColumn: ["id", "ownerId"],
377
+ conflictColumns: ["id"],
378
+ spanPrefix: "Document",
379
+ versionColumn: "lockVersion",
384
380
  });
385
381
 
386
382
  const reviseDocument = Effect.gen(function* () {
387
- const documents = yield* makeDocuments;
388
- const key = { id: crypto.randomUUID(), ownerId: "current-owner" };
389
- yield* documents.insertOrIgnore({ ...key, name: "Draft" });
390
- const original = yield* documents.findById(key);
391
- const saved = yield* documents.update({ ...original, name: "Published" });
392
- 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);
393
389
  });
394
390
  ```
395
391
 
@@ -408,19 +404,19 @@ import { Data } from "effect";
408
404
  import { transact } from "@shivaedev/sql/transact.ts";
409
405
 
410
406
  class StorageUnavailable extends Data.TaggedError("StorageUnavailable")<{
411
- readonly reason: string;
407
+ readonly reason: string;
412
408
  }> {}
413
409
 
414
410
  const saveInvoiceLine = (input: typeof InvoiceLine.update.Type) =>
415
- Effect.gen(function* () {
416
- const lines = yield* makeInvoiceLines;
417
- const saved = yield* lines.update(input);
418
- return saved;
419
- }).pipe(
420
- transact({
421
- onSqlError: (error) => new StorageUnavailable({ reason: error.message }),
422
- }),
423
- );
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
+ );
424
420
  ```
425
421
 
426
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.
@@ -432,10 +428,10 @@ import { bucket } from "@shivaedev/changes/buckets.ts";
432
428
  import { collection, invalidationKeys } from "@shivaedev/changes/keys.ts";
433
429
 
434
430
  class Note extends Model.Class<Note>("Note")({
435
- id: Schema.Number,
436
- listId: bucket(Schema.Number),
437
- owner: bucket(Schema.String),
438
- title: Schema.String,
431
+ id: Schema.Number,
432
+ listId: bucket(Schema.Number),
433
+ owner: bucket(Schema.String),
434
+ title: Schema.String,
439
435
  }) {}
440
436
 
441
437
  const notes = collection("notes", Note.fields.id, { listId: Note.fields.listId, owner: Note.fields.owner });
@@ -446,7 +442,6 @@ Use the repository's table name as the collection name in query read keys. Every
446
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.
447
443
 
448
444
  The transaction rules follow the database owner:
449
-
450
445
  - A typed failure or interruption before commit rolls back and announces nothing. A failed deferred `COMMIT` fails through `onSqlError` and also announces nothing.
451
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.
452
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.
@@ -466,14 +461,15 @@ Serialize cooperating jobs inside `transact`:
466
461
  ```ts
467
462
  import { advisoryXactLock } from "@shivaedev/sql/advisoryXactLock.ts";
468
463
 
469
- const serializeInvoiceJob = (input: typeof InvoiceLine.update.Type) => Effect.gen(function* () {
470
- yield* advisoryXactLock("jobs:invoice-import");
471
- yield* saveInvoiceLine(input);
472
- }).pipe(
473
- transact({
474
- onSqlError: (error) => new StorageUnavailable({ reason: error.message }),
475
- }),
476
- );
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
+ );
477
473
  ```
478
474
 
479
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.
@@ -487,8 +483,8 @@ import { Effect } from "effect";
487
483
  import { SqlClient } from "effect/sql";
488
484
 
489
485
  export default Effect.gen(function* () {
490
- const sql = yield* SqlClient.SqlClient;
491
- 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)`;
492
488
  });
493
489
  ```
494
490
 
@@ -501,9 +497,9 @@ import { migrationsFromDirectory } from "@shivaedev/sql/migrationsFromDirectory.
501
497
  import { migratePostgres } from "@shivaedev/sql/migrations.ts";
502
498
 
503
499
  const migrateOrders = migratePostgres({
504
- table: "app_migrations",
505
- lockTimeout: "5 seconds",
506
- 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)),
507
503
  });
508
504
  ```
509
505
 
@@ -517,7 +513,7 @@ Use sequential numbers, not timestamps. Effect's Migrator skips any migration wh
517
513
 
518
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.
519
515
 
520
- 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.
521
517
 
522
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.
523
519
 
@@ -527,8 +523,7 @@ For a structured readiness readout, use `pendingPostgresMigrations({ loader, tab
527
523
 
528
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.
529
525
 
530
- 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.
531
-
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.
532
527
 
533
528
  ### PostgreSQL delivery between processes
534
529
 
@@ -536,9 +531,9 @@ Server's `appDatabase` supplies the optional `WorkerRelease` Context service in
536
531
 
537
532
  ```ts
538
533
  const delivery = postgresChanges({
539
- channel: "application_changes",
540
- client: pg,
541
- resync: hints.resync,
534
+ channel: "application_changes",
535
+ client: pg,
536
+ resync: hints.resync,
542
537
  });
543
538
  ```
544
539
 
@@ -551,26 +546,29 @@ NOTIFY is a wake-up signal, not a durable log. Transaction payloads contain only
551
546
  ### API
552
547
 
553
548
  Import the module that defines the name; there is no root entry.
554
-
555
- | Module | Export | Input or role |
556
- | --- | --- | --- |
557
- | `@shivaedev/sql/clientOptions.ts` | `clientOptions(options, { snakeCase? })`, `SqlNameTransforms` | Add default native name transforms to driver or app host options; opt out with `snakeCase: false`. |
558
- | `@shivaedev/sql/repository.ts` | `makeRepository(model, options)` | `tableName`, scalar or composite `idColumn`, `spanPrefix`, optional `versionColumn`, `conflictColumns` and `conflictWhere`; requires native `SqlClient`. |
559
- | `@shivaedev/sql/repository.ts` | `OptimisticConcurrencyError` | Typed stale or missing-row failure for versioned writes, with `tableName`, `id`, `expectedVersion`, `reason: "missing" | "stale"`. |
560
- | `@shivaedev/sql/transact.ts` | `transact(effect, options)` or `effect.pipe(transact(options))` | Required `onSqlError` mapper; requires `SqlClient` and `Reactivity`. |
561
- | `@shivaedev/sql/transact.ts` | `invalidateOnCommit(keys)` | Records keys for its client, or invalidates immediately outside a transaction. |
562
- | `@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`. |
563
- | `@shivaedev/sql/migrationConvention.ts` | `MIGRATIONS_DIRECTORY`, `MIGRATION_FILE_PATTERN` | Application folder and four-digit sequential snake_case TypeScript filename pattern. |
564
- | `@shivaedev/sql/migrationsFromDirectory.ts` | `migrationsFromDirectory(root)` | Discover migrations under the application root with native `FileSystem` and `Path`. |
565
- | `@shivaedev/sql/migrations.ts` | `migratePostgres(options)` | Native loader, required `lockTimeout`, optional `components` and ledger `table`, default `effect_sql_migrations`. |
566
- | `@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. |
567
- | `@shivaedev/sql/pendingPostgresMigrations.ts` | `pendingPostgresMigrations({ loader, table?, components? })` | Pending ledger migrations as ID and name, and pending schema components as name and detail. |
568
- | `@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.
569
567
 
570
568
  ### Install and database setup
571
569
 
572
570
  ```sh
573
- pnpm add @shivaedev/sql effect@4.0.5 @effect/sql-pg@4.0.5
571
+ pnpm add @shivaedev/sql effect @effect/sql-pg
574
572
  ```
575
573
 
576
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`:
@@ -581,16 +579,13 @@ import { Layer, Redacted } from "effect";
581
579
  import * as Reactivity from "effect/reactivity/Reactivity";
582
580
  import { clientOptions } from "@shivaedev/sql/clientOptions.ts";
583
581
 
584
- const database = Layer.merge(
585
- PgClient.layer(clientOptions({ url: Redacted.make("postgresql://localhost/example") })),
586
- Reactivity.layer,
587
- );
582
+ const database = Layer.merge(PgClient.layer(clientOptions({ url: Redacted.make("postgresql://localhost/example") })), Reactivity.layer);
588
583
 
589
584
  const save = saveInvoiceLine({
590
- id: 1,
591
- name: "Consulting",
592
- amount: 52,
593
- note: null,
585
+ id: 1,
586
+ name: "Consulting",
587
+ amount: 52,
588
+ note: null,
594
589
  }).pipe(Effect.provide(database));
595
590
  ```
596
591
 
@@ -607,22 +602,20 @@ import { mapSqlError } from "@shivaedev/sql/mapSqlError.ts";
607
602
  import { transact } from "@shivaedev/sql/transact.ts";
608
603
 
609
604
  const onSqlError = mapSqlError({
610
- constraints: {
611
- "invoice_line_name_key": () => new NameTaken(),
612
- "invoice_line_invoice_fkey": () => new InvoiceMissing(),
613
- "invoice_line_amount_check": () => new InvalidAmount(),
614
- },
615
- fallback: (error) => new StorageUnavailable({ reason: error.message }),
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 }),
616
611
  });
617
612
 
618
- const insertNamedLine = lines.insert({ amount: 52, name: "Consulting", note: null }).pipe(
619
- transact({ onSqlError }),
620
- );
613
+ const insertNamedLine = lines.insert({ amount: 52, name: "Consulting", note: null }).pipe(transact({ onSqlError }));
621
614
  ```
622
615
 
623
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.
624
617
 
625
- 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](../../docs/framework/constraint-errors.md).
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).
626
619
 
627
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.
628
621
 
package/dist/transact.js CHANGED
@@ -25,7 +25,7 @@ const guard = Effect.fn("Transact.guard")(function* () {
25
25
  const invalidations = makeChannel({
26
26
  name: "@shivaedev/sql/transact",
27
27
  owner: Effect.map(SqlClient.SqlClient, (sql) => sql.transactionService),
28
- publish: (keys) => Reactivity.invalidate(keys),
28
+ publish: (keys) => Reactivity.Reactivity.use((reactivity) => Effect.sync(() => reactivity.invalidateUnsafe(keys))),
29
29
  unowned: guard(),
30
30
  });
31
31
  export const ChangeObserver = invalidations.Observer;
@@ -1 +1 @@
1
- {"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transact.js","sourceRoot":"","sources":["../src/transact.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shivaedev/sql",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Schema-derived repositories over native Effect SQL",
5
5
  "keywords": [
6
6
  "effect"
@@ -43,23 +43,24 @@
43
43
  "README.md"
44
44
  ],
45
45
  "dependencies": {
46
- "@shivaedev/changes": "0.5.0"
46
+ "@shivaedev/changes": "0.5.1"
47
47
  },
48
48
  "devDependencies": {
49
- "@effect/platform-node": "4.0.5",
50
- "@effect/sql-pg": "4.0.5",
51
- "@effect/sql-sqlite-node": "4.0.5",
52
- "@shivaedev/quality": "0.43.0",
53
- "@shivaedev/service": "0.5.0",
54
- "@shivaedev/test": "0.10.0",
55
- "@shivaedev/test-story": "0.22.0",
49
+ "@effect/platform-node": "4.0.6",
50
+ "@effect/sql-pg": "4.0.6",
51
+ "@effect/sql-sqlite-node": "4.0.6",
52
+ "@shivaedev/quality": "0.45.0",
53
+ "@shivaedev/service": "0.5.1",
54
+ "@shivaedev/test": "0.10.1",
55
+ "@shivaedev/test-sql": "0.11.0",
56
+ "@shivaedev/test-story": "0.24.0",
56
57
  "@types/node": "24.19.2",
57
- "effect": "4.0.5",
58
+ "effect": "4.0.6",
58
59
  "typescript": "7.0.2",
59
60
  "vitest": "5.0.5"
60
61
  },
61
62
  "peerDependencies": {
62
- "effect": "4.0.5"
63
+ "effect": "4.0.6"
63
64
  },
64
65
  "engines": {
65
66
  "node": ">=24"
package/src/transact.ts CHANGED
@@ -35,7 +35,7 @@ const guard = Effect.fn("Transact.guard")(function* () {
35
35
  const invalidations = makeChannel<unknown, SqlClient.SqlClient | Reactivity.Reactivity, SqlError>({
36
36
  name: "@shivaedev/sql/transact",
37
37
  owner: Effect.map(SqlClient.SqlClient, (sql) => sql.transactionService),
38
- publish: (keys) => Reactivity.invalidate(keys),
38
+ publish: (keys) => Reactivity.Reactivity.use((reactivity) => Effect.sync(() => reactivity.invalidateUnsafe(keys))),
39
39
  unowned: guard(),
40
40
  });
41
41