metal-orm 1.1.25 → 1.1.27

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 (32) hide show
  1. package/dist/index.cjs +1151 -510
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +281 -248
  4. package/dist/index.d.ts +281 -248
  5. package/dist/index.js +1133 -508
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -1
  8. package/src/core/ddl/dialects/index.ts +5 -6
  9. package/src/core/ddl/dialects/mssql-schema-dialect.ts +129 -126
  10. package/src/core/ddl/dialects/mysql-schema-dialect.ts +119 -111
  11. package/src/core/ddl/dialects/postgres-schema-dialect.ts +173 -164
  12. package/src/core/ddl/dialects/render-reference.test.ts +37 -57
  13. package/src/core/ddl/dialects/sqlite-schema-dialect.ts +110 -121
  14. package/src/core/ddl/schema-dialect-composer.ts +129 -0
  15. package/src/core/ddl/schema-dialect.ts +40 -27
  16. package/src/core/ddl/schema-diff.ts +119 -90
  17. package/src/core/dialect/abstract.ts +7 -229
  18. package/src/core/dialect/base/sql-dialect-composer.ts +294 -0
  19. package/src/core/dialect/base/standard-sql-services.ts +2 -6
  20. package/src/core/dialect/base/upsert-strategy.ts +1 -2
  21. package/src/core/dialect/dialect-factory.ts +17 -49
  22. package/src/core/dialect/mssql/index.ts +56 -27
  23. package/src/core/dialect/mysql/index.ts +69 -36
  24. package/src/core/dialect/postgres/index.ts +71 -43
  25. package/src/core/dialect/sqlite/index.ts +68 -38
  26. package/src/core/driver/mssql-driver.ts +6 -8
  27. package/src/core/driver/mysql-driver.ts +6 -8
  28. package/src/core/driver/postgres-driver.ts +6 -8
  29. package/src/core/driver/sqlite-driver.ts +6 -8
  30. package/src/index.ts +13 -1
  31. package/src/core/ddl/dialects/base-schema-dialect.ts +0 -96
  32. package/src/core/dialect/base/sql-dialect.ts +0 -217
@@ -1,174 +1,183 @@
1
- import { BaseSchemaDialect } from './base-schema-dialect.js';
2
1
  import { deriveIndexName } from '../naming-strategy.js';
