@kairos-es/store-postgres 1.0.0-alpha.3 → 1.0.0-alpha.5

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 (91) hide show
  1. package/README.md +61 -29
  2. package/dist/cjs/clients.js +34 -24
  3. package/dist/cjs/clients.js.map +1 -1
  4. package/dist/cjs/config.js +9 -15
  5. package/dist/cjs/config.js.map +1 -1
  6. package/dist/cjs/ensureSchema.js +8 -3
  7. package/dist/cjs/ensureSchema.js.map +1 -1
  8. package/dist/cjs/index.js.map +1 -1
  9. package/dist/cjs/internal/ddl.js +29 -12
  10. package/dist/cjs/internal/ddl.js.map +1 -1
  11. package/dist/cjs/internal/matchSql.js +57 -4
  12. package/dist/cjs/internal/matchSql.js.map +1 -1
  13. package/dist/cjs/internal/readPlan.js +33 -5
  14. package/dist/cjs/internal/readPlan.js.map +1 -1
  15. package/dist/cjs/internal/transport.js +42 -15
  16. package/dist/cjs/internal/transport.js.map +1 -1
  17. package/dist/cjs/store.js +30 -25
  18. package/dist/cjs/store.js.map +1 -1
  19. package/dist/dts-cjs/clients.d.ts +66 -0
  20. package/dist/dts-cjs/clients.d.ts.map +1 -0
  21. package/dist/{dts → dts-cjs}/config.d.ts +9 -19
  22. package/dist/{dts → dts-cjs}/config.d.ts.map +1 -1
  23. package/dist/{dts → dts-cjs}/ensureSchema.d.ts +8 -3
  24. package/dist/dts-cjs/ensureSchema.d.ts.map +1 -0
  25. package/dist/{dts → dts-cjs}/index.d.ts +4 -3
  26. package/dist/dts-cjs/index.d.ts.map +1 -0
  27. package/dist/{dts → dts-cjs}/internal/ddl.d.ts +1 -1
  28. package/dist/{dts → dts-cjs}/internal/ddl.d.ts.map +1 -1
  29. package/dist/{dts → dts-cjs}/internal/matchSql.d.ts +26 -2
  30. package/dist/dts-cjs/internal/matchSql.d.ts.map +1 -0
  31. package/dist/{dts → dts-cjs}/internal/readPlan.d.ts +1 -1
  32. package/dist/{dts → dts-cjs}/internal/readPlan.d.ts.map +1 -1
  33. package/dist/{dts → dts-cjs}/internal/transport.d.ts +34 -19
  34. package/dist/dts-cjs/internal/transport.d.ts.map +1 -0
  35. package/dist/dts-cjs/package.json +3 -0
  36. package/dist/dts-cjs/store.d.ts.map +1 -0
  37. package/dist/dts-esm/clients.d.ts +66 -0
  38. package/dist/dts-esm/clients.d.ts.map +1 -0
  39. package/dist/dts-esm/config.d.ts +48 -0
  40. package/dist/dts-esm/config.d.ts.map +1 -0
  41. package/dist/dts-esm/ensureSchema.d.ts +40 -0
  42. package/dist/dts-esm/ensureSchema.d.ts.map +1 -0
  43. package/dist/dts-esm/index.d.ts +42 -0
  44. package/dist/dts-esm/index.d.ts.map +1 -0
  45. package/dist/dts-esm/internal/ddl.d.ts +174 -0
  46. package/dist/dts-esm/internal/ddl.d.ts.map +1 -0
  47. package/dist/dts-esm/internal/matchSql.d.ts +164 -0
  48. package/dist/dts-esm/internal/matchSql.d.ts.map +1 -0
  49. package/dist/dts-esm/internal/readPlan.d.ts +72 -0
  50. package/dist/dts-esm/internal/readPlan.d.ts.map +1 -0
  51. package/dist/dts-esm/internal/transport.d.ts +131 -0
  52. package/dist/dts-esm/internal/transport.d.ts.map +1 -0
  53. package/dist/dts-esm/package.json +3 -0
  54. package/dist/dts-esm/store.d.ts +17 -0
  55. package/dist/dts-esm/store.d.ts.map +1 -0
  56. package/dist/esm/clients.js +34 -24
  57. package/dist/esm/clients.js.map +1 -1
  58. package/dist/esm/config.js +9 -15
  59. package/dist/esm/config.js.map +1 -1
  60. package/dist/esm/ensureSchema.js +8 -3
  61. package/dist/esm/ensureSchema.js.map +1 -1
  62. package/dist/esm/index.js +4 -3
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/internal/ddl.js +30 -13
  65. package/dist/esm/internal/ddl.js.map +1 -1
  66. package/dist/esm/internal/matchSql.js +55 -2
  67. package/dist/esm/internal/matchSql.js.map +1 -1
  68. package/dist/esm/internal/readPlan.js +32 -5
  69. package/dist/esm/internal/readPlan.js.map +1 -1
  70. package/dist/esm/internal/transport.js +42 -15
  71. package/dist/esm/internal/transport.js.map +1 -1
  72. package/dist/esm/store.js +30 -25
  73. package/dist/esm/store.js.map +1 -1
  74. package/package.json +13 -11
  75. package/src/clients.ts +43 -31
  76. package/src/config.ts +12 -13
  77. package/src/ensureSchema.ts +8 -3
  78. package/src/index.ts +4 -3
  79. package/src/internal/ddl.ts +33 -13
  80. package/src/internal/matchSql.ts +63 -1
  81. package/src/internal/readPlan.ts +11 -6
  82. package/src/internal/transport.ts +71 -24
  83. package/src/store.ts +35 -28
  84. package/dist/dts/clients.d.ts +0 -34
  85. package/dist/dts/clients.d.ts.map +0 -1
  86. package/dist/dts/ensureSchema.d.ts.map +0 -1
  87. package/dist/dts/index.d.ts.map +0 -1
  88. package/dist/dts/internal/matchSql.d.ts.map +0 -1
  89. package/dist/dts/internal/transport.d.ts.map +0 -1
  90. package/dist/dts/store.d.ts.map +0 -1
  91. /package/dist/{dts → dts-cjs}/store.d.ts +0 -0
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Single source of truth for the tag-first match SQL.
3
+ *
4
+ * The servable-query matching semantics — the tagged AND-superset match with
5
+ * distinct-tag counting, the allowed-types filter, the exclusive `after` bound —
6
+ * were hand-duplicated between the TS read path (`store.ts`) and the plpgsql
7
+ * conflict check (`ddl.ts`): two SQL dialect-copies, in two files, that drift
8
+ * independently (the read's wildcard mis-routing of a degenerate empty item was
9
+ * one such drift). This module emits the shared fragments ONCE, parameterised by:
10
+ *
11
+ * - the PLACEHOLDER TOKENS each call site binds — plpgsql parameter names
12
+ * (`query_items`, `COALESCE(after_id, 0)`) for the conflict body vs bound
13
+ * `$n` placeholders for the runtime `sql.unsafe(text, params)` read; and
14
+ * - the PROJECTION — full rows (read) vs an `EXISTS`/`LIMIT 1` probe (conflict),
15
+ * which each call site wraps around the shared `qualifiedRows` source.
16
+ *
17
+ * The `jsonb_to_recordset` event column list also lives here as ONE descriptor,
18
+ * so the write recordset (`ddl.ts`) and the read projection cannot disagree.
19
+ *
20
+ * Table names arrive PRE-QUOTED (via `quoteQualified` in `ddl.ts`), so this
21
+ * module never quotes identifiers itself and stays free of the naming logic.
22
+ * Every fragment assumes the main events table is aliased `m`.
23
+ */
24
+ /**
25
+ * The event payload columns shared by the write recordset and every read
26
+ * projection, as `[name, recordsetType]` pairs. `id` (the `bigserial` position)
27
+ * is prepended by the read projection and absent from the events recordset.
28
+ *
29
+ * `as const` freezes it into a tuple of literal names so `EventColumnName` (below)
30
+ * can derive the exact column-name union — the whole point of the "one descriptor"
31
+ * story: the TS transport shapes in `internal/transport.ts` are mapped types over
32
+ * that union, so a column rename here is a COMPILE error there, not a silent drift
33
+ * the lax read boundary (ADR-0006) would only surface as corrupt data under test.
34
+ */
35
+ declare const EVENT_COLUMNS: readonly [readonly ["type", "text"], readonly ["data", "text"], readonly ["tags", "text[]"], readonly ["uuid", "text"], readonly ["occurred_at", "timestamptz"], readonly ["metadata", "jsonb"]];
36
+ /**
37
+ * The event payload column NAMES as a string-literal union, derived from the ONE
38
+ * `EVENT_COLUMNS` descriptor. `internal/transport.ts` builds its transport shapes
39
+ * (`EventJson`, the JSON sent to the append functions; `EventRow`, a raw driver
40
+ * row) as mapped types over this union, so the transport shapes cannot drift from
41
+ * the SQL recordset/projection — a rename in the descriptor removes the old key
42
+ * from the union and makes the mapped type fail to compile.
43
+ */
44
+ export type EventColumnName = (typeof EVENT_COLUMNS)[number][0];
45
+ /**
46
+ * The `jsonb_to_recordset(new_events) AS (…)` column definitions —
47
+ * `type text, data text, tags text[], uuid text, occurred_at timestamptz,
48
+ * metadata jsonb`. The
49
+ * transport `data` is base64 TEXT (restored to `bytea` by `decode(…, 'base64')`
50
+ * at insert), which is why it is `text` here, not `bytea`.
51
+ */
52
+ export declare const eventRecordsetColumns: string;
53
+ /**
54
+ * The read projection column list (`id` + the event columns), optionally
55
+ * prefixed with a table alias (`'m.'`). Sourced from the one descriptor so it
56
+ * cannot drift from the recordset.
57
+ */
58
+ export declare const readColumns: (prefix?: string) => string;
59
+ /**
60
+ * The alias the append body gives the expanded `new_events` recordset — the `ne`
61
+ * CTE in `ddl.ts` — which every expression in `EVENT_COLUMN_SOURCE` reads from.
62
+ * Exported so the CTE is declared under the one name its readers assume.
63
+ */
64
+ export declare const RECORDSET_ALIAS = "ne";
65
+ /**
66
+ * The bare event column names — `type, data, tags, uuid, occurred_at, metadata` —
67
+ * which the append body uses twice: as the recordset CTE's projection and as the
68
+ * `INSERT INTO t (…)` target list.
69
+ */
70
+ export declare const eventColumnList: string;
71
+ /**
72
+ * The append insert's SELECT list: one source expression per column, in the SAME
73
+ * ORDER as `eventColumnList`.
74
+ *
75
+ * The two halves of `INSERT INTO t (a, b) SELECT x, y` agree POSITION BY POSITION
76
+ * and nothing checks that they do: two columns of compatible type swap without an
77
+ * error at any layer and the data lands in the wrong one. Both halves are therefore
78
+ * one walk of the one descriptor, so the append body has no second ordering to keep
79
+ * in step and no hand-written list for a new column to be missing from.
80
+ */
81
+ export declare const eventInsertSelectList: string;
82
+ /**
83
+ * The allowed-types filter for a candidate row aliased `m`: an item with no
84
+ * types matches ANY type; otherwise the event's type must be one of them.
85
+ * `typesCol` is the `text[]` column holding the item's types — `q.allowed_types`
86
+ * for a tagged match, `qi.types` for a tagless one.
87
+ */
88
+ export declare const allowedTypesPredicate: (typesCol: string) => string;
89
+ /** The placeholder tokens + pre-quoted table names one call site binds. */
90
+ export interface MatchTokens {
91
+ /** SQL yielding the query-items jsonb: `query_items` or `$1::jsonb`. */
92
+ readonly queryItemsExpr: string;
93
+ /** SQL for the exclusive lower bound: `COALESCE(after_id, 0)` or `$2`. */
94
+ readonly afterExpr: string;
95
+ /** Pre-quoted main events table. */
96
+ readonly main: string;
97
+ /** Pre-quoted tag junction table. */
98
+ readonly tag: string;
99
+ }
100
+ /**
101
+ * The tag-first match CTE chain shared by the read and conflict paths: expand
102
+ * the query items (`qi`), join the tag junction, count DISTINCT matched tags per
103
+ * (event, item), and keep events matching an item's FULL tag superset. Returns
104
+ * `qi, initial_matches, matched_groups, qualified_ids` — the caller follows it
105
+ * with a projection CTE selecting from `qualified_ids`.
106
+ *
107
+ * ## Both sides of the count are SET SIZES, and that is the whole invariant
108
+ *
109
+ * `qualified_ids` compares two numbers, and the ONE thing an edit here must
110
+ * preserve is that they count the same kind of thing: `matched_tag_count` is the
111
+ * number of DISTINCT tags the event carried out of the item's requirement, so
112
+ * `required_tag_count` is the size of that requirement AS A SET and never the
113
+ * length of the list it arrived as. A query item may list one tag twice — no
114
+ * constructor de-duplicates, `assertServableQuery` admits it and
115
+ * `classifyServableQuery` routes it here — and the in-memory oracle folds an
116
+ * item's tags into a superset test that a repeat cannot change. Comparing a
117
+ * de-duplicated 1 against a list length of 2 refused such a match: the READ
118
+ * returned nothing and, because the plpgsql conflict body composes this same
119
+ * chain, the GUARD found no conflict and the append it should have refused
120
+ * succeeded — an optimistic-concurrency check that escaped silently.
121
+ *
122
+ * The asymmetry has two repairs — de-duplicate both sides, or expand both — and
123
+ * only the first is available here. The matched side CANNOT become a plain row
124
+ * count, because this engine's junction holds one row per (tag, event) OCCURRENCE
125
+ * and not per pair, so an event whose own `tags` list repeats a tag has two rows
126
+ * there and a row count would refuse a single-tag item against it. `ddl.ts` states
127
+ * that property on the junction, and it is what `COUNT(DISTINCT tag)` is here to
128
+ * absorb; the requirement is de-duplicated to meet it.
129
+ *
130
+ * `required_tag_count` is therefore computed in `qi`, once per ITEM, rather than
131
+ * in the group that consumes it — the chain groups per (event, item), so a
132
+ * subquery in `matched_groups` would be re-evaluated per matched event. It is
133
+ * `count(*)` over a `SELECT DISTINCT` rather than `count(DISTINCT …)` because the
134
+ * latter drops a NULL element: a `[tag, NULL]` requirement would shrink to 1,
135
+ * meet a single matched tag and match, where the oracle's superset test cannot be
136
+ * satisfied by a NULL and refuses it.
137
+ *
138
+ * The `array_length(qi.tags, 1) >= 1` guard excludes tagless items from the
139
+ * tagged branch (load-bearing for the conflict body, which also has a separate
140
+ * tagless branch; a no-op for the read, whose tagged shape is all-tagged). It
141
+ * reads the item's own array because it is a test of the item's SHAPE — tagged or
142
+ * tagless — and not part of the match arithmetic above.
143
+ */
144
+ export declare const taggedMatchChain: (tokens: MatchTokens) => string;
145
+ /**
146
+ * The shared qualified-rows source: join `qualified_ids` back to the main table
147
+ * (aliased `m`), re-apply the exclusive `after` bound, and filter by allowed
148
+ * types. The read wraps this in an ordered/limited `DISTINCT m.id` CTE; the
149
+ * conflict wraps it in a `SELECT 1 … LIMIT 1` existence probe.
150
+ */
151
+ export declare const qualifiedRows: (tokens: Pick<MatchTokens, "main" | "afterExpr">) => string;
152
+ /** The wildcard read (no items): every event after the bound, ordered/limited. */
153
+ export declare const wildcardReadSql: (main: string) => string;
154
+ /** The type-only read (single tagless item): filter by `type = ANY($2)`. */
155
+ export declare const typeOnlyReadSql: (main: string) => string;
156
+ /**
157
+ * The tagged read: the shared match chain, then a `filtered_ids` CTE that orders
158
+ * and limits the qualified ids by position, then a full-row select. Bound params
159
+ * are `$1` = query-items jsonb, `$2` = exclusive `after`, `$3` = limit (`NULL` =
160
+ * no limit).
161
+ */
162
+ export declare const taggedReadSql: (main: string, tag: string) => string;
163
+ export {};
164
+ //# sourceMappingURL=matchSql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"matchSql.d.ts","sourceRoot":"","sources":["../../../src/internal/matchSql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH;;;;;;;;;;GAUG;AACH,QAAA,MAAM,aAAa,kMASlB,CAAA;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAA;AAE/D;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAExB,CAAA;AAEZ;;;;GAIG;AACH,eAAO,MAAM,WAAW,GAAI,eAAW,KAAG,MAGvC,CAAA;AAEH;;;;GAIG;AACH,eAAO,MAAM,eAAe,OAAO,CAAA;AAgCnC;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,MAE7B,CAAA;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAExB,CAAA;AAEZ;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,MACkD,CAAA;AAE3G,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,wEAAwE;IACxE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAA;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,eAAO,MAAM,gBAAgB,GAAI,QAAQ,WAAW,KAAG,MA6BnD,CAAA;AAEJ;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GACxB,QAAQ,IAAI,CAAC,WAAW,EAAE,MAAM,GAAG,WAAW,CAAC,KAC9C,MAImD,CAAA;AAEtD,kFAAkF;AAClF,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAKnC,CAAA;AAEZ,4EAA4E;AAC5E,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAKnC,CAAA;AAEZ;;;;;GAKG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,MAAM,EAAE,KAAK,MAAM,KAAG,MAWtC,CAAA"}
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The read-plan compiler for the Postgres engine, extracted from `store.ts` so it
3
+ * is a self-contained unit depending only on the PRE-QUOTED table names — never on
4
+ * the SQL clients. `store.ts`'s `read` and its `subscribe` — the latter feeding the
5
+ * plan to core's subscribe machine as that machine's `fetch` seam — both compile a
6
+ * plan here and run it against whichever client they hold.
7
+ *
8
+ * `compileFetchPlan` turns a servable `Query` into prepared SQL text plus a
9
+ * cursor-only `bind`, so a long-lived subscription re-binds only the cursor rather
10
+ * than re-dispatching the shape and re-serialising the query JSON on every poll.
11
+ * Which shape a query IS is not decided here: core's `classifyServableQuery` owns
12
+ * that dispatch beside the grammar it dispatches over (`Query.ts`), including the
13
+ * degenerate empty item's routing. The JSON/JSONB transport shapes and
14
+ * (de)serialisers — `EventRow`, `jsonbText`, `toQueryItemsJson`, `rowToSequenced`
15
+ * — live in `internal/transport.ts`, and this module only compiles plans.
16
+ *
17
+ * A plan compiled here is therefore the one place the Postgres engine dispatches
18
+ * a shape in TypeScript, and the exhaustiveness below protects BOTH verbs that
19
+ * compile one — `read`, and the `subscribe` poll sharing its plan. What it cannot
20
+ * reach is the append: its conflict guard covers the same three shapes inside its
21
+ * plpgsql body, where no compiler follows, and `ddl.ts` owns what that rests on
22
+ * instead.
23
+ */
24
+ import type { Position, ReadLimit } from '@kairos-es/core';
25
+ import { type Query } from '@kairos-es/core';
26
+ import type { Effect } from 'effect';
27
+ import type { SqlClient, SqlError } from 'effect/sql';
28
+ import { type EventRow } from './transport.js';
29
+ /** The pre-quoted table names a plan reads from (see `quoteQualified`). */
30
+ export interface ReadTables {
31
+ /** Pre-quoted main events table. */
32
+ readonly main: string;
33
+ /** Pre-quoted tag junction table. */
34
+ readonly tag: string;
35
+ }
36
+ /**
37
+ * A COMPILED read plan: the prepared SQL text plus a `bind` that produces the
38
+ * positional params for a given `after`/`limit`. Everything
39
+ * query-dependent-but-cursor-independent — the shape dispatch, the SQL text, the
40
+ * query-items JSON serialisation — is done ONCE, so a long-lived subscription's
41
+ * poll loop re-binds only the cursor rather than re-classifying and re-JSON-
42
+ * serialising on every iteration.
43
+ */
44
+ export interface FetchPlan {
45
+ readonly text: string;
46
+ readonly bind: (after: Position, limit: ReadLimit | null) => ReadonlyArray<unknown>;
47
+ }
48
+ /**
49
+ * Compile a servable query into a `FetchPlan`. The SQL comes from the shared
50
+ * `matchSql` builders (the tagged path composes the same emitted match chain as the
51
+ * conflict check, which binds its own placeholder tokens and wraps it differently),
52
+ * with the pre-quoted names embedded and the VALUES bound as `$n` parameters
53
+ * (never interpolated).
54
+ *
55
+ * The `switch` is exhaustive over core's `ServableQueryShape` with NO `default`
56
+ * and no trailing fall-through arm, and what that buys is a COMPILE error where
57
+ * there would otherwise be a silent mis-serve. Core owns that union and this
58
+ * module only consumes it, so a fourth member added there is a change this file
59
+ * cannot see coming: a cascade ending in an unguarded tagged branch would have
60
+ * SERVED that shape as tagged, and both `read` and the `subscribe` poll sharing
61
+ * its plan would have returned the wrong rows — no error raised, nothing failing
62
+ * to typecheck. Every arm returns instead, so the added member leaves this
63
+ * function with a reachable end and a return type that does not admit
64
+ * `undefined`, and the module stops compiling.
65
+ */
66
+ export declare const compileFetchPlan: (tables: ReadTables, query: Query) => FetchPlan;
67
+ /**
68
+ * Run a compiled plan for a given `after`/`limit`, honouring `after` (exclusive;
69
+ * absent → `0`) and `limit` (`NULL` = no limit), ordered by `id ASC`.
70
+ */
71
+ export declare const runFetch: (sql: SqlClient.SqlClient, plan: FetchPlan, after: Position | undefined, limit: ReadLimit | undefined) => Effect.Effect<ReadonlyArray<EventRow>, SqlError.SqlError>;
72
+ //# sourceMappingURL=readPlan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"readPlan.d.ts","sourceRoot":"","sources":["../../../src/internal/readPlan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAiC,KAAK,KAAK,EAAE,MAAM,iBAAiB,CAAA;AAC3E,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AACpC,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAErD,OAAO,EAAE,KAAK,QAAQ,EAA+B,MAAM,aAAa,CAAA;AAExE,2EAA2E;AAC3E,MAAM,WAAW,UAAU;IACzB,oCAAoC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,qCAAqC;IACrC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CACrB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,CACb,KAAK,EAAE,QAAQ,EACf,KAAK,EAAE,SAAS,GAAG,IAAI,KACpB,aAAa,CAAC,OAAO,CAAC,CAAA;CAC5B;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,QAAQ,UAAU,EAClB,OAAO,KAAK,KACX,SAiCF,CAAA;AAED;;;GAGG;AACH,eAAO,MAAM,QAAQ,GACnB,KAAK,SAAS,CAAC,SAAS,EACxB,MAAM,SAAS,EACf,OAAO,QAAQ,GAAG,SAAS,EAC3B,OAAO,SAAS,GAAG,SAAS,KAC3B,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,QAAQ,CACiB,CAAA"}
@@ -0,0 +1,131 @@
1
+ /**
2
+ * The JSON/JSONB transport for the Postgres engine: the wire shapes and the
3
+ * (de)serialisers that carry an event batch / query items ACROSS the boundary into
4
+ * the append functions, and map a raw driver row BACK to a core `SequencedEvent`.
5
+ * Extracted from `store.ts` (the write side) and `internal/readPlan.ts` (the read
6
+ * side) so the transport concept lives in ONE module rather than being split
7
+ * across the write path and the read-plan compiler — the module name now predicts
8
+ * where transport code lives, and the two per-column type maps (`EventJson`,
9
+ * `EventRow`) sit next to each other rather than in two files.
10
+ *
11
+ * The event column contract stays single-sourced on `EventColumnName`
12
+ * (`internal/matchSql.ts`), the one column-NAME descriptor: both type maps here
13
+ * are mapped types over it, so a column rename in `EVENT_COLUMNS` is a COMPILE
14
+ * error in this module, never a silent transport drift the lax read boundary
15
+ * (ADR-0006) would only surface as corrupt data under test.
16
+ *
17
+ * The read boundary (`rowToSequenced`) is the trusted, deliberately-lax
18
+ * deserialisation point (ADR-0006 store-laxness); the write boundary
19
+ * (`toEventJson`) serialises `data` as base64 text and domain time as an ISO
20
+ * string, both restored in SQL.
21
+ */
22
+ import type { DcbEvent, Metadata, Query, SequencedEvent } from '@kairos-es/core';
23
+ import type { EventColumnName } from './matchSql.js';
24
+ /**
25
+ * Serialise a value to a JSON STRING for transport as a `::jsonb`-cast bound
26
+ * parameter.
27
+ *
28
+ * WHY a string and not `sql.json(value)`: the read plan runs through
29
+ * `sql.unsafe(text, params)` with POSITIONAL parameters, and a positional
30
+ * parameter must be a plain scalar or a flat array of scalars — `@effect/sql-pg`
31
+ * cannot infer a Postgres type for an object or an array of objects, and a
32
+ * `sql.json` fragment is not a value that list can carry. A pre-stringified
33
+ * JSON string binds untyped and the `::jsonb` cast has Postgres parse it, which
34
+ * round-trips reliably for both objects and arrays on the positional path and
35
+ * the tagged-template path alike. Kept as a named helper so every
36
+ * JSON-transport call site is consistent and the reasoning lives in one place.
37
+ */
38
+ export declare const jsonbText: (value: unknown) => string;
39
+ /** The JSON shape of one query item: types OR-filter, tags AND-superset. */
40
+ export interface QueryItemJson {
41
+ readonly types: ReadonlyArray<string>;
42
+ readonly tags: ReadonlyArray<string>;
43
+ }
44
+ /** Serialise a `Query`'s items into their transport JSON shape. */
45
+ export declare const toQueryItemsJson: (query: Query) => ReadonlyArray<QueryItemJson>;
46
+ /**
47
+ * The per-column TS types of the transport JSON shape — one entry per
48
+ * `EVENT_COLUMNS` name. `data` is base64 TEXT and `occurred_at` an ISO string
49
+ * (both restored in SQL: `decode(…, 'base64')` / `timestamptz`); `metadata`
50
+ * travels as a nested JSON object, which `jsonb_to_recordset` hands to the
51
+ * insert as `jsonb` untouched; the rest map straight across. Kept as a plain
52
+ * map so `EventJson` below is a mapped type over the ONE column descriptor
53
+ * rather than a hand-restated column list.
54
+ */
55
+ interface EventJsonColumns {
56
+ readonly type: string;
57
+ readonly data: string;
58
+ readonly tags: ReadonlyArray<string>;
59
+ readonly uuid: string;
60
+ readonly occurred_at: string;
61
+ readonly metadata: Metadata;
62
+ }
63
+ /**
64
+ * The JSON shape of one event as sent to the append functions, as a mapped type
65
+ * over the shared `EventColumnName` descriptor (`internal/matchSql.ts`). `data`
66
+ * is base64 text (restored to `bytea` by `decode(…, 'base64')` in plpgsql);
67
+ * `occurred_at` is an ISO string parsed by `timestamptz`. Because the keys come
68
+ * from the same descriptor that builds the `jsonb_to_recordset(new_events)`
69
+ * column list, the two cannot drift — rename a column in `EVENT_COLUMNS` and this
70
+ * mapped type stops compiling (the old key is gone from `EventJsonColumns`).
71
+ */
72
+ export type EventJson = {
73
+ readonly [K in EventColumnName]: EventJsonColumns[K];
74
+ };
75
+ /** Serialise one core `DcbEvent` into its transport JSON shape. */
76
+ export declare const toEventJson: (event: DcbEvent) => EventJson;
77
+ /**
78
+ * The per-column TS types of a raw driver row — one entry per `EVENT_COLUMNS`
79
+ * name. These are the JS types `@effect/sql-pg`'s default binary codecs decode
80
+ * to: `data` a `Uint8Array` (or null for a payload-less event), `occurred_at` a
81
+ * JS `Date`, `tags` a `string[]`, `metadata` whatever the driver parses a
82
+ * `jsonb` value into — declared
83
+ * `unknown`, because `jsonb` is the one column here whose SQL type does not
84
+ * fix its JS shape, and `metadataFrom` below is where it is checked. `id`
85
+ * (the `bigserial` position) is NOT here — it is prepended by the read projection
86
+ * and absent from the events recordset, so it sits outside the shared descriptor.
87
+ */
88
+ interface EventRowColumns {
89
+ readonly type: string;
90
+ readonly data: Uint8Array | null;
91
+ readonly tags: ReadonlyArray<string>;
92
+ readonly uuid: string;
93
+ readonly occurred_at: Date;
94
+ readonly metadata: unknown;
95
+ }
96
+ /**
97
+ * A raw row from the main table as the driver returns it, as `id` plus a
98
+ * mapped type over the shared `EventColumnName` descriptor. `id` is `int8`, which
99
+ * the driver's default codec hands back as a `bigint` (JS `number` is unsafe past
100
+ * 2^53). `rowToSequenced` still passes it through `BigInt(…)`, so a client
101
+ * configured with its own `int8` codec that yields the decimal string instead
102
+ * still mints a real `bigint` position.
103
+ * The payload columns cannot drift from the read projection: a rename in
104
+ * `EVENT_COLUMNS` breaks this mapped type.
105
+ */
106
+ export type EventRow = {
107
+ readonly id: bigint | string;
108
+ } & {
109
+ readonly [K in EventColumnName]: EventRowColumns[K];
110
+ };
111
+ /**
112
+ * Map a raw main-table row to a core `SequencedEvent`. `data` NULL → empty bytes
113
+ * (a payload-less event round-trips to a zero-length `Uint8Array`); `occurred_at`
114
+ * Date → `DateTime.Utc`; `id` → branded `Position`; `metadata` through
115
+ * `metadataFrom`. The column is `NOT NULL DEFAULT '{}'`, so there is no absent
116
+ * case to reconcile: an event appended with an empty bag reads back as `{}` on
117
+ * this engine exactly as on the in-memory oracle.
118
+ *
119
+ * The branded scalars (`type`, `tags`, `uuid`, `position`) are minted by trusted
120
+ * `as`-cast, NOT by re-running `EventType.make` / `Tag.make` etc. This is
121
+ * deliberate: these bytes were validated at write time, and the store schema is
122
+ * intentionally LAX on read (ADR-0006 store-laxness — hand-assembled or pre-ADR
123
+ * events must remain readable), so re-validating here could REJECT a legitimately
124
+ * stored value and break that contract. It mirrors the in-memory oracle, which
125
+ * likewise mints branded positions by cast at its trusted allocation boundary
126
+ * (`inMemory.ts`). The read boundary is the analogous trusted deserialisation
127
+ * point.
128
+ */
129
+ export declare const rowToSequenced: (row: EventRow) => SequencedEvent;
130
+ export {};
131
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../../src/internal/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,OAAO,KAAK,EACV,QAAQ,EACR,QAAQ,EAER,KAAK,EACL,cAAc,EACf,MAAM,iBAAiB,CAAA;AAGxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEjD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,GAAI,OAAO,OAAO,KAAG,MAA+B,CAAA;AAE1E,4EAA4E;AAC5E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;CACrC;AAED,mEAAmE;AACnE,eAAO,MAAM,gBAAgB,GAAI,OAAO,KAAK,KAAG,aAAa,CAAC,aAAa,CAItE,CAAA;AAEL;;;;;;;;GAQG;AACH,UAAU,gBAAgB;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,EAAE,CAAC,IAAI,eAAe,GAAG,gBAAgB,CAAC,CAAC,CAAC;CAAE,CAAA;AAEhF,mEAAmE;AACnE,eAAO,MAAM,WAAW,GAAI,OAAO,QAAQ,KAAG,SAW5C,CAAA;AAEF;;;;;;;;;;GAUG;AACH,UAAU,eAAe;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI,CAAA;IAChC,QAAQ,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,WAAW,EAAE,IAAI,CAAA;IAC1B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAC3B;AAED;;;;;;;;;GASG;AACH,MAAM,MAAM,QAAQ,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG;IACxD,QAAQ,EAAE,CAAC,IAAI,eAAe,GAAG,eAAe,CAAC,CAAC,CAAC;CACpD,CAAA;AAuBD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,QAAQ,KAAG,cAU7C,CAAA"}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -0,0 +1,17 @@
1
+ import { DcbEventStore } from '@kairos-es/core';
2
+ import { Layer } from 'effect';
3
+ import { PgClientDirect, PgClientPooled } from './clients.js';
4
+ import { type PostgresStoreConfig } from './config.js';
5
+ /**
6
+ * The Postgres `DcbEventStore` layer factory. Builds the engine over
7
+ * `PgClientPooled` (reads/append/notify) and `PgClientDirect` (the subscribe
8
+ * listener). The caller wires those two client layers — pointing `PgClientDirect`
9
+ * at a direct (non-pooler) endpoint on Neon, or at the same URL as `PgClientPooled`
10
+ * everywhere else.
11
+ *
12
+ * `ensureSchema(config)` must have been run (by the caller's migrator or at
13
+ * boot) before the layer is used; the layer itself performs no DDL, so it stays
14
+ * a pure runtime dependency requiring only the two clients.
15
+ */
16
+ export declare const layer: (config?: PostgresStoreConfig) => Layer.Layer<DcbEventStore, never, PgClientPooled | PgClientDirect>;
17
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/store.ts"],"names":[],"mappings":"AAiDA,OAAO,EAGL,aAAa,EAQd,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAA6B,KAAK,EAAsB,MAAM,QAAQ,CAAA;AAE7E,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAC1D,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,UAAU,CAAA;AA2U9E;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GAChB,SAAQ,mBAAwB,KAC/B,KAAK,CAAC,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,cAAc,GAAG,cAAc,CASnE,CAAA"}
@@ -1,51 +1,61 @@
1
1
  /**
2
2
  * The TWO store-owned Postgres clients — the "Neon trap" mitigation (ADR-0002).
3
3
  *
4
- * `@effect/sql-pg` exposes a single `PgClient` (and `SqlClient`) tag; a
4
+ * `@effect/sql-pg` exposes a single `PgClient` (and `SqlClient`) service; a
5
5
  * `PgClient.layer` claims BOTH. If we built two `PgClient.layer`s they would
6
- * collide on that one tag — the second would shadow the first. So instead we
7
- * define two DISTINCT `Context.Tag`s, each HOLDING a `PgClient` value:
6
+ * collide on that one key — the second would shadow the first. So instead we
7
+ * define two DISTINCT `Context.Service`s, each HOLDING a `PgClient` value:
8
8
  *
9
9
  * - `PgClientPooled` — the pooled client used for reads, the append function,
10
- * and `notify`. Backed by a `pg.Pool`; connections are recycled per query.
10
+ * and `notify`. Its statements borrow a connection from the client's pool per
11
+ * query.
11
12
  * - `PgClientDirect` — a client used ONLY by `subscribe`'s `listen`. Its
12
- * built-in `listen` constructs a STANDALONE `pg.Client` from the pool's
13
- * `options`, inheriting the pool's host. On a Neon POOLED endpoint
14
- * (`…-pooler…`) that standalone connection lands on the PgBouncer front door,
15
- * which SILENTLY DROPS `LISTEN`/`NOTIFY` — the subscription would simply never
13
+ * built-in `listen` RESERVES a connection from that client's own pool and
14
+ * holds it for the life of the subscription, so the listening session lives
15
+ * on whatever host the client was configured with. On a Neon POOLED endpoint
16
+ * (`…-pooler…`) that session is a PgBouncer front-door connection, which
17
+ * SILENTLY DROPS `LISTEN`/`NOTIFY` — the subscription would simply never
16
18
  * wake. Pointing `PgClientDirect` at Neon's DIRECT (non-pooler) endpoint fixes
17
19
  * it. `subscribe` is the ONLY method that touches `PgClientDirect`.
18
20
  *
19
- * For a plain (non-Neon) Postgres both tags point at the same URL; the split
20
- * costs nothing there. The two-tag design is what makes the Neon fix a
21
+ * Because each live subscription holds one of `PgClientDirect`'s pooled
22
+ * connections until it closes, that client's `maxConnections` is also the number
23
+ * of subscriptions that can be listening at once. A subscription past it waits
24
+ * for a connection on its wake fibre only — delivery still proceeds at the poll
25
+ * rate meanwhile (see `store.ts`) — so size the direct client for the expected
26
+ * subscriber count rather than for query throughput.
27
+ *
28
+ * For a plain (non-Neon) Postgres both services point at the same URL; the split
29
+ * costs nothing there. The two-service design is what makes the Neon fix a
21
30
  * deployment-time wiring choice rather than a code change.
22
31
  */
