• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rogerpadilla / uql / 23874861559

01 Apr 2026 10:56PM UTC coverage: 94.915% (-0.03%) from 94.943%
23874861559

push

github

rogerpadilla
refactor: streamline Bun SQL Postgres update calls in tests for improved readability

2976 of 3299 branches covered (90.21%)

Branch coverage included in aggregate %.

5256 of 5374 relevant lines covered (97.8%)

346.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.77
/packages/uql-orm/src/migrate/schemaGenerator.ts
1
import { type AbstractDialect, AbstractSqlDialect } from '../dialect/index.js';
2
import { getMeta } from '../entity/index.js';
3
import {
4
  areTypesEqual,
5
  canonicalToSql,
6
  fieldOptionsToCanonical,
7
  isVectorCategory,
8
  sqlToCanonical,
9
} from '../schema/canonicalType.js';
10
import { SchemaASTBuilder } from '../schema/schemaASTBuilder.js';
11
import type {
12
  CanonicalType,
13
  ColumnNode,
14
  ForeignKeyAction,
15
  IndexNode,
16
  RelationshipNode,
17
  TableNode,
18
} from '../schema/types.js';
19
import type {
20
  ColumnSchema,
21
  DialectFeatures,
22
  EntityMeta,
23
  FieldKey,
24
  FieldOptions,
25
  IndexSchema,
26
  NamingStrategy,
27
  SchemaDiff,
28
  SchemaGenerator,
29
  Type,
30
  VectorDistance,
31
} from '../type/index.js';
32
import { escapeSqlId, getKeys, isAutoIncrement } from '../util/index.js';
33
import { formatDefaultValue } from './builder/expressions.js';
34
import type { FullColumnDefinition, TableDefinition, TableForeignKeyDefinition } from './builder/types.js';
35

36
/** Maps UQL distance metric names to inline DDL keywords (e.g. MariaDB `DISTANCE=euclidean`). */
37
const INLINE_VECTOR_DISTANCE_MAP: Partial<Record<VectorDistance, string>> = { cosine: 'cosine', l2: 'euclidean' };
20✔
38

39
/**
40
 * Unified SQL schema generator.
41
 * Parameterized by dialect to handle Postgres, MySQL, MariaDB, and SQLite.
42
 */
