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.
Files changed (61) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +3 -3
  2. package/dist/browser/type/clientQuerier.d.ts +3 -3
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +5 -5
  5. package/dist/dialect/abstractSqlDialect.d.ts +41 -14
  6. package/dist/dialect/abstractSqlDialect.js +75 -44
  7. package/dist/dialect/jsonSql.d.ts +3 -2
  8. package/dist/dialect/jsonSql.js +7 -5
  9. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -1
  10. package/dist/dialect/mysqlLikeSqlDialect.js +3 -1
  11. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -1
  12. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  13. package/dist/dialect/vectorCast.d.ts +0 -6
  14. package/dist/dialect/vectorCast.js +0 -8
  15. package/dist/entity/decorator/entity.d.ts +1 -1
  16. package/dist/entity/decorator/entity.js +1 -1
  17. package/dist/entity/decorator/members.d.ts +5 -2
  18. package/dist/entity/metadata/definition.js +29 -12
  19. package/dist/maria/mariaDialect.js +4 -3
  20. package/dist/migrate/builder/tableBuilder.js +5 -4
  21. package/dist/migrate/drift/driftDetector.js +21 -1
  22. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +8 -1
  23. package/dist/migrate/generator/mongoSchemaGenerator.js +25 -29
  24. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +9 -1
  25. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +11 -2
  26. package/dist/migrate/introspection/baseSqlIntrospector.js +6 -3
  27. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  28. package/dist/migrate/introspection/sqliteIntrospector.js +7 -3
  29. package/dist/migrate/migrator.js +6 -0
  30. package/dist/migrate/schemaGenerator.d.ts +43 -37
  31. package/dist/migrate/schemaGenerator.js +163 -150
  32. package/dist/mongo/mongoDialect.js +22 -8
  33. package/dist/postgres/postgresDialect.js +1 -1
  34. package/dist/querier/abstractQuerier.js +18 -7
  35. package/dist/querier/relationCount.js +9 -7
  36. package/dist/schema/canonicalType.d.ts +19 -4
  37. package/dist/schema/canonicalType.js +114 -164
  38. package/dist/schema/indexDifferences.d.ts +28 -0
  39. package/dist/schema/indexDifferences.js +46 -0
  40. package/dist/schema/schemaASTBuilder.js +27 -33
  41. package/dist/schema/schemaASTDiffer.d.ts +27 -1
  42. package/dist/schema/schemaASTDiffer.js +59 -19
  43. package/dist/schema/types.d.ts +46 -7
  44. package/dist/sqlite/sqliteDialect.d.ts +2 -1
  45. package/dist/sqlite/sqliteDialect.js +4 -1
  46. package/dist/type/dialect.d.ts +6 -0
  47. package/dist/type/entity.d.ts +23 -6
  48. package/dist/type/migration.d.ts +20 -1
  49. package/dist/type/query.d.ts +2 -6
  50. package/dist/util/field.util.d.ts +28 -7
  51. package/dist/util/field.util.js +56 -48
  52. package/dist/util/fieldOption.util.d.ts +79 -0
  53. package/dist/util/fieldOption.util.js +84 -0
  54. package/dist/util/index.d.ts +1 -0
  55. package/dist/util/index.js +1 -0
  56. package/dist/util/object.util.js +11 -3
  57. package/dist/util/relationQuery.util.d.ts +10 -0
  58. package/dist/util/relationQuery.util.js +22 -2
  59. package/dist/util/sql.util.d.ts +28 -7
  60. package/dist/util/sql.util.js +79 -10
  61. package/package.json +2 -2