23
- import { Reactivity } from '@effect/experimental';
24
32
  import { PgClient } from '@effect/sql-pg';
25
33
  import { Context, Layer } from 'effect';
34
+ import { Reactivity } from 'effect/reactivity';
26
35
  /**
27
- * The pooled client tag: reads, the append function, and `notify`. Holds a
36
+ * The pooled client service: reads, the append function, and `notify`. Holds a
28
37
  * `PgClient` value rather than being a `PgClient.layer`, so it never collides
29
- * with the direct client on the shared `PgClient`/`SqlClient` tag.
38
+ * with the direct client on the shared `PgClient`/`SqlClient` key.
30
39
  */
31
- export class PgClientPooled extends /*#__PURE__*/Context.Tag('@kairos-es/store-postgres/PgClientPooled')() {}
40
+ export class PgClientPooled extends /*#__PURE__*/Context.Service()('@kairos-es/store-postgres/PgClientPooled') {}
32
41
  /**
33
- * The direct (non-pooler) client tag: the `subscribe` listener ONLY. Point this
34
- * at a direct endpoint on Neon so `LISTEN`/`NOTIFY` is not dropped by the pooler.
42
+ * The direct (non-pooler) client service: the `subscribe` listener ONLY. Point
43
+ * this at a direct endpoint on Neon so `LISTEN`/`NOTIFY` is not dropped by the
44
+ * pooler.
35
45
  */
