uql-orm 0.42.0 → 0.43.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/browser/querier/httpQuerier.d.ts +3 -3
- package/dist/browser/type/clientQuerier.d.ts +3 -3
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +5 -5
- package/dist/dialect/abstractSqlDialect.d.ts +41 -14
- package/dist/dialect/abstractSqlDialect.js +75 -44
- package/dist/dialect/jsonSql.d.ts +3 -2
- package/dist/dialect/jsonSql.js +7 -5
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -1
- package/dist/dialect/mysqlLikeSqlDialect.js +3 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/dialect/vectorCast.d.ts +0 -6
- package/dist/dialect/vectorCast.js +0 -8
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/entity/decorator/members.d.ts +5 -2
- package/dist/entity/metadata/definition.js +29 -12
- package/dist/maria/mariaDialect.js +4 -3
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +21 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +8 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +25 -29
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
- package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
- package/dist/migrate/introspection/postgresIntrospector.js +1 -1
- package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
- package/dist/migrate/migrator.js +6 -0
- package/dist/migrate/schemaGenerator.d.ts +43 -37
- package/dist/migrate/schemaGenerator.js +163 -150
- package/dist/mongo/mongoDialect.js +22 -8
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.js +18 -7
- package/dist/querier/relationCount.js +9 -7
- package/dist/schema/canonicalType.d.ts +19 -4
- package/dist/schema/canonicalType.js +114 -164
- package/dist/schema/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +27 -33
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +59 -19
- package/dist/schema/types.d.ts +46 -7
- package/dist/sqlite/sqliteDialect.d.ts +2 -1
- package/dist/sqlite/sqliteDialect.js +4 -1
- package/dist/type/dialect.d.ts +6 -0
- package/dist/type/entity.d.ts +23 -6
- package/dist/type/migration.d.ts +20 -1
- package/dist/type/query.d.ts +2 -6
- package/dist/util/field.util.d.ts +28 -7
- package/dist/util/field.util.js +56 -48
- package/dist/util/fieldOption.util.d.ts +79 -0
- package/dist/util/fieldOption.util.js +84 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.js +11 -3
- package/dist/util/relationQuery.util.d.ts +10 -0
- package/dist/util/relationQuery.util.js +22 -2
- package/dist/util/sql.util.d.ts +28 -7
- package/dist/util/sql.util.js +79 -10
- package/package.json +2 -2
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - Schema synchronization
|
|
9
9
|
*/
|
|
10
10
|
import { areTypesEqual, isBreakingTypeChange } from './canonicalType.js';
|
|
11
|
-
import { describeIndexDifferences } from './indexDifferences.js';
|
|
11
|
+
import { describeIndexDifferences, indexNameStem } from './indexDifferences.js';
|
|
12
12
|
import { DEFAULT_FOREIGN_KEY_ACTION } from './types.js';
|
|
13
13
|
/**
|
|
14
14
|
* Default diff options.
|
|
@@ -17,6 +17,8 @@ const DEFAULT_OPTIONS = {
|
|
|
17
17
|
compareIndexes: true,
|
|
18
18
|
indexFacets: new Set(),
|
|
19
19
|
compareRelationships: true,
|
|
20
|
+
normalizeType: (type) => type,
|
|
21
|
+
defaultsEqual: (expected, actual) => normalizeDefault(expected) === normalizeDefault(actual),
|
|
20
22
|
ignoreCase: false,
|
|
21
23
|
excludeTables: [],
|
|
22
24
|
};
|
|
@@ -61,20 +63,25 @@ export function diffSchemas(source, target, options = {}) {
|
|
|
61
63
|
.filter((tableDiff) => tableDiff !== undefined);
|
|
62
64
|
const columnDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.columnDiffs ?? []);
|
|
63
65
|
const indexDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.indexDiffs ?? []);
|
|
66
|
+
const primaryKeyDiffs = tablesToAlter.flatMap((tableDiff) => tableDiff.primaryKeyDiff ?? []);
|
|
64
67
|
// Relationships span tables, so they are compared over the whole schema rather than per table.
|
|
65
68
|
const relationshipDiffs = opts.compareRelationships ? diffRelationships(source, target, opts) : [];
|
|
66
69
|
const hasDifferences = tablesToCreate.length > 0 ||
|
|
67
70
|
tablesToDrop.length > 0 ||
|
|
68
71
|
tablesToAlter.length > 0 ||
|
|
69
72
|
relationshipDiffs.length > 0 ||
|
|
70
|
-
indexDiffs.length > 0
|
|
71
|
-
|
|
73
|
+
indexDiffs.length > 0 ||
|
|
74
|
+
primaryKeyDiffs.length > 0;
|
|
75
|
+
// Rewriting a key drops a constraint and rebuilds an index over the whole table, and fails outright
|
|
76
|
+
// where its new columns are null on rows that already exist. Breaking by any measure.
|
|
77
|
+
const hasBreakingChanges = tablesToDrop.length > 0 || primaryKeyDiffs.length > 0 || columnDiffs.some((d) => d.isBreaking);
|
|
72
78
|
return {
|
|
73
79
|
tablesToCreate,
|
|
74
80
|
tablesToDrop,
|
|
75
81
|
tablesToAlter,
|
|
76
82
|
columnDiffs,
|
|
77
83
|
indexDiffs,
|
|
84
|
+
primaryKeyDiffs,
|
|
78
85
|
relationshipDiffs,
|
|
79
86
|
hasDifferences,
|
|
80
87
|
hasBreakingChanges,
|
|
@@ -82,14 +89,35 @@ export function diffSchemas(source, target, options = {}) {
|
|
|
82
89
|
}
|
|
83
90
|
/**
|
|
84
91
|
* Compare two tables and return the differences.
|
|
92
|
+
*
|
|
93
|
+
* Exported because it is also how a migration is planned: the generator diffs one entity's table
|
|
94
|
+
* against the one the database reported, then projects the result into a `SchemaDiff`. One
|
|
95
|
+
* comparison serves both, so drift and migrations can no longer disagree about what has changed.
|
|
85
96
|
*/
|
|
86
|
-
function diffTable(source, target,
|
|
97
|
+
export function diffTable(source, target, options = {}) {
|
|
98
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
87
99
|
const columnDiffs = diffTableColumns(source, target, opts);
|
|
88
100
|
const indexDiffs = opts.compareIndexes ? diffTableIndexes(source, target, opts) : [];
|
|
89
|
-
|
|
101
|
+
const primaryKeyDiff = diffPrimaryKey(source, target);
|
|
102
|
+
if (columnDiffs.length === 0 && indexDiffs.length === 0 && !primaryKeyDiff) {
|
|
90
103
|
return undefined;
|
|
91
104
|
}
|
|
92
|
-
return { name: source.name, type: 'alter', columnDiffs, indexDiffs };
|
|
105
|
+
return { name: source.name, type: 'alter', columnDiffs, indexDiffs, primaryKeyDiff };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The two keys, where they hold different columns.
|
|
109
|
+
*
|
|
110
|
+
* Compared by columns and in order. Not by name: the engine named the constraint on every table that
|
|
111
|
+
* already exists, so matching on one would report every table as drifted the moment the convention
|
|
112
|
+
* that derives names changes.
|
|
113
|
+
*/
|
|
114
|
+
function diffPrimaryKey(source, target) {
|
|
115
|
+
const expected = source.primaryKey.map((column) => column.name);
|
|
116
|
+
const actual = target.primaryKey.map((column) => column.name);
|
|
117
|
+
if (expected.length === actual.length && expected.every((column, i) => column === actual[i])) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return { table: source.name, expected, actual, actualName: target.primaryKeyName };
|
|
93
121
|
}
|
|
94
122
|
/**
|
|
95
123
|
* Compare columns between two tables.
|
|
@@ -114,7 +142,7 @@ function diffTableColumns(source, target, opts) {
|
|
|
114
142
|
description: `Drop column "${column.name}"`,
|
|
115
143
|
})),
|
|
116
144
|
...matched
|
|
117
|
-
.map(([sourceColumn, targetColumn]) => diffColumn(source.name, sourceColumn, targetColumn))
|
|
145
|
+
.map(([sourceColumn, targetColumn]) => diffColumn(source.name, sourceColumn, targetColumn, opts))
|
|
118
146
|
.filter((diff) => diff !== undefined),
|
|
119
147
|
];
|
|
120
148
|
}
|
|
@@ -123,7 +151,7 @@ function diffTableColumns(source, target, opts) {
|
|
|
123
151
|
*/
|
|
124
152
|
function diffTableIndexes(source, target, opts) {
|
|
125
153
|
const normalizeName = nameNormalizer(opts);
|
|
126
|
-
const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(index.name));
|
|
154
|
+
const { created, dropped, matched } = matchByKey(source.indexes, target.indexes, (index) => normalizeName(indexNameStem(index.name)));
|
|
127
155
|
return [
|
|
128
156
|
...created.map((index) => ({ name: index.name, table: source.name, type: 'create', expected: index })),
|
|
129
157
|
...dropped.map((index) => ({ name: index.name, table: target.name, type: 'drop', actual: index })),
|
|
@@ -135,26 +163,35 @@ function diffTableIndexes(source, target, opts) {
|
|
|
135
163
|
/**
|
|
136
164
|
* Compare two columns and return the difference.
|
|
137
165
|
*/
|
|
138
|
-
function diffColumn(tableName, source, target) {
|
|
166
|
+
function diffColumn(tableName, source, target, opts) {
|
|
139
167
|
const differences = [];
|
|
140
|
-
//
|
|
141
|
-
|
|
168
|
+
// Two things a key column implies rather than states, and catalogues report inconsistently: its
|
|
169
|
+
// type, which is the dialect's serial spelling rather than one the entity chose and does not round
|
|
170
|
+
// trip (`BIGINT UNSIGNED AUTO_INCREMENT` reads back as `BIGINT(20) UNSIGNED`), and its nullability,
|
|
171
|
+
// which is NOT NULL in every engine whatever is reported - SQLite's `PRAGMA table_info` says
|
|
172
|
+
// `notnull: 0` for the `INTEGER PRIMARY KEY` that is the table's own rowid. Comparing either asked
|
|
173
|
+
// to rewrite the column on every sync, and on SQLite, which cannot alter one at all, failed
|
|
174
|
+
// outright. Everything else about a key column is still compared, which the blanket "never alter a
|
|
175
|
+
// key column" rule these two replace used to hide.
|
|
176
|
+
const generatedType = source.isAutoIncrement && target.isAutoIncrement;
|
|
177
|
+
const impliedNotNull = source.isPrimaryKey && target.isPrimaryKey;
|
|
178
|
+
const typeChanged = !generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type));
|
|
179
|
+
if (typeChanged) {
|
|
142
180
|
differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
|
|
143
181
|
}
|
|
144
|
-
|
|
145
|
-
if (source.nullable !== target.nullable) {
|
|
182
|
+
if (!impliedNotNull && source.nullable !== target.nullable) {
|
|
146
183
|
differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
|
|
147
184
|
}
|
|
148
185
|
// Compare unique constraint
|
|
149
186
|
if (source.isUnique !== target.isUnique) {
|
|
150
187
|
differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
|
|
151
188
|
}
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
189
|
+
// Auto-increment is deliberately not compared. No engine turns a column into an identity, or out of
|
|
190
|
+
// one, without rewriting the table, and there is no DDL here that does it - so a difference could
|
|
191
|
+
// only ever be reported, never settled, and the statements emitted for it (a bare `ALTER COLUMN
|
|
192
|
+
// TYPE`) do not change it. The same rule `describeIndexDifferences` follows for what it cannot read.
|
|
156
193
|
// Compare default values (if both defined)
|
|
157
|
-
if (
|
|
194
|
+
if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
|
|
158
195
|
differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
|
|
159
196
|
}
|
|
160
197
|
if (differences.length === 0) {
|
|
@@ -166,7 +203,10 @@ function diffColumn(tableName, source, target) {
|
|
|
166
203
|
type: 'alter',
|
|
167
204
|
expected: source,
|
|
168
205
|
actual: target,
|
|
169
|
-
|
|
206
|
+
// Only the type this diff actually reports: a column altered for its default carries no data loss,
|
|
207
|
+
// and a generated key's type - never compared above - reads as unsigned against an entity that
|
|
208
|
+
// cannot say so.
|
|
209
|
+
isBreaking: typeChanged && isBreakingTypeChange(target.type, source.type),
|
|
170
210
|
description: differences.join(', '),
|
|
171
211
|
};
|
|
172
212
|
}
|
package/dist/schema/types.d.ts
CHANGED
|
@@ -122,7 +122,7 @@ export interface ColumnNode {
|
|
|
122
122
|
export interface TableNode {
|
|
123
123
|
/**
|
|
124
124
|
* The table's own name, never qualified. Everything derived from a table reads this: an index or
|
|
125
|
-
* constraint name is a single identifier, and `
|
|
125
|
+
* constraint name is a single identifier, and `sales.Order_total_idx` is a syntax error.
|
|
126
126
|
*/
|
|
127
127
|
readonly name: string;
|
|
128
128
|
/**
|
|
@@ -133,8 +133,14 @@ export interface TableNode {
|
|
|
133
133
|
readonly schema?: string;
|
|
134
134
|
/** Map of column name to column node */
|
|
135
135
|
readonly columns: Map<string, ColumnNode>;
|
|
136
|
-
/** Primary key columns (supports composite keys) */
|
|
136
|
+
/** Primary key columns, in key order (supports composite keys) */
|
|
137
137
|
readonly primaryKey: ColumnNode[];
|
|
138
|
+
/**
|
|
139
|
+
* What the constraint is called, where a name is known: read back from the database on an
|
|
140
|
+
* introspected table, absent on one built from entities, where nothing has named it yet. A `DROP`
|
|
141
|
+
* is the only thing that needs it - see {@link TableSchema.primaryKeyName}.
|
|
142
|
+
*/
|
|
143
|
+
primaryKeyName?: string;
|
|
138
144
|
/** Indexes on this table */
|
|
139
145
|
readonly indexes: IndexNode[];
|
|
140
146
|
/** `CHECK` constraints on this table. Optional: a node can be built without ever naming one. */
|
|
@@ -151,7 +157,7 @@ export interface TableNode {
|
|
|
151
157
|
* Represents a foreign key relationship between tables.
|
|
152
158
|
*/
|
|
153
159
|
export interface RelationshipNode {
|
|
154
|
-
/** Constraint name (e.g.,
|
|
160
|
+
/** Constraint name (e.g., posts_author_id_fk) */
|
|
155
161
|
readonly name: string;
|
|
156
162
|
/** Type of relationship */
|
|
157
163
|
readonly type: RelationshipType;
|
|
@@ -202,13 +208,28 @@ export interface SchemaAST {
|
|
|
202
208
|
}
|
|
203
209
|
/**
|
|
204
210
|
* Difference between two column definitions.
|
|
211
|
+
*
|
|
212
|
+
* A union rather than one shape with two optional sides, so which node is present follows from the
|
|
213
|
+
* kind of difference: an added column has only the `expected` one, a dropped column only the
|
|
214
|
+
* `actual` one, and an altered column both. Stated as optionals, every reader had to assert its way
|
|
215
|
+
* past a `undefined` the kind had already ruled out.
|
|
205
216
|
*/
|
|
206
|
-
export
|
|
217
|
+
export type ColumnDiff = ColumnDiffBase & ({
|
|
218
|
+
readonly type: 'add';
|
|
219
|
+
readonly expected: ColumnNode;
|
|
220
|
+
readonly actual?: undefined;
|
|
221
|
+
} | {
|
|
222
|
+
readonly type: 'drop';
|
|
223
|
+
readonly expected?: undefined;
|
|
224
|
+
readonly actual: ColumnNode;
|
|
225
|
+
} | {
|
|
226
|
+
readonly type: 'alter';
|
|
227
|
+
readonly expected: ColumnNode;
|
|
228
|
+
readonly actual: ColumnNode;
|
|
229
|
+
});
|
|
230
|
+
interface ColumnDiffBase {
|
|
207
231
|
readonly table: string;
|
|
208
232
|
readonly column: string;
|
|
209
|
-
readonly type: 'add' | 'drop' | 'alter';
|
|
210
|
-
readonly expected?: ColumnNode;
|
|
211
|
-
readonly actual?: ColumnNode;
|
|
212
233
|
/** Whether this change could cause data loss */
|
|
213
234
|
readonly isBreaking?: boolean;
|
|
214
235
|
readonly description?: string;
|
|
@@ -221,6 +242,21 @@ export interface TableDiff {
|
|
|
221
242
|
readonly type: 'create' | 'drop' | 'alter';
|
|
222
243
|
readonly columnDiffs?: ColumnDiff[];
|
|
223
244
|
readonly indexDiffs?: IndexDiff[];
|
|
245
|
+
/** Set only where the two keys hold different columns. See {@link PrimaryKeyDiff}. */
|
|
246
|
+
readonly primaryKeyDiff?: PrimaryKeyDiff;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Two primary keys that hold different columns.
|
|
250
|
+
*
|
|
251
|
+
* By columns and in order, never by name: `(a, b)` is a different key from `(b, a)`, while the same
|
|
252
|
+
* key called `Member_pkey` on one side and `Member__userId_pk` on the other is one key, not two.
|
|
253
|
+
*/
|
|
254
|
+
export interface PrimaryKeyDiff {
|
|
255
|
+
readonly table: string;
|
|
256
|
+
readonly expected: string[];
|
|
257
|
+
readonly actual: string[];
|
|
258
|
+
/** What the *actual* side calls its constraint, which is the only name a `DROP` can use. */
|
|
259
|
+
readonly actualName?: string;
|
|
224
260
|
}
|
|
225
261
|
/**
|
|
226
262
|
* Difference between two index definitions.
|
|
@@ -258,6 +294,8 @@ export interface SchemaDiffResult {
|
|
|
258
294
|
readonly columnDiffs: ColumnDiff[];
|
|
259
295
|
/** All index diffs */
|
|
260
296
|
readonly indexDiffs: IndexDiff[];
|
|
297
|
+
/** Every table whose primary key holds different columns than the entity declares. */
|
|
298
|
+
readonly primaryKeyDiffs: PrimaryKeyDiff[];
|
|
261
299
|
/** All relationship/FK diffs */
|
|
262
300
|
readonly relationshipDiffs: RelationshipDiff[];
|
|
263
301
|
/** Whether there are any differences */
|
|
@@ -321,3 +359,4 @@ export interface DriftReport {
|
|
|
321
359
|
readonly info: number;
|
|
322
360
|
};
|
|
323
361
|
}
|
|
362
|
+
export {};
|
|
@@ -5,7 +5,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
5
5
|
protected readonly featureDefaults: DialectFeatures;
|
|
6
6
|
readonly dialectName = "sqlite";
|
|
7
7
|
readonly escapeIdChar = "`";
|
|
8
|
-
readonly
|
|
8
|
+
readonly serialType = "INTEGER PRIMARY KEY AUTOINCREMENT";
|
|
9
|
+
readonly serialDeclaresPrimaryKey = true;
|
|
9
10
|
readonly tableOptions = "";
|
|
10
11
|
readonly beginTransactionCommand = "BEGIN TRANSACTION";
|
|
11
12
|
readonly commitTransactionCommand = "COMMIT";
|
|
@@ -13,6 +13,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
13
13
|
dropTableCascade: false,
|
|
14
14
|
renameColumn: true,
|
|
15
15
|
foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
|
|
16
|
+
primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
|
|
16
17
|
columnComment: false, // SQLite does not support column comments
|
|
17
18
|
vectorIndexRequiresNotNull: false,
|
|
18
19
|
vectorSupportsLength: false,
|
|
@@ -21,7 +22,9 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
21
22
|
};
|
|
22
23
|
dialectName = 'sqlite';
|
|
23
24
|
escapeIdChar = '`';
|
|
24
|
-
|
|
25
|
+
serialType = 'INTEGER PRIMARY KEY AUTOINCREMENT';
|
|
26
|
+
// `AUTOINCREMENT` is only legal in that exact phrase, so the key cannot be lifted to table level.
|
|
27
|
+
serialDeclaresPrimaryKey = true;
|
|
25
28
|
tableOptions = '';
|
|
26
29
|
beginTransactionCommand = 'BEGIN TRANSACTION';
|
|
27
30
|
commitTransactionCommand = 'COMMIT';
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -91,6 +91,12 @@ export interface EngineFeatures {
|
|
|
91
91
|
readonly dropTableCascade: boolean;
|
|
92
92
|
readonly renameColumn: boolean;
|
|
93
93
|
readonly foreignKeyAlter: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Whether a table's primary key can be changed on an existing table. False on SQLite, whose only
|
|
96
|
+
* route is rebuilding the table - so a migration that would change one is refused by name rather
|
|
97
|
+
* than emitting DDL the engine rejects.
|
|
98
|
+
*/
|
|
99
|
+
readonly primaryKeyAlter: boolean;
|
|
94
100
|
/** Whether the dialect supports inline COMMENT on columns (MySQL/MariaDB). */
|
|
95
101
|
readonly columnComment: boolean;
|
|
96
102
|
/**
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -49,6 +49,8 @@ export type RelationKey<E> = Exclude<Key<E>, FieldKey<E> | MethodKey<E>>;
|
|
|
49
49
|
* inferring junk like `T = string`), while the marker key only exists on branded types.
|
|
50
50
|
*/
|
|
51
51
|
type IsJson<T> = '__json' extends keyof T ? true : false;
|
|
52
|
+
/** Whether `T` is what a JSON column holds: the branded payload, or an array of them. */
|
|
53
|
+
type IsJsonColumn<T> = IsJson<T> extends true ? true : IsJson<NonNullable<Unpacked<T>>>;
|
|
52
54
|
/** The payload `P` of a branded `Json<P>`, or `never` for any non-JSON type. */
|
|
53
55
|
type UnwrapJson<T> = IsJson<T> extends true ? (T extends Json<infer P> ? P : never) : never;
|
|
54
56
|
/**
|
|
@@ -276,7 +278,7 @@ export type FieldType = StringConstructor | NumberConstructor | BooleanConstruct
|
|
|
276
278
|
* Both `Json<T>` and `Json<T>[]` have to be recognised, and the array check has to precede the scalar
|
|
277
279
|
* arms so a `number[]` vector is not read as a `number`.
|
|
278
280
|
*/
|
|
279
|
-
export type TypeFor<V, T = NonNullable<V>> =
|
|
281
|
+
export type TypeFor<V, T = NonNullable<V>> = IsJsonColumn<T> extends true ? JsonColumnType : T extends readonly number[] ? VectorColumnType : T extends string ? StringConstructor | StringColumnType : T extends number ? NumberConstructor | NumericColumnType : T extends bigint ? BigIntConstructor | NumericColumnType : T extends boolean ? BooleanConstructor | BooleanColumnType : T extends Date ? DateConstructor | DateColumnType : T extends Uint8Array ? BlobColumnType : FieldType;
|
|
280
282
|
/**
|
|
281
283
|
* A field as the registry holds it: what the user authored, plus what registration worked out.
|
|
282
284
|
*
|
|
@@ -384,11 +386,9 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
|
|
|
384
386
|
*/
|
|
385
387
|
readonly unique?: boolean;
|
|
386
388
|
/**
|
|
387
|
-
* The column's DDL default, rendered into `CREATE TABLE` by `formatDefaultValue
|
|
388
|
-
* the generators above. A JSONB column defaults with the SQL literal it stores, `defaultValue: '{}'`,
|
|
389
|
-
* which is a string whatever the field's TypeScript type is.
|
|
389
|
+
* The column's DDL default, rendered into `CREATE TABLE` by `formatDefaultValue`.
|
|
390
390
|
*/
|
|
391
|
-
readonly defaultValue?:
|
|
391
|
+
readonly defaultValue?: DdlDefault<V>;
|
|
392
392
|
/**
|
|
393
393
|
* Whether the column is auto-incrementing (for integer IDs).
|
|
394
394
|
*/
|
|
@@ -403,6 +403,17 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
|
|
|
403
403
|
readonly comment?: string;
|
|
404
404
|
};
|
|
405
405
|
export type OnFieldCallback<V = TsTypeOf<FieldType>> = V | QueryRaw | (() => V | QueryRaw);
|
|
406
|
+
/**
|
|
407
|
+
* What a column may default to: the value it holds, except on a JSON column, which defaults with the
|
|
408
|
+
* SQL literal it stores (`defaultValue: '{}'`) whatever the property's TypeScript type is. Opening
|
|
409
|
+
* that exception to every field is what let `@Field({ type: Number, defaultValue: 'hello' })` compile.
|
|
410
|
+
*
|
|
411
|
+
* The erased shape - `FieldOptions` with no field in mind - admits every column's default at once, or
|
|
412
|
+
* no `FieldOptions<V>` would be assignable to the one the registry and the dialects read.
|
|
413
|
+
*/
|
|
414
|
+
type DdlDefault<V, T = NonNullable<V>> = IsJsonColumn<T> extends true ? JsonDdlDefault : [TsTypeOf<FieldType>] extends [T] ? JsonDdlDefault | T : T;
|
|
415
|
+
/** What a JSON column, and the field-less `FieldOptions`, may default to. */
|
|
416
|
+
type JsonDdlDefault = Scalar | Record<string, unknown>;
|
|
406
417
|
/**
|
|
407
418
|
* The TypeScript types a field may be declared as, given the `type` it registers: the inverse of
|
|
408
419
|
* {@link TypeFor}.
|
|
@@ -760,7 +771,13 @@ export type EntityMeta<E> = {
|
|
|
760
771
|
checks?: CheckSchema[];
|
|
761
772
|
/** Lifecycle hooks registered via @BeforeInsert, @AfterUpdate, etc. */
|
|
762
773
|
hooks?: Partial<Record<HookEvent, HookRegistration[]>>;
|
|
763
|
-
|
|
774
|
+
/**
|
|
775
|
+
* Bumped by every `define*` call, so anything derived from this metadata can tell that it changed.
|
|
776
|
+
* A content type registered at runtime keeps adding to an entity that has already been read.
|
|
777
|
+
*/
|
|
778
|
+
revision: number;
|
|
779
|
+
/** The revision `getMeta` last finalized, which is what makes finalizing idempotent and re-entrant. */
|
|
780
|
+
processedAt?: number;
|
|
764
781
|
};
|
|
765
782
|
/**
|
|
766
783
|
* Configurable options for an entity (`@Entity()` / `defineEntity`).
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -123,7 +123,14 @@ export interface ColumnSchema {
|
|
|
123
123
|
export interface TableSchema {
|
|
124
124
|
readonly name: string;
|
|
125
125
|
readonly columns: ColumnSchema[];
|
|
126
|
+
/** The key's columns **in order**, which is what a composite is: `(a, b)` is not `(b, a)`. */
|
|
126
127
|
readonly primaryKey?: string[];
|
|
128
|
+
/**
|
|
129
|
+
* What the engine calls the key's constraint, where it names one at all - Postgres's `Member_pkey`,
|
|
130
|
+
* MySQL's literal `PRIMARY`, nothing on SQLite. Only a `DROP` needs it, and only the name the
|
|
131
|
+
* database actually reported will do: a derived one would name a constraint that is not there.
|
|
132
|
+
*/
|
|
133
|
+
readonly primaryKeyName?: string;
|
|
127
134
|
readonly indexes?: IndexSchema[];
|
|
128
135
|
readonly foreignKeys?: ForeignKeySchema[];
|
|
129
136
|
}
|
|
@@ -176,6 +183,18 @@ export interface SchemaDiff {
|
|
|
176
183
|
*/
|
|
177
184
|
readonly schema?: string;
|
|
178
185
|
readonly type: 'create' | 'alter' | 'drop';
|
|
186
|
+
/**
|
|
187
|
+
* The key the table has against the key the entity declares, set only when they differ.
|
|
188
|
+
*
|
|
189
|
+
* Compared by columns, never by name: the engine named the existing one, so requiring a derived
|
|
190
|
+
* name to match would rewrite the primary key of every table on the first migration after
|
|
191
|
+
* upgrading. `fromName` is what the database reported, and the only name a `DROP` can use.
|
|
192
|
+
*/
|
|
193
|
+
readonly primaryKey?: {
|
|
194
|
+
readonly from: string[];
|
|
195
|
+
readonly to: string[];
|
|
196
|
+
readonly fromName?: string;
|
|
197
|
+
};
|
|
179
198
|
readonly columnsToAdd?: ColumnSchema[];
|
|
180
199
|
readonly columnsToAlter?: {
|
|
181
200
|
from: ColumnSchema;
|
|
@@ -243,7 +262,7 @@ export interface SchemaGenerator {
|
|
|
243
262
|
/**
|
|
244
263
|
* Get the SQL type for a field based on its options
|
|
245
264
|
*/
|
|
246
|
-
getSqlType(fieldOptions: FieldOptions
|
|
265
|
+
getSqlType(fieldOptions: FieldOptions): string;
|
|
247
266
|
/**
|
|
248
267
|
* Compare an entity with a database table node and return the differences.
|
|
249
268
|
*/
|
package/dist/type/query.d.ts
CHANGED
|
@@ -394,12 +394,8 @@ type CountedRelations<C extends PropertyKey> = [C] extends [never] ? unknown : {
|
|
|
394
394
|
};
|
|
395
395
|
};
|
|
396
396
|
/** @internal */
|
|
397
|
-
type QueryProjectedRow<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = [S | X] extends [never] ? E : IsUniform<V> extends true ? [PopulatedToMany<E, P>] extends [never] ?
|
|
398
|
-
|
|
399
|
-
} : // A populated to-many is always a list, empty where the parent has no children, so it maps
|
|
400
|
-
{
|
|
401
|
-
[K in keyof E as K extends Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> ? K : never]: E[K];
|
|
402
|
-
} & {
|
|
397
|
+
type QueryProjectedRow<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = [S | X] extends [never] ? E : IsUniform<V> extends true ? [PopulatedToMany<E, P>] extends [never] ? Pick<E, ProjectedKeys<E, S, V, X, P, C> & keyof E> : // A populated to-many is always a list, empty where the parent has no children, so it maps
|
|
398
|
+
Pick<E, Exclude<ProjectedKeys<E, S, V, X, P, C>, PopulatedToMany<E, P>> & keyof E> & {
|
|
403
399
|
[K in PopulatedToMany<E, P>]-?: NonNullable<E[K]>;
|
|
404
400
|
} : E;
|
|
405
401
|
/** The to-many relations a query populated, which come back as lists rather than as optional ones. */
|
|
@@ -1,16 +1,37 @@
|
|
|
1
|
-
import type { FieldOptions } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, FieldOptions } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* The kind of column a field lands on, which is what decides whether an option means anything on it:
|
|
4
|
+
* `length` is a string's, `precision` a number's, `dimensions` a vector's. Named in the words an
|
|
5
|
+
* error reports it in, so there is no second table of labels to keep in step.
|
|
4
6
|
*/
|
|
5
|
-
export
|
|
7
|
+
export type ColumnFamily = 'string' | 'numeric' | 'boolean' | 'date' | 'json' | 'blob' | 'vector';
|
|
6
8
|
/**
|
|
7
|
-
*
|
|
9
|
+
* The runtime half of the column-type unions in `type/entity.ts`, which TypeScript erases. Each list
|
|
10
|
+
* is checked against its own union, so a type cannot be filed under the wrong family, and
|
|
11
|
+
* {@link UnplacedColumnType} refuses to compile if a new one is filed under none. Nothing here
|
|
12
|
+
* restates the unions: the compile-time side of the same question reads them directly.
|
|
8
13
|
*/
|
|
9
|
-
export declare
|
|
14
|
+
export declare const COLUMN_TYPES_BY_FAMILY: {
|
|
15
|
+
readonly numeric: readonly ["int", "integer", "tinyint", "smallint", "bigint", "float", "float4", "float8", "double", "double precision", "decimal", "numeric", "real", "serial", "smallserial", "bigserial"];
|
|
16
|
+
readonly string: readonly ["char", "varchar", "text", "uuid"];
|
|
17
|
+
readonly date: readonly ["date", "time", "datetime", "timestamp", "timestamptz"];
|
|
18
|
+
readonly json: readonly ["json", "jsonb"];
|
|
19
|
+
readonly blob: readonly ["blob", "bytea"];
|
|
20
|
+
readonly boolean: readonly ["bool", "boolean"];
|
|
21
|
+
readonly vector: readonly ["vector", "halfvec", "sparsevec"];
|
|
22
|
+
};
|
|
23
|
+
/** The family of a logical field type, or `undefined` where it names none. */
|
|
24
|
+
export declare function columnFamily(type: unknown): ColumnFamily | undefined;
|
|
10
25
|
/**
|
|
11
|
-
*
|
|
26
|
+
* Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
|
|
27
|
+
* and the only one that may state `PRIMARY KEY` in its own column definition.
|
|
28
|
+
*
|
|
29
|
+
* One column of a composite is a value the caller supplies, and the table states the key over every
|
|
30
|
+
* column at once. Asked in one place because the two schema paths - the AST that builds a
|
|
31
|
+
* `CREATE TABLE` and the diff that builds an `ALTER` - have to answer it the same way, and each
|
|
32
|
+
* answering for itself is what put a serial `PRIMARY KEY` on both columns of a composite.
|
|
12
33
|
*/
|
|
13
|
-
export declare function
|
|
34
|
+
export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOptions): boolean;
|
|
14
35
|
/**
|
|
15
36
|
* Checks if a field should be treated as auto-incrementing.
|
|
16
37
|
*/
|
package/dist/util/field.util.js
CHANGED
|
@@ -1,56 +1,65 @@
|
|
|
1
|
-
|
|
2
|
-
int: true,
|
|
3
|
-
integer: true,
|
|
4
|
-
tinyint: true,
|
|
5
|
-
smallint: true,
|
|
6
|
-
bigint: true,
|
|
7
|
-
float: true,
|
|
8
|
-
float4: true,
|
|
9
|
-
float8: true,
|
|
10
|
-
double: true,
|
|
11
|
-
'double precision': true,
|
|
12
|
-
decimal: true,
|
|
13
|
-
numeric: true,
|
|
14
|
-
real: true,
|
|
15
|
-
serial: true,
|
|
16
|
-
smallserial: true,
|
|
17
|
-
bigserial: true,
|
|
18
|
-
};
|
|
19
|
-
const JSON_COLUMN_TYPES = {
|
|
20
|
-
json: true,
|
|
21
|
-
jsonb: true,
|
|
22
|
-
};
|
|
1
|
+
import { getKeys } from './object.util.js';
|
|
23
2
|
/**
|
|
24
|
-
*
|
|
3
|
+
* The runtime half of the column-type unions in `type/entity.ts`, which TypeScript erases. Each list
|
|
4
|
+
* is checked against its own union, so a type cannot be filed under the wrong family, and
|
|
5
|
+
* {@link UnplacedColumnType} refuses to compile if a new one is filed under none. Nothing here
|
|
6
|
+
* restates the unions: the compile-time side of the same question reads them directly.
|
|
25
7
|
*/
|
|
26
|
-
export
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
8
|
+
export const COLUMN_TYPES_BY_FAMILY = {
|
|
9
|
+
numeric: [
|
|
10
|
+
'int',
|
|
11
|
+
'integer',
|
|
12
|
+
'tinyint',
|
|
13
|
+
'smallint',
|
|
14
|
+
'bigint',
|
|
15
|
+
'float',
|
|
16
|
+
'float4',
|
|
17
|
+
'float8',
|
|
18
|
+
'double',
|
|
19
|
+
'double precision',
|
|
20
|
+
'decimal',
|
|
21
|
+
'numeric',
|
|
22
|
+
'real',
|
|
23
|
+
'serial',
|
|
24
|
+
'smallserial',
|
|
25
|
+
'bigserial',
|
|
26
|
+
],
|
|
27
|
+
string: ['char', 'varchar', 'text', 'uuid'],
|
|
28
|
+
date: ['date', 'time', 'datetime', 'timestamp', 'timestamptz'],
|
|
29
|
+
json: ['json', 'jsonb'],
|
|
30
|
+
blob: ['blob', 'bytea'],
|
|
31
|
+
boolean: ['bool', 'boolean'],
|
|
32
|
+
vector: ['vector', 'halfvec', 'sparsevec'],
|
|
33
|
+
};
|
|
34
|
+
// Constructors and type strings in one map: a logical type is either, and every caller asks the same
|
|
35
|
+
// question of both.
|
|
36
|
+
const FAMILY_OF = new Map([
|
|
37
|
+
[String, 'string'],
|
|
38
|
+
[Number, 'numeric'],
|
|
39
|
+
[BigInt, 'numeric'],
|
|
40
|
+
[Boolean, 'boolean'],
|
|
41
|
+
[Date, 'date'],
|
|
42
|
+
]);
|
|
43
|
+
for (const family of getKeys(COLUMN_TYPES_BY_FAMILY)) {
|
|
44
|
+
for (const columnType of COLUMN_TYPES_BY_FAMILY[family]) {
|
|
45
|
+
FAMILY_OF.set(columnType, family);
|
|
31
46
|
}
|
|
32
|
-
return false;
|
|
33
47
|
}
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
export function isBooleanType(type) {
|
|
38
|
-
if (type === Boolean)
|
|
39
|
-
return true;
|
|
40
|
-
if (typeof type === 'string') {
|
|
41
|
-
const lowered = type.toLowerCase();
|
|
42
|
-
return lowered === 'bool' || lowered === 'boolean';
|
|
43
|
-
}
|
|
44
|
-
return false;
|
|
48
|
+
/** The family of a logical field type, or `undefined` where it names none. */
|
|
49
|
+
export function columnFamily(type) {
|
|
50
|
+
return FAMILY_OF.get(typeof type === 'string' ? type.toLowerCase() : type);
|
|
45
51
|
}
|
|
46
52
|
/**
|
|
47
|
-
*
|
|
53
|
+
* Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
|
|
54
|
+
* and the only one that may state `PRIMARY KEY` in its own column definition.
|
|
55
|
+
*
|
|
56
|
+
* One column of a composite is a value the caller supplies, and the table states the key over every
|
|
57
|
+
* column at once. Asked in one place because the two schema paths - the AST that builds a
|
|
58
|
+
* `CREATE TABLE` and the diff that builds an `ALTER` - have to answer it the same way, and each
|
|
59
|
+
* answering for itself is what put a serial `PRIMARY KEY` on both columns of a composite.
|
|
48
60
|
*/
|
|
49
|
-
export function
|
|
50
|
-
|
|
51
|
-
return type.toLowerCase() in JSON_COLUMN_TYPES;
|
|
52
|
-
}
|
|
53
|
-
return false;
|
|
61
|
+
export function isSoleIdField(meta, field) {
|
|
62
|
+
return field.isId === true && meta.ids.length === 1;
|
|
54
63
|
}
|
|
55
64
|
/**
|
|
56
65
|
* Checks if a field should be treated as auto-incrementing.
|
|
@@ -63,6 +72,5 @@ export function isAutoIncrement(field, isPrimaryKey) {
|
|
|
63
72
|
const colType = field.columnType?.toLowerCase();
|
|
64
73
|
if (colType === 'serial' || colType === 'smallserial' || colType === 'bigserial')
|
|
65
74
|
return true;
|
|
66
|
-
|
|
67
|
-
return isPrimaryKey && isNumeric && !field.onInsert && !field.columnType;
|
|
75
|
+
return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.columnType;
|
|
68
76
|
}
|