43
export class SqlSchemaGenerator implements SchemaGenerator {
44
  constructor(
45
    protected readonly dialect: AbstractSqlDialect,
202✔
46
    protected readonly defaultForeignKeyAction: ForeignKeyAction = 'NO ACTION',
202✔
47
  ) {}
48

49
  get namingStrategy(): NamingStrategy | undefined {
50
    return this.dialect.namingStrategy;
×
51
  }
52

53
  get features(): DialectFeatures {
54
    return this.dialect.features;
176✔
55
  }
56

57
  resolveTableName<E>(entity: Type<E>, meta: EntityMeta<E>): string {
58
    return this.dialect.resolveTableName(entity, meta);
286✔
59
  }
60

61
  resolveColumnName(key: string, field: FieldOptions): string {
62
    return this.dialect.resolveColumnName(key, field);
×
63
  }
64

65
  /**
66
   * Escape an identifier (table name, column name, etc.)
67
   */
68
  protected escapeId(identifier: string): string {
69
    return escapeSqlId(identifier, this.dialect.quoteChar);
827✔
70
  }
71

72
  /**
73
   * Primary key type for auto-increment integer IDs
74
   */
75
  protected get serialPrimaryKeyType(): string {
76
    return this.dialect.serialPrimaryKey;
76✔
77
  }
78

79
  // ============================================================================
80
  // CanonicalType Integration (Unified Type System)
81
  // ============================================================================
82

83
  /**
84
   * Convert FieldOptions to CanonicalType using the unified type system.
85
   */
86
  protected getCanonicalType(field: FieldOptions, fieldType?: unknown): CanonicalType {
87
    return fieldOptionsToCanonical(field, fieldType);
177✔
88
  }
89

90
  /**
91
   * Convert CanonicalType to SQL type string for this dialect.
92
   * Also handles legacy string types for backward compatibility.
93
   */
94
  protected canonicalTypeToSql(type: CanonicalType | string): string {
95
    // Handle legacy string types
96
    if (typeof type === 'string') {
557✔
97
      return type;
7✔
98
    }
99
    return canonicalToSql(type, this.dialect);
550✔
100
  }
101

102
  // ============================================================================
103
  // SchemaGenerator Implementation
104
  // ============================================================================
105

106
  generateCreateTable<E>(entity: Type<E>, options: { ifNotExists?: boolean } = {}): string {
43✔
107
    const builder = new SchemaASTBuilder(this.dialect.namingStrategy);
43✔
108
    const ast = builder.fromEntities([entity]);
43✔
109
    const tableNode = ast.getTables()[0];
43✔
110
    return this.generateCreateTableFromNode(tableNode, options);
43✔
111
  }
112

113
  generateDropTable<E>(entity: Type<E>): string {
114
    const meta = getMeta(entity);
8✔
115
    const tableName = this.dialect.resolveTableName(entity, meta);
8✔
116
    return `DROP TABLE IF EXISTS ${this.escapeId(tableName)};`;
8✔
117
  }
118

119
  generateAlterTable(diff: SchemaDiff): string[] {
120
    const statements: string[] = [];
47✔
121
    const tableName = this.escapeId(diff.tableName);
47✔
122

123
    // Add new columns
124
    if (diff.columnsToAdd?.length) {
47✔
125
      for (const column of diff.columnsToAdd) {
29✔
126
        const colDef = this.generateColumnDefinitionFromSchema(column);
39✔
127
        statements.push(`ALTER TABLE ${tableName} ADD COLUMN ${colDef};`);
39✔
128
      }
129
    }
130

131
    // Alter existing columns
132
    if (diff.columnsToAlter?.length) {
47✔
133
      for (const { to } of diff.columnsToAlter) {
5✔
134
        const colDef = this.generateColumnDefinitionFromSchema(to);
5✔
135
        const colStatements = this.generateAlterColumnStatements(diff.tableName, to, colDef);
5✔
136
        statements.push(...colStatements);
5✔
137
      }
138
    }
139

140
    // Drop columns
141
    if (diff.columnsToDrop?.length) {
46✔
142
      for (const columnName of diff.columnsToDrop) {
6✔
143
        statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.escapeId(columnName)};`);
6✔
144
      }
145
    }
146

147
    // Add indexes
148
    if (diff.indexesToAdd?.length) {
46✔
149
      for (const index of diff.indexesToAdd) {
2✔
150
        statements.push(this.generateCreateIndex(diff.tableName, index));
2✔
151
      }
152
    }
153

154
    // Drop indexes
155
    if (diff.indexesToDrop?.length) {
46✔
156
      for (const indexName of diff.indexesToDrop) {
2✔
157
        statements.push(this.generateDropIndex(diff.tableName, indexName));
2✔
158
      }
159
    }
160

161
    return statements;
46✔
162
  }
163

164
  generateAlterTableDown(diff: SchemaDiff): string[] {
165
    const statements: string[] = [];
5✔
166
    const tableName = this.escapeId(diff.tableName);
5✔
167

168
    // Reverse column additions by dropping them
169
    if (diff.columnsToAdd?.length) {
5✔
170
      for (const column of diff.columnsToAdd) {
1✔
171
        statements.push(`ALTER TABLE ${tableName} DROP COLUMN ${this.escapeId(column.name)};`);
1✔
172
      }
173
    }
174

175
    // Reverse column alterations by restoring original schema
176
    if (diff.columnsToAlter?.length) {
5✔
177
      for (const { from } of diff.columnsToAlter) {
1✔
178
        const colDef = this.generateColumnDefinitionFromSchema(from, { includePrimaryKey: false });
1✔
179
        const colStatements = this.generateAlterColumnStatements(diff.tableName, from, colDef);
1✔
180
        statements.push(...colStatements);
1✔
181
      }
182
    }
183

184
    // Reverse index additions by dropping them
185
    if (diff.indexesToAdd?.length) {
5✔
186
      for (const index of diff.indexesToAdd) {
1✔
187
        statements.push(this.generateDropIndex(diff.tableName, index.name));
1✔
188
      }
189
    }
190

191
    if (diff.columnsToDrop?.length || diff.indexesToDrop?.length) {
5✔
192
      statements.push(`-- TODO: Manual reversal needed for dropped columns/indexes`);
2✔
193
    }
194

195
    return statements;
5✔
196
  }
197

198
  generateCreateIndex(tableName: string, index: IndexSchema, options: { ifNotExists?: boolean } = {}): string {
21✔
199
    const unique = index.unique ? 'UNIQUE ' : '';
21✔
200
    const ifNotExists = (options.ifNotExists ?? this.features.indexIfNotExists) ? 'IF NOT EXISTS ' : '';
21✔
201
    const opsClassMap = this.dialect.vectorOpsClass;
21✔
202
    const hasOpsClass = opsClassMap && (index.type === 'hnsw' || index.type === 'ivfflat');
21✔
203

204
    // For pgvector indexes: append operator class to the column
205
    const columns = index.columns
21✔
206
      .map((c) => {
207
        const escaped = this.escapeId(c);
29✔
208
        if (hasOpsClass && index.distance) {
29✔
209
          const opsClass = opsClassMap[index.distance];
3✔
210
          return opsClass ? `${escaped} ${opsClass}` : escaped;
3!
211
        }
212
        return escaped;
26✔
213
      })
214
      .join(', ');
215

216
    const using = index.type ? ` USING ${index.type}` : '';
21✔
217

218
    // Build WITH params for vector indexes with operator class support
219
    let withClause = '';
21✔
220
    if (hasOpsClass) {
21✔
221
      const withParams: string[] = [];
3✔
222
      if (index.m !== undefined) withParams.push(`m = ${index.m}`);
3✔
223
      if (index.efConstruction !== undefined) withParams.push(`ef_construction = ${index.efConstruction}`);
3✔
224
      if (index.lists !== undefined) withParams.push(`lists = ${index.lists}`);
3✔
225
      withClause = withParams.length > 0 ? ` WITH (${withParams.join(', ')})` : '';
3✔
226
    }
227

228
    return `CREATE ${unique}INDEX ${ifNotExists}${this.escapeId(index.name)} ON ${this.escapeId(tableName)}${using} (${columns})${withClause};`;
21✔
229
  }
230

231
  generateDropIndex(tableName: string, indexName: string): string {
232
    if (this.dialect.dropIndexSyntax === 'on-table') {
7✔
233
      return `DROP INDEX ${this.escapeId(indexName)} ON ${this.escapeId(tableName)};`;
2✔
234
    }
235
    return `DROP INDEX IF EXISTS ${this.escapeId(indexName)};`;
5✔
236
  }
237

238
  /**
239
   * Generate column definition from a ColumnSchema object
240
   */
241
  public generateColumnDefinitionFromSchema(
242
    column: ColumnSchema,
243
    options: { includePrimaryKey?: boolean; includeUnique?: boolean } = {},
45✔
244
  ): string {
245
    const { includePrimaryKey = true, includeUnique = true } = options;
45✔
246

247
    let type = column.type;
45✔
248

249
    if (!type.includes('(')) {
45✔
250
      if (column.precision !== undefined) {
30!
251
        if (column.scale !== undefined) {
×
252
          type += `(${column.precision}, ${column.scale})`;
×
253
        } else {
254
          type += `(${column.precision})`;
×
255
        }
256
      } else if (column.length !== undefined) {
30!
257
        type += `(${column.length})`;
×
258
      }
259
    }
260

261
    if (!includePrimaryKey) {
45✔
262
      type = type.replace(/\s+PRIMARY\s+KEY/i, '');
1✔
263
    }
264

265
    let definition = `${this.escapeId(column.name)} ${type}`;
45✔
266

267
    if (includePrimaryKey && column.isPrimaryKey && !type.includes('PRIMARY KEY')) {
45!
268
      definition += ' PRIMARY KEY';
×
269
    }
270

271
    if (!column.nullable && !column.isPrimaryKey) {
45!
272
      definition += ' NOT NULL';
×
273
    }
274

275
    if (includeUnique && column.isUnique && !column.isPrimaryKey) {
45!
276
      definition += ' UNIQUE';
×
277
    }
278

279
    if (column.defaultValue !== undefined) {
45!
280
      definition += ` DEFAULT ${this.formatDefaultValue(column.defaultValue)}`;
×
281
    }
282

283
    if (column.comment) {
45!
284
      definition += this.generateColumnComment(column.name, column.comment);
×
285
    }
286

287
    return definition;
45✔
288
  }
289

290
  public getSqlType(field: FieldOptions, fieldType?: unknown): string {
291
    // If field has a reference, inherit type from the target primary key
292
    if (field.references) {
178✔
293
      const refEntity = field.references();
1✔
294
      const refMeta = getMeta(refEntity);
1✔
295
      const refIdField = refMeta.fields[refMeta.id!];
1✔
296
      return this.getSqlType(
1✔
297
        { ...refIdField!, references: undefined, isId: undefined, autoIncrement: false },
298
        refIdField!.type,
299
      );
300
    }
301

302
    // Get canonical type and convert to SQL
303
    const canonical = this.getCanonicalType(field, fieldType);
177✔
304

305
    // Special case for serial primary keys
306
    if (isAutoIncrement(field, field.isId === true)) {
177✔
307
      return this.dialect.serialPrimaryKey;
53✔
308
    }
309

310
    return this.canonicalTypeToSql(canonical);
124✔
311
  }
312

313
  /**
314
   * Get the boolean type for this database
315
   */
316
  public getBooleanType(): string {
317
    return this.canonicalTypeToSql({ category: 'boolean' });
3✔
318
  }
319

320
  /**
321
   * Generate ALTER COLUMN statements (database-specific)
322
   */
323
  public generateAlterColumnStatements(tableName: string, column: ColumnSchema, newDefinition: string): string[] {
324
    const table = this.escapeId(tableName);
10✔
325
    const colName = this.escapeId(column.name);
10✔
326

327
    if (this.dialect.alterColumnSyntax === 'none') {
10✔
328
      throw new Error(
2✔
329
        `${this.dialect}: Cannot alter column "${column.name}" - you must recreate the table. ` +
330
          `This database does not support ALTER COLUMN.`,
331
      );
332
    }
333

334
    if (this.dialect.alterColumnStrategy === 'separate-clauses') {
8✔
335
      const statements: string[] = [];
4✔
336
      // Separate ALTER COLUMN clauses for different changes (Postgres)
337
      statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} TYPE ${column.type};`);
4✔
338

339
      if (column.nullable) {
4✔
340
        statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} DROP NOT NULL;`);
2✔
341
      } else {
342
        statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} SET NOT NULL;`);
2✔
343
      }
