@kairos-es/read-postgres 0.0.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/LICENSE +28 -0
- package/README.md +238 -0
- package/dist/cjs/config.js +86 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/ensureCheckpointSchema.js +58 -0
- package/dist/cjs/ensureCheckpointSchema.js.map +1 -0
- package/dist/cjs/index.js +20 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/ddl.js +172 -0
- package/dist/cjs/internal/ddl.js.map +1 -0
- package/dist/cjs/internal/statements.js +83 -0
- package/dist/cjs/internal/statements.js.map +1 -0
- package/dist/cjs/projectionStore.js +154 -0
- package/dist/cjs/projectionStore.js.map +1 -0
- package/dist/dts/config.d.ts +65 -0
- package/dist/dts/config.d.ts.map +1 -0
- package/dist/dts/ensureCheckpointSchema.d.ts +39 -0
- package/dist/dts/ensureCheckpointSchema.d.ts.map +1 -0
- package/dist/dts/index.d.ts +90 -0
- package/dist/dts/index.d.ts.map +1 -0
- package/dist/dts/internal/ddl.d.ts +146 -0
- package/dist/dts/internal/ddl.d.ts.map +1 -0
- package/dist/dts/internal/statements.d.ts +100 -0
- package/dist/dts/internal/statements.d.ts.map +1 -0
- package/dist/dts/projectionStore.d.ts +30 -0
- package/dist/dts/projectionStore.d.ts.map +1 -0
- package/dist/esm/config.js +79 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/ensureCheckpointSchema.js +50 -0
- package/dist/esm/ensureCheckpointSchema.js.map +1 -0
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/ddl.js +161 -0
- package/dist/esm/internal/ddl.js.map +1 -0
- package/dist/esm/internal/statements.js +74 -0
- package/dist/esm/internal/statements.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/projectionStore.js +147 -0
- package/dist/esm/projectionStore.js.map +1 -0
- package/package.json +33 -0
- package/src/config.ts +83 -0
- package/src/ensureCheckpointSchema.ts +51 -0
- package/src/index.ts +89 -0
- package/src/internal/ddl.ts +246 -0
- package/src/internal/statements.ts +144 -0
- package/src/projectionStore.ts +300 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.keyParams = exports.compileStatements = exports.advanceParams = void 0;
|
|
7
|
+
var _ddl = require("./ddl.js");
|
|
8
|
+
/**
|
|
9
|
+
* Build the statement texts for a resolved name set.
|
|
10
|
+
*
|
|
11
|
+
* **The two guard shapes, and why one cannot serve both.** The guarded advance is
|
|
12
|
+
* a compare-and-set on `last_position`, and its correct SQL depends on whether the
|
|
13
|
+
* expected value is `ORIGIN`:
|
|
14
|
+
*
|
|
15
|
+
* - `expected === ORIGIN` needs `upsertFromOrigin`. A never-committed key has NO
|
|
16
|
+
* ROW (a key reads back as `ORIGIN` by absence, so "fresh" and "reset" are one
|
|
17
|
+
* state), so a plain `UPDATE` would match zero rows and report
|
|
18
|
+
* `CheckpointSuperseded` — a fresh read model could never make its FIRST commit.
|
|
19
|
+
* The `ON CONFLICT … DO UPDATE … WHERE chk.last_position = $4` arm keeps the
|
|
20
|
+
* upsert a real guard rather than a blind write: two racers both starting from
|
|
21
|
+
* `ORIGIN` cannot both win, because the second one's arm blocks on the first's
|
|
22
|
+
* uncommitted row, then re-reads it and finds a position that is no longer
|
|
23
|
+
* `ORIGIN`, matching zero rows. That arm also makes "row present at `0`"
|
|
24
|
+
* (an operator zeroing the column by hand instead of deleting the row) behave
|
|
25
|
+
* identically to "row absent", so the two ways of expressing a reset are
|
|
26
|
+
* indistinguishable to a first commit.
|
|
27
|
+
* - `expected > ORIGIN` needs `advance`, the plain `UPDATE`. The upsert would be
|
|
28
|
+
* WRONG here: `ON CONFLICT` guards only the branch where the row exists, so an
|
|
29
|
+
* absent row would be INSERTED unconditionally — a `resetCheckpoint` racing an
|
|
30
|
+
* in-flight commit would be silently undone, the key resurrected at `next`, and
|
|
31
|
+
* the entire backlog the reset existed to replay skipped with no error at all.
|
|
32
|
+
* The `UPDATE`'s zero rows for an absent row is the CORRECT outcome: somebody
|
|
33
|
+
* reset it, so this commit must lose and the runner must re-read.
|
|
34
|
+
*
|
|
35
|
+
* Both shapes `RETURNING` a column so the outcome is a ROW COUNT: zero rows means
|
|
36
|
+
* the guard lost. That mirrors the event store's append, where a conflict is the
|
|
37
|
+
* function's zero-row result and never a caught SQL error — no unique-violation
|
|
38
|
+
* or serialisation-failure path to classify, so a lost guard can never arrive
|
|
39
|
+
* mistagged as an infrastructure fault.
|
|
40
|
+
*
|
|
41
|
+
* The `AS chk` alias exists so the `DO UPDATE` guard can reference the EXISTING
|
|
42
|
+
* row unambiguously: the insert target is schema-qualified, and without an alias
|
|
43
|
+
* the guard would need a three-part column reference.
|
|
44
|
+
*/
|
|
45
|
+
const compileStatements = names => {
|
|
46
|
+
const table = (0, _ddl.quoteQualified)(names.checkpointTable);
|
|
47
|
+
const columns = _ddl.CHECKPOINT_COLUMNS;
|
|
48
|
+
const byKey = `${columns.projection} = $1 AND ${columns.partition} = $2`;
|
|
49
|
+
return {
|
|
50
|
+
select: `SELECT ${columns.lastPosition}
|
|
51
|
+
FROM ${table}
|
|
52
|
+
WHERE ${byKey}`,
|
|
53
|
+
upsertFromOrigin: `INSERT INTO ${table} AS chk (
|
|
54
|
+
${columns.projection},
|
|
55
|
+
${columns.partition},
|
|
56
|
+
${columns.lastPosition}
|
|
57
|
+
) VALUES ($1, $2, $3)
|
|
58
|
+
ON CONFLICT (${columns.projection}, ${columns.partition})
|
|
59
|
+
DO UPDATE SET ${columns.lastPosition} = $3
|
|
60
|
+
WHERE chk.${columns.lastPosition} = $4
|
|
61
|
+
RETURNING ${columns.lastPosition}`,
|
|
62
|
+
advance: `UPDATE ${table}
|
|
63
|
+
SET ${columns.lastPosition} = $3
|
|
64
|
+
WHERE ${byKey} AND ${columns.lastPosition} = $4
|
|
65
|
+
RETURNING ${columns.lastPosition}`,
|
|
66
|
+
reset: `DELETE FROM ${table} WHERE ${byKey}`
|
|
67
|
+
};
|
|
68
|
+
};
|
|
69
|
+
/** The `$1`/`$2` key parameters the `select` and the `reset` take. */
|
|
70
|
+
exports.compileStatements = compileStatements;
|
|
71
|
+
const keyParams = key => [key.projection, key.partition];
|
|
72
|
+
/**
|
|
73
|
+
* The `$1`-`$4` parameters both guard shapes take, read from named fields.
|
|
74
|
+
*
|
|
75
|
+
* The argument is an object rather than three positional parameters for the reason
|
|
76
|
+
* `CheckpointStatements` gives: `next` and `expected` are interchangeable to the
|
|
77
|
+
* type checker and catastrophic to swap, so the ONE place their order is written
|
|
78
|
+
* out reads each of them by name.
|
|
79
|
+
*/
|
|
80
|
+
exports.keyParams = keyParams;
|
|
81
|
+
const advanceParams = args => [args.key.projection, args.key.partition, args.next, args.expected];
|
|
82
|
+
exports.advanceParams = advanceParams;
|
|
83
|
+
//# sourceMappingURL=statements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statements.js","names":["_ddl","require","compileStatements","names","table","quoteQualified","checkpointTable","columns","CHECKPOINT_COLUMNS","byKey","projection","partition","select","lastPosition","upsertFromOrigin","advance","reset","exports","keyParams","key","advanceParams","args","next","expected"],"sources":["../../../src/internal/statements.ts"],"sourcesContent":[null],"mappings":";;;;;;AAqBA,IAAAA,IAAA,GAAAC,OAAA;AA+BA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCO,MAAMC,iBAAiB,GAC5BC,KAA8B,IACN;EACxB,MAAMC,KAAK,GAAG,IAAAC,mBAAc,EAACF,KAAK,CAACG,eAAe,CAAC;EACnD,MAAMC,OAAO,GAAGC,uBAAkB;EAClC,MAAMC,KAAK,GAAG,GAAGF,OAAO,CAACG,UAAU,aAAaH,OAAO,CAACI,SAAS,OAAO;EAExE,OAAO;IACLC,MAAM,EAAE,UAAUL,OAAO,CAACM,YAAY;oBACtBT,KAAK;qBACJK,KAAK,EAAE;IAExBK,gBAAgB,EAAE,eAAeV,KAAK;2BACfG,OAAO,CAACG,UAAU;2BAClBH,OAAO,CAACI,SAAS;2BACjBJ,OAAO,CAACM,YAAY;;sCAETN,OAAO,CAACG,UAAU,KAAKH,OAAO,CAACI,SAAS;uCACvCJ,OAAO,CAACM,YAAY;mCACxBN,OAAO,CAACM,YAAY;mCACpBN,OAAO,CAACM,YAAY,EAAE;IAErDE,OAAO,EAAE,UAAUX,KAAK;oBACRG,OAAO,CAACM,YAAY;sBAClBJ,KAAK,QAAQF,OAAO,CAACM,YAAY;0BAC7BN,OAAO,CAACM,YAAY,EAAE;IAE5CG,KAAK,EAAE,eAAeZ,KAAK,UAAUK,KAAK;GAC3C;AACH,CAAC;AAED;AAAAQ,OAAA,CAAAf,iBAAA,GAAAA,iBAAA;AACO,MAAMgB,SAAS,GAAIC,GAAkB,IAA6B,CACvEA,GAAG,CAACT,UAAU,EACdS,GAAG,CAACR,SAAS,CACd;AAED;;;;;;;;AAAAM,OAAA,CAAAC,SAAA,GAAAA,SAAA;AAQO,MAAME,aAAa,GAAIC,IAI7B,IAA6B,CAC5BA,IAAI,CAACF,GAAG,CAACT,UAAU,EACnBW,IAAI,CAACF,GAAG,CAACR,SAAS,EAClBU,IAAI,CAACC,IAAI,EACTD,IAAI,CAACE,QAAQ,CACd;AAAAN,OAAA,CAAAG,aAAA,GAAAA,aAAA","ignoreList":[]}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.makeSqlProjectionStore = void 0;
|
|
7
|
+
var _sql = require("@effect/sql");
|
|
8
|
+
var _core = require("@kairos-es/core");
|
|
9
|
+
var _read = require("@kairos-es/read");
|
|
10
|
+
var _effect = require("effect");
|
|
11
|
+
var _config = require("./config.js");
|
|
12
|
+
var _ddl = require("./internal/ddl.js");
|
|
13
|
+
var _statements = require("./internal/statements.js");
|
|
14
|
+
/**
|
|
15
|
+
* Turn a `ProjectionStoreError` out of a SQL fault, naming what was being
|
|
16
|
+
* attempted and for which key. `reason` is the human-readable line a supervisor
|
|
17
|
+
* logs, so it carries the key and the driver's message; `cause` carries the
|
|
18
|
+
* `SqlError` itself for anything that wants to inspect it.
|
|
19
|
+
*/
|
|
20
|
+
const storeError = (attempt, key) => cause => new _read.ProjectionStoreError({
|
|
21
|
+
reason: `${attempt} for projection '${key.projection}' partition '${key.partition}' failed: ${cause.message}`,
|
|
22
|
+
cause
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Restore the port's declared error channel after the transaction: unwrap the
|
|
26
|
+
* caller's `E`, and classify a transaction-frame `SqlError` as infrastructure.
|
|
27
|
+
* Both `CheckpointSuperseded` and an already-classified `ProjectionStoreError`
|
|
28
|
+
* pass through as they are.
|
|
29
|
+
*/
|
|
30
|
+
const unwrapCommitError = (key, error) => {
|
|
31
|
+
if (error._tag === 'ViewWriteFailed') {
|
|
32
|
+
return error.error;
|
|
33
|
+
}
|
|
34
|
+
if (error._tag === 'SqlError') {
|
|
35
|
+
return storeError('the checkpoint commit transaction', key)(error);
|
|
36
|
+
}
|
|
37
|
+
return error;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Build a durable `ProjectionStore` over the ambient `SqlClient`.
|
|
41
|
+
*
|
|
42
|
+
* A factory `Effect` rather than a `Layer`, matching the in-memory store: the view
|
|
43
|
+
* store is selected PER MATERIALISATION, so it is a value handed to a read model, not
|
|
44
|
+
* a context tag one store could claim process-wide. Per materialisation rather than
|
|
45
|
+
* per read model, because one read model may be materialised several times at once —
|
|
46
|
+
* a store from here beside an in-memory one, wired by `@kairos-es/read`'s
|
|
47
|
+
* `runProjections` — which is a shape a process-wide tag could not express at all. It
|
|
48
|
+
* acquires the client ONCE and closes over it, which is what keeps `commit`'s `R`
|
|
49
|
+
* down to the caller's own.
|
|
50
|
+
*
|
|
51
|
+
* **The atomic bracket covers writes issued through THIS client.** The caller's
|
|
52
|
+
* `viewWrites` join the transaction because `@effect/sql` carries the transaction's
|
|
53
|
+
* connection in the Effect CONTEXT, not in the client value — so any statement run
|
|
54
|
+
* through the same underlying client inside `commit` is in the same transaction,
|
|
55
|
+
* including through a differently-transformed copy of it. A write to a different
|
|
56
|
+
* database, or through an unrelated client, is NOT bracketed and the exactly-once
|
|
57
|
+
* property does not extend to it.
|
|
58
|
+
*
|
|
59
|
+
* `ensureCheckpointSchema(config)` must have run (in the caller's migrator or at
|
|
60
|
+
* boot) before the store is used; this factory performs no DDL, so it stays a pure
|
|
61
|
+
* runtime dependency on the client alone.
|
|
62
|
+
*/
|
|
63
|
+
const makeSqlProjectionStore = (config = {}) => _effect.Effect.gen(function* () {
|
|
64
|
+
// `withoutTransforms()` because this package takes the CONSUMER's generic
|
|
65
|
+
// client, which may carry a result-name transform (`@effect/sql` will happily
|
|
66
|
+
// camel-case every returned key). Under one, `last_position` would arrive as
|
|
67
|
+
// `lastPosition`, `readCheckpoint` would see `undefined` and report `ORIGIN`,
|
|
68
|
+
// and a durable projection would silently replay its whole backlog on every
|
|
69
|
+
// boot. Stripping the transform for OUR statements is the fix; the caller's
|
|
70
|
+
// own view writes keep their client's transforms and still join the same
|
|
71
|
+
// transaction (the connection travels in the context, not the client).
|
|
72
|
+
const sql = (yield* _sql.SqlClient.SqlClient).withoutTransforms();
|
|
73
|
+
// Decode INSIDE the Effect so a malformed config (empty/dotted atom, an
|
|
74
|
+
// over-63-byte derived name) surfaces as a defect on the fibre building the
|
|
75
|
+
// store rather than as an exception thrown out of the factory call, matching
|
|
76
|
+
// `ensureCheckpointSchema`.
|
|
77
|
+
const names = (0, _ddl.resolveCheckpointNames)((0, _config.decodeReadPostgresConfig)(config));
|
|
78
|
+
const statements = (0, _statements.compileStatements)(names);
|
|
79
|
+
const readCheckpoint = key => sql.unsafe(statements.select, (0, _statements.keyParams)(key)).pipe(_effect.Effect.mapError(storeError('reading the checkpoint', key)), _effect.Effect.map(rows => {
|
|
80
|
+
const stored = rows[0]?.last_position;
|
|
81
|
+
// No row means never committed (or reset), which the port defines as
|
|
82
|
+
// `ORIGIN` — so a fresh read model and a reset one both simply catch up
|
|
83
|
+
// from the start of the log.
|
|
84
|
+
return stored === undefined ? _core.ORIGIN :
|
|
85
|
+
// `Position.make` re-validates non-negativity, so a corrupted
|
|
86
|
+
// column becomes a loud defect here rather than a nonsensical
|
|
87
|
+
// cursor that silently reads nothing.
|
|
88
|
+
_core.Position.make(BigInt(stored));
|
|
89
|
+
}));
|
|
90
|
+
/**
|
|
91
|
+
* The compare-and-set half of `commit`, run INSIDE the caller's transaction.
|
|
92
|
+
* Which shape is correct depends on `expected`; see `compileStatements` for
|
|
93
|
+
* why neither shape can serve both cases.
|
|
94
|
+
*
|
|
95
|
+
* Takes its three values as NAMED FIELDS, and `commit` hands it the very
|
|
96
|
+
* object the port handed `commit`, so `expected` and `next` are never lined up
|
|
97
|
+
* positionally anywhere between the port's boundary and the bound `$n`
|
|
98
|
+
* (`advanceParams` does the binding, also by name). They are both a `Position`,
|
|
99
|
+
* so nothing else would notice them swapping — and swapped, the guard would
|
|
100
|
+
* compare the stored cursor against the position it was about to write.
|
|
101
|
+
*/
|
|
102
|
+
const guardedAdvance = args => {
|
|
103
|
+
const {
|
|
104
|
+
key,
|
|
105
|
+
expected
|
|
106
|
+
} = args;
|
|
107
|
+
const [text, attempt] = expected === _core.ORIGIN ? [statements.upsertFromOrigin, 'inserting the first checkpoint'] : [statements.advance, 'advancing the checkpoint'];
|
|
108
|
+
// `next` is bound AS IS — it is the last processed event's position, never
|
|
109
|
+
// `expected + 1`: positions are strictly increasing but not gapless, so a
|
|
110
|
+
// jump is normal and arithmetic would be wrong.
|
|
111
|
+
return sql.unsafe(text, (0, _statements.advanceParams)(args)).pipe(_effect.Effect.mapError(storeError(attempt, key)), _effect.Effect.flatMap(rows =>
|
|
112
|
+
// Zero rows = the guard lost. Failing (rather than returning an
|
|
113
|
+
// outcome) is what aborts the enclosing transaction and rolls the
|
|
114
|
+
// view writes back WITH it.
|
|
115
|
+
rows.length === 0 ? _effect.Effect.fail(new _read.CheckpointSuperseded({
|
|
116
|
+
key,
|
|
117
|
+
expected
|
|
118
|
+
})) : _effect.Effect.void));
|
|
119
|
+
};
|
|
120
|
+
const commit = args => sql.withTransaction(args.viewWrites.pipe(_effect.Effect.mapError(error => ({
|
|
121
|
+
_tag: 'ViewWriteFailed',
|
|
122
|
+
error
|
|
123
|
+
})),
|
|
124
|
+
// View writes first, guard last. Ordering is a performance choice,
|
|
125
|
+
// not a correctness one: the transaction makes the pair atomic
|
|
126
|
+
// either way, and deferring the advance holds the checkpoint row
|
|
127
|
+
// lock only for the advance rather than across the whole batch. (The
|
|
128
|
+
// in-memory store has no such freedom — with nothing to roll back it
|
|
129
|
+
// MUST check first.) No `uninterruptible` wrapper either:
|
|
130
|
+
// `withTransaction` masks interruption around its own frame, so an
|
|
131
|
+
// interrupt landing mid-body rolls back exactly like a failure.
|
|
132
|
+
// `args` itself, not three values pulled out of it in an order: see
|
|
133
|
+
// `guardedAdvance`.
|
|
134
|
+
_effect.Effect.zipRight(guardedAdvance(args)))).pipe(_effect.Effect.mapError(error => unwrapCommitError(args.key, error)));
|
|
135
|
+
const resetCheckpoint = key =>
|
|
136
|
+
// DELETE rather than `SET last_position = 0`, so "never committed" and
|
|
137
|
+
// "reset" stay ONE state — which is exactly what the port promises the
|
|
138
|
+
// read-back is, and what the in-memory store's map delete does.
|
|
139
|
+
sql.unsafe(statements.reset, (0, _statements.keyParams)(key)).pipe(_effect.Effect.mapError(storeError('resetting the checkpoint', key)), _effect.Effect.asVoid);
|
|
140
|
+
// Annotated at the definition site rather than only through the exported
|
|
141
|
+
// signature, per the rule on `ProjectionStore` itself.
|
|
142
|
+
const store = {
|
|
143
|
+
// Outlives the process, which is the R2 input: a durable view is legal only
|
|
144
|
+
// over a durable log, and the runner dies at construction if it is wired
|
|
145
|
+
// over an ephemeral one.
|
|
146
|
+
durability: 'durable',
|
|
147
|
+
readCheckpoint,
|
|
148
|
+
commit,
|
|
149
|
+
resetCheckpoint
|
|
150
|
+
};
|
|
151
|
+
return store;
|
|
152
|
+
});
|
|
153
|
+
exports.makeSqlProjectionStore = makeSqlProjectionStore;
|
|
154
|
+
//# sourceMappingURL=projectionStore.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"projectionStore.js","names":["_sql","require","_core","_read","_effect","_config","_ddl","_statements","storeError","attempt","key","cause","ProjectionStoreError","reason","projection","partition","message","unwrapCommitError","error","_tag","makeSqlProjectionStore","config","Effect","gen","sql","SqlClient","withoutTransforms","names","resolveCheckpointNames","decodeReadPostgresConfig","statements","compileStatements","readCheckpoint","unsafe","select","keyParams","pipe","mapError","map","rows","stored","last_position","undefined","ORIGIN","Position","make","BigInt","guardedAdvance","args","expected","text","upsertFromOrigin","advance","advanceParams","flatMap","length","fail","CheckpointSuperseded","void","commit","withTransaction","viewWrites","zipRight","resetCheckpoint","reset","asVoid","store","durability","exports"],"sources":["../../src/projectionStore.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public configuration for the Postgres `ProjectionStore`.
|
|
3
|
+
*
|
|
4
|
+
* A read-side store is defined entirely by its `{ schema, tablePrefix }` naming
|
|
5
|
+
* pair: the checkpoint table name is DERIVED from it (see `internal/ddl.ts`), so
|
|
6
|
+
* one config value is one completely isolated checkpoint namespace, cohabiting
|
|
7
|
+
* per prefix exactly as the event store's objects do (ADR-0002).
|
|
8
|
+
*
|
|
9
|
+
* It is a `Schema`, not a bare interface, so malformed input fails LOUDLY at
|
|
10
|
+
* construction (`makeSqlProjectionStore`/`ensureCheckpointSchema` decode through
|
|
11
|
+
* it) rather than late and obscurely on the first commit — the same
|
|
12
|
+
* fail-at-the-boundary discipline core applies to `Tag`/`ReadLimit`. It rejects an
|
|
13
|
+
* empty or dotted `schema`/`tablePrefix` atom (a dot would break the
|
|
14
|
+
* qualified-name dot-split) and — via `oversizedIdentifier` — any pair whose
|
|
15
|
+
* derived table name would exceed Postgres's 63-byte identifier limit and be
|
|
16
|
+
* silently truncated.
|
|
17
|
+
*
|
|
18
|
+
* **WHY this config is DUPLICATED rather than imported from
|
|
19
|
+
* `@kairos-es/store-postgres`.** Importing it would mean peering that package,
|
|
20
|
+
* and peering an event-store BACKEND from the read side is precisely what the
|
|
21
|
+
* per-materialisation view-store design exists to avoid: a durable view must be
|
|
22
|
+
* free to sit over any log satisfying `DcbEventStore` — the in-memory oracle,
|
|
23
|
+
* Postgres, a future backend — so this package depends on the PORT
|
|
24
|
+
* (`@kairos-es/read`) and on `@effect/sql`, never on a sibling backend. The
|
|
25
|
+
* duplication also keeps the two guards honestly independent: each package
|
|
26
|
+
* measures only the identifiers IT materialises, so neither inherits a byte budget
|
|
27
|
+
* from names it never creates.
|
|
28
|
+
*
|
|
29
|
+
* The shape is intentionally the event store's MINUS everything read-side wiring
|
|
30
|
+
* has no business choosing: no `lockTimeout` (the checkpoint's guarded
|
|
31
|
+
* compare-and-set takes only ordinary row locks, and a blocked writer is a
|
|
32
|
+
* competing runner whose loss is a `CheckpointSuperseded` the supervisor already
|
|
33
|
+
* handles), no `pollInterval`/`catchUpPageSize` (those belong to `subscribe`, on
|
|
34
|
+
* the log side). What remains is naming, and only naming.
|
|
35
|
+
*/
|
|
36
|
+
import { Schema } from 'effect';
|
|
37
|
+
/**
|
|
38
|
+
* Configuration for a read-side checkpoint store.
|
|
39
|
+
*
|
|
40
|
+
* - `schema` — the Postgres schema that owns the checkpoint table (default
|
|
41
|
+
* `public`). `ensureCheckpointSchema` creates it defensively.
|
|
42
|
+
* - `tablePrefix` — the base name the checkpoint table derives from (default
|
|
43
|
+
* `dcb_events`, giving `dcb_events_checkpoints`). Pass the SAME pair the event
|
|
44
|
+
* store is configured with and the checkpoint table lands beside that store's
|
|
45
|
+
* objects, one isolated store per prefix.
|
|
46
|
+
*/
|
|
47
|
+
export declare const ReadPostgresConfig: Schema.filter<Schema.Struct<{
|
|
48
|
+
schema: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.String>>, {
|
|
49
|
+
exact: true;
|
|
50
|
+
}>;
|
|
51
|
+
tablePrefix: Schema.optionalWith<Schema.filter<Schema.filter<typeof Schema.String>>, {
|
|
52
|
+
exact: true;
|
|
53
|
+
}>;
|
|
54
|
+
}>>;
|
|
55
|
+
export type ReadPostgresConfig = typeof ReadPostgresConfig.Type;
|
|
56
|
+
/**
|
|
57
|
+
* Decode an unknown value into a validated `ReadPostgresConfig`, throwing a
|
|
58
|
+
* `ParseError` on any violation. Used by `makeSqlProjectionStore` and
|
|
59
|
+
* `ensureCheckpointSchema` so a bad config fails at construction time.
|
|
60
|
+
*/
|
|
61
|
+
export declare const decodeReadPostgresConfig: (u: unknown, overrideOptions?: import("effect/SchemaAST").ParseOptions) => {
|
|
62
|
+
readonly schema?: string;
|
|
63
|
+
readonly tablePrefix?: string;
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAiB/B;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB;;;;;;;GAW9B,CAAA;AACD,MAAM,MAAM,kBAAkB,GAAG,OAAO,kBAAkB,CAAC,IAAI,CAAA;AAE/D;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;CACS,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ensureCheckpointSchema` — the idempotent checkpoint DDL, as a building block
|
|
3
|
+
* rather than a migrator (ADR-0007, mirroring the event store's `ensureSchema`).
|
|
4
|
+
*
|
|
5
|
+
* It is a single `Effect<void, SqlError, SqlClient>` that creates (or refreshes)
|
|
6
|
+
* the one object this package owns: the checkpoint table. It requires the GENERIC
|
|
7
|
+
* `SqlClient` tag — not a package-owned client tag — precisely so a consumer can
|
|
8
|
+
* drop it straight into THEIR OWN migrator, e.g.:
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* Migrator.fromRecord({
|
|
12
|
+
* '0002_kairos_checkpoints': ensureCheckpointSchema(config),
|
|
13
|
+
* })
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* or simply run it once at boot, beside `ensureSchema` from the event-store
|
|
17
|
+
* backend. We ship no `FileSystem`/`Path`/`ChildProcess` dependency and no
|
|
18
|
+
* migrator: migration ORCHESTRATION belongs to the consumer, this package only
|
|
19
|
+
* provides the idempotent PAYLOAD. That is also why schema ownership never
|
|
20
|
+
* silently moves — a read model cannot conjure its own table at runtime.
|
|
21
|
+
*
|
|
22
|
+
* Idempotence: every statement is `CREATE … IF NOT EXISTS`, so re-running against
|
|
23
|
+
* an existing checkpoint table is a no-op. Names come pre-quoted from
|
|
24
|
+
* `internal/ddl.ts`; the DDL carries no bound values (pure schema).
|
|
25
|
+
*
|
|
26
|
+
* Unlike the runtime store, this path needs no `withoutTransforms()` guard: it
|
|
27
|
+
* reads no rows and passes no identifier through `sql(identifier)`, so a
|
|
28
|
+
* consumer's name transforms have nothing here to rewrite.
|
|
29
|
+
*/
|
|
30
|
+
import { SqlClient, type SqlError } from '@effect/sql';
|
|
31
|
+
import { Effect } from 'effect';
|
|
32
|
+
import { type ReadPostgresConfig } from './config.js';
|
|
33
|
+
/**
|
|
34
|
+
* Produce the checkpoint migration payload for `config`. Resolves the table name
|
|
35
|
+
* once and runs the ordered idempotent DDL against whatever `SqlClient` the
|
|
36
|
+
* caller's migrator (or boot code) provides.
|
|
37
|
+
*/
|
|
38
|
+
export declare const ensureCheckpointSchema: (config: ReadPostgresConfig) => Effect.Effect<void, SqlError.SqlError, SqlClient.SqlClient>;
|
|
39
|
+
//# sourceMappingURL=ensureCheckpointSchema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ensureCheckpointSchema.d.ts","sourceRoot":"","sources":["../../src/ensureCheckpointSchema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,OAAO,EAAE,SAAS,EAAE,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC/B,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAG5E;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,GACjC,QAAQ,kBAAkB,KACzB,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC,SAAS,CASzD,CAAA"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kairos-es/read-postgres — the SQL `ProjectionStore`: a durable checkpoint
|
|
3
|
+
* table plus the guarded compare-and-set that brackets a read model's own view
|
|
4
|
+
* writes in one ordinary transaction (ADR-0007).
|
|
5
|
+
*
|
|
6
|
+
* Peers `@effect/sql` and needs only the GENERIC `SqlClient`, deliberately not
|
|
7
|
+
* `@effect/sql-pg` and deliberately no reserved connection: the event store owns
|
|
8
|
+
* the LISTEN, so the read side needs no dedicated connection of its own, and
|
|
9
|
+
* requiring the generic client is what lets `ensureCheckpointSchema` drop into a
|
|
10
|
+
* consumer's existing migrator instead of shipping a migrator here.
|
|
11
|
+
*
|
|
12
|
+
* A store is one `{ schema, tablePrefix }` naming pair (`ReadPostgresConfig`) —
|
|
13
|
+
* the same pair the event store is configured with — so the checkpoint table
|
|
14
|
+
* cohabits PER PREFIX and ADR-0002's "each prefix is a complete, isolated store"
|
|
15
|
+
* keeps holding across the read side.
|
|
16
|
+
*
|
|
17
|
+
* Wiring, end to end:
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* import {
|
|
21
|
+
* checkpointKey,
|
|
22
|
+
* DurableEventLog,
|
|
23
|
+
* forKey,
|
|
24
|
+
* ProjectionId,
|
|
25
|
+
* projectionLayer,
|
|
26
|
+
* } from '@kairos-es/read'
|
|
27
|
+
* import {
|
|
28
|
+
* ensureCheckpointSchema,
|
|
29
|
+
* makeSqlProjectionStore,
|
|
30
|
+
* } from '@kairos-es/read-postgres'
|
|
31
|
+
*
|
|
32
|
+
* const config = { schema: 'public', tablePrefix: 'dcb_events' }
|
|
33
|
+
*
|
|
34
|
+
* // 1. Register `ensureCheckpointSchema(config)` in YOUR migrator (or run it at
|
|
35
|
+
* // boot), beside the event-store backend's own `ensureSchema(config)`.
|
|
36
|
+
* // 2. Build the store where the read model is wired, so `commit` closes over the
|
|
37
|
+
* // acquired client and the read model itself stays free of `SqlClient`.
|
|
38
|
+
* const courseView = Layer.unwrapEffect(
|
|
39
|
+
* Effect.gen(function* () {
|
|
40
|
+
* const store = yield* makeSqlProjectionStore(config)
|
|
41
|
+
* return projectionLayer({
|
|
42
|
+
* // One store, many read models: `forKey` binds it to THIS one's checkpoint,
|
|
43
|
+
* // and from there no operation takes a key to get wrong.
|
|
44
|
+
* store: forKey(store, checkpointKey(ProjectionId.make('course-subs'))),
|
|
45
|
+
* slices,
|
|
46
|
+
* apply,
|
|
47
|
+
* })
|
|
48
|
+
* }),
|
|
49
|
+
* )
|
|
50
|
+
*
|
|
51
|
+
* // 3. Declare what the LOG is worth ONCE, beside the layer that chose it —
|
|
52
|
+
* // `DcbEventStore` exposes no durability, so the application is the only
|
|
53
|
+
* // honest source, and R2 (a durable view needs a durable log) is checked
|
|
54
|
+
* // against this. It is a required input of every projection layer, and one
|
|
55
|
+
* // declaration satisfies every one of them.
|
|
56
|
+
* const main = Layer.provide(
|
|
57
|
+
* Layer.mergeAll(courseView, studentView),
|
|
58
|
+
* Layer.mergeAll(eventStoreLayer, DurableEventLog, SerializerDefault),
|
|
59
|
+
* )
|
|
60
|
+
* ```
|
|
61
|
+
*
|
|
62
|
+
* The resulting layer requires the `SqlClient` the checkpoint (and typically the
|
|
63
|
+
* view) is written through, the `EventLogDurability` declaration above, plus
|
|
64
|
+
* whatever the read model's own `apply` needs. The checkpoint table is assumed to
|
|
65
|
+
* already exist, exactly as the event store's `layer` assumes its objects do: this
|
|
66
|
+
* package performs no DDL at runtime.
|
|
67
|
+
*
|
|
68
|
+
* `commit` is `sql.withTransaction(viewWrites *> guardedAdvance)`. The author's own
|
|
69
|
+
* `sql` statements join that transaction through the same client, and
|
|
70
|
+
* `withTransaction` is savepoint-aware, so a projection already inside a
|
|
71
|
+
* transaction composes without a second physical one. A guarded advance touching
|
|
72
|
+
* zero rows raises `CheckpointSuperseded`, whose FAILURE aborts the transaction and
|
|
73
|
+
* rolls the view writes back with it — the abort that makes the projection
|
|
74
|
+
* exactly-once. An infrastructure fault surfaces as `ProjectionStoreError` on the
|
|
75
|
+
* ERROR CHANNEL rather than as a defect, inverting the event store's rule
|
|
76
|
+
* (ADR-0002) because the read side has a supervisor above it that must distinguish
|
|
77
|
+
* retry-with-backoff from give-up.
|
|
78
|
+
*
|
|
79
|
+
* The config and the identifier guards are duplicated here rather than imported
|
|
80
|
+
* from `@kairos-es/store-postgres`, and that duplication is deliberate: peering
|
|
81
|
+
* the event-store backend would couple the read side to one specific event-store
|
|
82
|
+
* implementation, which is precisely what per-materialisation view-store selection
|
|
83
|
+
* exists to avoid. A durable view must be free to sit over any log satisfying
|
|
84
|
+
* `DcbEventStore`, so this package depends on the port (`@kairos-es/read`) and
|
|
85
|
+
* on `@effect/sql`, never on a sibling backend.
|
|
86
|
+
*/
|
|
87
|
+
export type { ReadPostgresConfig } from './config.js';
|
|
88
|
+
export { ensureCheckpointSchema } from './ensureCheckpointSchema.js';
|
|
89
|
+
export { makeSqlProjectionStore } from './projectionStore.js';
|
|
90
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AACH,YAAY,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAA;AAClD,OAAO,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAA;AACjE,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAA"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal object-naming and DDL for the Postgres `ProjectionStore`.
|
|
3
|
+
*
|
|
4
|
+
* This module is the single source of truth for the ONE object this package owns
|
|
5
|
+
* — the checkpoint table — and for the idempotent DDL that creates it. It is
|
|
6
|
+
* factored apart from the public surface for the same reason the event store's
|
|
7
|
+
* `internal/ddl.ts` is: the name is derived ONCE, from `{ schema, tablePrefix }`,
|
|
8
|
+
* and reused by both `ensureCheckpointSchema` (the migration payload) and the
|
|
9
|
+
* runtime statements in `projectionStore.ts`, so a migration and a query can
|
|
10
|
+
* never disagree about what the table is called.
|
|
11
|
+
*
|
|
12
|
+
* The checkpoint table derives from the SAME `{ schema, tablePrefix }` pair the
|
|
13
|
+
* event store uses, so a checkpoint table cohabits PER PREFIX: two prefixes are
|
|
14
|
+
* two complete, isolated stores (ADR-0002) with two independent checkpoint
|
|
15
|
+
* namespaces, and that isolation keeps holding once the read side is added rather
|
|
16
|
+
* than being punctured by one shared checkpoint table.
|
|
17
|
+
*
|
|
18
|
+
* WHY names are never string-concatenated into a query: every identifier is
|
|
19
|
+
* pre-quoted here (`quoteQualified` for the dotted table, `quoteAtom` for a bare
|
|
20
|
+
* atom) and embedded as literal text into a `sql.unsafe(text, params)` statement,
|
|
21
|
+
* exactly as the event store's read plans are; VALUES are always bound `$n`
|
|
22
|
+
* parameters and never interpolated. A hostile or merely awkward
|
|
23
|
+
* `schema`/`tablePrefix` therefore cannot inject SQL or collide with a reserved
|
|
24
|
+
* word.
|
|
25
|
+
*
|
|
26
|
+
* The runtime deliberately does NOT route these names through `sql(identifier)`.
|
|
27
|
+
* This package takes the CONSUMER's generic `SqlClient`, which may carry a
|
|
28
|
+
* query-name transform (`@effect/sql` applies it to every `sql(identifier)`), so
|
|
29
|
+
* a camel-casing consumer would silently rewrite `last_position` to
|
|
30
|
+
* `lastPosition` and address a column that does not exist. Pre-quoted text inside
|
|
31
|
+
* `sql.unsafe` is transform-proof; the result side is handled by
|
|
32
|
+
* `withoutTransforms()` in `projectionStore.ts`.
|
|
33
|
+
*/
|
|
34
|
+
import type { SqlClient, Statement } from '@effect/sql';
|
|
35
|
+
/**
|
|
36
|
+
* The one object a read-side config owns. An interface rather than a bare string
|
|
37
|
+
* so the `{ schema, tablePrefix }` atoms stay available to error messages and so
|
|
38
|
+
* a second derived object (should one ever be needed) is an additive change here
|
|
39
|
+
* rather than a new parameter at every call site.
|
|
40
|
+
*/
|
|
41
|
+
export interface ResolvedCheckpointNames {
|
|
42
|
+
readonly schema: string;
|
|
43
|
+
readonly tablePrefix: string;
|
|
44
|
+
/** `<schema>.<prefix>_checkpoints` — the scalar checkpoint table. */
|
|
45
|
+
readonly checkpointTable: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Default schema when the config omits one. Identical to the event store's
|
|
49
|
+
* default, on purpose: the common wiring is one `{ schema, tablePrefix }` pair
|
|
50
|
+
* used for both sides, so both defaults must agree or a caller omitting the
|
|
51
|
+
* config would silently split the log and its checkpoints across two schemas.
|
|
52
|
+
*/
|
|
53
|
+
export declare const DEFAULT_SCHEMA = "public";
|
|
54
|
+
/** Default table prefix when the config omits one — again the event store's. */
|
|
55
|
+
export declare const DEFAULT_TABLE_PREFIX = "dcb_events";
|
|
56
|
+
/**
|
|
57
|
+
* The first derived identifier that exceeds 63 bytes, as a human-readable
|
|
58
|
+
* message, or `undefined` when every name fits. Shared by
|
|
59
|
+
* `resolveCheckpointNames` (which throws) and the `ReadPostgresConfig` schema
|
|
60
|
+
* filter (which fails validation), so the byte bound is enforced identically at
|
|
61
|
+
* both the name-derivation and the config-decode boundary.
|
|
62
|
+
*
|
|
63
|
+
* The bound is checked INDEPENDENTLY of the event store's: this package measures
|
|
64
|
+
* only the names IT materialises, so a prefix accepted there may be rejected here
|
|
65
|
+
* (and vice versa). Each package guards its own names, which is what lets the
|
|
66
|
+
* read side stay uncoupled from any particular event-store backend.
|
|
67
|
+
*/
|
|
68
|
+
export declare const oversizedIdentifier: (config: {
|
|
69
|
+
readonly schema?: string;
|
|
70
|
+
readonly tablePrefix?: string;
|
|
71
|
+
}) => string | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Derive the checkpoint table name from `{ schema, tablePrefix }`. The table
|
|
74
|
+
* carries its schema explicitly so `quoteQualified` dot-splits it into the two
|
|
75
|
+
* atoms Postgres expects.
|
|
76
|
+
*
|
|
77
|
+
* Throws at construction if a derived identifier would be silently truncated (see
|
|
78
|
+
* `oversizedIdentifier`) — a loud failure at the single choke point beats an
|
|
79
|
+
* invisible cross-prefix checkpoint collision later.
|
|
80
|
+
*/
|
|
81
|
+
export declare const resolveCheckpointNames: (config: {
|
|
82
|
+
readonly schema?: string;
|
|
83
|
+
readonly tablePrefix?: string;
|
|
84
|
+
}) => ResolvedCheckpointNames;
|
|
85
|
+
/**
|
|
86
|
+
* Quote a possibly-qualified identifier for embedding as literal SQL text.
|
|
87
|
+
* Splits on `.` and double-quotes each atom, mirroring what `sql(identifier)`
|
|
88
|
+
* emits, so a pre-quoted name and an `sql(identifier)` reference resolve to
|
|
89
|
+
* exactly the same object. Any embedded `"` is doubled per SQL
|
|
90
|
+
* identifier-quoting rules. (The dot-split is why `ReadPostgresConfig` bars a dot
|
|
91
|
+
* inside either atom: an embedded dot would silently re-partition the name into
|
|
92
|
+
* the wrong schema.)
|
|
93
|
+
*/
|
|
94
|
+
export declare const quoteQualified: (name: string) => string;
|
|
95
|
+
/**
|
|
96
|
+
* The checkpoint table's columns, PRE-QUOTED, as one shared descriptor.
|
|
97
|
+
*
|
|
98
|
+
* Shared by the DDL here and by every runtime statement in `projectionStore.ts`,
|
|
99
|
+
* so a column rename cannot half-land: the table it creates and the statements
|
|
100
|
+
* that address it read the same constant. Pre-quoted because `partition` is
|
|
101
|
+
* Postgres's own partitioning vocabulary — it parses unquoted today, but quoting
|
|
102
|
+
* removes the question, and quoting all three keeps the emitted SQL uniform.
|
|
103
|
+
*
|
|
104
|
+
* These names are FIXED and owned by this package (never config-derived), which
|
|
105
|
+
* is why they are plain constants rather than something the caller can influence.
|
|
106
|
+
*/
|
|
107
|
+
export declare const CHECKPOINT_COLUMNS: {
|
|
108
|
+
/** The read model's `ProjectionId`. */
|
|
109
|
+
readonly projection: string;
|
|
110
|
+
/** The read model's `PartitionId` (`default` while sharding is cut). */
|
|
111
|
+
readonly partition: string;
|
|
112
|
+
/**
|
|
113
|
+
* The scalar checkpoint: `bigint`, because a `Position` is 64-bit and exceeds
|
|
114
|
+
* JS `number` safety at 2^53. It is read back as a decimal STRING and
|
|
115
|
+
* reconstructed with `BigInt(...)`, never coerced through `number`.
|
|
116
|
+
*/
|
|
117
|
+
readonly lastPosition: string;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* The ordered, idempotent DDL that creates (or refreshes) the checkpoint table.
|
|
121
|
+
* Each entry is a `sql.unsafe` statement string; no VALUES are involved (pure
|
|
122
|
+
* DDL), and every identifier is pre-quoted to match the runtime's resolution.
|
|
123
|
+
*
|
|
124
|
+
* Notes on the shape, both load-bearing:
|
|
125
|
+
*
|
|
126
|
+
* - `PRIMARY KEY (projection, partition)` is what makes `readCheckpoint` an index
|
|
127
|
+
* lookup AND supplies the arbiter index the first-commit upsert's
|
|
128
|
+
* `ON CONFLICT (projection, partition)` names. Without it that upsert would not
|
|
129
|
+
* compile, so the key is not merely hygiene.
|
|
130
|
+
* - `last_position bigint NOT NULL` with NO row for a never-committed key: a key
|
|
131
|
+
* reads back as `ORIGIN` by ABSENCE, so "never committed" and "reset" are one
|
|
132
|
+
* state (`resetCheckpoint` deletes the row rather than zeroing it), exactly as
|
|
133
|
+
* the in-memory store's map deletes its entry.
|
|
134
|
+
* - No autovacuum storage parameters, unlike the event store's tables. Those are
|
|
135
|
+
* tuned for an append-heavy, effectively-immutable log; this table is a handful
|
|
136
|
+
* of hot-UPDATEd rows, where the stock thresholds already fire aggressively.
|
|
137
|
+
*/
|
|
138
|
+
export declare const checkpointDdlStatements: (names: ResolvedCheckpointNames) => ReadonlyArray<string>;
|
|
139
|
+
/**
|
|
140
|
+
* Run the ordered DDL against the generic `SqlClient`. Kept here (not in the
|
|
141
|
+
* public `ensureCheckpointSchema`) so the same builder feeds both the public
|
|
142
|
+
* migration payload and any internal test setup. Each statement is a separate
|
|
143
|
+
* `sql.unsafe` call so a driver that rejects multi-statement strings still works.
|
|
144
|
+
*/
|
|
145
|
+
export declare const runCheckpointDdl: (sql: SqlClient.SqlClient, names: ResolvedCheckpointNames) => ReadonlyArray<Statement.Statement<unknown>>;
|
|
146
|
+
//# sourceMappingURL=ddl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl.d.ts","sourceRoot":"","sources":["../../../src/internal/ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAEvD;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,qEAAqE;IACrE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAA;CACjC;AAED;;;;;GAKG;AACH,eAAO,MAAM,cAAc,WAAW,CAAA;AACtC,gFAAgF;AAChF,eAAO,MAAM,oBAAoB,eAAe,CAAA;AA0ChD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,mBAAmB,GAAI,QAAQ;IAC1C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,KAAG,MAAM,GAAG,SAQZ,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,GAAI,QAAQ;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,KAAG,uBAYH,CAAA;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,MAAM,KAAG,MAIhC,CAAA;AAQd;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB;IAC7B,uCAAuC;;IAEvC,wEAAwE;;IAExE;;;;OAIG;;CAEK,CAAA;AAEV;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAO,uBAAuB,KAC7B,aAAa,CAAC,MAAM,CAiBtB,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,GAC3B,KAAK,SAAS,CAAC,SAAS,EACxB,OAAO,uBAAuB,KAC7B,aAAa,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,CAAC,CAC6B,CAAA"}
|