@@ -0,0 +1,79 @@
1
+ import type { BlobColumnType, BooleanColumnType, DateColumnType, FieldOptions, JsonColumnType, NumericColumnType, QueryRaw, StringColumnType, VectorColumnType } from '../type/index.js';
2
+ import { type ColumnFamily } from './field.util.js';
3
+ /**
4
+ * The column family each field option means anything on, or `'*'` where it applies to every column.
5
+ * Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
6
+ * discipline `INDEX_FEATURE_LABELS` uses for index features.
7
+ */
8
+ declare const FIELD_OPTION_FAMILY: {
9
+ readonly name: '*';
10
+ readonly isId: '*';
11
+ readonly type: '*';
12
+ readonly dimensions: 'vector';
13
+ readonly distance: 'vector';
14
+ readonly references: '*';
15
+ readonly onDelete: '*';
16
+ readonly enum: '*';
17
+ readonly virtual: '*';
18
+ readonly updatable: '*';
19
+ readonly eager: '*';
20
+ readonly onInsert: '*';
21
+ readonly onUpdate: '*';
22
+ readonly softDelete: '*';
23
+ readonly columnType: '*';
24
+ readonly length: 'string';
25
+ readonly precision: 'numeric';
26
+ readonly scale: 'numeric';
27
+ readonly nullable: '*';
28
+ readonly unique: '*';
29
+ readonly defaultValue: '*';
30
+ readonly autoIncrement: 'numeric';
31
+ readonly index: '*';
32
+ readonly comment: '*';
33
+ };
34
+ /**
35
+ * The only options a `virtual` field reaches: it is skipped in DDL and dropped from every insert and
36
+ * update, so the whole persistence half of the options above is dead on one. Stated as what survives
37
+ * rather than on each option that dies, because it is one fact rather than nineteen - and because an
38
+ * option added without a thought then lands on the safe side of it.
39
+ */
40
+ declare const VIRTUAL_READS: readonly ["type", "virtual", "enum", "eager", "distance"];
41
+ type VirtualRead = (typeof VIRTUAL_READS)[number];
42
+ /**
43
+ * The first option `opts` cannot use, phrased as the tail of `'Entity.field' ...`, or `undefined`
44
+ * where every option applies. The runtime half of the decorators' check, so the imperative API and
45
+ * plain JavaScript reach the same answer.
46
+ */
47
+ export declare function fieldOptionConflict(opts: FieldOptions): string | undefined;
48
+ /** The family the options put the column in; every family where they name no type to put it in. */
49
+ type FamilyOf<O> = O extends {
50
+ readonly columnType: infer C;
51
+ } ? FamilyOfType<C> : O extends {
52
+ readonly type: infer T;
53
+ } ? FamilyOfType<T> : ColumnFamily;
54
+ /** Read off the column-type unions themselves, which is what `COLUMN_TYPES_BY_FAMILY` is checked against. */
55
+ type FamilyOfType<T> = T extends NumericColumnType | NumberConstructor | BigIntConstructor ? 'numeric' : T extends StringColumnType | StringConstructor ? 'string' : T extends VectorColumnType ? 'vector' : T extends JsonColumnType ? 'json' : T extends DateColumnType | DateConstructor ? 'date' : T extends BooleanColumnType | BooleanConstructor ? 'boolean' : T extends BlobColumnType ? 'blob' : ColumnFamily;
56
+ /** What the field's own values leave unread, matching {@link deadOn} line for line. */
57
+ type DeadOptions<O> = (O extends {
58
+ readonly virtual: QueryRaw;
59
+ } ? Exclude<keyof FieldOptions, VirtualRead> : never) | (O extends {
60
+ readonly isId: true;
61
+ readonly nullable: true;
62
+ } ? 'nullable' : never) | (O extends {
63
+ readonly updatable: false;
64
+ } ? 'onUpdate' : never);
65
+ type Given<O> = Extract<keyof O, keyof FieldOptions>;
66
+ type Offending<O> = {
67
+ [K in Given<O>]: (typeof FIELD_OPTION_FAMILY)[K] extends FamilyOf<O> | '*' ? K extends DeadOptions<O> ? K : never : K;
68
+ }[Given<O>];
69
+ /**
70
+ * Maps every option `O` states but cannot use to `never`, the way `RejectUnknown` maps a typo'd one,
71
+ * so an option that would be silently ignored reads as the same compile error. Resolves to `unknown`
72
+ * - an inert intersection member - when there are none.
73
+ *
74
+ * `@Id` adds `{ nullable?: false }` of its own rather than passing the `isId` it stamps on, which
75
+ * would have to reach `O` as an intersection - and a non-naked `O` in its own constraint stops it
76
+ * inferring from the options at all.
77
+ */
78
+ export type RejectIncompatible<O> = [Offending<O>] extends [never] ? unknown : Record<Offending<O> & string, never>;
79
+ export {};
@@ -0,0 +1,84 @@
1
+ import { columnFamily } from './field.util.js';
2
+ import { getKeys } from './object.util.js';
3
+ /**
4
+ * The column family each field option means anything on, or `'*'` where it applies to every column.
5
+ * Exhaustive over {@link FieldOptions}, so a new option cannot be added without placing it - the
6
+ * discipline `INDEX_FEATURE_LABELS` uses for index features.
7
+ */
8
+ const FIELD_OPTION_FAMILY = {
9
+ name: '*',
10
+ isId: '*',
11
+ type: '*',
12
+ dimensions: 'vector',
13
+ distance: 'vector',
14
+ references: '*',
15
+ onDelete: '*',
16
+ enum: '*',
17
+ virtual: '*',
18
+ updatable: '*',
19
+ eager: '*',
20
+ onInsert: '*',
21
+ onUpdate: '*',
22
+ softDelete: '*',
23
+ columnType: '*',
24
+ length: 'string',
25
+ precision: 'numeric',
26
+ scale: 'numeric',
27
+ nullable: '*',
28
+ unique: '*',
29
+ defaultValue: '*',
30
+ autoIncrement: 'numeric',
31
+ index: '*',
32
+ comment: '*',
33
+ };
34
+ /**
35
+ * The only options a `virtual` field reaches: it is skipped in DDL and dropped from every insert and
36
+ * update, so the whole persistence half of the options above is dead on one. Stated as what survives
37
+ * rather than on each option that dies, because it is one fact rather than nineteen - and because an
38
+ * option added without a thought then lands on the safe side of it.
39
+ */
40
+ const VIRTUAL_READS = [
41
+ 'type',
42
+ 'virtual',
43
+ 'enum',
44
+ 'eager',
45
+ 'distance',
46
+ ];
47
+ /**
48
+ * Whatever leaves `key` unread, named for the message, or `undefined` where the field reads it. Only
49
+ * `nullable: true` contradicts a key: `nullable: false` says what the key already is, and rejecting
50
+ * an accurate statement teaches an author to distrust the check.
51
+ */
52
+ function deadOn(opts, key) {
53
+ if (opts.virtual !== undefined && !VIRTUAL_READS.some((read) => read === key))
54
+ return 'a virtual field';
55
+ if (opts.isId === true && key === 'nullable' && opts.nullable === true)
56
+ return 'a primary key';
57
+ if (opts.updatable === false && key === 'onUpdate')
58
+ return "a field declared 'updatable: false'";
59
+ return undefined;
60
+ }
61
+ /**
62
+ * The first option `opts` cannot use, phrased as the tail of `'Entity.field' ...`, or `undefined`
63
+ * where every option applies. The runtime half of the decorators' check, so the imperative API and
64
+ * plain JavaScript reach the same answer.
65
+ */
66
+ export function fieldOptionConflict(opts) {
67
+ const family = columnFamily(opts.columnType ?? opts.type);
68
+ // Walked in table order, not in the order the field happened to be written, so a field with two
69
+ // conflicts always reports the same one. An option no rule knows is a typo, which `RejectUnknown`
70
+ // reports where it can still be spelled right.
71
+ for (const key of getKeys(FIELD_OPTION_FAMILY)) {
72
+ const applies = FIELD_OPTION_FAMILY[key];
73
+ if (opts[key] === undefined)
74
+ continue;
75
+ if (family && applies !== '*' && applies !== family) {
76
+ return `cannot use '${key}': it applies to a ${applies} column, not to a ${family} one`;
77
+ }
78
+ const dead = deadOn(opts, key);
79
+ if (dead) {
80
+ return `cannot use '${key}': it is ignored on ${dead}`;
81
+ }
82
+ }
83
+ return undefined;
84
+ }
@@ -1,5 +1,6 @@
1
1
  export * from './dialect.util.js';
