uql-orm 0.45.0 → 0.46.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 (42) hide show
  1. package/README.md +1 -1
  2. package/dist/dialect/abstractSqlDialect.d.ts +9 -2
  3. package/dist/dialect/abstractSqlDialect.js +25 -16
  4. package/dist/dialect/mysqlLikeSqlDialect.js +2 -1
  5. package/dist/dialect/pgLikeSqlDialect.js +2 -1
  6. package/dist/entity/metadata/definition.js +8 -1
  7. package/dist/migrate/builder/columnBuilder.d.ts +14 -0
  8. package/dist/migrate/builder/columnBuilder.js +28 -9
  9. package/dist/migrate/builder/tableBuilder.js +8 -19
  10. package/dist/migrate/builder/types.d.ts +16 -33
  11. package/dist/migrate/codegen/entityCodeGenerator.js +4 -5
  12. package/dist/migrate/codegen/fieldOptionsSource.d.ts +2 -0
  13. package/dist/migrate/codegen/fieldOptionsSource.js +49 -33
  14. package/dist/migrate/codegen/indexDecoratorSource.js +1 -9
  15. package/dist/migrate/codegen/sourceLiteral.d.ts +12 -0
  16. package/dist/migrate/codegen/sourceLiteral.js +17 -0
  17. package/dist/migrate/generator/definitionToNode.d.ts +21 -0
  18. package/dist/migrate/generator/definitionToNode.js +47 -25
  19. package/dist/migrate/introspection/baseSqlIntrospector.d.ts +4 -2
  20. package/dist/migrate/introspection/baseSqlIntrospector.js +11 -11
  21. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  22. package/dist/migrate/introspection/mongoIntrospector.js +2 -2
  23. package/dist/migrate/introspection/sqliteIntrospector.d.ts +5 -0
  24. package/dist/migrate/introspection/sqliteIntrospector.js +6 -2
  25. package/dist/migrate/schemaGenerator.d.ts +48 -2
  26. package/dist/migrate/schemaGenerator.js +120 -38
  27. package/dist/mongo/mongoDialect.js +2 -1
  28. package/dist/schema/schemaAST.d.ts +34 -3
  29. package/dist/schema/schemaAST.js +4 -9
  30. package/dist/schema/schemaASTBuilder.js +5 -2
  31. package/dist/schema/schemaASTDiffer.js +8 -4
  32. package/dist/schema/types.d.ts +2 -0
  33. package/dist/sqlite/sqliteDialect.js +2 -1
  34. package/dist/type/dialect.d.ts +21 -2
  35. package/dist/type/entity.d.ts +21 -0
  36. package/dist/type/migration.d.ts +11 -11
  37. package/dist/util/dialect.util.js +5 -4
  38. package/dist/util/field.util.d.ts +23 -0
  39. package/dist/util/field.util.js +28 -0
  40. package/dist/util/fieldOption.util.d.ts +16 -3
  41. package/dist/util/fieldOption.util.js +23 -4
  42. package/package.json +1 -1
@@ -6,6 +6,8 @@
6
6
  * - Database introspection results (TableSchema[])
7
7
  */
8
8
  import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
9
+ import { ddlText } from '../util/ddlExpression.util.js';
10
+ import { computedExpression, isInlinedExpression } from '../util/field.util.js';
9
11
  import { isSoleIdField } from '../util/field.util.js';
10
12
  import { isAutoIncrement } from '../util/field.util.js';
