uql-orm 0.41.1 → 0.42.1
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 +7 -7
- package/dist/browser/querier/httpQuerier.js +21 -6
- 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 +4 -4
- package/dist/dialect/abstractSqlDialect.d.ts +32 -3
- package/dist/dialect/abstractSqlDialect.js +58 -22
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +3 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +5 -3
- package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
- package/dist/dialect/pgLikeSqlDialect.js +2 -1
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/decorator/entity.js +1 -1
- package/dist/entity/decorator/members.d.ts +10 -2
- package/dist/entity/index.d.ts +1 -1
- package/dist/entity/index.js +1 -1
- package/dist/entity/metadata/definition.d.ts +18 -1
- package/dist/entity/metadata/definition.js +91 -36
- package/dist/http/handler.js +4 -4
- package/dist/maria/mariaDialect.d.ts +2 -2
- package/dist/maria/mariaDialect.js +3 -2
- package/dist/migrate/builder/tableBuilder.js +5 -4
- package/dist/migrate/drift/driftDetector.js +16 -0
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +7 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +24 -28
- 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 +44 -34
- package/dist/migrate/schemaGenerator.js +158 -141
- package/dist/mongo/mongoDialect.js +39 -14
- package/dist/mongo/mongodbQuerier.js +6 -3
- package/dist/postgres/postgresDialect.js +1 -1
- package/dist/querier/abstractQuerier.d.ts +16 -12
- package/dist/querier/abstractQuerier.js +88 -40
- package/dist/querier/abstractQuerierPool.d.ts +8 -8
- package/dist/querier/abstractSqlQuerier.d.ts +1 -1
- package/dist/querier/abstractSqlQuerier.js +12 -7
- package/dist/querier/relationCount.js +44 -33
- package/dist/schema/indexDifferences.d.ts +28 -0
- package/dist/schema/indexDifferences.js +46 -0
- package/dist/schema/schemaASTBuilder.js +38 -31
- package/dist/schema/schemaASTDiffer.d.ts +27 -1
- package/dist/schema/schemaASTDiffer.js +54 -18
- 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 +57 -14
- package/dist/type/migration.d.ts +19 -0
- package/dist/type/queryWhere.d.ts +7 -2
- package/dist/type/universalQuerier.d.ts +8 -8
- package/dist/util/dialect.util.js +15 -14
- package/dist/util/field.util.d.ts +11 -1
- package/dist/util/field.util.js +12 -0
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/object.util.d.ts +6 -0
- package/dist/util/object.util.js +18 -0
- package/dist/util/relationQuery.util.d.ts +48 -7
- package/dist/util/relationQuery.util.js +70 -9
- package/dist/util/rowKey.util.d.ts +9 -0
- package/dist/util/rowKey.util.js +24 -0
- package/dist/util/sql.util.d.ts +24 -7
- package/dist/util/sql.util.js +75 -10
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { COUNT_ALIAS } from '../dialect/aliases.js';
|
|
2
|
-
import { getMeta } from '../entity/index.js';
|
|
2
|
+
import { getMeta, soleIdOf } from '../entity/index.js';
|
|
3
3
|
import { COUNT_RESULT_KEY } from '../type/index.js';
|
|
4
|
-
import { asSelectMap, getKeys,
|
|
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
|
|
@@ -9,27 +9,33 @@ import { asSelectMap, getKeys, parentKeyColumn, targetKeyColumn } from '../util/
|
|
|
9
9
|
*/
|
|
10
10
|
export function withIdForCounts(entity, q) {
|
|
11
11
|
const meta = getMeta(entity);
|
|
12
|
-
if (!q.$count
|
|
12
|
+
if (!q.$count) {
|
|
13
13
|
return q;
|
|
14
14
|
}
|
|
15
|
+
const { ids } = meta;
|
|
16
|
+
const named = ids.join(', ');
|
|
15
17
|
if (q.$distinct) {
|
|
16
|
-
// The
|
|
17
|
-
// `$distinct` collapses on: adding
|
|
18
|
-
throw new TypeError(`$count cannot be combined with $distinct: the tallies group by each row's '${
|
|
18
|
+
// The keys would have to join the projection for the tallies to group by, and that is the set
|
|
19
|
+
// `$distinct` collapses on: adding them silently stops the grouping collapsing anything at all.
|
|
20
|
+
throw new TypeError(`$count cannot be combined with $distinct: the tallies group by each row's '${named}', which the grouping does not keep.`);
|
|
19
21
|
}
|
|
20
22
|
if (Array.isArray(q.$select)) {
|
|
21
|
-
// Nothing to add the
|
|
23
|
+
// Nothing to add the keys to, and without them every tally would be looked up by `undefined` and
|
|
22
24
|
// come back zero. Refused rather than answered wrong, the way an unorderable clause is.
|
|
23
|
-
throw new TypeError(`$count needs the '${
|
|
25
|
+
throw new TypeError(`$count needs the '${named}' of each row to group its tallies by, which a raw $select cannot carry. Use a $select map, or drop the $count.`);
|
|
24
26
|
}
|
|
25
27
|
const next = { ...q };
|
|
26
28
|
const select = asSelectMap(q.$select);
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
const missing = select ? ids.filter((key) => !select[key]) : [];
|
|
30
|
+
if (missing.length) {
|
|
31
|
+
next.$select = { ...select, ...Object.fromEntries(missing.map((key) => [key, true])) };
|
|
29
32
|
}
|
|
30
|
-
|
|
33
|
+
const excluded = ids.filter((key) => q.$exclude?.[key]);
|
|
34
|
+
if (excluded.length) {
|
|
31
35
|
const kept = { ...q.$exclude };
|
|
32
|
-
|
|
36
|
+
for (const key of excluded) {
|
|
37
|
+
delete kept[key];
|
|
38
|
+
}
|
|
33
39
|
next.$exclude = kept;
|
|
34
40
|
}
|
|
35
41
|
return next;
|
|
@@ -44,9 +50,9 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
44
50
|
return;
|
|
45
51
|
}
|
|
46
52
|
const meta = getMeta(entity);
|
|
47
|
-
// The
|
|
48
|
-
//
|
|
49
|
-
|
|
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.
|
|
50
56
|
const counted = new Map();
|
|
51
57
|
for (const relKey of getKeys(count)) {
|
|
52
58
|
const value = count[relKey];
|
|
@@ -55,50 +61,55 @@ export async function fillRelationCounts(querier, entity, payload, count) {
|
|
|
55
61
|
continue;
|
|
56
62
|
}
|
|
57
63
|
const where = typeof value === 'object' ? value.$where : undefined;
|
|
58
|
-
|
|
64
|
+
const joins = parentJoins(relOpts, meta.ids.length);
|
|
65
|
+
counted.set(relKey, { joins, byParent: await countPerParent(querier, relOpts, joins, payload, where) });
|
|
59
66
|
}
|
|
60
67
|
for (const parent of payload) {
|
|
61
|
-
const id = String(parent[meta.id]);
|
|
62
68
|
const row = {};
|
|
63
|
-
for (const [relKey, byParent] of counted) {
|
|
69
|
+
for (const [relKey, { joins, byParent }] of counted) {
|
|
64
70
|
// A parent the grouped result has no row for matched nothing, which is a zero rather than a
|
|
65
71
|
// gap: `_count` names what the caller asked to count, so every key it asked for is present.
|
|
66
|
-
row[relKey] = byParent[
|
|
72
|
+
row[relKey] = byParent[parentRowKey(joins, parent)] ?? 0;
|
|
67
73
|
}
|
|
68
74
|
parent[COUNT_RESULT_KEY] = row;
|
|
69
75
|
}
|
|
70
76
|
}
|
|
71
|
-
async function countPerParent(querier, relOpts,
|
|
77
|
+
async function countPerParent(querier, relOpts, joins, parents, where) {
|
|
72
78
|
const through = relOpts.through;
|
|
73
79
|
if (through) {
|
|
74
|
-
return countThroughPerParent(querier, relOpts, through(),
|
|
80
|
+
return countThroughPerParent(querier, relOpts, through(), joins, parents, where);
|
|
75
81
|
}
|
|
76
|
-
|
|
77
|
-
|
|
82
|
+
return groupedCount(querier, relOpts.entity(), joins, {
|
|
83
|
+
...where,
|
|
84
|
+
...parentsIn(joins, parents),
|
|
85
|
+
});
|
|
78
86
|
}
|
|
79
87
|
/**
|
|
80
88
|
* A many-to-many counts its junction rows, one per pairing. A filter names the target's columns,
|
|
81
89
|
* which the junction does not have, so the matching targets are resolved first and the junction
|
|
82
90
|
* counted against them - the one shape here that costs a second statement, and only when filtered.
|
|
83
91
|
*/
|
|
84
|
-
async function countThroughPerParent(querier, relOpts, throughEntity,
|
|
85
|
-
const
|
|
86
|
-
const throughWhere = { [local]: ids };
|
|
92
|
+
async function countThroughPerParent(querier, relOpts, throughEntity, joins, parents, where) {
|
|
93
|
+
const throughWhere = parentsIn(joins, parents);
|
|
87
94
|
if (where) {
|
|
88
95
|
const target = relOpts.entity();
|
|
89
|
-
const targetId = getMeta(target)
|
|
96
|
+
const targetId = soleIdOf(getMeta(target), 'a many-to-many target');
|
|
90
97
|
const targets = await querier.findMany(target, { $select: { [targetId]: true }, $where: where });
|
|
91
|
-
|
|
98
|
+
const [targetColumn] = targetKeyColumns(relOpts, joins.length);
|
|
99
|
+
throughWhere[targetColumn] = targets.map((it) => it[targetId]);
|
|
92
100
|
}
|
|
93
|
-
return groupedCount(querier, throughEntity,
|
|
101
|
+
return groupedCount(querier, throughEntity, joins, throughWhere);
|
|
94
102
|
}
|
|
95
|
-
/** `SELECT <
|
|
96
|
-
async function groupedCount(querier, entity,
|
|
103
|
+
/** `SELECT <keys>, COUNT(*) ... GROUP BY <keys>`, as a lookup from parent key to tally. */
|
|
104
|
+
async function groupedCount(querier, entity, joins, where) {
|
|
97
105
|
const $agg = { [COUNT_ALIAS]: { $count: '*' } };
|
|
98
|
-
const
|
|
106
|
+
const $group = joinedColumns(joins);
|
|
107
|
+
const rows = await querier.aggregate(entity, { $group, $agg, $where: where });
|
|
99
108
|
const byParent = {};
|
|
100
109
|
for (const row of rows) {
|
|
101
|
-
|
|
110
|
+
// Keyed by every joined column, which is how a tally finds the one parent whose whole key it
|
|
111
|
+
// matches - and how the rows an over-selecting `IN` brought back find no parent at all.
|
|
112
|
+
byParent[joinedRowKey(joins, row)] = Number(row[COUNT_ALIAS]);
|
|
102
113
|
}
|
|
103
114
|
return byParent;
|
|
104
115
|
}
|
|
@@ -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
|
*
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* - Entity metadata (decorator-based entities)
|
|
6
6
|
* - Database introspection results (TableSchema[])
|
|
7
7
|
*/
|
|
8
|
-
import { getMeta } from '../entity/metadata/definition.js';
|
|
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';
|
|
@@ -57,7 +58,9 @@ function resolveColumnCanonicalType(field, seen = new Set()) {
|
|
|
57
58
|
if (!hasExplicitType && field.references && !seen.has(field.references)) {
|
|
58
59
|
seen.add(field.references);
|
|
59
60
|
const referencedMeta = getMeta(field.references());
|
|
60
|
-
|
|
61
|
+
// The column names which key it points at when the target has several; otherwise there is one.
|
|
62
|
+
const referencedKey = field.referencedKey ?? soleIdOf(referencedMeta, 'a foreign key');
|
|
63
|
+
const referencedIdField = referencedMeta.fields[referencedKey];
|
|
61
64
|
if (referencedIdField) {
|
|
62
65
|
return resolveColumnCanonicalType(referencedIdField, seen);
|
|
63
66
|
}
|
|
@@ -83,7 +86,8 @@ function addTableFromEntity(ctx, meta) {
|
|
|
83
86
|
continue;
|
|
84
87
|
const columnName = ctx.resolveColumnName(key, field);
|
|
85
88
|
const type = resolveColumnCanonicalType(field);
|
|
86
|
-
const isPrimaryKey =
|
|
89
|
+
const isPrimaryKey = field.isId === true;
|
|
90
|
+
const isSoleKey = isSoleIdField(meta, field);
|
|
87
91
|
const column = {
|
|
88
92
|
name: columnName,
|
|
89
93
|
type,
|
|
@@ -92,7 +96,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
92
96
|
nullable: isPrimaryKey ? false : (field.nullable ?? true),
|
|
93
97
|
defaultValue: field.defaultValue,
|
|
94
98
|
isPrimaryKey,
|
|
95
|
-
isAutoIncrement: field.autoIncrement ?? (
|
|
99
|
+
isAutoIncrement: field.autoIncrement ?? (isSoleKey && type.category === 'integer'),
|
|
96
100
|
isUnique: field.unique ?? false,
|
|
97
101
|
comment: field.comment,
|
|
98
102
|
enum: field.enum,
|
|
@@ -101,7 +105,7 @@ function addTableFromEntity(ctx, meta) {
|
|
|
101
105
|
references: undefined,
|
|
102
106
|
};
|
|
103
107
|
columns.set(columnName, column);
|
|
104
|
-
if (
|
|
108
|
+
if (field.isId) {
|
|
105
109
|
primaryKey.push(column);
|
|
106
110
|
}
|
|
107
111
|
}
|
|
@@ -133,33 +137,36 @@ function addRelationshipsFromEntity(ctx, meta) {
|
|
|
133
137
|
// user_profile(creatorId)`), which SQLite rejects outright as a foreign key mismatch.
|
|
134
138
|
const ownsForeignKey = relation.cardinality === 'm1' || (relation.cardinality === '11' && !relation.mappedBy);
|
|
135
139
|
if (ownsForeignKey) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const relNode = {
|
|
150
|
-
name: derivedForeignKeyName(table.name, [localColName]),
|
|
151
|
-
type: relation.cardinality === 'm1' ? 'ManyToOne' : 'OneToOne',
|
|
152
|
-
from: { table, columns: [localColumn] },
|
|
153
|
-
to: { table: relatedTable, columns: [foreignColumn] },
|
|
154
|
-
// Falls back to the FK column's own `onDelete`, which is what makes a bare `@Field({
|
|
155
|
-
// references, onDelete })` work with no relation declared at all.
|
|
156
|
-
onDelete: relation.onDelete ?? localField.onDelete ?? ctx.defaultForeignKeyAction,
|
|
157
|
-
onUpdate: relation.onUpdate ?? ctx.defaultForeignKeyAction,
|
|
158
|
-
confidence: 1.0,
|
|
159
|
-
inferredFrom: 'entity_decorator',
|
|
160
|
-
};
|
|
161
|
-
ctx.ast.addRelationship(relNode);
|
|
140
|
+
// Every pair, not just the first: a composite key is one constraint over all its columns, and
|
|
141
|
+
// the engine requires the referenced columns to match a unique constraint as a whole.
|
|
142
|
+
const localColumns = [];
|
|
143
|
+
const foreignColumns = [];
|
|
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;
|
|
151
|
+
localColumns.push(localColumn);
|
|
152
|
+
foreignColumns.push(foreignColumn);
|
|
162
153
|
}
|
|
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
|
+
});
|
|
163
170
|
}
|
|
164
171
|
}
|
|
165
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;
|
|
@@ -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,34 @@ 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
|
+
if (!generatedType && !areTypesEqual(opts.normalizeType(source.type), opts.normalizeType(target.type))) {
|
|
142
179
|
differences.push(`type: ${formatType(source.type)} → ${formatType(target.type)}`);
|
|
143
180
|
}
|
|
144
|
-
|
|
145
|
-
if (source.nullable !== target.nullable) {
|
|
181
|
+
if (!impliedNotNull && source.nullable !== target.nullable) {
|
|
146
182
|
differences.push(`nullable: ${target.nullable} → ${source.nullable}`);
|
|
147
183
|
}
|
|
148
184
|
// Compare unique constraint
|
|
149
185
|
if (source.isUnique !== target.isUnique) {
|
|
150
186
|
differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
|
|
151
187
|
}
|
|
152
|
-
//
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
188
|
+
// Auto-increment is deliberately not compared. No engine turns a column into an identity, or out of
|
|
189
|
+
// one, without rewriting the table, and there is no DDL here that does it - so a difference could
|
|
190
|
+
// only ever be reported, never settled, and the statements emitted for it (a bare `ALTER COLUMN
|
|
191
|
+
// TYPE`) do not change it. The same rule `describeIndexDifferences` follows for what it cannot read.
|
|
156
192
|
// Compare default values (if both defined)
|
|
157
|
-
if (
|
|
193
|
+
if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
|
|
158
194
|
differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
|
|
159
195
|
}
|
|
160
196
|
if (differences.length === 0) {
|
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";
|