2
2
  export * from './field.util.js';
3
+ export * from './fieldOption.util.js';
3
4
  export * from './filters.util.js';
4
5
  export * from './hook.util.js';
5
6
  export * from './ddlExpression.util.js';
@@ -1,5 +1,6 @@
1
1
  export * from './dialect.util.js';
2
2
  export * from './field.util.js';
3
+ export * from './fieldOption.util.js';
3
4
  export * from './filters.util.js';
4
5
  export * from './hook.util.js';
5
6
  export * from './ddlExpression.util.js';
@@ -69,7 +69,15 @@ export function getFieldKeys(fields) {
69
69
  * is what a `$where` map and a composite key's id object both are; an array is a list of either.
70
70
  */
71
71
  export function isScalarId(value) {
72
- return (typeof value !== 'object' ||
73
- value === null ||
74
- (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype));
72
+ if (typeof value !== 'object' || value === null) {
73
+ return true;
74
+ }
75
+ if (Array.isArray(value)) {
76
+ return false;
77
+ }
78
+ // `null` as well as `Object.prototype`: an object with no prototype is what a query-string parser
79
+ // hands back (`qs`, express's `req.params`), and reading one as a bare id would name one column
80
+ // with a map of several.
81
+ const proto = Object.getPrototypeOf(value);
82
+ return proto !== Object.prototype && proto !== null;
75
83
  }
@@ -32,6 +32,16 @@ export declare function parentJoins(relOpts: Pick<RelationMeta, 'references' | '
32
32
  * which is the mistake this module exists to prevent.
33
33
  */
34
34
  export declare function targetKeyColumns(relOpts: Pick<RelationMeta, 'references'>, parentKeyCount: number): string[];
35
+ /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
36
+ export declare function joinedColumns(joins: readonly ParentJoin[]): Record<string, true>;
37
+ /**
38
+ * A parent row keyed by the columns a relation joins *from*, and a child or tally row keyed by the
39
+ * columns it carries that key in. The two halves of matching children to parents: they must agree on
40
+ * every column, so each is read through `joins` rather than through the parent's own key list - which
41
+ * is the same set only for a to-many, and silently a different one otherwise.
42
+ */
43
+ export declare function parentRowKey(joins: readonly ParentJoin[], parent: unknown): string;
44
+ export declare function joinedRowKey(joins: readonly ParentJoin[], row: unknown): string;
35
45
  /**
36
46
  * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
37
47
  * children in one statement.
@@ -1,5 +1,6 @@
1
1
  import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, } from '../type/query.js';
2
2
  import { getKeys, someKey } from './object.util.js';
3
+ import { rowKey } from './rowKey.util.js';
3
4
  /**
4
5
  * Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
5
6
  * the one field it reads, so it answers for a relation being declared as well as for a resolved one.
@@ -34,6 +35,22 @@ export function parentJoins(relOpts, parentKeyCount) {
34
35
  export function targetKeyColumns(relOpts, parentKeyCount) {
35
36
  return relOpts.references.slice(parentKeyCount).map(({ local }) => local);
36
37
  }
38
+ /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
39
+ export function joinedColumns(joins) {
40
+ return Object.fromEntries(joins.map(({ joined }) => [joined, true]));
41
+ }
42
+ /**
43
+ * A parent row keyed by the columns a relation joins *from*, and a child or tally row keyed by the
44
+ * columns it carries that key in. The two halves of matching children to parents: they must agree on
45
+ * every column, so each is read through `joins` rather than through the parent's own key list - which
46
+ * is the same set only for a to-many, and silently a different one otherwise.
47
+ */
48
+ export function parentRowKey(joins, parent) {
49
+ return rowKey(joins.map(({ parent: key }) => read(parent, key)));
50
+ }
51
+ export function joinedRowKey(joins, row) {
52
+ return rowKey(joins.map(({ joined }) => read(row, joined)));
53
+ }
37
54
  /**
38
55
  * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
39
56
  * children in one statement.
@@ -43,7 +60,7 @@ export function targetKeyColumns(relOpts, parentKeyCount) {
43
60
  * cheaper than the row-value comparison no engine spells the same way.
44
61
  */
45
62
  export function parentsIn(joins, parents) {
46
- return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => it[parent])]));
63
+ return Object.fromEntries(joins.map(({ parent, joined }) => [joined, parents.map((it) => read(it, parent))]));
47
64
  }
