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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
2
|
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
4
|
-
import { asSelectMap, getKeys, parentJoins,
|
|
4
|
+
import { asSelectMap, getKeys, joinedColumns, joinedRowKey, parentJoins, parentRowKey, parentsIn, targetKeyColumns, } from '../util/index.js';
|
|
5
5
|
/**
|
|
6
6
|
* A `$count` groups its tallies by the parent's id, so the id has to outlive the projection - the
|
|
7
7
|
* same reason populating a relation keeps it. A whitelisting `$select` gains the key and an
|
|
@@ -50,6 +50,9 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
const meta = getMeta(entity);
|
|
53
|
+
// The tallies come back keyed by the columns *this relation* joins from, so its `joins` are kept
|
|
54
|
+
// beside them: reading the parent through `meta.ids` instead matches only where the two coincide,
|
|
55
|
+
// which is a to-many and nothing else.
|
|
53
56
|
const counted = new Map();
|
|
54
57
|
for (const relKey of getKeys(count)) {
|
|
55
58
|
const value = count[relKey];
|
|
@@ -59,15 +62,14 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
59
62
|
}
|
|
60
63
|
const where = typeof value === 'object' ? value.$where : undefined;
|
|
61
64
|
const joins = parentJoins(relOpts, meta.ids.length);
|
|
62
|
-
counted.set(relKey, await countPerParent(querier, relOpts, joins, payload, where));
|
|
65
|
+
counted.set(relKey, { joins, byParent: await countPerParent(querier, relOpts, joins, payload, where) });
|
|
63
66
|
}
|
|
64
67
|
for (const parent of payload) {
|
|
65
|
-
const id = rowKey(meta.ids.map((key) => parent[key]));
|
|
66
68
|
const row = {};
|
|
67
|
-
for (const [relKey, byParent] of counted) {
|
|
69
|
+
for (const [relKey, { joins, byParent }] of counted) {
|
|
68
70
|
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
69
71
|
// gap: `_count` names what the caller asked to count, so every key it asked for is present.
|
|
70
|
-
row[relKey] = byParent[
|
|
72
|
+
row[relKey] = byParent[parentRowKey(joins, parent)] ?? 0;
|
|
71
73
|
}
|
|
72
74
|
parent[COUNT_RESULT_KEY] = row;
|
|
73
75
|
}
|
|
@@ -101,13 +103,13 @@ async function countThroughPerParent(querier, relOpts, throughEntity, joins, par
|
|
|
101
103
|
/** `SELECT <keys>, COUNT(*) ... GROUP BY <keys>`, as a lookup from parent key to tally. */
|
|
102
104
|
async function groupedCount(querier, entity, joins, where) {
|
|
103
105
|
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
104
|
-
const $group =
|
|
106
|
+
const $group = joinedColumns(joins);
|
|
105
107
|
const rows = await querier.aggregate(entity, { $group, $agg, $where: where });
|
|
106
108
|
const byParent = {};
|
|
107
109
|
for (const row of rows) {
|
|
108
110
|
// Keyed by every joined column, which is how a tally finds the one parent whose whole key it
|
|
109
111
|
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
110
|
-
byParent[
|
|
112
|
+
byParent[joinedRowKey(joins, row)] = Number(row[COUNT_ALIAS]);
|
|
111
113
|
}
|
|
112
114
|
return byParent;
|
|
113
115
|
}
|
|
@@ -36,17 +36,32 @@ export declare function canonicalToSql(type: CanonicalType, dialect: AbstractDia
|
|
|
36
36
|
* Convert a canonical type to a TypeScript type string.
|
|
37
37
|
*/
|
|
38
38
|
export declare function canonicalToTypeScript(type: CanonicalType): string;
|
|
39
|
+
/**
|
|
40
|
+
* A type as `dialect` would actually store it: rendered to that engine's SQL and read back.
|
|
41
|
+
*
|
|
42
|
+
* Several canonical types share one storage type per engine - a `boolean` is `TINYINT(1)` on MySQL and
|
|
43
|
+
* `INTEGER` on SQLite - and only the engine settles an unstated bound, since `VARCHAR` is 255 on MySQL
|
|
44
|
+
* and `TEXT` on Postgres. Both paths that diff a schema compare through this, so a migration and a
|
|
45
|
+
* drift report cannot disagree about what changed.
|
|
46
|
+
*/
|
|
47
|
+
export declare function engineType(dialect: AbstractDialect): (type: CanonicalType) => CanonicalType;
|
|
39
48
|
/**
|
|
40
49
|
* Convert UQL FieldOptions to a canonical type.
|
|
41
50
|
*/
|
|
42
|
-
export declare function fieldOptionsToCanonical(options: FieldOptions
|
|
51
|
+
export declare function fieldOptionsToCanonical(options: FieldOptions): CanonicalType;
|
|
43
52
|
/**
|
|
44
|
-
* Compare two canonical types for equality.
|
|
45
|
-
*
|
|
53
|
+
* Compare two canonical types for equality. Used for schema diffing.
|
|
54
|
+
*
|
|
55
|
+
* Nothing here guesses an engine's own default for an unstated bound: `VARCHAR` is 255 on MySQL and
|
|
56
|
+
* `TEXT` on Postgres, and a comparison that assumed either was blind to that difference on the other.
|
|
57
|
+
* `DiffOptions.normalizeType` is what settles it, by putting both sides through the engine first.
|
|
46
58
|
*/
|
|
47
59
|
export declare function areTypesEqual(a: CanonicalType, b: CanonicalType): boolean;
|
|
48
60
|
/**
|
|
49
|
-
*
|
|
61
|
+
* Whether changing a column from one type to the other can lose what it holds.
|
|
62
|
+
*
|
|
63
|
+
* An unstated bound is the engine's widest, so stating one for the first time narrows the column just
|
|
64
|
+
* as lowering one does: `TEXT` to `VARCHAR(50)` truncates, and `NUMERIC` to `NUMERIC(5,2)` rounds.
|
|
50
65
|
*/
|
|
51
66
|
export declare function isBreakingTypeChange(from: CanonicalType, to: CanonicalType): boolean;
|
|
52
67
|
/**
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Canonical types (dialect-agnostic)
|
|
7
7
|
* - TypeScript types (for entity generation)
|
|
8
8
|
*/
|
|
9
|
+
import { columnFamily } from '../util/field.util.js';
|
|
9
10
|
/** Whether a category is one of the vector types, narrowing it to the cast pgvector names use. */
|
|
10
11
|
export function isVectorCategory(category) {
|
|
11
12
|
return category === 'vector' || category === 'halfvec' || category === 'sparsevec';
|
|
@@ -108,66 +109,57 @@ const PG_TYPE_MAP = {
|
|
|
108
109
|
halfvec: 'HALFVEC',
|
|
109
110
|
sparsevec: 'SPARSEVEC',
|
|
110
111
|
};
|
|
112
|
+
const MYSQL_SCALAR_MAP = {
|
|
113
|
+
integer: 'INT',
|
|
114
|
+
float: 'FLOAT',
|
|
115
|
+
decimal: 'DECIMAL',
|
|
116
|
+
string: 'VARCHAR',
|
|
117
|
+
boolean: 'TINYINT(1)',
|
|
118
|
+
date: 'DATE',
|
|
119
|
+
time: 'TIME',
|
|
120
|
+
timestamp: 'DATETIME',
|
|
121
|
+
json: 'JSON',
|
|
122
|
+
uuid: 'CHAR(36)',
|
|
123
|
+
blob: 'BLOB',
|
|
124
|
+
};
|
|
125
|
+
const SQLITE_SCALAR_MAP = {
|
|
126
|
+
integer: 'INTEGER',
|
|
127
|
+
float: 'REAL',
|
|
128
|
+
decimal: 'REAL',
|
|
129
|
+
string: 'TEXT',
|
|
130
|
+
boolean: 'INTEGER',
|
|
131
|
+
date: 'TEXT',
|
|
132
|
+
time: 'TEXT',
|
|
133
|
+
timestamp: 'TEXT',
|
|
134
|
+
json: 'TEXT',
|
|
135
|
+
uuid: 'TEXT',
|
|
136
|
+
blob: 'BLOB',
|
|
137
|
+
};
|
|
138
|
+
/** MongoDB uses BSON types, not SQL types. These are placeholders for compatibility. */
|
|
139
|
+
const MONGO_SCALAR_MAP = {
|
|
140
|
+
integer: 'int',
|
|
141
|
+
float: 'double',
|
|
142
|
+
decimal: 'decimal128',
|
|
143
|
+
string: 'string',
|
|
144
|
+
boolean: 'bool',
|
|
145
|
+
date: 'date',
|
|
146
|
+
time: 'string',
|
|
147
|
+
timestamp: 'timestamp',
|
|
148
|
+
json: 'object',
|
|
149
|
+
uuid: 'binData',
|
|
150
|
+
blob: 'binData',
|
|
151
|
+
};
|
|
152
|
+
/** Every engine's scalars, and the one spelling it gives all three vector widths. */
|
|
111
153
|
const CANONICAL_TO_SQL = {
|
|
112
154
|
postgres: PG_TYPE_MAP,
|
|
113
155
|
// CockroachDB's VECTOR is native, no extension needed.
|
|
114
156
|
cockroachdb: withVectorType(PG_SCALAR_MAP, 'VECTOR'),
|
|
115
|
-
mysql: withVectorType({
|
|
116
|
-
integer: 'INT',
|
|
117
|
-
float: 'FLOAT',
|
|
118
|
-
decimal: 'DECIMAL',
|
|
119
|
-
string: 'VARCHAR',
|
|
120
|
-
boolean: 'TINYINT(1)',
|
|
121
|
-
date: 'DATE',
|
|
122
|
-
time: 'TIME',
|
|
123
|
-
timestamp: 'DATETIME',
|
|
124
|
-
json: 'JSON',
|
|
125
|
-
uuid: 'CHAR(36)',
|
|
126
|
-
blob: 'BLOB',
|
|
127
|
-
},
|
|
128
157
|
// MySQL does have a `VECTOR` type (26.7), but no distance function outside HeatWave and no vector
|
|
129
158
|
// index, so JSON keeps the column queryable with the JSON operators and needs no conversion.
|
|
130
|
-
'JSON'),
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
decimal: 'REAL',
|
|
135
|
-
string: 'TEXT',
|
|
136
|
-
boolean: 'INTEGER',
|
|
137
|
-
date: 'TEXT',
|
|
138
|
-
time: 'TEXT',
|
|
139
|
-
timestamp: 'TEXT',
|
|
140
|
-
json: 'TEXT',
|
|
141
|
-
uuid: 'TEXT',
|
|
142
|
-
blob: 'BLOB',
|
|
143
|
-
}, 'TEXT'),
|
|
144
|
-
mariadb: withVectorType({
|
|
145
|
-
integer: 'INT',
|
|
146
|
-
float: 'FLOAT',
|
|
147
|
-
decimal: 'DECIMAL',
|
|
148
|
-
string: 'VARCHAR',
|
|
149
|
-
boolean: 'TINYINT(1)',
|
|
150
|
-
date: 'DATE',
|
|
151
|
-
time: 'TIME',
|
|
152
|
-
timestamp: 'DATETIME',
|
|
153
|
-
json: 'JSON',
|
|
154
|
-
uuid: 'CHAR(36)',
|
|
155
|
-
blob: 'BLOB',
|
|
156
|
-
}, 'VECTOR'),
|
|
157
|
-
// MongoDB uses BSON types, not SQL types. These are placeholders for compatibility.
|
|
158
|
-
mongodb: withVectorType({
|
|
159
|
-
integer: 'int',
|
|
160
|
-
float: 'double',
|
|
161
|
-
decimal: 'decimal128',
|
|
162
|
-
string: 'string',
|
|
163
|
-
boolean: 'bool',
|
|
164
|
-
date: 'date',
|
|
165
|
-
time: 'string',
|
|
166
|
-
timestamp: 'timestamp',
|
|
167
|
-
json: 'object',
|
|
168
|
-
uuid: 'binData',
|
|
169
|
-
blob: 'binData',
|
|
170
|
-
}, 'array'),
|
|
159
|
+
mysql: withVectorType(MYSQL_SCALAR_MAP, 'JSON'),
|
|
160
|
+
mariadb: withVectorType(MYSQL_SCALAR_MAP, 'VECTOR'),
|
|
161
|
+
sqlite: withVectorType(SQLITE_SCALAR_MAP, 'TEXT'),
|
|
162
|
+
mongodb: withVectorType(MONGO_SCALAR_MAP, 'array'),
|
|
171
163
|
};
|
|
172
164
|
/**
|
|
173
165
|
* Size variant modifiers for SQL types.
|
|
@@ -239,7 +231,7 @@ const CANONICAL_TO_TS = {
|
|
|
239
231
|
timestamp: 'Date',
|
|
240
232
|
json: 'unknown',
|
|
241
233
|
uuid: 'string',
|
|
242
|
-
blob: '
|
|
234
|
+
blob: 'Uint8Array',
|
|
243
235
|
vector: 'number[]',
|
|
244
236
|
halfvec: 'number[]',
|
|
245
237
|
sparsevec: 'number[]',
|
|
@@ -387,136 +379,94 @@ function formatDecimalSqlType(type, dialect, baseType) {
|
|
|
387
379
|
export function canonicalToTypeScript(type) {
|
|
388
380
|
return CANONICAL_TO_TS[type.category];
|
|
389
381
|
}
|
|
382
|
+
/**
|
|
383
|
+
* A type as `dialect` would actually store it: rendered to that engine's SQL and read back.
|
|
384
|
+
*
|
|
385
|
+
* Several canonical types share one storage type per engine - a `boolean` is `TINYINT(1)` on MySQL and
|
|
386
|
+
* `INTEGER` on SQLite - and only the engine settles an unstated bound, since `VARCHAR` is 255 on MySQL
|
|
387
|
+
* and `TEXT` on Postgres. Both paths that diff a schema compare through this, so a migration and a
|
|
388
|
+
* drift report cannot disagree about what changed.
|
|
389
|
+
*/
|
|
390
|
+
export function engineType(dialect) {
|
|
391
|
+
return (type) => sqlToCanonical(canonicalToSql(type, dialect));
|
|
392
|
+
}
|
|
390
393
|
/**
|
|
391
394
|
* Convert UQL FieldOptions to a canonical type.
|
|
392
395
|
*/
|
|
393
|
-
export function fieldOptionsToCanonical(options
|
|
394
|
-
//
|
|
396
|
+
export function fieldOptionsToCanonical(options) {
|
|
397
|
+
// An explicit column type is read exactly as an introspected one is: the SQL type, plus whatever
|
|
398
|
+
// bounds are stated beside it.
|
|
395
399
|
if (options.columnType) {
|
|
396
|
-
|
|
397
|
-
return {
|
|
398
|
-
...base,
|
|
399
|
-
length: options.length ?? base.length,
|
|
400
|
-
precision: options.precision ?? base.precision,
|
|
401
|
-
scale: options.scale ?? base.scale,
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
// Infer from type (could be constructor or string)
|
|
405
|
-
const type = options.type || tsType;
|
|
406
|
-
if (type === String) {
|
|
407
|
-
return {
|
|
408
|
-
category: 'string',
|
|
409
|
-
length: options.length,
|
|
410
|
-
};
|
|
411
|
-
}
|
|
412
|
-
if (type === Number) {
|
|
413
|
-
if (options.precision || options.scale) {
|
|
414
|
-
return {
|
|
415
|
-
category: 'decimal',
|
|
416
|
-
precision: options.precision,
|
|
417
|
-
scale: options.scale,
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
|
-
// BIGINT for every `Number`, key or not: a 32-bit column is a migration waiting to happen, and
|
|
421
|
-
// the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
|
|
422
|
-
return { category: 'integer', size: 'big' };
|
|
423
|
-
}
|
|
424
|
-
if (type === Boolean) {
|
|
425
|
-
return { category: 'boolean' };
|
|
426
|
-
}
|
|
427
|
-
if (type === Date) {
|
|
428
|
-
return { category: 'timestamp' };
|
|
429
|
-
}
|
|
430
|
-
if (type === BigInt) {
|
|
431
|
-
return { category: 'integer', size: 'big' };
|
|
400
|
+
return canonicalColumnType(options.columnType, options);
|
|
432
401
|
}
|
|
402
|
+
// Infer from type, which is a SQL type string or one of the constructors.
|
|
403
|
+
const { type } = options;
|
|
433
404
|
if (typeof type === 'string') {
|
|
434
405
|
const canonical = sqlToCanonical(type);
|
|
435
406
|
// Propagate explicit dimensions into CanonicalType.length for vector types
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
407
|
+
return options.dimensions && isVectorCategory(canonical.category)
|
|
408
|
+
? { ...canonical, length: options.dimensions }
|
|
409
|
+
: canonical;
|
|
410
|
+
}
|
|
411
|
+
switch (columnFamily(type)) {
|
|
412
|
+
case 'numeric':
|
|
413
|
+
// BIGINT for every `Number` without a scale, key or not: a 32-bit column is a migration waiting
|
|
414
|
+
// to happen, and the pools decode it back to a JS number at the wire (see `pgNumericTypes`).
|
|
415
|
+
return type === Number && (options.precision || options.scale)
|
|
416
|
+
? { category: 'decimal', precision: options.precision, scale: options.scale }
|
|
417
|
+
: { category: 'integer', size: 'big' };
|
|
418
|
+
case 'boolean':
|
|
419
|
+
return { category: 'boolean' };
|
|
420
|
+
case 'date':
|
|
421
|
+
return { category: 'timestamp' };
|
|
422
|
+
// `String`, and anything a reflected type left unrecognised.
|
|
423
|
+
default:
|
|
424
|
+
return { category: 'string', length: options.length };
|
|
440
425
|
}
|
|
441
|
-
// Default to string
|
|
442
|
-
return { category: 'string', length: options.length };
|
|
443
426
|
}
|
|
444
427
|
/**
|
|
445
|
-
* Compare two canonical types for equality.
|
|
446
|
-
*
|
|
428
|
+
* Compare two canonical types for equality. Used for schema diffing.
|
|
429
|
+
*
|
|
430
|
+
* Nothing here guesses an engine's own default for an unstated bound: `VARCHAR` is 255 on MySQL and
|
|
431
|
+
* `TEXT` on Postgres, and a comparison that assumed either was blind to that difference on the other.
|
|
432
|
+
* `DiffOptions.normalizeType` is what settles it, by putting both sides through the engine first.
|
|
447
433
|
*/
|
|
448
434
|
export function areTypesEqual(a, b) {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
// For strings, compare length (allowing for default 255)
|
|
458
|
-
if (a.category === 'string') {
|
|
459
|
-
const lengthA = a.length ?? 255;
|
|
460
|
-
const lengthB = b.length ?? 255;
|
|
461
|
-
if (lengthA !== lengthB) {
|
|
462
|
-
return false;
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
// For decimals, compare precision and scale (allowing for default 10, 2)
|
|
466
|
-
if (a.category === 'decimal') {
|
|
467
|
-
const pA = a.precision ?? 10;
|
|
468
|
-
const pB = b.precision ?? 10;
|
|
469
|
-
const sA = a.scale ?? 2;
|
|
470
|
-
const sB = b.scale ?? 2;
|
|
471
|
-
if (pA !== pB || sA !== sB) {
|
|
472
|
-
return false;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
// For timestamps, compare timezone
|
|
476
|
-
if (a.category === 'timestamp') {
|
|
477
|
-
if (!!a.withTimezone !== !!b.withTimezone) {
|
|
478
|
-
return false;
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
// Unsigned must match
|
|
482
|
-
if (!!a.unsigned !== !!b.unsigned) {
|
|
483
|
-
return false;
|
|
484
|
-
}
|
|
485
|
-
return true;
|
|
435
|
+
return (a.category === b.category &&
|
|
436
|
+
a.size === b.size &&
|
|
437
|
+
a.length === b.length &&
|
|
438
|
+
a.precision === b.precision &&
|
|
439
|
+
a.scale === b.scale &&
|
|
440
|
+
!!a.withTimezone === !!b.withTimezone &&
|
|
441
|
+
!!a.unsigned === !!b.unsigned);
|
|
486
442
|
}
|
|
487
443
|
/**
|
|
488
|
-
*
|
|
444
|
+
* Whether changing a column from one type to the other can lose what it holds.
|
|
445
|
+
*
|
|
446
|
+
* An unstated bound is the engine's widest, so stating one for the first time narrows the column just
|
|
447
|
+
* as lowering one does: `TEXT` to `VARCHAR(50)` truncates, and `NUMERIC` to `NUMERIC(5,2)` rounds.
|
|
489
448
|
*/
|
|
490
449
|
export function isBreakingTypeChange(from, to) {
|
|
491
|
-
// Changing category is always potentially breaking
|
|
492
450
|
if (from.category !== to.category) {
|
|
493
451
|
return true;
|
|
494
452
|
}
|
|
495
|
-
//
|
|
496
|
-
|
|
497
|
-
if (from.size && to.size) {
|
|
498
|
-
|
|
499
|
-
const toIndex = sizeOrder.indexOf(to.size);
|
|
500
|
-
if (toIndex < fromIndex) {
|
|
501
|
-
return true;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
// Reducing string length is breaking
|
|
505
|
-
if (from.category === 'string' && from.length && to.length) {
|
|
506
|
-
if (to.length < from.length) {
|
|
507
|
-
return true;
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
// Reducing precision/scale is breaking
|
|
511
|
-
if (from.category === 'decimal') {
|
|
512
|
-
if (from.precision && to.precision && to.precision < from.precision) {
|
|
513
|
-
return true;
|
|
514
|
-
}
|
|
515
|
-
if (from.scale && to.scale && to.scale < from.scale) {
|
|
516
|
-
return true;
|
|
517
|
-
}
|
|
453
|
+
// Only where both state a size: an unsized member of a family is the engine's base type, wider than
|
|
454
|
+
// `tiny` and narrower than `big`, and which of those it is depends on a family this does not know.
|
|
455
|
+
if (from.size && to.size && SIZE_ORDER.indexOf(to.size) < SIZE_ORDER.indexOf(from.size)) {
|
|
456
|
+
return true;
|
|
518
457
|
}
|
|
519
|
-
return
|
|
458
|
+
return (narrows(from.length, to.length) ||
|
|
459
|
+
narrows(from.precision, to.precision) ||
|
|
460
|
+
narrows(from.scale, to.scale) ||
|
|
461
|
+
// Either direction drops half the range: signed loses the top bit, unsigned the negatives.
|
|
462
|
+
!!from.unsigned !== !!to.unsigned ||
|
|
463
|
+
// An offset the column stops keeping cannot be recovered from what is left.
|
|
464
|
+
(!!from.withTimezone && !to.withTimezone));
|
|
465
|
+
}
|
|
466
|
+
const SIZE_ORDER = ['tiny', 'small', 'medium', 'big'];
|
|
467
|
+
/** A bound narrowed, where an unstated one is unbounded. */
|
|
468
|
+
function narrows(from, to) {
|
|
469
|
+
return to !== undefined && (from === undefined || to < from);
|
|
520
470
|
}
|
|
521
471
|
/**
|
|
522
472
|
* Get the UQL ColumnType that best matches a canonical type.
|
|
@@ -9,6 +9,34 @@ import type { IndexNode } from './types.js';
|
|
|
9
9
|
* back, MySQL emits one it cannot describe afterwards.
|
|
10
10
|
*/
|
|
11
11
|
export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include';
|
|
12
|
+
/**
|
|
13
|
+
* Whether the table already has this index, for the additive sync that only ever *creates* one.
|
|
14
|
+
*
|
|
15
|
+
* Its shape, never its name: the table's indexes were named by whoever created them, so an index
|
|
16
|
+
* that is already there must not be created a second time under a name we happen to prefer. A
|
|
17
|
+
* derived name is no handle at all - the convention can change, an engine silently truncates one
|
|
18
|
+
* past its identifier limit, and SQLite reports names it made up.
|
|
19
|
+
*
|
|
20
|
+
* Uniqueness counts, because a unique index and a plain one over the same columns enforce different
|
|
21
|
+
* things and no engine can alter one into the other. An index over an expression or a JSON path has
|
|
22
|
+
* no comparable columns - engines reprint SQL text from their parse tree, the same reason
|
|
23
|
+
* {@link describeIndexDifferences} leaves those entries alone - so it falls back to its name.
|
|
24
|
+
*/
|
|
25
|
+
export declare function indexSignature(index: Pick<IndexNode, 'name' | 'entries' | 'unique'>): string;
|
|
26
|
+
/**
|
|
27
|
+
* A constraint name without its kind marker.
|
|
28
|
+
*
|
|
29
|
+
* What pairs two sides of a *report*: an index whose uniqueness or columns changed is one index that
|
|
30
|
+
* differs, not one dropped and another created, and only a handle independent of its shape can say
|
|
31
|
+
* so. Stripping the marker is what lets `idx_User_email`, named before the convention moved it to
|
|
32
|
+
* the end, recognise the `User__email_idx` derived for it now, so upgrading reports no drift.
|
|
33
|
+
*
|
|
34
|
+
* Exactly one marker, and the trailing one first. Stripping both ends would eat a leading marker
|
|
35
|
+
* that belongs to the *table* - an index over `pk_registry` is not a primary key - leaving it unable
|
|
36
|
+
* to pair with its own older name. The separator is levelled last, since only one convention doubles
|
|
37
|
+
* it.
|
|
38
|
+
*/
|
|
39
|
+
export declare function indexNameStem(name: string): string;
|
|
12
40
|
/**
|
|
13
41
|
* Everything an index differs by, named, or nothing when the two match.
|
|
14
42
|
*
|
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether the table already has this index, for the additive sync that only ever *creates* one.
|
|
3
|
+
*
|
|
4
|
+
* Its shape, never its name: the table's indexes were named by whoever created them, so an index
|
|
5
|
+
* that is already there must not be created a second time under a name we happen to prefer. A
|
|
6
|
+
* derived name is no handle at all - the convention can change, an engine silently truncates one
|
|
7
|
+
* past its identifier limit, and SQLite reports names it made up.
|
|
8
|
+
*
|
|
9
|
+
* Uniqueness counts, because a unique index and a plain one over the same columns enforce different
|
|
10
|
+
* things and no engine can alter one into the other. An index over an expression or a JSON path has
|
|
11
|
+
* no comparable columns - engines reprint SQL text from their parse tree, the same reason
|
|
12
|
+
* {@link describeIndexDifferences} leaves those entries alone - so it falls back to its name.
|
|
13
|
+
*/
|
|
14
|
+
export function indexSignature(index) {
|
|
15
|
+
const comparable = !index.entries.some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
|
|
16
|
+
const identity = comparable
|
|
17
|
+
? index.entries.map((entry) => entry.column).join(',')
|
|
18
|
+
: `name:${indexNameStem(index.name)}`;
|
|
19
|
+
return `${index.unique ? 'unique' : 'plain'}(${identity})`;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A constraint name without its kind marker.
|
|
23
|
+
*
|
|
24
|
+
* What pairs two sides of a *report*: an index whose uniqueness or columns changed is one index that
|
|
25
|
+
* differs, not one dropped and another created, and only a handle independent of its shape can say
|
|
26
|
+
* so. Stripping the marker is what lets `idx_User_email`, named before the convention moved it to
|
|
27
|
+
* the end, recognise the `User__email_idx` derived for it now, so upgrading reports no drift.
|
|
28
|
+
*
|
|
29
|
+
* Exactly one marker, and the trailing one first. Stripping both ends would eat a leading marker
|
|
30
|
+
* that belongs to the *table* - an index over `pk_registry` is not a primary key - leaving it unable
|
|
31
|
+
* to pair with its own older name. The separator is levelled last, since only one convention doubles
|
|
32
|
+
* it.
|
|
33
|
+
*/
|
|
34
|
+
export function indexNameStem(name) {
|
|
35
|
+
const withoutSuffix = name.replace(KIND_SUFFIX, '');
|
|
36
|
+
const bare = withoutSuffix === name ? name.replace(KIND_PREFIX, '') : withoutSuffix;
|
|
37
|
+
return bare.replace(/__/g, '_');
|
|
38
|
+
}
|
|
39
|
+
/** What this version emits. */
|
|
40
|
+
const KIND_SUFFIX = /_(?:idx|fk|ck|pk|uk|uq)$/i;
|
|
41
|
+
/**
|
|
42
|
+
* What it only ever *reads*: uql wrote `idx_User_email` until 0.42.1, and a database it did not
|
|
43
|
+
* create at all - the one `generate:from-db` points at - most often spells it that way too. Tried
|
|
44
|
+
* second, so a name already marked at the end keeps a leading `pk_` that is part of its table.
|
|
45
|
+
*/
|
|
46
|
+
const KIND_PREFIX = /^(?:idx|fk|ck|pk|uk|uq)_/i;
|
|
1
47
|
/**
|
|
2
48
|
* Everything an index differs by, named, or nothing when the two match.
|
|
3
49
|
*
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
8
|
import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
9
|
+
import { isSoleIdField } from '../util/field.util.js';
|
|
9
10
|
import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
|
|
10
11
|
import { fieldOptionsToCanonical } from './canonicalType.js';
|
|
11
12
|
import { createTableNode, SchemaAST } from './schemaAST.js';
|
|
@@ -64,7 +65,7 @@ function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
|
64
65
|
return resolveColumnCanonicalType(referencedIdField, seen);
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
|
-
return fieldOptionsToCanonical(field
|
|
68
|
+
return fieldOptionsToCanonical(field);
|
|
68
69
|
}
|
|
69
70
|
/**
|
|
70
71
|
* Add a table from entity metadata.
|
|
@@ -86,7 +87,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
86
87
|
const columnName = ctx.resolveColumnName(key, field);
|
|
87
88
|
const type = resolveColumnCanonicalType(field);
|
|
88
89
|
const isPrimaryKey = field.isId === true;
|
|
89
|
-
const isSoleKey =
|
|
90
|
+
const isSoleKey = isSoleIdField(meta, field);
|
|
90
91
|
const column = {
|
|
91
92
|
name: columnName,
|
|
92
93
|
type,
|
|
@@ -95,9 +96,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
95
96
|
nullable: isPrimaryKey ? false : (field.nullable ?? true),
|
|
96
97
|
defaultValue: field.defaultValue,
|
|
97
98
|
isPrimaryKey,
|
|
98
|
-
|
|
99
|
-
// caller supplies, and the serial type carries an inline `PRIMARY KEY` the table already states.
|
|
100
|
-
isAutoIncrement: field.autoIncrement ?? (isPrimaryKey && isSoleKey && type.category === 'integer'),
|
|
99
|
+
isAutoIncrement: field.autoIncrement ?? (isSoleKey && type.category === 'integer'),
|
|
101
100
|
isUnique: field.unique ?? false,
|
|
102
101
|
comment: field.comment,
|
|
103
102
|
enum: field.enum,
|
|
@@ -142,37 +141,32 @@ function addRelationshipsFromEntity(ctx, meta) {
|
|
|
142
141
|
// the engine requires the referenced columns to match a unique constraint as a whole.
|
|
143
142
|
const localColumns = [];
|
|
144
143
|
const foreignColumns = [];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const localColumn = table.columns.get(ctx.resolveColumnName(localPropName, localField));
|
|
153
|
-
const foreignColumn = relatedTable.columns.get(ctx.resolveColumnName(foreignPropName, foreignField));
|
|
154
|
-
if (!localColumn || !foreignColumn) {
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
firstLocalField ??= localField;
|
|
144
|
+
for (const { local: localProp, foreign: foreignProp } of relation.references) {
|
|
145
|
+
const localField = meta.fields[localProp];
|
|
146
|
+
const foreignField = relatedMeta.fields[foreignProp];
|
|
147
|
+
const localColumn = localField && table.columns.get(ctx.resolveColumnName(localProp, localField));
|
|
148
|
+
const foreignColumn = foreignField && relatedTable.columns.get(ctx.resolveColumnName(foreignProp, foreignField));
|
|
149
|
+
if (!localColumn || !foreignColumn)
|
|
150
|
+
break;
|
|
158
151
|
localColumns.push(localColumn);
|
|
159
152
|
foreignColumns.push(foreignColumn);
|
|
160
153
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
154
|
+
// A pair that cannot be resolved drops the whole constraint: half of one enforces a rule
|
|
155
|
+
// nobody declared, over a subset of the key.
|
|
156
|
+
if (localColumns.length !== relation.references.length)
|
|
157
|
+
continue;
|
|
158
|
+
ctx.ast.addRelationship({
|
|
159
|
+
name: derivedForeignKeyName(table.name, localColumns.map((column) => column.name)),
|
|
160
|
+
type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
|
|
161
|
+
from: { table, columns: localColumns },
|
|
162
|
+
to: { table: relatedTable, columns: foreignColumns },
|
|
163
|
+
// Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
|
|
164
|
+
// references, onDelete })` work with no relation declared at all.
|
|
165
|
+
onDelete: relation.onDelete ?? meta.fields[relation.references[0].local]?.onDelete ?? ctx.defaultForeignKeyAction,
|
|
166
|
+
onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
|
|
167
|
+
confidence: 1.0,
|
|
168
|
+
inferredFrom: 'entity_decorator',
|
|
169
|
+
});
|
|
176
170
|
}
|
|
177
171
|
}
|
|
178
172
|
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type IndexFacet } from './indexDifferences.js';
|
|
11
11
|
import type { SchemaAST } from './schemaAST.js';
|
|
12
|
-
import type {
|
|
12
|
+
import type { CanonicalType } from './types.js';
|
|
13
|
+
import type { SchemaDiffResult, TableDiff, TableNode } from './types.js';
|
|
13
14
|
/**
|
|
14
15
|
* Options for schema diffing.
|
|
15
16
|
*/
|
|
@@ -24,6 +25,23 @@ export interface DiffOptions {
|
|
|
24
25
|
ignoreCase?: boolean;
|
|
25
26
|
/** Tables to exclude from comparison */
|
|
26
27
|
excludeTables?: string[];
|
|
28
|
+
/**
|
|
29
|
+
* A type as the engine would actually store it, for the caller that has a dialect.
|
|
30
|
+
*
|
|
31
|
+
* Several canonical types share one storage type per engine - a `boolean` is `TINYINT(1)` on MySQL
|
|
32
|
+
* and `INTEGER` on SQLite - so comparing them canonically reports an alteration on every sync for
|
|
33
|
+
* those columns. Passing both sides through the engine first is what settles that, and it is the
|
|
34
|
+
* only thing here a dialect is needed for, so it arrives as a function rather than as a dependency.
|
|
35
|
+
*/
|
|
36
|
+
normalizeType?: (type: CanonicalType) => CanonicalType;
|
|
37
|
+
/**
|
|
38
|
+
* Whether two defaults are the same value, for the caller that has a dialect.
|
|
39
|
+
*
|
|
40
|
+
* A database reprints a default from its parse tree, so `'active'` comes back as
|
|
41
|
+
* `'active'::character varying` on Postgres and a symbolic `now()` matches no spelling of
|
|
42
|
+
* `CURRENT_TIMESTAMP`. Undoing that needs the dialect that wrote it, so it arrives as a function.
|
|
43
|
+
*/
|
|
44
|
+
defaultsEqual?: (expected: unknown, actual: unknown) => boolean;
|
|
27
45
|
}
|
|
28
46
|
/**
|
|
29
47
|
* Compare two schemas and return the differences.
|
|
@@ -34,3 +52,11 @@ export interface DiffOptions {
|
|
|
34
52
|
* @returns Detailed diff result
|
|
35
53
|
*/
|
|
36
54
|
export declare function diffSchemas(source: SchemaAST, target: SchemaAST, options?: DiffOptions): SchemaDiffResult;
|
|
55
|
+
/**
|
|
56
|
+
* Compare two tables and return the differences.
|
|
57
|
+
*
|
|
58
|
+
* Exported because it is also how a migration is planned: the generator diffs one entity's table
|
|
59
|
+
* against the one the database reported, then projects the result into a `SchemaDiff`. One
|
|
60
|
+
* comparison serves both, so drift and migrations can no longer disagree about what has changed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function diffTable(source: TableNode, target: TableNode, options?: DiffOptions): TableDiff | undefined;
|