uql-orm 0.81.0 → 0.83.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 (113) hide show
  1. package/README.md +3 -3
  2. package/dist/browser/querier/httpQuerier.d.ts +2 -2
  3. package/dist/browser/querier/httpQuerier.js +2 -1
  4. package/dist/browser/type/clientQuerier.d.ts +2 -2
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +9 -8
  7. package/dist/bunSql/bunSql.util.js +2 -1
  8. package/dist/cockroachdb/crdbQuerierPool.js +2 -2
  9. package/dist/dialect/abstractSqlDialect.d.ts +16 -6
  10. package/dist/dialect/abstractSqlDialect.js +110 -41
  11. package/dist/dialect/hydrateColumn.js +2 -12
  12. package/dist/dialect/mysqlLikeSqlDialect.d.ts +2 -0
  13. package/dist/dialect/mysqlLikeSqlDialect.js +5 -0
  14. package/dist/dialect/operators.d.ts +7 -1
  15. package/dist/dialect/operators.js +13 -1
  16. package/dist/dialect/pgLikeSqlDialect.d.ts +1 -0
  17. package/dist/dialect/pgLikeSqlDialect.js +13 -4
  18. package/dist/entity/metadata/definition.d.ts +1 -2
  19. package/dist/entity/metadata/definition.js +37 -39
  20. package/dist/http/handler.js +5 -4
  21. package/dist/http/query.d.ts +1 -1
  22. package/dist/http/query.js +2 -2
  23. package/dist/index.d.ts +1 -0
  24. package/dist/index.js +1 -0
  25. package/dist/maria/mariadbQuerierPool.js +4 -2
  26. package/dist/migrate/acquireQuerierForMigrations.js +2 -1
  27. package/dist/migrate/assertCliConfig.js +7 -6
  28. package/dist/migrate/bin.js +0 -0
  29. package/dist/migrate/builder/expressions.d.ts +2 -0
  30. package/dist/migrate/builder/expressions.js +20 -10
  31. package/dist/migrate/builder/tableBuilder.js +1 -1
  32. package/dist/migrate/cli-config.js +5 -4
  33. package/dist/migrate/ddl/indexDdl.js +4 -3
  34. package/dist/migrate/ddl/mysqlIndexDdl.js +5 -4
  35. package/dist/migrate/ddl/pgIndexDdl.js +2 -1
  36. package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
  37. package/dist/migrate/ddl/tableDdl.js +2 -1
  38. package/dist/migrate/generator/mongoCommand.js +2 -1
  39. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
  40. package/dist/migrate/generator/mongoSchemaGenerator.js +7 -6
  41. package/dist/migrate/indexPredicate.js +2 -1
  42. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +2 -1
  43. package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
  44. package/dist/migrate/introspection/mongoIntrospector.js +3 -2
  45. package/dist/migrate/introspection/mssqlIntrospector.js +13 -1
  46. package/dist/migrate/introspection/mysqlIntrospector.d.ts +2 -0
  47. package/dist/migrate/introspection/mysqlIntrospector.js +6 -2
  48. package/dist/migrate/introspection/postgresIntrospector.js +1 -1
  49. package/dist/migrate/migrationTarget.js +2 -1
  50. package/dist/migrate/migrator.js +2 -1
  51. package/dist/migrate/schemaGenerator.js +5 -4
  52. package/dist/migrate/storage/databaseStorage.js +1 -1
  53. package/dist/migrate/triggerSql.d.ts +1 -1
  54. package/dist/migrate/triggerSql.js +77 -61
  55. package/dist/mongo/mongoDialect.d.ts +1 -3
  56. package/dist/mongo/mongoDialect.js +9 -14
  57. package/dist/mongo/mongodbQuerier.js +7 -10
  58. package/dist/mssql/mssqlQuerier.d.ts +2 -0
  59. package/dist/mssql/mssqlQuerier.js +8 -5
  60. package/dist/mysql/mysql2QuerierPool.d.ts +1 -0
  61. package/dist/mysql/mysql2QuerierPool.js +20 -2
  62. package/dist/neon/neonQuerierPool.js +2 -2
  63. package/dist/pglite/pgliteQuerierPool.js +10 -4
  64. package/dist/postgres/pgQuerierPool.js +2 -2
  65. package/dist/postgres/{pgNumericTypes.d.ts → pgWireTypes.d.ts} +4 -3
  66. package/dist/postgres/{pgNumericTypes.js → pgWireTypes.js} +7 -3
  67. package/dist/querier/abstractQuerier.d.ts +9 -4
  68. package/dist/querier/abstractQuerier.js +26 -19
  69. package/dist/querier/abstractQuerierPool.d.ts +3 -3
  70. package/dist/querier/abstractSqlQuerier.d.ts +2 -2
  71. package/dist/querier/abstractSqlQuerier.js +1 -1
  72. package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
  73. package/dist/querier/queryError.d.ts +2 -2
  74. package/dist/schema/canonicalType.d.ts +3 -0
  75. package/dist/schema/canonicalType.js +31 -9
  76. package/dist/schema/schemaASTBuilder.js +2 -1
  77. package/dist/schema/schemaASTDiffer.js +4 -2
  78. package/dist/sqlite/sqliteDialect.d.ts +1 -3
  79. package/dist/sqlite/sqliteDialect.js +3 -6
  80. package/dist/type/dialect.d.ts +23 -1
  81. package/dist/type/entity.d.ts +16 -12
  82. package/dist/type/logger.d.ts +2 -2
  83. package/dist/type/querier.d.ts +3 -3
  84. package/dist/type/query.d.ts +3 -13
  85. package/dist/type/queryAggregate.d.ts +4 -10
  86. package/dist/type/queryRaw.d.ts +17 -3
  87. package/dist/type/queryRaw.js +2 -1
  88. package/dist/type/queryWhere.d.ts +7 -7
  89. package/dist/type/universalQuerier.d.ts +3 -3
  90. package/dist/type/vector.d.ts +2 -1
  91. package/dist/type/vector.js +2 -1
  92. package/dist/util/date.d.ts +11 -0
  93. package/dist/util/date.js +19 -0
  94. package/dist/util/dialect.util.d.ts +13 -5
  95. package/dist/util/dialect.util.js +28 -20
  96. package/dist/util/field.util.d.ts +4 -4
  97. package/dist/util/field.util.js +10 -2
  98. package/dist/util/fieldOption.util.d.ts +5 -3
  99. package/dist/util/fieldOption.util.js +6 -5
  100. package/dist/util/hook.util.d.ts +1 -1
  101. package/dist/util/hook.util.js +8 -1
  102. package/dist/util/index.d.ts +1 -0
  103. package/dist/util/index.js +1 -0
  104. package/dist/util/logger.d.ts +3 -3
  105. package/dist/util/object.util.js +3 -2
  106. package/dist/util/raw.d.ts +6 -7
  107. package/dist/util/raw.js +10 -12
  108. package/dist/util/sqlLiteral.d.ts +8 -1
  109. package/dist/util/sqlLiteral.js +14 -9
  110. package/dist/util/triggerWrite.d.ts +15 -0
  111. package/dist/util/triggerWrite.js +20 -0
  112. package/package.json +1 -1
  113. package/skills/uql-orm/SKILL.md +4 -4