48
65
  /**
49
66
  * The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
@@ -58,9 +75,12 @@ export function childrenOf(joins, parentIds) {
58
75
  return { [first.joined]: parentIds };
59
76
  }
60
77
  return {
61
- $or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, id[parent]]))),
78
+ $or: parentIds.map((id) => Object.fromEntries(joins.map(({ parent, joined }) => [joined, read(id, parent)]))),
62
79
  };
63
80
  }
81
+ function read(row, key) {
82
+ return row[key];
83
+ }
64
84
  /**
65
85
  * What a joined relation cannot carry, and why. A to-many is loaded by a query of its own, which is
66
86
  * what gives these four a meaning there; a to-one is one row of the parent's, so every backend used
@@ -13,20 +13,41 @@ export declare function obtainAttrsPaths<T extends object>(row: T): {
13
13
  * A name behind its namespace, or bare where there is none: the one place the two are joined, so a
14
14
  * table's key, its statement operand and its escaped form cannot spell it differently. Never the
15
15
  * seed for a derived identifier - an index or constraint name is a single identifier, and
16
- * `idx_sales.Order_total` is a syntax error.
16
+ * `sales.Order_total_idx` is a syntax error.
17
17
  */
18
18
  export declare function qualifyName(name: string, schema?: string): string;
