@kairos-es/feed-postgres 0.0.0-stage → 1.0.0-alpha.6
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 +84 -2
- package/dist/cjs/config.js +46 -0
- package/dist/cjs/config.js.map +1 -0
- package/dist/cjs/ensureFeedSchema.js +39 -0
- package/dist/cjs/ensureFeedSchema.js.map +1 -0
- package/dist/cjs/feed.js +176 -0
- package/dist/cjs/feed.js.map +1 -0
- package/dist/cjs/index.js +84 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/internal/context.js +45 -0
- package/dist/cjs/internal/context.js.map +1 -0
- package/dist/cjs/internal/ddl.js +149 -0
- package/dist/cjs/internal/ddl.js.map +1 -0
- package/dist/cjs/internal/statements.js +270 -0
- package/dist/cjs/internal/statements.js.map +1 -0
- package/dist/cjs/lease.js +342 -0
- package/dist/cjs/lease.js.map +1 -0
- package/dist/cjs/syncStore.js +179 -0
- package/dist/cjs/syncStore.js.map +1 -0
- package/dist/cjs/withFeed.js +51 -0
- package/dist/cjs/withFeed.js.map +1 -0
- package/dist/dts-cjs/config.d.ts +37 -0
- package/dist/dts-cjs/config.d.ts.map +1 -0
- package/dist/dts-cjs/ensureFeedSchema.d.ts +24 -0
- package/dist/dts-cjs/ensureFeedSchema.d.ts.map +1 -0
- package/dist/dts-cjs/feed.d.ts +81 -0
- package/dist/dts-cjs/feed.d.ts.map +1 -0
- package/dist/dts-cjs/index.d.ts +50 -0
- package/dist/dts-cjs/index.d.ts.map +1 -0
- package/dist/dts-cjs/internal/context.d.ts +28 -0
- package/dist/dts-cjs/internal/context.d.ts.map +1 -0
- package/dist/dts-cjs/internal/ddl.d.ts +99 -0
- package/dist/dts-cjs/internal/ddl.d.ts.map +1 -0
- package/dist/dts-cjs/internal/statements.d.ts +127 -0
- package/dist/dts-cjs/internal/statements.d.ts.map +1 -0
- package/dist/dts-cjs/lease.d.ts +138 -0
- package/dist/dts-cjs/lease.d.ts.map +1 -0
- package/dist/dts-cjs/package.json +3 -0
- package/dist/dts-cjs/syncStore.d.ts +57 -0
- package/dist/dts-cjs/syncStore.d.ts.map +1 -0
- package/dist/dts-cjs/withFeed.d.ts +69 -0
- package/dist/dts-cjs/withFeed.d.ts.map +1 -0
- package/dist/dts-esm/config.d.ts +37 -0
- package/dist/dts-esm/config.d.ts.map +1 -0
- package/dist/dts-esm/ensureFeedSchema.d.ts +24 -0
- package/dist/dts-esm/ensureFeedSchema.d.ts.map +1 -0
- package/dist/dts-esm/feed.d.ts +81 -0
- package/dist/dts-esm/feed.d.ts.map +1 -0
- package/dist/dts-esm/index.d.ts +50 -0
- package/dist/dts-esm/index.d.ts.map +1 -0
- package/dist/dts-esm/internal/context.d.ts +28 -0
- package/dist/dts-esm/internal/context.d.ts.map +1 -0
- package/dist/dts-esm/internal/ddl.d.ts +99 -0
- package/dist/dts-esm/internal/ddl.d.ts.map +1 -0
- package/dist/dts-esm/internal/statements.d.ts +127 -0
- package/dist/dts-esm/internal/statements.d.ts.map +1 -0
- package/dist/dts-esm/lease.d.ts +138 -0
- package/dist/dts-esm/lease.d.ts.map +1 -0
- package/dist/dts-esm/package.json +3 -0
- package/dist/dts-esm/syncStore.d.ts +57 -0
- package/dist/dts-esm/syncStore.d.ts.map +1 -0
- package/dist/dts-esm/withFeed.d.ts +69 -0
- package/dist/dts-esm/withFeed.d.ts.map +1 -0
- package/dist/esm/config.js +39 -0
- package/dist/esm/config.js.map +1 -0
- package/dist/esm/ensureFeedSchema.js +31 -0
- package/dist/esm/ensureFeedSchema.js.map +1 -0
- package/dist/esm/feed.js +166 -0
- package/dist/esm/feed.js.map +1 -0
- package/dist/esm/index.js +50 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/internal/context.js +34 -0
- package/dist/esm/internal/context.js.map +1 -0
- package/dist/esm/internal/ddl.js +136 -0
- package/dist/esm/internal/ddl.js.map +1 -0
- package/dist/esm/internal/statements.js +262 -0
- package/dist/esm/internal/statements.js.map +1 -0
- package/dist/esm/lease.js +330 -0
- package/dist/esm/lease.js.map +1 -0
- package/dist/esm/package.json +4 -0
- package/dist/esm/syncStore.js +171 -0
- package/dist/esm/syncStore.js.map +1 -0
- package/dist/esm/withFeed.js +44 -0
- package/dist/esm/withFeed.js.map +1 -0
- package/package.json +35 -3
- package/src/config.ts +46 -0
- package/src/ensureFeedSchema.ts +32 -0
- package/src/feed.ts +284 -0
- package/src/index.ts +67 -0
- package/src/internal/context.ts +44 -0
- package/src/internal/ddl.ts +233 -0
- package/src/internal/statements.ts +387 -0
- package/src/lease.ts +503 -0
- package/src/syncStore.ts +340 -0
- package/src/withFeed.ts +148 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/** The default schema, identical to the event store's and the read side's. */
|
|
2
|
+
export const DEFAULT_SCHEMA = 'public';
|
|
3
|
+
/** The default table prefix, identical to the event store's and the read side's. */
|
|
4
|
+
export const DEFAULT_TABLE_PREFIX = 'dcb_events';
|
|
5
|
+
const MAX_IDENTIFIER_BYTES = 63;
|
|
6
|
+
const utf8ByteLength = value => new TextEncoder().encode(value).length;
|
|
7
|
+
const suffixes = {
|
|
8
|
+
rows: '_feed_rows',
|
|
9
|
+
rowsSeqIndex: '_feed_rows_seq_idx',
|
|
10
|
+
sequence: '_feed_seq',
|
|
11
|
+
generations: '_feed_generations',
|
|
12
|
+
cursors: '_feed_cursors',
|
|
13
|
+
parked: '_feed_parked'
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The single-atom identifiers Postgres materialises for a config. The index is
|
|
17
|
+
* measured, unlike a primary key's implicit one, because it is named explicitly
|
|
18
|
+
* and `CREATE INDEX IF NOT EXISTS` would skip a truncated name that collides with
|
|
19
|
+
* another prefix's index instead of reporting it.
|
|
20
|
+
*/
|
|
21
|
+
const derivedAtoms = config => {
|
|
22
|
+
const schema = config.schema ?? DEFAULT_SCHEMA;
|
|
23
|
+
const prefix = config.tablePrefix ?? DEFAULT_TABLE_PREFIX;
|
|
24
|
+
return [['schema', schema], ['feed rows table', `${prefix}${suffixes.rows}`], ['feed rows index', `${prefix}${suffixes.rowsSeqIndex}`], ['feed sequence', `${prefix}${suffixes.sequence}`], ['feed generations table', `${prefix}${suffixes.generations}`], ['feed cursors table', `${prefix}${suffixes.cursors}`], ['feed parked table', `${prefix}${suffixes.parked}`]];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* The first derived identifier over Postgres's 63-byte limit, as a message, or
|
|
28
|
+
* `undefined` when every name fits. Postgres truncates an over-long identifier
|
|
29
|
+
* with only a notice, so two prefixes sharing a 63-byte stem would silently share
|
|
30
|
+
* one feed table; refusing at the boundary beats that.
|
|
31
|
+
*/
|
|
32
|
+
export const oversizedIdentifier = config => {
|
|
33
|
+
for (const [label, atom] of derivedAtoms(config)) {
|
|
34
|
+
const bytes = utf8ByteLength(atom);
|
|
35
|
+
if (bytes > MAX_IDENTIFIER_BYTES) {
|
|
36
|
+
return `the ${label} identifier "${atom}" is ${bytes} bytes, over Postgres's ${MAX_IDENTIFIER_BYTES}-byte limit; it would be silently truncated, letting two prefixes share one feed object`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
};
|
|
41
|
+
/** Derive every name from the pair; throws on an over-long identifier. */
|
|
42
|
+
export const resolveFeedNames = config => {
|
|
43
|
+
const problem = oversizedIdentifier(config);
|
|
44
|
+
if (problem !== undefined) {
|
|
45
|
+
throw new Error(`resolveFeedNames: ${problem}`);
|
|
46
|
+
}
|
|
47
|
+
const schema = config.schema ?? DEFAULT_SCHEMA;
|
|
48
|
+
const tablePrefix = config.tablePrefix ?? DEFAULT_TABLE_PREFIX;
|
|
49
|
+
const qualified = suffix => `${schema}.${tablePrefix}${suffix}`;
|
|
50
|
+
return {
|
|
51
|
+
schema,
|
|
52
|
+
tablePrefix,
|
|
53
|
+
rowsTable: qualified(suffixes.rows),
|
|
54
|
+
rowsSeqIndex: `${tablePrefix}${suffixes.rowsSeqIndex}`,
|
|
55
|
+
sequence: qualified(suffixes.sequence),
|
|
56
|
+
generationsTable: qualified(suffixes.generations),
|
|
57
|
+
cursorsTable: qualified(suffixes.cursors),
|
|
58
|
+
parkedTable: qualified(suffixes.parked)
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
/** Quote a possibly qualified identifier, atom by atom, for literal SQL text. */
|
|
62
|
+
export const quoteQualified = name => name.split('.').map(atom => quoteAtom(atom)).join('.');
|
|
63
|
+
/** Quote one identifier atom, doubling any embedded `"`. */
|
|
64
|
+
export const quoteAtom = name => `"${name.replace(/"/g, '""')}"`;
|
|
65
|
+
/**
|
|
66
|
+
* The quoted sequence name as a SQL string LITERAL, for `nextval('…')`. The
|
|
67
|
+
* literal is parsed as a `regclass`, which honours the double quotes inside it, so
|
|
68
|
+
* an awkward prefix resolves to exactly the object the DDL created; any `'` in it
|
|
69
|
+
* is doubled so the literal cannot be closed early.
|
|
70
|
+
*/
|
|
71
|
+
export const sequenceLiteral = names => `'${quoteQualified(names.sequence).replace(/'/g, "''")}'`;
|
|
72
|
+
/**
|
|
73
|
+
* The ordered, idempotent DDL. Every statement is `IF NOT EXISTS`, so a second
|
|
74
|
+
* run against existing objects is a no-op that keeps every row.
|
|
75
|
+
*
|
|
76
|
+
* Shapes that are load-bearing:
|
|
77
|
+
*
|
|
78
|
+
* - `_feed_rows` is keyed `(feed, key)`: one row per record, which is what
|
|
79
|
+
* "coalesced, latest wins" means in storage, and the arbiter the writer's
|
|
80
|
+
* `ON CONFLICT` names. `doc` is NULL for a tombstone; a document that encodes to
|
|
81
|
+
* JSON `null` is the jsonb value `null`, which is not SQL NULL, so the two
|
|
82
|
+
* cannot be confused.
|
|
83
|
+
* - The `(feed, seq)` index is what `changesAfter` pages by.
|
|
84
|
+
* - ONE sequence serves every feed in the prefix. A feed's `seq` values therefore
|
|
85
|
+
* have gaps, which the cursor never minds: it compares, never counts.
|
|
86
|
+
* - `_feed_cursors` holds the cursor AND the lease, so the cursor's
|
|
87
|
+
* compare-and-set can fence on the lease holder in the same row it moves. The
|
|
88
|
+
* cursor is `(generation, seq)`, both NULL before a sink's first sync and never
|
|
89
|
+
* one without the other, which the `CHECK` holds. `last_advance_at` is when
|
|
90
|
+
* the cursor last moved, NULL until it first does; taking a lease leaves it
|
|
91
|
+
* alone, so a sink registered and never run shows as one.
|
|
92
|
+
* - `_feed_generations` holds the checkpoint key a feed is bound to, so binding
|
|
93
|
+
* one feed to two materialisations is refused across processes, not only within
|
|
94
|
+
* one.
|
|
95
|
+
*/
|
|
96
|
+
export const feedDdlStatements = names => {
|
|
97
|
+
const rows = quoteQualified(names.rowsTable);
|
|
98
|
+
const generations = quoteQualified(names.generationsTable);
|
|
99
|
+
const cursors = quoteQualified(names.cursorsTable);
|
|
100
|
+
const parked = quoteQualified(names.parkedTable);
|
|
101
|
+
return [`CREATE SCHEMA IF NOT EXISTS ${quoteAtom(names.schema)};`, `CREATE SEQUENCE IF NOT EXISTS ${quoteQualified(names.sequence)};`, `CREATE TABLE IF NOT EXISTS ${rows} (
|
|
102
|
+
"feed" text NOT NULL,
|
|
103
|
+
"key" text NOT NULL,
|
|
104
|
+
"doc" jsonb NULL,
|
|
105
|
+
"seq" bigint NOT NULL,
|
|
106
|
+
"event_position" bigint NOT NULL,
|
|
107
|
+
PRIMARY KEY ("feed", "key")
|
|
108
|
+
);`, `CREATE INDEX IF NOT EXISTS ${quoteAtom(names.rowsSeqIndex)}
|
|
109
|
+
ON ${rows} ("feed", "seq");`, `CREATE TABLE IF NOT EXISTS ${generations} (
|
|
110
|
+
"feed" text PRIMARY KEY,
|
|
111
|
+
"generation" bigint NOT NULL,
|
|
112
|
+
"checkpoint_projection" text NULL,
|
|
113
|
+
"checkpoint_partition" text NULL
|
|
114
|
+
);`, `CREATE TABLE IF NOT EXISTS ${cursors} (
|
|
115
|
+
"feed" text NOT NULL,
|
|
116
|
+
"sink" text NOT NULL,
|
|
117
|
+
"generation" bigint NULL,
|
|
118
|
+
"seq" bigint NULL,
|
|
119
|
+
"lease_holder" text NULL,
|
|
120
|
+
"lease_expires_at" timestamptz NULL,
|
|
121
|
+
"last_advance_at" timestamptz NULL,
|
|
122
|
+
PRIMARY KEY ("feed", "sink"),
|
|
123
|
+
CHECK (("generation" IS NULL) = ("seq" IS NULL))
|
|
124
|
+
);`, `CREATE TABLE IF NOT EXISTS ${parked} (
|
|
125
|
+
"feed" text NOT NULL,
|
|
126
|
+
"sink" text NOT NULL,
|
|
127
|
+
"key" text NOT NULL,
|
|
128
|
+
"seq" bigint NOT NULL,
|
|
129
|
+
"reason" text NOT NULL,
|
|
130
|
+
"parked_at" timestamptz NOT NULL,
|
|
131
|
+
PRIMARY KEY ("feed", "sink", "key")
|
|
132
|
+
);`];
|
|
133
|
+
};
|
|
134
|
+
/** The DDL as statements on `sql`, one `sql.unsafe` each, in order. */
|
|
135
|
+
export const runFeedDdl = (sql, names) => feedDdlStatements(names).map(statement => sql.unsafe(statement));
|
|
136
|
+
//# sourceMappingURL=ddl.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ddl.js","names":["DEFAULT_SCHEMA","DEFAULT_TABLE_PREFIX","MAX_IDENTIFIER_BYTES","utf8ByteLength","value","TextEncoder","encode","length","suffixes","rows","rowsSeqIndex","sequence","generations","cursors","parked","derivedAtoms","config","schema","prefix","tablePrefix","oversizedIdentifier","label","atom","bytes","undefined","resolveFeedNames","problem","Error","qualified","suffix","rowsTable","generationsTable","cursorsTable","parkedTable","quoteQualified","name","split","map","quoteAtom","join","replace","sequenceLiteral","names","feedDdlStatements","runFeedDdl","sql","statement","unsafe"],"sources":["../../../src/internal/ddl.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every runtime statement text the Postgres change feed issues, compiled once per
|
|
3
|
+
* name set, and the conventions their parameters follow.
|
|
4
|
+
*
|
|
5
|
+
* One module, so what the feed sends to the database is auditable in one file.
|
|
6
|
+
* The Effect wiring that issues them lives in `feed.ts`, `syncStore.ts`, `lease.ts`
|
|
7
|
+
* and `withFeed.ts`; none of those writes SQL of its own.
|
|
8
|
+
*
|
|
9
|
+
* ## Parameter conventions
|
|
10
|
+
*
|
|
11
|
+
* - Every 64-bit value (`seq`, `generation`, `event_position`) is bound as a
|
|
12
|
+
* decimal STRING and cast (`$n::bigint`), and read back as text and turned into
|
|
13
|
+
* a `bigint`, so no value ever passes through a JS `number` and no driver's
|
|
14
|
+
* integer handling can round one.
|
|
15
|
+
* - A batch of rows is bound as ONE `jsonb` parameter and unpacked server-side
|
|
16
|
+
* (`jsonb_to_recordset`, `jsonb_array_elements_text`), so a batch is one
|
|
17
|
+
* statement whatever its size and no array-binding support is asked of the
|
|
18
|
+
* driver.
|
|
19
|
+
* - A timestamp is bound as epoch MILLISECONDS from Effect's `Clock`, and read back
|
|
20
|
+
* the same way. Lease expiry is judged against the worker's Clock rather than the
|
|
21
|
+
* database's `now()`, so a `TestClock` governs it in tests as it governs every
|
|
22
|
+
* other time in this repository; the cost is that workers on several hosts must
|
|
23
|
+
* keep their clocks within a small fraction of the lease length of each other.
|
|
24
|
+
* - A document is read back as `doc::text` and parsed here, so a consumer's JSON
|
|
25
|
+
* parsing or name transforms cannot reshape it on the way.
|
|
26
|
+
* - A column read back as text is ALIASED to its own name, so every `ORDER BY`
|
|
27
|
+
* qualifies the column with its table alias: an unqualified name in `ORDER BY`
|
|
28
|
+
* resolves to the output column first, and `seq` would then sort as text, with
|
|
29
|
+
* `10` before `4`.
|
|
30
|
+
*
|
|
31
|
+
* ## The feed lock
|
|
32
|
+
*
|
|
33
|
+
* The generations row of a feed doubles as its WRITE LOCK. A feed's `seq` is
|
|
34
|
+
* ascending in commit order only while one transaction at a time takes `seq`
|
|
35
|
+
* values for that feed, and the subscribe machine silently loses a row whose `seq`
|
|
36
|
+
* commits below one already read (ADR-0009). The materialisation is one writer, but
|
|
37
|
+
* two other things take `seq` values for a feed outside its commit: `retryParked`
|
|
38
|
+
* re-stamps a row, and a reset empties the feed. So the writer takes the row
|
|
39
|
+
* `FOR SHARE` before it takes any `seq`, `retryParked` takes it `FOR UPDATE`, and
|
|
40
|
+
* the reset UPDATEs it; each waits for the others to commit, and `seq` order is
|
|
41
|
+
* commit order for all three.
|
|
42
|
+
*/
|
|
43
|
+
import { quoteQualified, sequenceLiteral } from "./ddl.js";
|
|
44
|
+
/** Compile every statement for `names`. */
|
|
45
|
+
export const compileFeedStatements = names => {
|
|
46
|
+
const rows = quoteQualified(names.rowsTable);
|
|
47
|
+
const generations = quoteQualified(names.generationsTable);
|
|
48
|
+
const cursors = quoteQualified(names.cursorsTable);
|
|
49
|
+
const parked = quoteQualified(names.parkedTable);
|
|
50
|
+
const nextSeq = `nextval(${sequenceLiteral(names)})`;
|
|
51
|
+
const msToTimestamp = param => `to_timestamp(${param}::double precision / 1000)`;
|
|
52
|
+
const timestampToMs = column => `round(extract(epoch from ${column}) * 1000)::bigint::text`;
|
|
53
|
+
const advisoryKeyOf = param => `hashtextextended(${param}, 0)`;
|
|
54
|
+
const advisoryKey = advisoryKeyOf('$1');
|
|
55
|
+
// Whether THIS session holds the advisory lock named by `param`. A bigint
|
|
56
|
+
// advisory key is stored in `pg_locks` split into two 32-bit halves, `classid`
|
|
57
|
+
// the high and `objid` the low, with `objsubid` 1.
|
|
58
|
+
const sessionHoldsLock = param => `EXISTS (
|
|
59
|
+
SELECT 1 FROM pg_locks l
|
|
60
|
+
WHERE l.locktype = 'advisory'
|
|
61
|
+
AND l.pid = pg_backend_pid()
|
|
62
|
+
AND l.granted
|
|
63
|
+
AND l.objsubid = 1
|
|
64
|
+
AND l.classid = ((${advisoryKeyOf(param)} >> 32) & 4294967295)::oid
|
|
65
|
+
AND l.objid = (${advisoryKeyOf(param)} & 4294967295)::oid
|
|
66
|
+
)`;
|
|
67
|
+
return {
|
|
68
|
+
ensureGeneration: `INSERT INTO ${generations} ("feed", "generation")
|
|
69
|
+
VALUES ($1, 0)
|
|
70
|
+
ON CONFLICT ("feed") DO NOTHING`,
|
|
71
|
+
lockFeedShared: `SELECT 1 AS locked FROM ${generations}
|
|
72
|
+
WHERE "feed" = $1 FOR SHARE`,
|
|
73
|
+
// One `nextval` per INSERTED candidate, kept by the update arm through
|
|
74
|
+
// `excluded.seq`, so an unchanged document burns a value and writes nothing:
|
|
75
|
+
// the `WHERE … IS DISTINCT FROM` arm compares the normalised jsonb, so key
|
|
76
|
+
// order is no change.
|
|
77
|
+
upsertRows: `INSERT INTO ${rows} AS r ("feed", "key", "doc", "seq", "event_position")
|
|
78
|
+
SELECT $1, t."key", t."doc", ${nextSeq}, $2::bigint
|
|
79
|
+
FROM jsonb_to_recordset($3::jsonb) AS t("key" text, "doc" jsonb)
|
|
80
|
+
ON CONFLICT ("feed", "key") DO UPDATE
|
|
81
|
+
SET "doc" = excluded."doc",
|
|
82
|
+
"seq" = excluded."seq",
|
|
83
|
+
"event_position" = excluded."event_position"
|
|
84
|
+
WHERE r."doc" IS DISTINCT FROM excluded."doc"`,
|
|
85
|
+
// An UPDATE, never an insert: a remove of an absent key writes nothing, and
|
|
86
|
+
// neither does a remove of a key that is already a tombstone.
|
|
87
|
+
tombstoneRows: `UPDATE ${rows} AS r
|
|
88
|
+
SET "doc" = NULL, "seq" = ${nextSeq}, "event_position" = $2::bigint
|
|
89
|
+
FROM jsonb_array_elements_text($3::jsonb) AS t("key")
|
|
90
|
+
WHERE r."feed" = $1 AND r."key" = t."key" AND r."doc" IS NOT NULL`,
|
|
91
|
+
selectGeneration: `SELECT "generation"::text AS generation
|
|
92
|
+
FROM ${generations} WHERE "feed" = $1`,
|
|
93
|
+
changesAfter: `SELECT "key" AS key, "doc"::text AS doc, "seq"::text AS seq,
|
|
94
|
+
"event_position"::text AS event_position
|
|
95
|
+
FROM ${rows} AS r
|
|
96
|
+
WHERE r."feed" = $1 AND r."seq" > $2::bigint
|
|
97
|
+
ORDER BY r."seq"
|
|
98
|
+
LIMIT $3`,
|
|
99
|
+
selectCursor: `SELECT "generation"::text AS generation, "seq"::text AS seq
|
|
100
|
+
FROM ${cursors} WHERE "feed" = $1 AND "sink" = $2`,
|
|
101
|
+
// Guarded like a checkpoint AND fenced on the holder: zero rows means the
|
|
102
|
+
// stored cursor was not the expected one, or the lease is somebody else's.
|
|
103
|
+
casCursor: `UPDATE ${cursors}
|
|
104
|
+
SET "generation" = $4::bigint, "seq" = $5::bigint,
|
|
105
|
+
"last_advance_at" = ${msToTimestamp('$8')}
|
|
106
|
+
WHERE "feed" = $1 AND "sink" = $2 AND "lease_holder" = $3
|
|
107
|
+
AND "generation" IS NOT DISTINCT FROM $6::bigint
|
|
108
|
+
AND "seq" IS NOT DISTINCT FROM $7::bigint
|
|
109
|
+
RETURNING 1 AS landed`,
|
|
110
|
+
clearParked: `DELETE FROM ${parked} WHERE "feed" = $1 AND "sink" = $2`,
|
|
111
|
+
unparkKeys: `DELETE FROM ${parked}
|
|
112
|
+
WHERE "feed" = $1 AND "sink" = $2
|
|
113
|
+
AND "key" IN (SELECT jsonb_array_elements_text($3::jsonb))`,
|
|
114
|
+
parkKeys: `INSERT INTO ${parked} ("feed", "sink", "key", "seq", "reason", "parked_at")
|
|
115
|
+
SELECT $1, $2, t."key", t."seq"::bigint, t."reason", ${msToTimestamp('t."parked_at_ms"')}
|
|
116
|
+
FROM jsonb_to_recordset($3::jsonb)
|
|
117
|
+
AS t("key" text, "seq" text, "reason" text, "parked_at_ms" text)
|
|
118
|
+
ON CONFLICT ("feed", "sink", "key") DO UPDATE
|
|
119
|
+
SET "seq" = excluded."seq", "reason" = excluded."reason",
|
|
120
|
+
"parked_at" = excluded."parked_at"`,
|
|
121
|
+
selectParked: `SELECT "key" AS key, "seq"::text AS seq, "reason" AS reason,
|
|
122
|
+
${timestampToMs('"parked_at"')} AS parked_at_ms
|
|
123
|
+
FROM ${parked} AS p
|
|
124
|
+
WHERE p."feed" = $1 AND p."sink" = $2
|
|
125
|
+
ORDER BY p."seq", p."key"`,
|
|
126
|
+
lockFeedExclusive: `SELECT 1 AS locked FROM ${generations}
|
|
127
|
+
WHERE "feed" = $1 FOR UPDATE`,
|
|
128
|
+
unparkOne: `DELETE FROM ${parked}
|
|
129
|
+
WHERE "feed" = $1 AND "sink" = $2 AND "key" = $3
|
|
130
|
+
RETURNING 1 AS unparked`,
|
|
131
|
+
restampRow: `UPDATE ${rows} SET "seq" = ${nextSeq}
|
|
132
|
+
WHERE "feed" = $1 AND "key" = $2`,
|
|
133
|
+
// Every sink REGISTERED on the feed counts — every cursor row, whatever
|
|
134
|
+
// generation its cursor was saved at — and the lowest of their cursors bounds
|
|
135
|
+
// what goes. A cursor at an older generation needs no arm of its own:
|
|
136
|
+
// `FeedSyncStore.ts`'s tombstone section owns why the minimum already holds it
|
|
137
|
+
// below every row, and this store keeps that section's premise, since a reset
|
|
138
|
+
// (`withFeed.ts`) moves the generation and empties the feed in one
|
|
139
|
+
// transaction and the one sequence never goes back. A sink with no cursor is
|
|
140
|
+
// resyncing from the origin and blocks collection outright, which `bool_or`
|
|
141
|
+
// says. No cursor row at all leaves both aggregates NULL, so the guard is NULL
|
|
142
|
+
// and nothing goes. The cursors are read in the delete's own statement
|
|
143
|
+
// snapshot, so a cursor saved after it can only be one that blocked here
|
|
144
|
+
// (nothing deleted) or one that moved up. A sink whose cursor row is CREATED
|
|
145
|
+
// after the snapshot is not counted, and need not be: that row is made when
|
|
146
|
+
// the sink takes its lease, before it reads the feed, and a feed holds one row
|
|
147
|
+
// per key, so a tombstone this collects had replaced its key's live row before
|
|
148
|
+
// such a sink could read anything.
|
|
149
|
+
collectTombstones: `WITH "low" AS (
|
|
150
|
+
SELECT min(c."seq") AS "seq",
|
|
151
|
+
bool_or(c."seq" IS NULL) AS "blocked"
|
|
152
|
+
FROM ${cursors} c
|
|
153
|
+
WHERE c."feed" = $1
|
|
154
|
+
)
|
|
155
|
+
DELETE FROM ${rows} r USING "low"
|
|
156
|
+
WHERE r."feed" = $1 AND r."doc" IS NULL
|
|
157
|
+
AND NOT "low"."blocked" AND r."seq" <= "low"."seq"
|
|
158
|
+
RETURNING 1 AS collected`,
|
|
159
|
+
// The transaction-scoped twin of `tryAdvisoryLock`, on the same key, so it
|
|
160
|
+
// conflicts with a session lock any advisory worker holds and is let go at
|
|
161
|
+
// commit or rollback without an unlock of its own.
|
|
162
|
+
tryAdvisoryXactLock: `SELECT pg_try_advisory_xact_lock(${advisoryKey}) AS taken`,
|
|
163
|
+
// A row whose expiry is later than the caller's now is a live lease row and
|
|
164
|
+
// stays. A holder with no expiry is an advisory one, and is deleted only
|
|
165
|
+
// because the caller holds the advisory lock, which proves its session gone.
|
|
166
|
+
// The test is in the delete itself, so a lease row taken after an earlier
|
|
167
|
+
// read is still refused here rather than deleted from under its worker.
|
|
168
|
+
deleteCursorUnlessLeased: `DELETE FROM ${cursors}
|
|
169
|
+
WHERE "feed" = $1 AND "sink" = $2
|
|
170
|
+
AND ("lease_expires_at" IS NULL
|
|
171
|
+
OR "lease_expires_at" <= ${msToTimestamp('$3')})
|
|
172
|
+
RETURNING 1 AS removed`,
|
|
173
|
+
cursorExists: `SELECT 1 AS present FROM ${cursors}
|
|
174
|
+
WHERE "feed" = $1 AND "sink" = $2`,
|
|
175
|
+
deleteSinkParked: `DELETE FROM ${parked} WHERE "feed" = $1 AND "sink" = $2
|
|
176
|
+
RETURNING 1 AS removed`,
|
|
177
|
+
selectHead: `SELECT COALESCE(max("seq"), 0)::text AS head
|
|
178
|
+
FROM ${rows} WHERE "feed" = $1`,
|
|
179
|
+
describeSinks: `SELECT c."sink" AS sink,
|
|
180
|
+
c."generation"::text AS generation,
|
|
181
|
+
c."seq"::text AS seq,
|
|
182
|
+
c."lease_holder" AS lease_holder,
|
|
183
|
+
${timestampToMs('c."lease_expires_at"')} AS lease_expires_at_ms,
|
|
184
|
+
${timestampToMs('c."last_advance_at"')} AS last_advance_at_ms,
|
|
185
|
+
(SELECT count(*) FROM ${rows} r
|
|
186
|
+
WHERE r."feed" = c."feed"
|
|
187
|
+
AND CASE WHEN c."seq" IS NULL THEN r."doc" IS NOT NULL
|
|
188
|
+
ELSE r."seq" > c."seq" END)::text AS pending
|
|
189
|
+
FROM ${cursors} c
|
|
190
|
+
WHERE c."feed" = $1
|
|
191
|
+
ORDER BY c."sink"`,
|
|
192
|
+
describeParked: `SELECT "sink" AS sink, "key" AS key, "seq"::text AS seq,
|
|
193
|
+
"reason" AS reason,
|
|
194
|
+
${timestampToMs('"parked_at"')} AS parked_at_ms
|
|
195
|
+
FROM ${parked} AS p
|
|
196
|
+
WHERE p."feed" = $1
|
|
197
|
+
ORDER BY p."sink", p."seq", p."key"`,
|
|
198
|
+
// Taken when nobody holds it, when the holder's expiry has passed, or when the
|
|
199
|
+
// holder has no expiry — an ADVISORY holder — and the caller holds that
|
|
200
|
+
// sink's advisory lock for its own transaction (`$6`), which an advisory
|
|
201
|
+
// worker's live session would have refused, so the session that recorded the
|
|
202
|
+
// holder is gone. The caller holds that lock until this statement commits, so
|
|
203
|
+
// no advisory worker can take the lock and record itself in between.
|
|
204
|
+
takeLeaseRow: `INSERT INTO ${cursors} AS c ("feed", "sink", "lease_holder", "lease_expires_at")
|
|
205
|
+
VALUES ($1, $2, $3, ${msToTimestamp('$4')})
|
|
206
|
+
ON CONFLICT ("feed", "sink") DO UPDATE
|
|
207
|
+
SET "lease_holder" = excluded."lease_holder",
|
|
208
|
+
"lease_expires_at" = excluded."lease_expires_at"
|
|
209
|
+
WHERE c."lease_holder" IS NULL
|
|
210
|
+
OR (c."lease_expires_at" IS NOT NULL
|
|
211
|
+
AND c."lease_expires_at" <= ${msToTimestamp('$5')})
|
|
212
|
+
OR (c."lease_expires_at" IS NULL AND $6::boolean)
|
|
213
|
+
RETURNING 1 AS taken`,
|
|
214
|
+
// Never moves the expiry back: a renewal the worker abandoned at its timeout
|
|
215
|
+
// can still land after a later one, if its cancel came too late, and must not
|
|
216
|
+
// shorten the lease the later one recorded, which the worker's `ensureHeld`
|
|
217
|
+
// already counts on.
|
|
218
|
+
renewLeaseRow: `UPDATE ${cursors}
|
|
219
|
+
SET "lease_expires_at" = GREATEST("lease_expires_at", ${msToTimestamp('$4')})
|
|
220
|
+
WHERE "feed" = $1 AND "sink" = $2 AND "lease_holder" = $3
|
|
221
|
+
RETURNING 1 AS renewed`,
|
|
222
|
+
releaseLease: `UPDATE ${cursors}
|
|
223
|
+
SET "lease_holder" = NULL, "lease_expires_at" = NULL
|
|
224
|
+
WHERE "feed" = $1 AND "sink" = $2 AND "lease_holder" = $3`,
|
|
225
|
+
// Run on the SESSION that took the advisory lock, and proposes its row only
|
|
226
|
+
// while that session still holds the lock, so a claim from a session that has
|
|
227
|
+
// died — or let the lock go — since its try-lock records nothing, and cannot
|
|
228
|
+
// overwrite a successor that took the lock in between. Holding the lock proves
|
|
229
|
+
// that any ADVISORY holder recorded here (one with no expiry) is a dead
|
|
230
|
+
// session's, so that one is taken over. A lease-row holder whose expiry has not
|
|
231
|
+
// passed is live and is never taken over, as `takeLeaseRow` takes over an
|
|
232
|
+
// advisory holder only once its lock is free: whichever kind took the row
|
|
233
|
+
// first keeps it while it lives.
|
|
234
|
+
claimAdvisoryHolder: `INSERT INTO ${cursors} AS c ("feed", "sink", "lease_holder", "lease_expires_at")
|
|
235
|
+
SELECT $1::text, $2::text, $3::text, NULL::timestamptz
|
|
236
|
+
WHERE ${sessionHoldsLock('$5')}
|
|
237
|
+
ON CONFLICT ("feed", "sink") DO UPDATE
|
|
238
|
+
SET "lease_holder" = excluded."lease_holder",
|
|
239
|
+
"lease_expires_at" = NULL
|
|
240
|
+
WHERE c."lease_holder" IS NULL
|
|
241
|
+
OR c."lease_expires_at" IS NULL
|
|
242
|
+
OR c."lease_expires_at" <= ${msToTimestamp('$4')}
|
|
243
|
+
RETURNING 1 AS claimed`,
|
|
244
|
+
tryAdvisoryLock: `SELECT pg_try_advisory_lock(${advisoryKey}) AS taken`,
|
|
245
|
+
holdsAdvisoryLock: `SELECT ${sessionHoldsLock('$1')} AS held`,
|
|
246
|
+
advisoryUnlock: `SELECT pg_advisory_unlock(${advisoryKey}) AS released`,
|
|
247
|
+
bindFeed: `INSERT INTO ${generations} AS g
|
|
248
|
+
("feed", "generation", "checkpoint_projection", "checkpoint_partition")
|
|
249
|
+
VALUES ($1, 0, $2, $3)
|
|
250
|
+
ON CONFLICT ("feed") DO UPDATE
|
|
251
|
+
SET "checkpoint_projection" = COALESCE(g."checkpoint_projection", excluded."checkpoint_projection"),
|
|
252
|
+
"checkpoint_partition" = COALESCE(g."checkpoint_partition", excluded."checkpoint_partition")
|
|
253
|
+
RETURNING "checkpoint_projection" AS projection,
|
|
254
|
+
"checkpoint_partition" AS partition`,
|
|
255
|
+
bumpGeneration: `INSERT INTO ${generations} AS g ("feed", "generation")
|
|
256
|
+
VALUES ($1, 1)
|
|
257
|
+
ON CONFLICT ("feed") DO UPDATE
|
|
258
|
+
SET "generation" = g."generation" + 1`,
|
|
259
|
+
deleteFeedRows: `DELETE FROM ${rows} WHERE "feed" = $1`
|
|
260
|
+
};
|
|
261
|
+
};
|
|
262
|
+
//# sourceMappingURL=statements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"statements.js","names":["quoteQualified","sequenceLiteral","compileFeedStatements","names","rows","rowsTable","generations","generationsTable","cursors","cursorsTable","parked","parkedTable","nextSeq","msToTimestamp","param","timestampToMs","column","advisoryKeyOf","advisoryKey","sessionHoldsLock","ensureGeneration","lockFeedShared","upsertRows","tombstoneRows","selectGeneration","changesAfter","selectCursor","casCursor","clearParked","unparkKeys","parkKeys","selectParked","lockFeedExclusive","unparkOne","restampRow","collectTombstones","tryAdvisoryXactLock","deleteCursorUnlessLeased","cursorExists","deleteSinkParked","selectHead","describeSinks","describeParked","takeLeaseRow","renewLeaseRow","releaseLease","claimAdvisoryHolder","tryAdvisoryLock","holdsAdvisoryLock","advisoryUnlock","bindFeed","bumpGeneration","deleteFeedRows"],"sources":["../../../src/internal/statements.ts"],"sourcesContent":[null],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0CA,SAASA,cAAc,EAA0BC,eAAe,QAAQ,UAAO;AA0F/E;AACA,OAAO,MAAMC,qBAAqB,GAChCC,KAAwB,IACN;EAClB,MAAMC,IAAI,GAAGJ,cAAc,CAACG,KAAK,CAACE,SAAS,CAAC;EAC5C,MAAMC,WAAW,GAAGN,cAAc,CAACG,KAAK,CAACI,gBAAgB,CAAC;EAC1D,MAAMC,OAAO,GAAGR,cAAc,CAACG,KAAK,CAACM,YAAY,CAAC;EAClD,MAAMC,MAAM,GAAGV,cAAc,CAACG,KAAK,CAACQ,WAAW,CAAC;EAChD,MAAMC,OAAO,GAAG,WAAWX,eAAe,CAACE,KAAK,CAAC,GAAG;EACpD,MAAMU,aAAa,GAAIC,KAAa,IAClC,gBAAgBA,KAAK,4BAA4B;EACnD,MAAMC,aAAa,GAAIC,MAAc,IACnC,4BAA4BA,MAAM,yBAAyB;EAC7D,MAAMC,aAAa,GAAIH,KAAa,IAAK,oBAAoBA,KAAK,MAAM;EACxE,MAAMI,WAAW,GAAGD,aAAa,CAAC,IAAI,CAAC;EACvC;EACA;EACA;EACA,MAAME,gBAAgB,GAAIL,KAAa,IAAK;;;;;;gDAMEG,aAAa,CAACH,KAAK,CAAC;6CACvBG,aAAa,CAACH,KAAK,CAAC;0BACvC;EAExB,OAAO;IACLM,gBAAgB,EAAE,eAAed,WAAW;;uDAEO;IAEnDe,cAAc,EAAE,2BAA2Bf,WAAW;iDACT;IAE7C;IACA;IACA;IACA;IACAgB,UAAU,EAAE,eAAelB,IAAI;gDACaQ,OAAO;;;;;;+DAMQ;IAE3D;IACA;IACAW,aAAa,EAAE,UAAUnB,IAAI;gDACeQ,OAAO;;sFAE+B;IAElFY,gBAAgB,EAAE;8BACQlB,WAAW,oBAAoB;IAEzDmB,YAAY,EAAE;;0BAEQrB,IAAI;;;4BAGF;IAExBsB,YAAY,EAAE;0BACQlB,OAAO,oCAAoC;IAEjE;IACA;IACAmB,SAAS,EAAE,UAAUnB,OAAO;;0CAEUK,aAAa,CAAC,IAAI,CAAC;;;;sCAIvB;IAElCe,WAAW,EAAE,eAAelB,MAAM,oCAAoC;IAEtEmB,UAAU,EAAE,eAAenB,MAAM;;8EAEyC;IAE1EoB,QAAQ,EAAE,eAAepB,MAAM;sEACmCG,aAAa,CAAC,kBAAkB,CAAC;;;;;sDAKjD;IAElDkB,YAAY,EAAE;4BACUhB,aAAa,CAAC,aAAa,CAAC;0BAC9BL,MAAM;;6CAEa;IAEzCsB,iBAAiB,EAAE,2BAA2B1B,WAAW;qDACR;IAEjD2B,SAAS,EAAE,eAAevB,MAAM;;wCAEI;IAEpCwB,UAAU,EAAE,UAAU9B,IAAI,gBAAgBQ,OAAO;kDACH;IAE9C;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACAuB,iBAAiB,EAAE;;;iCAGU3B,OAAO;;;sCAGFJ,IAAI;;;iDAGO;IAE7C;IACA;IACA;IACAgC,mBAAmB,EAAE,oCAAoClB,WAAW,YAAY;IAEhF;IACA;IACA;IACA;IACA;IACAmB,wBAAwB,EAAE,eAAe7B,OAAO;;;iEAGaK,aAAa,CAAC,IAAI,CAAC;sDAC9B;IAElDyB,YAAY,EAAE,4BAA4B9B,OAAO;qDACA;IAEjD+B,gBAAgB,EAAE,eAAe7B,MAAM;8CACG;IAE1C8B,UAAU,EAAE;wBACQpC,IAAI,oBAAoB;IAE5CqC,aAAa,EAAE;;;;6BAIU1B,aAAa,CAAC,sBAAsB,CAAC;6BACrCA,aAAa,CAAC,qBAAqB,CAAC;mDACdX,IAAI;;;;2BAI5BI,OAAO;;sCAEI;IAElCkC,cAAc,EAAE;;8BAEU3B,aAAa,CAAC,aAAa,CAAC;4BAC9BL,MAAM;;yDAEuB;IAErD;IACA;IACA;IACA;IACA;IACA;IACAiC,YAAY,EAAE,eAAenC,OAAO;yCACCK,aAAa,CAAC,IAAI,CAAC;;;;;;wDAMJA,aAAa,CAAC,IAAI,CAAC;;wCAEnC;IAEpC;IACA;IACA;IACA;IACA+B,aAAa,EAAE,UAAUpC,OAAO;4EACwCK,aAAa,CAAC,IAAI,CAAC;;2CAEpD;IAEvCgC,YAAY,EAAE,UAAUrC,OAAO;;6EAE0C;IAEzE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACAsC,mBAAmB,EAAE,eAAetC,OAAO;;kCAEbW,gBAAgB,CAAC,IAAI,CAAC;;;;;;0DAMEN,aAAa,CAAC,IAAI,CAAC;iDAC5B;IAE7CkC,eAAe,EAAE,+BAA+B7B,WAAW,YAAY;IAEvE8B,iBAAiB,EAAE,UAAU7B,gBAAgB,CAAC,IAAI,CAAC,UAAU;IAE7D8B,cAAc,EAAE,6BAA6B/B,WAAW,eAAe;IAEvEgC,QAAQ,EAAE,eAAe5C,WAAW;;;;;;;6DAOqB;IAEzD6C,cAAc,EAAE,eAAe7C,WAAW;;;2DAGa;IAEvD8C,cAAc,EAAE,eAAehD,IAAI;GACpC;AACH,CAAC","ignoreList":[]}
|