36
- export class PgClientDirect extends /*#__PURE__*/Context.Tag('@kairos-es/store-postgres/PgClientDirect')() {}
46
+ export class PgClientDirect extends /*#__PURE__*/Context.Service()('@kairos-es/store-postgres/PgClientDirect') {}
37
47
  /**
38
- * Build a `PgClientPooled` layer from a `PgClient` config. We construct the
39
- * client via `PgClient.make` (scoped) and place it in our OWN tag, then provide
40
- * `Reactivity.layer` (which `PgClient.make` requires) — exactly what the
48
+ * Build a `PgClientPooled` layer from a `PgClient` pool config. We construct the
49
+ * client via `PgClient.make` (scoped) and place it in our OWN service, then
50
+ * provide `Reactivity.layer` (which `PgClient.make` requires) — exactly what the
41
51
  * built-in `PgClient.layer` does internally, minus the collision-prone claim on
42
- * the shared `PgClient` tag.
52
+ * the shared `PgClient` key.
43
53
  */
44
- export const pooledLayer = config => Layer.scoped(PgClientPooled, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
54
+ export const pooledLayer = config => Layer.effect(PgClientPooled, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
45
55
  /**
46
- * Build a `PgClientDirect` layer from a `PgClient` config. Same construction as
56
+ * Build a `PgClientDirect` layer from a `PgClient` pool config. Same construction as
47
57
  * `pooledLayer`; the two differ only in which endpoint the caller points them
48
58
  * at (and that only matters on Neon).
49
59
  */
50
- export const directLayer = config => Layer.scoped(PgClientDirect, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
60
+ export const directLayer = config => Layer.effect(PgClientDirect, PgClient.make(config)).pipe(Layer.provide(Reactivity.layer));
51
61
  //# sourceMappingURL=clients.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"clients.js","names":["Reactivity","PgClient","Context","Layer","PgClientPooled","Tag","PgClientDirect","pooledLayer","config","scoped","make","pipe","provide","layer","directLayer"],"sources":["../../src/clients.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAASA,UAAU,QAAQ,sBAAsB;AAEjD,SAASC,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,OAAO,EAAEC,KAAK,QAAQ,QAAQ;AAEvC;;;;;AAKA,OAAM,MAAOC,cAAe,sBAAQF,OAAO,CAACG,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;AAIA,OAAM,MAAOC,cAAe,sBAAQJ,OAAO,CAACG,GAAG,CAC7C,0CAA0C,CAC3C,EAAqC;AAEtC;;;;;;;AAOA,OAAO,MAAME,WAAW,GACtBC,MAA+B,IAE/BL,KAAK,CAACM,MAAM,CAACL,cAAc,EAAEH,QAAQ,CAACS,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDR,KAAK,CAACS,OAAO,CAACZ,UAAU,CAACa,KAAK,CAAC,CAChC;AAEH;;;;;AAKA,OAAO,MAAMC,WAAW,GACtBN,MAA+B,IAE/BL,KAAK,CAACM,MAAM,CAACH,cAAc,EAAEL,QAAQ,CAACS,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDR,KAAK,CAACS,OAAO,CAACZ,UAAU,CAACa,KAAK,CAAC,CAChC","ignoreList":[]}
1
+ {"version":3,"file":"clients.js","names":["PgClient","Context","Layer","Reactivity","PgClientPooled","Service","PgClientDirect","pooledLayer","config","effect","make","pipe","provide","layer","directLayer"],"sources":["../../src/clients.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,SAASA,QAAQ,QAAQ,gBAAgB;AACzC,SAASC,OAAO,EAAEC,KAAK,QAAQ,QAAQ;AACvC,SAASC,UAAU,QAAQ,mBAAmB;AAG9C;;;;;AAKA,OAAM,MAAOC,cAAe,sBAAQH,OAAO,CAACI,OAAO,EAGhD,CAAC,0CAA0C,CAAC;AAE/C;;;;;AAKA,OAAM,MAAOC,cAAe,sBAAQL,OAAO,CAACI,OAAO,EAGhD,CAAC,0CAA0C,CAAC;AAE/C;;;;;;;AAOA,OAAO,MAAME,WAAW,GACtBC,MAA6B,IAE7BN,KAAK,CAACO,MAAM,CAACL,cAAc,EAAEJ,QAAQ,CAACU,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDT,KAAK,CAACU,OAAO,CAACT,UAAU,CAACU,KAAK,CAAC,CAChC;AAEH;;;;;AAKA,OAAO,MAAMC,WAAW,GACtBN,MAA6B,IAE7BN,KAAK,CAACO,MAAM,CAACH,cAAc,EAAEN,QAAQ,CAACU,IAAI,CAACF,MAAM,CAAC,CAAC,CAACG,IAAI,CACtDT,KAAK,CAACU,OAAO,CAACT,UAAU,CAACU,KAAK,CAAC,CAChC","ignoreList":[]}
@@ -27,7 +27,7 @@ import { oversizedIdentifier } from "./internal/ddl.js";
27
27
  * `sql(identifier)` dot-splits, so an embedded dot would silently re-partition
28
28
  * the name into the wrong schema/object.
29
29
  */
30
- const IdentifierAtom = /*#__PURE__*/Schema.String.pipe(/*#__PURE__*/Schema.nonEmptyString(), /*#__PURE__*/Schema.pattern(/^[^.]+$/, {
30
+ const IdentifierAtom = /*#__PURE__*/Schema.String.check(/*#__PURE__*/Schema.isNonEmpty(), /*#__PURE__*/Schema.isPattern(/^[^.]+$/, {
31
31
  identifier: 'PostgresIdentifierAtom',
32
32
  description: 'a non-empty identifier atom containing no "."'
33
33
  }));
@@ -58,30 +58,24 @@ const IdentifierAtom = /*#__PURE__*/Schema.String.pipe(/*#__PURE__*/Schema.nonEm
58
58
  * fetches before looping.
59
59
  */
60
60
  export const PostgresStoreConfig = /*#__PURE__*/Schema.Struct({
61
- // `exact: true` keeps the decoded type at `key?: T` (never `T | undefined`),
61
+ // `optionalKey` keeps the decoded type at `key?: T` (never `T | undefined`),
62
62
  // matching the repo's `exactOptionalPropertyTypes` discipline — an absent key,
63
63
  // not an explicit `undefined`.
64
- schema: Schema.optionalWith(IdentifierAtom, {
65
- exact: true
66
- }),
67
- tablePrefix: Schema.optionalWith(IdentifierAtom, {
68
- exact: true
69
- }),
70
- lockTimeout: Schema.optionalWith(Schema.Number.pipe(Schema.nonNegative(), Schema.finite()), {
71
- exact: true
72
- }),
64
+ schema: Schema.optionalKey(IdentifierAtom),
65
+ tablePrefix: Schema.optionalKey(IdentifierAtom),
66
+ lockTimeout: Schema.optionalKey(Schema.Number.check(Schema.isGreaterThanOrEqualTo(0), Schema.isFinite())),
73
67
  // The two subscribe knobs are core's field definitions, not this engine's: see
74
68
  // `subscriptionTuningFields`.
75
69
  ...subscriptionTuningFields
76
- }).pipe(
70
+ }).check(
77
71
  /*#__PURE__*/
78
72
  // Cross-field guard: the derived table/index/function/channel names must all
79
73
  // fit Postgres's 63-byte identifier limit. Returning the message string fails
80
- // the decode with it; `undefined`/`true` passes.
81
- Schema.filter(config => oversizedIdentifier(config) ?? true));
74
+ // the decode with it; `undefined` passes.
75
+ Schema.makeFilter(config => oversizedIdentifier(config)));
82
76
  /**
83
77
  * Decode an unknown value into a validated `PostgresStoreConfig`, throwing a
84
- * `ParseError` on any violation. Used by `layer`/`ensureSchema` so a bad config
78
+ * `SchemaError` on any violation. Used by `layer`/`ensureSchema` so a bad config
85
79
  * fails at construction time.
86
80
  */
87
81
  export const decodePostgresStoreConfig = /*#__PURE__*/Schema.decodeUnknownSync(PostgresStoreConfig);
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","names":["subscriptionTuningFields","Schema","oversizedIdentifier","IdentifierAtom","String","pipe","nonEmptyString","pattern","identifier","description","PostgresStoreConfig","Struct","schema","optionalWith","exact","tablePrefix","lockTimeout","Number","nonNegative","finite","filter","config","decodePostgresStoreConfig","decodeUnknownSync"],"sources":["../../src/config.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;AAoBA,SAASA,wBAAwB,QAAQ,iBAAiB;AAC1D,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,mBAAmB,QAAQ,mBAAgB;AAEpD;;;;;;AAMA,MAAMC,cAAc,gBAAGF,MAAM,CAACG,MAAM,CAACC,IAAI,cACvCJ,MAAM,CAACK,cAAc,EAAE,eACvBL,MAAM,CAACM,OAAO,CAAC,SAAS,EAAE;EACxBC,UAAU,EAAE,wBAAwB;EACpCC,WAAW,EAAE;CACd,CAAC,CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,OAAO,MAAMC,mBAAmB,gBAAGT,MAAM,CAACU,MAAM,CAAC;EAC/C;EACA;EACA;EACAC,MAAM,EAAEX,MAAM,CAACY,YAAY,CAACV,cAAc,EAAE;IAAEW,KAAK,EAAE;EAAI,CAAE,CAAC;EAC5DC,WAAW,EAAEd,MAAM,CAACY,YAAY,CAACV,cAAc,EAAE;IAAEW,KAAK,EAAE;EAAI,CAAE,CAAC;EACjEE,WAAW,EAAEf,MAAM,CAACY,YAAY,CAC9BZ,MAAM,CAACgB,MAAM,CAACZ,IAAI,CAACJ,MAAM,CAACiB,WAAW,EAAE,EAAEjB,MAAM,CAACkB,MAAM,EAAE,CAAC,EACzD;IAAEL,KAAK,EAAE;EAAI,CAAE,CAChB;EACD;EACA;EACA,GAAGd;CACJ,CAAC,CAACK,IAAI;AAAA;AACL;AACA;AACA;AACAJ,MAAM,CAACmB,MAAM,CAAEC,MAAM,IAAKnB,mBAAmB,CAACmB,MAAM,CAAC,IAAI,IAAI,CAAC,CAC/D;AAGD;;;;;AAKA,OAAO,MAAMC,yBAAyB,gBACpCrB,MAAM,CAACsB,iBAAiB,CAACb,mBAAmB,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"config.js","names":["subscriptionTuningFields","Schema","oversizedIdentifier","IdentifierAtom","String","check","isNonEmpty","isPattern","identifier","description","PostgresStoreConfig","Struct","schema","optionalKey","tablePrefix","lockTimeout","Number","isGreaterThanOrEqualTo","isFinite","makeFilter","config","decodePostgresStoreConfig","decodeUnknownSync"],"sources":["../../src/config.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;AAoBA,SAASA,wBAAwB,QAAQ,iBAAiB;AAC1D,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,mBAAmB,QAAQ,mBAAgB;AAEpD;;;;;;AAMA,MAAMC,cAAc,gBAAGF,MAAM,CAACG,MAAM,CAACC,KAAK,cACxCJ,MAAM,CAACK,UAAU,EAAE,eACnBL,MAAM,CAACM,SAAS,CAAC,SAAS,EAAE;EAC1BC,UAAU,EAAE,wBAAwB;EACpCC,WAAW,EAAE;CACd,CAAC,CACH;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,OAAO,MAAMC,mBAAmB,gBAAGT,MAAM,CAACU,MAAM,CAAC;EAC/C;EACA;EACA;EACAC,MAAM,EAAEX,MAAM,CAACY,WAAW,CAACV,cAAc,CAAC;EAC1CW,WAAW,EAAEb,MAAM,CAACY,WAAW,CAACV,cAAc,CAAC;EAC/CY,WAAW,EAAEd,MAAM,CAACY,WAAW,CAC7BZ,MAAM,CAACe,MAAM,CAACX,KAAK,CAACJ,MAAM,CAACgB,sBAAsB,CAAC,CAAC,CAAC,EAAEhB,MAAM,CAACiB,QAAQ,EAAE,CAAC,CACzE;EACD;EACA;EACA,GAAGlB;CACJ,CAAC,CAACK,KAAK;AAAA;AACN;AACA;AACA;AACAJ,MAAM,CAACkB,UAAU,CAAEC,MAAM,IAAKlB,mBAAmB,CAACkB,MAAM,CAAC,CAAC,CAC3D;AAGD;;;;;AAKA,OAAO,MAAMC,yBAAyB,gBACpCpB,MAAM,CAACqB,iBAAiB,CAACZ,mBAAmB,CAAC","ignoreList":[]}
@@ -4,7 +4,7 @@
4
4
  * This is deliberately NOT a migrator. It is a single `Effect<void, SqlError,
5
5
  * SqlClient>` that creates (or refreshes) every object a store owns: both
6
6
  * tables, both indexes, and both plpgsql functions. It requires the GENERIC
7
- * `SqlClient` tag — NOT `PgClientPooled` — precisely so a consumer can drop it
7
+ * `SqlClient` service — NOT `PgClientPooled` — precisely so a consumer can drop it
8
8
  * straight into THEIR OWN migrator, e.g.:
9
9
  *
10
10
  * ```ts
@@ -18,13 +18,18 @@
18
18
  * consumer, this package only provides the idempotent PAYLOAD.
19
19
  *
20
20
  * Idempotence: every statement is `CREATE … IF NOT EXISTS` or `CREATE OR REPLACE
21
- * FUNCTION`, so re-running against an existing store is a no-op. Names come from
21
+ * FUNCTION`, so re-running it is safe. It is NOT a no-op across versions: a
22
+ * re-run replaces the append functions with this version's, and does NOT alter
23
+ * an existing table, so a column a newer version adds must be added by hand
24
+ * before the re-run (the release notes give the statement). A store whose
25
+ * table gained a column but whose functions were never replaced keeps writing
26
+ * through the old ones, which do not know the column. Names come from
22
27
  * `sql(identifier)` (via the pre-quoted builder in `internal/ddl.ts`); the DDL
23
28
  * bodies carry no bound values (pure schema), and the append functions keep
24
29
  * their runtime VALUES as bound parameters, never interpolated.
25
30
  */
26
- import { SqlClient } from '@effect/sql';
27
31
  import { Effect } from 'effect';
32
+ import { SqlClient } from 'effect/sql';
28
33
  import { decodePostgresStoreConfig } from "./config.js";
29
34
  import { resolveNames, runDdl } from "./internal/ddl.js";
30
35
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"ensureSchema.js","names":["SqlClient","Effect","decodePostgresStoreConfig","resolveNames","runDdl","ensureSchema","config","gen","sql","names","all","discard"],"sources":["../../src/ensureSchema.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAASA,SAAS,QAAuB,aAAa;AACtD,SAASC,MAAM,QAAQ,QAAQ;AAC/B,SAASC,yBAAyB,QAAkC,aAAU;AAC9E,SAASC,YAAY,EAAEC,MAAM,QAAQ,mBAAgB;AAErD;;;;;AAKA,OAAO,MAAMC,YAAY,GACvBC,MAA2B,IAE3BL,MAAM,CAACM,GAAG,CAAC,aAAS;EAClB,MAAMC,GAAG,GAAG,OAAOR,SAAS,CAACA,SAAS;EACtC;EACA;EACA,MAAMS,KAAK,GAAGN,YAAY,CAACD,yBAAyB,CAACI,MAAM,CAAC,CAAC;EAC7D;EACA,OAAOL,MAAM,CAACS,GAAG,CAACN,MAAM,CAACI,GAAG,EAAEC,KAAK,CAAC,EAAE;IAAEE,OAAO,EAAE;EAAI,CAAE,CAAC;AAC1D,CAAC,CAAC","ignoreList":[]}
1
+ {"version":3,"file":"ensureSchema.js","names":["Effect","SqlClient","decodePostgresStoreConfig","resolveNames","runDdl","ensureSchema","config","gen","sql","names","all","discard"],"sources":["../../src/ensureSchema.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,SAASA,MAAM,QAAQ,QAAQ;AAC/B,SAASC,SAAS,QAAuB,YAAY;AACrD,SAASC,yBAAyB,QAAkC,aAAU;AAC9E,SAASC,YAAY,EAAEC,MAAM,QAAQ,mBAAgB;AAErD;;;;;AAKA,OAAO,MAAMC,YAAY,GACvBC,MAA2B,IAE3BN,MAAM,CAACO,GAAG,CAAC,aAAS;EAClB,MAAMC,GAAG,GAAG,OAAOP,SAAS,CAACA,SAAS;EACtC;EACA;EACA,MAAMQ,KAAK,GAAGN,YAAY,CAACD,yBAAyB,CAACI,MAAM,CAAC,CAAC;EAC7D;EACA,OAAON,MAAM,CAACU,GAAG,CAACN,MAAM,CAACI,GAAG,EAAEC,KAAK,CAAC,EAAE;IAAEE,OAAO,EAAE;EAAI,CAAE,CAAC;AAC1D,CAAC,CAAC","ignoreList":[]}
package/dist/esm/index.js CHANGED
@@ -30,9 +30,10 @@
30
30
  * const store = layer(config).pipe(Layer.provide(clients))
31
31
  * ```
32
32
  *
33
- * The store `layer` performs NO DDL: it depends only on the two client tags and
34
- * assumes `ensureSchema` has run. `ensureSchema` requires the generic `SqlClient`
35
- * tag so it slots into any migrator; the runtime uses the store-owned tags.
33
+ * The store `layer` performs NO DDL: it depends only on the two client services
34
+ * and assumes `ensureSchema` has run. `ensureSchema` requires the generic
35
+ * `SqlClient` service so it slots into any migrator; the runtime uses the
36
+ * store-owned services.
36
37
  */
37
38
  export { directLayer, PgClientDirect, PgClientPooled, pooledLayer } from "./clients.js";
38
39
  export { ensureSchema } from "./ensureSchema.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["directLayer","PgClientDirect","PgClientPooled","pooledLayer","ensureSchema","layer"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SACEA,WAAW,EACXC,cAAc,EACdC,cAAc,EACdC,WAAW,QACN,cAAW;AAElB,SAASC,YAAY,QAAQ,mBAAgB;AAC7C,SAASC,KAAK,QAAQ,YAAS","ignoreList":[]}
1
+ {"version":3,"file":"index.js","names":["directLayer","PgClientDirect","PgClientPooled","pooledLayer","ensureSchema","layer"],"sources":["../../src/index.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCA,SACEA,WAAW,EACXC,cAAc,EACdC,cAAc,EACdC,WAAW,QACN,cAAW;AAElB,SAASC,YAAY,QAAQ,mBAAgB;AAC7C,SAASC,KAAK,QAAQ,YAAS","ignoreList":[]}