19
19
  /**
20
- * The name a derived index or constraint gets when nothing named it: `idx_Order_total`.
20
+ * The name a derived index or constraint gets when nothing named it: `Order__total_idx`.
21
21
  *
22
- * One owner, because it is a rule two layers apply and a third has to match: the entity AST derives
23
- * it, the DDL generator falls back to it, and a diff compares what the database reports against it.
22
+ * One owner for all four kinds, because it is a rule two layers apply and a third has to match: the
23
+ * entity AST derives it, the DDL generator falls back to it, and a `DROP` names what it drops.
24
24
  * `table` is the table's own name, never qualified - the result is a single identifier.
25
+ *
26
+ * The kind goes last, as Postgres spells its own (`users_pkey`, `users_email_idx`), so a table's
27
+ * constraints sort together under the table they belong to.
28
+ *
29
+ * Not overridable, deliberately: a `NamingStrategy` hook would have to reach the eight call sites
30
+ * these have, an introspector and two builders among them, to replace a name any declaration can
31
+ * already set outright with `name:`. Worth revisiting only for a case that option cannot express.
32
+ */
33
+ export declare function derivedConstraintName(table: string, parts: readonly (string | number)[], kind: ConstraintKind): string;
34
+ /** The kinds of derived name, which is also what `indexNameStem` strips to compare them. */
35
+ export type ConstraintKind = 'pk' | 'fk' | 'idx' | 'ck' | 'uk';
36
+ /**
37
+ * The name a derived index gets when nothing named it: `Order__total_idx`, or `Order__total_uk` for a
38
+ * unique one - which the builder has always spelled apart, and which reads as what it enforces.
39
+ */
40
+ export declare function derivedIndexName(table: string, columns: readonly string[], unique?: boolean): string;
41
+ /**
42
+ * The constraint name a primary key gets when we name one: `Enrolment__studentId_courseId_pk`.
43
+ *
44
+ * Only ever used to *emit* a key. Which columns a key holds is what decides whether two keys are the
45
+ * same, so an existing constraint keeps whatever the engine called it - see `SchemaDiff.primaryKey`.
25
46
  */
