@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/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
|
|
212
|
-
- **
|
|
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` |
|
|
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';
|
package/dist/browser.d.ts.map
CHANGED
|
@@ -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;
|
|
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 };
|
package/dist/class.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"class.d.ts","sourceRoot":"","sources":["../src/class.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzD,OAAO,EAAE,KAAK,QAAQ,EAAS,MAAM,mBAAmB,CAAC;AACzD,OAAO,KAAK,EACV,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAgB,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACxE,OAAO,KAAK,EAGV,kBAAkB,EAClB,eAAe,EACf,YAAY,EACZ,qBAAqB,EACrB,aAAa,EAEb,iBAAiB,EAClB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EACL,KAAK,iBAAiB,EAGvB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,KAAK,EAEV,aAAa,EAEb,aAAa,EACb,YAAY,EACb,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;
|
|
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
|
-
|
|
252
|
-
...isMemoryDb ? {} : { dbid: `smrt:${
|
|
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
|
-
...
|
|
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
|
});
|