@descryy/runtime-database-observation 0.4.8 → 0.5.1
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/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mysql-row-collector.d.ts +28 -0
- package/dist/mysql-row-collector.d.ts.map +1 -0
- package/dist/mysql-row-collector.js +53 -0
- package/dist/mysql-row-collector.js.map +1 -0
- package/dist/postgres-decoding-parser.d.ts +53 -0
- package/dist/postgres-decoding-parser.d.ts.map +1 -0
- package/dist/postgres-decoding-parser.js +92 -0
- package/dist/postgres-decoding-parser.js.map +1 -0
- package/dist/postgres-query-collector.d.ts +6 -3
- package/dist/postgres-query-collector.d.ts.map +1 -1
- package/dist/postgres-query-collector.js +6 -3
- package/dist/postgres-query-collector.js.map +1 -1
- package/dist/postgres-row-collector.d.ts +67 -0
- package/dist/postgres-row-collector.d.ts.map +1 -0
- package/dist/postgres-row-collector.js +273 -0
- package/dist/postgres-row-collector.js.map +1 -0
- package/dist/sqlite-row-snapshot.d.ts +57 -0
- package/dist/sqlite-row-snapshot.d.ts.map +1 -0
- package/dist/sqlite-row-snapshot.js +210 -0
- package/dist/sqlite-row-snapshot.js.map +1 -0
- package/package.json +13 -9
package/dist/index.d.ts
CHANGED
|
@@ -11,4 +11,12 @@ export type { PostgresQueryCollectorOptions } from "./postgres-query-collector.t
|
|
|
11
11
|
export { createPostgresQueryCollector } from "./postgres-query-collector.ts";
|
|
12
12
|
export type { PostgresStatementLogSource } from "./postgres-log-source.ts";
|
|
13
13
|
export { createPostgresLogSource } from "./postgres-log-source.ts";
|
|
14
|
+
export type { ParsedRowChange, RowChangeOp } from "./postgres-decoding-parser.ts";
|
|
15
|
+
export { parseTestDecodingLine } from "./postgres-decoding-parser.ts";
|
|
16
|
+
export type { PgRowClient, PostgresRowChangeCollectorOptions } from "./postgres-row-collector.ts";
|
|
17
|
+
export { createPostgresRowChangeCollector } from "./postgres-row-collector.ts";
|
|
18
|
+
export type { SqliteRowSource, SqliteRowOp, SqliteRowDiff, TableSnapshot, SqliteRowChangeCollectorOptions } from "./sqlite-row-snapshot.ts";
|
|
19
|
+
export { createSqliteRowChangeCollector, diffTableSnapshot } from "./sqlite-row-snapshot.ts";
|
|
20
|
+
export type { MysqlRowChangeCollectorOptions } from "./mysql-row-collector.ts";
|
|
21
|
+
export { createMysqlRowChangeCollector, MYSQL_ROW_CAPTURE_REFUSAL_REASON } from "./mysql-row-collector.ts";
|
|
14
22
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,YAAY,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEhG,YAAY,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,YAAY,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,YAAY,EAAE,6BAA6B,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEhG,YAAY,EAAE,+BAA+B,EAAE,MAAM,oBAAoB,CAAC;AAC1E,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAElE,YAAY,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAEhE,YAAY,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,0BAA0B,EAAE,MAAM,0BAA0B,CAAC;AAC3E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AAClF,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAEtE,YAAY,EAAE,WAAW,EAAE,iCAAiC,EAAE,MAAM,6BAA6B,CAAC;AAClG,OAAO,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAC;AAE/E,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,+BAA+B,EAAE,MAAM,0BAA0B,CAAC;AAC5I,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE7F,YAAY,EAAE,8BAA8B,EAAE,MAAM,0BAA0B,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -6,4 +6,8 @@ export { resolveObservedDatabaseQuery } from "./graph-mapping.js";
|
|
|
6
6
|
export { parsePostgresLogLine } from "./postgres-log-parser.js";
|
|
7
7
|
export { createPostgresQueryCollector } from "./postgres-query-collector.js";
|
|
8
8
|
export { createPostgresLogSource } from "./postgres-log-source.js";
|
|
9
|
+
export { parseTestDecodingLine } from "./postgres-decoding-parser.js";
|
|
10
|
+
export { createPostgresRowChangeCollector } from "./postgres-row-collector.js";
|
|
11
|
+
export { createSqliteRowChangeCollector, diffTableSnapshot } from "./sqlite-row-snapshot.js";
|
|
12
|
+
export { createMysqlRowChangeCollector, MYSQL_ROW_CAPTURE_REFUSAL_REASON } from "./mysql-row-collector.js";
|
|
9
13
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAA2C,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGxD,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAGhG,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGhE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE3D,OAAO,EAAE,YAAY,EAA2C,MAAM,wBAAwB,CAAC;AAC/F,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAGxD,OAAO,EAAE,4BAA4B,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAGhG,OAAO,EAAE,4BAA4B,EAAE,MAAM,oBAAoB,CAAC;AAGlE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAGhE,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAGnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAGtE,OAAO,EAAE,gCAAgC,EAAE,MAAM,6BAA6B,CAAC;AAG/E,OAAO,EAAE,8BAA8B,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAG7F,OAAO,EAAE,6BAA6B,EAAE,gCAAgC,EAAE,MAAM,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL/MariaDB row-change capture: a named, remediable refusal, not a
|
|
3
|
+
* binlog implementation. See DEC-NEXT-mysql-row-and-schema-capture-refused-no-driver
|
|
4
|
+
* (descry-core decisions inbox) for the full reasoning — in short, this
|
|
5
|
+
* repo's `pnpm-lock.yaml` carries no MySQL driver (`mysql2` or otherwise)
|
|
6
|
+
* today, and this lane cannot install one, so writing binlog-protocol
|
|
7
|
+
* parsing code that has never run against a real driver would be a worse
|
|
8
|
+
* violation of honest degradation (rule 7) than declining to write it.
|
|
9
|
+
*
|
|
10
|
+
* Shaped as a `Collector` for the same reason `createPostgresRowChangeCollector`
|
|
11
|
+
* is: a caller wiring up database observation for a run can try to start
|
|
12
|
+
* this collector unconditionally, exactly like the others, and get back a
|
|
13
|
+
* refusal it can disclose rather than a branch it has to special-case.
|
|
14
|
+
*
|
|
15
|
+
* The moment `mysql2` (with `binlog_format=ROW` and full row images) becomes
|
|
16
|
+
* an installed dependency, this refusal should be replaced with a real
|
|
17
|
+
* binlog client, following `postgres-row-collector.ts`'s dynamic-import
|
|
18
|
+
* pattern -- optional dependency, loaded lazily, refuse-by-name if absent.
|
|
19
|
+
*/
|
|
20
|
+
import type { Collector } from "@descryy/runtime-contracts";
|
|
21
|
+
export interface MysqlRowChangeCollectorOptions {
|
|
22
|
+
readonly connectionString: string;
|
|
23
|
+
readonly service?: string;
|
|
24
|
+
readonly processId?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const MYSQL_ROW_CAPTURE_REFUSAL_REASON: string;
|
|
27
|
+
export declare function createMysqlRowChangeCollector(options: MysqlRowChangeCollectorOptions): Collector;
|
|
28
|
+
//# sourceMappingURL=mysql-row-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql-row-collector.d.ts","sourceRoot":"","sources":["../src/mysql-row-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAiE,MAAM,4BAA4B,CAAC;AAE3H,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,gCAAgC,QAEkC,CAAC;AAEhF,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,8BAA8B,GAAG,SAAS,CAmChG"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MySQL/MariaDB row-change capture: a named, remediable refusal, not a
|
|
3
|
+
* binlog implementation. See DEC-NEXT-mysql-row-and-schema-capture-refused-no-driver
|
|
4
|
+
* (descry-core decisions inbox) for the full reasoning — in short, this
|
|
5
|
+
* repo's `pnpm-lock.yaml` carries no MySQL driver (`mysql2` or otherwise)
|
|
6
|
+
* today, and this lane cannot install one, so writing binlog-protocol
|
|
7
|
+
* parsing code that has never run against a real driver would be a worse
|
|
8
|
+
* violation of honest degradation (rule 7) than declining to write it.
|
|
9
|
+
*
|
|
10
|
+
* Shaped as a `Collector` for the same reason `createPostgresRowChangeCollector`
|
|
11
|
+
* is: a caller wiring up database observation for a run can try to start
|
|
12
|
+
* this collector unconditionally, exactly like the others, and get back a
|
|
13
|
+
* refusal it can disclose rather than a branch it has to special-case.
|
|
14
|
+
*
|
|
15
|
+
* The moment `mysql2` (with `binlog_format=ROW` and full row images) becomes
|
|
16
|
+
* an installed dependency, this refusal should be replaced with a real
|
|
17
|
+
* binlog client, following `postgres-row-collector.ts`'s dynamic-import
|
|
18
|
+
* pattern -- optional dependency, loaded lazily, refuse-by-name if absent.
|
|
19
|
+
*/
|
|
20
|
+
export const MYSQL_ROW_CAPTURE_REFUSAL_REASON = "MySQL row-change capture needs the optional dependency \"mysql2\" (with binlog_format=ROW and full row images), " +
|
|
21
|
+
"which is not installed in this build; the query-text channel is unaffected.";
|
|
22
|
+
export function createMysqlRowChangeCollector(options) {
|
|
23
|
+
const processId = options.processId ?? "mysql-row-changes";
|
|
24
|
+
function capabilities() {
|
|
25
|
+
const unrelated = {
|
|
26
|
+
availability: "unavailable",
|
|
27
|
+
reason: "MysqlRowChangeCollector observes row-level DATABASE_QUERY (row-change) events only.",
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
domObservation: unrelated,
|
|
31
|
+
consoleObservation: unrelated,
|
|
32
|
+
networkObservation: unrelated,
|
|
33
|
+
backendLogAccess: unrelated,
|
|
34
|
+
distributedTrace: unrelated,
|
|
35
|
+
sourceMapping: unrelated,
|
|
36
|
+
stackCapture: unrelated,
|
|
37
|
+
processLifecycle: unrelated,
|
|
38
|
+
databaseObservation: { availability: "unavailable", reason: MYSQL_ROW_CAPTURE_REFUSAL_REASON },
|
|
39
|
+
externalServiceObservation: unrelated,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
collectorId: `mysql-row-collector:${processId}`,
|
|
44
|
+
start(_context) {
|
|
45
|
+
return Promise.resolve({ available: false, reason: MYSQL_ROW_CAPTURE_REFUSAL_REASON });
|
|
46
|
+
},
|
|
47
|
+
stop() {
|
|
48
|
+
return Promise.resolve();
|
|
49
|
+
},
|
|
50
|
+
capabilities,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=mysql-row-collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysql-row-collector.js","sourceRoot":"","sources":["../src/mysql-row-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAUH,MAAM,CAAC,MAAM,gCAAgC,GAC3C,kHAAkH;IAClH,6EAA6E,CAAC;AAEhF,MAAM,UAAU,6BAA6B,CAAC,OAAuC;IACnF,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,mBAAmB,CAAC;IAE3D,SAAS,YAAY;QACnB,MAAM,SAAS,GAAG;YAChB,YAAY,EAAE,aAAsB;YACpC,MAAM,EAAE,qFAAqF;SAC9F,CAAC;QACF,OAAO;YACL,cAAc,EAAE,SAAS;YACzB,kBAAkB,EAAE,SAAS;YAC7B,kBAAkB,EAAE,SAAS;YAC7B,gBAAgB,EAAE,SAAS;YAC3B,gBAAgB,EAAE,SAAS;YAC3B,aAAa,EAAE,SAAS;YACxB,YAAY,EAAE,SAAS;YACvB,gBAAgB,EAAE,SAAS;YAC3B,mBAAmB,EAAE,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,EAAE,gCAAgC,EAAE;YAC9F,0BAA0B,EAAE,SAAS;SACtC,CAAC;IACJ,CAAC;IAED,OAAO;QACL,WAAW,EAAE,uBAAuB,SAAS,EAAE;QAE/C,KAAK,CAAC,QAA0B;YAC9B,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,gCAAgC,EAAE,CAAC,CAAC;QACzF,CAAC;QAED,IAAI;YACF,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC3B,CAAC;QAED,YAAY;KACb,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded reader over `test_decoding`'s documented output format (the
|
|
3
|
+
* built-in logical-decoding output plugin every Postgres ships — no
|
|
4
|
+
* extension install needed). `postgres-row-collector.ts` reads each row's
|
|
5
|
+
* `data` column back from `pg_logical_slot_get_changes` and hands it here,
|
|
6
|
+
* one line at a time; `xid` arrives as its own column on that same result
|
|
7
|
+
* row, so this parser does not need to track BEGIN/COMMIT for transaction
|
|
8
|
+
* identity — those lines simply refuse (`null`), same as this package's
|
|
9
|
+
* other parsers (`postgres-log-parser.ts`, `table-extraction.ts`) refuse
|
|
10
|
+
* rather than guess at a shape they don't recognise.
|
|
11
|
+
*
|
|
12
|
+
* Recognised shapes (per https://www.postgresql.org/docs/current/test-decoding.html):
|
|
13
|
+
*
|
|
14
|
+
* - `table <schema>.<table>: INSERT: <col>[<type>]:<value> ...`
|
|
15
|
+
* - `table <schema>.<table>: UPDATE: old-key: <col>[<type>]:<value> ... new-tuple: <col>[<type>]:<value> ...`
|
|
16
|
+
* — present only when the table's `REPLICA IDENTITY` makes an old key
|
|
17
|
+
* decodable at all (FULL, or DEFAULT when the key columns changed).
|
|
18
|
+
* - `table <schema>.<table>: UPDATE: <col>[<type>]:<value> ...` — no
|
|
19
|
+
* `old-key:` segment; the new row only, no before-state in this line.
|
|
20
|
+
* - `table <schema>.<table>: DELETE: <col>[<type>]:<value> ...` — full or
|
|
21
|
+
* key-only, depending on identity.
|
|
22
|
+
* - `table <schema>.<table>: DELETE: (no-tuple-data)` — `REPLICA IDENTITY
|
|
23
|
+
* NOTHING`; no columns at all.
|
|
24
|
+
*
|
|
25
|
+
* Column values are captured as raw strings, never coerced to a JS number/
|
|
26
|
+
* boolean/null — `name[text]:'it''s here'` unescapes SQL-style doubled
|
|
27
|
+
* quotes, but a bareword value (`1`, `null`, `t`, `unchanged-toast-datum`)
|
|
28
|
+
* is reported exactly as `test_decoding` printed it. Coercing to a native
|
|
29
|
+
* type would need the column's Postgres type, which the `[type]` tag names
|
|
30
|
+
* but this parser deliberately does not interpret — same "don't guess past
|
|
31
|
+
* what's declared" stance as `live-schema.ts`'s column reader.
|
|
32
|
+
*
|
|
33
|
+
* What this parser does NOT decide: whether a given row's before-value is
|
|
34
|
+
* "full", "key-only" or "none" for a *table* — that is a property of
|
|
35
|
+
* `REPLICA IDENTITY`, looked up once from `pg_class.relreplident` by
|
|
36
|
+
* `postgres-row-collector.ts` and attached to every emitted event, not
|
|
37
|
+
* inferred from which of the three UPDATE/DELETE shapes above matched.
|
|
38
|
+
*/
|
|
39
|
+
export type RowChangeOp = "INSERT" | "UPDATE" | "DELETE";
|
|
40
|
+
export interface ParsedRowChange {
|
|
41
|
+
readonly schema: string;
|
|
42
|
+
readonly table: string;
|
|
43
|
+
readonly op: RowChangeOp;
|
|
44
|
+
readonly before: Record<string, string> | null;
|
|
45
|
+
readonly after: Record<string, string> | null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `null` for anything not one of the recognised `test_decoding` shapes —
|
|
49
|
+
* including `BEGIN <xid>` / `COMMIT <xid>` lines, which this parser is not
|
|
50
|
+
* asked to interpret (xid comes from the calling row's own column).
|
|
51
|
+
*/
|
|
52
|
+
export declare function parseTestDecodingLine(data: string): ParsedRowChange | null;
|
|
53
|
+
//# sourceMappingURL=postgres-decoding-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-decoding-parser.d.ts","sourceRoot":"","sources":["../src/postgres-decoding-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;CAC/C;AA0BD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,IAAI,CA6B1E"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded reader over `test_decoding`'s documented output format (the
|
|
3
|
+
* built-in logical-decoding output plugin every Postgres ships — no
|
|
4
|
+
* extension install needed). `postgres-row-collector.ts` reads each row's
|
|
5
|
+
* `data` column back from `pg_logical_slot_get_changes` and hands it here,
|
|
6
|
+
* one line at a time; `xid` arrives as its own column on that same result
|
|
7
|
+
* row, so this parser does not need to track BEGIN/COMMIT for transaction
|
|
8
|
+
* identity — those lines simply refuse (`null`), same as this package's
|
|
9
|
+
* other parsers (`postgres-log-parser.ts`, `table-extraction.ts`) refuse
|
|
10
|
+
* rather than guess at a shape they don't recognise.
|
|
11
|
+
*
|
|
12
|
+
* Recognised shapes (per https://www.postgresql.org/docs/current/test-decoding.html):
|
|
13
|
+
*
|
|
14
|
+
* - `table <schema>.<table>: INSERT: <col>[<type>]:<value> ...`
|
|
15
|
+
* - `table <schema>.<table>: UPDATE: old-key: <col>[<type>]:<value> ... new-tuple: <col>[<type>]:<value> ...`
|
|
16
|
+
* — present only when the table's `REPLICA IDENTITY` makes an old key
|
|
17
|
+
* decodable at all (FULL, or DEFAULT when the key columns changed).
|
|
18
|
+
* - `table <schema>.<table>: UPDATE: <col>[<type>]:<value> ...` — no
|
|
19
|
+
* `old-key:` segment; the new row only, no before-state in this line.
|
|
20
|
+
* - `table <schema>.<table>: DELETE: <col>[<type>]:<value> ...` — full or
|
|
21
|
+
* key-only, depending on identity.
|
|
22
|
+
* - `table <schema>.<table>: DELETE: (no-tuple-data)` — `REPLICA IDENTITY
|
|
23
|
+
* NOTHING`; no columns at all.
|
|
24
|
+
*
|
|
25
|
+
* Column values are captured as raw strings, never coerced to a JS number/
|
|
26
|
+
* boolean/null — `name[text]:'it''s here'` unescapes SQL-style doubled
|
|
27
|
+
* quotes, but a bareword value (`1`, `null`, `t`, `unchanged-toast-datum`)
|
|
28
|
+
* is reported exactly as `test_decoding` printed it. Coercing to a native
|
|
29
|
+
* type would need the column's Postgres type, which the `[type]` tag names
|
|
30
|
+
* but this parser deliberately does not interpret — same "don't guess past
|
|
31
|
+
* what's declared" stance as `live-schema.ts`'s column reader.
|
|
32
|
+
*
|
|
33
|
+
* What this parser does NOT decide: whether a given row's before-value is
|
|
34
|
+
* "full", "key-only" or "none" for a *table* — that is a property of
|
|
35
|
+
* `REPLICA IDENTITY`, looked up once from `pg_class.relreplident` by
|
|
36
|
+
* `postgres-row-collector.ts` and attached to every emitted event, not
|
|
37
|
+
* inferred from which of the three UPDATE/DELETE shapes above matched.
|
|
38
|
+
*/
|
|
39
|
+
const LINE_PATTERN = /^table\s+([^.\s]+)\.([^:\s]+):\s+(INSERT|UPDATE|DELETE):\s*(.*)$/;
|
|
40
|
+
const OLD_KEY_NEW_TUPLE = /^old-key:\s*(.*?)\s+new-tuple:\s*(.*)$/;
|
|
41
|
+
const NO_TUPLE_DATA = "(no-tuple-data)";
|
|
42
|
+
/** `col[type]:'quoted value'` or `col[type]:bareword` pairs, in appearance order. */
|
|
43
|
+
const COLUMN_PATTERN = /([A-Za-z_][A-Za-z0-9_]*)\[[^\]]*\]:('(?:[^']|'')*'|\S+)/g;
|
|
44
|
+
function unquote(rawValue) {
|
|
45
|
+
if (rawValue.length >= 2 && rawValue.startsWith("'") && rawValue.endsWith("'")) {
|
|
46
|
+
return rawValue.slice(1, -1).replace(/''/g, "'");
|
|
47
|
+
}
|
|
48
|
+
return rawValue;
|
|
49
|
+
}
|
|
50
|
+
function parseColumns(segment) {
|
|
51
|
+
const columns = {};
|
|
52
|
+
for (const match of segment.matchAll(COLUMN_PATTERN)) {
|
|
53
|
+
const name = match[1];
|
|
54
|
+
const rawValue = match[2];
|
|
55
|
+
columns[name] = unquote(rawValue);
|
|
56
|
+
}
|
|
57
|
+
return columns;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* `null` for anything not one of the recognised `test_decoding` shapes —
|
|
61
|
+
* including `BEGIN <xid>` / `COMMIT <xid>` lines, which this parser is not
|
|
62
|
+
* asked to interpret (xid comes from the calling row's own column).
|
|
63
|
+
*/
|
|
64
|
+
export function parseTestDecodingLine(data) {
|
|
65
|
+
const match = LINE_PATTERN.exec(data.trim());
|
|
66
|
+
if (match === null)
|
|
67
|
+
return null;
|
|
68
|
+
const [, schema, table, opRaw, rest] = match;
|
|
69
|
+
const op = opRaw;
|
|
70
|
+
if (op === "INSERT") {
|
|
71
|
+
return { schema: schema, table: table, op, before: null, after: parseColumns(rest) };
|
|
72
|
+
}
|
|
73
|
+
if (op === "DELETE") {
|
|
74
|
+
if (rest.trim() === NO_TUPLE_DATA) {
|
|
75
|
+
return { schema: schema, table: table, op, before: null, after: null };
|
|
76
|
+
}
|
|
77
|
+
return { schema: schema, table: table, op, before: parseColumns(rest), after: null };
|
|
78
|
+
}
|
|
79
|
+
// UPDATE
|
|
80
|
+
const oldKeyMatch = OLD_KEY_NEW_TUPLE.exec(rest);
|
|
81
|
+
if (oldKeyMatch !== null) {
|
|
82
|
+
return {
|
|
83
|
+
schema: schema,
|
|
84
|
+
table: table,
|
|
85
|
+
op,
|
|
86
|
+
before: parseColumns(oldKeyMatch[1]),
|
|
87
|
+
after: parseColumns(oldKeyMatch[2]),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return { schema: schema, table: table, op, before: null, after: parseColumns(rest) };
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=postgres-decoding-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-decoding-parser.js","sourceRoot":"","sources":["../src/postgres-decoding-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAYH,MAAM,YAAY,GAAG,kEAAkE,CAAC;AACxF,MAAM,iBAAiB,GAAG,wCAAwC,CAAC;AACnE,MAAM,aAAa,GAAG,iBAAiB,CAAC;AAExC,qFAAqF;AACrF,MAAM,cAAc,GAAG,0DAA0D,CAAC;AAElF,SAAS,OAAO,CAAC,QAAgB;IAC/B,IAAI,QAAQ,CAAC,MAAM,IAAI,CAAC,IAAI,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC/E,OAAO,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC;IACnD,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,YAAY,CAAC,OAAe;IACnC,MAAM,OAAO,GAA2B,EAAE,CAAC;IAC3C,KAAK,MAAM,KAAK,IAAI,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;QACrD,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;QACvB,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAE,CAAC;QAC3B,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC7C,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,KAAK,CAAC;IAC7C,MAAM,EAAE,GAAG,KAAoB,CAAC;IAEhC,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,IAAK,CAAC,EAAE,CAAC;IAC1F,CAAC;IAED,IAAI,EAAE,KAAK,QAAQ,EAAE,CAAC;QACpB,IAAI,IAAK,CAAC,IAAI,EAAE,KAAK,aAAa,EAAE,CAAC;YACnC,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QAC3E,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,CAAC,IAAK,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAC1F,CAAC;IAED,SAAS;IACT,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAK,CAAC,CAAC;IAClD,IAAI,WAAW,KAAK,IAAI,EAAE,CAAC;QACzB,OAAO;YACL,MAAM,EAAE,MAAO;YACf,KAAK,EAAE,KAAM;YACb,EAAE;YACF,MAAM,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;YACrC,KAAK,EAAE,YAAY,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC;SACrC,CAAC;IACJ,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,MAAO,EAAE,KAAK,EAAE,KAAM,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,YAAY,CAAC,IAAK,CAAC,EAAE,CAAC;AAC1F,CAAC"}
|
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
* this observes query *text* — `extractTargetTable` still resolves the
|
|
13
13
|
* touched table — never a captured before/after row. Postgres's statement
|
|
14
14
|
* log is a text stream, not a row-change feed; reading one would need a
|
|
15
|
-
* separate mechanism
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* separate mechanism this collector does not add. That mechanism is now a
|
|
16
|
+
* separate collector in this package — `postgres-row-collector.ts`, via a
|
|
17
|
+
* temporary logical replication slot — opted into independently rather than
|
|
18
|
+
* folded into this one, since it needs its own connection, its own
|
|
19
|
+
* preconditions (`wal_level`, replication privilege) and its own refusal
|
|
20
|
+
* path when either is absent.
|
|
18
21
|
*
|
|
19
22
|
* **Live-tested against a real, disposable `postgres:16` Docker container**
|
|
20
23
|
* (`test/postgres-live.test.ts`): a real `INSERT`/`SELECT` issued via
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-query-collector.d.ts","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"postgres-query-collector.d.ts","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAC7I,OAAO,EAAyB,KAAK,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAMvG,MAAM,WAAW,6BAA6B;IAC5C,+HAA+H;IAC/H,QAAQ,CAAC,MAAM,EAAE,mBAAmB,CAAC;IACrC,mHAAmH;IACnH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AA2BD,wBAAgB,4BAA4B,CAAC,OAAO,EAAE,6BAA6B,GAAG,SAAS,CA6F9F"}
|
|
@@ -12,9 +12,12 @@
|
|
|
12
12
|
* this observes query *text* — `extractTargetTable` still resolves the
|
|
13
13
|
* touched table — never a captured before/after row. Postgres's statement
|
|
14
14
|
* log is a text stream, not a row-change feed; reading one would need a
|
|
15
|
-
* separate mechanism
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* separate mechanism this collector does not add. That mechanism is now a
|
|
16
|
+
* separate collector in this package — `postgres-row-collector.ts`, via a
|
|
17
|
+
* temporary logical replication slot — opted into independently rather than
|
|
18
|
+
* folded into this one, since it needs its own connection, its own
|
|
19
|
+
* preconditions (`wal_level`, replication privilege) and its own refusal
|
|
20
|
+
* path when either is absent.
|
|
18
21
|
*
|
|
19
22
|
* **Live-tested against a real, disposable `postgres:16` Docker container**
|
|
20
23
|
* (`test/postgres-live.test.ts`): a real `INSERT`/`SELECT` issued via
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-query-collector.js","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"postgres-query-collector.js","sourceRoot":"","sources":["../src/postgres-query-collector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closes the gap `postgres-query-collector.ts` discloses: "queries only ...
|
|
3
|
+
* no before/after row state." Postgres's own logical decoding is the
|
|
4
|
+
* mechanism — no extension install, no schema change, no trigger/audit
|
|
5
|
+
* table the target application doesn't already have.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle, deliberately shaped around `Collector.start()`/`stop()` rather
|
|
8
|
+
* than a continuous stream (unlike `PostgresQueryCollector`, which tails a
|
|
9
|
+
* log line-by-line as it's written):
|
|
10
|
+
*
|
|
11
|
+
* - `start()`: opens ONE `pg` connection, checks two preconditions
|
|
12
|
+
* (`wal_level = 'logical'`, and the role has `REPLICATION` or superuser),
|
|
13
|
+
* refuses by name (never throws) if either fails, then creates a
|
|
14
|
+
* **temporary** logical replication slot (`pg_create_logical_replication_slot`
|
|
15
|
+
* with `test_decoding` and `temporary = true`). Temporary slots drop on
|
|
16
|
+
* disconnect — a crash between create and read cannot leave WAL retained
|
|
17
|
+
* on the server, which a permanent slot could. The `CollectorContext`
|
|
18
|
+
* passed in is retained (closed over) for use at `stop()`, since
|
|
19
|
+
* `Collector.stop()` itself takes none.
|
|
20
|
+
* - `stop()`: reads `pg_logical_slot_get_changes` once on that same
|
|
21
|
+
* connection, parses each row via `postgres-decoding-parser.ts`, emits one
|
|
22
|
+
* `DATABASE_QUERY` evidence item per recognised row change, then closes
|
|
23
|
+
* the connection (which also drops the temporary slot).
|
|
24
|
+
*
|
|
25
|
+
* Before-value completeness is a table-level fact (`REPLICA IDENTITY`), read
|
|
26
|
+
* once via `pg_class.relreplident` at slot-creation time and attached to
|
|
27
|
+
* every emitted row as `beforeCompleteness` — disclosed per DEC-NEXT
|
|
28
|
+
* row-change-evidence-reuses-database-query-type, never inferred from which
|
|
29
|
+
* of `postgres-decoding-parser.ts`'s three UPDATE/DELETE shapes matched.
|
|
30
|
+
*
|
|
31
|
+
* `pg` is an *optional* dependency here, loaded via dynamic `import()`
|
|
32
|
+
* exactly like `adapter-sql`'s `live-schema.ts` (see that file's own header
|
|
33
|
+
* for why: keeps every consumer that never wants row capture from needing a
|
|
34
|
+
* Postgres client library, and keeps `tsc` from requiring `pg`'s types to
|
|
35
|
+
* check this file). Not installed → the import throws → this collector
|
|
36
|
+
* refuses by name, same as a failed precondition check.
|
|
37
|
+
*/
|
|
38
|
+
import type { Collector } from "@descryy/runtime-contracts";
|
|
39
|
+
/** The narrow slice of `pg`'s `Client` this module calls — same shape `live-schema.ts` uses, small enough to be satisfied by a test double with no real database involved. */
|
|
40
|
+
export interface PgRowClient {
|
|
41
|
+
connect(): Promise<void>;
|
|
42
|
+
query(text: string, params?: readonly unknown[]): Promise<{
|
|
43
|
+
rows: readonly Record<string, unknown>[];
|
|
44
|
+
}>;
|
|
45
|
+
end(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
export interface PostgresRowChangeCollectorOptions {
|
|
48
|
+
readonly connectionString: string;
|
|
49
|
+
/** Which owned system this collector's evidence should be attributed to (plan §16.7). */
|
|
50
|
+
readonly service?: string;
|
|
51
|
+
readonly processId?: string;
|
|
52
|
+
/** Test/advanced seam — defaults to a real `pg.Client` loaded dynamically. */
|
|
53
|
+
readonly clientFactory?: (connectionString: string) => Promise<PgRowClient> | PgRowClient;
|
|
54
|
+
}
|
|
55
|
+
type PgClientConstructor = new (config: unknown) => PgRowClient;
|
|
56
|
+
/**
|
|
57
|
+
* The `Client` constructor from a dynamically imported `pg`. `pg` is CommonJS,
|
|
58
|
+
* so under `import()` its exports sit on `default` and Node's named-export
|
|
59
|
+
* detection does not surface `Client` at the top level; the first release read
|
|
60
|
+
* only the top level and every real connection failed with "pgModule.Client is
|
|
61
|
+
* not a constructor". Both shapes are read, and a module exposing neither is a
|
|
62
|
+
* loud error rather than a silent no-op.
|
|
63
|
+
*/
|
|
64
|
+
export declare function pgClientConstructor(loaded: unknown): PgClientConstructor;
|
|
65
|
+
export declare function createPostgresRowChangeCollector(options: PostgresRowChangeCollectorOptions): Collector;
|
|
66
|
+
export {};
|
|
67
|
+
//# sourceMappingURL=postgres-row-collector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-row-collector.d.ts","sourceRoot":"","sources":["../src/postgres-row-collector.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAK7I,8KAA8K;AAC9K,MAAM,WAAW,WAAW;IAC1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAA;KAAE,CAAC,CAAC;IACxG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,MAAM,WAAW,iCAAiC;IAChD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,gBAAgB,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,CAAC,GAAG,WAAW,CAAC;CAC3F;AAMD,KAAK,mBAAmB,GAAG,KAAK,MAAM,EAAE,OAAO,KAAK,WAAW,CAAC;AAEhE;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,OAAO,GAAG,mBAAmB,CAOxE;AAsBD,wBAAgB,gCAAgC,CAAC,OAAO,EAAE,iCAAiC,GAAG,SAAS,CAsMtG"}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Closes the gap `postgres-query-collector.ts` discloses: "queries only ...
|
|
3
|
+
* no before/after row state." Postgres's own logical decoding is the
|
|
4
|
+
* mechanism — no extension install, no schema change, no trigger/audit
|
|
5
|
+
* table the target application doesn't already have.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle, deliberately shaped around `Collector.start()`/`stop()` rather
|
|
8
|
+
* than a continuous stream (unlike `PostgresQueryCollector`, which tails a
|
|
9
|
+
* log line-by-line as it's written):
|
|
10
|
+
*
|
|
11
|
+
* - `start()`: opens ONE `pg` connection, checks two preconditions
|
|
12
|
+
* (`wal_level = 'logical'`, and the role has `REPLICATION` or superuser),
|
|
13
|
+
* refuses by name (never throws) if either fails, then creates a
|
|
14
|
+
* **temporary** logical replication slot (`pg_create_logical_replication_slot`
|
|
15
|
+
* with `test_decoding` and `temporary = true`). Temporary slots drop on
|
|
16
|
+
* disconnect — a crash between create and read cannot leave WAL retained
|
|
17
|
+
* on the server, which a permanent slot could. The `CollectorContext`
|
|
18
|
+
* passed in is retained (closed over) for use at `stop()`, since
|
|
19
|
+
* `Collector.stop()` itself takes none.
|
|
20
|
+
* - `stop()`: reads `pg_logical_slot_get_changes` once on that same
|
|
21
|
+
* connection, parses each row via `postgres-decoding-parser.ts`, emits one
|
|
22
|
+
* `DATABASE_QUERY` evidence item per recognised row change, then closes
|
|
23
|
+
* the connection (which also drops the temporary slot).
|
|
24
|
+
*
|
|
25
|
+
* Before-value completeness is a table-level fact (`REPLICA IDENTITY`), read
|
|
26
|
+
* once via `pg_class.relreplident` at slot-creation time and attached to
|
|
27
|
+
* every emitted row as `beforeCompleteness` — disclosed per DEC-NEXT
|
|
28
|
+
* row-change-evidence-reuses-database-query-type, never inferred from which
|
|
29
|
+
* of `postgres-decoding-parser.ts`'s three UPDATE/DELETE shapes matched.
|
|
30
|
+
*
|
|
31
|
+
* `pg` is an *optional* dependency here, loaded via dynamic `import()`
|
|
32
|
+
* exactly like `adapter-sql`'s `live-schema.ts` (see that file's own header
|
|
33
|
+
* for why: keeps every consumer that never wants row capture from needing a
|
|
34
|
+
* Postgres client library, and keeps `tsc` from requiring `pg`'s types to
|
|
35
|
+
* check this file). Not installed → the import throws → this collector
|
|
36
|
+
* refuses by name, same as a failed precondition check.
|
|
37
|
+
*/
|
|
38
|
+
var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
|
|
39
|
+
if (typeof path === "string" && /^\.\.?\//.test(path)) {
|
|
40
|
+
return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
|
|
41
|
+
return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return path;
|
|
45
|
+
};
|
|
46
|
+
import { parseTestDecodingLine } from "./postgres-decoding-parser.js";
|
|
47
|
+
import { COLLECTOR_VERSION } from "./collector-version.js";
|
|
48
|
+
const EVENT_TYPE = "DATABASE_QUERY";
|
|
49
|
+
/**
|
|
50
|
+
* The `Client` constructor from a dynamically imported `pg`. `pg` is CommonJS,
|
|
51
|
+
* so under `import()` its exports sit on `default` and Node's named-export
|
|
52
|
+
* detection does not surface `Client` at the top level; the first release read
|
|
53
|
+
* only the top level and every real connection failed with "pgModule.Client is
|
|
54
|
+
* not a constructor". Both shapes are read, and a module exposing neither is a
|
|
55
|
+
* loud error rather than a silent no-op.
|
|
56
|
+
*/
|
|
57
|
+
export function pgClientConstructor(loaded) {
|
|
58
|
+
const module = loaded;
|
|
59
|
+
const candidate = module?.Client ?? module?.default?.Client;
|
|
60
|
+
if (typeof candidate !== "function") {
|
|
61
|
+
throw new Error('"pg" was loaded but exposes no Client constructor, at its top level or on its default export');
|
|
62
|
+
}
|
|
63
|
+
return candidate;
|
|
64
|
+
}
|
|
65
|
+
async function defaultClientFactory(connectionString) {
|
|
66
|
+
// Read from a variable, not a string literal -- see the module header:
|
|
67
|
+
// keeps `tsc` from requiring `pg`'s type declarations to check this file,
|
|
68
|
+
// an ordinary dynamic import at runtime either way.
|
|
69
|
+
const moduleName = "pg";
|
|
70
|
+
const Client = pgClientConstructor(await import(__rewriteRelativeImportExtension(moduleName)));
|
|
71
|
+
return new Client({ connectionString });
|
|
72
|
+
}
|
|
73
|
+
function makeSlotName(processId) {
|
|
74
|
+
const safe = processId.replace(/[^a-zA-Z0-9_]/g, "_").slice(0, 40);
|
|
75
|
+
return `descry_rowcap_${safe}_${Date.now()}_${Math.floor(Math.random() * 1_000_000)}`;
|
|
76
|
+
}
|
|
77
|
+
function replidentToCompleteness(code) {
|
|
78
|
+
if (code === "f")
|
|
79
|
+
return "full";
|
|
80
|
+
if (code === "n" || code === "i")
|
|
81
|
+
return "none";
|
|
82
|
+
return "key-only"; // "d" (default, PK-only) and anything unrecognised -- assume the narrower claim, never the wider one
|
|
83
|
+
}
|
|
84
|
+
export function createPostgresRowChangeCollector(options) {
|
|
85
|
+
const processId = options.processId ?? "postgres-row-changes";
|
|
86
|
+
const clientFactory = options.clientFactory ?? defaultClientFactory;
|
|
87
|
+
let client = null;
|
|
88
|
+
let slot = null;
|
|
89
|
+
let savedContext = null;
|
|
90
|
+
let refusalReason = null;
|
|
91
|
+
let identityByTable = new Map();
|
|
92
|
+
function computeCapabilities() {
|
|
93
|
+
const unrelated = {
|
|
94
|
+
availability: "unavailable",
|
|
95
|
+
reason: "PostgresRowChangeCollector observes row-level DATABASE_QUERY (row-change) events only.",
|
|
96
|
+
};
|
|
97
|
+
const databaseObservation = refusalReason !== null
|
|
98
|
+
? { availability: "unavailable", reason: refusalReason }
|
|
99
|
+
: {
|
|
100
|
+
availability: "available",
|
|
101
|
+
reason: "row-level INSERT/UPDATE/DELETE via a temporary logical replication slot (test_decoding); " +
|
|
102
|
+
"before-values are full only for tables with REPLICA IDENTITY FULL -- key-only or absent " +
|
|
103
|
+
"otherwise, disclosed per row as beforeCompleteness.",
|
|
104
|
+
};
|
|
105
|
+
return {
|
|
106
|
+
domObservation: unrelated,
|
|
107
|
+
consoleObservation: unrelated,
|
|
108
|
+
networkObservation: unrelated,
|
|
109
|
+
backendLogAccess: unrelated,
|
|
110
|
+
distributedTrace: unrelated,
|
|
111
|
+
sourceMapping: unrelated,
|
|
112
|
+
stackCapture: unrelated,
|
|
113
|
+
processLifecycle: unrelated,
|
|
114
|
+
databaseObservation,
|
|
115
|
+
externalServiceObservation: unrelated,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function emitRowChange(context, parsed, xid) {
|
|
119
|
+
const completeness = identityByTable.get(`${parsed.schema}.${parsed.table}`) ?? "key-only";
|
|
120
|
+
context.emit({
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
source: "backend-process",
|
|
123
|
+
service: options.service ?? null,
|
|
124
|
+
process: processId,
|
|
125
|
+
eventType: EVENT_TYPE,
|
|
126
|
+
payload: {
|
|
127
|
+
kind: "row-change",
|
|
128
|
+
schema: parsed.schema,
|
|
129
|
+
table: parsed.table,
|
|
130
|
+
op: parsed.op,
|
|
131
|
+
before: parsed.before,
|
|
132
|
+
after: parsed.after,
|
|
133
|
+
xid: xid === undefined || xid === null ? null : String(xid),
|
|
134
|
+
beforeCompleteness: completeness,
|
|
135
|
+
},
|
|
136
|
+
traceId: null,
|
|
137
|
+
requestId: null,
|
|
138
|
+
correlationId: null,
|
|
139
|
+
graphNodeId: null,
|
|
140
|
+
sourceLocation: null,
|
|
141
|
+
stackTrace: null,
|
|
142
|
+
confidence: 1,
|
|
143
|
+
redactionStatus: "pending-redaction",
|
|
144
|
+
collectorVersion: COLLECTOR_VERSION,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
function emitCollectorError(context, detail) {
|
|
148
|
+
try {
|
|
149
|
+
context.emit({
|
|
150
|
+
timestamp: new Date().toISOString(),
|
|
151
|
+
source: "backend-process",
|
|
152
|
+
service: options.service ?? null,
|
|
153
|
+
process: processId,
|
|
154
|
+
eventType: "COLLECTOR_ERROR",
|
|
155
|
+
payload: {
|
|
156
|
+
raw: `postgres row-change collector for process "${processId}" failed: ${detail}`,
|
|
157
|
+
collectorId: `postgres-row-collector:${processId}`,
|
|
158
|
+
error: detail,
|
|
159
|
+
},
|
|
160
|
+
traceId: null,
|
|
161
|
+
requestId: null,
|
|
162
|
+
correlationId: null,
|
|
163
|
+
graphNodeId: null,
|
|
164
|
+
sourceLocation: null,
|
|
165
|
+
stackTrace: null,
|
|
166
|
+
confidence: 1,
|
|
167
|
+
redactionStatus: "pending-redaction",
|
|
168
|
+
collectorVersion: COLLECTOR_VERSION,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
catch (emitError) {
|
|
172
|
+
console.error(`[postgres-row-collector:${processId}] failed (${detail}) AND the failure could not be emitted: ${String(emitError)}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
async function closeClient() {
|
|
176
|
+
if (client === null)
|
|
177
|
+
return;
|
|
178
|
+
try {
|
|
179
|
+
await client.end();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Best-effort close -- the temporary slot drops on disconnect regardless.
|
|
183
|
+
}
|
|
184
|
+
client = null;
|
|
185
|
+
slot = null;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
collectorId: `postgres-row-collector:${processId}`,
|
|
189
|
+
async start(context) {
|
|
190
|
+
savedContext = context;
|
|
191
|
+
let candidate;
|
|
192
|
+
try {
|
|
193
|
+
candidate = await clientFactory(options.connectionString);
|
|
194
|
+
await candidate.connect();
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
198
|
+
refusalReason = `could not connect for row-level capture (${detail}); the query-text channel is unaffected.`;
|
|
199
|
+
return { available: false, reason: refusalReason };
|
|
200
|
+
}
|
|
201
|
+
client = candidate;
|
|
202
|
+
try {
|
|
203
|
+
const walLevelResult = await client.query("SHOW wal_level");
|
|
204
|
+
const walLevel = String(walLevelResult.rows[0]?.wal_level ?? "").toLowerCase();
|
|
205
|
+
if (walLevel !== "logical") {
|
|
206
|
+
refusalReason =
|
|
207
|
+
`wal_level is "${walLevel || "unknown"}", not "logical" -- row-level capture needs logical replication ` +
|
|
208
|
+
"enabled on the server (a superuser-only setting); the query-text channel is unaffected.";
|
|
209
|
+
await closeClient();
|
|
210
|
+
return { available: false, reason: refusalReason };
|
|
211
|
+
}
|
|
212
|
+
const roleResult = await client.query("SELECT rolreplication, rolsuper FROM pg_roles WHERE rolname = current_user");
|
|
213
|
+
const role = roleResult.rows[0];
|
|
214
|
+
if (role === undefined || (role.rolreplication !== true && role.rolsuper !== true)) {
|
|
215
|
+
refusalReason =
|
|
216
|
+
"the connection's role has neither REPLICATION nor superuser privilege -- row-level capture needs one " +
|
|
217
|
+
"of them to create a logical replication slot; the query-text channel is unaffected.";
|
|
218
|
+
await closeClient();
|
|
219
|
+
return { available: false, reason: refusalReason };
|
|
220
|
+
}
|
|
221
|
+
const identityResult = await client.query("SELECT n.nspname AS schema, c.relname AS table, c.relreplident AS identity " +
|
|
222
|
+
"FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace " +
|
|
223
|
+
"WHERE c.relkind = 'r' AND n.nspname NOT IN ('pg_catalog', 'information_schema')");
|
|
224
|
+
identityByTable = new Map();
|
|
225
|
+
for (const row of identityResult.rows) {
|
|
226
|
+
const r = row;
|
|
227
|
+
identityByTable.set(`${String(r.schema)}.${String(r.table)}`, replidentToCompleteness(r.identity));
|
|
228
|
+
}
|
|
229
|
+
slot = makeSlotName(processId);
|
|
230
|
+
await client.query("SELECT * FROM pg_create_logical_replication_slot($1, 'test_decoding', false, true)", [slot]);
|
|
231
|
+
return { available: true };
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
235
|
+
refusalReason = `could not start row-level capture: ${detail}`;
|
|
236
|
+
await closeClient();
|
|
237
|
+
return { available: false, reason: refusalReason };
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
async stop() {
|
|
241
|
+
if (client === null || slot === null)
|
|
242
|
+
return;
|
|
243
|
+
const activeClient = client;
|
|
244
|
+
const activeSlot = slot;
|
|
245
|
+
const context = savedContext;
|
|
246
|
+
try {
|
|
247
|
+
const changes = await activeClient.query("SELECT xid, data FROM pg_logical_slot_get_changes($1, NULL, NULL)", [activeSlot]);
|
|
248
|
+
for (const row of changes.rows) {
|
|
249
|
+
const data = String(row.data ?? "");
|
|
250
|
+
const xid = row.xid;
|
|
251
|
+
const parsed = parseTestDecodingLine(data);
|
|
252
|
+
if (parsed === null)
|
|
253
|
+
continue; // BEGIN/COMMIT or unrecognised -- refused, not guessed
|
|
254
|
+
if (context !== null)
|
|
255
|
+
emitRowChange(context, parsed, xid);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
catch (error) {
|
|
259
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
260
|
+
refusalReason = `could not read the replication slot: ${detail}`;
|
|
261
|
+
if (context !== null)
|
|
262
|
+
emitCollectorError(context, detail);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
await closeClient();
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
capabilities() {
|
|
269
|
+
return computeCapabilities();
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=postgres-row-collector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgres-row-collector.js","sourceRoot":"","sources":["../src/postgres-row-collector.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite row-change capture: before/after snapshot-and-diff, not a
|
|
3
|
+
* change-feed — SQLite has no logical-decoding equivalent, so this observes
|
|
4
|
+
* by re-reading, not by tailing. `sqlite-instrumentation.ts`'s preload
|
|
5
|
+
* already captures query *text*; this closes the same before/after-row gap
|
|
6
|
+
* `postgres-row-collector.ts` closes for Postgres, by a different mechanism
|
|
7
|
+
* because SQLite offers a different one.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle: `start()` opens the target database **read-only**
|
|
10
|
+
* (`new DatabaseSync(path, { readOnly: true })` — this collector never
|
|
11
|
+
* writes) and snapshots every user table (`sqlite_master`, excluding
|
|
12
|
+
* `sqlite_%` internal tables) up to `rowCap` rows each, keyed by `rowid`.
|
|
13
|
+
* `stop()` re-reads the same table set and diffs by `rowid`: present only
|
|
14
|
+
* before → DELETE, present only after → INSERT, present in both with
|
|
15
|
+
* different column values → UPDATE. See
|
|
16
|
+
* DEC-NEXT-sqlite-row-snapshot-scope-and-cap for why scope is "every table
|
|
17
|
+
* in the database" rather than a query-derived subset, and why a
|
|
18
|
+
* `WITHOUT ROWID` table (no `rowid` to key by) is skipped and disclosed by
|
|
19
|
+
* name rather than guessed at with a substitute key.
|
|
20
|
+
*
|
|
21
|
+
* `truncated: true` on an emitted row's table means that table had more
|
|
22
|
+
* than `rowCap` rows at one of the two snapshots — a change outside the
|
|
23
|
+
* sampled window could exist and go unreported. Disclosed per row, not
|
|
24
|
+
* buried in a capability string alone.
|
|
25
|
+
*/
|
|
26
|
+
import type { Collector } from "@descryy/runtime-contracts";
|
|
27
|
+
/** The narrow slice of `node:sqlite` this module needs — swappable in tests for a fake with no real file on disk. */
|
|
28
|
+
export interface SqliteRowSource {
|
|
29
|
+
listTables(): readonly string[];
|
|
30
|
+
/** Reads up to `limit + 1` rows (the `+1` lets the caller detect truncation without a second COUNT query), keyed by `rowid`. Throws for a table this source cannot key by rowid (e.g. `WITHOUT ROWID`). */
|
|
31
|
+
readRows(table: string, limit: number): readonly Record<string, unknown>[];
|
|
32
|
+
close(): void;
|
|
33
|
+
}
|
|
34
|
+
export interface TableSnapshot {
|
|
35
|
+
readonly rowsByRowid: Map<string, Record<string, unknown>>;
|
|
36
|
+
readonly truncated: boolean;
|
|
37
|
+
}
|
|
38
|
+
export type SqliteRowOp = "INSERT" | "UPDATE" | "DELETE";
|
|
39
|
+
export interface SqliteRowDiff {
|
|
40
|
+
readonly table: string;
|
|
41
|
+
readonly op: SqliteRowOp;
|
|
42
|
+
readonly before: Record<string, unknown> | null;
|
|
43
|
+
readonly after: Record<string, unknown> | null;
|
|
44
|
+
readonly truncated: boolean;
|
|
45
|
+
}
|
|
46
|
+
/** Pure, independently testable: diffs one table's before/after snapshot by rowid. Exported for direct unit testing without a real database file. */
|
|
47
|
+
export declare function diffTableSnapshot(table: string, before: TableSnapshot, after: TableSnapshot): SqliteRowDiff[];
|
|
48
|
+
export interface SqliteRowChangeCollectorOptions {
|
|
49
|
+
readonly databasePath: string;
|
|
50
|
+
readonly rowCap?: number;
|
|
51
|
+
readonly service?: string;
|
|
52
|
+
readonly processId?: string;
|
|
53
|
+
/** Test/advanced seam — defaults to a real read-only `node:sqlite` handle. */
|
|
54
|
+
readonly sourceFactory?: (databasePath: string) => SqliteRowSource;
|
|
55
|
+
}
|
|
56
|
+
export declare function createSqliteRowChangeCollector(options: SqliteRowChangeCollectorOptions): Collector;
|
|
57
|
+
//# sourceMappingURL=sqlite-row-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-row-snapshot.d.ts","sourceRoot":"","sources":["../src/sqlite-row-snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAmF,MAAM,4BAA4B,CAAC;AAM7I,qHAAqH;AACrH,MAAM,WAAW,eAAe;IAC9B,UAAU,IAAI,SAAS,MAAM,EAAE,CAAC;IAChC,2MAA2M;IAC3M,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAC3E,KAAK,IAAI,IAAI,CAAC;CACf;AAwBD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAqBD,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,EAAE,WAAW,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;CAC7B;AAED,qJAAqJ;AACrJ,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,KAAK,EAAE,aAAa,GAAG,aAAa,EAAE,CAgB7G;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,eAAe,CAAC;CACpE;AAID,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,+BAA+B,GAAG,SAAS,CA4HlG"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite row-change capture: before/after snapshot-and-diff, not a
|
|
3
|
+
* change-feed — SQLite has no logical-decoding equivalent, so this observes
|
|
4
|
+
* by re-reading, not by tailing. `sqlite-instrumentation.ts`'s preload
|
|
5
|
+
* already captures query *text*; this closes the same before/after-row gap
|
|
6
|
+
* `postgres-row-collector.ts` closes for Postgres, by a different mechanism
|
|
7
|
+
* because SQLite offers a different one.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle: `start()` opens the target database **read-only**
|
|
10
|
+
* (`new DatabaseSync(path, { readOnly: true })` — this collector never
|
|
11
|
+
* writes) and snapshots every user table (`sqlite_master`, excluding
|
|
12
|
+
* `sqlite_%` internal tables) up to `rowCap` rows each, keyed by `rowid`.
|
|
13
|
+
* `stop()` re-reads the same table set and diffs by `rowid`: present only
|
|
14
|
+
* before → DELETE, present only after → INSERT, present in both with
|
|
15
|
+
* different column values → UPDATE. See
|
|
16
|
+
* DEC-NEXT-sqlite-row-snapshot-scope-and-cap for why scope is "every table
|
|
17
|
+
* in the database" rather than a query-derived subset, and why a
|
|
18
|
+
* `WITHOUT ROWID` table (no `rowid` to key by) is skipped and disclosed by
|
|
19
|
+
* name rather than guessed at with a substitute key.
|
|
20
|
+
*
|
|
21
|
+
* `truncated: true` on an emitted row's table means that table had more
|
|
22
|
+
* than `rowCap` rows at one of the two snapshots — a change outside the
|
|
23
|
+
* sampled window could exist and go unreported. Disclosed per row, not
|
|
24
|
+
* buried in a capability string alone.
|
|
25
|
+
*/
|
|
26
|
+
import { DatabaseSync } from "node:sqlite";
|
|
27
|
+
import { COLLECTOR_VERSION } from "./collector-version.js";
|
|
28
|
+
const DEFAULT_ROW_CAP = 10_000;
|
|
29
|
+
function quoteIdentifier(name) {
|
|
30
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
31
|
+
}
|
|
32
|
+
function defaultSourceFactory(databasePath) {
|
|
33
|
+
const db = new DatabaseSync(databasePath, { readOnly: true });
|
|
34
|
+
return {
|
|
35
|
+
listTables() {
|
|
36
|
+
const rows = db
|
|
37
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite\\_%' ESCAPE '\\'")
|
|
38
|
+
.all();
|
|
39
|
+
return rows.map((row) => row.name);
|
|
40
|
+
},
|
|
41
|
+
readRows(table, limit) {
|
|
42
|
+
return db.prepare(`SELECT rowid AS __rowid, * FROM ${quoteIdentifier(table)} LIMIT ?`).all(limit);
|
|
43
|
+
},
|
|
44
|
+
close() {
|
|
45
|
+
db.close();
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function snapshotTable(source, table, rowCap) {
|
|
50
|
+
const rows = source.readRows(table, rowCap + 1);
|
|
51
|
+
const truncated = rows.length > rowCap;
|
|
52
|
+
const bounded = truncated ? rows.slice(0, rowCap) : rows;
|
|
53
|
+
const rowsByRowid = new Map();
|
|
54
|
+
for (const row of bounded) {
|
|
55
|
+
const { __rowid, ...rest } = row;
|
|
56
|
+
rowsByRowid.set(String(__rowid), rest);
|
|
57
|
+
}
|
|
58
|
+
return { rowsByRowid, truncated };
|
|
59
|
+
}
|
|
60
|
+
function rowsEqual(a, b) {
|
|
61
|
+
const aKeys = Object.keys(a);
|
|
62
|
+
const bKeys = Object.keys(b);
|
|
63
|
+
if (aKeys.length !== bKeys.length)
|
|
64
|
+
return false;
|
|
65
|
+
return aKeys.every((key) => Object.is(a[key], b[key]) || String(a[key]) === String(b[key]));
|
|
66
|
+
}
|
|
67
|
+
/** Pure, independently testable: diffs one table's before/after snapshot by rowid. Exported for direct unit testing without a real database file. */
|
|
68
|
+
export function diffTableSnapshot(table, before, after) {
|
|
69
|
+
const diffs = [];
|
|
70
|
+
const truncated = before.truncated || after.truncated;
|
|
71
|
+
const allRowids = new Set([...before.rowsByRowid.keys(), ...after.rowsByRowid.keys()]);
|
|
72
|
+
for (const rowid of allRowids) {
|
|
73
|
+
const beforeRow = before.rowsByRowid.get(rowid) ?? null;
|
|
74
|
+
const afterRow = after.rowsByRowid.get(rowid) ?? null;
|
|
75
|
+
if (beforeRow === null && afterRow !== null) {
|
|
76
|
+
diffs.push({ table, op: "INSERT", before: null, after: afterRow, truncated });
|
|
77
|
+
}
|
|
78
|
+
else if (beforeRow !== null && afterRow === null) {
|
|
79
|
+
diffs.push({ table, op: "DELETE", before: beforeRow, after: null, truncated });
|
|
80
|
+
}
|
|
81
|
+
else if (beforeRow !== null && afterRow !== null && !rowsEqual(beforeRow, afterRow)) {
|
|
82
|
+
diffs.push({ table, op: "UPDATE", before: beforeRow, after: afterRow, truncated });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return diffs;
|
|
86
|
+
}
|
|
87
|
+
const EVENT_TYPE = "DATABASE_QUERY";
|
|
88
|
+
export function createSqliteRowChangeCollector(options) {
|
|
89
|
+
const processId = options.processId ?? "sqlite-row-changes";
|
|
90
|
+
const rowCap = options.rowCap ?? DEFAULT_ROW_CAP;
|
|
91
|
+
const sourceFactory = options.sourceFactory ?? defaultSourceFactory;
|
|
92
|
+
let source = null;
|
|
93
|
+
let tables = [];
|
|
94
|
+
let beforeSnapshots = new Map();
|
|
95
|
+
let skippedTables = [];
|
|
96
|
+
let refusalReason = null;
|
|
97
|
+
function capabilities() {
|
|
98
|
+
const unrelated = {
|
|
99
|
+
availability: "unavailable",
|
|
100
|
+
reason: "SqliteRowChangeCollector observes row-level DATABASE_QUERY (row-change) events only.",
|
|
101
|
+
};
|
|
102
|
+
const databaseObservation = refusalReason !== null
|
|
103
|
+
? { availability: "unavailable", reason: refusalReason }
|
|
104
|
+
: {
|
|
105
|
+
availability: "available",
|
|
106
|
+
reason: `row-level INSERT/UPDATE/DELETE via before/after snapshot-and-diff, capped at ${rowCap} rows per table ` +
|
|
107
|
+
"(truncated tables are disclosed per row as truncated: true)." +
|
|
108
|
+
(skippedTables.length > 0
|
|
109
|
+
? ` Skipped (no rowid to key by, likely WITHOUT ROWID): ${skippedTables.join(", ")}.`
|
|
110
|
+
: ""),
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
domObservation: unrelated,
|
|
114
|
+
consoleObservation: unrelated,
|
|
115
|
+
networkObservation: unrelated,
|
|
116
|
+
backendLogAccess: unrelated,
|
|
117
|
+
distributedTrace: unrelated,
|
|
118
|
+
sourceMapping: unrelated,
|
|
119
|
+
stackCapture: unrelated,
|
|
120
|
+
processLifecycle: unrelated,
|
|
121
|
+
databaseObservation,
|
|
122
|
+
externalServiceObservation: unrelated,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function emitDiff(context, diff) {
|
|
126
|
+
context.emit({
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
source: "backend-process",
|
|
129
|
+
service: options.service ?? null,
|
|
130
|
+
process: processId,
|
|
131
|
+
eventType: EVENT_TYPE,
|
|
132
|
+
payload: {
|
|
133
|
+
kind: "row-change",
|
|
134
|
+
table: diff.table,
|
|
135
|
+
op: diff.op,
|
|
136
|
+
before: diff.before,
|
|
137
|
+
after: diff.after,
|
|
138
|
+
truncated: diff.truncated,
|
|
139
|
+
},
|
|
140
|
+
traceId: null,
|
|
141
|
+
requestId: null,
|
|
142
|
+
correlationId: null,
|
|
143
|
+
graphNodeId: null,
|
|
144
|
+
sourceLocation: null,
|
|
145
|
+
stackTrace: null,
|
|
146
|
+
confidence: 1,
|
|
147
|
+
redactionStatus: "pending-redaction",
|
|
148
|
+
collectorVersion: COLLECTOR_VERSION,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
let savedContext = null;
|
|
152
|
+
return {
|
|
153
|
+
collectorId: `sqlite-row-collector:${processId}`,
|
|
154
|
+
start(context) {
|
|
155
|
+
savedContext = context;
|
|
156
|
+
try {
|
|
157
|
+
source = sourceFactory(options.databasePath);
|
|
158
|
+
tables = source.listTables();
|
|
159
|
+
beforeSnapshots = new Map();
|
|
160
|
+
skippedTables = [];
|
|
161
|
+
for (const table of tables) {
|
|
162
|
+
try {
|
|
163
|
+
beforeSnapshots.set(table, snapshotTable(source, table, rowCap));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
skippedTables.push(table);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return Promise.resolve({ available: true });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const detail = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
|
|
173
|
+
refusalReason = `could not open "${options.databasePath}" read-only for row-level capture: ${detail}`;
|
|
174
|
+
source = null;
|
|
175
|
+
return Promise.resolve({ available: false, reason: refusalReason });
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
async stop() {
|
|
179
|
+
if (source === null)
|
|
180
|
+
return;
|
|
181
|
+
const activeSource = source;
|
|
182
|
+
const context = savedContext;
|
|
183
|
+
try {
|
|
184
|
+
for (const table of tables) {
|
|
185
|
+
const before = beforeSnapshots.get(table);
|
|
186
|
+
if (before === undefined)
|
|
187
|
+
continue; // skipped at start() -- stays skipped, disclosed via capabilities
|
|
188
|
+
let after;
|
|
189
|
+
try {
|
|
190
|
+
after = snapshotTable(activeSource, table, rowCap);
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const diffs = diffTableSnapshot(table, before, after);
|
|
196
|
+
if (context !== null) {
|
|
197
|
+
for (const diff of diffs)
|
|
198
|
+
emitDiff(context, diff);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
activeSource.close();
|
|
204
|
+
source = null;
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
capabilities,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=sqlite-row-snapshot.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlite-row-snapshot.js","sourceRoot":"","sources":["../src/sqlite-row-snapshot.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descryy/runtime-database-observation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "Database
|
|
5
|
+
"description": "Database observation (plan §18, checklist §14): real DATABASE_QUERY capture via driver instrumentation, never parsed application logs, plus row-level before/after state where the driver honestly supports it (Postgres logical decoding, SQLite before/after snapshot; MySQL refuses by name pending an installed driver). Driver #1 is node:sqlite.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -25,16 +25,20 @@
|
|
|
25
25
|
"registry": "https://registry.npmjs.org",
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
|
-
"scripts": {
|
|
29
|
-
"build": "tsc -b"
|
|
30
|
-
},
|
|
31
28
|
"dependencies": {
|
|
32
29
|
"@descryy/runtime-contracts": "0.3.3",
|
|
33
30
|
"@descryy/runtime-backend-observation": "0.3.6",
|
|
34
|
-
"@descryy/runtime-graph-correlator": "0.
|
|
31
|
+
"@descryy/runtime-graph-correlator": "0.5.0"
|
|
35
32
|
},
|
|
36
33
|
"devDependencies": {
|
|
37
|
-
"@descryy/
|
|
38
|
-
"@descryy/runtime-
|
|
34
|
+
"@descryy/ir": "0.11.0",
|
|
35
|
+
"@descryy/runtime-controller": "0.3.9",
|
|
36
|
+
"@descryy/runtime-orchestrator": "0.4.12"
|
|
37
|
+
},
|
|
38
|
+
"optionalDependencies": {
|
|
39
|
+
"pg": "^8.13.0"
|
|
40
|
+
},
|
|
41
|
+
"scripts": {
|
|
42
|
+
"build": "tsc -b"
|
|
39
43
|
}
|
|
40
|
-
}
|
|
44
|
+
}
|