@@ -1,4 +1,5 @@
1
1
  import { indexDistance, isVectorIndexType, unsupportedVectorMetric } from '../../type/vector.js';
2
+ import { UqlUsageError } from '../../util/uqlError.js';
2
3
  import { IndexDdl } from './indexDdl.js';
3
4
  /**
4
5
  * SQLite's `CREATE INDEX`, which names no index type. A vector index is libSQL's DiskANN where the dialect
@@ -31,7 +32,7 @@ export class SqliteIndexDdl extends IndexDdl {
31
32
  }
32
33
  // Its tables are named after the index, unquoted: any other name fails with "unable to initialize diskann".
33
34
  if (!/^\w+$/.test(index.name) || index.entries.length !== 1) {
34
- throw new TypeError(`libSQL names a vector index only by letters, digits and underscores, over one column (index "${index.name}")`);
35
+ throw new UqlUsageError(`libSQL names a vector index only by letters, digits and underscores, over one column (index "${index.name}")`);
35
36
  }
36
37
  return metric;
37
38
  }
@@ -1,3 +1,4 @@
1
+ import { UqlUsageError } from '../../util/uqlError.js';
1
2
  import { formatDefaultValue, sameDefault } from '../builder/expressions.js';
2
3
  /**
3
4
  * A column's type with the size it was read back with, unless its spelling already carries one:
@@ -39,7 +40,7 @@ export class TableDdl {
39
40
  */
