@objectstack/driver-sql 9.11.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +248 -1
- package/dist/index.d.ts +248 -1
- package/dist/index.js +560 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +555 -18
- package/dist/index.mjs.map +1 -1
- package/package.json +7 -7
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,115 @@
|
|
|
1
1
|
import { AutonumberToken, SchemaMode, QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
2
2
|
import { IDataDriver } from '@objectstack/spec/contracts';
|
|
3
|
+
import { SchemaDiffEntry } from '@objectstack/spec/shared';
|
|
3
4
|
import knex, { Knex } from 'knex';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Managed-datasource schema drift (issue #2186).
|
|
8
|
+
*
|
|
9
|
+
* The driver's `initObjects` sync is *additive-only*: it creates missing
|
|
10
|
+
* tables and adds missing columns, but never alters or drops existing ones.
|
|
11
|
+
* So a non-additive metadata change (relax `required`, change a type/length,
|
|
12
|
+
* drop or rename a field) silently diverges from an existing database — the
|
|
13
|
+
* served metadata says one thing and the physical column enforces another.
|
|
14
|
+
*
|
|
15
|
+
* This module is the single source of truth for *detecting* that divergence
|
|
16
|
+
* (metadata is authoritative on a `managed` datasource) and for *categorising*
|
|
17
|
+
* each divergence by how dangerous it is to reconcile:
|
|
18
|
+
*
|
|
19
|
+
* - `safe` — loosening that cannot lose data and cannot fail:
|
|
20
|
+
* relax NOT NULL → NULL, widen a varchar. Applied
|
|
21
|
+
* automatically by dev auto-reconcile (P2).
|
|
22
|
+
* - `needs_confirm`— a change a human should eyeball but that does not
|
|
23
|
+
* destroy data (e.g. a non-narrowing type change).
|
|
24
|
+
* - `destructive` — drops or tightenings that can lose data or fail:
|
|
25
|
+
* drop an orphaned column, narrow a varchar, add a
|
|
26
|
+
* NOT NULL constraint over possibly-null data. Only
|
|
27
|
+
* applied by `os migrate apply --allow-destructive`.
|
|
28
|
+
*
|
|
29
|
+
* The detector reuses {@link SchemaDiffEntry} (the same shape the external /
|
|
30
|
+
* federated validator emits, ADR-0015 §5.2) so CLI / Studio / audit can render
|
|
31
|
+
* managed and external drift uniformly.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
type SqlDialectName = 'sqlite' | 'postgres' | 'mysql' | 'unknown';
|
|
35
|
+
type DriftCategory = 'safe' | 'needs_confirm' | 'destructive';
|
|
36
|
+
/** A reconcilable schema operation, machine-readable for the reconciler. */
|
|
37
|
+
type DriftOp = {
|
|
38
|
+
type: 'relax_not_null';
|
|
39
|
+
table: string;
|
|
40
|
+
column: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'tighten_not_null';
|
|
43
|
+
table: string;
|
|
44
|
+
column: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'widen_varchar';
|
|
47
|
+
table: string;
|
|
48
|
+
column: string;
|
|
49
|
+
to: number;
|
|
50
|
+
from?: number;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'narrow_varchar';
|
|
53
|
+
table: string;
|
|
54
|
+
column: string;
|
|
55
|
+
to: number;
|
|
56
|
+
from?: number;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'drop_column';
|
|
59
|
+
table: string;
|
|
60
|
+
column: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A managed-schema drift finding: a {@link SchemaDiffEntry} enriched with the
|
|
64
|
+
* owning table, a reconcile {@link DriftOp}, and a {@link DriftCategory}.
|
|
65
|
+
*/
|
|
66
|
+
interface ManagedDriftEntry extends SchemaDiffEntry {
|
|
67
|
+
table: string;
|
|
68
|
+
category: DriftCategory;
|
|
69
|
+
op: DriftOp;
|
|
70
|
+
/** Human one-liner with an actionable hint. */
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
/** Columns the driver creates unconditionally — never metadata fields. */
|
|
74
|
+
declare const BUILTIN_COLUMNS: Set<string>;
|
|
75
|
+
/** Minimal shape of an introspected physical column (see SqlDriver.introspectColumns). */
|
|
76
|
+
interface PhysicalColumn {
|
|
77
|
+
name: string;
|
|
78
|
+
type: string;
|
|
79
|
+
nullable: boolean;
|
|
80
|
+
maxLength?: number;
|
|
81
|
+
}
|
|
82
|
+
/** Minimal shape of a metadata field definition. */
|
|
83
|
+
interface FieldDef {
|
|
84
|
+
type?: string;
|
|
85
|
+
required?: boolean;
|
|
86
|
+
multiple?: boolean;
|
|
87
|
+
maxLength?: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Does this metadata field materialise a physical column? Mirrors
|
|
91
|
+
* `SqlDriver.createColumn` exactly: `formula` is virtual (computed, no column);
|
|
92
|
+
* everything else — including `multiple` (a JSON column) — gets one.
|
|
93
|
+
*/
|
|
94
|
+
declare function fieldHasColumn(field: FieldDef): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Diff one table's metadata fields against its physical columns and return the
|
|
97
|
+
* set of *drift* findings. Metadata is authoritative.
|
|
98
|
+
*
|
|
99
|
+
* Note: a metadata field with no physical column is NOT reported — the
|
|
100
|
+
* additive sync (`ALTER TABLE ADD COLUMN`) already covers added fields, so by
|
|
101
|
+
* the time this runs every expected column exists. We only surface the
|
|
102
|
+
* non-additive divergences the additive sync can never fix.
|
|
103
|
+
*/
|
|
104
|
+
declare function diffManagedTable(args: {
|
|
105
|
+
table: string;
|
|
106
|
+
fields: Record<string, FieldDef>;
|
|
107
|
+
columns: PhysicalColumn[];
|
|
108
|
+
dialect: SqlDialectName;
|
|
109
|
+
}): ManagedDriftEntry[];
|
|
110
|
+
/** Stable de-dup / sort key for a drift entry. */
|
|
111
|
+
declare function driftKey(d: ManagedDriftEntry): string;
|
|
112
|
+
|
|
5
113
|
/**
|
|
6
114
|
* SQL Driver for ObjectStack
|
|
7
115
|
*
|
|
@@ -44,6 +152,14 @@ interface IntrospectedSchema {
|
|
|
44
152
|
*/
|
|
45
153
|
type SqlDriverConfig = Knex.Config & {
|
|
46
154
|
schemaMode?: SchemaMode;
|
|
155
|
+
/**
|
|
156
|
+
* Dev-only schema auto-reconcile (issue #2186). When `'safe'`, `initObjects`
|
|
157
|
+
* automatically applies *non-destructive* alters (relax NOT NULL, widen
|
|
158
|
+
* varchar) so an existing database self-heals after a metadata change
|
|
159
|
+
* loosens a constraint. `'off'` (default) only warns. Never applies
|
|
160
|
+
* destructive DDL, and is force-disabled when `NODE_ENV==='production'`.
|
|
161
|
+
*/
|
|
162
|
+
autoMigrate?: 'off' | 'safe';
|
|
47
163
|
};
|
|
48
164
|
/**
|
|
49
165
|
* SQL Driver for ObjectStack.
|
|
@@ -101,6 +217,20 @@ declare class SqlDriver implements IDataDriver {
|
|
|
101
217
|
protected numericFields: Record<string, string[]>;
|
|
102
218
|
protected dateFields: Record<string, Set<string>>;
|
|
103
219
|
protected datetimeFields: Record<string, Set<string>>;
|
|
220
|
+
/**
|
|
221
|
+
* Federation read path (ADR-0015). For external objects whose physical
|
|
222
|
+
* remote table differs from the object name, these map between the two so
|
|
223
|
+
* {@link getBuilder} targets the remote table while the coercion maps above
|
|
224
|
+
* stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
|
|
225
|
+
* managed objects, so the managed query path is unchanged.
|
|
226
|
+
*/
|
|
227
|
+
protected physicalTableByObject: Record<string, string>;
|
|
228
|
+
protected physicalSchemaByObject: Record<string, string>;
|
|
229
|
+
protected objectByPhysicalTable: Record<string, string>;
|
|
230
|
+
/** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
|
|
231
|
+
protected fieldColumnByObject: Record<string, Record<string, string>>;
|
|
232
|
+
/** External columnMap inverse: physical remote column -> logical field (for read output remap). */
|
|
233
|
+
protected columnFieldByObject: Record<string, Record<string, string>>;
|
|
104
234
|
protected tablesWithTimestamps: Set<string>;
|
|
105
235
|
/**
|
|
106
236
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -157,6 +287,7 @@ declare class SqlDriver implements IDataDriver {
|
|
|
157
287
|
*/
|
|
158
288
|
protected logger: {
|
|
159
289
|
warn: (msg: string, meta?: any) => void;
|
|
290
|
+
info?: (msg: string, meta?: any) => void;
|
|
160
291
|
};
|
|
161
292
|
/** Whether the underlying database is a SQLite variant (sqlite3 or better-sqlite3). */
|
|
162
293
|
protected get isSqlite(): boolean;
|
|
@@ -198,6 +329,20 @@ declare class SqlDriver implements IDataDriver {
|
|
|
198
329
|
* `'managed'` so existing callers are unaffected.
|
|
199
330
|
*/
|
|
200
331
|
protected readonly schemaMode: SchemaMode;
|
|
332
|
+
/**
|
|
333
|
+
* Dev-only auto-reconcile policy (issue #2186). See {@link SqlDriverConfig.autoMigrate}.
|
|
334
|
+
*/
|
|
335
|
+
protected readonly autoMigrate: 'off' | 'safe';
|
|
336
|
+
/**
|
|
337
|
+
* Metadata field defs for every table this driver manages, captured during
|
|
338
|
+
* `initObjects` (tableName → fields). The source of truth that
|
|
339
|
+
* {@link detectManagedDrift} diffs the physical schema against.
|
|
340
|
+
*/
|
|
341
|
+
protected managedObjectFields: Map<string, Record<string, any>>;
|
|
342
|
+
/** Declared indexes per managed table (tableName → indexes[]), captured in `initObjects`. Used to recreate indexes after a SQLite table rebuild. */
|
|
343
|
+
protected managedObjectIndexes: Map<string, any[]>;
|
|
344
|
+
/** De-dup set for boot-time drift warnings (keyed by {@link driftKey}). */
|
|
345
|
+
protected driftWarned: Set<string>;
|
|
201
346
|
constructor(config: SqlDriverConfig);
|
|
202
347
|
/**
|
|
203
348
|
* DDL gate (ADR-0015 §5.1). Single choke-point asserting that
|
|
@@ -334,10 +479,89 @@ declare class SqlDriver implements IDataDriver {
|
|
|
334
479
|
/**
|
|
335
480
|
* Batch-initialise tables from an array of object definitions.
|
|
336
481
|
*/
|
|
482
|
+
/**
|
|
483
|
+
* DDL-free metadata registration for a federated (external) object — the
|
|
484
|
+
* read-path counterpart to {@link initObjects} (ADR-0015 federation).
|
|
485
|
+
*
|
|
486
|
+
* `initObjects` is gated by `assertSchemaMutable` and therefore throws for
|
|
487
|
+
* any non-`managed` driver, which left external objects with NO read-coercion
|
|
488
|
+
* metadata and the query path resolving to a table named after the object
|
|
489
|
+
* instead of its remote table. This populates the same coercion maps (keyed
|
|
490
|
+
* by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
|
|
491
|
+
* records the physical remote table (`external.remoteName`, optionally
|
|
492
|
+
* `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
|
|
493
|
+
* any DDL (createTable/alterTable/columnInfo). Keep the field-classification
|
|
494
|
+
* below in sync with initObjects() if the field-type -> storage mapping changes.
|
|
495
|
+
*/
|
|
496
|
+
registerExternalObject(schema: {
|
|
497
|
+
name: string;
|
|
498
|
+
fields?: Record<string, any>;
|
|
499
|
+
tenancy?: any;
|
|
500
|
+
external?: {
|
|
501
|
+
remoteName?: string;
|
|
502
|
+
remoteSchema?: string;
|
|
503
|
+
columnMap?: Record<string, string>;
|
|
504
|
+
};
|
|
505
|
+
}): void;
|
|
337
506
|
initObjects(objects: Array<{
|
|
338
507
|
name: string;
|
|
339
508
|
fields?: Record<string, any>;
|
|
340
509
|
}>): Promise<void>;
|
|
510
|
+
/** Canonical dialect name for the drift differ. */
|
|
511
|
+
protected get dialectName(): SqlDialectName;
|
|
512
|
+
/** True only when running under `NODE_ENV=production` — auto-DDL is force-disabled there. */
|
|
513
|
+
protected isProductionEnv(): boolean;
|
|
514
|
+
/** Diff one table's metadata fields against its physical columns. */
|
|
515
|
+
protected detectTableDrift(tableName: string, fields: Record<string, any>): Promise<ManagedDriftEntry[]>;
|
|
516
|
+
/**
|
|
517
|
+
* Detect every managed-schema divergence between metadata and the physical
|
|
518
|
+
* database. Metadata is the source of truth. Returns one entry per drift,
|
|
519
|
+
* sorted by table then column. Used by `os migrate` (P3) and tests.
|
|
520
|
+
*
|
|
521
|
+
* @param objects optional explicit object list; defaults to whatever
|
|
522
|
+
* `initObjects` last synced (captured in {@link managedObjectFields}).
|
|
523
|
+
*/
|
|
524
|
+
detectManagedDrift(objects?: Array<{
|
|
525
|
+
name: string;
|
|
526
|
+
fields?: Record<string, any>;
|
|
527
|
+
}>): Promise<ManagedDriftEntry[]>;
|
|
528
|
+
/**
|
|
529
|
+
* Boot-time per-table drift handling (P1 + P2): detect divergence, in dev
|
|
530
|
+
* auto-reconcile the *safe* (loosening) subset when `autoMigrate==='safe'`,
|
|
531
|
+
* then WARN once per remaining divergence with an actionable hint.
|
|
532
|
+
*/
|
|
533
|
+
protected reconcileAndWarnDrift(tableName: string, fields: Record<string, any>): Promise<void>;
|
|
534
|
+
/**
|
|
535
|
+
* Apply a set of drift entries to the physical schema. Destructive entries
|
|
536
|
+
* are skipped unless `allowDestructive` is set. Postgres/MySQL alter columns
|
|
537
|
+
* in place; SQLite (which cannot alter constraints in place) rebuilds each
|
|
538
|
+
* affected table (copy → swap) applying only the requested edits.
|
|
539
|
+
*
|
|
540
|
+
* @returns the entries actually applied and those skipped (e.g. destructive
|
|
541
|
+
* without `allowDestructive`, or unsupported on the dialect).
|
|
542
|
+
*/
|
|
543
|
+
applyMigrationEntries(entries: ManagedDriftEntry[], opts?: {
|
|
544
|
+
allowDestructive?: boolean;
|
|
545
|
+
}): Promise<{
|
|
546
|
+
applied: ManagedDriftEntry[];
|
|
547
|
+
skipped: ManagedDriftEntry[];
|
|
548
|
+
}>;
|
|
549
|
+
/** Apply a single drift op in place (Postgres / MySQL). Returns false if unsupported. */
|
|
550
|
+
protected applyDriftOpInPlace(op: DriftOp): Promise<boolean>;
|
|
551
|
+
/**
|
|
552
|
+
* Rebuild a SQLite table applying a set of column edits (relax/tighten NOT
|
|
553
|
+
* NULL, drop column), preserving all other columns and their data. Follows
|
|
554
|
+
* the official SQLite procedure: create patched table → copy → drop → rename.
|
|
555
|
+
* varchar widen/narrow are no-ops on SQLite (dynamic typing) and ignored.
|
|
556
|
+
*
|
|
557
|
+
* Unique field-level constraints and declared indexes are recreated from
|
|
558
|
+
* metadata afterwards (the source of truth). DB-level foreign keys declared
|
|
559
|
+
* by `lookup` fields are not re-added (ObjectStack enforces relationships at
|
|
560
|
+
* the application layer, not via SQLite FK constraints).
|
|
561
|
+
*/
|
|
562
|
+
protected rebuildSqliteTablePatched(table: string, ents: ManagedDriftEntry[]): Promise<void>;
|
|
563
|
+
/** Map an introspected SQLite column to a knex builder for the rebuilt table. */
|
|
564
|
+
protected buildRebuiltColumn(t: Knex.CreateTableBuilder, c: IntrospectedColumn): any;
|
|
341
565
|
/**
|
|
342
566
|
* Build a deterministic index name for a declared index so repeated
|
|
343
567
|
* `initObjects` runs converge on the same identifier (and can detect an
|
|
@@ -439,6 +663,16 @@ declare class SqlDriver implements IDataDriver {
|
|
|
439
663
|
* `initObjects`). Returns null when the builder is not table-scoped yet.
|
|
440
664
|
*/
|
|
441
665
|
protected tableNameForBuilder(builder: any): string | null;
|
|
666
|
+
/**
|
|
667
|
+
* Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
|
|
668
|
+
* OBJECT name, but after the federation change {@link getBuilder} targets the
|
|
669
|
+
* physical remote table, so a builder reports the remote name. Map it back to
|
|
670
|
+
* the object name for external objects; identity for managed ones (no reverse
|
|
671
|
+
* entry). Note datetime coercion is a SQLite-only concern (see
|
|
672
|
+
* coerceFilterValue), and SQLite external tables are bare-named, so this is
|
|
673
|
+
* exact where it matters.
|
|
674
|
+
*/
|
|
675
|
+
protected coercionKey(builder: any): string | null;
|
|
442
676
|
/**
|
|
443
677
|
* Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
|
|
444
678
|
* calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
|
|
@@ -498,6 +732,19 @@ declare class SqlDriver implements IDataDriver {
|
|
|
498
732
|
private applyContainsLike;
|
|
499
733
|
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or', tableHint?: string | null): void;
|
|
500
734
|
protected mapSortField(field: string): string;
|
|
735
|
+
/**
|
|
736
|
+
* Physical column for a logical field on an external object that declares an
|
|
737
|
+
* `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
|
|
738
|
+
* per-site resolution) when the object has no columnMap, so managed objects
|
|
739
|
+
* and external objects without a columnMap are byte-for-byte unchanged.
|
|
740
|
+
*/
|
|
741
|
+
protected remoteColumn(object: string | null | undefined, field: string, fallback: string): string;
|
|
742
|
+
/**
|
|
743
|
+
* Remap a write payload's logical field keys to physical remote columns for an
|
|
744
|
+
* external object with a columnMap. No-op otherwise. Applied AFTER formatInput
|
|
745
|
+
* (whose value coercion is keyed by logical field name).
|
|
746
|
+
*/
|
|
747
|
+
protected applyWriteColumnMap(object: string, data: any): any;
|
|
501
748
|
protected mapAggregateFunc(func: string): string;
|
|
502
749
|
protected buildWindowFunction(spec: any): string;
|
|
503
750
|
protected createColumn(table: Knex.CreateTableBuilder, name: string, field: any): void;
|
|
@@ -518,4 +765,4 @@ declare const _default: {
|
|
|
518
765
|
onEnable: (context: any) => Promise<void>;
|
|
519
766
|
};
|
|
520
767
|
|
|
521
|
-
export { type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, SqlDriver, type SqlDriverConfig, _default as default };
|
|
768
|
+
export { BUILTIN_COLUMNS, type DriftCategory, type FieldDef as DriftFieldDef, type DriftOp, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, type ManagedDriftEntry, type PhysicalColumn, type SqlDialectName, SqlDriver, type SqlDriverConfig, _default as default, diffManagedTable, driftKey, fieldHasColumn };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,115 @@
|
|
|
1
1
|
import { AutonumberToken, SchemaMode, QueryAST, DriverOptions } from '@objectstack/spec/data';
|
|
2
2
|
import { IDataDriver } from '@objectstack/spec/contracts';
|
|
3
|
+
import { SchemaDiffEntry } from '@objectstack/spec/shared';
|
|
3
4
|
import knex, { Knex } from 'knex';
|
|
4
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Managed-datasource schema drift (issue #2186).
|
|
8
|
+
*
|
|
9
|
+
* The driver's `initObjects` sync is *additive-only*: it creates missing
|
|
10
|
+
* tables and adds missing columns, but never alters or drops existing ones.
|
|
11
|
+
* So a non-additive metadata change (relax `required`, change a type/length,
|
|
12
|
+
* drop or rename a field) silently diverges from an existing database — the
|
|
13
|
+
* served metadata says one thing and the physical column enforces another.
|
|
14
|
+
*
|
|
15
|
+
* This module is the single source of truth for *detecting* that divergence
|
|
16
|
+
* (metadata is authoritative on a `managed` datasource) and for *categorising*
|
|
17
|
+
* each divergence by how dangerous it is to reconcile:
|
|
18
|
+
*
|
|
19
|
+
* - `safe` — loosening that cannot lose data and cannot fail:
|
|
20
|
+
* relax NOT NULL → NULL, widen a varchar. Applied
|
|
21
|
+
* automatically by dev auto-reconcile (P2).
|
|
22
|
+
* - `needs_confirm`— a change a human should eyeball but that does not
|
|
23
|
+
* destroy data (e.g. a non-narrowing type change).
|
|
24
|
+
* - `destructive` — drops or tightenings that can lose data or fail:
|
|
25
|
+
* drop an orphaned column, narrow a varchar, add a
|
|
26
|
+
* NOT NULL constraint over possibly-null data. Only
|
|
27
|
+
* applied by `os migrate apply --allow-destructive`.
|
|
28
|
+
*
|
|
29
|
+
* The detector reuses {@link SchemaDiffEntry} (the same shape the external /
|
|
30
|
+
* federated validator emits, ADR-0015 §5.2) so CLI / Studio / audit can render
|
|
31
|
+
* managed and external drift uniformly.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
type SqlDialectName = 'sqlite' | 'postgres' | 'mysql' | 'unknown';
|
|
35
|
+
type DriftCategory = 'safe' | 'needs_confirm' | 'destructive';
|
|
36
|
+
/** A reconcilable schema operation, machine-readable for the reconciler. */
|
|
37
|
+
type DriftOp = {
|
|
38
|
+
type: 'relax_not_null';
|
|
39
|
+
table: string;
|
|
40
|
+
column: string;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'tighten_not_null';
|
|
43
|
+
table: string;
|
|
44
|
+
column: string;
|
|
45
|
+
} | {
|
|
46
|
+
type: 'widen_varchar';
|
|
47
|
+
table: string;
|
|
48
|
+
column: string;
|
|
49
|
+
to: number;
|
|
50
|
+
from?: number;
|
|
51
|
+
} | {
|
|
52
|
+
type: 'narrow_varchar';
|
|
53
|
+
table: string;
|
|
54
|
+
column: string;
|
|
55
|
+
to: number;
|
|
56
|
+
from?: number;
|
|
57
|
+
} | {
|
|
58
|
+
type: 'drop_column';
|
|
59
|
+
table: string;
|
|
60
|
+
column: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* A managed-schema drift finding: a {@link SchemaDiffEntry} enriched with the
|
|
64
|
+
* owning table, a reconcile {@link DriftOp}, and a {@link DriftCategory}.
|
|
65
|
+
*/
|
|
66
|
+
interface ManagedDriftEntry extends SchemaDiffEntry {
|
|
67
|
+
table: string;
|
|
68
|
+
category: DriftCategory;
|
|
69
|
+
op: DriftOp;
|
|
70
|
+
/** Human one-liner with an actionable hint. */
|
|
71
|
+
message: string;
|
|
72
|
+
}
|
|
73
|
+
/** Columns the driver creates unconditionally — never metadata fields. */
|
|
74
|
+
declare const BUILTIN_COLUMNS: Set<string>;
|
|
75
|
+
/** Minimal shape of an introspected physical column (see SqlDriver.introspectColumns). */
|
|
76
|
+
interface PhysicalColumn {
|
|
77
|
+
name: string;
|
|
78
|
+
type: string;
|
|
79
|
+
nullable: boolean;
|
|
80
|
+
maxLength?: number;
|
|
81
|
+
}
|
|
82
|
+
/** Minimal shape of a metadata field definition. */
|
|
83
|
+
interface FieldDef {
|
|
84
|
+
type?: string;
|
|
85
|
+
required?: boolean;
|
|
86
|
+
multiple?: boolean;
|
|
87
|
+
maxLength?: number;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Does this metadata field materialise a physical column? Mirrors
|
|
91
|
+
* `SqlDriver.createColumn` exactly: `formula` is virtual (computed, no column);
|
|
92
|
+
* everything else — including `multiple` (a JSON column) — gets one.
|
|
93
|
+
*/
|
|
94
|
+
declare function fieldHasColumn(field: FieldDef): boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Diff one table's metadata fields against its physical columns and return the
|
|
97
|
+
* set of *drift* findings. Metadata is authoritative.
|
|
98
|
+
*
|
|
99
|
+
* Note: a metadata field with no physical column is NOT reported — the
|
|
100
|
+
* additive sync (`ALTER TABLE ADD COLUMN`) already covers added fields, so by
|
|
101
|
+
* the time this runs every expected column exists. We only surface the
|
|
102
|
+
* non-additive divergences the additive sync can never fix.
|
|
103
|
+
*/
|
|
104
|
+
declare function diffManagedTable(args: {
|
|
105
|
+
table: string;
|
|
106
|
+
fields: Record<string, FieldDef>;
|
|
107
|
+
columns: PhysicalColumn[];
|
|
108
|
+
dialect: SqlDialectName;
|
|
109
|
+
}): ManagedDriftEntry[];
|
|
110
|
+
/** Stable de-dup / sort key for a drift entry. */
|
|
111
|
+
declare function driftKey(d: ManagedDriftEntry): string;
|
|
112
|
+
|
|
5
113
|
/**
|
|
6
114
|
* SQL Driver for ObjectStack
|
|
7
115
|
*
|
|
@@ -44,6 +152,14 @@ interface IntrospectedSchema {
|
|
|
44
152
|
*/
|
|
45
153
|
type SqlDriverConfig = Knex.Config & {
|
|
46
154
|
schemaMode?: SchemaMode;
|
|
155
|
+
/**
|
|
156
|
+
* Dev-only schema auto-reconcile (issue #2186). When `'safe'`, `initObjects`
|
|
157
|
+
* automatically applies *non-destructive* alters (relax NOT NULL, widen
|
|
158
|
+
* varchar) so an existing database self-heals after a metadata change
|
|
159
|
+
* loosens a constraint. `'off'` (default) only warns. Never applies
|
|
160
|
+
* destructive DDL, and is force-disabled when `NODE_ENV==='production'`.
|
|
161
|
+
*/
|
|
162
|
+
autoMigrate?: 'off' | 'safe';
|
|
47
163
|
};
|
|
48
164
|
/**
|
|
49
165
|
* SQL Driver for ObjectStack.
|
|
@@ -101,6 +217,20 @@ declare class SqlDriver implements IDataDriver {
|
|
|
101
217
|
protected numericFields: Record<string, string[]>;
|
|
102
218
|
protected dateFields: Record<string, Set<string>>;
|
|
103
219
|
protected datetimeFields: Record<string, Set<string>>;
|
|
220
|
+
/**
|
|
221
|
+
* Federation read path (ADR-0015). For external objects whose physical
|
|
222
|
+
* remote table differs from the object name, these map between the two so
|
|
223
|
+
* {@link getBuilder} targets the remote table while the coercion maps above
|
|
224
|
+
* stay keyed by OBJECT name (matching formatInput/formatOutput). Empty for
|
|
225
|
+
* managed objects, so the managed query path is unchanged.
|
|
226
|
+
*/
|
|
227
|
+
protected physicalTableByObject: Record<string, string>;
|
|
228
|
+
protected physicalSchemaByObject: Record<string, string>;
|
|
229
|
+
protected objectByPhysicalTable: Record<string, string>;
|
|
230
|
+
/** External columnMap (ADR-0015): logical field -> physical remote column (for WHERE/ORDER BY/writes). */
|
|
231
|
+
protected fieldColumnByObject: Record<string, Record<string, string>>;
|
|
232
|
+
/** External columnMap inverse: physical remote column -> logical field (for read output remap). */
|
|
233
|
+
protected columnFieldByObject: Record<string, Record<string, string>>;
|
|
104
234
|
protected tablesWithTimestamps: Set<string>;
|
|
105
235
|
/**
|
|
106
236
|
* Autonumber field configs per table, captured during initObjects.
|
|
@@ -157,6 +287,7 @@ declare class SqlDriver implements IDataDriver {
|
|
|
157
287
|
*/
|
|
158
288
|
protected logger: {
|
|
159
289
|
warn: (msg: string, meta?: any) => void;
|
|
290
|
+
info?: (msg: string, meta?: any) => void;
|
|
160
291
|
};
|
|
161
292
|
/** Whether the underlying database is a SQLite variant (sqlite3 or better-sqlite3). */
|
|
162
293
|
protected get isSqlite(): boolean;
|
|
@@ -198,6 +329,20 @@ declare class SqlDriver implements IDataDriver {
|
|
|
198
329
|
* `'managed'` so existing callers are unaffected.
|
|
199
330
|
*/
|
|
200
331
|
protected readonly schemaMode: SchemaMode;
|
|
332
|
+
/**
|
|
333
|
+
* Dev-only auto-reconcile policy (issue #2186). See {@link SqlDriverConfig.autoMigrate}.
|
|
334
|
+
*/
|
|
335
|
+
protected readonly autoMigrate: 'off' | 'safe';
|
|
336
|
+
/**
|
|
337
|
+
* Metadata field defs for every table this driver manages, captured during
|
|
338
|
+
* `initObjects` (tableName → fields). The source of truth that
|
|
339
|
+
* {@link detectManagedDrift} diffs the physical schema against.
|
|
340
|
+
*/
|
|
341
|
+
protected managedObjectFields: Map<string, Record<string, any>>;
|
|
342
|
+
/** Declared indexes per managed table (tableName → indexes[]), captured in `initObjects`. Used to recreate indexes after a SQLite table rebuild. */
|
|
343
|
+
protected managedObjectIndexes: Map<string, any[]>;
|
|
344
|
+
/** De-dup set for boot-time drift warnings (keyed by {@link driftKey}). */
|
|
345
|
+
protected driftWarned: Set<string>;
|
|
201
346
|
constructor(config: SqlDriverConfig);
|
|
202
347
|
/**
|
|
203
348
|
* DDL gate (ADR-0015 §5.1). Single choke-point asserting that
|
|
@@ -334,10 +479,89 @@ declare class SqlDriver implements IDataDriver {
|
|
|
334
479
|
/**
|
|
335
480
|
* Batch-initialise tables from an array of object definitions.
|
|
336
481
|
*/
|
|
482
|
+
/**
|
|
483
|
+
* DDL-free metadata registration for a federated (external) object — the
|
|
484
|
+
* read-path counterpart to {@link initObjects} (ADR-0015 federation).
|
|
485
|
+
*
|
|
486
|
+
* `initObjects` is gated by `assertSchemaMutable` and therefore throws for
|
|
487
|
+
* any non-`managed` driver, which left external objects with NO read-coercion
|
|
488
|
+
* metadata and the query path resolving to a table named after the object
|
|
489
|
+
* instead of its remote table. This populates the same coercion maps (keyed
|
|
490
|
+
* by OBJECT name, matching formatInput/formatOutput/coerceFilterValue) and
|
|
491
|
+
* records the physical remote table (`external.remoteName`, optionally
|
|
492
|
+
* `external.remoteSchema`) so {@link getBuilder} targets it — WITHOUT running
|
|
493
|
+
* any DDL (createTable/alterTable/columnInfo). Keep the field-classification
|
|
494
|
+
* below in sync with initObjects() if the field-type -> storage mapping changes.
|
|
495
|
+
*/
|
|
496
|
+
registerExternalObject(schema: {
|
|
497
|
+
name: string;
|
|
498
|
+
fields?: Record<string, any>;
|
|
499
|
+
tenancy?: any;
|
|
500
|
+
external?: {
|
|
501
|
+
remoteName?: string;
|
|
502
|
+
remoteSchema?: string;
|
|
503
|
+
columnMap?: Record<string, string>;
|
|
504
|
+
};
|
|
505
|
+
}): void;
|
|
337
506
|
initObjects(objects: Array<{
|
|
338
507
|
name: string;
|
|
339
508
|
fields?: Record<string, any>;
|
|
340
509
|
}>): Promise<void>;
|
|
510
|
+
/** Canonical dialect name for the drift differ. */
|
|
511
|
+
protected get dialectName(): SqlDialectName;
|
|
512
|
+
/** True only when running under `NODE_ENV=production` — auto-DDL is force-disabled there. */
|
|
513
|
+
protected isProductionEnv(): boolean;
|
|
514
|
+
/** Diff one table's metadata fields against its physical columns. */
|
|
515
|
+
protected detectTableDrift(tableName: string, fields: Record<string, any>): Promise<ManagedDriftEntry[]>;
|
|
516
|
+
/**
|
|
517
|
+
* Detect every managed-schema divergence between metadata and the physical
|
|
518
|
+
* database. Metadata is the source of truth. Returns one entry per drift,
|
|
519
|
+
* sorted by table then column. Used by `os migrate` (P3) and tests.
|
|
520
|
+
*
|
|
521
|
+
* @param objects optional explicit object list; defaults to whatever
|
|
522
|
+
* `initObjects` last synced (captured in {@link managedObjectFields}).
|
|
523
|
+
*/
|
|
524
|
+
detectManagedDrift(objects?: Array<{
|
|
525
|
+
name: string;
|
|
526
|
+
fields?: Record<string, any>;
|
|
527
|
+
}>): Promise<ManagedDriftEntry[]>;
|
|
528
|
+
/**
|
|
529
|
+
* Boot-time per-table drift handling (P1 + P2): detect divergence, in dev
|
|
530
|
+
* auto-reconcile the *safe* (loosening) subset when `autoMigrate==='safe'`,
|
|
531
|
+
* then WARN once per remaining divergence with an actionable hint.
|
|
532
|
+
*/
|
|
533
|
+
protected reconcileAndWarnDrift(tableName: string, fields: Record<string, any>): Promise<void>;
|
|
534
|
+
/**
|
|
535
|
+
* Apply a set of drift entries to the physical schema. Destructive entries
|
|
536
|
+
* are skipped unless `allowDestructive` is set. Postgres/MySQL alter columns
|
|
537
|
+
* in place; SQLite (which cannot alter constraints in place) rebuilds each
|
|
538
|
+
* affected table (copy → swap) applying only the requested edits.
|
|
539
|
+
*
|
|
540
|
+
* @returns the entries actually applied and those skipped (e.g. destructive
|
|
541
|
+
* without `allowDestructive`, or unsupported on the dialect).
|
|
542
|
+
*/
|
|
543
|
+
applyMigrationEntries(entries: ManagedDriftEntry[], opts?: {
|
|
544
|
+
allowDestructive?: boolean;
|
|
545
|
+
}): Promise<{
|
|
546
|
+
applied: ManagedDriftEntry[];
|
|
547
|
+
skipped: ManagedDriftEntry[];
|
|
548
|
+
}>;
|
|
549
|
+
/** Apply a single drift op in place (Postgres / MySQL). Returns false if unsupported. */
|
|
550
|
+
protected applyDriftOpInPlace(op: DriftOp): Promise<boolean>;
|
|
551
|
+
/**
|
|
552
|
+
* Rebuild a SQLite table applying a set of column edits (relax/tighten NOT
|
|
553
|
+
* NULL, drop column), preserving all other columns and their data. Follows
|
|
554
|
+
* the official SQLite procedure: create patched table → copy → drop → rename.
|
|
555
|
+
* varchar widen/narrow are no-ops on SQLite (dynamic typing) and ignored.
|
|
556
|
+
*
|
|
557
|
+
* Unique field-level constraints and declared indexes are recreated from
|
|
558
|
+
* metadata afterwards (the source of truth). DB-level foreign keys declared
|
|
559
|
+
* by `lookup` fields are not re-added (ObjectStack enforces relationships at
|
|
560
|
+
* the application layer, not via SQLite FK constraints).
|
|
561
|
+
*/
|
|
562
|
+
protected rebuildSqliteTablePatched(table: string, ents: ManagedDriftEntry[]): Promise<void>;
|
|
563
|
+
/** Map an introspected SQLite column to a knex builder for the rebuilt table. */
|
|
564
|
+
protected buildRebuiltColumn(t: Knex.CreateTableBuilder, c: IntrospectedColumn): any;
|
|
341
565
|
/**
|
|
342
566
|
* Build a deterministic index name for a declared index so repeated
|
|
343
567
|
* `initObjects` runs converge on the same identifier (and can detect an
|
|
@@ -439,6 +663,16 @@ declare class SqlDriver implements IDataDriver {
|
|
|
439
663
|
* `initObjects`). Returns null when the builder is not table-scoped yet.
|
|
440
664
|
*/
|
|
441
665
|
protected tableNameForBuilder(builder: any): string | null;
|
|
666
|
+
/**
|
|
667
|
+
* Coercion-map key for a builder. Coercion maps (date/datetime) are keyed by
|
|
668
|
+
* OBJECT name, but after the federation change {@link getBuilder} targets the
|
|
669
|
+
* physical remote table, so a builder reports the remote name. Map it back to
|
|
670
|
+
* the object name for external objects; identity for managed ones (no reverse
|
|
671
|
+
* entry). Note datetime coercion is a SQLite-only concern (see
|
|
672
|
+
* coerceFilterValue), and SQLite external tables are bare-named, so this is
|
|
673
|
+
* exact where it matters.
|
|
674
|
+
*/
|
|
675
|
+
protected coercionKey(builder: any): string | null;
|
|
442
676
|
/**
|
|
443
677
|
* Collapse a `Field.date` value to a timezone-naive `YYYY-MM-DD`
|
|
444
678
|
* calendar-day string (ADR-0053 Phase 1). A `Date` collapses to its UTC
|
|
@@ -498,6 +732,19 @@ declare class SqlDriver implements IDataDriver {
|
|
|
498
732
|
private applyContainsLike;
|
|
499
733
|
protected applyFilterCondition(builder: Knex.QueryBuilder, condition: any, logicalOp?: 'and' | 'or', tableHint?: string | null): void;
|
|
500
734
|
protected mapSortField(field: string): string;
|
|
735
|
+
/**
|
|
736
|
+
* Physical column for a logical field on an external object that declares an
|
|
737
|
+
* `external.columnMap` (ADR-0015). Returns `fallback` (the caller's existing
|
|
738
|
+
* per-site resolution) when the object has no columnMap, so managed objects
|
|
739
|
+
* and external objects without a columnMap are byte-for-byte unchanged.
|
|
740
|
+
*/
|
|
741
|
+
protected remoteColumn(object: string | null | undefined, field: string, fallback: string): string;
|
|
742
|
+
/**
|
|
743
|
+
* Remap a write payload's logical field keys to physical remote columns for an
|
|
744
|
+
* external object with a columnMap. No-op otherwise. Applied AFTER formatInput
|
|
745
|
+
* (whose value coercion is keyed by logical field name).
|
|
746
|
+
*/
|
|
747
|
+
protected applyWriteColumnMap(object: string, data: any): any;
|
|
501
748
|
protected mapAggregateFunc(func: string): string;
|
|
502
749
|
protected buildWindowFunction(spec: any): string;
|
|
503
750
|
protected createColumn(table: Knex.CreateTableBuilder, name: string, field: any): void;
|
|
@@ -518,4 +765,4 @@ declare const _default: {
|
|
|
518
765
|
onEnable: (context: any) => Promise<void>;
|
|
519
766
|
};
|
|
520
767
|
|
|
521
|
-
export { type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, SqlDriver, type SqlDriverConfig, _default as default };
|
|
768
|
+
export { BUILTIN_COLUMNS, type DriftCategory, type FieldDef as DriftFieldDef, type DriftOp, type IntrospectedColumn, type IntrospectedForeignKey, type IntrospectedSchema, type IntrospectedTable, type ManagedDriftEntry, type PhysicalColumn, type SqlDialectName, SqlDriver, type SqlDriverConfig, _default as default, diffManagedTable, driftKey, fieldHasColumn };
|