@happyvertical/smrt-core 0.40.69 → 0.40.70
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/AGENTS.md +29 -4
- package/README.md +20 -1
- package/agents/query-bounds.md +45 -0
- package/agents/schema-paths.md +328 -0
- package/dist/browser.d.ts +1 -0
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -1
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +28 -4
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +101 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +224 -43
- package/dist/collection.js.map +1 -1
- package/dist/database.d.ts +8 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +16 -8
- package/dist/database.js.map +1 -1
- package/dist/db-errors.d.ts +105 -0
- package/dist/db-errors.d.ts.map +1 -0
- package/dist/db-errors.js +382 -0
- package/dist/db-errors.js.map +1 -0
- package/dist/decorators/index.d.ts +17 -1
- package/dist/decorators/index.d.ts.map +1 -1
- package/dist/decorators/index.js +65 -7
- package/dist/decorators/index.js.map +1 -1
- package/dist/dispatch/bus.d.ts.map +1 -1
- package/dist/dispatch/bus.js +4 -3
- package/dist/dispatch/bus.js.map +1 -1
- package/dist/errors.d.ts +30 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +34 -2
- package/dist/errors.js.map +1 -1
- package/dist/generators/changes-route.d.ts.map +1 -1
- package/dist/generators/changes-route.js +6 -3
- package/dist/generators/changes-route.js.map +1 -1
- package/dist/generators/mcp-runtime-template.d.ts +8 -0
- package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
- package/dist/generators/mcp-runtime-template.js +38 -4
- package/dist/generators/mcp-runtime-template.js.map +1 -1
- package/dist/generators/mcp.d.ts +16 -0
- package/dist/generators/mcp.d.ts.map +1 -1
- package/dist/generators/mcp.js +41 -3
- package/dist/generators/mcp.js.map +1 -1
- package/dist/generators/rest.d.ts +22 -0
- package/dist/generators/rest.d.ts.map +1 -1
- package/dist/generators/rest.js +34 -3
- package/dist/generators/rest.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -3
- package/dist/manifest/static-manifest.js +1 -1
- package/dist/manifest/static-manifest.js.map +1 -1
- package/dist/manifest/store.js +1 -1
- package/dist/manifest.json +1 -1
- package/dist/migrations/differ.d.ts +202 -8
- package/dist/migrations/differ.d.ts.map +1 -1
- package/dist/migrations/differ.js +604 -46
- package/dist/migrations/differ.js.map +1 -1
- package/dist/migrations/generator.d.ts +31 -4
- package/dist/migrations/generator.d.ts.map +1 -1
- package/dist/migrations/generator.js +49 -5
- package/dist/migrations/generator.js.map +1 -1
- package/dist/migrations/index.d.ts +3 -2
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +5 -3
- package/dist/migrations/orchestrate.js +35 -6
- package/dist/migrations/orchestrate.js.map +1 -1
- package/dist/migrations/sqlite-rebuild.d.ts +142 -0
- package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
- package/dist/migrations/sqlite-rebuild.js +514 -0
- package/dist/migrations/sqlite-rebuild.js.map +1 -0
- package/dist/migrations/tracker.d.ts +114 -1
- package/dist/migrations/tracker.d.ts.map +1 -1
- package/dist/migrations/tracker.js +331 -16
- package/dist/migrations/tracker.js.map +1 -1
- package/dist/migrations/types.d.ts +19 -4
- package/dist/migrations/types.d.ts.map +1 -1
- package/dist/migrations.js +5 -3
- package/dist/object.d.ts +45 -6
- package/dist/object.d.ts.map +1 -1
- package/dist/object.js +60 -13
- package/dist/object.js.map +1 -1
- package/dist/postgres-timeouts.d.ts +240 -0
- package/dist/postgres-timeouts.d.ts.map +1 -0
- package/dist/postgres-timeouts.js +204 -0
- package/dist/postgres-timeouts.js.map +1 -0
- package/dist/query-bounds.d.ts +97 -0
- package/dist/query-bounds.d.ts.map +1 -0
- package/dist/query-bounds.js +173 -0
- package/dist/query-bounds.js.map +1 -0
- package/dist/registry/types.d.ts +32 -1
- package/dist/registry/types.d.ts.map +1 -1
- package/dist/registry.js +1 -1
- package/dist/scanner/types.d.ts +5 -0
- package/dist/scanner/types.d.ts.map +1 -1
- package/dist/scanner/types.js.map +1 -1
- package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
- package/dist/schema/ddl/base-strategy.js +2 -2
- package/dist/schema/ddl/base-strategy.js.map +1 -1
- package/dist/schema/generator.d.ts +70 -5
- package/dist/schema/generator.d.ts.map +1 -1
- package/dist/schema/generator.js +124 -6
- package/dist/schema/generator.js.map +1 -1
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/live-parity.d.ts +90 -0
- package/dist/schema/live-parity.d.ts.map +1 -0
- package/dist/schema/live-parity.js +600 -0
- package/dist/schema/live-parity.js.map +1 -0
- package/dist/schema/manifest-schema.d.ts +121 -0
- package/dist/schema/manifest-schema.d.ts.map +1 -0
- package/dist/schema/manifest-schema.js +267 -0
- package/dist/schema/manifest-schema.js.map +1 -0
- package/dist/schema/schema-aggregator.d.ts +24 -10
- package/dist/schema/schema-aggregator.d.ts.map +1 -1
- package/dist/schema/schema-aggregator.js +35 -90
- package/dist/schema/schema-aggregator.js.map +1 -1
- package/dist/schema/system-table-shapes.d.ts +52 -0
- package/dist/schema/system-table-shapes.d.ts.map +1 -0
- package/dist/schema/system-table-shapes.js +174 -0
- package/dist/schema/system-table-shapes.js.map +1 -0
- package/dist/schema/types.d.ts +103 -4
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/utils.d.ts +1 -0
- package/dist/schema/utils.d.ts.map +1 -1
- package/dist/schema/utils.js +3 -1
- package/dist/schema/utils.js.map +1 -1
- package/dist/schema.js +4 -1
- package/dist/smrt-knowledge.json +18 -6
- package/dist/sync/apply.d.ts.map +1 -1
- package/dist/sync/apply.js +9 -16
- package/dist/sync/apply.js.map +1 -1
- package/dist/testing/database.d.ts.map +1 -1
- package/dist/testing/database.js +1 -0
- package/dist/testing/database.js.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
- package/dist/vite-plugin/sveltekit-generator.js +71 -5
- package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
- package/dist/vite-plugin/web-collections.d.ts.map +1 -1
- package/dist/vite-plugin/web-collections.js +6 -4
- package/dist/vite-plugin/web-collections.js.map +1 -1
- package/package.json +5 -5
package/dist/database.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DatabaseInterface, SchemasOption } from '@happyvertical/sql';
|
|
2
|
+
import { PostgresTimeoutConfig } from './postgres-timeouts.js';
|
|
2
3
|
/**
|
|
3
4
|
* Unified type for all database configuration formats
|
|
4
5
|
*
|
|
@@ -28,6 +29,13 @@ export type DatabaseConfig = string | {
|
|
|
28
29
|
url?: string;
|
|
29
30
|
type?: 'sqlite' | 'postgres' | 'duckdb' | 'json';
|
|
30
31
|
authToken?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Runtime PostgreSQL timeouts for this connection (#2377). Ignored by
|
|
34
|
+
* every other engine.
|
|
35
|
+
*
|
|
36
|
+
* @see {@link PostgresTimeoutConfig}
|
|
37
|
+
*/
|
|
38
|
+
timeouts?: PostgresTimeoutConfig;
|
|
31
39
|
[key: string]: unknown;
|
|
32
40
|
} | DatabaseInterface;
|
|
33
41
|
/**
|
package/dist/database.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAE3E,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,wBAAwB,CAAC;AAEhC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,MAAM,cAAc,GACtB,MAAM,GACN;IACE,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,qBAAqB,CAAC;IACjC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB,GACD,iBAAiB,CAAC;AAEtB;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,8BAA8B,EAC9B,CAAC,MAAM,cAAc,GAAG,SAAS,CAAC,GAClC,SAAS,CAAC;CACf;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,OAAO,GACb,KAAK,IAAI,iBAAiB,CAO5B;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;OAKG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,cAAc,EACtB,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAAC,iBAAiB,CAAC,CAqC5B"}
|
package/dist/database.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyPostgresRuntimeTimeouts } from "./postgres-timeouts.js";
|
|
1
2
|
import { getDatabase } from "@happyvertical/sql";
|
|
2
3
|
//#region src/database.ts
|
|
3
4
|
/**
|
|
@@ -49,19 +50,26 @@ function isDatabaseInterface(value) {
|
|
|
49
50
|
async function resolveDatabase(config, options = {}) {
|
|
50
51
|
const { dbid, schemas } = options;
|
|
51
52
|
if (isDatabaseInterface(config)) return config;
|
|
52
|
-
if (typeof config === "string")
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
if (typeof config === "string") {
|
|
54
|
+
const isMemoryDb = config === ":memory:";
|
|
55
|
+
const bounded = applyPostgresRuntimeTimeouts({ url: config });
|
|
56
|
+
return getDatabase({
|
|
57
|
+
...bounded,
|
|
58
|
+
schemas,
|
|
59
|
+
...isMemoryDb ? {} : { dbid: dbid ?? `smrt:${bounded.url}` }
|
|
60
|
+
});
|
|
61
|
+
}
|
|
57
62
|
const canUseMemory = !config.type || config.type === "sqlite";
|
|
58
63
|
const dbUrl = config.url || (canUseMemory ? ":memory:" : "");
|
|
59
64
|
const isMemoryDb = dbUrl === ":memory:";
|
|
60
|
-
|
|
65
|
+
const bounded = applyPostgresRuntimeTimeouts({
|
|
61
66
|
...config,
|
|
62
|
-
url: dbUrl
|
|
67
|
+
url: dbUrl
|
|
68
|
+
});
|
|
69
|
+
return getDatabase({
|
|
70
|
+
...bounded,
|
|
63
71
|
schemas,
|
|
64
|
-
...isMemoryDb ? {} : { dbid: dbid ?? `smrt:${
|
|
72
|
+
...isMemoryDb ? {} : { dbid: dbid ?? `smrt:${bounded.url}` }
|
|
65
73
|
});
|
|
66
74
|
}
|
|
67
75
|
//#endregion
|
package/dist/database.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","names":[],"sources":["../src/database.ts"],"sourcesContent":["/**\n * Database configuration and resolution utilities\n *\n * This module provides a unified type for database configuration options\n * and a utility function to resolve any config format to a DatabaseInterface.\n *\n * @module\n */\n\nimport type { DatabaseInterface, SchemasOption } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\n\n/**\n * Unified type for all database configuration formats\n *\n * Supports three formats:\n * - **String**: Connection URL (e.g., 'products.db', ':memory:', 'postgres://...')\n * - **Config object**: Full configuration with type, url, and options\n * - **DatabaseInterface**: Pre-initialized database instance\n *\n * @example\n * ```typescript\n * // String shortcut\n * const config: DatabaseConfig = 'products.db';\n *\n * // Config object\n * const config: DatabaseConfig = {\n * type: 'sqlite',\n * url: 'products.db',\n * authToken: 'token'\n * };\n *\n * // Pre-initialized instance\n * const db = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * const config: DatabaseConfig = db;\n * ```\n */\nexport type DatabaseConfig =\n | string\n | {\n url?: string;\n type?: 'sqlite' | 'postgres' | 'duckdb' | 'json';\n authToken?: string;\n [key: string]: unknown;\n }\n | DatabaseInterface;\n\n/**\n * Canonical declaration of the request-scoped database global.\n *\n * The value is installed by whichever package owns request scoping (today\n * `@happyvertical/smrt-users`, via its session permission context) and is read\n * by the SvelteKit runtime config that `smrt-core`'s vite plugin generates into\n * consumer apps. Both sides used to `declare global` this name independently\n * with different types — `DatabaseConfig` here, a package-private\n * `QueryableDatabase` there — and TypeScript requires merged `var` declarations\n * to be *identical*, not merely compatible. Consumers whose program contained\n * both then failed to type-check (#2342).\n *\n * `smrt-core` owns the declaration because it is the base package every other\n * SMRT package and consumer already depends on. Writers should keep assigning a\n * live `DatabaseInterface`; it satisfies `DatabaseConfig` through that arm of\n * the union. Never redeclare this global elsewhere.\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtGetRequestScopedDatabase:\n | (() => DatabaseConfig | undefined)\n | undefined;\n}\n\n/**\n * Type guard to check if a value is a DatabaseInterface instance\n *\n * @param value - Value to check\n * @returns True if value has query() and close() methods\n *\n * @example\n * ```typescript\n * if (isDatabaseInterface(config)) {\n * // config is DatabaseInterface\n * await config.query('SELECT 1');\n * }\n * ```\n */\nexport function isDatabaseInterface(\n value: unknown,\n): value is DatabaseInterface {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'query' in value &&\n typeof (value as { query: unknown }).query === 'function'\n );\n}\n\n/**\n * Options for resolving a database configuration\n */\nexport interface ResolveDatabaseOptions {\n /**\n * Cache key for connection pooling.\n * If provided, the same dbid returns the same cached connection.\n */\n dbid?: string;\n\n /**\n * Optional pre-generated schemas to pass to the database adapter.\n *\n * Intended for explicit tooling and test utilities that bootstrap schema\n * ahead of runtime. Core runtime no longer passes these automatically.\n */\n schemas?: SchemasOption;\n}\n\n/**\n * Resolve any DatabaseConfig format to a DatabaseInterface instance\n *\n * This utility function normalizes the three config formats:\n * 1. **String URL**: Passed to getDatabase() with auto-detected type\n * 2. **Config object**: Passed to getDatabase() directly\n * 3. **DatabaseInterface**: Returned as-is\n *\n * @param config - Database configuration in any supported format\n * @param options - Resolution options (dbid for caching, optional explicit schemas)\n * @returns Promise resolving to a DatabaseInterface instance\n *\n * @example\n * ```typescript\n * // String URL\n * const db = await resolveDatabase('products.db');\n *\n * // Config object\n * const db = await resolveDatabase({\n * type: 'postgres',\n * url: 'postgres://localhost/mydb'\n * });\n *\n * // Pre-initialized instance (returned as-is)\n * const existingDb = await getDatabase({ url: ':memory:' });\n * const db = await resolveDatabase(existingDb);\n * console.log(db === existingDb); // true\n * ```\n */\nexport async function resolveDatabase(\n config: DatabaseConfig,\n options: ResolveDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const { dbid, schemas } = options;\n\n // Already a DatabaseInterface instance - return as-is\n if (isDatabaseInterface(config)) {\n return config;\n }\n\n // String URL shortcut\n if (typeof config === 'string') {\n const isMemoryDb = config === ':memory:';\n return getDatabase({\n
|
|
1
|
+
{"version":3,"file":"database.js","names":[],"sources":["../src/database.ts"],"sourcesContent":["/**\n * Database configuration and resolution utilities\n *\n * This module provides a unified type for database configuration options\n * and a utility function to resolve any config format to a DatabaseInterface.\n *\n * @module\n */\n\nimport type { DatabaseInterface, SchemasOption } from '@happyvertical/sql';\nimport { getDatabase } from '@happyvertical/sql';\nimport {\n applyPostgresRuntimeTimeouts,\n type PostgresTimeoutConfig,\n} from './postgres-timeouts.js';\n\n/**\n * Unified type for all database configuration formats\n *\n * Supports three formats:\n * - **String**: Connection URL (e.g., 'products.db', ':memory:', 'postgres://...')\n * - **Config object**: Full configuration with type, url, and options\n * - **DatabaseInterface**: Pre-initialized database instance\n *\n * @example\n * ```typescript\n * // String shortcut\n * const config: DatabaseConfig = 'products.db';\n *\n * // Config object\n * const config: DatabaseConfig = {\n * type: 'sqlite',\n * url: 'products.db',\n * authToken: 'token'\n * };\n *\n * // Pre-initialized instance\n * const db = await getDatabase({ type: 'sqlite', url: ':memory:' });\n * const config: DatabaseConfig = db;\n * ```\n */\nexport type DatabaseConfig =\n | string\n | {\n url?: string;\n type?: 'sqlite' | 'postgres' | 'duckdb' | 'json';\n authToken?: string;\n /**\n * Runtime PostgreSQL timeouts for this connection (#2377). Ignored by\n * every other engine.\n *\n * @see {@link PostgresTimeoutConfig}\n */\n timeouts?: PostgresTimeoutConfig;\n [key: string]: unknown;\n }\n | DatabaseInterface;\n\n/**\n * Canonical declaration of the request-scoped database global.\n *\n * The value is installed by whichever package owns request scoping (today\n * `@happyvertical/smrt-users`, via its session permission context) and is read\n * by the SvelteKit runtime config that `smrt-core`'s vite plugin generates into\n * consumer apps. Both sides used to `declare global` this name independently\n * with different types — `DatabaseConfig` here, a package-private\n * `QueryableDatabase` there — and TypeScript requires merged `var` declarations\n * to be *identical*, not merely compatible. Consumers whose program contained\n * both then failed to type-check (#2342).\n *\n * `smrt-core` owns the declaration because it is the base package every other\n * SMRT package and consumer already depends on. Writers should keep assigning a\n * live `DatabaseInterface`; it satisfies `DatabaseConfig` through that arm of\n * the union. Never redeclare this global elsewhere.\n */\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtGetRequestScopedDatabase:\n | (() => DatabaseConfig | undefined)\n | undefined;\n}\n\n/**\n * Type guard to check if a value is a DatabaseInterface instance\n *\n * @param value - Value to check\n * @returns True if value has query() and close() methods\n *\n * @example\n * ```typescript\n * if (isDatabaseInterface(config)) {\n * // config is DatabaseInterface\n * await config.query('SELECT 1');\n * }\n * ```\n */\nexport function isDatabaseInterface(\n value: unknown,\n): value is DatabaseInterface {\n return (\n value !== null &&\n typeof value === 'object' &&\n 'query' in value &&\n typeof (value as { query: unknown }).query === 'function'\n );\n}\n\n/**\n * Options for resolving a database configuration\n */\nexport interface ResolveDatabaseOptions {\n /**\n * Cache key for connection pooling.\n * If provided, the same dbid returns the same cached connection.\n */\n dbid?: string;\n\n /**\n * Optional pre-generated schemas to pass to the database adapter.\n *\n * Intended for explicit tooling and test utilities that bootstrap schema\n * ahead of runtime. Core runtime no longer passes these automatically.\n */\n schemas?: SchemasOption;\n}\n\n/**\n * Resolve any DatabaseConfig format to a DatabaseInterface instance\n *\n * This utility function normalizes the three config formats:\n * 1. **String URL**: Passed to getDatabase() with auto-detected type\n * 2. **Config object**: Passed to getDatabase() directly\n * 3. **DatabaseInterface**: Returned as-is\n *\n * @param config - Database configuration in any supported format\n * @param options - Resolution options (dbid for caching, optional explicit schemas)\n * @returns Promise resolving to a DatabaseInterface instance\n *\n * @example\n * ```typescript\n * // String URL\n * const db = await resolveDatabase('products.db');\n *\n * // Config object\n * const db = await resolveDatabase({\n * type: 'postgres',\n * url: 'postgres://localhost/mydb'\n * });\n *\n * // Pre-initialized instance (returned as-is)\n * const existingDb = await getDatabase({ url: ':memory:' });\n * const db = await resolveDatabase(existingDb);\n * console.log(db === existingDb); // true\n * ```\n */\nexport async function resolveDatabase(\n config: DatabaseConfig,\n options: ResolveDatabaseOptions = {},\n): Promise<DatabaseInterface> {\n const { dbid, schemas } = options;\n\n // Already a DatabaseInterface instance - return as-is\n if (isDatabaseInterface(config)) {\n return config;\n }\n\n // String URL shortcut\n if (typeof config === 'string') {\n const isMemoryDb = config === ':memory:';\n // The dbid is derived from the *bounded* URL so two configurations that\n // differ only in their PostgreSQL timeouts cannot share one pool (#2377).\n const bounded = applyPostgresRuntimeTimeouts({ url: config });\n return getDatabase({\n ...bounded,\n schemas,\n ...(isMemoryDb ? {} : { dbid: dbid ?? `smrt:${bounded.url}` }),\n } as Parameters<typeof getDatabase>[0]);\n }\n\n // Config object\n // Only default to :memory: for SQLite (or unspecified type which defaults to SQLite)\n // Other adapters (json, postgres, duckdb) require explicit URLs\n const canUseMemory = !config.type || config.type === 'sqlite';\n const dbUrl = config.url || (canUseMemory ? ':memory:' : '');\n const isMemoryDb = dbUrl === ':memory:';\n const bounded = applyPostgresRuntimeTimeouts({ ...config, url: dbUrl });\n // `config` is the loosely-typed config-object variant of `DatabaseConfig`\n // (it carries an open `[key: string]: unknown` index for adapter-specific\n // options). Cast the merged options to `getDatabase`'s own parameter type at\n // this boundary; the adapter validates the concrete shape at runtime.\n return getDatabase({\n ...bounded,\n schemas,\n ...(isMemoryDb ? {} : { dbid: dbid ?? `smrt:${bounded.url}` }),\n } as Parameters<typeof getDatabase>[0]);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAgGA,SAAgB,oBACd,OAC4B;CAC5B,OACE,UAAU,QACV,OAAO,UAAU,YACjB,WAAW,SACX,OAAQ,MAA6B,UAAU;AAEnD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,eAAsB,gBACpB,QACA,UAAkC,CAAC,GACP;CAC5B,MAAM,EAAE,MAAM,YAAY;CAG1B,IAAI,oBAAoB,MAAM,GAC5B,OAAO;CAIT,IAAI,OAAO,WAAW,UAAU;EAC9B,MAAM,aAAa,WAAW;EAG9B,MAAM,UAAU,6BAA6B,EAAE,KAAK,OAAO,CAAC;EAC5D,OAAO,YAAY;GACjB,GAAG;GACH;GACA,GAAI,aAAa,CAAC,IAAI,EAAE,MAAM,QAAQ,QAAQ,QAAQ,MAAM;EAC9D,CAAsC;CACxC;CAKA,MAAM,eAAe,CAAC,OAAO,QAAQ,OAAO,SAAS;CACrD,MAAM,QAAQ,OAAO,QAAQ,eAAe,aAAa;CACzD,MAAM,aAAa,UAAU;CAC7B,MAAM,UAAU,6BAA6B;EAAE,GAAG;EAAQ,KAAK;CAAM,CAAC;CAKtE,OAAO,YAAY;EACjB,GAAG;EACH;EACA,GAAI,aAAa,CAAC,IAAI,EAAE,MAAM,QAAQ,QAAQ,QAAQ,MAAM;CAC9D,CAAsC;AACxC"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver-error classification for the persistence path (#2366).
|
|
3
|
+
*
|
|
4
|
+
* `@happyvertical/sql` does not normalize driver errors: every adapter catches
|
|
5
|
+
* the driver failure and rethrows
|
|
6
|
+
* `DatabaseError('Failed to upsert record into table', { …, originalError })`,
|
|
7
|
+
* where `originalError` is the **string** produced by the SDK's `formatDbError`
|
|
8
|
+
* (`"<driver message>, code=<driver code>, detail=…, hint=…"`). The typed
|
|
9
|
+
* message therefore carries none of the constraint wording, which is why
|
|
10
|
+
* matching on `error.message` alone silently stopped recognizing constraint
|
|
11
|
+
* violations and let {@link ErrorUtils.withRetry} retry them.
|
|
12
|
+
*
|
|
13
|
+
* This module walks the whole causal chain — `cause`, `context.originalError`
|
|
14
|
+
* (Error *or* formatted string), `originalError`, `details.causeMessages`, and
|
|
15
|
+
* `AggregateError.errors` — collecting every driver code and message it finds,
|
|
16
|
+
* then classifies on the codes first (PostgreSQL SQLSTATE, SQLite result
|
|
17
|
+
* codes, Node network errno) and only falls back to dialect wording for DuckDB,
|
|
18
|
+
* which reports no machine-readable code.
|
|
19
|
+
*
|
|
20
|
+
* The classification separates two independent questions:
|
|
21
|
+
* - `deterministic` — a retry cannot change the outcome (constraint violation,
|
|
22
|
+
* bad input syntax, aborted transaction, missing table). Fail fast.
|
|
23
|
+
* - `retryable` — a retry plausibly can succeed (serialization failure,
|
|
24
|
+
* deadlock, lock timeout, connection loss, `SQLITE_BUSY`).
|
|
25
|
+
*
|
|
26
|
+
* They are not complements: `kind: 'unknown'` means "no opinion", leaving the
|
|
27
|
+
* caller's own default policy in charge.
|
|
28
|
+
*
|
|
29
|
+
* @module
|
|
30
|
+
*/
|
|
31
|
+
/** Coarse classification of a database driver failure. */
|
|
32
|
+
export type DatabaseErrorKind = 'unique_violation' | 'not_null_violation' | 'foreign_key_violation' | 'check_violation' | 'exclusion_violation' | 'invalid_input' | 'aborted_transaction' | 'undefined_object' | 'insufficient_privilege' | 'syntax_error' | 'transient' | 'unknown';
|
|
33
|
+
/** Structured result of {@link classifyDatabaseError}. */
|
|
34
|
+
export interface DatabaseErrorClassification {
|
|
35
|
+
/** What kind of failure this is, or `'unknown'` when nothing matched. */
|
|
36
|
+
kind: DatabaseErrorKind;
|
|
37
|
+
/** True when retrying cannot change the outcome. */
|
|
38
|
+
deterministic: boolean;
|
|
39
|
+
/** True when retrying plausibly can succeed. */
|
|
40
|
+
retryable: boolean;
|
|
41
|
+
/** PostgreSQL SQLSTATE recovered from the chain, when present. */
|
|
42
|
+
sqlstate?: string;
|
|
43
|
+
/** Raw driver code that produced the classification, when present. */
|
|
44
|
+
driverCode?: string;
|
|
45
|
+
/** Every driver code found anywhere in the chain, in discovery order. */
|
|
46
|
+
driverCodes: readonly string[];
|
|
47
|
+
/**
|
|
48
|
+
* Every message found anywhere in the chain, in discovery order. Includes
|
|
49
|
+
* the SDK's formatted `originalError` strings and the driver's own
|
|
50
|
+
* `detail`/`hint` fields, which carry the PostgreSQL
|
|
51
|
+
* `Key (col)=(value) already exists.` line used for column recovery.
|
|
52
|
+
*/
|
|
53
|
+
driverMessages: readonly string[];
|
|
54
|
+
/**
|
|
55
|
+
* Column named directly by the driver (`node-postgres` sets `error.column`
|
|
56
|
+
* on a `23502`), when one was reported. More reliable than parsing it back
|
|
57
|
+
* out of the message.
|
|
58
|
+
*/
|
|
59
|
+
column?: string;
|
|
60
|
+
/** Constraint named directly by the driver (`error.constraint`). */
|
|
61
|
+
constraint?: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Classifies a raw dialect message. Exposed so
|
|
65
|
+
* `SmrtObject.classifyConstraintError()` and this module share one set of
|
|
66
|
+
* regexes rather than drifting apart.
|
|
67
|
+
*/
|
|
68
|
+
export declare function classifyDialectMessage(message: string): DatabaseErrorKind;
|
|
69
|
+
/**
|
|
70
|
+
* Classifies a database failure by walking its causal chain.
|
|
71
|
+
*
|
|
72
|
+
* Codes win over wording: a PostgreSQL SQLSTATE or SQLite result code is
|
|
73
|
+
* authoritative, and dialect message matching only runs when no code in the
|
|
74
|
+
* chain was recognized (DuckDB, or an error already flattened to a string).
|
|
75
|
+
*
|
|
76
|
+
* Constraint violations are reported ahead of `aborted_transaction` so a chain
|
|
77
|
+
* carrying both surfaces the root cause rather than the downstream symptom;
|
|
78
|
+
* both are deterministic, so retry behavior is identical either way.
|
|
79
|
+
*
|
|
80
|
+
* @param error - Any thrown value. Non-database errors classify as `'unknown'`.
|
|
81
|
+
* @returns The structured classification; never throws.
|
|
82
|
+
*/
|
|
83
|
+
export declare function classifyDatabaseError(error: unknown): DatabaseErrorClassification;
|
|
84
|
+
/**
|
|
85
|
+
* True when retrying the statement cannot change the outcome.
|
|
86
|
+
*
|
|
87
|
+
* Used by {@link ErrorUtils.withRetry} to fail fast instead of burning the
|
|
88
|
+
* full 500 + 1000 + 2000 ms backoff on a constraint violation.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isDeterministicDatabaseError(error: unknown): boolean;
|
|
91
|
+
/** True when the failure is contention or availability and may clear. */
|
|
92
|
+
export declare function isTransientDatabaseError(error: unknown): boolean;
|
|
93
|
+
/** True when a unique or primary-key constraint was violated. */
|
|
94
|
+
export declare function isUniqueViolationError(error: unknown): boolean;
|
|
95
|
+
/** True when a NOT NULL constraint was violated. */
|
|
96
|
+
export declare function isNotNullViolationError(error: unknown): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* True when the failure came from running inside an already-aborted
|
|
99
|
+
* PostgreSQL transaction (SQLSTATE `25P02`).
|
|
100
|
+
*
|
|
101
|
+
* Detected independently of {@link DatabaseErrorKind} so a chain that also
|
|
102
|
+
* carries the root constraint code still answers `true` here.
|
|
103
|
+
*/
|
|
104
|
+
export declare function isAbortedTransactionError(error: unknown): boolean;
|
|
105
|
+
//# sourceMappingURL=db-errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"db-errors.d.ts","sourceRoot":"","sources":["../src/db-errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,0DAA0D;AAC1D,MAAM,MAAM,iBAAiB,GACzB,kBAAkB,GAClB,oBAAoB,GACpB,uBAAuB,GACvB,iBAAiB,GACjB,qBAAqB,GACrB,eAAe,GACf,qBAAqB,GACrB,kBAAkB,GAClB,wBAAwB,GACxB,cAAc,GACd,WAAW,GACX,SAAS,CAAC;AAEd,0DAA0D;AAC1D,MAAM,WAAW,2BAA2B;IAC1C,yEAAyE;IACzE,IAAI,EAAE,iBAAiB,CAAC;IACxB,oDAAoD;IACpD,aAAa,EAAE,OAAO,CAAC;IACvB,gDAAgD;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,kEAAkE;IAClE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,yEAAyE;IACzE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;OAKG;IACH,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAgWD;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,MAAM,GAAG,iBAAiB,CAMzE;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,GACb,2BAA2B,CA6G7B;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEpE;AAED,yEAAyE;AACzE,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAEhE;AAED,iEAAiE;AACjE,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE9D;AAED,oDAAoD;AACpD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAE/D;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAWjE"}
|
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
//#region src/db-errors.ts
|
|
2
|
+
/** Maximum chain depth walked, guarding against pathological nesting. */
|
|
3
|
+
var MAX_CHAIN_DEPTH = 12;
|
|
4
|
+
/** `formatDbError` renders driver codes as `code=<value>` inside its string. */
|
|
5
|
+
var FORMATTED_CODE_PATTERN = /(?:^|[\s,;([{])code=([A-Za-z0-9_]+)/g;
|
|
6
|
+
/**
|
|
7
|
+
* PostgreSQL SQLSTATE codes whose failure is fully determined by the statement
|
|
8
|
+
* and its data. Retrying re-executes the same deterministic rejection.
|
|
9
|
+
*/
|
|
10
|
+
var DETERMINISTIC_SQLSTATES = /* @__PURE__ */ new Map([
|
|
11
|
+
["23000", "check_violation"],
|
|
12
|
+
["23001", "foreign_key_violation"],
|
|
13
|
+
["23502", "not_null_violation"],
|
|
14
|
+
["23503", "foreign_key_violation"],
|
|
15
|
+
["23505", "unique_violation"],
|
|
16
|
+
["23514", "check_violation"],
|
|
17
|
+
["23P01", "exclusion_violation"],
|
|
18
|
+
["22001", "invalid_input"],
|
|
19
|
+
["22003", "invalid_input"],
|
|
20
|
+
["22007", "invalid_input"],
|
|
21
|
+
["22008", "invalid_input"],
|
|
22
|
+
["22023", "invalid_input"],
|
|
23
|
+
["22P02", "invalid_input"],
|
|
24
|
+
["22P03", "invalid_input"],
|
|
25
|
+
["25P02", "aborted_transaction"],
|
|
26
|
+
["42501", "insufficient_privilege"],
|
|
27
|
+
["42601", "syntax_error"],
|
|
28
|
+
["42703", "undefined_object"],
|
|
29
|
+
["42804", "invalid_input"],
|
|
30
|
+
["42883", "undefined_object"],
|
|
31
|
+
["42P01", "undefined_object"],
|
|
32
|
+
["42P07", "undefined_object"],
|
|
33
|
+
["42P10", "undefined_object"],
|
|
34
|
+
["3D000", "undefined_object"],
|
|
35
|
+
["3F000", "undefined_object"]
|
|
36
|
+
]);
|
|
37
|
+
/**
|
|
38
|
+
* PostgreSQL SQLSTATE codes describing a contention or availability failure.
|
|
39
|
+
* The same statement may well succeed on a later attempt.
|
|
40
|
+
*/
|
|
41
|
+
var TRANSIENT_SQLSTATES = /* @__PURE__ */ new Set([
|
|
42
|
+
"25P03",
|
|
43
|
+
"40001",
|
|
44
|
+
"40003",
|
|
45
|
+
"40P01",
|
|
46
|
+
"53200",
|
|
47
|
+
"53300",
|
|
48
|
+
"55P03",
|
|
49
|
+
"57014",
|
|
50
|
+
"57P01",
|
|
51
|
+
"57P02",
|
|
52
|
+
"57P03",
|
|
53
|
+
"58030",
|
|
54
|
+
"08000",
|
|
55
|
+
"08001",
|
|
56
|
+
"08003",
|
|
57
|
+
"08004",
|
|
58
|
+
"08006",
|
|
59
|
+
"08007",
|
|
60
|
+
"08P01"
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* SQLite extended result codes. `better-sqlite3`/`node:sqlite` frequently
|
|
64
|
+
* report the bare `SQLITE_CONSTRAINT` parent code, so the constraint sub-kind
|
|
65
|
+
* is resolved from the message instead (see {@link classifyDialectMessage}).
|
|
66
|
+
*/
|
|
67
|
+
var DETERMINISTIC_SQLITE_CODES = /* @__PURE__ */ new Map([
|
|
68
|
+
["SQLITE_CONSTRAINT_UNIQUE", "unique_violation"],
|
|
69
|
+
["SQLITE_CONSTRAINT_PRIMARYKEY", "unique_violation"],
|
|
70
|
+
["SQLITE_CONSTRAINT_ROWID", "unique_violation"],
|
|
71
|
+
["SQLITE_CONSTRAINT_NOTNULL", "not_null_violation"],
|
|
72
|
+
["SQLITE_CONSTRAINT_FOREIGNKEY", "foreign_key_violation"],
|
|
73
|
+
["SQLITE_CONSTRAINT_CHECK", "check_violation"],
|
|
74
|
+
["SQLITE_CONSTRAINT_TRIGGER", "check_violation"],
|
|
75
|
+
["SQLITE_MISMATCH", "invalid_input"],
|
|
76
|
+
["SQLITE_RANGE", "invalid_input"],
|
|
77
|
+
["SQLITE_TOOBIG", "invalid_input"],
|
|
78
|
+
["SQLITE_READONLY", "insufficient_privilege"],
|
|
79
|
+
["SQLITE_AUTH", "insufficient_privilege"]
|
|
80
|
+
]);
|
|
81
|
+
/** SQLite result codes that clear once contention or a lock resolves. */
|
|
82
|
+
var TRANSIENT_SQLITE_CODES = /* @__PURE__ */ new Set([
|
|
83
|
+
"SQLITE_BUSY",
|
|
84
|
+
"SQLITE_BUSY_SNAPSHOT",
|
|
85
|
+
"SQLITE_BUSY_TIMEOUT",
|
|
86
|
+
"SQLITE_LOCKED",
|
|
87
|
+
"SQLITE_LOCKED_SHAREDCACHE",
|
|
88
|
+
"SQLITE_PROTOCOL"
|
|
89
|
+
]);
|
|
90
|
+
/** Node/libuv socket errno values raised when a pooled connection drops. */
|
|
91
|
+
var TRANSIENT_NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
92
|
+
"ECONNRESET",
|
|
93
|
+
"ECONNREFUSED",
|
|
94
|
+
"ECONNABORTED",
|
|
95
|
+
"EPIPE",
|
|
96
|
+
"ETIMEDOUT",
|
|
97
|
+
"ENETUNREACH",
|
|
98
|
+
"ENETRESET",
|
|
99
|
+
"EHOSTUNREACH",
|
|
100
|
+
"EAI_AGAIN",
|
|
101
|
+
"ENOTFOUND"
|
|
102
|
+
]);
|
|
103
|
+
/**
|
|
104
|
+
* Dialect wording, in precedence order. Only DuckDB has no machine-readable
|
|
105
|
+
* code, but SQLite's bare `SQLITE_CONSTRAINT` and PostgreSQL errors that have
|
|
106
|
+
* lost their code through string formatting are recovered here too.
|
|
107
|
+
*
|
|
108
|
+
* NOT NULL is tested before UNIQUE because DuckDB phrases both with the same
|
|
109
|
+
* `Constraint Error … violates` shape, and foreign-key/check wording must not
|
|
110
|
+
* be mistaken for a unique violation (#1578).
|
|
111
|
+
*/
|
|
112
|
+
var DIALECT_MESSAGE_RULES = [
|
|
113
|
+
{
|
|
114
|
+
kind: "aborted_transaction",
|
|
115
|
+
pattern: /current transaction is aborted/i
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
kind: "not_null_violation",
|
|
119
|
+
pattern: /NOT NULL constraint failed|null value in column .* violates not-null/i
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
kind: "foreign_key_violation",
|
|
123
|
+
pattern: /violates foreign key constraint|FOREIGN KEY constraint failed/i
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
kind: "check_violation",
|
|
127
|
+
pattern: /violates check constraint|CHECK constraint failed/i
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
kind: "unique_violation",
|
|
131
|
+
pattern: /UNIQUE constraint failed|violates unique constraint|violates primary key constraint|duplicate key value violates/i
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
kind: "invalid_input",
|
|
135
|
+
pattern: /invalid input syntax for type|value too long for type/i
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
kind: "undefined_object",
|
|
139
|
+
pattern: /no such table|no such column|(?:relation|column|table|function) "[^"]*" does not exist/i
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
kind: "transient",
|
|
143
|
+
pattern: /database is locked|database table is locked|connection terminated|server closed the connection|too many clients|deadlock detected|could not serialize access/i
|
|
144
|
+
}
|
|
145
|
+
];
|
|
146
|
+
/** SMRT-level codes that already express a driver classification. */
|
|
147
|
+
var SMRT_CODE_KINDS = /* @__PURE__ */ new Map([["VALIDATION_UNIQUE_CONSTRAINT", "unique_violation"], ["DB_CONSTRAINT_VIOLATION", "check_violation"]]);
|
|
148
|
+
/**
|
|
149
|
+
* Reporting precedence among deterministic kinds, most specific first.
|
|
150
|
+
*
|
|
151
|
+
* `aborted_transaction` ranks last on purpose: PostgreSQL reports `25P02` for
|
|
152
|
+
* every statement issued after the transaction already failed, so a chain
|
|
153
|
+
* carrying both it and the constraint that caused the abort must name the
|
|
154
|
+
* constraint. Every kind here is non-retryable, so ranking changes only what
|
|
155
|
+
* is reported, never whether the operation is retried.
|
|
156
|
+
*/
|
|
157
|
+
var DETERMINISTIC_KIND_RANK = /* @__PURE__ */ new Map([
|
|
158
|
+
["unique_violation", 0],
|
|
159
|
+
["not_null_violation", 1],
|
|
160
|
+
["foreign_key_violation", 2],
|
|
161
|
+
["check_violation", 3],
|
|
162
|
+
["exclusion_violation", 4],
|
|
163
|
+
["invalid_input", 5],
|
|
164
|
+
["undefined_object", 6],
|
|
165
|
+
["insufficient_privilege", 7],
|
|
166
|
+
["syntax_error", 8],
|
|
167
|
+
["aborted_transaction", 9]
|
|
168
|
+
]);
|
|
169
|
+
function rankKind(kind) {
|
|
170
|
+
return DETERMINISTIC_KIND_RANK.get(kind) ?? Number.MAX_SAFE_INTEGER;
|
|
171
|
+
}
|
|
172
|
+
function pushUnique(collection, value) {
|
|
173
|
+
if (typeof value !== "string") return;
|
|
174
|
+
const entry = value.trim();
|
|
175
|
+
if (entry && !collection.includes(entry)) collection.push(entry);
|
|
176
|
+
}
|
|
177
|
+
function pushCode(signals, value) {
|
|
178
|
+
pushUnique(signals.codes, value);
|
|
179
|
+
}
|
|
180
|
+
function pushMessage(signals, value) {
|
|
181
|
+
if (typeof value !== "string") return;
|
|
182
|
+
const message = value.trim();
|
|
183
|
+
if (!message || signals.messages.includes(message)) return;
|
|
184
|
+
signals.messages.push(message);
|
|
185
|
+
FORMATTED_CODE_PATTERN.lastIndex = 0;
|
|
186
|
+
let match = FORMATTED_CODE_PATTERN.exec(message);
|
|
187
|
+
while (match) {
|
|
188
|
+
pushCode(signals, match[1]);
|
|
189
|
+
match = FORMATTED_CODE_PATTERN.exec(message);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Walks an error's causal chain, collecting every driver code and message.
|
|
194
|
+
*
|
|
195
|
+
* Visits `cause`, `context.originalError`, `originalError`,
|
|
196
|
+
* `details.causeMessage`/`details.causeMessages` and `AggregateError.errors`,
|
|
197
|
+
* because the SDK, core and consumer wrappers each nest the driver error
|
|
198
|
+
* differently. Cycles and depth are bounded.
|
|
199
|
+
*/
|
|
200
|
+
function collectChainSignals(error) {
|
|
201
|
+
const signals = {
|
|
202
|
+
codes: [],
|
|
203
|
+
messages: [],
|
|
204
|
+
columns: [],
|
|
205
|
+
constraints: []
|
|
206
|
+
};
|
|
207
|
+
const seen = /* @__PURE__ */ new Set();
|
|
208
|
+
const pending = [{
|
|
209
|
+
value: error,
|
|
210
|
+
depth: 0
|
|
211
|
+
}];
|
|
212
|
+
while (pending.length > 0) {
|
|
213
|
+
const entry = pending.pop();
|
|
214
|
+
if (!entry) break;
|
|
215
|
+
const { value, depth } = entry;
|
|
216
|
+
if (value === null || value === void 0 || depth > MAX_CHAIN_DEPTH) continue;
|
|
217
|
+
if (typeof value === "string") {
|
|
218
|
+
pushMessage(signals, value);
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (typeof value !== "object" || seen.has(value)) continue;
|
|
222
|
+
seen.add(value);
|
|
223
|
+
const node = value;
|
|
224
|
+
pushCode(signals, node.code);
|
|
225
|
+
pushCode(signals, node.sqlState);
|
|
226
|
+
pushCode(signals, node.sqlstate);
|
|
227
|
+
pushMessage(signals, node.message);
|
|
228
|
+
pushMessage(signals, node.detail);
|
|
229
|
+
pushMessage(signals, node.hint);
|
|
230
|
+
pushUnique(signals.columns, node.column);
|
|
231
|
+
pushUnique(signals.constraints, node.constraint);
|
|
232
|
+
const details = node.details;
|
|
233
|
+
if (details && typeof details === "object") {
|
|
234
|
+
pushMessage(signals, details.causeMessage);
|
|
235
|
+
if (Array.isArray(details.causeMessages)) for (const causeMessage of details.causeMessages) pushMessage(signals, causeMessage);
|
|
236
|
+
}
|
|
237
|
+
const context = node.context;
|
|
238
|
+
const next = [
|
|
239
|
+
node.cause,
|
|
240
|
+
node.originalError,
|
|
241
|
+
context && typeof context === "object" ? context.originalError : void 0
|
|
242
|
+
];
|
|
243
|
+
for (const child of next) if (child !== null && child !== void 0) pending.push({
|
|
244
|
+
value: child,
|
|
245
|
+
depth: depth + 1
|
|
246
|
+
});
|
|
247
|
+
if (Array.isArray(node.errors)) for (const child of node.errors) pending.push({
|
|
248
|
+
value: child,
|
|
249
|
+
depth: depth + 1
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
return signals;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Classifies a raw dialect message. Exposed so
|
|
256
|
+
* `SmrtObject.classifyConstraintError()` and this module share one set of
|
|
257
|
+
* regexes rather than drifting apart.
|
|
258
|
+
*/
|
|
259
|
+
function classifyDialectMessage(message) {
|
|
260
|
+
if (!message) return "unknown";
|
|
261
|
+
for (const rule of DIALECT_MESSAGE_RULES) if (rule.pattern.test(message)) return rule.kind;
|
|
262
|
+
return "unknown";
|
|
263
|
+
}
|
|
264
|
+
function isSqlState(code) {
|
|
265
|
+
return /^[0-9A-Z]{5}$/.test(code);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Classifies a database failure by walking its causal chain.
|
|
269
|
+
*
|
|
270
|
+
* Codes win over wording: a PostgreSQL SQLSTATE or SQLite result code is
|
|
271
|
+
* authoritative, and dialect message matching only runs when no code in the
|
|
272
|
+
* chain was recognized (DuckDB, or an error already flattened to a string).
|
|
273
|
+
*
|
|
274
|
+
* Constraint violations are reported ahead of `aborted_transaction` so a chain
|
|
275
|
+
* carrying both surfaces the root cause rather than the downstream symptom;
|
|
276
|
+
* both are deterministic, so retry behavior is identical either way.
|
|
277
|
+
*
|
|
278
|
+
* @param error - Any thrown value. Non-database errors classify as `'unknown'`.
|
|
279
|
+
* @returns The structured classification; never throws.
|
|
280
|
+
*/
|
|
281
|
+
function classifyDatabaseError(error) {
|
|
282
|
+
const signals = collectChainSignals(error);
|
|
283
|
+
const sqlstate = signals.codes.find((code) => isSqlState(code) && (DETERMINISTIC_SQLSTATES.has(code) || TRANSIENT_SQLSTATES.has(code)));
|
|
284
|
+
const base = {
|
|
285
|
+
driverCodes: signals.codes,
|
|
286
|
+
driverMessages: signals.messages,
|
|
287
|
+
...sqlstate ? { sqlstate } : {},
|
|
288
|
+
...signals.columns[0] ? { column: signals.columns[0] } : {},
|
|
289
|
+
...signals.constraints[0] ? { constraint: signals.constraints[0] } : {}
|
|
290
|
+
};
|
|
291
|
+
const deterministicCandidates = [];
|
|
292
|
+
for (const code of signals.codes) {
|
|
293
|
+
const kind = DETERMINISTIC_SQLSTATES.get(code) ?? DETERMINISTIC_SQLITE_CODES.get(code) ?? SMRT_CODE_KINDS.get(code) ?? (code === "SQLITE_CONSTRAINT" ? signals.messages.map(classifyDialectMessage).find((candidate) => DETERMINISTIC_KIND_RANK.has(candidate)) ?? "check_violation" : void 0);
|
|
294
|
+
if (kind) deterministicCandidates.push({
|
|
295
|
+
code,
|
|
296
|
+
kind
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
if (deterministicCandidates.length > 0) {
|
|
300
|
+
const best = deterministicCandidates.reduce((winner, candidate) => rankKind(candidate.kind) < rankKind(winner.kind) ? candidate : winner);
|
|
301
|
+
return {
|
|
302
|
+
...base,
|
|
303
|
+
kind: best.kind,
|
|
304
|
+
deterministic: true,
|
|
305
|
+
retryable: false,
|
|
306
|
+
driverCode: best.code
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
for (const code of signals.codes) {
|
|
310
|
+
if (TRANSIENT_SQLSTATES.has(code) || TRANSIENT_SQLITE_CODES.has(code)) return {
|
|
311
|
+
...base,
|
|
312
|
+
kind: "transient",
|
|
313
|
+
deterministic: false,
|
|
314
|
+
retryable: true,
|
|
315
|
+
driverCode: code
|
|
316
|
+
};
|
|
317
|
+
if (TRANSIENT_NETWORK_CODES.has(code)) return {
|
|
318
|
+
...base,
|
|
319
|
+
kind: "transient",
|
|
320
|
+
deterministic: false,
|
|
321
|
+
retryable: true,
|
|
322
|
+
driverCode: code
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
const messageKinds = signals.messages.map(classifyDialectMessage).filter((kind) => kind !== "unknown");
|
|
326
|
+
const deterministicFromMessage = messageKinds.filter((kind) => DETERMINISTIC_KIND_RANK.has(kind)).sort((left, right) => rankKind(left) - rankKind(right))[0];
|
|
327
|
+
if (deterministicFromMessage) return {
|
|
328
|
+
...base,
|
|
329
|
+
kind: deterministicFromMessage,
|
|
330
|
+
deterministic: true,
|
|
331
|
+
retryable: false
|
|
332
|
+
};
|
|
333
|
+
if (messageKinds.includes("transient")) return {
|
|
334
|
+
...base,
|
|
335
|
+
kind: "transient",
|
|
336
|
+
deterministic: false,
|
|
337
|
+
retryable: true
|
|
338
|
+
};
|
|
339
|
+
return {
|
|
340
|
+
...base,
|
|
341
|
+
kind: "unknown",
|
|
342
|
+
deterministic: false,
|
|
343
|
+
retryable: false
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* True when retrying the statement cannot change the outcome.
|
|
348
|
+
*
|
|
349
|
+
* Used by {@link ErrorUtils.withRetry} to fail fast instead of burning the
|
|
350
|
+
* full 500 + 1000 + 2000 ms backoff on a constraint violation.
|
|
351
|
+
*/
|
|
352
|
+
function isDeterministicDatabaseError(error) {
|
|
353
|
+
return classifyDatabaseError(error).deterministic;
|
|
354
|
+
}
|
|
355
|
+
/** True when the failure is contention or availability and may clear. */
|
|
356
|
+
function isTransientDatabaseError(error) {
|
|
357
|
+
return classifyDatabaseError(error).retryable;
|
|
358
|
+
}
|
|
359
|
+
/** True when a unique or primary-key constraint was violated. */
|
|
360
|
+
function isUniqueViolationError(error) {
|
|
361
|
+
return classifyDatabaseError(error).kind === "unique_violation";
|
|
362
|
+
}
|
|
363
|
+
/** True when a NOT NULL constraint was violated. */
|
|
364
|
+
function isNotNullViolationError(error) {
|
|
365
|
+
return classifyDatabaseError(error).kind === "not_null_violation";
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* True when the failure came from running inside an already-aborted
|
|
369
|
+
* PostgreSQL transaction (SQLSTATE `25P02`).
|
|
370
|
+
*
|
|
371
|
+
* Detected independently of {@link DatabaseErrorKind} so a chain that also
|
|
372
|
+
* carries the root constraint code still answers `true` here.
|
|
373
|
+
*/
|
|
374
|
+
function isAbortedTransactionError(error) {
|
|
375
|
+
const signals = collectChainSignals(error);
|
|
376
|
+
if (signals.codes.includes("25P02")) return true;
|
|
377
|
+
return signals.messages.some((message) => /current transaction is aborted/i.test(message));
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
export { classifyDatabaseError, classifyDialectMessage, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isTransientDatabaseError, isUniqueViolationError };
|
|
381
|
+
|
|
382
|
+
//# sourceMappingURL=db-errors.js.map
|