40
41
  alterColumn(table, column, definition, from) {
41
42
  if (this.dialect.alterColumnSyntax === 'none') {
42
- throw new TypeError(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
43
+ throw new UqlUsageError(`${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
43
44
  `This database does not support ALTER COLUMN.`);
44
45
  }
45
46
  const target = this.dialect.escapeId(table);
@@ -1,3 +1,4 @@
1
+ import { UqlUsageError } from '../../util/uqlError.js';
1
2
  export function serializeMongoCommand(command) {
2
3
  return JSON.stringify(command);
3
4
  }
@@ -9,7 +10,7 @@ function parseMongoCommand(statement) {
9
10
  return JSON.parse(statement);
10
11
  }
11
12
  function unsupportedMongoCommand(statement) {
12
- return new TypeError(`unsupported MongoDB migration command: ${statement}`);
13
+ return new UqlUsageError(`unsupported MongoDB migration command: ${statement}`);
13
14
  }
14
15
  /** Execute one emitted command. */
15
16
  export function runMongoCommand(db, statement) {
@@ -33,7 +33,7 @@ export declare class MongoSchemaGenerator extends MongoDialect implements Schema
33
33
  generateDropTable(tableName: string): string;
34
34
  /** A collection's indexes: each dropped, then each created, an alter as both. */
35
35
  generateAlterTable(diff: SchemaDiff): string[];
36
- /** MongoDB has no triggers, and a write to an entity declaring one is refused, so there is none to reconcile. */
36
+ /** MongoDB runs no trigger within a write, and a write to an entity declaring one is refused, so there is none to reconcile. */
37
37
  generateTriggers(): string[];
38
38
  generateTriggersDown(): string[];
39
39
  generateTriggerDrops(): string[];
@@ -6,6 +6,7 @@ import { QueryRaw, } from '../../type/index.js';
6
6
  import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
7
7
  import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../../util/ddlExpression.util.js';
8
8
  import { fulltextConfig, fulltextWeights } from '../../util/dialect.util.js';
9
+ import { UqlUsageError } from '../../util/uqlError.js';
9
10
  import { assertIndexFeatures, assertIndexType } from '../ddl/indexDdl.js';
10
11
  import { assertIndexPredicate, refusedIndexPredicate } from '../indexPredicate.js';
11
12
  import { sides } from '../schemaChange.js';
@@ -30,7 +31,7 @@ export class MongoSchemaGenerator extends MongoDialect {
30
31
  }
31
32
  /** A collection has no SQL to render a check, a computed column or an index expression into. */
32
33
  compileDdl() {
33
- throw new TypeError('mongodb has no SQL to render a check, a computed column or an index expression into');
34
+ throw new UqlUsageError('mongodb has no SQL to render a check, a computed column or an index expression into');
34
35
  }
35
36
  /**
36
37
  * A document store has no cross-collection constraint, so unlike the SQL generator there is nothing to
@@ -84,7 +85,7 @@ export class MongoSchemaGenerator extends MongoDialect {
84
85
  */
85
86
  compileIndexPredicate(where, entity, indexName) {
86
87
  if (where instanceof QueryRaw) {
87
- throw new TypeError(`mongodb does not support partial indexes from a SQL predicate (index "${indexName}")`);
88
+ throw new UqlUsageError(`mongodb does not support partial indexes from a SQL predicate (index "${indexName}")`);
88
89
  }
89
90
  assertIndexPredicate(where, this.dialectName, indexName);
90
91
  const filter = this.renderFilter(entity, where);
@@ -114,7 +115,7 @@ export class MongoSchemaGenerator extends MongoDialect {
114
115
  ...sides(diff.indexes, 'to').map((index) => this.generateCreateIndex(diff.tableName, index)),
115
116
  ];
116
117
  }
117
- /** MongoDB has no triggers, and a write to an entity declaring one is refused, so there is none to reconcile. */
118
+ /** MongoDB runs no trigger within a write, and a write to an entity declaring one is refused, so there is none to reconcile. */
118
119
  generateTriggers() {
119
120
  return [];
120
121
  }
@@ -160,7 +161,7 @@ export class MongoSchemaGenerator extends MongoDialect {
160
161
  assertIndexFeatures(index, new Set(), this.dialectName);
161
162
  const [vector, ...filters] = index.entries;
162
163
  if (!vector || index.dimensions === undefined) {
163
- throw new TypeError(`an Atlas vector search index states its field's dimensions (index "${index.name}")`);
164
+ throw new UqlUsageError(`an Atlas vector search index states its field's dimensions (index "${index.name}")`);
164
165
  }
165
166
  const distance = indexDistance(index);
166
167
  const similarity = ATLAS_SIMILARITY[distance];
@@ -192,7 +193,7 @@ export class MongoSchemaGenerator extends MongoDialect {
192
193
  case 'createTable': {
193
194
  const { name, columns, indexes } = operation.table;
194
195
  if (columns.length) {
195
- throw new TypeError(`mongodb does not support columns in a migration (collection "${name}")`);
196
+ throw new UqlUsageError(`mongodb does not support columns in a migration (collection "${name}")`);
196
197
  }
197
198
  return [
198
199
  serializeMongoCommand({ action: 'createCollection', name }),
@@ -208,7 +209,7 @@ export class MongoSchemaGenerator extends MongoDialect {
208
209
  case 'dropIndex':
209
210
  return [this.generateDropIndex(operation.tableName, operation.indexName)];
210
211
  default:
211
- throw new TypeError(`mongodb does not support ${operation.type} in a migration`);
212
+ throw new UqlUsageError(`mongodb does not support ${operation.type} in a migration`);
212
213
  }
213
214
  }
214
215
  diffSchema(entity, currentTable) {
@@ -1,5 +1,6 @@
1
1
  import { QueryRaw } from '../type/queryRaw.js';
2
2
  import { isOperatorObject } from '../util/object.util.js';
3
+ import { UqlUsageError } from '../util/uqlError.js';
3
4
  /**
4
5
  * The `$where` a partial index takes, on the engines where that is less than a query's: SQL Server's
5
6
  * `CREATE INDEX` `<filter_predicate>` (comparisons and `IN` on plain columns, joined by `AND`), and
@@ -30,7 +31,7 @@ export function assertIndexPredicate(where, dialectName, indexName) {
30
31
  }
31
32
  /** What refusing any part of a partial index's predicate reports. */
32
33
  export function refusedIndexPredicate(dialectName, part, indexName) {
33
- return new TypeError(`${dialectName} does not support ${part} in a partial index predicate (index "${indexName}")`);
34
+ return new UqlUsageError(`${dialectName} does not support ${part} in a partial index predicate (index "${indexName}")`);
34
35
  }
35
36
  /** The first part of `where` outside `grammar`, depth-first; a `raw` clause is left alone. */
36
37
  function refusedOperator(where, grammar) {
@@ -1,6 +1,7 @@
1
1
  import { FOREIGN_KEY_ACTIONS } from '../../schema/types.js';
2
2
  import { isSqlQuerier } from '../../type/index.js';
3
3
  import { isOwnedName } from '../../util/sql.util.js';
4
+ import { UqlUsageError } from '../../util/uqlError.js';
4
5
  import { BaseSqlIntrospector } from './baseSqlIntrospector.js';
5
6
  /** A SQL introspector: an engine states its catalogue queries (`get*Query`) and how their rows map (`map*Result`). */
6
7
  export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
@@ -73,7 +74,7 @@ export class AbstractSqlSchemaIntrospector extends BaseSqlIntrospector {
73
74
  withSqlQuerier(task) {
74
75
  return this.pool.withQuerier((querier) => {
75
76
  if (!isSqlQuerier(querier)) {
76
- throw new TypeError(`${this.constructor.name} requires a SQL-based querier`);
77
+ throw new UqlUsageError(`${this.constructor.name} requires a SQL-based querier`);
77
78
  }
78
79
  return task(querier);
79
80
  });
@@ -10,7 +10,7 @@ export declare class MongoSchemaIntrospector implements SchemaIntrospector {
10
10
  /** `listIndexes` reports keys, uniqueness and text weights; a `partialFilterExpression` is no SQL predicate. */
11
11
  readonly indexFacets: ReadonlySet<IndexFacet>;
12
12
  constructor(pool: QuerierPool);
13
- /** MongoDB has no triggers, so none is ever installed. */
13
+ /** MongoDB runs no trigger within a write, so uql installs none. */
14
14
  ownedTriggers(): Promise<InstalledTriggers>;
15
15
  introspect(tables?: readonly string[]): Promise<SchemaAST>;
16
16
  getTableSchema(tableName: string): Promise<TableSchema | undefined>;
@@ -1,6 +1,7 @@
1
1
  import { textConfigOf } from '../../mongo/textLanguage.js';
2
2
  import { createTableNode, SchemaAST } from '../../schema/schemaAST.js';
3
3
  import { isMongoQuerier, } from '../../type/index.js';
4
+ import { UqlUsageError } from '../../util/uqlError.js';
4
5
  /** What a server without Atlas Search answers a search index command with. */
5
6
  const SEARCH_NOT_ENABLED = 31082;
6
7
  /**
@@ -14,7 +15,7 @@ export class MongoSchemaIntrospector {
14
15
  constructor(pool) {
15
16
  this.pool = pool;
16
17
  }
17
- /** MongoDB has no triggers, so none is ever installed. */
18
+ /** MongoDB runs no trigger within a write, so uql installs none. */
18
19
  async ownedTriggers() {
19
20
  return new Map();
20
21
  }
@@ -80,7 +81,7 @@ export class MongoSchemaIntrospector {
80
81
  withDb(task) {
81
82
  return this.pool.withQuerier((querier) => {
82
83
  if (!isMongoQuerier(querier)) {
83
- throw new TypeError('MongoSchemaIntrospector requires a MongoDB querier');
84
+ throw new UqlUsageError('MongoSchemaIntrospector requires a MongoDB querier');
84
85
  }
85
86
  return task(querier.db);
86
87
  });
@@ -1,3 +1,4 @@
1
+ import { defaultTimestampPrecision } from '../../schema/canonicalType.js';
1
2
  import { AbstractSqlSchemaIntrospector, } from './abstractSqlSchemaIntrospector.js';
2
3
  /**
3
4
  * SQL Server schema introspector.
@@ -137,7 +138,7 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
137
138
  const bytes = this.toNumber(row.max_length);
138
139
  return {
139
140
  name: row.column_name,
140
- type: bytes === -1 && CHARACTER_TYPES.has(type) ? `${type}(MAX)` : type,
141
+ type: spelledType(type, bytes, this.toNumber(row.numeric_scale)),
141
142
  nullable: Boolean(row.is_nullable),
142
143
  defaultValue: this.parseDefaultValue(row.column_default),
143
144
  isAutoIncrement: Boolean(row.is_identity),
@@ -183,6 +184,17 @@ export class MsSqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
183
184
  /** The types whose `max_length` is a width rather than a fixed storage size. */
184
185
  const CHARACTER_TYPES = new Set(['CHAR', 'NCHAR', 'VARCHAR', 'NVARCHAR', 'BINARY', 'VARBINARY']);
185
186
  const NUMERIC_TYPES = new Set(['DECIMAL', 'NUMERIC']);
187
+ /** `(MAX)` on an unbounded character type, and a timestamp's fractional digits where not the engine's default. */
188
+ function spelledType(type, bytes, scale) {
189
+ if (bytes === -1 && CHARACTER_TYPES.has(type)) {
190
+ return `${type}(MAX)`;
191
+ }
192
+ return (type === 'DATETIME2' || type === 'DATETIMEOFFSET') &&
193
+ scale !== undefined &&
194
+ scale !== defaultTimestampPrecision('mssql')
195
+ ? `${type}(${scale})`
196
+ : type;
197
+ }
186
198
  /**
187
199
  * A column's declared width from `max_length`, which is bytes: an `N` type holds two a character, a
188
200
  * `VECTOR` is an 8-byte header and four a dimension, and `MAX` is `-1`. Read as bytes, every Unicode
@@ -29,6 +29,7 @@ export declare class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospect
29
29
  }
30
30
  type MysqlColumnRow = {
31
31
  column_name: string;
32
+ data_type: string;
32
33
  column_type: string;
33
34
  is_nullable: string;
34
35
  column_default: string | null;
@@ -36,6 +37,7 @@ type MysqlColumnRow = {
36
37
  extra: string;
37
38
  character_maximum_length: number | bigint | null;
38
39
  numeric_precision: number | bigint | null;
40
+ datetime_precision: number | bigint | null;
39
41
  numeric_scale: number | null;
40
42
  column_comment: string | null;
41
43
  generated_as: string | null;
@@ -47,6 +47,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
47
47
  CHARACTER_MAXIMUM_LENGTH as character_maximum_length,
48
48
  NUMERIC_PRECISION as numeric_precision,
49
49
  NUMERIC_SCALE as numeric_scale,
50
+ DATETIME_PRECISION as datetime_precision,
50
51
  COLUMN_KEY as column_key,
51
52
  EXTRA as extra,
52
53
  CASE WHEN EXTRA LIKE '%STORED GENERATED%' THEN GENERATION_EXPRESSION END as generated_as,
@@ -113,7 +114,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
113
114
  isUnique: row.column_key === 'UNI',
114
115
  // A `VECTOR`'s is its bytes, four a dimension, which `column_type` already states as dimensions.
115
116
  length: /^vector/i.test(row.column_type) ? undefined : this.toNumber(row.character_maximum_length),
116
- precision: this.toNumber(row.numeric_precision),
117
+ // A timestamp's fractional digits, stated even when 0, which uql's own unstated `DATETIME(3)` is not.
118
+ precision: this.toNumber(TIMESTAMP_TYPES.has(row.data_type) ? row.datetime_precision : row.numeric_precision),
117
119
  scale: this.toNumber(row.numeric_scale),
118
120
  comment: row.column_comment || undefined,
119
121
  generatedAs: row.generated_as ?? undefined,
@@ -146,7 +148,8 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
146
148
  if (normalized === 'NULL') {
147
149
  return null;
148
150
  }
149
- if (normalized === 'CURRENT_TIMESTAMP' || normalized === 'CURRENT_TIMESTAMP()') {
151
+ // Whatever precision it repeats from its column, which the column's own type already states.
152
+ if (/^CURRENT_TIMESTAMP(?:\(\d?\))?$/.test(normalized)) {
150
153
  return 'CURRENT_TIMESTAMP';
151
154
  }
152
155
  if (/^-?\d+(\.\d+)?$/.test(defaultValue)) {
@@ -158,6 +161,7 @@ export class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
158
161
  return quoted ? unescapeMysqlString(quoted[1]) : literal;
159
162
  }
160
163
  }
164
+ const TIMESTAMP_TYPES = new Set(['datetime', 'timestamp']);
161
165
  /**
162
166
  * MariaDB reads out of the same `information_schema` as MySQL, save for one column type it does not
163
167
  * have: `JSON` there is an alias for `LONGTEXT` plus a `json_valid()` check constraint named after
@@ -75,7 +75,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
75
75
  c.is_identity,
76
76
  c.identity_generation,
77
77
  CASE WHEN a.attgenerated = 's' THEN c.generation_expression END AS generated_as,
78
- CASE WHEN c.data_type IN ('USER-DEFINED', 'vector') AND a.atttypmod > -1
78
+ CASE WHEN (c.data_type IN ('USER-DEFINED', 'vector') OR c.data_type LIKE 'timestamp%') AND a.atttypmod > -1
79
79
  THEN format_type(a.atttypid, a.atttypmod) END AS formatted_type,
80
80
  EXISTS (
81
81
  SELECT 1 FROM information_schema.table_constraints tc
@@ -1,4 +1,5 @@
1
1
  import { isMongoQuerier, isSqlQuerier, } from '../type/index.js';
2
+ import { UqlUsageError } from '../util/uqlError.js';
2
3
  import { withMongoQuerierForMigrations, withSqlQuerierForMigrations } from './acquireQuerierForMigrations.js';
3
4
  import { MigrationBuilder } from './builder/migrationBuilder.js';
4
5
  import { migrationSource } from './codegen/migrationFile.js';
@@ -46,5 +47,5 @@ export async function migrationBuilderFor(querier) {
46
47
  if (isMongoQuerier(querier)) {
47
48
  return new MigrationBuilder(await mongoSchemaGenerator(), mongoSession(querier).run);
48
49
  }
49
- throw new TypeError('A migration builder needs a SQL or a MongoDB querier');
50
+ throw new UqlUsageError('A migration builder needs a SQL or a MongoDB querier');
50
51
  }
@@ -5,6 +5,7 @@ import { getEntities, getMeta } from '../entity/index.js';
5
5
  import { SchemaAST } from '../schema/index.js';
6
6
  import { hasTriggers } from '../util/field.util.js';
7
7
  import { LoggerWrapper } from '../util/index.js';
8
+ import { UqlUsageError } from '../util/uqlError.js';
8
9
  import { buildMigrationModule } from './codegen/migrationFile.js';
9
10
  import { introspectorFor } from './introspection/registry.js';
10
11
  import { migrationBuilderFor, migrationTargetFor } from './migrationTarget.js';
@@ -96,7 +97,7 @@ export class Migrator {
96
97
  if (options.to) {
97
98
  const toIndex = selected.findIndex((m) => m.name === options.to);
98
99
  if (toIndex === -1) {
99
- throw new TypeError(`Migration '${options.to}' not found`);
100
+ throw new UqlUsageError(`Migration '${options.to}' not found`);
100
101
  }
101
102
  selected = selected.slice(0, toIndex + 1);
102
103
  }
@@ -5,6 +5,7 @@ import { buildSchemaAST } from '../schema/schemaASTBuilder.js';
5
5
  import { diffRelationshipNodes, diffTable } from '../schema/schemaASTDiffer.js';
6
6
  import { isAutoIncrement, qualifyName } from '../util/index.js';
7
7
  import { derivedCheckName, derivedForeignKeyName, derivedPrimaryKeyName, isOwnedName } from '../util/sql.util.js';
8
+ import { UqlUsageError } from '../util/uqlError.js';
8
9
  import { sameDefault } from './builder/expressions.js';
9
10
  import { splitSqlStatements } from './builder/splitSqlStatements.js';
10
11
  import { indexDdlFor, tableDdlFor } from './ddl/index.js';
@@ -536,7 +537,7 @@ export class SqlSchemaGenerator {
536
537
  }
537
538
  generateAddForeignKeySql(tableName, foreignKey) {
538
539
  if (!this.features.foreignKeyAlter) {
539
- throw new TypeError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
540
+ throw new UqlUsageError(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
540
541
  }
541
542
  const constraint = this.foreignKeyConstraint(tableName, foreignKey, this.escapeId(foreignKey.references.table));
542
543
  return `ALTER TABLE ${this.escapeId(tableName)} ADD ${constraint};`;
@@ -566,7 +567,7 @@ export class SqlSchemaGenerator {
566
567
  return `ALTER TABLE ${table} DROP PRIMARY KEY;`;
567
568
  }
568
569
  if (!constraintName) {
569
- throw new TypeError(`Cannot drop the primary key of "${tableName}": ${this.dialect} names the constraint, and ` +
570
+ throw new UqlUsageError(`Cannot drop the primary key of "${tableName}": ${this.dialect} names the constraint, and ` +
570
571
  'introspection did not report a name for it.');
571
572
  }
572
573
  return `ALTER TABLE ${table} DROP CONSTRAINT ${this.escapeId(constraintName)};`;
@@ -579,7 +580,7 @@ export class SqlSchemaGenerator {
579
580
  if (!column.generatedAs || this.features.generatedColumnAdd) {
580
581
  return;
581
582
  }
582
- throw new TypeError(`${this.dialect}: Cannot add the computed column "${column.name}" to the existing table ` +
583
+ throw new UqlUsageError(`${this.dialect}: Cannot add the computed column "${column.name}" to the existing table ` +
583
584
  `"${tableName}" - this database only accepts one in a CREATE TABLE. Drop \`stored\` to have the ` +
584
585
  'expression spliced into each statement instead, or recreate the table in a written migration.');
585
586
  }
@@ -587,7 +588,7 @@ export class SqlSchemaGenerator {
587
588
  if (this.features.primaryKeyAlter) {
588
589
  return;
589
590
  }
590
- throw new TypeError(`${this.dialect}: Cannot change the primary key of "${tableName}" - this database has no ALTER ` +
591
+ throw new UqlUsageError(`${this.dialect}: Cannot change the primary key of "${tableName}" - this database has no ALTER ` +
591
592
  'for it. Recreate the table in a written migration.');
592
593
  }
593
594
  }
@@ -32,7 +32,7 @@ export class DatabaseMigrationStorage {
32
32
  async createTableIfNotExists(querier) {
33
33
  const table = new TableBuilder(this.tableName);
34
34
  table.string('name', { length: 255, primaryKey: true });
35
- table.timestamp('executed_at', { defaultValue: expr.now() });
35
+ table.timestamptz('executed_at', { defaultValue: expr.now() });
36
36
  const generator = new SqlSchemaGenerator(querier.dialect);
37
37
  for (const sql of generator.generateCreateTableFromDefinition(table.build(), { ifNotExists: true })) {
38
38
  await querier.run(sql);
@@ -21,4 +21,4 @@ export declare function dropTrigger<E>(dialect: AbstractSqlDialect, meta: Entity
21
21
  * column. Generated rather than authored, so unlike an authored body it renders on every engine from
22
22
  * one declaration - and not at all on the MySQL family, whose columns stamp themselves.
23
23
  */
24
- export declare function stampTriggers<E>(dialect: AbstractSqlDialect, meta: EntityMeta<E>): EntityTriggerMeta<E>[];
24
+ export declare function stampTriggers<E extends object>(dialect: AbstractSqlDialect, meta: EntityMeta<E>): EntityTriggerMeta<E>[];
@@ -1,7 +1,8 @@
1
1
  import { stampEvents } from '../util/field.util.js';
2
2
  import { definedEntries } from '../util/object.util.js';
3
- import { raw, rowColumn, rowRefs } from '../util/raw.js';
3
+ import { raw, refs, rowRefs } from '../util/raw.js';
4
4
  import { ownedName } from '../util/sql.util.js';
5
+ import { UqlUsageError } from '../util/uqlError.js';
5
6
  /**
6
7
  * One trigger for `dialect`, named for its table and label and ending in a hash of its own SQL. That hash
7
8
  * is the whole of change detection: a trigger is in place exactly when its name is installed, and an
@@ -20,7 +21,7 @@ export function renderTrigger(dialect, meta, trigger, position) {
20
21
  */
21
22
  export function dropTrigger(dialect, meta, name) {
22
23
  const { scope, body } = dialect.features.triggers;
23
- const table = dialect.escapeId(dialect.resolveTableName(meta));
24
+ const table = dialect.escapedTableName(meta);
24
25
  return [
25
26
  `DROP TRIGGER IF EXISTS ${triggerId(dialect, meta, name)}${scope === 'table' ? ` ON ${table}` : ''}`,
26
27
  ...(body === 'function' ? [`DROP FUNCTION IF EXISTS ${schemaObjectId(dialect, meta, name)}()`] : []),
@@ -45,17 +46,15 @@ function schemaObjectId(dialect, meta, name) {
45
46
  */
46
47
  function triggerStatements(dialect, meta, trigger, name) {
47
48
  const features = dialect.features.triggers;
48
- // The event, read apart once: `beforeUpdate` is `BEFORE` and `UPDATE`, and everything else follows.
49
- const before = trigger.on.startsWith('before');
50
- const operation = trigger.on.slice(before ? 6 : 5).toUpperCase();
49
+ const [timing, operation] = EVENT_PARTS[trigger.on];
50
+ const before = timing === 'BEFORE';
51
51
  const body = triggerBody(dialect, meta, trigger, before);
52
52
  const id = triggerId(dialect, meta, name);
53
- const table = dialect.escapeId(dialect.resolveTableName(meta));
54
- const column = (key) => dialect.escapeId(dialect.columnOf(meta, key));
53
+ const table = dialect.escapedTableName(meta);
55
54
  const names = rowNames(dialect);
56
- const rows = [rowRefs(names.$new), rowRefs(names.$old)];
57
- const sql = dialect.compileDdl(body(...rows), meta.entity);
58
- const guard = triggerGuard(dialect, meta, trigger, rows, names, column);
55
+ const rows = [rowRefs(meta.entity, names.$new), rowRefs(meta.entity, names.$old)];
56
+ const sql = dialect.compileDdl(body(...rows), meta.entity, { rows: rowsFrom(dialect, meta, operation) });
57
+ const guard = triggerGuard(dialect, meta, trigger, rows, names);
59
58
  const inBody = features.guards !== 'clause';
60
59
  const guarded = !guard || !inBody
61
60
  ? sql
@@ -66,14 +65,14 @@ function triggerStatements(dialect, meta, trigger, name) {
66
65
  // which rows are touched, so it runs even when the guard keeps the statements from running.
67
66
  const opened = features.preamble ? `${features.preamble}\n${guarded}` : guarded;
68
67
  const of = features.guards === 'clause' && operation === 'UPDATE' && trigger.of?.length
69
- ? ` OF ${trigger.of.map(column).join(', ')}`
68
+ ? ` OF ${trigger.of.map((key) => dialect.escapedColumnName(meta, key)).join(', ')}`
70
69
  : '';
71
70
  const each = features.rows === 'set' ? '' : '\nFOR EACH ROW';
72
71
  const clause = guard && !inBody ? `\nWHEN (${guard})` : '';
73
- const timing = `${before ? 'BEFORE' : 'AFTER'} ${operation}${of}`;
72
+ const when = `${timing} ${operation}${of}`;
74
73
  const header = features.layout === 'tableFirst'
75
- ? `CREATE TRIGGER ${id}\nON ${table} ${timing}${each}${clause}\nAS`
76
- : `CREATE TRIGGER ${id}\n${timing} ON ${table}${each}${clause}`;
74
+ ? `CREATE TRIGGER ${id}\nON ${table} ${when}${each}${clause}\nAS`
75
+ : `CREATE TRIGGER ${id}\n${when} ON ${table}${each}${clause}`;
77
76
  if (features.body !== 'function') {
78
77
  return [`${header}\nBEGIN\n${opened}\nEND`];
79
78
  }
@@ -83,6 +82,15 @@ function triggerStatements(dialect, meta, trigger, name) {
83
82
  const fn = schemaObjectId(dialect, meta, name);
84
83
  return [plpgsqlFunction(fn, `BEGIN\n${opened}\nRETURN ${returned};\nEND`), `${header}\nEXECUTE FUNCTION ${fn}()`];
85
84
  }
85
+ /** Each event read apart: `beforeUpdate` is `BEFORE` and `UPDATE`. */
86
+ const EVENT_PARTS = {
87
+ beforeInsert: ['BEFORE', 'INSERT'],
88
+ afterInsert: ['AFTER', 'INSERT'],
89
+ beforeUpdate: ['BEFORE', 'UPDATE'],
90
+ afterUpdate: ['AFTER', 'UPDATE'],
91
+ beforeDelete: ['BEFORE', 'DELETE'],
92
+ afterDelete: ['AFTER', 'DELETE'],
93
+ };
86
94
  /**
87
95
  * The body for the engine in use, refusing first what the engine cannot render at all. One body serves
88
96
  * every engine; in a map, one written for the family serves its forks: CockroachDB runs Postgres's
@@ -91,25 +99,25 @@ function triggerStatements(dialect, meta, trigger, name) {
91
99
  function triggerBody(dialect, meta, trigger, before) {
92
100
  const features = dialect.features.triggers;
93
101
  if (before && !features.before) {
94
- throw new TypeError(`${dialect.dialectName} has no BEFORE trigger, only AFTER and INSTEAD OF, so '${trigger.on}' cannot be ` +
102
+ throw new UqlUsageError(`${dialect.dialectName} has no BEFORE trigger, only AFTER and INSTEAD OF, so '${trigger.on}' cannot be ` +
95
103
  'rendered there. Use the matching after event, which sees the row already written.');
96
104
  }
97
105
  if (trigger.where && features.rows === 'set') {
98
- throw new TypeError(`${dialect.dialectName} fires a trigger once per statement, over the rows it touched, so no condition ` +
106
+ throw new UqlUsageError(`${dialect.dialectName} fires a trigger once per statement, over the rows it touched, so no condition ` +
99
107
  `can read one row: '${meta.entity.name}' cannot state a trigger 'where' there. Guard inside the body ` +
100
108
  'instead, where `inserted` and `deleted` can be read as tables.');
101
109
  }
102
110
  const { run } = trigger;
103
111
  const body = typeof run === 'function' ? run : (run[dialect.dialectName] ?? run[dialect.dialectFamily]);
104
112
  if (!body) {
105
- throw new TypeError(`'${meta.entity.name}' has a trigger with no body for ${dialect.dialectName}, the engine in use. ` +
113
+ throw new UqlUsageError(`'${meta.entity.name}' has a trigger with no body for ${dialect.dialectName}, the engine in use. ` +
106
114
  'Write one for it, or one body for every engine.');
107
115
  }
108
116
  return body;
109
117
  }
110
118
  /** The one condition both guards reduce to: any watched column that moved, and whatever `where` asks. */
111
- function triggerGuard(dialect, meta, trigger, rows, names, column) {
112
- const moved = movedColumns(dialect, meta, trigger.of ?? [], names, column);
119
+ function triggerGuard(dialect, meta, trigger, rows, names) {
120
+ const moved = movedColumns(dialect, meta, trigger.of ?? [], names);
113
121
  return [
114
122
  ...(moved ? [moved] : []),
115
123
  ...(trigger.where ? condition(dialect, meta, trigger.where, rows, names, Boolean(moved)) : []),
@@ -137,16 +145,10 @@ function condition(dialect, meta, where, rows, names, operand) {
137
145
  return [operand ? `(${sql})` : sql];
138
146
  }
139
147
  const predicates = definedEntries(where);
140
- return predicates.map(([row, predicate]) => {
141
- const ctx = dialect.createContext({ inlineValues: true });
142
- const escapedPrefix = `${names[row]}.`;
143
- dialect.where(ctx, meta.entity, predicate, {
144
- clause: false,
145
- operand: operand || predicates.length > 1,
146
- escapedPrefix,
147
- });
148
- return ctx.sql;
149
- });
148
+ return predicates.map(([row, predicate]) => dialect.compileDdl(predicate, meta.entity, {
149
+ escapedPrefix: `${names[row]}.`,
150
+ operand: operand || predicates.length > 1,
151
+ }));
150
152
  }
151
153
  /**
152
154
  * The triggers a stamp needs: one per event it names, each assigning the field's expression to its
@@ -157,8 +159,8 @@ export function stampTriggers(dialect, meta) {
157
159
  const features = dialect.features.triggers;
158
160
  // Restating the row has to wait for it to be there, so those engines stamp after the write.
159
161
  const after = !features.assignsRow;
160
- const newName = rowNames(dialect).$new;
161
- return definedEntries(meta.fields).flatMap(([key, field]) => {
162
+ const fields = meta.fields;
163
+ return definedEntries(fields).flatMap(([key, field]) => {
162
164
  const events = stampEvents(field);
163
165
  const { computed } = field;
164
166
  if (!events?.length || !computed) {
@@ -169,7 +171,7 @@ export function stampTriggers(dialect, meta) {
169
171
  // The event is part of the name: a stamp on both writes installs two triggers, and one identifier
170
172
  // between them would have the second replace the first rather than sit beside it.
171
173
  name: `${key}_${event}`,
172
- run: () => stampBody(dialect, meta, key, computed, newName),
174
+ run: (newRow) => stampBody(dialect, meta, key, computed, newRow),
173
175
  }));
174
176
  });
175
177
  }
@@ -178,26 +180,25 @@ const STAMP_EVENTS = {
178
180
  before: { insert: 'beforeInsert', update: 'beforeUpdate' },
179
181
  after: { insert: 'afterInsert', update: 'afterUpdate' },
180
182
  };
181
- /** The one statement a stamp runs, in whichever of the two shapes the engine leaves open. */
182
- function stampBody(dialect, meta, key, value, newName) {
183
- const features = dialect.features.triggers;
184
- const read = (member) => rowColumn(newName, member);
185
- if (features.assignsRow) {
186
- const target = read(key);
187
- return features.body === 'function' ? raw `${target} := ${value};` : raw `SET ${target} = ${value};`;
183
+ /**
184
+ * The one statement a stamp runs, in whichever of the two shapes the engine leaves open: an assignment
185
+ * to the incoming row, or where it may not be written, the row restated after the write, as any write in
186
+ * a trigger's body is, keyed on its whole key and only where the stamp still differs: that `UPDATE` fires
187
+ * the trigger again, and with recursive triggers on it then finds nothing left to change.
188
+ */
189
+ function stampBody(dialect, meta, key, value, newRow) {
190
+ const { entity } = meta;
191
+ if (dialect.features.triggers.assignsRow) {
192
+ const target = newRow[key];
193
+ return dialect.features.triggers.body === 'function' ? raw `${target} := ${value};` : raw `SET ${target} = ${value};`;
188
194
  }
189
- const table = dialect.escapeId(dialect.resolveTableName(meta));
190
- const column = dialect.escapeId(dialect.columnOf(meta, key));
191
- const keyed = meta.ids
192
- .map((id) => raw `${text(`${table}.${dialect.escapeId(dialect.columnOf(meta, id))}`)} = ${read(id)}`)
193
- .reduce((all, part) => raw `${all} AND ${part}`);
194
- // A set-based engine hands the rows as a table, which an `UPDATE` has to name in a `FROM` before its
195
- // condition can read one: `inserted."id"` binds to nothing on its own.
196
- const from = features.rows === 'set' ? ` FROM ${newName}` : '';
197
- // Only where the stamp still differs: this `UPDATE` fires the trigger again, and with recursive triggers
198
- // on, the restatement it runs then finds nothing left to change instead of recursing without end.
199
- const differs = dialect.neExpr(`${table}.${column}`, dialect.compileDdl(value, meta.entity));
200
- return raw `UPDATE ${text(table)} SET ${text(column)} = ${value}${text(from)} WHERE ${keyed} AND ${text(differs)};`;
195
+ const table = refs(entity);
196
+ const keyed = meta.ids.map((id) => raw `${table[id]} = ${newRow[id]}`);
197
+ // Read through the write's own qualifier, as `keyed` is: `inserted` holds the same column name.
198
+ const stamped = dialect.compileDdl(value, entity);
199
+ const differs = raw(({ ctx, escapedPrefix }) => ctx.append(dialect.neExpr(`${escapedPrefix}${dialect.escapedColumnName(meta, key)}`, stamped)));
200
+ const where = { $and: [...keyed, differs] };
201
+ return raw(({ ctx, rows }) => dialect.triggerWrite(ctx, { kind: 'update', entity, set: { [key]: value }, where }, rows));
201
202
  }
202
203
  /** A dollar quote the body does not contain, so no `$$` in it - a literal, a comment - ends the function early. */
203
204
  function dollarQuote(body) {
@@ -207,23 +208,38 @@ function dollarQuote(body) {
207
208
  }
208
209
  return tag;
209
210
  }
210
- /** SQL already written out, for the identifiers a statement splices rather than binds. */
211
- function text(sql) {
212
- return raw((opts) => opts.ctx.append(sql));
213
- }
214
211
  /**
215
212
  * Whether any watched column moved, null-safely, or `undefined` where none is watched: the two records
216
213
  * compared on a row-based engine, and on a set-based one the same question over a join of its two tables.
217
214
  */
218
- function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }, column) {
215
+ function movedColumns(dialect, meta, of, { $new: newName, $old: oldName }) {
219
216
  if (!of.length) {
220
217
  return undefined;
221
218
  }
222
- const differs = of.map((key) => dialect.neExpr(`${oldName}.${column(key)}`, `${newName}.${column(key)}`));
219
+ const differs = of.map((key) => {
220
+ const column = dialect.escapedColumnName(meta, key);
221
+ return dialect.neExpr(`${oldName}.${column}`, `${newName}.${column}`);
222
+ });
223
223
  const moved = differs.length > 1 ? `(${differs.join(' OR ')})` : differs.join('');
224
- if (dialect.features.triggers.rows === 'row') {
225
- return moved;
224
+ const source = rowsFrom(dialect, meta, 'UPDATE');
225
+ return source ? `EXISTS (SELECT 1 ${source} WHERE ${moved})` : moved;
226
+ }
227
+ /**
228
+ * Where a set-based engine's body reads the rows it fires for, as the `FROM` a statement names them in:
229
+ * `inserted` on an insert, `deleted` on a delete, and on an update both, joined on the whole key. None on a
230
+ * row-based engine, whose body reads `NEW` and `OLD` bare.
231
+ */
232
+ function rowsFrom(dialect, meta, operation) {
233
+ if (dialect.features.triggers.rows !== 'set') {
234
+ return undefined;
235
+ }
236
+ const { $new, $old } = rowNames(dialect);
237
+ if (operation !== 'UPDATE') {
238
+ return `FROM ${operation === 'INSERT' ? $new : $old}`;
226
239
  }
227
- const keyed = meta.ids.map((id) => `${newName}.${column(id)} = ${oldName}.${column(id)}`).join(' AND ');
228
- return `EXISTS (SELECT 1 FROM ${newName} JOIN ${oldName} ON ${keyed} WHERE ${moved})`;
240
+ const keyed = meta.ids.map((id) => {
241
+ const column = dialect.escapedColumnName(meta, id);
242
+ return `${$new}.${column} = ${$old}.${column}`;
243
+ });
244
+ return `FROM ${$new} JOIN ${$old} ON ${keyed.join(' AND ')}`;
229
245
  }
@@ -90,9 +90,7 @@ export declare class MongoDialect extends AbstractDialect {
90
90
  * apply to JSON paths.
91
91
  */
92
92
  private assertKnownPathRoot;
93
- /**
94
- * Transform UQL operators to MongoDB operators.
95
- */
93
+ /** Transform UQL operators to MongoDB operators, refusing as `refusal` a key that is none. */
96
94
  private transformOperators;
97
95
  /**
98
96
  * What a value holding `value` matches, as the SQL engines read it: an operator map tests it, an array