26
- export declare function derivedIndexName(table: string, columns: readonly string[]): string;
27
- /** The constraint name a check gets when nothing named it: `ck_Order_1`, by declaration order. */
47
+ export declare function derivedPrimaryKeyName(table: string, columns: readonly string[]): string;
48
+ /** The constraint name a check gets when nothing named it: `Order__1_ck`, by declaration order. */
28
49
  export declare function derivedCheckName(table: string, position: number): string;
29
- /** The constraint name a foreign key gets when nothing named it: `fk_Order_customerId`. */
50
+ /** The constraint name a foreign key gets when nothing named it: `Order__customerId_fk`. */
30
51
  export declare function derivedForeignKeyName(table: string, columns: readonly string[]): string;
31
52
  /**
32
53
  * Escape a SQL identifier (table name, column name, etc.)
@@ -52,28 +52,97 @@ export function obtainAttrsPaths(row) {
52
52
  * A name behind its namespace, or bare where there is none: the one place the two are joined, so a
53
53
  * table's key, its statement operand and its escaped form cannot spell it differently. Never the
54
54
  * seed for a derived identifier - an index or constraint name is a single identifier, and
55
- * `idx_sales.Order_total` is a syntax error.
55
+ * `sales.Order_total_idx` is a syntax error.
56
56
  */
57
57
  export function qualifyName(name, schema) {
58
58
  return schema ? `${schema}.${name}` : name;
59
59
  }