344

345
      if (column.defaultValue !== undefined) {
4✔
346
        statements.push(
1✔
347
          `ALTER TABLE ${table} ALTER COLUMN ${colName} SET DEFAULT ${this.formatDefaultValue(column.defaultValue)};`,
348
        );
349
      } else {
350
        statements.push(`ALTER TABLE ${table} ALTER COLUMN ${colName} DROP DEFAULT;`);
3✔
351
      }
352
      return statements;
4✔
353
    }
354

355
    return [`ALTER TABLE ${table} ${this.dialect.alterColumnSyntax} ${newDefinition};`];
4✔
356
  }
357

358
  /**
359
   * Get table options (e.g., ENGINE for MySQL)
360
   */
361
  public getTableOptions<E>(_meta: EntityMeta<E>): string {
362
    return this.dialect.tableOptions ? ` ${this.dialect.tableOptions}` : '';
2✔
363
  }
364

365
  /**
366
   * Generate column comment clause (if supported)
367
   */
368
  public generateColumnComment(columnName: string, comment: string): string {
369
    if (this.features.columnComment) {
8✔
370
      const escapedComment = comment.replace(/'/g, "''");
3✔
371
      return ` COMMENT '${escapedComment}'`;
3✔
372
    }
373
    return '';
5✔
374
  }
375

376
  /**
377
   * Format a default value for SQL
378
   */
379
  public formatDefaultValue(value: unknown): string {
380
    if (this.dialect.booleanLiteral === 'integer' && typeof value === 'boolean') {
26✔
381
      return value ? '1' : '0';
5✔
382
    }
383
    return formatDefaultValue(value);
21✔
384
  }
385

386
  /**
387
   * Compare an entity with a database table node and return the differences.
388
   */
389
  diffSchema<E>(entity: Type<E>, currentTable: TableNode | undefined): SchemaDiff | undefined {
390
    const meta = getMeta(entity);
82✔
391

392
    if (!currentTable) {
82✔
393
      return {
31✔
394
        tableName: this.dialect.resolveTableName(entity, meta),
395
        type: 'create',
396
      };
397
    }
398

399
    const columnsToAdd: ColumnSchema[] = [];
51✔
400
    const columnsToAlter: { from: ColumnSchema; to: ColumnSchema }[] = [];
51✔
401
    const columnsToDrop: string[] = [];
51✔
402

403
    const currentColumns = new Map<string, ColumnNode>(currentTable.columns);
51✔
404
    const fieldKeys = getKeys(meta.fields) as FieldKey<E>[];
51✔
405

406
    for (const key of fieldKeys) {
51✔
407
      const field = meta.fields[key];
132✔
408
      if (!field || field.virtual) continue;
132!
409

410
      const columnName = this.dialect.resolveColumnName(key, field);
132✔
411
      const currentColumn = currentColumns.get(columnName);
132✔
412

413
      if (!currentColumn) {
132✔
414
        columnsToAdd.push(this.fieldToColumnSchema(key, field, meta));
39✔
415
      } else {
416
        const desiredColumn = this.fieldToColumnSchema(key, field, meta);
93✔
417
        const currentColumnSchema = this.columnNodeToSchema(currentColumn);
93✔
418
        if (this.columnsNeedAlteration(currentColumnSchema, desiredColumn)) {
93✔
419
          columnsToAlter.push({ from: currentColumnSchema, to: desiredColumn });
16✔
420
        }
421
      }
422
      currentColumns.delete(columnName);
132✔
423
    }
424

425
    for (const [name] of currentColumns) {
51✔
426
      columnsToDrop.push(name);
17✔
427
    }
428

429
    if (columnsToAdd.length === 0 && columnsToAlter.length === 0 && columnsToDrop.length === 0) {
51✔
430
      return undefined;
4✔
431
    }
432

433
    return {
47✔
434
      tableName: this.dialect.resolveTableName(entity, meta),
435
      type: 'alter',
436
      columnsToAdd: columnsToAdd.length > 0 ? columnsToAdd : undefined,
47✔
437
      columnsToAlter: columnsToAlter.length > 0 ? columnsToAlter : undefined,
47✔
438
      columnsToDrop: columnsToDrop.length > 0 ? columnsToDrop : undefined,
47✔
439
    };
440
  }
441

442
  private columnNodeToSchema(col: ColumnNode): ColumnSchema {
443
    return {
93✔
444
      name: col.name,
445
      type: this.canonicalTypeToSql(col.type),
446
      nullable: col.nullable,
447
      defaultValue: col.defaultValue,
448
      isPrimaryKey: col.isPrimaryKey,
449
      isAutoIncrement: col.isAutoIncrement,
450
      isUnique: col.isUnique,
451
      comment: col.comment,
452
    };
453
  }
454

455
  /**
456
   * Convert field options to ColumnSchema
457
   */
458
  protected fieldToColumnSchema<E>(fieldKey: string, field: FieldOptions, meta: EntityMeta<E>): ColumnSchema {
459
    const isPrimaryKey = field.isId === true && meta.id === fieldKey;
132✔
460

461
    return {
132✔
462
      name: this.dialect.resolveColumnName(fieldKey, field),
463
      type: this.getSqlType(field, field.type),
464
      nullable: field.nullable ?? !isPrimaryKey,
264✔
465
      defaultValue: field.defaultValue,
466
      isPrimaryKey,
467
      isAutoIncrement: isAutoIncrement(field, isPrimaryKey),
468
      isUnique: field.unique ?? false,
264✔
469
      length: field.length,
470
      precision: field.precision,
471
      scale: field.scale,
472
      comment: field.comment,
473
    };
474
  }
475

476
  /**
477
   * Check if two columns differ enough to require alteration
478
   */
479
  protected columnsNeedAlteration(current: ColumnSchema, desired: ColumnSchema): boolean {
480
    if (current.isPrimaryKey && desired.isPrimaryKey) {
93✔
481
      return false;
51✔
482
    }
483

484
    if (current.isPrimaryKey !== desired.isPrimaryKey) return true;
42!
485
    if (current.nullable !== desired.nullable) return true;
42!
486
    if (current.isUnique !== desired.isUnique) return true;
42!
487

488
    if (!this.isTypeEqual(current, desired)) return true;
42✔
489
    if (!this.isDefaultValueEqual(current.defaultValue, desired.defaultValue)) return true;
32✔
490

491
    return false;
26✔
492
  }
493

494
  /**
495
   * Compare two column types for equality using the canonical type system
496
   */
497
  protected isTypeEqual(current: ColumnSchema, desired: ColumnSchema): boolean {
498
    const typeA = sqlToCanonical(current.type);
42✔
499
    const typeB = sqlToCanonical(desired.type);
42✔
500
    return areTypesEqual(typeA, typeB);
42✔
501
  }
502

503
  /**
504
   * Compare two default values for equality
505
   */
506
  protected isDefaultValueEqual(current: unknown, desired: unknown): boolean {
507
    if (current === desired) return true;
32✔
508
    if (current === undefined || desired === undefined) return current === desired;
6!
509

510
    const normalize = (val: unknown): string => {
×
511
      if (val === null) return 'null';
×
512
      if (typeof val === 'string') {
×
513
        let s = val.replace(/::[a-z_]+(\s+[a-z_]+)*(\[\])?$/i, '');
×
514
        s = s.replace(/^'(.*)'$/, '$1');
×
515
        if (s.toLowerCase() === 'null') return 'null';
×
516
        return s;
×
517
      }
518
      return typeof val === 'object' ? JSON.stringify(val) : String(val);
×
519
    };
520

521
    return normalize(current) === normalize(desired);
×
522
  }
523

524
  // ============================================================================
525
  // SchemaAST Support Methods
526
  // ============================================================================
527

528
  /**
529
   * Generate CREATE TABLE SQL from a TableNode.
530
   */
531
  generateCreateTableFromNode(table: TableNode, options: { ifNotExists?: boolean } = {}): string {
82✔
532
    const columns: string[] = [];
82✔
533
    const constraints: string[] = [];
82✔
534

535
    for (const col of table.columns.values()) {
82✔
536
      const colDef = this.generateColumnFromNode(col);
335✔
537
      columns.push(colDef);
335✔
538
    }
539

540
    if (table.primaryKey.length > 1) {
82✔
541
      const pkCols = table.primaryKey.map((c) => this.escapeId(c.name)).join(', ');
8✔
542
      constraints.push(`PRIMARY KEY (${pkCols})`);
4✔
543
    }
544

545
    // Add table-level foreign keys if any
546
    for (const rel of table.outgoingRelations) {
82✔
547
      if (rel.from.columns.length > 0) {
26!
548
        const fromCols = rel.from.columns.map((c) => this.escapeId(c.name)).join(', ');
26✔
549
        const toCols = rel.to.columns.map((c) => this.escapeId(c.name)).join(', ');
26✔
550
        const constraintName = rel.name ? `CONSTRAINT ${this.escapeId(rel.name)} ` : '';
26!
551
        constraints.push(
26✔
552
          `${constraintName}FOREIGN KEY (${fromCols}) REFERENCES ${this.escapeId(rel.to.table.name)} (${toCols})` +
553
            ` ON DELETE ${rel.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${rel.onUpdate ?? this.defaultForeignKeyAction}`,
52!
554
        );
555
      }
556
    }
557

558
    const ifNotExists = options.ifNotExists && this.features.ifNotExists ? 'IF NOT EXISTS ' : '';
82✔
559
    let sql = `CREATE TABLE ${ifNotExists}${this.escapeId(table.name)} (\n`;
82✔
560
    sql += columns.map((col) => `  ${col}`).join(',\n');
335✔
561

562
    if (constraints.length > 0) {
82✔
563
      sql += ',\n';
26✔
564
      sql += constraints.map((c) => `  ${c}`).join(',\n');
30✔
565
    }
566

567
    // Inline vector indexes (MariaDB requires them inside CREATE TABLE)
568
    const isInlineVectorIdx = this.features.vectorIndexStyle === 'inline';
82✔
569
    const vectorIndexes = isInlineVectorIdx ? table.indexes.filter((idx) => idx.type === 'vector') : [];
82✔
570
    const regularIndexes = isInlineVectorIdx ? table.indexes.filter((idx) => idx.type !== 'vector') : table.indexes;
82✔
571

572
    if (vectorIndexes.length > 0) {
82✔
573
      sql += ',\n';
3✔
574
      sql += vectorIndexes.map((idx) => `  ${this.generateInlineVectorIndex(idx)}`).join(',\n');
3✔
575
    }
576

577
    sql += '\n)';
82✔
578

579
    if (this.dialect.tableOptions) {
82✔
580
      sql += ` ${this.dialect.tableOptions}`;
24✔
581
    }
582

583
    sql += ';';
82✔
584

585
    // Generate regular indexes as separate statements
586
    const indexStatements = regularIndexes.map((idx) => this.generateCreateIndexFromNode(idx)).join('\n');
82✔
587

588
    if (indexStatements) {
82!
589
      sql += `\n${indexStatements}`;
×
590
    }
591

592
    // Prepend CREATE EXTENSION for dialects that require it (e.g. pgvector)
593
    if (this.dialect.vectorExtension) {
82✔
594
      const hasVectorCol = [...table.columns.values()].some((c) => isVectorCategory(c.type.category));
177✔
595
      if (hasVectorCol) {
39✔
596
        sql = `CREATE EXTENSION IF NOT EXISTS ${this.dialect.vectorExtension};\n${sql}`;
2✔
597
      }
598
    }
599

600
    return sql;
82✔
601
  }
602

603
  /**
604
   * Generate a column definition from a ColumnNode.
605
   */
606
  protected generateColumnFromNode(col: ColumnNode): string {
607
    const colName = this.escapeId(col.name);
337✔
608
    let sqlType = this.canonicalTypeToSql(col.type);
337✔
609

610
    if (col.isPrimaryKey && col.isAutoIncrement) {
337✔
611
      sqlType = this.serialPrimaryKeyType;
76✔
612
    }
613

614
    let def = `${colName} ${sqlType}`;
337✔
615

616
    if (!col.nullable && !col.isPrimaryKey) {
337✔
617
      def += ' NOT NULL';
44✔
618
    }
619

620
    if (col.isPrimaryKey && col.table.primaryKey.length === 1 && !sqlType.includes('PRIMARY KEY')) {
337✔
621
      def += ' PRIMARY KEY';
2✔
622
    }
623

624
    if (col.isUnique && !col.isPrimaryKey) {
337✔
625
      def += ' UNIQUE';
8✔
626
    }
627

628
    if (col.defaultValue !== undefined) {
337✔
629
      def += ` DEFAULT ${this.formatDefaultValue(col.defaultValue)}`;
16✔
630
    }
631

632
    if (col.comment) {
337!
633
      def += this.generateColumnComment(col.name, col.comment);
×
634
    }
635

636
    return def;
337✔
637
  }
638

639
  /**
640
   * Generate an inline VECTOR INDEX clause for use inside CREATE TABLE.
641
   * Syntax: `VECTOR INDEX (col) M=n DISTANCE=metric`
642
   */
643
  private generateInlineVectorIndex(index: IndexNode): string {
644
    const columns = index.columns.map((c) => this.escapeId(c.name)).join(', ');
3✔
645
    let clause = `VECTOR INDEX (${columns})`;
3✔
646

647
    if (index.m !== undefined) {
3✔
648
      clause += ` M=${index.m}`;
1✔
649
    }
650

651
    if (index.distance) {
3✔
652
      const metric = INLINE_VECTOR_DISTANCE_MAP[index.distance] ?? 'euclidean';
2!
653
      clause += ` DISTANCE=${metric}`;
2✔
654
    }
655

656
    return clause;
3✔
657
  }
658

659
  /**
660
   * Generate CREATE INDEX SQL from an IndexNode.
661
   * Delegates to `generateCreateIndex` for unified SQL assembly.
662
   */
663
  generateCreateIndexFromNode(index: IndexNode, options: { ifNotExists: boolean } = { ifNotExists: false }): string {
1✔
664
    return this.generateCreateIndex(
1✔
665
      index.table.name,
666
      {
667
        name: index.name,
668
        columns: index.columns.map((c) => c.name),
1✔
669
        unique: index.unique,
670
        type: index.type,
671
        distance: index.distance,
672
        m: index.m,
673
        efConstruction: index.efConstruction,
674
        lists: index.lists,
675
      },
676
      options,
677
    );
678
  }
679

680
  /**
681
   * Generate DROP TABLE SQL from a TableNode.
682
   */
683
  generateDropTableFromNode(table: TableNode, options: { ifExists?: boolean } = {}): string {
1✔
684
    const ifExists = options.ifExists ? 'IF EXISTS ' : '';
1!
685
    return `DROP TABLE ${ifExists}${this.escapeId(table.name)};`;
1✔
686
  }
687

688
  // ============================================================================
689
  // Phase 3: Builder Operation Methods (Moved forward for unification)
690
  // ============================================================================
691

692
  generateCreateTableFromDefinition(table: TableDefinition, options: { ifNotExists?: boolean } = {}): string {
34✔
693
    const tableNode = this.tableDefinitionToNode(table);
34✔
694
    return this.generateCreateTableFromNode(tableNode, options);
34✔
695
  }
696

697
  generateDropTableSql(tableName: string, options?: { ifExists?: boolean; cascade?: boolean }): string {
698
    const ifExists = options?.ifExists ? 'IF EXISTS ' : '';
65✔
699
    // Use dialect-specific cascade support from config
700
    const cascade = options?.cascade && this.features.dropTableCascade ? ' CASCADE' : '';
65✔
701
    return `DROP TABLE ${ifExists}${this.escapeId(tableName)}${cascade};`;
65✔
702
  }
703

704
  generateRenameTableSql(oldName: string, newName: string): string {
705
    if (this.dialect.renameTableSyntax === 'rename-table') {
1!
706
      return `RENAME TABLE ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
×
707
    }
708
    return `ALTER TABLE ${this.escapeId(oldName)} RENAME TO ${this.escapeId(newName)};`;
1✔
709
  }
710

711
  generateAddColumnSql(tableName: string, column: FullColumnDefinition): string {
712
    const colSql = this.generateColumnFromNode(this.fullColumnDefinitionToNode(column, tableName));
1✔
713
    return `ALTER TABLE ${this.escapeId(tableName)} ADD COLUMN ${colSql};`;
1✔
714
  }
715

716
  generateAlterColumnSql(tableName: string, columnName: string, column: FullColumnDefinition): string {
717
    const colSql = this.generateColumnFromNode(this.fullColumnDefinitionToNode(column, tableName));
1✔
718
    return this.generateAlterColumnStatements(tableName, { name: columnName, type: '' } as ColumnSchema, colSql).join(
1✔
719
      '\n',
720
    );
721
  }
722

723
  generateDropColumnSql(tableName: string, columnName: string): string {
724
    return `ALTER TABLE ${this.escapeId(tableName)} DROP COLUMN ${this.escapeId(columnName)};`;
1✔
725
  }
726

727
  generateRenameColumnSql(tableName: string, oldName: string, newName: string): string {
728
    return `ALTER TABLE ${this.escapeId(tableName)} RENAME COLUMN ${this.escapeId(oldName)} TO ${this.escapeId(newName)};`;
1✔
729
  }
730

731
  generateCreateIndexSql(tableName: string, index: IndexSchema): string {
732
    return this.generateCreateIndex(tableName, index);
13✔
733
  }
734

735
  generateDropIndexSql(tableName: string, indexName: string): string {
736
    return this.generateDropIndex(tableName, indexName);
1✔
737
  }
738

739
  generateAddForeignKeySql(tableName: string, foreignKey: TableForeignKeyDefinition): string {
740
    const fkCols = foreignKey.columns.map((c) => this.escapeId(c)).join(', ');
1✔
741
    const refCols = foreignKey.referencesColumns.map((c) => this.escapeId(c)).join(', ');
1✔
742
    const constraintName = foreignKey.name
1!
743
      ? this.escapeId(foreignKey.name)
744
      : this.escapeId(`fk_${tableName}_${foreignKey.columns.join('_')}`);
745

746
    if (!this.features.foreignKeyAlter) {
1!
747
      throw new Error(`Dialect ${this.dialect} does not support adding foreign keys to existing tables`);
×
748
    }
749

750
    return (
1✔
751
      `ALTER TABLE ${this.escapeId(tableName)} ADD CONSTRAINT ${constraintName} ` +
752
      `FOREIGN KEY (${fkCols}) REFERENCES ${this.escapeId(foreignKey.referencesTable)} (${refCols}) ` +
753
      `ON DELETE ${foreignKey.onDelete ?? this.defaultForeignKeyAction} ON UPDATE ${foreignKey.onUpdate ?? this.defaultForeignKeyAction};`
2!
754
    );
755
  }
756

757
  generateDropForeignKeySql(tableName: string, constraintName: string): string {
758
    return `ALTER TABLE ${this.escapeId(tableName)} ${this.dialect.dropForeignKeySyntax} ${this.escapeId(constraintName)};`;
1✔
759
  }
760

761
  private tableDefinitionToNode(def: TableDefinition): TableNode {
762
    const columns = new Map<string, ColumnNode>();
34✔
763
    const pkNodes: ColumnNode[] = [];
34✔
764

765
    const table: TableNode = {
34✔
766
      name: def.name,
767
      columns,
768
      primaryKey: [], // placeholder
769
      indexes: [],
770
      schema: { tables: new Map(), relationships: [], indexes: [] },
771
      incomingRelations: [],
772
      outgoingRelations: [],
773
      comment: def.comment,
774
    };
775

776
    for (const colDef of def.columns) {
34✔
777
      const node = this.fullColumnDefinitionToNode(colDef, def.name);
118✔
778
      (node as { table: TableNode }).table = table;
118✔
779
      columns.set(node.name, node);
118✔
780
      if (node.isPrimaryKey) {
118✔
781
        pkNodes.push(node);
38✔
782
      }
783
    }
784

785
    const finalPrimaryKey = def.primaryKey
34!
786
      ? def.primaryKey.map((name) => columns.get(name)).filter((c): c is ColumnNode => c !== undefined)
×
787
      : pkNodes;
788

789
    (table as { primaryKey: ColumnNode[] }).primaryKey = finalPrimaryKey;
34✔
790

791
    for (const idxDef of def.indexes) {
34✔
792
      const indexNode: IndexNode = {
×
793
        name: idxDef.name,
794
        table,
795
        columns: idxDef.columns.map((name) => columns.get(name)).filter((c): c is ColumnNode => c !== undefined),
×
796
        unique: idxDef.unique,
797
      };
798
      table.indexes.push(indexNode);
×
799
    }
800

801
    for (const fkDef of def.foreignKeys) {
34✔
802
      const relNode: RelationshipNode = {
20✔
803
        name: fkDef.name ?? `fk_${def.name}_${fkDef.columns.join('_')}`,
40✔
804
        type: 'ManyToOne', // Builder default
805
        from: {
806
          table,
807
          columns: fkDef.columns.map((name) => columns.get(name)).filter((c): c is ColumnNode => c !== undefined),
20✔
808
        },
809
        to: {
810
          table: { name: fkDef.referencesTable } as TableNode,
811
          columns: fkDef.referencesColumns.map((name) => ({ name }) as ColumnNode),
20✔
812
        },
813
        onDelete: fkDef.onDelete,
814
        onUpdate: fkDef.onUpdate,
815
      };
816
      table.outgoingRelations.push(relNode);
20✔
817
    }
818

819
    return table;
34✔
820
  }
821

822
  private fullColumnDefinitionToNode(col: FullColumnDefinition, tableName: string): ColumnNode {
823
    return {
120✔
824
      name: col.name,
825
      type: col.type,
826
      nullable: col.nullable,
827
      defaultValue: col.defaultValue,
828
      isPrimaryKey: col.primaryKey,
829
      isAutoIncrement: col.autoIncrement,
830
      isUnique: col.unique,
831
      comment: col.comment,
832
      table: { name: tableName } as TableNode,
833
      referencedBy: [],
834
      references: col.foreignKey
120✔
835
        ? {
836
            name: `fk_${tableName}_${col.name}`,
837
            type: 'ManyToOne',
838
            from: { table: { name: tableName } as TableNode, columns: [] },
839
            to: {
840
              table: { name: col.foreignKey.table } as TableNode,
841
              columns: col.foreignKey.columns.map((name) => ({ name }) as ColumnNode),
20✔
842
            },
843
            onDelete: col.foreignKey.onDelete,
844
            onUpdate: col.foreignKey.onUpdate,
845
          }
846
        : undefined,
847
    };
848
  }
849
}
850

851
import { MongoSchemaGenerator } from './generator/mongoSchemaGenerator.js';
852

853
export { MongoSchemaGenerator };
854

855
/**
856
 * Factory function to create a SchemaGenerator for a specific dialect.
857
 * Returns undefined for unsupported dialects.
858
 */
859
export function createSchemaGenerator(
860
  dialect: AbstractDialect,
861
  namingStrategy?: NamingStrategy,
862
  defaultForeignKeyAction?: ForeignKeyAction,
863
): SchemaGenerator | undefined {
864
  if (dialect.dialectName === 'mongodb') {
185✔
865
    return new MongoSchemaGenerator(namingStrategy, defaultForeignKeyAction);
5✔
866
  }
867
  if (!(dialect instanceof AbstractSqlDialect)) {
180✔
868
    return undefined;
1✔
869
  }
870
  return new SqlSchemaGenerator(dialect, defaultForeignKeyAction);
179✔
871
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc