@happyvertical/smrt-core 0.40.68 → 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.
Files changed (144) hide show
  1. package/AGENTS.md +29 -4
  2. package/README.md +20 -1
  3. package/agents/query-bounds.md +45 -0
  4. package/agents/schema-paths.md +328 -0
  5. package/dist/browser.d.ts +1 -0
  6. package/dist/browser.d.ts.map +1 -1
  7. package/dist/browser.js +2 -1
  8. package/dist/class.d.ts.map +1 -1
  9. package/dist/class.js +28 -4
  10. package/dist/class.js.map +1 -1
  11. package/dist/collection.d.ts +101 -2
  12. package/dist/collection.d.ts.map +1 -1
  13. package/dist/collection.js +224 -43
  14. package/dist/collection.js.map +1 -1
  15. package/dist/database.d.ts +28 -0
  16. package/dist/database.d.ts.map +1 -1
  17. package/dist/database.js +16 -8
  18. package/dist/database.js.map +1 -1
  19. package/dist/db-errors.d.ts +105 -0
  20. package/dist/db-errors.d.ts.map +1 -0
  21. package/dist/db-errors.js +382 -0
  22. package/dist/db-errors.js.map +1 -0
  23. package/dist/decorators/index.d.ts +17 -1
  24. package/dist/decorators/index.d.ts.map +1 -1
  25. package/dist/decorators/index.js +65 -7
  26. package/dist/decorators/index.js.map +1 -1
  27. package/dist/dispatch/bus.d.ts.map +1 -1
  28. package/dist/dispatch/bus.js +4 -3
  29. package/dist/dispatch/bus.js.map +1 -1
  30. package/dist/errors.d.ts +30 -2
  31. package/dist/errors.d.ts.map +1 -1
  32. package/dist/errors.js +34 -2
  33. package/dist/errors.js.map +1 -1
  34. package/dist/generators/changes-route.d.ts.map +1 -1
  35. package/dist/generators/changes-route.js +6 -3
  36. package/dist/generators/changes-route.js.map +1 -1
  37. package/dist/generators/mcp-runtime-template.d.ts +8 -0
  38. package/dist/generators/mcp-runtime-template.d.ts.map +1 -1
  39. package/dist/generators/mcp-runtime-template.js +38 -4
  40. package/dist/generators/mcp-runtime-template.js.map +1 -1
  41. package/dist/generators/mcp.d.ts +16 -0
  42. package/dist/generators/mcp.d.ts.map +1 -1
  43. package/dist/generators/mcp.js +41 -3
  44. package/dist/generators/mcp.js.map +1 -1
  45. package/dist/generators/rest.d.ts +22 -0
  46. package/dist/generators/rest.d.ts.map +1 -1
  47. package/dist/generators/rest.js +34 -3
  48. package/dist/generators/rest.js.map +1 -1
  49. package/dist/index.d.ts +6 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +8 -3
  52. package/dist/manifest/static-manifest.js +1 -1
  53. package/dist/manifest/static-manifest.js.map +1 -1
  54. package/dist/manifest/store.js +1 -1
  55. package/dist/manifest.json +1 -1
  56. package/dist/migrations/differ.d.ts +202 -8
  57. package/dist/migrations/differ.d.ts.map +1 -1
  58. package/dist/migrations/differ.js +604 -46
  59. package/dist/migrations/differ.js.map +1 -1
  60. package/dist/migrations/generator.d.ts +31 -4
  61. package/dist/migrations/generator.d.ts.map +1 -1
  62. package/dist/migrations/generator.js +49 -5
  63. package/dist/migrations/generator.js.map +1 -1
  64. package/dist/migrations/index.d.ts +3 -2
  65. package/dist/migrations/index.d.ts.map +1 -1
  66. package/dist/migrations/index.js +5 -3
  67. package/dist/migrations/orchestrate.js +35 -6
  68. package/dist/migrations/orchestrate.js.map +1 -1
  69. package/dist/migrations/sqlite-rebuild.d.ts +142 -0
  70. package/dist/migrations/sqlite-rebuild.d.ts.map +1 -0
  71. package/dist/migrations/sqlite-rebuild.js +514 -0
  72. package/dist/migrations/sqlite-rebuild.js.map +1 -0
  73. package/dist/migrations/tracker.d.ts +114 -1
  74. package/dist/migrations/tracker.d.ts.map +1 -1
  75. package/dist/migrations/tracker.js +331 -16
  76. package/dist/migrations/tracker.js.map +1 -1
  77. package/dist/migrations/types.d.ts +19 -4
  78. package/dist/migrations/types.d.ts.map +1 -1
  79. package/dist/migrations.js +5 -3
  80. package/dist/object.d.ts +45 -6
  81. package/dist/object.d.ts.map +1 -1
  82. package/dist/object.js +60 -13
  83. package/dist/object.js.map +1 -1
  84. package/dist/postgres-timeouts.d.ts +240 -0
  85. package/dist/postgres-timeouts.d.ts.map +1 -0
  86. package/dist/postgres-timeouts.js +204 -0
  87. package/dist/postgres-timeouts.js.map +1 -0
  88. package/dist/query-bounds.d.ts +97 -0
  89. package/dist/query-bounds.d.ts.map +1 -0
  90. package/dist/query-bounds.js +173 -0
  91. package/dist/query-bounds.js.map +1 -0
  92. package/dist/registry/types.d.ts +32 -1
  93. package/dist/registry/types.d.ts.map +1 -1
  94. package/dist/registry.js +1 -1
  95. package/dist/scanner/types.d.ts +5 -0
  96. package/dist/scanner/types.d.ts.map +1 -1
  97. package/dist/scanner/types.js.map +1 -1
  98. package/dist/schema/ddl/base-strategy.d.ts.map +1 -1
  99. package/dist/schema/ddl/base-strategy.js +2 -2
  100. package/dist/schema/ddl/base-strategy.js.map +1 -1
  101. package/dist/schema/generator.d.ts +70 -5
  102. package/dist/schema/generator.d.ts.map +1 -1
  103. package/dist/schema/generator.js +124 -6
  104. package/dist/schema/generator.js.map +1 -1
  105. package/dist/schema/index.d.ts +3 -0
  106. package/dist/schema/index.d.ts.map +1 -1
  107. package/dist/schema/index.js +4 -1
  108. package/dist/schema/live-parity.d.ts +90 -0
  109. package/dist/schema/live-parity.d.ts.map +1 -0
  110. package/dist/schema/live-parity.js +600 -0
  111. package/dist/schema/live-parity.js.map +1 -0
  112. package/dist/schema/manifest-schema.d.ts +121 -0
  113. package/dist/schema/manifest-schema.d.ts.map +1 -0
  114. package/dist/schema/manifest-schema.js +267 -0
  115. package/dist/schema/manifest-schema.js.map +1 -0
  116. package/dist/schema/schema-aggregator.d.ts +24 -10
  117. package/dist/schema/schema-aggregator.d.ts.map +1 -1
  118. package/dist/schema/schema-aggregator.js +35 -90
  119. package/dist/schema/schema-aggregator.js.map +1 -1
  120. package/dist/schema/system-table-shapes.d.ts +52 -0
  121. package/dist/schema/system-table-shapes.d.ts.map +1 -0
  122. package/dist/schema/system-table-shapes.js +174 -0
  123. package/dist/schema/system-table-shapes.js.map +1 -0
  124. package/dist/schema/types.d.ts +103 -4
  125. package/dist/schema/types.d.ts.map +1 -1
  126. package/dist/schema/utils.d.ts +1 -0
  127. package/dist/schema/utils.d.ts.map +1 -1
  128. package/dist/schema/utils.js +3 -1
  129. package/dist/schema/utils.js.map +1 -1
  130. package/dist/schema.js +4 -1
  131. package/dist/smrt-knowledge.json +18 -6
  132. package/dist/sync/apply.d.ts.map +1 -1
  133. package/dist/sync/apply.js +9 -16
  134. package/dist/sync/apply.js.map +1 -1
  135. package/dist/testing/database.d.ts.map +1 -1
  136. package/dist/testing/database.js +1 -0
  137. package/dist/testing/database.js.map +1 -1
  138. package/dist/vite-plugin/sveltekit-generator.d.ts.map +1 -1
  139. package/dist/vite-plugin/sveltekit-generator.js +97 -13
  140. package/dist/vite-plugin/sveltekit-generator.js.map +1 -1
  141. package/dist/vite-plugin/web-collections.d.ts.map +1 -1
  142. package/dist/vite-plugin/web-collections.js +6 -4
  143. package/dist/vite-plugin/web-collections.js.map +1 -1
  144. package/package.json +11 -11