3
- import { renderIndexColumns, createLiteralFormatter } from '../sql-writing.js';
4
- import { ColumnDef, ForeignKeyReference, normalizeColumnType, renderTypeWithArgs } from '../../../schema/column-types.js';
5
- import { IndexDef, TableDef } from '../../../schema/table.js';
6
- import { ColumnDiff, DatabaseColumn, DatabaseTable } from '../schema-types.js';
7
- import { DialectName } from '../schema-dialect.js';
8
-
9
- /** PostgreSQL schema dialect implementation. */
10
- export class PostgresSchemaDialect extends BaseSchemaDialect {
11
- readonly name: DialectName = 'postgres';
12
-
13
- private _literalFormatter = createLiteralFormatter({
14
- booleanTrue: 'TRUE',
15
- booleanFalse: 'FALSE',
16
- });
17
-
18
- get literalFormatter() {
19
- return this._literalFormatter;
2
+ import {
3
+ createLiteralFormatter,
4
+ renderIndexColumns
5
+ } from '../sql-writing.js';
6
+ import {
7
+ composeSchemaDialect,
8
+ createStandardDropColumnCapability,
9
+ createStandardDropTableCapability,
10
+ type SchemaDialectServices
11
+ } from '../schema-dialect-composer.js';
12
+ import type { SchemaDialect } from '../schema-dialect.js';
13
+ import {
14
+ normalizeColumnType,
15
+ renderTypeWithArgs,
16
+ type ColumnDef
17
+ } from '../../../schema/column-types.js';
18
+ import type { IndexDef, TableDef } from '../../../schema/table.js';
19
+ import type { DatabaseTable } from '../schema-types.js';
20
+
21
+ const quoteIdentifier = (id: string): string => `"${id}"`;
22
+ const literalFormatter = createLiteralFormatter();
23
+
24
+ const renderPostgresColumnType = (
25
+ column: ColumnDef,
26
+ services: SchemaDialectServices
27
+ ): string => {
28
+ const override = column.dialectTypes?.[services.name] ?? column.dialectTypes?.default;
29
+ if (override) return renderTypeWithArgs(override, column.args);
30
+
31
+ const type = normalizeColumnType(column.type);
32
+ switch (type) {
33
+ case 'int':
34
+ case 'integer': return 'integer';
35
+ case 'bigint': return 'bigint';
36
+ case 'uuid': return 'uuid';
37
+ case 'boolean': return 'boolean';
38
+ case 'json': return 'jsonb';
39
+ case 'decimal':
40
+ return column.args?.length ? `numeric(${column.args[0]}, ${column.args[1] ?? 0})` : 'numeric';
41
+ case 'float':
42
+ case 'double': return 'double precision';
43
+ case 'timestamptz': return 'timestamptz';
44
+ case 'timestamp': return 'timestamp';
45
+ case 'date': return 'date';
46
+ case 'datetime': return 'timestamp';
47
+ case 'varchar': return column.args?.length ? `varchar(${column.args[0]})` : 'varchar';
48
+ case 'text': return 'text';
49
+ case 'enum': return 'text';
50
+ case 'binary':
51
+ case 'varbinary':
52
+ case 'blob':
53
+ case 'bytea': return 'bytea';
54
+ case 'vector':
55
+ return column.vectorOptions?.elementType === 'float16'
56
+ ? `halfvec(${column.vectorOptions.dimensions})`
57
+ : column.args?.length
58
+ ? `vector(${column.args[0]})`
59
+ : 'vector';
60
+ case 'halfvec': return column.args?.length ? `halfvec(${column.args[0]})` : 'halfvec';
61
+ default: return renderTypeWithArgs(String(type).toLowerCase(), column.args);
20
62
  }
21
-
22
- quoteIdentifier(id: string): string {
23
- return `"${id}"`;
24
- }
25
-
26
- renderColumnType(column: ColumnDef): string {
27
- const override = column.dialectTypes?.[this.name] ?? column.dialectTypes?.default;
28
- if (override) {
29
- return renderTypeWithArgs(override, column.args);
30
- }
31
-
32
- const type = normalizeColumnType(column.type);
33
- switch (type) {
34
- case 'int':
35
- case 'integer':
36
- return 'integer';
37
- case 'bigint':
38
- return 'bigint';
39
- case 'uuid':
40
- return 'uuid';
41
- case 'boolean':
42
- return 'boolean';
43
- case 'json':
44
- return 'jsonb';
45
- case 'decimal':
46
- return column.args?.length ? `numeric(${column.args[0]}, ${column.args[1] ?? 0})` : 'numeric';
47
- case 'float':
48
- case 'double':
49
- return 'double precision';
50
- case 'timestamptz':
51
- return 'timestamptz';
52
- case 'timestamp':
53
- return 'timestamp';
54
- case 'date':
55
- return 'date';
56
- case 'datetime':
57
- return 'timestamp';
58
- case 'varchar':
59
- return column.args?.length ? `varchar(${column.args[0]})` : 'varchar';
60
- case 'text':
61
- return 'text';
62
- case 'enum':
63
- return 'text';
64
- case 'binary':
65
- case 'varbinary':
66
- case 'blob':
67
- case 'bytea':
68
- return 'bytea';
69
- case 'vector':
70
- return column.vectorOptions?.elementType === 'float16'
71
- ? `halfvec(${column.vectorOptions.dimensions})`
72
- : column.args?.length
73
- ? `vector(${column.args[0]})`
74
- : 'vector';
75
- case 'halfvec':
76
- return column.args?.length ? `halfvec(${column.args[0]})` : 'halfvec';
77
- default:
78
- return renderTypeWithArgs(String(type).toLowerCase(), column.args);
63
+ };
64
+
65
+ const renderPostgresIndex = (
66
+ table: TableDef,
67
+ index: IndexDef,
68
+ services: SchemaDialectServices
69
+ ): string => {
70
+ const name = index.name || deriveIndexName(table, index);
71
+ let columns = renderIndexColumns(services, index.columns);
72
+ if (index.ops) columns = `${columns} ${index.ops}`;
73
+ const unique = index.unique ? 'UNIQUE ' : '';
74
+ const using = index.using ? ` USING ${index.using}` : '';
75
+ let withClause = '';
76
+ if (index.with) {
77
+ if (typeof index.with === 'string') {
78
+ withClause = ` WITH (${index.with})`;
79
+ } else {
80
+ const params = Object.entries(index.with).map(([key, value]) => `${key} = ${value}`).join(', ');
81
+ withClause = ` WITH (${params})`;
79
82
  }
80
83
  }
81
-
82
- renderAutoIncrement(column: ColumnDef): string | undefined {
83
- if (!column.autoIncrement) return undefined;
84
- const strategy = column.generated === 'always' ? 'GENERATED ALWAYS' : 'GENERATED BY DEFAULT';
85
- return `${strategy} AS IDENTITY`;
86
- }
87
-
88
- renderIndex(table: TableDef, index: IndexDef): string {
89
- const name = index.name || deriveIndexName(table, index);
90
- let cols = renderIndexColumns(this, index.columns);
91
- if (index.ops) {
92
- cols = `${cols} ${index.ops}`;
93
- }
94
- const unique = index.unique ? 'UNIQUE ' : '';
95
- const using = index.using ? ` USING ${index.using}` : '';
96
- let withClause = '';
97
- if (index.with) {
98
- if (typeof index.with === 'string') {
99
- withClause = ` WITH (${index.with})`;
100
- } else {
101
- const params = Object.entries(index.with).map(([k, v]) => `${k} = ${v}`).join(', ');
102
- withClause = ` WITH (${params})`;
84
+ const where = index.where ? ` WHERE ${index.where}` : '';
85
+ return `CREATE ${unique}INDEX IF NOT EXISTS ${services.quoteIdentifier(name)} ON ${services.formatTableName(table)}${using} (${columns})${withClause}${where};`;
86
+ };
87
+
88
+ export const createPostgresSchemaDialect = (): SchemaDialect =>
89
+ composeSchemaDialect({
90
+ name: 'postgres',
91
+ quoteIdentifier,
92
+ literalFormatter,
93
+ renderColumnType: renderPostgresColumnType,
94
+ renderAutoIncrement: column => {
95
+ if (!column.autoIncrement) return undefined;
96
+ const strategy = column.generated === 'always' ? 'GENERATED ALWAYS' : 'GENERATED BY DEFAULT';
97
+ return `${strategy} AS IDENTITY`;
98
+ },
99
+ renderIndex: renderPostgresIndex,
100
+ renderReferenceSuffix: ref => ref.deferrable ? 'DEFERRABLE INITIALLY DEFERRED' : undefined,
101
+ supportsPartialIndexes: true,
102
+ mutations: services => ({
103
+ dropTable: createStandardDropTableCapability(services),
104
+ dropColumn: createStandardDropColumnCapability(services),
105
+ dropIndex: {
106
+ compile(table, index) {
107
+ const qualified = table.schema
108
+ ? `${services.quoteIdentifier(table.schema)}.${services.quoteIdentifier(index)}`
109
+ : services.quoteIdentifier(index);
110
+ return [`DROP INDEX IF EXISTS ${qualified};`];
111
+ }
112
+ },
113
+ alterColumn: {
114
+ compile(table, column, actualColumn, diff) {
115
+ void actualColumn;
116
+ const statements: string[] = [];
117
+ const tableName = services.formatTableName(table);
118
+ const columnName = services.quoteIdentifier(column.name);
119
+
120
+ if (diff.typeChanged) {
121
+ statements.push(
122
+ `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} TYPE ${renderPostgresColumnType(column, services)};`
123
+ );
124
+ }
125
+ if (diff.defaultChanged) {
126
+ statements.push(
127
+ column.default === undefined
128
+ ? `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} DROP DEFAULT;`
129
+ : `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} SET DEFAULT ${services.renderDefault(column.default, column)};`
130
+ );
131
+ }
132
+ if (diff.nullabilityChanged) {
133
+ statements.push(
134
+ `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} ${column.notNull ? 'SET' : 'DROP'} NOT NULL;`
135
+ );
136
+ }
137
+ if (diff.autoIncrementChanged) {
138
+ if (column.autoIncrement) {
139
+ const strategy = column.generated === 'always' ? 'ALWAYS' : 'BY DEFAULT';
140
+ statements.push(
141
+ `ALTER TABLE ${tableName} ALTER COLUMN ${columnName} ADD GENERATED ${strategy} AS IDENTITY;`
142
+ );
143
+ } else {
144
+ statements.push(`ALTER TABLE ${tableName} ALTER COLUMN ${columnName} DROP IDENTITY IF EXISTS;`);
145
+ }
146
+ }
147
+ return statements;
148
+ },
149
+ warning(table, column, actualColumn, diff) {
150
+ void table;
151
+ void column;
152
+ void actualColumn;
153
+ return diff.autoIncrementChanged
154
+ ? 'Altering identity properties may fail if an existing sequence is attached; verify generated column state.'
155
+ : undefined;
156
+ }
103
157
  }
104
- }
105
- const where = index.where ? ` WHERE ${index.where}` : '';
106
- return `CREATE ${unique}INDEX IF NOT EXISTS ${this.quoteIdentifier(name)} ON ${this.formatTableName(table)}${using} (${cols})${withClause}${where};`;
107
- }
108
-
109
- supportsPartialIndexes(): boolean {
110
- return true;
111
- }
158
+ })
159
+ });
112
160
 
113
- protected renderReferenceSuffix(ref: ForeignKeyReference, _table: TableDef): string | undefined {
114
- void _table;
115
- if (ref.deferrable) {
116
- return 'DEFERRABLE INITIALLY DEFERRED';
117
- }
118
- return undefined;
161
+ /** Ergonomic facade; DDL rendering itself is pure composition. */
162
+ export class PostgresSchemaDialect implements SchemaDialect {
163
+ private readonly delegate = createPostgresSchemaDialect();
164
+ readonly name = this.delegate.name;
165
+ readonly mutations = this.delegate.mutations;
166
+
167
+ quoteIdentifier(id: string): string { return this.delegate.quoteIdentifier(id); }
168
+ formatTableName(table: TableDef | DatabaseTable): string { return this.delegate.formatTableName(table); }
169
+ renderColumnType(column: ColumnDef): string { return this.delegate.renderColumnType(column); }
170
+ renderDefault(value: unknown, column: ColumnDef): string { return this.delegate.renderDefault(value, column); }
171
+ renderAutoIncrement(column: ColumnDef, table: TableDef): string | undefined {
172
+ return this.delegate.renderAutoIncrement(column, table);
119
173
  }
120
-
121
- dropColumnSql(table: DatabaseTable, column: string): string[] {
122
- return [`ALTER TABLE ${this.formatTableName(table)} DROP COLUMN ${this.quoteIdentifier(column)};`];
174
+ renderReference(ref: Parameters<SchemaDialect['renderReference']>[0], table: TableDef): string {
175
+ return this.delegate.renderReference(ref, table);
123
176
  }
124
-
125
- dropIndexSql(table: DatabaseTable, index: string): string[] {
126
- const qualified = table.schema
127
- ? `${this.quoteIdentifier(table.schema)}.${this.quoteIdentifier(index)}`
128
- : this.quoteIdentifier(index);
129
- return [`DROP INDEX IF EXISTS ${qualified};`];
130
- }
131
-
132
- alterColumnSql(table: TableDef, column: ColumnDef, actualColumn: DatabaseColumn, diff: ColumnDiff): string[] {
133
- void actualColumn;
134
- const stmts: string[] = [];
135
- const tableName = this.formatTableName(table);
136
- const colName = this.quoteIdentifier(column.name);
137
-
138
- if (diff.typeChanged) {
139
- stmts.push(`ALTER TABLE ${tableName} ALTER COLUMN ${colName} TYPE ${this.renderColumnType(column)};`);
140
- }
141
- if (diff.defaultChanged) {
142
- if (column.default === undefined) {
143
- stmts.push(`ALTER TABLE ${tableName} ALTER COLUMN ${colName} DROP DEFAULT;`);
144
- } else {
145
- stmts.push(
146
- `ALTER TABLE ${tableName} ALTER COLUMN ${colName} SET DEFAULT ${this.renderDefault(column.default, column)};`
147
- );
148
- }
149
- }
150
- if (diff.nullabilityChanged) {
151
- stmts.push(`ALTER TABLE ${tableName} ALTER COLUMN ${colName} ${column.notNull ? 'SET' : 'DROP'} NOT NULL;`);
152
- }
153
- if (diff.autoIncrementChanged) {
154
- if (column.autoIncrement) {
155
- const strategy = column.generated === 'always' ? 'ALWAYS' : 'BY DEFAULT';
156
- stmts.push(`ALTER TABLE ${tableName} ALTER COLUMN ${colName} ADD GENERATED ${strategy} AS IDENTITY;`);
157
- } else {
158
- stmts.push(`ALTER TABLE ${tableName} ALTER COLUMN ${colName} DROP IDENTITY IF EXISTS;`);
159
- }
160
- }
161
- return stmts;
162
- }
163
-
164
- warnAlterColumn(_table: TableDef, _column: ColumnDef, _actual: DatabaseColumn, diff: ColumnDiff): string | undefined {
165
- void _table;
166
- void _column;
167
- void _actual;
168
- if (diff.autoIncrementChanged) {
169
- return 'Altering identity properties may fail if an existing sequence is attached; verify generated column state.';
170
- }
171
- return undefined;
177
+ renderIndex(table: TableDef, index: IndexDef): string { return this.delegate.renderIndex(table, index); }
178
+ renderTableOptions(table: TableDef): string | undefined { return this.delegate.renderTableOptions(table); }
179
+ supportsPartialIndexes(): boolean { return this.delegate.supportsPartialIndexes(); }
180
+ preferInlinePkAutoincrement(column: ColumnDef, table: TableDef, pk: string[]): boolean {
181
+ return this.delegate.preferInlinePkAutoincrement(column, table, pk);
172
182
  }
173
183
  }
174
-
@@ -1,70 +1,50 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { BaseSchemaDialect } from './base-schema-dialect.js';
2
+ import { composeSchemaDialect } from '../schema-dialect-composer.js';
3
3
  import { PostgresSchemaDialect } from './postgres-schema-dialect.js';
4
- import { TableDef } from '../../../schema/table.js';
5
- import { ForeignKeyReference } from '../../../schema/column-types.js';
4
+ import type { TableDef } from '../../../schema/table.js';
5
+ import type { ForeignKeyReference } from '../../../schema/column-types.js';
6
6
  import { createLiteralFormatter } from '../sql-writing.js';
7
7
 
8
- class DummySchemaDialect extends BaseSchemaDialect {
9
- readonly name = 'sqlite';
10
- private readonly formatter = createLiteralFormatter({
11
- booleanTrue: '1',
12
- booleanFalse: '0',
13
- });
14
-
15
- get literalFormatter() {
16
- return this.formatter;
17
- }
18
-
19
- quoteIdentifier(id: string): string {
20
- return `"${id}"`;
21
- }
22
-
23
- renderColumnType(): string {
24
- return 'INTEGER';
25
- }
26
-
27
- renderAutoIncrement(): string | undefined {
28
- return undefined;
29
- }
30
-
31
- renderIndex(): string {
32
- return 'CREATE INDEX dummy;';
33
- }
34
- }
8
+ const createDummySchemaDialect = () => composeSchemaDialect({
9
+ name: 'sqlite',
10
+ quoteIdentifier: id => `"${id}"`,
11
+ literalFormatter: createLiteralFormatter({ booleanTrue: '1', booleanFalse: '0' }),
12
+ renderColumnType: () => 'INTEGER',
13
+ renderAutoIncrement: () => undefined,
14
+ renderIndex: () => 'CREATE INDEX dummy;'
15
+ });
35
16
 
36
17
  const table: TableDef = {
37
- name: 'child',
38
- columns: {},
39
- relations: {},
18
+ name: 'child',
19
+ columns: {},
20
+ relations: {}
40
21
  };
41
22
 
42
23
  const deferrableReference: ForeignKeyReference = {
43
- table: 'parent',
44
- column: 'id',
45
- deferrable: true,
46
- onDelete: 'CASCADE',
47
- onUpdate: 'NO ACTION',
24
+ table: 'parent',
25
+ column: 'id',
26
+ deferrable: true,
27
+ onDelete: 'CASCADE',
28
+ onUpdate: 'NO ACTION'
48
29
  };
49
30
 
50
31
  describe('renderReference deferrable handling', () => {
51
- it('base dialect remains agnostic to deferrable flags', () => {
52
- const dialect = new DummySchemaDialect();
53
- const sql = dialect.renderReference(deferrableReference, table);
54
- expect(sql).toContain('REFERENCES "parent"');
55
- expect(sql).not.toContain('DEFERRABLE INITIALLY DEFERRED');
56
- });
57
-
58
- it('Postgres dialect renders the deferrable clause', () => {
59
- const dialect = new PostgresSchemaDialect();
60
- const sql = dialect.renderReference(deferrableReference, table);
61
- expect(sql).toContain('DEFERRABLE INITIALLY DEFERRED');
62
- });
63
-
64
- it('Postgres dialect skips the clause when the flag is missing', () => {
65
- const dialect = new PostgresSchemaDialect();
66
- const sql = dialect.renderReference({ table: 'parent', column: 'id' }, table);
67
- expect(sql).not.toContain('DEFERRABLE INITIALLY DEFERRED');
68
- });
32
+ it('composed generic dialect remains agnostic to deferrable flags', () => {
33
+ const dialect = createDummySchemaDialect();
34
+ const sql = dialect.renderReference(deferrableReference, table);
35
+ expect(sql).toContain('REFERENCES "parent"');
36
+ expect(sql).not.toContain('DEFERRABLE INITIALLY DEFERRED');
37
+ });
38
+
39
+ it('Postgres dialect renders the deferrable clause', () => {
40
+ const dialect = new PostgresSchemaDialect();
41
+ const sql = dialect.renderReference(deferrableReference, table);
42
+ expect(sql).toContain('DEFERRABLE INITIALLY DEFERRED');
43
+ });
44
+
45
+ it('Postgres dialect skips the clause when the flag is missing', () => {
46
+ const dialect = new PostgresSchemaDialect();
47
+ const sql = dialect.renderReference({ table: 'parent', column: 'id' }, table);
48
+ expect(sql).not.toContain('DEFERRABLE INITIALLY DEFERRED');
49
+ });
69
50
  });
70
-