@objectstack/metadata 17.0.0-rc.2 → 17.0.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +789 -0
- package/dist/errors.cjs +61 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.d.cts +18 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +34 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.cjs +885 -60
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +578 -4
- package/dist/index.d.ts +578 -4
- package/dist/index.js +889 -57
- package/dist/index.js.map +1 -1
- package/dist/node.cjs +885 -60
- package/dist/node.cjs.map +1 -1
- package/dist/node.js +889 -57
- package/dist/node.js.map +1 -1
- package/package.json +13 -8
package/dist/errors.cjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/errors.ts
|
|
21
|
+
var errors_exports = {};
|
|
22
|
+
__export(errors_exports, {
|
|
23
|
+
isMissingTableError: () => isMissingTableError
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(errors_exports);
|
|
26
|
+
|
|
27
|
+
// src/utils/schema-sync-errors.ts
|
|
28
|
+
var MISSING_TABLE = {
|
|
29
|
+
codes: /* @__PURE__ */ new Set([
|
|
30
|
+
"42P01",
|
|
31
|
+
// PostgreSQL undefined_table
|
|
32
|
+
"ER_NO_SUCH_TABLE"
|
|
33
|
+
// MySQL / MariaDB 1146
|
|
34
|
+
]),
|
|
35
|
+
errnos: /* @__PURE__ */ new Set([1146]),
|
|
36
|
+
/**
|
|
37
|
+
* - SQLite / libsql: `no such table: sys_metadata_history`
|
|
38
|
+
* - PostgreSQL: `relation "sys_metadata_history" does not exist`
|
|
39
|
+
* - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
|
|
40
|
+
*/
|
|
41
|
+
message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i
|
|
42
|
+
};
|
|
43
|
+
var MAX_CAUSE_DEPTH = 4;
|
|
44
|
+
function matchesDriverError(error, signature, depth) {
|
|
45
|
+
if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
|
|
46
|
+
if (typeof error === "string") return signature.message.test(error);
|
|
47
|
+
if (typeof error !== "object") return false;
|
|
48
|
+
const err = error;
|
|
49
|
+
if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
|
|
50
|
+
if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
|
|
51
|
+
if (typeof err.message === "string" && signature.message.test(err.message)) return true;
|
|
52
|
+
return matchesDriverError(err.cause, signature, depth + 1);
|
|
53
|
+
}
|
|
54
|
+
function isMissingTableError(error, depth = 0) {
|
|
55
|
+
return matchesDriverError(error, MISSING_TABLE, depth);
|
|
56
|
+
}
|
|
57
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
58
|
+
0 && (module.exports = {
|
|
59
|
+
isMissingTableError
|
|
60
|
+
});
|
|
61
|
+
//# sourceMappingURL=errors.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/utils/schema-sync-errors.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * `@objectstack/metadata/errors` — the shared driver-error discriminators for\n * the metadata storage seams (#4728 / #4825 / #4867 family).\n *\n * ## Why this subpath exists\n *\n * The \"which driver failures may be silenced?\" question is not local to one\n * package. It was answered first for DDL in `@objectstack/metadata`\n * (`ensureSchema`, #4728), then for reads on the legacy `DatabaseLoader` path\n * (`nextEventSeq`, #4825) — and the *canonical* transactional producer of the\n * very same numbers, `SysMetadataRepository`, lives in a different package\n * (`@objectstack/metadata-protocol`, #4867) and carried the identical defect.\n *\n * Three ways to serve that second package were considered; the third is the\n * one taken, and the first is the one this module exists to prevent:\n *\n * 1. **Copy the predicate.** Rejected. Two hand-rolled vocabularies of\n * \"benign driver error\" is precisely the dual-source debt #4825 killed:\n * a driver quirk taught to one copy and not the other produces two\n * packages that disagree about whether data may be silently invented.\n * 2. **Sink it into a common dependency** (`@objectstack/types`,\n * `@objectstack/spec/shared`). Architecturally attractive and explicitly\n * *not* precluded by this module — but out of scope on the round that\n * needed it (spec was frozen; types was under concurrent change).\n * 3. **Export it deliberately from its current home** — this file. One\n * declaration, one implementation, one place a new driver quirk is taught.\n *\n * ## Why a subpath and not the package entry\n *\n * `@objectstack/metadata`'s root entry pulls the manager, every loader and the\n * YAML/filesystem machinery behind them. A consumer that wants a 40-line\n * predicate should not have to load any of that, and the weight is exactly\n * what would tempt the next author back to option 1. This entry re-exports\n * one leaf module and nothing else, so the cross-package edge stays a leaf\n * edge — and stays a single, greppable seam to delete if the maintainer later\n * takes option 2.\n *\n * ## Scope of the promise\n *\n * Only {@link isMissingTableError} is exported: it has a cross-package\n * consumer today. Its sibling `isSchemaAlreadyExistsError` deliberately stays\n * internal to this package — it has no consumer outside it, and an exported\n * symbol nobody imports is a promise made for nothing (Prime Directive #10,\n * pointed at our own API surface). Add it here the day something outside\n * `@objectstack/metadata` needs it, not before.\n */\n\nexport { isMissingTableError } from './utils/schema-sync-errors.js';\n","// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Driver-error classification for the metadata storage seams (#4728, #4825;\n * rule from #4632).\n *\n * Two questions live here, and they share one mechanism on purpose. A second\n * hand-rolled `catch`-and-guess elsewhere in this package would be a second\n * de-facto vocabulary of \"which driver errors are benign\" — the exact debt this\n * module exists to retire. Both predicates below are thin wrappers over one\n * signature matcher, so a driver quirk is taught to the package once.\n *\n * 1. {@link isSchemaAlreadyExistsError} — \"was this DDL failure just the table\n * already being there?\" (#4728, `ensureSchema` / `ensureHistorySchema`).\n * 2. {@link isMissingTableError} — \"did this READ fail because the table has\n * not been provisioned yet?\" (#4825, `nextEventSeq`).\n *\n * They are deliberately **not** each other's negation. Each answers \"is this\n * the one benign reason?\" and defaults to *not benign*, so an error neither\n * recognises is loud under both.\n *\n * ---\n *\n * ## 1. DDL failure classification (#4728)\n *\n * `IDataDriver.syncSchema()` is contractually **idempotent** (\"creates tables if\n * missing, adds columns, updates indexes\"), so in principle a re-sync of an\n * existing table should not throw at all. In practice a driver may surface the\n * already-provisioned case as an error instead of a no-op — `CREATE TABLE`\n * without `IF NOT EXISTS`, an `ALTER TABLE ADD COLUMN` for a column that is\n * already there. That single failure reason is benign: the table and its columns\n * exist, so the bytes will land.\n *\n * **Every other** DDL failure is not benign, and the difference is the whole\n * point of this module. Insufficient privileges, a datasource that never\n * connected, an incompatible column type — after those, the table or column does\n * not exist, yet the process keeps looking healthy while everything it claims to\n * persist has nowhere to land. That is the #4420 shape, and AGENTS.md →\n * \"Degradation log levels\" requires it to be reported at `error`.\n *\n * The defect this replaces was a `catch` whose comment named the benign reason\n * (\"e.g. table already exists\") and used it to excuse **all** of them. Callers\n * must therefore ask the question by error *type*:\n *\n * ```ts\n * catch (error) {\n * if (!isSchemaAlreadyExistsError(error)) {\n * console.error('… consequence … fix …', error); // loud, and stay not-ready\n * return;\n * }\n * // benign only: the table is already provisioned, carry on\n * }\n * ```\n *\n * Classification is deliberately conservative — anything not positively\n * recognised as \"already exists\" is treated as a real failure, because the cost\n * of a false \"benign\" (silent data loss) is far higher than the cost of a false\n * \"real\" (one extra error line).\n *\n * ---\n *\n * ## 2. Missing-table classification for reads (#4825)\n *\n * `DatabaseLoader.nextEventSeq()` reads `sys_metadata_history` to decide what\n * `event_seq` the NEXT history row gets. Its `catch` named both reasons a read\n * can fail — \"table not provisioned yet\" (benign: 1 really is the next number)\n * and \"driver error\" (**not** benign) — and answered both with `return 1`.\n *\n * That is the #4728 shape one layer down, but the damage is the opposite kind\n * and worse. #4728 was *bytes that never landed*; this is **bytes that land\n * wrong**: with N rows already in the table, one flaky read hands the next row\n * `event_seq = 1`, colliding with an existing row. The insert **succeeds**, no\n * line is logged, and `event_seq` — the ordering key that history listing and\n * rollback targeting both stand on — is now silently untrustworthy.\n *\n * So the read seam gets the same treatment, with the same conservative default:\n *\n * ```ts\n * catch (error) {\n * if (isMissingTableError(error)) return 1; // benign: nothing to collide with\n * throw error; // caller reports the consequence\n * }\n * ```\n */\n\n/** One \"which errors mean X?\" vocabulary, in the three forms drivers use. */\ninterface DriverErrorSignature {\n /** `error.code` — Postgres SQLSTATE, or mysql2's symbolic name. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB numeric equivalents. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only signal SQLite-family drivers give. */\n readonly message: RegExp;\n}\n\n/**\n * Driver/SQLSTATE codes that mean \"the thing you asked me to create is already\n * there\". Postgres reports SQLSTATE on `code`; mysql2 reports its symbolic name.\n */\nconst ALREADY_EXISTS: DriverErrorSignature = {\n codes: new Set([\n // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)\n '42P07', // duplicate_table\n '42701', // duplicate_column\n '42710', // duplicate_object — index / constraint already exists\n // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)\n 'ER_TABLE_EXISTS_ERROR', // 1050\n 'ER_DUP_FIELDNAME', // 1060\n 'ER_DUP_KEYNAME', // 1061\n ]),\n errnos: new Set([1050, 1060, 1061]),\n /**\n * Message fallback for drivers that carry no machine-readable code —\n * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for\n * every DDL failure, so the message is the only signal available:\n * - `table sys_metadata already exists`\n * - `duplicate column name: environment_id`\n * - `index idx_x already exists`\n * Postgres phrases its own as `relation \"x\" already exists` /\n * `column \"x\" of relation \"y\" already exists`, which matches the same test.\n */\n message: /already exists|duplicate column name|duplicate key name/i,\n};\n\n/**\n * Codes/messages that mean \"the table you tried to READ has not been created\".\n *\n * Narrower than it looks, on purpose. `does not exist` on its own also covers\n * `role \"x\" does not exist` (42704), `database \"x\" does not exist` (3D000) and\n * `column \"x\" does not exist` (42703) — every one of them a **real** failure\n * that must stay loud, and every one of them a case where \"start numbering at\n * 1\" would be the wrong answer against a table that may be full of rows. So the\n * message test demands the word table/relation next to the phrase rather than\n * the phrase alone, and the code set carries only the table-scoped SQLSTATEs.\n */\nconst MISSING_TABLE: DriverErrorSignature = {\n codes: new Set([\n '42P01', // PostgreSQL undefined_table\n 'ER_NO_SUCH_TABLE', // MySQL / MariaDB 1146\n ]),\n errnos: new Set([1146]),\n /**\n * - SQLite / libsql: `no such table: sys_metadata_history`\n * - PostgreSQL: `relation \"sys_metadata_history\" does not exist`\n * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`\n */\n message:\n /no such table|relation [\"'`][^\"'`]+[\"'`] does not exist|table [\"'`][^\"'`]+[\"'`] doesn'?t exist|unknown table/i,\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * The single matcher both predicates run on: code, then errno, then message,\n * then one step down the `cause` chain.\n *\n * Unrecognised is always `false` — a benign verdict must be *earned*, never\n * defaulted to, because a false \"benign\" corrupts data while a false \"real\"\n * costs one error line.\n */\nfunction matchesDriverError(\n error: unknown,\n signature: DriverErrorSignature,\n depth: number,\n): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') return signature.message.test(error);\n if (typeof error !== 'object') return false;\n\n const err = error as {\n code?: unknown;\n errno?: unknown;\n message?: unknown;\n cause?: unknown;\n };\n\n if (typeof err.code === 'string' && signature.codes.has(err.code)) return true;\n if (typeof err.errno === 'number' && signature.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && signature.message.test(err.message)) return true;\n\n // Drivers commonly re-throw with the original attached as `cause`.\n return matchesDriverError(err.cause, signature, depth + 1);\n}\n\n/**\n * Is this DDL error the benign \"already provisioned\" case?\n *\n * @param error - The value thrown by `syncSchema()` (or any DDL call).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/column/index-already-exists. Anything else — including an\n * unrecognised error, `undefined`, or a permission/connection failure —\n * returns `false` and MUST be reported loudly by the caller.\n */\nexport function isSchemaAlreadyExistsError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, ALREADY_EXISTS, depth);\n}\n\n/**\n * Is this READ error the benign \"table has not been provisioned yet\" case?\n *\n * The only failure that licenses a caller to treat an empty table as the truth\n * — there are no rows, so there is nothing to be inconsistent with. A\n * connection drop, a timeout, a permission denial or a query error all mean the\n * rows may well exist and simply were not seen; those return `false` and the\n * caller must report the consequence and give up rather than compute an answer\n * from data it never read (#4825).\n *\n * @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/relation-does-not-exist.\n */\nexport function isMissingTableError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, MISSING_TABLE, depth);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACuIA,IAAM,gBAAsC;AAAA,EACxC,OAAO,oBAAI,IAAI;AAAA,IACX;AAAA;AAAA,IACA;AAAA;AAAA,EACJ,CAAC;AAAA,EACD,QAAQ,oBAAI,IAAI,CAAC,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,SACI;AACR;AAGA,IAAM,kBAAkB;AAUxB,SAAS,mBACL,OACA,WACA,OACO;AACP,MAAI,UAAU,QAAQ,UAAU,UAAa,QAAQ,gBAAiB,QAAO;AAE7E,MAAI,OAAO,UAAU,SAAU,QAAO,UAAU,QAAQ,KAAK,KAAK;AAClE,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,MAAM;AAOZ,MAAI,OAAO,IAAI,SAAS,YAAY,UAAU,MAAM,IAAI,IAAI,IAAI,EAAG,QAAO;AAC1E,MAAI,OAAO,IAAI,UAAU,YAAY,UAAU,OAAO,IAAI,IAAI,KAAK,EAAG,QAAO;AAC7E,MAAI,OAAO,IAAI,YAAY,YAAY,UAAU,QAAQ,KAAK,IAAI,OAAO,EAAG,QAAO;AAGnF,SAAO,mBAAmB,IAAI,OAAO,WAAW,QAAQ,CAAC;AAC7D;AA+BO,SAAS,oBAAoB,OAAgB,QAAQ,GAAY;AACpE,SAAO,mBAAmB,OAAO,eAAe,KAAK;AACzD;","names":[]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this READ error the benign "table has not been provisioned yet" case?
|
|
3
|
+
*
|
|
4
|
+
* The only failure that licenses a caller to treat an empty table as the truth
|
|
5
|
+
* — there are no rows, so there is nothing to be inconsistent with. A
|
|
6
|
+
* connection drop, a timeout, a permission denial or a query error all mean the
|
|
7
|
+
* rows may well exist and simply were not seen; those return `false` and the
|
|
8
|
+
* caller must report the consequence and give up rather than compute an answer
|
|
9
|
+
* from data it never read (#4825).
|
|
10
|
+
*
|
|
11
|
+
* @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).
|
|
12
|
+
* @param depth - Internal `cause`-chain recursion counter; callers pass nothing.
|
|
13
|
+
* @returns `true` only when the error positively identifies as
|
|
14
|
+
* table/relation-does-not-exist.
|
|
15
|
+
*/
|
|
16
|
+
declare function isMissingTableError(error: unknown, depth?: number): boolean;
|
|
17
|
+
|
|
18
|
+
export { isMissingTableError };
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is this READ error the benign "table has not been provisioned yet" case?
|
|
3
|
+
*
|
|
4
|
+
* The only failure that licenses a caller to treat an empty table as the truth
|
|
5
|
+
* — there are no rows, so there is nothing to be inconsistent with. A
|
|
6
|
+
* connection drop, a timeout, a permission denial or a query error all mean the
|
|
7
|
+
* rows may well exist and simply were not seen; those return `false` and the
|
|
8
|
+
* caller must report the consequence and give up rather than compute an answer
|
|
9
|
+
* from data it never read (#4825).
|
|
10
|
+
*
|
|
11
|
+
* @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).
|
|
12
|
+
* @param depth - Internal `cause`-chain recursion counter; callers pass nothing.
|
|
13
|
+
* @returns `true` only when the error positively identifies as
|
|
14
|
+
* table/relation-does-not-exist.
|
|
15
|
+
*/
|
|
16
|
+
declare function isMissingTableError(error: unknown, depth?: number): boolean;
|
|
17
|
+
|
|
18
|
+
export { isMissingTableError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/utils/schema-sync-errors.ts
|
|
2
|
+
var MISSING_TABLE = {
|
|
3
|
+
codes: /* @__PURE__ */ new Set([
|
|
4
|
+
"42P01",
|
|
5
|
+
// PostgreSQL undefined_table
|
|
6
|
+
"ER_NO_SUCH_TABLE"
|
|
7
|
+
// MySQL / MariaDB 1146
|
|
8
|
+
]),
|
|
9
|
+
errnos: /* @__PURE__ */ new Set([1146]),
|
|
10
|
+
/**
|
|
11
|
+
* - SQLite / libsql: `no such table: sys_metadata_history`
|
|
12
|
+
* - PostgreSQL: `relation "sys_metadata_history" does not exist`
|
|
13
|
+
* - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`
|
|
14
|
+
*/
|
|
15
|
+
message: /no such table|relation ["'`][^"'`]+["'`] does not exist|table ["'`][^"'`]+["'`] doesn'?t exist|unknown table/i
|
|
16
|
+
};
|
|
17
|
+
var MAX_CAUSE_DEPTH = 4;
|
|
18
|
+
function matchesDriverError(error, signature, depth) {
|
|
19
|
+
if (error === null || error === void 0 || depth > MAX_CAUSE_DEPTH) return false;
|
|
20
|
+
if (typeof error === "string") return signature.message.test(error);
|
|
21
|
+
if (typeof error !== "object") return false;
|
|
22
|
+
const err = error;
|
|
23
|
+
if (typeof err.code === "string" && signature.codes.has(err.code)) return true;
|
|
24
|
+
if (typeof err.errno === "number" && signature.errnos.has(err.errno)) return true;
|
|
25
|
+
if (typeof err.message === "string" && signature.message.test(err.message)) return true;
|
|
26
|
+
return matchesDriverError(err.cause, signature, depth + 1);
|
|
27
|
+
}
|
|
28
|
+
function isMissingTableError(error, depth = 0) {
|
|
29
|
+
return matchesDriverError(error, MISSING_TABLE, depth);
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
isMissingTableError
|
|
33
|
+
};
|
|
34
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/schema-sync-errors.ts"],"sourcesContent":["// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.\n\n/**\n * Driver-error classification for the metadata storage seams (#4728, #4825;\n * rule from #4632).\n *\n * Two questions live here, and they share one mechanism on purpose. A second\n * hand-rolled `catch`-and-guess elsewhere in this package would be a second\n * de-facto vocabulary of \"which driver errors are benign\" — the exact debt this\n * module exists to retire. Both predicates below are thin wrappers over one\n * signature matcher, so a driver quirk is taught to the package once.\n *\n * 1. {@link isSchemaAlreadyExistsError} — \"was this DDL failure just the table\n * already being there?\" (#4728, `ensureSchema` / `ensureHistorySchema`).\n * 2. {@link isMissingTableError} — \"did this READ fail because the table has\n * not been provisioned yet?\" (#4825, `nextEventSeq`).\n *\n * They are deliberately **not** each other's negation. Each answers \"is this\n * the one benign reason?\" and defaults to *not benign*, so an error neither\n * recognises is loud under both.\n *\n * ---\n *\n * ## 1. DDL failure classification (#4728)\n *\n * `IDataDriver.syncSchema()` is contractually **idempotent** (\"creates tables if\n * missing, adds columns, updates indexes\"), so in principle a re-sync of an\n * existing table should not throw at all. In practice a driver may surface the\n * already-provisioned case as an error instead of a no-op — `CREATE TABLE`\n * without `IF NOT EXISTS`, an `ALTER TABLE ADD COLUMN` for a column that is\n * already there. That single failure reason is benign: the table and its columns\n * exist, so the bytes will land.\n *\n * **Every other** DDL failure is not benign, and the difference is the whole\n * point of this module. Insufficient privileges, a datasource that never\n * connected, an incompatible column type — after those, the table or column does\n * not exist, yet the process keeps looking healthy while everything it claims to\n * persist has nowhere to land. That is the #4420 shape, and AGENTS.md →\n * \"Degradation log levels\" requires it to be reported at `error`.\n *\n * The defect this replaces was a `catch` whose comment named the benign reason\n * (\"e.g. table already exists\") and used it to excuse **all** of them. Callers\n * must therefore ask the question by error *type*:\n *\n * ```ts\n * catch (error) {\n * if (!isSchemaAlreadyExistsError(error)) {\n * console.error('… consequence … fix …', error); // loud, and stay not-ready\n * return;\n * }\n * // benign only: the table is already provisioned, carry on\n * }\n * ```\n *\n * Classification is deliberately conservative — anything not positively\n * recognised as \"already exists\" is treated as a real failure, because the cost\n * of a false \"benign\" (silent data loss) is far higher than the cost of a false\n * \"real\" (one extra error line).\n *\n * ---\n *\n * ## 2. Missing-table classification for reads (#4825)\n *\n * `DatabaseLoader.nextEventSeq()` reads `sys_metadata_history` to decide what\n * `event_seq` the NEXT history row gets. Its `catch` named both reasons a read\n * can fail — \"table not provisioned yet\" (benign: 1 really is the next number)\n * and \"driver error\" (**not** benign) — and answered both with `return 1`.\n *\n * That is the #4728 shape one layer down, but the damage is the opposite kind\n * and worse. #4728 was *bytes that never landed*; this is **bytes that land\n * wrong**: with N rows already in the table, one flaky read hands the next row\n * `event_seq = 1`, colliding with an existing row. The insert **succeeds**, no\n * line is logged, and `event_seq` — the ordering key that history listing and\n * rollback targeting both stand on — is now silently untrustworthy.\n *\n * So the read seam gets the same treatment, with the same conservative default:\n *\n * ```ts\n * catch (error) {\n * if (isMissingTableError(error)) return 1; // benign: nothing to collide with\n * throw error; // caller reports the consequence\n * }\n * ```\n */\n\n/** One \"which errors mean X?\" vocabulary, in the three forms drivers use. */\ninterface DriverErrorSignature {\n /** `error.code` — Postgres SQLSTATE, or mysql2's symbolic name. */\n readonly codes: ReadonlySet<string>;\n /** `error.errno` — MySQL/MariaDB numeric equivalents. */\n readonly errnos: ReadonlySet<number>;\n /** `error.message` — the only signal SQLite-family drivers give. */\n readonly message: RegExp;\n}\n\n/**\n * Driver/SQLSTATE codes that mean \"the thing you asked me to create is already\n * there\". Postgres reports SQLSTATE on `code`; mysql2 reports its symbolic name.\n */\nconst ALREADY_EXISTS: DriverErrorSignature = {\n codes: new Set([\n // PostgreSQL SQLSTATE (class 42 — syntax error or access rule violation)\n '42P07', // duplicate_table\n '42701', // duplicate_column\n '42710', // duplicate_object — index / constraint already exists\n // MySQL / MariaDB (mysql2 puts the symbolic name on `code`)\n 'ER_TABLE_EXISTS_ERROR', // 1050\n 'ER_DUP_FIELDNAME', // 1060\n 'ER_DUP_KEYNAME', // 1061\n ]),\n errnos: new Set([1050, 1060, 1061]),\n /**\n * Message fallback for drivers that carry no machine-readable code —\n * notably SQLite, whose `code` is the undifferentiated `SQLITE_ERROR` for\n * every DDL failure, so the message is the only signal available:\n * - `table sys_metadata already exists`\n * - `duplicate column name: environment_id`\n * - `index idx_x already exists`\n * Postgres phrases its own as `relation \"x\" already exists` /\n * `column \"x\" of relation \"y\" already exists`, which matches the same test.\n */\n message: /already exists|duplicate column name|duplicate key name/i,\n};\n\n/**\n * Codes/messages that mean \"the table you tried to READ has not been created\".\n *\n * Narrower than it looks, on purpose. `does not exist` on its own also covers\n * `role \"x\" does not exist` (42704), `database \"x\" does not exist` (3D000) and\n * `column \"x\" does not exist` (42703) — every one of them a **real** failure\n * that must stay loud, and every one of them a case where \"start numbering at\n * 1\" would be the wrong answer against a table that may be full of rows. So the\n * message test demands the word table/relation next to the phrase rather than\n * the phrase alone, and the code set carries only the table-scoped SQLSTATEs.\n */\nconst MISSING_TABLE: DriverErrorSignature = {\n codes: new Set([\n '42P01', // PostgreSQL undefined_table\n 'ER_NO_SUCH_TABLE', // MySQL / MariaDB 1146\n ]),\n errnos: new Set([1146]),\n /**\n * - SQLite / libsql: `no such table: sys_metadata_history`\n * - PostgreSQL: `relation \"sys_metadata_history\" does not exist`\n * - MySQL/MariaDB: `Table 'app.sys_metadata_history' doesn't exist`\n */\n message:\n /no such table|relation [\"'`][^\"'`]+[\"'`] does not exist|table [\"'`][^\"'`]+[\"'`] doesn'?t exist|unknown table/i,\n};\n\n/** How far to follow an `error.cause` chain — drivers wrap, but not deeply. */\nconst MAX_CAUSE_DEPTH = 4;\n\n/**\n * The single matcher both predicates run on: code, then errno, then message,\n * then one step down the `cause` chain.\n *\n * Unrecognised is always `false` — a benign verdict must be *earned*, never\n * defaulted to, because a false \"benign\" corrupts data while a false \"real\"\n * costs one error line.\n */\nfunction matchesDriverError(\n error: unknown,\n signature: DriverErrorSignature,\n depth: number,\n): boolean {\n if (error === null || error === undefined || depth > MAX_CAUSE_DEPTH) return false;\n\n if (typeof error === 'string') return signature.message.test(error);\n if (typeof error !== 'object') return false;\n\n const err = error as {\n code?: unknown;\n errno?: unknown;\n message?: unknown;\n cause?: unknown;\n };\n\n if (typeof err.code === 'string' && signature.codes.has(err.code)) return true;\n if (typeof err.errno === 'number' && signature.errnos.has(err.errno)) return true;\n if (typeof err.message === 'string' && signature.message.test(err.message)) return true;\n\n // Drivers commonly re-throw with the original attached as `cause`.\n return matchesDriverError(err.cause, signature, depth + 1);\n}\n\n/**\n * Is this DDL error the benign \"already provisioned\" case?\n *\n * @param error - The value thrown by `syncSchema()` (or any DDL call).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/column/index-already-exists. Anything else — including an\n * unrecognised error, `undefined`, or a permission/connection failure —\n * returns `false` and MUST be reported loudly by the caller.\n */\nexport function isSchemaAlreadyExistsError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, ALREADY_EXISTS, depth);\n}\n\n/**\n * Is this READ error the benign \"table has not been provisioned yet\" case?\n *\n * The only failure that licenses a caller to treat an empty table as the truth\n * — there are no rows, so there is nothing to be inconsistent with. A\n * connection drop, a timeout, a permission denial or a query error all mean the\n * rows may well exist and simply were not seen; those return `false` and the\n * caller must report the consequence and give up rather than compute an answer\n * from data it never read (#4825).\n *\n * @param error - The value thrown by a driver/engine read (`find`, `findOne`, …).\n * @param depth - Internal `cause`-chain recursion counter; callers pass nothing.\n * @returns `true` only when the error positively identifies as\n * table/relation-does-not-exist.\n */\nexport function isMissingTableError(error: unknown, depth = 0): boolean {\n return matchesDriverError(error, MISSING_TABLE, depth);\n}\n"],"mappings":";AAuIA,IAAM,gBAAsC;AAAA,EACxC,OAAO,oBAAI,IAAI;AAAA,IACX;AAAA;AAAA,IACA;AAAA;AAAA,EACJ,CAAC;AAAA,EACD,QAAQ,oBAAI,IAAI,CAAC,IAAI,CAAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMtB,SACI;AACR;AAGA,IAAM,kBAAkB;AAUxB,SAAS,mBACL,OACA,WACA,OACO;AACP,MAAI,UAAU,QAAQ,UAAU,UAAa,QAAQ,gBAAiB,QAAO;AAE7E,MAAI,OAAO,UAAU,SAAU,QAAO,UAAU,QAAQ,KAAK,KAAK;AAClE,MAAI,OAAO,UAAU,SAAU,QAAO;AAEtC,QAAM,MAAM;AAOZ,MAAI,OAAO,IAAI,SAAS,YAAY,UAAU,MAAM,IAAI,IAAI,IAAI,EAAG,QAAO;AAC1E,MAAI,OAAO,IAAI,UAAU,YAAY,UAAU,OAAO,IAAI,IAAI,KAAK,EAAG,QAAO;AAC7E,MAAI,OAAO,IAAI,YAAY,YAAY,UAAU,QAAQ,KAAK,IAAI,OAAO,EAAG,QAAO;AAGnF,SAAO,mBAAmB,IAAI,OAAO,WAAW,QAAQ,CAAC;AAC7D;AA+BO,SAAS,oBAAoB,OAAgB,QAAQ,GAAY;AACpE,SAAO,mBAAmB,OAAO,eAAe,KAAK;AACzD;","names":[]}
|