11
13
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
@@ -82,8 +84,8 @@ function addTableFromEntity(ctx, meta) {
82
84
  const field = fields[key];
83
85
  if (!field)
84
86
  continue;
85
- // Skip virtual fields
86
- if (field.virtual)
87
+ // An inlined expression has no column; a stored one is a column like any other.
88
+ if (isInlinedExpression(field))
87
89
  continue;
88
90
  const columnName = ctx.resolveColumnName(key, field);
89
91
  const type = resolveColumnCanonicalType(field);
@@ -99,6 +101,7 @@ function addTableFromEntity(ctx, meta) {
99
101
  isPrimaryKey,
100
102
  isAutoIncrement: isAutoIncrement(field, isSoleKey),
101
103
  isUnique: field.unique ?? false,
104
+ generatedAs: ddlText(computedExpression(field), `the computed column '${columnName}'`),
102
105
  comment: field.comment,
103
106
  enum: field.enum,
104
107
  table,
@@ -196,10 +196,14 @@ function diffColumn(tableName, source, target, opts) {
196
196
  if (source.isUnique !== target.isUnique) {
197
197
  differences.push(`unique: ${target.isUnique} → ${source.isUnique}`);
198
198
  }
199
- // Auto-increment is deliberately not compared. No engine turns a column into an identity, or out of
200
- // one, without rewriting the table, and there is no DDL here that does it - so a difference could
201
- // only ever be reported, never settled, and the statements emitted for it (a bare `ALTER COLUMN
202
- // TYPE`) do not change it. The same rule `describeIndexDifferences` follows for what it cannot read.
199
+ // Four things are deliberately not compared, all for one reason: a difference here could only be
200
+ // reported, never settled, because no statement this generator emits would change it.
201
+ // - `isAutoIncrement`: no engine makes a column an identity, or unmakes one, without rewriting the
202
+ // table, and the alter emitted for it is a bare `ALTER COLUMN TYPE`.
203
+ // - `enum`: a check, and a database reprints one from its parse tree. See the roadmap.
204
+ // - `generatedAs`: only Postgres 17 and the MySQL family can rewrite an expression in place.
205
+ // - `comment`: fixable on every engine but SQLite, and the only one of the four worth revisiting.
206
+ // The same rule `describeIndexDifferences` follows for what it cannot read.
203
207
  // Compare default values (if both defined)
204
208
  if (!opts.defaultsEqual(source.defaultValue, target.defaultValue)) {
205
209
  differences.push(`default: ${target.defaultValue ?? 'NULL'} → ${source.defaultValue ?? 'NULL'}`);
@@ -106,6 +106,8 @@ export interface ColumnNode {
106
106
  readonly isUnique: boolean;
107
107
  /** The values the column accepts. See {@link EnumValues}. */
108
108
  readonly enum?: EnumValues;
109
+ /** The SQL an engine-generated column is computed from, as `GENERATED ALWAYS AS (...) STORED`. */
110
+ readonly generatedAs?: string;
109
111
  /** Column comment/description */
110
112
  readonly comment?: string;
111
113
  /** Reference to the parent table */
@@ -14,7 +14,8 @@ export class SqliteDialect extends AbstractSqlDialect {
14
14
  renameColumn: true,
15
15
  foreignKeyAlter: false, // SQLite does not support adding FKs to existing tables
16
16
  primaryKeyAlter: false, // nor changing a key: the only route is rebuilding the table
17
- columnComment: false, // SQLite does not support column comments
17
+ generatedColumnAdd: false, // accepted in a CREATE TABLE, rejected in an ALTER
18
+ commentSyntax: 'none',
18
19
  vectorIndexRequiresNotNull: false,
19
20
  vectorSupportsLength: false,
20
21
  supportsTimestamptz: false,
@@ -97,8 +97,27 @@ export interface EngineFeatures {
97
97
  * than emitting DDL the engine rejects.
98
98
  */
99
99
  readonly primaryKeyAlter: boolean;
100
- /** Whether the dialect supports inline COMMENT on columns (MySQL/MariaDB). */
101
- readonly columnComment: boolean;
100
+ /**
101
+ * Whether a stored generated column can be added to a table that already exists. False on SQLite,
102
+ * which takes one in a `CREATE TABLE` and rejects the same column in an `ALTER` ("cannot add a
103
+ * STORED column"), since filling it would rewrite every row - so a sync that would add one is
104
+ * refused by name rather than by the driver.
105
+ *
106
+ * A boolean rather than a mode: SQLite would accept a `VIRTUAL` column here, but emitting one where
107
+ * the entity said `stored` makes the same entity a stored column on a new database and a virtual one
108
+ * on an old, which nothing diffs and no one can see. Refusal has one form; only what UQL *emits*
109
+ * earns a mode, which is what makes {@link EngineFeatures.commentSyntax} three-way.
110
+ */
111
+ readonly generatedColumnAdd: boolean;
112
+ /**
113
+ * How this engine carries a comment on a table or a column, if at all: `inline` writes it into the
114
+ * declaration (MySQL, MariaDB), `statement` needs a `COMMENT ON` of its own (Postgres, CockroachDB),
115
+ * `none` has no such thing (SQLite, MongoDB).
116
+ *
117
+ * One knob rather than a boolean because the answer is three-way. Read as a boolean, Postgres landed
118
+ * on the same branch as SQLite and a documented column silently lost its comment.
119
+ */
120
+ readonly commentSyntax: 'inline' | 'statement' | 'none';
102
121
  /**
103
122
  * Whether every column of a vector index has to be `NOT NULL`, which MariaDB 12.3 enforces ("All
104
123
  * parts of a VECTOR index must be NOT NULL") and CockroachDB 26.3 does not - so being indexed, not
@@ -344,7 +344,28 @@ export type FieldOptions<V = TsTypeOf<FieldType>> = {
344
344
  * @example `@Field({ type: String, enum: ['draft', 'paid'] as const })`
345
345
  */
346
346
  readonly enum?: EnumValues;
347
+ /**
348
+ * @deprecated Renamed to {@link FieldOptions.computed}, which also takes `stored`. `npx uql-codemod`
349
+ * rewrites it. Giving both throws.
350
+ */
347
351
  readonly virtual?: QueryRaw;
352
+ /**
353
+ * An expression the database computes, rather than a value the caller writes. Never part of an
354
+ * insert or update either way.
355
+ *
356
+ * Unstored, it is spliced into each statement that reads the field, so nothing is persisted and any
357
+ * expression will do. With `stored`, it becomes a real column - `GENERATED ALWAYS AS (...) STORED` -
358
+ * which the engine keeps up to date, so it can be indexed and read like any other.
359
+ *
360
+ * @example `@Field({ type: String, computed: raw`"first" || ' ' || "last"`, stored: true })`
361
+ */
362
+ readonly computed?: QueryRaw;
363
+ /**
364
+ * Whether {@link FieldOptions.computed} is a column the database keeps, rather than an expression
365
+ * spliced into each statement. The dial to flip after profiling: `$select`, `$where` and `$sort`
366
+ * read the field the same way either side of it, so no call site changes.
367
+ */
368
+ readonly stored?: boolean;
348
369
  readonly updatable?: boolean;
349
370
  readonly eager?: boolean;
350
371
  readonly onInsert?: OnFieldCallback<V>;
@@ -2,7 +2,7 @@ import type { VectorCast } from '../dialect/vectorCast.js';
2
2
  import type { FullColumnDefinition, TableDefinition } from '../migrate/builder/types.js';
3
3
  import type { IndexFacet } from '../schema/indexDifferences.js';
4
4
  import type { SchemaAST } from '../schema/schemaAST.js';
5
- import type { ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
5
+ import type { ColumnNode, ForeignKeyAction, IndexNode, IndexType, TableNode } from '../schema/types.js';
6
6
  import type { EntityMeta, FieldOptions, IndexColumnSchema, LoggingOptions, SqlQuerier, Type, VectorIndexOptions } from './index.js';
7
7
  /**
8
8
  * Defines a migration using a simple object literal
@@ -95,10 +95,14 @@ export interface MigrationResult {
95
95
  readonly error?: Error;
96
96
  }
97
97
  /**
98
- * Represents a column in a database table schema
98
+ * A column as a statement describes one: {@link ColumnNode} with the engine's type spelling in place
99
+ * of the canonical one, and without the graph links.
100
+ *
101
+ * Derived so a field the node gains reaches every path that renders a column. Listed field by field,
102
+ * this dropped `enum` and then `generatedAs`, and a column added to an existing table arrived without
103
+ * the constraint or the expression the entity declared.
99
104
  */
100
- export interface ColumnSchema {
101
- readonly name: string;
105
+ export interface ColumnSchema extends Omit<ColumnNode, 'type' | 'table' | 'referencedBy' | 'references'> {
102
106
  /**
103
107
  * The engine's own type spelling, as introspection read it (`tinyint(1)`, `DATETIME`, `VARCHAR`).
104
108
  * Deliberately not a {@link CanonicalType}: the diff has to compare what the engine would *store*,
@@ -107,15 +111,10 @@ export interface ColumnSchema {
107
111
  * every sync for those columns. Use `sqlToCanonical` to interpret it.
108
112
  */
109
113
  readonly type: string;
110
- readonly nullable: boolean;
111
- readonly defaultValue?: unknown;
112
- readonly isPrimaryKey: boolean;
113
- readonly isAutoIncrement: boolean;
114
- readonly isUnique: boolean;
114
+ /** Bounds introspection reports beside the type, where the engine states them separately. */
115
115
  readonly length?: number;
116
116
  readonly precision?: number;
117
117
  readonly scale?: number;
118
- readonly comment?: string;
119
118
  }
120
119
  /**
121
120
  * Represents a database table schema
@@ -376,7 +375,8 @@ export interface SchemaIntrospector {
376
375
  /**
377
376
  * Introspect entire database schema and return SchemaAST.
378
377
  */
379
- introspect(): Promise<SchemaAST>;
378
+ /** The whole database, or just the tables named. Names nothing matches are left out. */
379
+ introspect(tables?: readonly string[]): Promise<SchemaAST>;
380
380
  /**
381
381
  * Get all table names in the database
382
382
  */
@@ -2,19 +2,20 @@ import { getContext, UqlSecurityError } from '../context/context.js';
2
2
  import { soleIdOf } from '../entity/metadata/definition.js';
3
3
  import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
4
4
  import { VECTOR_INDEX_TYPES } from '../type/vector.js';
5
+ import { isDatabaseWritten } from './field.util.js';
5
6
  import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, someKey } from './object.util.js';
6
7
  export function filterFieldKeys(meta, payload, callbackKey) {
7
8
  return getKeys(payload).filter((key) => {
8
9
  const fieldOpts = meta.fields[key];
9
- return fieldOpts && !fieldOpts.virtual && (callbackKey !== 'onUpdate' || fieldOpts.updatable !== false);
10
+ return fieldOpts && !isDatabaseWritten(fieldOpts) && (callbackKey !== 'onUpdate' || fieldOpts.updatable !== false);
10
11
  });
11
12
  }
12
- /** Whether `key` is a real, non-virtual field that `record` provides a defined value for. */
13
+ /** Whether `key` is a field the caller writes, and `record` provides a defined value for. */
13
14
  function isInsertableField(meta, record, key) {
14
15
  const field = meta.fields[key];
15
- return !!field && !field.virtual && record[key] !== undefined;
16
+ return !!field && !isDatabaseWritten(field) && record[key] !== undefined;
16
17
  }
17
- /** Appends `record`'s not-yet-`seen` insertable keys (real, non-virtual, defined value) to `keys`. */
18
+ /** Appends `record`'s not-yet-`seen` insertable keys (real, caller-written, defined value) to `keys`. */
18
19
  function addInsertFieldKeys(meta, record, seen, keys) {
19
20
  for (const key of getKeys(record)) {
20
21
  if (!seen.has(key) && isInsertableField(meta, record, key)) {
@@ -1,4 +1,5 @@
1
1
  import type { EntityMeta, FieldOptions } from '../type/index.js';
2
+ import type { QueryRaw } from '../type/queryRaw.js';
2
3
  /**
3
4
  * The kind of column a field lands on, which is what decides whether an option means anything on it:
4
5
  * `length` is a string's, `precision` a number's, `dimensions` a vector's. Named in the words an
@@ -22,6 +23,28 @@ export declare const COLUMN_TYPES_BY_FAMILY: {
22
23
  };
23
24
  /** The family of a logical field type, or `undefined` where it names none. */
24
25
  export declare function columnFamily(type: unknown): ColumnFamily | undefined;
26
+ /**
27
+ * The expression the database computes for this field, whichever key declared it.
28
+ *
29
+ * `virtual` is `computed` under its old name and is read here so both spell one behaviour. Giving
30
+ * both is refused at registration rather than resolved, since only the author knows which was meant.
31
+ */
32
+ export declare function computedExpression(field: FieldOptions): QueryRaw | undefined;
33
+ /**
34
+ * Whether the field's expression is spliced into each statement that reads it, rather than stored.
35
+ *
36
+ * One of the two questions `virtual` used to answer alone. Every read site asks this - the DDL skip,
37
+ * the projection, the `$where` operand, the `ORDER BY` operand - because an inlined field has no
38
+ * column to name, while a stored one is read exactly like any other.
39
+ */
40
+ export declare function isInlinedExpression(field: FieldOptions): boolean;
41
+ /**
42
+ * Whether the database supplies this field's value, so no insert or update may write it.
43
+ *
44
+ * The other question, and the one that makes `stored` more than a rename: a stored computed column
45
+ * *is* a real column, so it is read like one - but writing to it is an error on every engine.
46
+ */
47
+ export declare function isDatabaseWritten(field: FieldOptions): boolean;
25
48
  /**
26
49
  * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
27
50
  * and the only one that may state `PRIMARY KEY` in its own column definition.
@@ -46,6 +46,34 @@ for (const family of getKeys(COLUMN_TYPES_BY_FAMILY)) {
46
46
  export function columnFamily(type) {
47
47
  return FAMILY_OF.get(typeof type === 'string' ? type.toLowerCase() : type);
48
48
  }
49
+ /**
50
+ * The expression the database computes for this field, whichever key declared it.
51
+ *
52
+ * `virtual` is `computed` under its old name and is read here so both spell one behaviour. Giving
53
+ * both is refused at registration rather than resolved, since only the author knows which was meant.
54
+ */
55
+ export function computedExpression(field) {
56
+ return field.computed ?? field.virtual;
57
+ }
58
+ /**
59
+ * Whether the field's expression is spliced into each statement that reads it, rather than stored.
60
+ *
61
+ * One of the two questions `virtual` used to answer alone. Every read site asks this - the DDL skip,
62
+ * the projection, the `$where` operand, the `ORDER BY` operand - because an inlined field has no
63
+ * column to name, while a stored one is read exactly like any other.
64
+ */
65
+ export function isInlinedExpression(field) {
66
+ return computedExpression(field) !== undefined && field.stored !== true;
67
+ }
68
+ /**
69
+ * Whether the database supplies this field's value, so no insert or update may write it.
70
+ *
71
+ * The other question, and the one that makes `stored` more than a rename: a stored computed column
72
+ * *is* a real column, so it is read like one - but writing to it is an error on every engine.
73
+ */
74
+ export function isDatabaseWritten(field) {
75
+ return computedExpression(field) !== undefined;
76
+ }
49
77
  /**
50
78
  * Whether the field is the entity's *whole* primary key - the only kind a serial can stand in for,
51
79
  * and the only one that may state `PRIMARY KEY` in its own column definition.
@@ -15,6 +15,8 @@ declare const FIELD_OPTION_FAMILY: {
15
15
  readonly onDelete: '*';
16
16
  readonly enum: '*';
17
17
  readonly virtual: '*';
18
+ readonly computed: '*';
19
+ readonly stored: '*';
18
20
  readonly updatable: '*';
19
21
  readonly eager: '*';
20
22
  readonly onInsert: '*';
@@ -37,8 +39,15 @@ declare const FIELD_OPTION_FAMILY: {
37
39
  * rather than on each option that dies, because it is one fact rather than nineteen - and because an
38
40
  * option added without a thought then lands on the safe side of it.
39
41
  */
40
- declare const VIRTUAL_READS: readonly ["type", "virtual", "enum", "eager", "distance"];
41
- type VirtualRead = (typeof VIRTUAL_READS)[number];
42
+ declare const INLINE_READS: readonly ["type", "virtual", "computed", "stored", "enum", "eager", "distance"];
43
+ type InlineRead = (typeof INLINE_READS)[number];
44
+ /**
45
+ * What a column the *database* writes cannot use. A stored computed column is a real column - it has
46
+ * DDL, an index, a comment, a name - so only the write half is dead on one: the engine fills it, and
47
+ * `GENERATED ALWAYS AS` and `DEFAULT` are mutually exclusive on every engine that has both.
48
+ */
49
+ declare const GENERATED_WRITES: readonly ["updatable", "onInsert", "onUpdate", "softDelete", "defaultValue", "autoIncrement"];
50
+ type GeneratedWrite = (typeof GENERATED_WRITES)[number];
42
51
  /**
43
52
  * The first option `opts` cannot use, phrased as the tail of `'Entity.field' ...`, or `undefined`
44
53
  * where every option applies. The runtime half of the decorators' check, so the imperative API and
@@ -55,8 +64,12 @@ type FamilyOf<O> = O extends {
55
64
  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
65
  /** What the field's own values leave unread, matching {@link deadOn} line for line. */
57
66
  type DeadOptions<O> = (O extends {
67
+ readonly stored: true;
68
+ } ? GeneratedWrite : O extends {
58
69
  readonly virtual: QueryRaw;
59
- } ? Exclude<keyof FieldOptions, VirtualRead> : never) | (O extends {
70
+ } | {
71
+ readonly computed: QueryRaw;
72
+ } ? Exclude<keyof FieldOptions, InlineRead> : never) | (O extends {
60
73
  readonly isId: true;
61
74
  readonly nullable: true;
62
75
  } ? 'nullable' : never) | (O extends {
@@ -1,4 +1,4 @@
1
- import { columnFamily } from './field.util.js';
1
+ import { columnFamily, isInlinedExpression } from './field.util.js';
2
2
  import { getKeys } from './object.util.js';
3
3
  /**
4
4
  * The column family each field option means anything on, or `'*'` where it applies to every column.
@@ -15,6 +15,8 @@ const FIELD_OPTION_FAMILY = {
15
15
  onDelete: '*',
16
16
  enum: '*',
17
17
  virtual: '*',
18
+ computed: '*',
19
+ stored: '*',
18
20
  updatable: '*',
19
21
  eager: '*',
20
22
  onInsert: '*',
@@ -37,21 +39,38 @@ const FIELD_OPTION_FAMILY = {
37
39
  * rather than on each option that dies, because it is one fact rather than nineteen - and because an
38
40
  * option added without a thought then lands on the safe side of it.
39
41
  */
40
- const VIRTUAL_READS = [
42
+ const INLINE_READS = [
41
43
  'type',
42
44
  'virtual',
45
+ 'computed',
46
+ 'stored',
43
47
  'enum',
44
48
  'eager',
45
49
  'distance',
46
50
  ];
51
+ /**
52
+ * What a column the *database* writes cannot use. A stored computed column is a real column - it has
53
+ * DDL, an index, a comment, a name - so only the write half is dead on one: the engine fills it, and
54
+ * `GENERATED ALWAYS AS` and `DEFAULT` are mutually exclusive on every engine that has both.
55
+ */
56
+ const GENERATED_WRITES = [
57
+ 'updatable',
58
+ 'onInsert',
59
+ 'onUpdate',
60
+ 'softDelete',
61
+ 'defaultValue',
62
+ 'autoIncrement',
63
+ ];
47
64
  /**
48
65
  * Whatever leaves `key` unread, named for the message, or `undefined` where the field reads it. Only
49
66
  * `nullable: true` contradicts a key: `nullable: false` says what the key already is, and rejecting
50
67
  * an accurate statement teaches an author to distrust the check.
51
68
  */
52
69
  function deadOn(opts, key) {
53
- if (opts.virtual !== undefined && !VIRTUAL_READS.some((read) => read === key))
54
- return 'a virtual field';
70
+ if (isInlinedExpression(opts) && !INLINE_READS.some((read) => read === key))
71
+ return 'an inlined computed field';
72
+ if (opts.stored === true && GENERATED_WRITES.some((write) => write === key))
73
+ return 'a stored computed column';
55
74
  if (opts.isId === true && key === 'nullable' && opts.nullable === true)
56
75
  return 'a primary key';
57
76
  if (opts.updatable === false && key === 'onUpdate')
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. 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.45.0",
6
+ "version": "0.46.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"