package/AGENTS.md CHANGED
@@ -17,6 +17,7 @@ subsystem you are editing. This file keeps what holds across all of them.
17
17
  | `src/change-feed.ts` | the adapter-agnostic change-observation spine — `_smrt_changes`, cursors, table versions, generated `_changes` routes, retention | [agents/change-feed.md](agents/change-feed.md) |
18
18
  | `src/change-signals.ts` + the generated `_events` SSE route | the push companion to the change feed — the signal bus, cross-replica fan-out, the SSE route, and its documented gaps | [agents/change-signals.md](agents/change-signals.md) |
19
19
  | `src/generators/` + `src/vite-plugin/web-collections.ts` | REST/CLI/MCP/web-collection generation, the `manifestHash` emission sites, and generated conditional-GET / ETag v2 semantics | [agents/generators.md](agents/generators.md) |
20
+ | `src/schema/` | the five `SchemaGenerator` entry points, which two reach production, why schema drift stayed invisible, and the #2382 index/tenancy rules | [agents/schema-paths.md](agents/schema-paths.md) |
20
21
 
21
22
  ## SmrtObject Lifecycle
22
23
 
@@ -25,6 +26,7 @@ subsystem you are editing. This file keeps what holds across all of them.
25
26
  - `initialize()`: loads field initializers, applies option values (options override initializers), loads from DB if id/slug provided
26
27
  - `save()`: upsert with STI validation, interceptor execution, auto-embeddings. Persisted objects (`isPersisted` — set by DB hydration and successful saves) upsert on `['id']` so natural-key edits (e.g. slug renames) update in place; new objects upsert on the natural-key conflict columns for ingestion-style dedup (#1472)
27
28
  - `is(criteria)` / `do(instructions)` / `describe()`: AI operations via function calling. They inject the object's own `toPublicJSON()` (sensitive fields stripped) as a "content body" so the model reasons over the instance. Options: `includeData: false` skips injection (for callers that already curate the relevant fields into the instruction); `maxDataLength` overrides the truncation budget. Neither key is forwarded to `ai.message()`. (#1567)
29
+ - `save()` error contract (#2366): unique/PK violation → `ValidationError` `VALIDATION_UNIQUE_CONSTRAINT`, NOT NULL → `VALIDATION_REQUIRED_FIELD`, both on the first attempt on every adapter; any other database failure → `DatabaseError` with the driver error on `cause`
28
30
  - `getSlug()`: auto-generates from name → title → label → id
29
31
  - `loadRelated(fieldName)`: lazy-loads relationships (cached in `_loadedRelationships` Map)
30
32
 
@@ -74,7 +76,7 @@ references). Re-adding either requires the query builder to support it first;
74
76
  `src/__tests__/issue-2276-where-contract.test.ts` executes every accepted
75
77
  operator against a database to keep the two in step.
76
78
 
77
- STI child collections auto-filter by `_meta_type`.
79
+ STI child collections auto-filter by `_meta_type`. Query bounds — `LIMIT 1` on `get()`, the `limit`/`offset` parser, the `orderBy` whitelist and sensitive/permission refusals, and the deterministic generated-list ordering (#2367) — are in [agents/query-bounds.md](agents/query-bounds.md).
78
80
 
79
81
  ## Bounded Collection Read Plans
80
82
 
@@ -97,7 +99,7 @@ Two persistence primitives every `SmrtObject`/`SmrtCollection` inherits — load
97
99
 
98
100
  ## @smrt() Decorator Options
99
101
 
100
- Key options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).
102
+ Key options: `tableName`, `tableStrategy` ('cti'|'sti'), `conflictColumns`, `indexes` (declared multi-column indexes, #2357 — see "Schema paths"), `api`/`mcp`/`cli` (generation config), `ai` (callable methods), `hooks` (beforeSave/afterSave/beforeDelete/afterDelete), `embeddings` (auto-generate), `tenantScoped`, `agent`, `ui` (`{ icon, label, description }` — nav/help hints round-tripped through the manifest as plain data; `description` is the object-level seed for form-level help, #2046).
101
103
 
102
104
  Registration sets `SMRT_TABLE_NAME` static property (survives minification).
103
105
 
@@ -203,13 +205,36 @@ byte/provenance/path/content drift, skips scans and manifest writes, and still
203
205
  generates routes, types, registration, and virtual modules. Omit it for normal
204
206
  local development and watch mode.
205
207
 
208
+ ## Schema paths (#2382)
209
+
210
+ Production DDL comes from the **manifest** paths
211
+ (`generateSTISchemaFromManifest`/`generateCTISchemaFromManifest`, selected in
212
+ `src/scanner/manifest-generator.ts` → registered `schema` → `db:migrate`). The
213
+ **registry** paths feed `getTestDatabase()` and emit foreign-key indexes
214
+ production never gets: the suite runs on a richer schema than it ships.
215
+
216
+ - Change column/index emission on every shipping path, proven by a path-parity
217
+ test (#2359 adds one). A "same as migrations" comment is a claim to check.
218
+ - Every new query predicate ships with its index, or a reason it doesn't.
219
+ - Numeric types, uuid casts, conflict targets, timestamps, migrations: run the
220
+ `test:postgres` lane — SQLite affinity accepts what PostgreSQL rejects.
221
+ - Read `dist/manifest.json`/regenerated schemas for what a decorator produced;
222
+ count across all packages instead of sampling.
223
+ - Tenant scoping is whole-path: every unique constraint and conflict target on a
224
+ tenant-scoped table carries the tenant column, and every read path — not only
225
+ `list()` — is interceptor-aware.
226
+ - Rolling indexes out is part of the change: a bulk `CREATE INDEX` batch needs
227
+ the bounded, concurrent migrate path (#2362, Gotchas), or it takes production
228
+ down on deploy.
229
+
206
230
  ## Gotchas
207
231
 
208
232
  - **Filesystem support is a lazy boundary (#1979)**: `SmrtClass` acquires `options.fs` adapters via `createFilesystemAdapter()` (`src/filesystem-loader.ts`), never a static `@happyvertical/files` import — the files SDK statically pulls @aws-sdk/client-s3 and reaches googleapis, and a static edge here would land it in every downstream SSR bundle. Node/tsx/vite-dev runtimes resolve it on first use; fully-bundled deployments import `@happyvertical/smrt-core/filesystem` at startup. Use `importOptionalDependency()` (`src/lazy-external.ts`) for any similar optional heavyweight dependency.
209
233
  - **Never override toJSON()** — handles STI discriminator + meta field extraction. Use `transformJSON()`
210
234
  - **Property init order**: TypeScript initializers run first, then `initialize()` applies option values (options win)
211
- - **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime only verifies and fails clearly
212
- - **Retry logic**: `db.get()` (3 retries, 250ms) and `db.upsert()` (3 retries, 500ms) have built-in retry
235
+ - **No runtime schema creation**: application tables must be prepared explicitly via migrations/tooling; runtime verification is `tableExists()` only (`src/schema/table-verifier.ts`) no column, type, or index check
236
+ - **PostgreSQL migrate batches are always time-bounded (#2362)**: `MigrationTracker.applyAll({ atomic: true })` emits `SET LOCAL lock_timeout`/`statement_timeout` before any DDL, so a batch blocked on one table cannot hold its earlier locks indefinitely. `postgresSafe: true` adds concurrent-index mode — non-index DDL commits atomically, then index DDL runs `CONCURRENTLY` on a session pinned via `db.acquireSession()` (a pooled `db.query` would not keep the `SET` and the DDL on one connection). That mode is deliberately **not atomic**: unfinished index migrations are recorded `failed`, not `running`, and their `error_message` carries a `[smrt: concurrent-index phase 1 committed]` marker so a reconciling re-run resumes at the index build instead of replaying committed DDL. INVALID indexes are found via `pg_index.indisvalid` (`pg_indexes` reports them as present) and dropped before rebuild. Operational detail: `packages/cli/AGENTS.md`.
237
+ - **Retry logic is transient-only (#2366)**: `db.get()`/`db.upsert()` retry 4× total (initial + 3), but `ErrorUtils.withRetry` classifies via the cause chain (`src/db-errors.ts`) and rethrows deterministic failures immediately — constraint violations, bad input syntax, missing tables, aborted PG tx (`25P02`). `@happyvertical/sql` stringifies the driver text into `context.originalError`, so **never match `error.message`**; use `classifyDatabaseError()` / `isUniqueViolationError()` / `isAbortedTransactionError()`.
213
238
  - **Field caching**: `_cachedFields` populated during `Collection.create()` — eliminates async `getFields()` per query
214
239
  - **Smart cloning**: arrays/objects shallow-cloned in property init to prevent aliasing (Issue #22)
215
240
  - **Table verification cache**: `isTableVerified(dbUrl, tableName)` avoids redundant `tableExists()` calls
package/README.md CHANGED
@@ -425,7 +425,26 @@ Every `SmrtObject`/`SmrtCollection` can persist learned knowledge via `remember(
425
425
  | `AIError` | AI provider failures |
426
426
  | `ValidationError` | Field/object validation failures |
427
427
  | `RuntimeError` | General runtime failures |
428
- | `ErrorUtils` | Sanitization and formatting helpers |
428
+ | `ErrorUtils` | Retry policy (`withRetry`, `isRetryable`) plus sanitization helpers |
429
+
430
+ ### Database error classification
431
+
432
+ Driver errors reach the model layer wrapped by `@happyvertical/sql`, which
433
+ stringifies the driver text into `context.originalError` — so the constraint
434
+ wording never appears on `error.message`. Classify through the cause chain
435
+ instead of matching messages.
436
+
437
+ | Export | Description |
438
+ |--------|------------|
439
+ | `classifyDatabaseError` | Walks the cause chain and returns the kind plus `deterministic` / `retryable` |
440
+ | `classifyDialectMessage` | Matches a single raw dialect message (the DuckDB fallback) |
441
+ | `isUniqueViolationError` | Unique or primary-key violation anywhere in the chain |
442
+ | `isNotNullViolationError` | NOT NULL violation anywhere in the chain |
443
+ | `isAbortedTransactionError` | Statement issued inside an aborted PostgreSQL transaction (`25P02`) |
444
+ | `isDeterministicDatabaseError` | A retry cannot change the outcome |
445
+ | `isTransientDatabaseError` | Contention or availability; a retry may succeed |
446
+ | `DatabaseErrorKind` | Union of classification kinds |
447
+ | `DatabaseErrorClassification` | Structured result of `classifyDatabaseError` |
429
448
 
430
449
  ### Tools (AI Function Calling)
431
450
 
@@ -0,0 +1,45 @@
1
+ <!-- Module doc for packages/core/AGENTS.md. Linked from the Modules table there. -->
2
+
3
+ # Query bounds (#2367)
4
+
5
+ `src/query-bounds.ts` is the single parser every generated read surface uses for
6
+ `limit`/`offset`. A bound is a non-negative integer or it is a client error:
7
+ malformed input raises a 400-typed `QueryBoundsError` (a `ValidationError`, so
8
+ `withRetry` never retries it and `normalizeTypedHttpError` renders it as a
9
+ structured 400) instead of reaching the driver as `LIMIT NaN`. Oversized pages
10
+ are **clamped** to `MAX_LIST_LIMIT` (1000) rather than rejected, and an explicit
11
+ `0` means zero rows — it is not folded into `DEFAULT_LIST_LIMIT` (50).
12
+
13
+ - `collection.get()` / `findOne()` / `findById()` emit `LIMIT 1`.
14
+ - `collection.list()` validates `limit`/`offset` but applies **no implicit
15
+ default**: it is the framework's bulk-read primitive and relationship,
16
+ junction, hierarchy and `listByIds()` callers all expect every matching row, so
17
+ a framework-wide default would truncate correct queries. Applications opt in
18
+ per collection with `defaultListLimit` / `maxListLimit` (validated at
19
+ construction). The generated surfaces enforce the ceiling on their own
20
+ untrusted input regardless.
21
+ - `orderBy` runs the same rail as `where` (#1540) and `select` (#1902): terms are
22
+ checked against the field whitelist and refused for `@field({ sensitive: true })`
23
+ and `@field({ readPermission })` columns — ordering is a comparison, and
24
+ `?orderBy=api_secret&limit=1` is an oracle over a column the request may neither
25
+ filter on nor project — and for fields that are registered but **not
26
+ column-backed** — `oneToMany`/`manyToMany`/`meta`/`transient`, plus the
27
+ `id`/`slug`/`context` system columns on a custom-primary-key class, which the
28
+ schema generator omits — all of which otherwise reach the driver as
29
+ `no such column` and surface as a 500. The whitelist is skipped only for
30
+ manifest-less inline test classes (#869), exactly as `where` skips it. `where`
31
+ still whitelists those omitted system columns; that is a pre-existing gap of
32
+ the same family, not closed here because `where: { id }` is on internal
33
+ hydration paths.
34
+ - Every generated list surface (REST, SvelteKit, MCP, and the emitted stdio MCP
35
+ runtime) pages with `ORDER BY created_at DESC, <pk> ASC` unless the caller
36
+ supplies `orderBy` — `LIMIT`/`OFFSET` with no ordering is not pagination, and
37
+ `created_at` alone still ties. `<pk>` follows a declared
38
+ `@field({ primaryKey: true })` (read from `_meta.primaryKey` in manifests)
39
+ because custom-primary-key classes have no synthetic `id` column. The stdio
40
+ runtime gets the per-object ordering baked in via `RuntimeOptions.listOrderBy`;
41
+ it cannot resolve a primary key on its own. Index: #2363.
42
+ - `listByIds()` chunks its `IN` list at `IN_LIST_CHUNK_SIZE` (900), like the
43
+ relationship/junction/hierarchy loaders.
44
+
45
+ Keyset pagination is deliberately out of scope.
@@ -0,0 +1,328 @@
1
+ # smrt-core/schema paths
2
+
3
+ Module semantics for `src/schema/` — which `SchemaGenerator` entry point reaches
4
+ a real database, what each one emits, and the rules that keep them in step.
5
+ Package orientation, the cross-module invariants, and the traps that apply
6
+ before editing anything live in [../AGENTS.md](../AGENTS.md) — read that first;
7
+ its "Schema paths" section is the short form of everything below.
8
+
9
+ Written from the 2026-08-17 database-layer gap assessment (epic #2382). Symbol
10
+ names here are stable; the line numbers the assessment quotes are not, so trust
11
+ this call graph and re-grep before citing a location.
12
+
13
+ ## Five entry points, two of which ship
14
+
15
+ `src/schema/generator.ts` exposes five index-emitting entry points. They do not
16
+ produce the same schema for the same class.
17
+
18
+ | Entry point | Selected by | Status |
19
+ |---|---|---|
20
+ | `generateSTISchemaFromManifest` | `src/scanner/manifest-generator.ts` | **production** |
21
+ | `generateCTISchemaFromManifest` | `src/scanner/manifest-generator.ts` | **production** |
22
+ | `generateSTISchemaFromRegistry` | `src/testing/database.ts` (`getTestDatabase()`), `src/schema/utils.ts` (`generateSchema`; `ensureSchema` only as a fallback) | tests + runtime helpers |
23
+ | `generateSchemaFromRegistry` | the same two callers | tests + runtime helpers |
24
+ | `generateSchema` (AST) | the `smrt:schema` virtual module, which has no consumer | dead (#2380) |
25
+
26
+ Production DDL takes the manifest route:
27
+
28
+ ```
29
+ @smrt() class ─▶ scanner ─▶ manifest.json ─▶ generate{STI,CTI}SchemaFromManifest
30
+ ─▶ registered `schema` ─▶ ObjectRegistry.getAllSchemasAsDefinitions()
31
+ ├─▶ smrt db:migrate | db:diff | db:status
32
+ │ (the CLI drives SchemaComparer + MigrationTracker directly)
33
+ └─▶ migrateSmrtSchemas() / getPendingSchemaStatements()
34
+ (src/migrations/orchestrate.ts — exported for programmatic
35
+ use; no in-repo caller outside its own tests)
36
+ ```
37
+
38
+ The suite takes the registry route, and the registry route emits indexes the
39
+ manifest route does not — per-column foreign-key indexes, and STI partial FK
40
+ indexes filtered by `_meta_type`. Tests therefore run against a richer schema
41
+ than any deployment receives. `src/testing/database.ts`'s "Generate schema using
42
+ SchemaGenerator (same as migrations)" comment describes an intent, not the code.
43
+
44
+ The manifest STI path even populates a `fkColumnsByClass` map and never reads it
45
+ — only its registry counterpart iterates one — and the manifest CTI path has no
46
+ FK loop at all. Both manifest paths then skip the explicit
47
+ `@foreignKey(X, { indexed: true })` opt-in — the CTI one under a comment claiming
48
+ "FK columns and unique columns get their own indexes", which holds on the
49
+ registry paths and not on this one.
50
+
51
+ `src/schema/utils.ts` sits in between, and the two exports differ:
52
+
53
+ - `generateSchema()` (reached from `SmrtCollection.generateSchema()`) always
54
+ rebuilds from the registry and writes the result back into the registry,
55
+ replacing whatever the manifest registered for that class.
56
+ - `ensureSchema()` (reached from the deprecated `smrt db:setup`) is
57
+ manifest-first: it takes `ObjectRegistry.getSchema()` plus the merged
58
+ `getAllSchemasAsDefinitions()` table definition, and only falls back to
59
+ `generateSchema()` when no schema is registered at all.
60
+
61
+ So a normal build keeps the manifest schema through `db:setup`, and a
62
+ registry-derived schema is a dev/test artifact. `smrt-content` shows what one
63
+ looks like: `packages/content/src/hooks.server.ts` `bootstrapSchema()` calls
64
+ `generateSchema()` for every registered class and then `ensureSchema()` from the
65
+ SvelteKit `handle` hook on any `/api/*` request, so that process holds
66
+ registry-derived schemas rather than the manifest ones. It reaches only that
67
+ package's own `vite dev` app — the library build excludes the file and the
68
+ package never exports it — but it is the shape to recognize. Check which route a
69
+ process actually took before trusting a reproduction.
70
+
71
+ ## Why the drift stayed invisible
72
+
73
+ Every drift oracle compares a database with the same artifact that dropped the
74
+ index:
75
+
76
+ - `verifyPersistenceTable()` (`src/schema/table-verifier.ts`) calls
77
+ `db.tableExists()` and nothing else. "Runtime verifies schema" has always meant
78
+ existence-only — no column, type, constraint, or index comparison.
79
+ - `smrt doctor` never opens a database connection.
80
+ - `db:status` and `db:diff` diff the live database against
81
+ `getAllSchemasAsDefinitions()`, i.e. the manifest projection.
82
+
83
+ An index the manifest never emitted is "in sync" by construction. That is how a
84
+ production database reached 164 unindexed `tenant_id` columns while `db:status`
85
+ reported no drift (#2356 → #2359). The assessment's other counts — 196/231
86
+ `@foreignKey` and 91/92 `@crossPackageRef` columns with no production index,
87
+ 238/238 tables carrying a redundant index on the primary key, zero DB-level
88
+ foreign-key constraints on any engine — come from regenerating every package's
89
+ schema against a live database, so re-measure rather than quote them once the
90
+ epic's fixes land.
91
+
92
+ ## Rules
93
+
94
+ ### 1. Verify against the production path, not the test path
95
+
96
+ Any change to column or index emission goes on **all** paths that ship and is
97
+ proven by a path-parity test. #2359 adds that test under `src/schema/`; until it
98
+ lands, assert the parity yourself in the nearest generator test — a green suite
99
+ otherwise proves the registry paths only. Read the call graph before believing a
100
+ comment: "same as migrations" was wrong for years.
101
+
102
+ ### 2. Every new query predicate ships with its index
103
+
104
+ Collection methods, poll loops, auth lookups, junction right-side filters, and
105
+ polymorphic owner lookups all count — or write down why the predicate does not
106
+ need one. For list workloads, EXPLAIN on a PostgreSQL snapshot; the measured
107
+ spread on the assessed workload was 21 ms → 0.1 ms.
108
+
109
+ ### 3. Run the PostgreSQL lane
110
+
111
+ Anything touching numeric types, uuid casts, upsert conflict targets, timestamps,
112
+ or migrations runs the package's `test:postgres` script:
113
+
114
+ ```bash
115
+ pnpm --filter @happyvertical/smrt-<pkg> test:postgres
116
+ ```
117
+
118
+ core, cli, users, sales, marketing, analytics, and vitest carry the lane.
119
+ SQLite's type affinity accepts values PostgreSQL rejects — a money field declared
120
+ `number = 0` compiles to INTEGER and only fails on PG (#2361).
121
+
122
+ ### 4. Read the built artifact, not the source
123
+
124
+ What a decorator produced is in `dist/manifest.json` and in regenerated schemas:
125
+ `integer` vs `decimal`, the actual index list, the actual conflict columns. When
126
+ the question is "how many tables/columns/indexes", regenerate and count across
127
+ every package; do not sample a few and extrapolate.
128
+
129
+ ### 5. Index intent belongs on both the constraint and the read path
130
+
131
+ A conflict target is not automatically a unique index, and a unique index is not
132
+ automatically the index a read path uses. Custom `conflictColumns` replace the
133
+ `(slug, context)` index while `loadFromSlug`/`getId` still query slug+context;
134
+ STI drops `@field({ unique: true })`. Check the pair, not the declaration.
135
+
136
+ ### 6. Multi-tenancy is a whole-path property
137
+
138
+ Every unique constraint and every conflict target on a tenant-scoped table
139
+ includes the tenant column — otherwise a second tenant's `save()` of the same
140
+ natural key updates the first tenant's row through `DO UPDATE SET` (#2360). And
141
+ every read path is interceptor-aware: hydration (`loadFromId`/`loadFromSlug`),
142
+ get-by-slug, vector search, and collection memory, not only `list()` (#2365).
143
+
144
+ ### 7. Retry only transient errors
145
+
146
+ Classify through the cause chain (SQLSTATE), never on a message substring, and
147
+ never retry inside an aborted PostgreSQL transaction (`25P02`). Test the
148
+ contract end to end against a real database, not only the classifier (#2366).
149
+
150
+ ### 8. Thread new decorator options through every config-rebuild site
151
+
152
+ A new `@smrt()` or `@field()` option that affects schema must reach the
153
+ `SchemaGeneratorConfig` type in `src/schema/generator.ts` and every site that
154
+ rebuilds that config — `src/schema/utils.ts` and `src/testing/database.ts` — or
155
+ it is silently dropped on the paths that rebuild it (#2357).
156
+
157
+ ### 9. Delete or wire dead paths, and write docs to what the code does
158
+
159
+ Dead code that looks canonical misleads the next agent: the AST `generateSchema`
160
+ path, `SchemaOverrideSystem`, and the never-emitted `triggers: []` all read as
161
+ supported surfaces (#2380). Documentation follows the implementation, not the
162
+ intent — say "verifies the table exists" when that is what runs.
163
+
164
+ ### 10. Untracked "known limitation" comments are bugs nobody will read
165
+
166
+ File the issue and link it from the comment. A `products` comment explaining why
167
+ a conflict-column change was refrained from sat there for months — and
168
+ misdescribed the failure mode the whole time.
169
+
170
+ ### 11. Consumer repair scripts are signals
171
+
172
+ Downstream repair tooling (anytown's `db-repair-plan.ts` carried column-type
173
+ repairs, missing STI columns and indexes, and `tenant_id` backfills since April)
174
+ is the consumer-side record of framework gaps. Mine it during triage.
175
+
176
+ ### 12. Try to falsify before filing, and treat operations as correctness
177
+
178
+ Re-verify a finding at source before it becomes an issue — one assessment
179
+ candidate claimed conflict indexes past two columns were narrowed to two
180
+ columns, when only the index *name* is shortened. And an index fix that ships
181
+ without a bounded-timeout, `CONCURRENTLY`-capable migrate path can take
182
+ production down on rollout (#2362).
183
+
184
+ ### 13. Composite indexes are declared, not inferred (#2357)
185
+
186
+ The generated set only covers foreign keys, unique/conflict columns,
187
+ `updated_at`, the STI discriminator, `tenant_id`, and single columns opted in
188
+ with `@field({ indexed: true })`. A list workload's access path is composite,
189
+ so declare it:
190
+
191
+ ```ts
192
+ @smrt({
193
+ indexes: [
194
+ { name: 'contents_tenant_id_publish_date_idx',
195
+ columns: ['tenantId', 'publish_date'] },
196
+ ],
197
+ })
198
+ ```
199
+
200
+ `columns` takes field names or column names in access-path order — filter
201
+ columns first, sort column last. Declare columns, not a direction: PostgreSQL
202
+ scans a btree either way, so an ascending index also serves the matching
203
+ `ORDER BY ... DESC` as an ordered scan with no Sort node. `unique` and `where`
204
+ (partial index) are honoured.
205
+
206
+ `appendDeclaredIndexes()` runs on all five entry points, before
207
+ `ensureTenantIdIndex()`, so a declared composite leading with the tenant column
208
+ replaces the automatic standalone `tenant_id` index rather than duplicating it.
209
+ Unknown columns, malformed entries, and a name collision with a different index
210
+ all fail generation — a silently dropped index only surfaces later as a
211
+ production slowdown. Rule 8 above is why this works at runtime at all.
212
+
213
+ ### 14. Relationship targets resolve to a class name on both paths
214
+
215
+ `@foreignKey`/`@oneToMany`/`@manyToMany` accept a class, a name string, or a
216
+ `() => Target` thunk. The decorator invokes the thunk and throws when the target
217
+ cannot be resolved (never `related: ''`); the scanner unwraps the same thunk
218
+ from raw source (never `related: '() => Target'`). An unresolved target silently
219
+ costs the relationship edge, `loadRelated()`, and the FK-derived index (#2379).
220
+ A thunk resolves at decoration time, so a target declared later in the same
221
+ module is still in its temporal dead zone — use the string form there.
222
+
223
+ ### 15. A SQLite type change is a table rebuild (#2370)
224
+
225
+ SQLite has no `ALTER TABLE ... ALTER COLUMN ... TYPE`, so
226
+ `src/migrations/sqlite-rebuild.ts` answers a `type_upgrade` on SQLite with the
227
+ statement list SQLite's own docs prescribe: stage a new table under
228
+ `_smrt_rebuild_<table>`, copy, drop, rename, replay the indexes and triggers.
229
+ `SchemaComparer.compareTable` swaps that plan in for the differ's
230
+ "requires table recreation" placeholder, so `db:migrate` applies it inside the
231
+ normal atomic batch instead of exiting 1 forever.
232
+
233
+ Four properties of that module are load-bearing; keep them if you touch it:
234
+
235
+ - **The target shape comes from the live `sqlite_master` DDL**, retyping only
236
+ the drifted columns. It is not regenerated from the manifest, so the rebuild
237
+ never becomes an implicit `DROP COLUMN`, and it preserves table constraints,
238
+ `CHECK`s, and `WITHOUT ROWID`/`STRICT`.
239
+ - **The rebuild is hoisted ahead of the table's other column changes.** Its
240
+ staging DDL and copy list are captured at diff time, and the differ emits
241
+ changes in manifest field order, so a new field declared above the retyped
242
+ one would otherwise run `ALTER TABLE ... ADD COLUMN` first and have the
243
+ rebuild silently drop it — both statements succeed and the batch commits.
244
+ Rebuild first, then add columns to the rebuilt table.
245
+ - **The copy carries no `CAST`.** SQLite applies the destination column's
246
+ affinity on insert — the same conversion a fresh table performs. An explicit
247
+ cast is worse: non-numeric TEXT cast to REAL/INTEGER silently becomes `0`,
248
+ and an ISO timestamp cast to NUMERIC-affinity `DATETIME` becomes its year.
249
+ - **It refuses when any table has a foreign key onto the target and
250
+ `PRAGMA foreign_keys` is ON** (the SMRT adapter's default). `DROP TABLE`
251
+ performs an implicit `DELETE FROM` that fires `ON DELETE CASCADE` on
252
+ children, and `defer_foreign_keys` defers constraint *checks*, not FK
253
+ *actions* — verified: the child rows go. The target's own self-reference
254
+ counts, because the staging table copies that clause and becomes a child of
255
+ the table being dropped (verified: a two-row self-referencing table finishes
256
+ the rebuild holding one row). Such a column stays manual drift.
257
+ - **`PRAGMA legacy_alter_table` brackets the rename**, because SQLite ≥ 3.25
258
+ re-parses the schema on `ALTER TABLE ... RENAME` and a view still pointing at
259
+ the just-dropped table makes it fail outright. It is restored immediately
260
+ after; a rolled-back batch leaves it set on that connection, which is inert
261
+ here only because nothing else in SMRT renames a table.
262
+
263
+ All the drifted columns of one table share a single rebuild: the first change
264
+ carries the plan and the rest become `no change needed` comments that the CLI
265
+ classifies as no-ops.
266
+
267
+ ## What the differ compares (#2369)
268
+
269
+ `SchemaComparer` (`src/migrations/differ.ts`) compares each manifest column's
270
+ type, then — unless the type itself is drifting — its nullability and default,
271
+ and always reports what it will not touch:
272
+
273
+ - **Strengthening** (`SET NOT NULL`, `SET DEFAULT`) is executable on
274
+ PostgreSQL/DuckDB. `SET NOT NULL` is preceded by an `UPDATE … WHERE c IS NULL`
275
+ backfill of the manifest default; without a default the live data is probed
276
+ and, if NULLs exist, the change is reported (comment SQL + `advisory`) instead
277
+ of emitting an ALTER that would abort the atomic batch.
278
+ - **Relaxing** (`DROP NOT NULL`, `DROP DEFAULT`) is a report-only advisory until
279
+ the caller passes `relaxColumns` (`db:migrate --relax-columns`). The manifest
280
+ can be under-specified (#2372 registration-order weakness), so a live column
281
+ that is stricter than the manifest is never weakened silently.
282
+ - **Orphans** — DB columns absent from the manifest, DB tables no manifest
283
+ declares (`SchemaDiff.orphan_tables`), and unclaimed `*_key` unique constraint
284
+ indexes — are always reported. A NOT NULL orphan without a default is a
285
+ `warning` advisory (every ORM insert fails on it); `includeDroppedColumns`
286
+ (`--drop-columns`) drops it, `relaxColumns` relaxes it. Advisory-only changes
287
+ carry no SQL, never reach the tracker, and do not fail `db:migrate`.
288
+ - **ADD COLUMN** is planned per engine: DuckDB rejects every inline constraint
289
+ (add with `DEFAULT`, then `SET NOT NULL`, `CREATE UNIQUE INDEX`); SQLite
290
+ rejects inline `UNIQUE` (separate `CREATE UNIQUE INDEX <table>_<col>_key`, the
291
+ PostgreSQL constraint-index name, so the orphan sweep leaves it alone) and
292
+ `NOT NULL` without a default on a populated table; PostgreSQL keeps constraints
293
+ inline. DuckDB has no `ADD CONSTRAINT`, so the separate index is the only
294
+ way to add uniqueness there; the bundled DuckDB 1.4.x resolves
295
+ `ON CONFLICT (col)` through that index (the old #12684 limitation the DuckDB
296
+ strategy's `requiresInlineUnique()` note describes no longer reproduces —
297
+ the #2369 DuckDB test pins the upsert), older DuckDB builds may not. A required column with no default is enforced only on an empty table;
298
+ on a populated one it is added nullable and the `NOT NULL` is reported as a
299
+ manual follow-up on every engine.
300
+ - **SQLite** has no `ALTER COLUMN`: nullability/default alterations are manual
301
+ (comment SQL → `db:migrate` exit 1). The #2370 rebuild (rule 15) consumes
302
+ only `type_upgrade` placeholders today; extending it to rewrite constraints
303
+ would lift this.
304
+ - Defaults compare through `canonicalizeDefault()`, which folds engine
305
+ renderings (`'x'::text`, `CAST('t' AS BOOLEAN)`, `CURRENT_TIMESTAMP` vs
306
+ `now()`) by manifest type; an unclassifiable rendering skips the comparison
307
+ rather than risking a false positive that would churn every run. The
308
+ round-trip test (create from each DDL strategy → compare → zero changes) in
309
+ `src/migrations/__tests__/issue-2369-*.test.ts` guards this.
310
+
311
+ ### 16. `schema.ddl` is a preview, not the table
312
+
313
+ `SchemaDefinition.ddl` / `manifest.json` `schema.ddl` is the engine-neutral
314
+ CREATE TABLE string from `SchemaGenerator.generateSQL()` with no engine: no
315
+ indexes, no triggers, abstract `REAL`/`JSON`/`UUID`/`TIMESTAMP`. It is kept for
316
+ backward compatibility only. Everything that needs an executable table renders
317
+ `columns` + `indexes` through `getDDLStrategy(engine)` — `db:migrate`
318
+ (`migrations/orchestrate.ts`), `MigrationGenerator` (default
319
+ `materializeStructuredSchema: true`; `false` is a deprecated opt-out),
320
+ `SchemaAggregator`, and `createIsolatedTestDbFromManifest` in smrt-vitest, the
321
+ last two via `src/schema/manifest-schema.ts` (`collectManifestTables` /
322
+ `renderCollectedManifestTable`). The cached string is merged in only for a
323
+ table whose contributors expose no structured columns (hand-authored
324
+ manifests); table constraints that exist only in the string are dropped with a
325
+ warning, as `db:migrate` drops them. Do not add a new consumer of the
326
+ string, and do not write a private CREATE INDEX renderer — the retired ones
327
+ dropped `where` and `jsonPath` (#2358). Every DDL strategy also spells out
328
+ `PRIMARY KEY NOT NULL`: SQLite lets a bare non-INTEGER PRIMARY KEY hold NULL.
package/dist/browser.d.ts CHANGED
@@ -18,6 +18,7 @@ export * from './class';
18
18
  export * from './collection';
19
19
  export type { AiUsageConfig, GlobalSignalConfig, MetricsConfig, PubSubConfig, } from './config';
20
20
  export { config } from './config';
21
+ export { classifyDatabaseError, classifyDialectMessage, type DatabaseErrorClassification, type DatabaseErrorKind, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isTransientDatabaseError, isUniqueViolationError, } from './db-errors';
21
22
  export * from './errors';
22
23
  export { type HierarchyView, SmrtHierarchical } from './hierarchical';
23
24
  export { type JunctionAttachOptions, type JunctionFilterOptions, SmrtJunction, } from './junction';
@@ -1 +1 @@
1
- {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,KAAK,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,cAAc,UAAU,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,KAAK,iCAAiC,GACvC,MAAM,2BAA2B,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAElD,cAAc,iBAAiB,CAAC;AAEhC,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,cAAc,eAAe,CAAC"}
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,YAAY,EACV,aAAa,EACb,kBAAkB,EAClB,aAAa,EACb,YAAY,GACb,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAOlC,OAAO,EACL,qBAAqB,EACrB,sBAAsB,EACtB,KAAK,2BAA2B,EAChC,KAAK,iBAAiB,EACtB,yBAAyB,EACzB,4BAA4B,EAC5B,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,cAAc,UAAU,CAAC;AACzB,OAAO,EAAE,KAAK,aAAa,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtE,OAAO,EACL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,YAAY,GACb,MAAM,YAAY,CAAC;AACpB,cAAc,UAAU,CAAC;AACzB,OAAO,EACL,0BAA0B,EAC1B,KAAK,iCAAiC,GACvC,MAAM,2BAA2B,CAAC;AACnC,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AAElD,cAAc,iBAAiB,CAAC;AAEhC,cAAc,gBAAgB,CAAC;AAC/B,YAAY,EACV,iBAAiB,EACjB,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,gBAAgB,EAChB,aAAa,EACb,iBAAiB,GAClB,MAAM,gBAAgB,CAAC;AAExB,cAAc,eAAe,CAAC"}
package/dist/browser.js CHANGED
@@ -8,6 +8,7 @@ import { SignalSanitizer } from "./signals/sanitizer.js";
8
8
  import { SignalBus } from "./signals/bus.js";
9
9
  import { assertPostgresSystemTimestampsCurrent, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, getDatabaseEngine, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, tableExists } from "./system/compatibility.js";
10
10
  import { SmrtClass } from "./class.js";
11
+ import { classifyDatabaseError, classifyDialectMessage, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isTransientDatabaseError, isUniqueViolationError } from "./db-errors.js";
11
12
  import { AIError, ConfigurationError, DatabaseError, ErrorUtils, FilesystemError, NetworkError, RuntimeError, SmrtError, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils } from "./errors.js";
12
13
  import { convertTypeToJsonSchema, generateToolFromMethod, generateToolManifest, shouldIncludeMethod } from "./tools/tool-generator.js";
13
14
  import { executeToolCall, executeToolCalls, formatToolResults, validateToolCall } from "./tools/tool-executor.js";
@@ -21,4 +22,4 @@ import { SmrtPolymorphicAssociation } from "./polymorphic-association.js";
21
22
  import "./signals/index.js";
22
23
  import "./system/index.js";
23
24
  import "./tools/index.js";
24
- export { AIError, AiUsageCollector, AiUsagePersistenceHandler, ConfigurationError, DEFAULT_AI_COST_RATES, DatabaseError, ErrorUtils, FilesystemError, MetricsAdapter, NetworkError, ObjectRegistry, PubSubAdapter, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, assertPostgresSystemTimestampsCurrent, config, convertTypeToJsonSchema, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, estimateAiUsageCost, executeToolCall, executeToolCalls, formatToolResults, generateToolFromMethod, generateToolManifest, getDatabaseEngine, isSmrtCollectionExtendsName, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, shouldIncludeMethod, smrt, smrt as smrtRegistry, tableExists, validateToolCall };
25
+ export { AIError, AiUsageCollector, AiUsagePersistenceHandler, ConfigurationError, DEFAULT_AI_COST_RATES, DatabaseError, ErrorUtils, FilesystemError, MetricsAdapter, NetworkError, ObjectRegistry, PubSubAdapter, RuntimeError, SMRT_COLLECTION_BASE_NAMES, SignalBus, SignalSanitizer, SmrtClass, SmrtCollection, SmrtError, SmrtHierarchical, SmrtJunction, SmrtObject, SmrtPolymorphicAssociation, TenantIsolationError, ValidationError, ValidationReport, ValidationUtils, assertPostgresSystemTimestampsCurrent, classifyDatabaseError, classifyDialectMessage, config, convertTypeToJsonSchema, ensureDispatchSubscriptionsSystemTableCompatibility, ensureDispatchSystemTableCompatibility, ensureJobEventsSystemTableCompatibility, ensureJobsSystemTableCompatibility, ensureLegacySystemTableCompatibility, estimateAiUsageCost, executeToolCall, executeToolCalls, formatToolResults, generateToolFromMethod, generateToolManifest, getDatabaseEngine, isAbortedTransactionError, isDeterministicDatabaseError, isNotNullViolationError, isSmrtCollectionExtendsName, isTransientDatabaseError, isUniqueViolationError, migratePostgresSystemTimestamps, planPostgresSystemTimestampMigrations, shouldIncludeMethod, smrt, smrt as smrtRegistry, tableExists, validateToolCall };
@@ -1 +1 @@
1
- {"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../src/class.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../src/class.ts"],"names":[],"mappings":"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"}
package/dist/class.js CHANGED
@@ -5,6 +5,7 @@ import { SMRT_SCHEMA_VERSION, getSystemTableDDL } from "./system/schema.js";
5
5
  import { ensurePostgresChangeFeedAppendFunction, registerChangeFeedWriter } from "./change-feed.js";
6
6
  import { config } from "./config.js";
7
7
  import { createFilesystemAdapter } from "./filesystem-loader.js";
8
+ import { applyPostgresRuntimeTimeouts } from "./postgres-timeouts.js";
8
9
  import { SignalBus } from "./signals/bus.js";
9
10
  import { assertPostgresSystemTimestampsCurrent, ensureLegacySystemTableCompatibility, tableExists } from "./system/compatibility.js";
10
11
  import { createLogger } from "@happyvertical/logger";
@@ -12,6 +13,25 @@ import { getAI } from "@happyvertical/ai";
12
13
  import { getDatabase } from "@happyvertical/sql";
13
14
  //#region src/class.ts
14
15
  var SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL = "SELECT pg_advisory_xact_lock(hashtext('smrt'), hashtext('system-tables'))";
16
+ /**
17
+ * Timeout budget for the PostgreSQL system-table bootstrap transaction.
18
+ *
19
+ * The runtime pool's session `lock_timeout`/`statement_timeout` (#2377) are
20
+ * sized for request work. This transaction is not request work: it holds the
21
+ * advisory lock across up to 29 sequential DDL round-trips — ~18.85 s on the
22
+ * high-latency link `bootstrapSystemTables` documents — and a second replica
23
+ * cold-starting against the same fresh database *waits* on that lock. Both GUCs
24
+ * bound that wait, because `pg_advisory_xact_lock` is an ordinary statement in
25
+ * the lock manager, so at the runtime defaults the second replica would abort
26
+ * with "canceling statement due to lock timeout" where it previously waited and
27
+ * succeeded.
28
+ *
29
+ * Five minutes is an order of magnitude above the documented worst case and
30
+ * still bounded — this is a raise, not a disable. `SET LOCAL` scopes it to this
31
+ * transaction, the same lever migrations use for the same reason (#2362).
32
+ */
33
+ var SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_MS = 3e5;
34
+ var SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL = [`SET LOCAL lock_timeout = '${SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_MS}ms'`, `SET LOCAL statement_timeout = '${SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_MS}ms'`];
15
35
  var logger = createLogger({ level: "info" });
16
36
  function firstString(...candidates) {
17
37
  return candidates.find((candidate) => {
@@ -247,9 +267,10 @@ var SmrtClass = class SmrtClass {
247
267
  } else {
248
268
  if (typeof this.options.db === "string") {
249
269
  const isMemoryDb = this.options.db === ":memory:";
270
+ const bounded = applyPostgresRuntimeTimeouts({ url: this.options.db });
250
271
  this._db = await getDatabase({
251
- url: this.options.db,
252
- ...isMemoryDb ? {} : { dbid: `smrt:${this.options.db}` }
272
+ ...bounded,
273
+ ...isMemoryDb ? {} : { dbid: `smrt:${bounded.url}` }
253
274
  });
254
275
  } else if ("query" in this.options.db) this._db = this.options.db;
255
276
  else if ("client" in this.options.db && this.options.db.client) {
@@ -262,9 +283,10 @@ var SmrtClass = class SmrtClass {
262
283
  } else {
263
284
  const dbUrl = this.options.db.url || "memory";
264
285
  const isMemoryDb = dbUrl === ":memory:" || dbUrl === "memory";
286
+ const bounded = applyPostgresRuntimeTimeouts({ ...this.options.db });
265
287
  this._db = await getDatabase({
266
- ...this.options.db,
267
- ...isMemoryDb ? {} : { dbid: `smrt:${dbUrl}` }
288
+ ...bounded,
289
+ ...isMemoryDb ? {} : { dbid: `smrt:${bounded.url ?? dbUrl}` }
268
290
  });
269
291
  }
270
292
  /**
@@ -406,6 +428,7 @@ var SmrtClass = class SmrtClass {
406
428
  const tx = await beginTransaction.call(this._db);
407
429
  if (!tx) throw new Error("Database transaction could not be started");
408
430
  try {
431
+ for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);
409
432
  await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);
410
433
  const result = await callback(tx);
411
434
  await tx.commit();
@@ -416,6 +439,7 @@ var SmrtClass = class SmrtClass {
416
439
  }
417
440
  }
418
441
  if (typeof transaction === "function") return transaction.bind(this._db)(async (tx) => {
442
+ for (const sql of SYSTEM_TABLE_BOOTSTRAP_TIMEOUT_SQL) await tx.query(sql);
419
443
  await tx.query(SYSTEM_TABLE_BOOTSTRAP_LOCK_SQL);
420
444
  return callback(tx);
421
445
  });