60
60
  /**
61
- * The name a derived index or constraint gets when nothing named it: `idx_Order_total`.
61
+ * The longest identifier every engine here accepts. Postgres truncates silently at 63 bytes and
62
+ * MySQL errors at 64, so one conservative limit needs no per-dialect plumbing to be safe on both -
63
+ * and SQLite, which has no limit, loses nothing by observing it.
64
+ */
65
+ const MAX_IDENTIFIER_LENGTH = 63;
66
+ /** Hex chars of hash kept when a name has to be shortened. 24 bits over one table's constraints. */
67
+ const NAME_HASH_LENGTH = 6;
68
+ /**
69
+ * The name a derived index or constraint gets when nothing named it: `Order__total_idx`.
62
70
  *
63
- * One owner, because it is a rule two layers apply and a third has to match: the entity AST derives
64
- * it, the DDL generator falls back to it, and a diff compares what the database reports against it.
71
+ * One owner for all four kinds, because it is a rule two layers apply and a third has to match: the
72
+ * entity AST derives it, the DDL generator falls back to it, and a `DROP` names what it drops.
65
73
  * `table` is the table's own name, never qualified - the result is a single identifier.
74
+ *
75
+ * The kind goes last, as Postgres spells its own (`users_pkey`, `users_email_idx`), so a table's
76
+ * constraints sort together under the table they belong to.
77
+ *
78
+ * Not overridable, deliberately: a `NamingStrategy` hook would have to reach the eight call sites
79
+ * these have, an introspector and two builders among them, to replace a name any declaration can
80
+ * already set outright with `name:`. Worth revisiting only for a case that option cannot express.
81
+ */
82
+ export function derivedConstraintName(table, parts, kind) {
83
+ const body = parts.length ? `${table}${TABLE_SEPARATOR}${parts.join('_')}` : table;
84
+ return clampIdentifier(`${body}_${kind}`);
85
+ }
86
+ /**
87
+ * What separates the table from the columns, doubled where every other join is single.
88
+ *
89
+ * Postgres and SQLite keep index and constraint names in one flat namespace across the whole
90
+ * database rather than scoping them to a table, so a single underscore lets two tables collide:
91
+ * `user` + `profile_id` and `user_profile` + `id` both reduce to `user_profile_id_idx`. Doubling the
92
+ * one ambiguous boundary settles it, on the same assumption Drupal made for the same engines - that
93
+ * nothing sane carries `__` in a table or column name.
94
+ */
95
+ const TABLE_SEPARATOR = '__';
96
+ /**
97
+ * A name the engine will store whole, shortened around a hash of the full one when it is too long.
98
+ *
99
+ * Truncating alone collides - two long names over the same table differ only in their tail - and a
100
+ * collision means one constraint silently replacing another. The hash is of the *whole* name, so it
101
+ * stays the same on every run, which is what lets a later migration still recognise what it made.
102
+ */
103
+ function clampIdentifier(name) {
104
+ if (name.length <= MAX_IDENTIFIER_LENGTH) {
105
+ return name;
106
+ }
107
+ const suffix = `_${hashIdentifier(name)}`;
108
+ return name.slice(0, MAX_IDENTIFIER_LENGTH - suffix.length) + suffix;
109
+ }
110
+ /**
111
+ * FNV-1a, by hand: the package ships zero runtime dependencies, and `node:crypto` is not reachable
112
+ * from the browser and edge entries this module is bundled into. Not a security hash - it only has
113
+ * to spread the names of one table's constraints.
114
+ */
115
+ function hashIdentifier(value) {
116
+ let hash = 0x811c9dc5;
117
+ for (let i = 0; i < value.length; i++) {
118
+ hash ^= value.charCodeAt(i);
119
+ hash = Math.imul(hash, 0x01000193) >>> 0;
120
+ }
121
+ return hash.toString(16).padStart(NAME_HASH_LENGTH, '0').slice(-NAME_HASH_LENGTH);
122
+ }
123
+ /**
124
+ * The name a derived index gets when nothing named it: `Order__total_idx`, or `Order__total_uk` for a
125
+ * unique one - which the builder has always spelled apart, and which reads as what it enforces.
126
+ */
127
+ export function derivedIndexName(table, columns, unique = false) {
128
+ return derivedConstraintName(table, columns, unique ? 'uk' : 'idx');
129
+ }
130
+ /**
131
+ * The constraint name a primary key gets when we name one: `Enrolment__studentId_courseId_pk`.
132
+ *
133
+ * Only ever used to *emit* a key. Which columns a key holds is what decides whether two keys are the
134
+ * same, so an existing constraint keeps whatever the engine called it - see `SchemaDiff.primaryKey`.
66
135
  */
67
- export function derivedIndexName(table, columns) {
68
- return `idx_${table}_${columns.join('_')}`;
136
+ export function derivedPrimaryKeyName(table, columns) {
137
+ return derivedConstraintName(table, columns, 'pk');
69
138
  }
70
- /** The constraint name a check gets when nothing named it: `ck_Order_1`, by declaration order. */
139
+ /** The constraint name a check gets when nothing named it: `Order__1_ck`, by declaration order. */
71
140
  export function derivedCheckName(table, position) {
72
- return `ck_${table}_${position}`;
141
+ return derivedConstraintName(table, [position], 'ck');
73
142
  }
74
- /** The constraint name a foreign key gets when nothing named it: `fk_Order_customerId`. */
143
+ /** The constraint name a foreign key gets when nothing named it: `Order__customerId_fk`. */
75
144
  export function derivedForeignKeyName(table, columns) {
76
- return `fk_${table}_${columns.join('_')}`;
145
+ return derivedConstraintName(table, columns, 'fk');
77
146
  }
78
147
  /**
79
148
  * Escape a SQL identifier (table name, column name, etc.)
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "uql-orm",
3
3
  "homepage": "https://uql-orm.dev",
4
- "description": "JSON-native ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
4
+ "description": "JSON-native TypeScript ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.42.0",
6
+ "version": "0.43.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"