uql-orm 0.6.0 → 0.6.1

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.
@@ -5418,3 +5418,1698 @@ const hooks = {
5418
5418
 
5419
5419
  export { BaseEntity, _Company as Company, _InventoryAdjustment as InventoryAdjustment, _Item as Item, _ItemAdjustment as ItemAdjustment, _ItemTag as ItemTag, _LedgerAccount as LedgerAccount, _MeasureUnit as MeasureUnit, _MeasureUnitCategory as MeasureUnitCategory, _Profile as Profile, _Storehouse as Storehouse, _Tag as Tag, _Tax as Tax, _TaxCategory as TaxCategory, _User as User, _UserWithNonUpdatableId as UserWithNonUpdatableId, _VectorItem as VectorItem, clearTables, createSpec, createTables, dropTables };
5420
5420
  //# sourceMappingURL=uql-browser.min.js.map
5421
+ ms);
5422
+ return this.mapIndexesResult(querier, tableName, results);
5423
+ }
5424
+ async getForeignKeys(querier, tableName) {
5425
+ const sql = this.getForeignKeysQuery(tableName);
5426
+ const params = this.getForeignKeysParams(tableName);
5427
+ const results = await querier.all(sql, params);
5428
+ return this.mapForeignKeysResult(querier, tableName, results);
5429
+ }
5430
+ async getPrimaryKey(querier, tableName) {
5431
+ const sql = this.getPrimaryKeyQuery(tableName);
5432
+ const params = this.getPrimaryKeyParams(tableName);
5433
+ const results = await querier.all(sql, params);
5434
+ return this.mapPrimaryKeyResult(results);
5435
+ }
5436
+ // ============================================================================
5437
+ // Parameter Methods (can be overridden by dialects)
5438
+ // ============================================================================
5439
+ tableExistsParams(tableName) {
5440
+ return [
5441
+ tableName
5442
+ ];
5443
+ }
5444
+ getColumnsParams(tableName) {
5445
+ return [
5446
+ tableName
5447
+ ];
5448
+ }
5449
+ getIndexesParams(tableName) {
5450
+ return [
5451
+ tableName
5452
+ ];
5453
+ }
5454
+ getForeignKeysParams(tableName) {
5455
+ return [
5456
+ tableName
5457
+ ];
5458
+ }
5459
+ getPrimaryKeyParams(tableName) {
5460
+ return [
5461
+ tableName
5462
+ ];
5463
+ }
5464
+ // ============================================================================
5465
+ // Shared Utilities
5466
+ // ============================================================================
5467
+ /**
5468
+ * Normalize referential action string to standard type.
5469
+ */ normalizeReferentialAction(action) {
5470
+ switch(action.toUpperCase()){
5471
+ case 'CASCADE':
5472
+ return 'CASCADE';
5473
+ case 'SET NULL':
5474
+ return 'SET NULL';
5475
+ case 'RESTRICT':
5476
+ return 'RESTRICT';
5477
+ case 'NO ACTION':
5478
+ return 'NO ACTION';
5479
+ default:
5480
+ return undefined;
5481
+ }
5482
+ }
5483
+ /**
5484
+ * Convert bigint/null values to number safely.
5485
+ */ toNumber(value) {
5486
+ if (value == null || value === '') {
5487
+ return undefined;
5488
+ }
5489
+ return Number(value);
5490
+ }
5491
+ }
5492
+
5493
+ /**
5494
+ * MongoDB schema introspector.
5495
+ * MongoDB doesn't have a fixed schema, so this primarily focuses on collections and indexes.
5496
+ */ class MongoSchemaIntrospector {
5497
+ constructor(pool){
5498
+ this.pool = pool;
5499
+ }
5500
+ async introspect() {
5501
+ const tableNames = await this.getTableNames();
5502
+ const ast = new SchemaAST();
5503
+ for (const name of tableNames){
5504
+ const schema = await this.getTableSchema(name);
5505
+ if (schema) {
5506
+ const columns = new Map();
5507
+ const table = {
5508
+ name,
5509
+ columns,
5510
+ primaryKey: [],
5511
+ indexes: [],
5512
+ schema: ast,
5513
+ incomingRelations: [],
5514
+ outgoingRelations: []
5515
+ };
5516
+ if (schema.indexes) {
5517
+ for (const idx of schema.indexes){
5518
+ const indexColumns = [];
5519
+ for (const colName of idx.columns){
5520
+ let column = columns.get(colName);
5521
+ if (!column) {
5522
+ column = {
5523
+ name: colName,
5524
+ type: {
5525
+ category: 'string'
5526
+ },
5527
+ nullable: true,
5528
+ isPrimaryKey: false,
5529
+ isAutoIncrement: false,
5530
+ isUnique: false,
5531
+ table,
5532
+ referencedBy: []
5533
+ };
5534
+ columns.set(colName, column);
5535
+ }
5536
+ indexColumns.push(column);
5537
+ }
5538
+ table.indexes.push({
5539
+ name: idx.name,
5540
+ table,
5541
+ columns: indexColumns,
5542
+ unique: idx.unique
5543
+ });
5544
+ }
5545
+ }
5546
+ ast.addTable(table);
5547
+ }
5548
+ }
5549
+ return ast;
5550
+ }
5551
+ async getTableSchema(tableName) {
5552
+ const querier = await this.pool.getQuerier();
5553
+ try {
5554
+ const { db } = querier;
5555
+ const collections = await db.listCollections({
5556
+ name: tableName
5557
+ }).toArray();
5558
+ if (collections.length === 0) {
5559
+ return undefined;
5560
+ }
5561
+ // MongoDB doesn't have a fixed schema, but we can look at the indexes
5562
+ const indexes = await db.collection(tableName).indexes();
5563
+ return {
5564
+ name: tableName,
5565
+ columns: [],
5566
+ indexes: indexes.map((idx)=>({
5567
+ name: idx.name ?? Object.keys(idx.key).join('_'),
5568
+ columns: Object.keys(idx.key),
5569
+ unique: !!idx.unique
5570
+ }))
5571
+ };
5572
+ } finally{
5573
+ await querier.release();
5574
+ }
5575
+ }
5576
+ async getTableNames() {
5577
+ const querier = await this.pool.getQuerier();
5578
+ try {
5579
+ const { db } = querier;
5580
+ const collections = await db.listCollections().toArray();
5581
+ return collections.map((c)=>c.name);
5582
+ } finally{
5583
+ await querier.release();
5584
+ }
5585
+ }
5586
+ async tableExists(tableName) {
5587
+ const names = await this.getTableNames();
5588
+ return names.includes(tableName);
5589
+ }
5590
+ }
5591
+
5592
+ /**
5593
+ * MySQL/MariaDB schema introspector.
5594
+ * Works with both MySQL and MariaDB as they share the same information_schema structure.
5595
+ */ class MysqlSchemaIntrospector extends AbstractSqlSchemaIntrospector {
5596
+ constructor(pool){
5597
+ super('mysql');
5598
+ this.pool = pool;
5599
+ }
5600
+ // ============================================================================
5601
+ // SQL Queries (dialect-specific)
5602
+ // ============================================================================
5603
+ getTableNamesQuery() {
5604
+ return /*sql*/ `
5605
+ SELECT TABLE_NAME as table_name
5606
+ FROM information_schema.TABLES
5607
+ WHERE TABLE_SCHEMA = DATABASE()
5608
+ AND TABLE_TYPE = 'BASE TABLE'
5609
+ ORDER BY TABLE_NAME
5610
+ `;
5611
+ }
5612
+ tableExistsQuery() {
5613
+ return /*sql*/ `
5614
+ SELECT COUNT(*) as count
5615
+ FROM information_schema.TABLES
5616
+ WHERE TABLE_SCHEMA = DATABASE()
5617
+ AND TABLE_NAME = ?
5618
+ `;
5619
+ }
5620
+ parseTableExistsResult(results) {
5621
+ const row = results[0];
5622
+ if (row?.count !== undefined) {
5623
+ return (this.toNumber(row.count) ?? 0) > 0;
5624
+ }
5625
+ return false;
5626
+ }
5627
+ getColumnsQuery(_tableName) {
5628
+ return /*sql*/ `
5629
+ SELECT
5630
+ COLUMN_NAME as column_name,
5631
+ DATA_TYPE as data_type,
5632
+ COLUMN_TYPE as column_type,
5633
+ IS_NULLABLE as is_nullable,
5634
+ COLUMN_DEFAULT as column_default,
5635
+ CHARACTER_MAXIMUM_LENGTH as character_maximum_length,
5636
+ NUMERIC_PRECISION as numeric_precision,
5637
+ NUMERIC_SCALE as numeric_scale,
5638
+ COLUMN_KEY as column_key,
5639
+ EXTRA as extra,
5640
+ COLUMN_COMMENT as column_comment
5641
+ FROM information_schema.COLUMNS
5642
+ WHERE TABLE_SCHEMA = DATABASE()
5643
+ AND TABLE_NAME = ?
5644
+ ORDER BY ORDINAL_POSITION
5645
+ `;
5646
+ }
5647
+ getIndexesQuery(_tableName) {
5648
+ return /*sql*/ `
5649
+ SELECT
5650
+ INDEX_NAME as index_name,
5651
+ GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX) as columns,
5652
+ NOT NON_UNIQUE as is_unique
5653
+ FROM information_schema.STATISTICS
5654
+ WHERE TABLE_SCHEMA = DATABASE()
5655
+ AND TABLE_NAME = ?
5656
+ AND INDEX_NAME != 'PRIMARY'
5657
+ GROUP BY INDEX_NAME, NON_UNIQUE
5658
+ ORDER BY INDEX_NAME
5659
+ `;
5660
+ }
5661
+ getForeignKeysQuery(_tableName) {
5662
+ return /*sql*/ `
5663
+ SELECT
5664
+ kcu.CONSTRAINT_NAME as constraint_name,
5665
+ GROUP_CONCAT(kcu.COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as columns,
5666
+ kcu.REFERENCED_TABLE_NAME as referenced_table,
5667
+ GROUP_CONCAT(kcu.REFERENCED_COLUMN_NAME ORDER BY kcu.ORDINAL_POSITION) as referenced_columns,
5668
+ rc.DELETE_RULE as delete_rule,
5669
+ rc.UPDATE_RULE as update_rule
5670
+ FROM information_schema.KEY_COLUMN_USAGE kcu
5671
+ JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
5672
+ ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
5673
+ AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA
5674
+ WHERE kcu.TABLE_SCHEMA = DATABASE()
5675
+ AND kcu.TABLE_NAME = ?
5676
+ AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
5677
+ GROUP BY kcu.CONSTRAINT_NAME, kcu.REFERENCED_TABLE_NAME, rc.DELETE_RULE, rc.UPDATE_RULE
5678
+ ORDER BY kcu.CONSTRAINT_NAME
5679
+ `;
5680
+ }
5681
+ getPrimaryKeyQuery(_tableName) {
5682
+ return /*sql*/ `
5683
+ SELECT COLUMN_NAME as column_name
5684
+ FROM information_schema.KEY_COLUMN_USAGE
5685
+ WHERE TABLE_SCHEMA = DATABASE()
5686
+ AND TABLE_NAME = ?
5687
+ AND CONSTRAINT_NAME = 'PRIMARY'
5688
+ ORDER BY ORDINAL_POSITION
5689
+ `;
5690
+ }
5691
+ // ============================================================================
5692
+ // Internal Types
5693
+ // ============================================================================
5694
+ mapTableNameRow(row) {
5695
+ return row.table_name;
5696
+ }
5697
+ async mapColumnsResult(_querier, _tableName, results) {
5698
+ return results.map((row)=>({
5699
+ name: row.column_name,
5700
+ type: (row.column_type || '').toUpperCase(),
5701
+ nullable: row.is_nullable === 'YES',
5702
+ defaultValue: this.parseDefaultValue(row.column_default),
5703
+ isPrimaryKey: row.column_key === 'PRI',
5704
+ isAutoIncrement: (row.extra || '').toLowerCase().includes('auto_increment'),
5705
+ isUnique: row.column_key === 'UNI',
5706
+ length: this.toNumber(row.character_maximum_length),
5707
+ precision: this.toNumber(row.numeric_precision),
5708
+ scale: this.toNumber(row.numeric_scale),
5709
+ comment: row.column_comment || undefined
5710
+ }));
5711
+ }
5712
+ async mapIndexesResult(_querier, _tableName, results) {
5713
+ return results.map((row)=>({
5714
+ name: row.index_name,
5715
+ columns: (row.columns || '').split(','),
5716
+ unique: Boolean(row.is_unique)
5717
+ }));
5718
+ }
5719
+ async mapForeignKeysResult(_querier, _tableName, results) {
5720
+ return results.map((row)=>({
5721
+ name: row.constraint_name,
5722
+ columns: (row.columns || '').split(','),
5723
+ referencedTable: row.referenced_table,
5724
+ referencedColumns: (row.referenced_columns || '').split(','),
5725
+ onDelete: this.normalizeReferentialAction(row.delete_rule),
5726
+ onUpdate: this.normalizeReferentialAction(row.update_rule)
5727
+ }));
5728
+ }
5729
+ mapPrimaryKeyResult(results) {
5730
+ if (results.length === 0) {
5731
+ return undefined;
5732
+ }
5733
+ return results.map((r)=>r.column_name);
5734
+ }
5735
+ parseDefaultValue(defaultValue) {
5736
+ if (defaultValue === null) {
5737
+ return undefined;
5738
+ }
5739
+ const normalized = defaultValue.toUpperCase();
5740
+ if (normalized === 'NULL') {
5741
+ return null;
5742
+ }
5743
+ if (normalized === 'CURRENT_TIMESTAMP' || normalized === 'CURRENT_TIMESTAMP()') {
5744
+ return 'CURRENT_TIMESTAMP';
5745
+ }
5746
+ if (/^-?\d+$/.test(defaultValue)) {
5747
+ return Number.parseInt(defaultValue, 10);
5748
+ }
5749
+ if (/^-?\d+\.\d+$/.test(defaultValue)) {
5750
+ return Number.parseFloat(defaultValue);
5751
+ }
5752
+ if (defaultValue?.startsWith("'") && defaultValue?.endsWith("'")) {
5753
+ return defaultValue.slice(1, -1);
5754
+ }
5755
+ return defaultValue;
5756
+ }
5757
+ }
5758
+ /**
5759
+ * Alias for MysqlSchemaIntrospector.
5760
+ * MariaDB uses the same information_schema structure as MySQL.
5761
+ */ const MariadbSchemaIntrospector = MysqlSchemaIntrospector;
5762
+
5763
+ /**
5764
+ * PostgreSQL schema introspector
5765
+ */ class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
5766
+ constructor(pool){
5767
+ super('postgres');
5768
+ this.pool = pool;
5769
+ }
5770
+ // ============================================================================
5771
+ // SQL Queries (dialect-specific)
5772
+ // ============================================================================
5773
+ getTableNamesQuery() {
5774
+ return /*sql*/ `
5775
+ SELECT table_name
5776
+ FROM information_schema.tables
5777
+ WHERE table_schema = 'public'
5778
+ AND table_type = 'BASE TABLE'
5779
+ ORDER BY table_name
5780
+ `;
5781
+ }
5782
+ tableExistsQuery() {
5783
+ return /*sql*/ `
5784
+ SELECT EXISTS (
5785
+ SELECT FROM information_schema.tables
5786
+ WHERE table_schema = 'public'
5787
+ AND table_name = $1
5788
+ ) AS exists
5789
+ `;
5790
+ }
5791
+ parseTableExistsResult(results) {
5792
+ return results[0]?.['exists'] ?? false;
5793
+ }
5794
+ getColumnsQuery(_tableName) {
5795
+ return /*sql*/ `
5796
+ SELECT
5797
+ c.column_name,
5798
+ c.data_type,
5799
+ c.udt_name,
5800
+ c.is_nullable,
5801
+ c.column_default,
5802
+ c.character_maximum_length,
5803
+ c.numeric_precision,
5804
+ c.numeric_scale,
5805
+ c.is_identity,
5806
+ c.identity_generation,
5807
+ COALESCE(
5808
+ (SELECT TRUE FROM information_schema.table_constraints tc
5809
+ JOIN information_schema.key_column_usage kcu
5810
+ ON tc.constraint_name = kcu.constraint_name
5811
+ WHERE tc.table_name = c.table_name
5812
+ AND tc.constraint_type = 'PRIMARY KEY'
5813
+ AND kcu.column_name = c.column_name
5814
+ LIMIT 1),
5815
+ FALSE
5816
+ ) AS is_primary_key,
5817
+ COALESCE(
5818
+ (SELECT TRUE FROM information_schema.table_constraints tc
5819
+ JOIN information_schema.key_column_usage kcu
5820
+ ON tc.constraint_name = kcu.constraint_name
5821
+ WHERE tc.table_name = c.table_name
5822
+ AND tc.constraint_type = 'UNIQUE'
5823
+ AND kcu.column_name = c.column_name
5824
+ LIMIT 1),
5825
+ FALSE
5826
+ ) AS is_unique,
5827
+ pg_catalog.col_description(
5828
+ (SELECT oid FROM pg_catalog.pg_class WHERE relname = c.table_name),
5829
+ c.ordinal_position
5830
+ ) AS column_comment
5831
+ FROM information_schema.columns c
5832
+ WHERE c.table_schema = 'public'
5833
+ AND c.table_name = $1
5834
+ ORDER BY c.ordinal_position
5835
+ `;
5836
+ }
5837
+ getIndexesQuery(_tableName) {
5838
+ return /*sql*/ `
5839
+ SELECT
5840
+ i.relname AS index_name,
5841
+ array_to_json(array_agg(a.attname ORDER BY k.n)) AS columns,
5842
+ ix.indisunique AS is_unique
5843
+ FROM pg_class t
5844
+ JOIN pg_index ix ON t.oid = ix.indrelid
5845
+ JOIN pg_class i ON i.oid = ix.indexrelid
5846
+ JOIN pg_namespace n ON n.oid = t.relnamespace
5847
+ CROSS JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n)
5848
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
5849
+ WHERE t.relname = $1
5850
+ AND n.nspname = 'public'
5851
+ AND NOT ix.indisprimary
5852
+ GROUP BY i.relname, ix.indisunique
5853
+ ORDER BY i.relname
5854
+ `;
5855
+ }
5856
+ getForeignKeysQuery(_tableName) {
5857
+ return /*sql*/ `
5858
+ SELECT
5859
+ tc.constraint_name,
5860
+ array_to_json(array_agg(kcu.column_name ORDER BY kcu.ordinal_position)) AS columns,
5861
+ ccu.table_name AS referenced_table,
5862
+ array_to_json(array_agg(ccu.column_name ORDER BY kcu.ordinal_position)) AS referenced_columns,
5863
+ rc.delete_rule,
5864
+ rc.update_rule
5865
+ FROM information_schema.table_constraints tc
5866
+ JOIN information_schema.key_column_usage kcu
5867
+ ON tc.constraint_name = kcu.constraint_name
5868
+ AND tc.table_schema = kcu.table_schema
5869
+ JOIN information_schema.constraint_column_usage ccu
5870
+ ON ccu.constraint_name = tc.constraint_name
5871
+ AND ccu.table_schema = tc.table_schema
5872
+ JOIN information_schema.referential_constraints rc
5873
+ ON rc.constraint_name = tc.constraint_name
5874
+ AND rc.constraint_schema = tc.table_schema
5875
+ WHERE tc.constraint_type = 'FOREIGN KEY'
5876
+ AND tc.table_name = $1
5877
+ AND tc.table_schema = 'public'
5878
+ GROUP BY tc.constraint_name, ccu.table_name, rc.delete_rule, rc.update_rule
5879
+ ORDER BY tc.constraint_name
5880
+ `;
5881
+ }
5882
+ getPrimaryKeyQuery(_tableName) {
5883
+ return /*sql*/ `
5884
+ SELECT kcu.column_name
5885
+ FROM information_schema.table_constraints tc
5886
+ JOIN information_schema.key_column_usage kcu
5887
+ ON tc.constraint_name = kcu.constraint_name
5888
+ AND tc.table_schema = kcu.table_schema
5889
+ WHERE tc.constraint_type = 'PRIMARY KEY'
5890
+ AND tc.table_name = $1
5891
+ AND tc.table_schema = 'public'
5892
+ ORDER BY kcu.ordinal_position
5893
+ `;
5894
+ }
5895
+ // ============================================================================
5896
+ // Internal Types
5897
+ // ============================================================================
5898
+ mapTableNameRow(row) {
5899
+ return row.table_name;
5900
+ }
5901
+ async mapColumnsResult(_querier, _tableName, results) {
5902
+ return results.map((row)=>({
5903
+ name: row.column_name,
5904
+ type: this.normalizeType(row.data_type, row.udt_name),
5905
+ nullable: row.is_nullable === 'YES',
5906
+ defaultValue: this.parseDefaultValue(row.column_default),
5907
+ isPrimaryKey: row.is_primary_key,
5908
+ isAutoIncrement: this.isAutoIncrement(row.column_default, row.is_identity),
5909
+ isUnique: row.is_unique,
5910
+ length: row.character_maximum_length ?? undefined,
5911
+ precision: row.numeric_precision ?? undefined,
5912
+ scale: row.numeric_scale ?? undefined,
5913
+ comment: row.column_comment ?? undefined
5914
+ }));
5915
+ }
5916
+ async mapIndexesResult(_querier, _tableName, results) {
5917
+ return results.map((row)=>({
5918
+ name: row.index_name,
5919
+ columns: row.columns,
5920
+ unique: row.is_unique
5921
+ }));
5922
+ }
5923
+ async mapForeignKeysResult(_querier, _tableName, results) {
5924
+ return results.map((row)=>({
5925
+ name: row.constraint_name,
5926
+ columns: row.columns,
5927
+ referencedTable: row.referenced_table,
5928
+ referencedColumns: row.referenced_columns,
5929
+ onDelete: this.normalizeReferentialAction(row.delete_rule),
5930
+ onUpdate: this.normalizeReferentialAction(row.update_rule)
5931
+ }));
5932
+ }
5933
+ mapPrimaryKeyResult(results) {
5934
+ if (results.length === 0) {
5935
+ return undefined;
5936
+ }
5937
+ return results.map((r)=>r.column_name);
5938
+ }
5939
+ // ============================================================================
5940
+ // PostgreSQL-specific helpers
5941
+ // ============================================================================
5942
+ normalizeType(dataType, udtName) {
5943
+ // Handle user-defined types and arrays
5944
+ if (dataType === 'USER-DEFINED') {
5945
+ return udtName.toUpperCase();
5946
+ }
5947
+ if (dataType === 'ARRAY') {
5948
+ return `${udtName.replace(/^_/, '').toUpperCase()}[]`;
5949
+ }
5950
+ return dataType.toUpperCase();
5951
+ }
5952
+ parseDefaultValue(defaultValue) {
5953
+ if (!defaultValue) {
5954
+ return undefined;
5955
+ }
5956
+ // Remove type casting (e.g., ::text, ::character varying, ::text[])
5957
+ const cleaned = defaultValue.replace(/::[a-z_]+(\s+[a-z_]+)?(\[\])?/gi, '').trim();
5958
+ if (cleaned.startsWith("'") && cleaned.endsWith("'")) {
5959
+ return cleaned.slice(1, -1);
5960
+ }
5961
+ if (cleaned === 'true' || cleaned === 'false') {
5962
+ return cleaned === 'true';
5963
+ }
5964
+ if (cleaned === 'NULL') {
5965
+ return null;
5966
+ }
5967
+ if (/^-?\d+$/.test(cleaned)) {
5968
+ return Number.parseInt(cleaned, 10);
5969
+ }
5970
+ if (/^-?\d+\.\d+$/.test(cleaned)) {
5971
+ return Number.parseFloat(cleaned);
5972
+ }
5973
+ // Return cleaned value for functions like CURRENT_TIMESTAMP, nextval(), etc.
5974
+ return cleaned;
5975
+ }
5976
+ isAutoIncrement(columnDefault, isIdentity) {
5977
+ // PostgreSQL identity columns (GENERATED ... AS IDENTITY)
5978
+ if (isIdentity === 'YES') {
5979
+ return true;
5980
+ }
5981
+ // Serial/bigserial columns use nextval()
5982
+ return columnDefault?.includes('nextval(') ?? false;
5983
+ }
5984
+ }
5985
+
5986
+ /**
5987
+ * SQLite schema introspector
5988
+ */ class SqliteSchemaIntrospector extends AbstractSqlSchemaIntrospector {
5989
+ constructor(pool){
5990
+ super('sqlite');
5991
+ this.pool = pool;
5992
+ }
5993
+ // ============================================================================
5994
+ // SQL Queries (dialect-specific)
5995
+ // ============================================================================
5996
+ getTableNamesQuery() {
5997
+ return /*sql*/ `
5998
+ SELECT name
5999
+ FROM sqlite_master
6000
+ WHERE type = 'table'
6001
+ AND name NOT LIKE 'sqlite_%'
6002
+ ORDER BY name
6003
+ `;
6004
+ }
6005
+ tableExistsQuery() {
6006
+ return /*sql*/ `
6007
+ SELECT COUNT(*) as count
6008
+ FROM sqlite_master
6009
+ WHERE type = 'table'
6010
+ AND name = ?
6011
+ `;
6012
+ }
6013
+ parseTableExistsResult(results) {
6014
+ const row = results[0];
6015
+ if (row?.count !== undefined) {
6016
+ return (this.toNumber(row.count) ?? 0) > 0;
6017
+ }
6018
+ return false;
6019
+ }
6020
+ // SQLite uses PRAGMA which doesn't use parameterized queries in the same way
6021
+ getColumnsQuery(tableName) {
6022
+ return `PRAGMA table_info(${this.escapeId(tableName)})`;
6023
+ }
6024
+ getIndexesQuery(tableName) {
6025
+ return `PRAGMA index_list(${this.escapeId(tableName)})`;
6026
+ }
6027
+ getForeignKeysQuery(tableName) {
6028
+ return `PRAGMA foreign_key_list(${this.escapeId(tableName)})`;
6029
+ }
6030
+ getPrimaryKeyQuery(tableName) {
6031
+ return `PRAGMA table_info(${this.escapeId(tableName)})`;
6032
+ }
6033
+ getColumnsParams(_tableName) {
6034
+ return [];
6035
+ }
6036
+ getIndexesParams(_tableName) {
6037
+ return [];
6038
+ }
6039
+ getForeignKeysParams(_tableName) {
6040
+ return [];
6041
+ }
6042
+ getPrimaryKeyParams(_tableName) {
6043
+ return [];
6044
+ }
6045
+ // ============================================================================
6046
+ // Row Mapping (dialect-specific)
6047
+ // ============================================================================
6048
+ mapTableNameRow(row) {
6049
+ return row.name;
6050
+ }
6051
+ async mapColumnsResult(querier, tableName, results) {
6052
+ // Get unique columns from indexes
6053
+ const uniqueColumns = await this.getUniqueColumns(querier, tableName);
6054
+ return results.map((row)=>({
6055
+ name: row.name,
6056
+ type: this.normalizeType(row.type),
6057
+ nullable: row.notnull === 0,
6058
+ defaultValue: this.parseDefaultValue(row.dflt_value),
6059
+ isPrimaryKey: row.pk > 0,
6060
+ isAutoIncrement: row.pk > 0 && row.type.toUpperCase() === 'INTEGER',
6061
+ isUnique: uniqueColumns.has(row.name),
6062
+ length: this.extractLength(row.type),
6063
+ precision: undefined,
6064
+ scale: undefined,
6065
+ comment: undefined
6066
+ }));
6067
+ }
6068
+ async mapIndexesResult(querier, _tableName, results) {
6069
+ const indexSchemas = [];
6070
+ for (const index of results){
6071
+ const columns = await querier.all(`PRAGMA index_info(${this.escapeId(index.name)})`);
6072
+ // Include user-created indexes ('c') and multi-column unique constraints ('u')
6073
+ // Skip primary key indexes ('pk') and single-column unique constraints
6074
+ const isUserCreated = index.origin === 'c';
6075
+ const isCompositeUnique = index.origin === 'u' && columns.length > 1;
6076
+ if (isUserCreated || isCompositeUnique) {
6077
+ indexSchemas.push({
6078
+ name: index.name,
6079
+ columns: columns.map((c)=>c.name),
6080
+ unique: Boolean(index.unique)
6081
+ });
6082
+ }
6083
+ }
6084
+ return indexSchemas;
6085
+ }
6086
+ async mapForeignKeysResult(_querier, tableName, results) {
6087
+ // Group by id to handle composite foreign keys
6088
+ const grouped = new Map();
6089
+ for (const row of results){
6090
+ const id = row.id;
6091
+ const existing = grouped.get(id) ?? [];
6092
+ existing.push(row);
6093
+ grouped.set(id, existing);
6094
+ }
6095
+ return Array.from(grouped.entries()).map(([id, rows])=>{
6096
+ const first = rows[0];
6097
+ return {
6098
+ name: `fk_${tableName}_${id}`,
6099
+ columns: rows.map((r)=>r.from),
6100
+ referencedTable: first.table,
6101
+ referencedColumns: rows.map((r)=>r.to),
6102
+ onDelete: this.normalizeReferentialAction(first.on_delete),
6103
+ onUpdate: this.normalizeReferentialAction(first.on_update)
6104
+ };
6105
+ });
6106
+ }
6107
+ mapPrimaryKeyResult(results) {
6108
+ const pkColumns = results.filter((r)=>r.pk > 0).sort((a, b)=>a.pk - b.pk);
6109
+ if (pkColumns.length === 0) {
6110
+ return undefined;
6111
+ }
6112
+ return pkColumns.map((r)=>r.name);
6113
+ }
6114
+ // ============================================================================
6115
+ // SQLite-specific helpers
6116
+ // ============================================================================
6117
+ async getUniqueColumns(querier, tableName) {
6118
+ const results = await querier.all(`PRAGMA index_list(${this.escapeId(tableName)})`);
6119
+ const uniqueColumns = new Set();
6120
+ for (const index of results){
6121
+ if (index.unique) {
6122
+ const indexInfo = await querier.all(`PRAGMA index_info(${this.escapeId(index.name)})`);
6123
+ // Only single-column unique constraints
6124
+ if (indexInfo.length === 1) {
6125
+ uniqueColumns.add(indexInfo[0].name);
6126
+ }
6127
+ }
6128
+ }
6129
+ return uniqueColumns;
6130
+ }
6131
+ normalizeType(type) {
6132
+ // Extract base type without length/precision
6133
+ const match = type.match(/^([A-Za-z]+)/);
6134
+ return match ? match[1].toUpperCase() : type.toUpperCase();
6135
+ }
6136
+ extractLength(type) {
6137
+ const match = type.match(/\((\d+)\)/);
6138
+ return match ? Number.parseInt(match[1], 10) : undefined;
6139
+ }
6140
+ parseDefaultValue(defaultValue) {
6141
+ if (defaultValue === null) {
6142
+ return undefined;
6143
+ }
6144
+ if (defaultValue === 'NULL') {
6145
+ return null;
6146
+ }
6147
+ if (defaultValue === 'CURRENT_TIMESTAMP' || defaultValue === 'CURRENT_DATE' || defaultValue === 'CURRENT_TIME') {
6148
+ return defaultValue;
6149
+ }
6150
+ if (/^'.*'$/.test(defaultValue)) {
6151
+ return defaultValue.slice(1, -1);
6152
+ }
6153
+ if (/^-?\d+$/.test(defaultValue)) {
6154
+ return Number.parseInt(defaultValue, 10);
6155
+ }
6156
+ if (/^-?\d+\.\d+$/.test(defaultValue)) {
6157
+ return Number.parseFloat(defaultValue);
6158
+ }
6159
+ return defaultValue;
6160
+ }
6161
+ }
6162
+
6163
+ /**
6164
+ * Schema AST Module
6165
+ *
6166
+ * Provides a unified graph representation of database schema for:
6167
+ * - Schema diffing and migration generation
6168
+ * - Entity code generation from database
6169
+ * - Drift detection
6170
+ * - Smart relation inference
6171
+ */ // Dialect configuration
6172
+ /**
6173
+ * Introspect the database and build a SchemaAST from it.
6174
+ *
6175
+ * @param introspector - The schema introspector to use
6176
+ * @returns The SchemaAST representing the database schema
6177
+ */ async function introspectSchema(introspector) {
6178
+ return introspector.introspect();
6179
+ }
6180
+
6181
+ /**
6182
+ * Stores migration state in a database table.
6183
+ * Uses the querier's dialect for escaping and placeholders.
6184
+ */ class DatabaseMigrationStorage {
6185
+ constructor(pool, options = {}){
6186
+ this.pool = pool;
6187
+ this.storageInitialized = false;
6188
+ this.tableName = options.tableName ?? 'uql_migrations';
6189
+ }
6190
+ async ensureStorage() {
6191
+ if (this.storageInitialized) {
6192
+ return;
6193
+ }
6194
+ const querier = await this.pool.getQuerier();
6195
+ if (!isSqlQuerier(querier)) {
6196
+ await querier.release();
6197
+ throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
6198
+ }
6199
+ try {
6200
+ await this.createTableIfNotExists(querier);
6201
+ this.storageInitialized = true;
6202
+ } finally{
6203
+ await querier.release();
6204
+ }
6205
+ }
6206
+ async createTableIfNotExists(querier) {
6207
+ const { dialect } = querier;
6208
+ const sql = `
6209
+ CREATE TABLE IF NOT EXISTS ${dialect.escapeId(this.tableName)} (
6210
+ ${dialect.escapeId('name')} VARCHAR(255) PRIMARY KEY,
6211
+ ${dialect.escapeId('executed_at')} TIMESTAMP DEFAULT CURRENT_TIMESTAMP
6212
+ )
6213
+ `;
6214
+ await querier.run(sql);
6215
+ }
6216
+ async executed() {
6217
+ await this.ensureStorage();
6218
+ const querier = await this.pool.getQuerier();
6219
+ if (!isSqlQuerier(querier)) {
6220
+ await querier.release();
6221
+ throw new Error('DatabaseMigrationStorage requires a SQL-based querier');
6222
+ }
6223
+ try {
6224
+ const { dialect } = querier;
6225
+ const sql = `SELECT ${dialect.escapeId('name')} FROM ${dialect.escapeId(this.tableName)} ORDER BY ${dialect.escapeId('name')} ASC`;
6226
+ const results = await querier.all(sql);
6227
+ return results.map((r)=>r.name);
6228
+ } finally{
6229
+ await querier.release();
6230
+ }
6231
+ }
6232
+ /**
6233
+ * Log a migration as executed - uses provided querier (within transaction)
6234
+ */ async logWithQuerier(querier, migrationName) {
6235
+ await this.ensureStorage();
6236
+ const { dialect } = querier;
6237
+ const sql = `INSERT INTO ${dialect.escapeId(this.tableName)} (${dialect.escapeId('name')}) VALUES (${dialect.placeholder(1)})`;
6238
+ await querier.run(sql, [
6239
+ migrationName
6240
+ ]);
6241
+ }
6242
+ /**
6243
+ * Unlog a migration - uses provided querier (within transaction)
6244
+ */ async unlogWithQuerier(querier, migrationName) {
6245
+ await this.ensureStorage();
6246
+ const { dialect } = querier;
6247
+ const sql = `DELETE FROM ${dialect.escapeId(this.tableName)} WHERE ${dialect.escapeId('name')} = ${dialect.placeholder(1)}`;
6248
+ await querier.run(sql, [
6249
+ migrationName
6250
+ ]);
6251
+ }
6252
+ }
6253
+
6254
+ /**
6255
+ * Main class for managing database migrations
6256
+ */ class Migrator {
6257
+ get logger() {
6258
+ return this._logger;
6259
+ }
6260
+ set logger(value) {
6261
+ this._logger = new LoggerWrapper(value);
6262
+ }
6263
+ get entities() {
6264
+ return this._entities ?? getEntities();
6265
+ }
6266
+ constructor(pool, options = {}){
6267
+ this.pool = pool;
6268
+ this.dialect = options.dialect ?? pool.dialect ?? 'postgres';
6269
+ this.storage = options.storage ?? new DatabaseMigrationStorage(pool, {
6270
+ tableName: options.tableName
6271
+ });
6272
+ this.migrationsPath = options.migrationsPath ?? './migrations';
6273
+ this._logger = new LoggerWrapper(options.logger, options.slowQuery);
6274
+ this._entities = options.entities;
6275
+ this.schemaIntrospector = this.createIntrospector();
6276
+ this.schemaGenerator = options.schemaGenerator ?? this.createGenerator(options.namingStrategy);
6277
+ }
6278
+ /**
6279
+ * Set the schema generator for DDL operations
6280
+ */ setSchemaGenerator(generator) {
6281
+ this.schemaGenerator = generator;
6282
+ }
6283
+ createIntrospector() {
6284
+ switch(this.dialect){
6285
+ case 'cockroachdb':
6286
+ case 'postgres':
6287
+ return new PostgresSchemaIntrospector(this.pool);
6288
+ case 'mysql':
6289
+ case 'mariadb':
6290
+ return new MysqlSchemaIntrospector(this.pool);
6291
+ case 'sqlite':
6292
+ return new SqliteSchemaIntrospector(this.pool);
6293
+ case 'mongodb':
6294
+ return new MongoSchemaIntrospector(this.pool);
6295
+ default:
6296
+ return undefined;
6297
+ }
6298
+ }
6299
+ createGenerator(namingStrategy) {
6300
+ return createSchemaGenerator(this.dialect, namingStrategy);
6301
+ }
6302
+ /**
6303
+ * Get the SQL dialect
6304
+ */ getDialect() {
6305
+ return this.dialect;
6306
+ }
6307
+ /**
6308
+ * Get all discovered migrations from the migrations directory
6309
+ */ async getMigrations() {
6310
+ const files = await this.getMigrationFiles();
6311
+ const migrations = [];
6312
+ for (const file of files){
6313
+ const migration = await this.loadMigration(file);
6314
+ if (migration) {
6315
+ migrations.push(migration);
6316
+ }
6317
+ }
6318
+ // Sort by name (which typically includes timestamp)
6319
+ return migrations.sort((a, b)=>a.name.localeCompare(b.name));
6320
+ }
6321
+ /**
6322
+ * Get list of pending migrations (not yet executed)
6323
+ */ async pending() {
6324
+ const [migrations, executed] = await Promise.all([
6325
+ this.getMigrations(),
6326
+ this.storage.executed()
6327
+ ]);
6328
+ const executedSet = new Set(executed);
6329
+ return migrations.filter((m)=>!executedSet.has(m.name));
6330
+ }
6331
+ /**
6332
+ * Get list of executed migrations
6333
+ */ async executed() {
6334
+ return this.storage.executed();
6335
+ }
6336
+ /**
6337
+ * Run all pending migrations
6338
+ */ async up(options = {}) {
6339
+ const pendingMigrations = await this.pending();
6340
+ const results = [];
6341
+ let migrationsToRun = pendingMigrations;
6342
+ if (options.to) {
6343
+ const toIndex = migrationsToRun.findIndex((m)=>m.name === options.to);
6344
+ if (toIndex === -1) {
6345
+ throw new Error(`Migration '${options.to}' not found`);
6346
+ }
6347
+ migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
6348
+ }
6349
+ if (options.step !== undefined) {
6350
+ migrationsToRun = migrationsToRun.slice(0, options.step);
6351
+ }
6352
+ for (const migration of migrationsToRun){
6353
+ const result = await this.runMigration(migration, 'up');
6354
+ results.push(result);
6355
+ if (!result.success) {
6356
+ break; // Stop on first failure
6357
+ }
6358
+ }
6359
+ return results;
6360
+ }
6361
+ /**
6362
+ * Rollback migrations
6363
+ */ async down(options = {}) {
6364
+ const [migrations, executed] = await Promise.all([
6365
+ this.getMigrations(),
6366
+ this.storage.executed()
6367
+ ]);
6368
+ const executedSet = new Set(executed);
6369
+ const executedMigrations = migrations.filter((m)=>executedSet.has(m.name)).reverse(); // Rollback in reverse order
6370
+ const results = [];
6371
+ let migrationsToRun = executedMigrations;
6372
+ if (options.to) {
6373
+ const toIndex = migrationsToRun.findIndex((m)=>m.name === options.to);
6374
+ if (toIndex === -1) {
6375
+ throw new Error(`Migration '${options.to}' not found`);
6376
+ }
6377
+ migrationsToRun = migrationsToRun.slice(0, toIndex + 1);
6378
+ }
6379
+ if (options.step !== undefined) {
6380
+ migrationsToRun = migrationsToRun.slice(0, options.step);
6381
+ }
6382
+ for (const migration of migrationsToRun){
6383
+ const result = await this.runMigration(migration, 'down');
6384
+ results.push(result);
6385
+ if (!result.success) {
6386
+ break; // Stop on first failure
6387
+ }
6388
+ }
6389
+ return results;
6390
+ }
6391
+ /**
6392
+ * Run a single migration within a transaction
6393
+ */ async runMigration(migration, direction) {
6394
+ const startTime = Date.now();
6395
+ const querier = await this.pool.getQuerier();
6396
+ if (!isSqlQuerier(querier)) {
6397
+ await querier.release();
6398
+ throw new Error('Migrator requires a SQL-based querier');
6399
+ }
6400
+ try {
6401
+ this.logger.logMigration(`${direction === 'up' ? 'Running' : 'Reverting'} migration: ${migration.name}`);
6402
+ await querier.beginTransaction();
6403
+ if (direction === 'up') {
6404
+ await migration.up(querier);
6405
+ // Log within the same transaction
6406
+ await this.storage.logWithQuerier(querier, migration.name);
6407
+ } else {
6408
+ await migration.down(querier);
6409
+ // Unlog within the same transaction
6410
+ await this.storage.unlogWithQuerier(querier, migration.name);
6411
+ }
6412
+ await querier.commitTransaction();
6413
+ const duration = Date.now() - startTime;
6414
+ this.logger.logMigration(`Migration ${migration.name} ${direction === 'up' ? 'applied' : 'reverted'} in ${duration}ms`);
6415
+ return {
6416
+ name: migration.name,
6417
+ direction,
6418
+ duration,
6419
+ success: true
6420
+ };
6421
+ } catch (error) {
6422
+ await querier.rollbackTransaction();
6423
+ const duration = Date.now() - startTime;
6424
+ this.logger.logError(`Migration ${migration.name} failed: ${error.message}`, error);
6425
+ return {
6426
+ name: migration.name,
6427
+ direction,
6428
+ duration,
6429
+ success: false,
6430
+ error: error
6431
+ };
6432
+ } finally{
6433
+ await querier.release();
6434
+ }
6435
+ }
6436
+ /**
6437
+ * Generate a new migration file
6438
+ */ async generate(name) {
6439
+ const timestamp = this.getTimestamp();
6440
+ const fileName = `${timestamp}_${this.slugify(name)}.ts`;
6441
+ const filePath = join(this.migrationsPath, fileName);
6442
+ const content = this.generateMigrationContent(name);
6443
+ await mkdir(this.migrationsPath, {
6444
+ recursive: true
6445
+ });
6446
+ await writeFile(filePath, content, 'utf-8');
6447
+ this.logger.logInfo(`Created migration: ${filePath}`);
6448
+ return filePath;
6449
+ }
6450
+ /**
6451
+ * Generate a migration based on entity schema differences
6452
+ */ async generateFromEntities(name) {
6453
+ if (!this.schemaGenerator) {
6454
+ throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
6455
+ }
6456
+ const diffs = await this.getDiffs();
6457
+ const upStatements = [];
6458
+ const downStatements = [];
6459
+ for (const diff of diffs){
6460
+ if (diff.type === 'create') {
6461
+ const entity = this.findEntityForTable(diff.tableName);
6462
+ if (entity) {
6463
+ upStatements.push(this.schemaGenerator.generateCreateTable(entity));
6464
+ downStatements.push(this.schemaGenerator.generateDropTable(entity));
6465
+ }
6466
+ } else if (diff.type === 'alter') {
6467
+ const alterStatements = this.schemaGenerator.generateAlterTable(diff);
6468
+ upStatements.push(...alterStatements);
6469
+ const alterDownStatements = this.schemaGenerator.generateAlterTableDown(diff);
6470
+ downStatements.push(...alterDownStatements);
6471
+ }
6472
+ }
6473
+ if (upStatements.length === 0) {
6474
+ this.logger.logInfo('No schema changes detected.');
6475
+ return '';
6476
+ }
6477
+ const timestamp = this.getTimestamp();
6478
+ const fileName = `${timestamp}_${this.slugify(name)}.ts`;
6479
+ const filePath = join(this.migrationsPath, fileName);
6480
+ const content = this.generateMigrationContentWithStatements(name, upStatements, downStatements.reverse());
6481
+ await mkdir(this.migrationsPath, {
6482
+ recursive: true
6483
+ });
6484
+ await writeFile(filePath, content, 'utf-8');
6485
+ this.logger.logInfo(`Created migration from entities: ${filePath}`);
6486
+ return filePath;
6487
+ }
6488
+ /**
6489
+ * Get all schema differences between entities and database
6490
+ */ async getDiffs() {
6491
+ if (!this.schemaGenerator || !this.schemaIntrospector) {
6492
+ throw new Error('Schema generator and introspector must be set');
6493
+ }
6494
+ const ast = await introspectSchema(this.schemaIntrospector);
6495
+ const diffs = [];
6496
+ for (const entity of this.entities){
6497
+ const meta = getMeta$1(entity);
6498
+ const tableName = this.schemaGenerator.resolveTableName(entity, meta);
6499
+ const currentTable = ast.getTable(tableName);
6500
+ const diff = this.schemaGenerator.diffSchema(entity, currentTable);
6501
+ if (diff) {
6502
+ diffs.push(diff);
6503
+ }
6504
+ }
6505
+ return diffs;
6506
+ }
6507
+ findEntityForTable(tableName) {
6508
+ for (const entity of this.entities){
6509
+ const meta = getMeta$1(entity);
6510
+ const name = this.schemaGenerator.resolveTableName(entity, meta);
6511
+ if (name === tableName) {
6512
+ return entity;
6513
+ }
6514
+ }
6515
+ return undefined;
6516
+ }
6517
+ /**
6518
+ * Sync schema directly (for development only - not for production!)
6519
+ */ async sync(options = {}) {
6520
+ if (options.force) {
6521
+ return this.syncForce();
6522
+ }
6523
+ return this.autoSync({
6524
+ safe: true
6525
+ });
6526
+ }
6527
+ /**
6528
+ * Drops and recreates all tables (Development only!)
6529
+ */ async syncForce() {
6530
+ if (!this.schemaGenerator) {
6531
+ throw new Error('Schema generator not set. Call setSchemaGenerator() first.');
6532
+ }
6533
+ const querier = await this.pool.getQuerier();
6534
+ if (!isSqlQuerier(querier)) {
6535
+ await querier.release();
6536
+ throw new Error('Migrator requires a SQL-based querier');
6537
+ }
6538
+ try {
6539
+ await querier.beginTransaction();
6540
+ // Drop all tables first (in reverse order for foreign keys)
6541
+ for (const entity of [
6542
+ ...this.entities
6543
+ ].reverse()){
6544
+ const dropSql = this.schemaGenerator.generateDropTable(entity);
6545
+ this.logger.logSchema(`Executing: ${dropSql}`);
6546
+ await querier.run(dropSql);
6547
+ }
6548
+ // Create all tables
6549
+ for (const entity of this.entities){
6550
+ const createSql = this.schemaGenerator.generateCreateTable(entity);
6551
+ this.logger.logSchema(`Executing: ${createSql}`);
6552
+ await querier.run(createSql);
6553
+ }
6554
+ await querier.commitTransaction();
6555
+ this.logger.logSchema('Schema sync (force) completed');
6556
+ } catch (error) {
6557
+ await querier.rollbackTransaction();
6558
+ throw error;
6559
+ } finally{
6560
+ await querier.release();
6561
+ }
6562
+ }
6563
+ /**
6564
+ * Safely synchronizes the schema by only adding missing tables and columns.
6565
+ */ async autoSync(options = {}) {
6566
+ if (!this.schemaGenerator || !this.schemaIntrospector) {
6567
+ throw new Error('Schema generator and introspector must be set');
6568
+ }
6569
+ const diffs = await this.getDiffs();
6570
+ const statements = [];
6571
+ for (const diff of diffs){
6572
+ if (diff.type === 'create') {
6573
+ const entity = this.findEntityForTable(diff.tableName);
6574
+ if (entity) {
6575
+ statements.push(this.schemaGenerator.generateCreateTable(entity));
6576
+ }
6577
+ } else if (diff.type === 'alter') {
6578
+ const filteredDiff = this.filterDiff(diff, options);
6579
+ const alterStatements = this.schemaGenerator.generateAlterTable(filteredDiff);
6580
+ statements.push(...alterStatements);
6581
+ }
6582
+ }
6583
+ if (statements.length === 0) {
6584
+ if (options.logging) this.logger.logSchema('Schema is already in sync.');
6585
+ return;
6586
+ }
6587
+ await this.executeSyncStatements(statements, options);
6588
+ }
6589
+ filterDiff(diff, options) {
6590
+ const filteredDiff = {
6591
+ ...diff
6592
+ };
6593
+ if (options.safe !== false) {
6594
+ // In safe mode, we only allow additions (creating tables/columns)
6595
+ // We block drops and alterations to prevent accidental data loss
6596
+ if (filteredDiff.columnsToDrop?.length) {
6597
+ this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToDrop.join(', ')} (safe mode active)`);
6598
+ delete filteredDiff.columnsToDrop;
6599
+ }
6600
+ if (filteredDiff.columnsToAlter?.length) {
6601
+ this.logger.logSkippedMigration(`[AutoSync] Skipped altering ${filteredDiff.columnsToAlter.length} columns in table '${diff.tableName}': ${filteredDiff.columnsToAlter.map((c)=>c.to.name).join(', ')} (safe mode active). Use a migration or { safe: false } to apply.`);
6602
+ delete filteredDiff.columnsToAlter;
6603
+ }
6604
+ delete filteredDiff.indexesToDrop;
6605
+ delete filteredDiff.foreignKeysToDrop;
6606
+ }
6607
+ if (!options.drop && filteredDiff.columnsToDrop?.length) {
6608
+ this.logger.logSkippedMigration(`[AutoSync] Skipped dropping ${filteredDiff.columnsToDrop.length} columns in table '${diff.tableName}' (drop: false). Use { drop: true } to apply.`);
6609
+ delete filteredDiff.columnsToDrop;
6610
+ }
6611
+ return filteredDiff;
6612
+ }
6613
+ async executeSyncStatements(statements, options) {
6614
+ const querier = await this.pool.getQuerier();
6615
+ try {
6616
+ if (this.dialect === 'mongodb') {
6617
+ await this.executeMongoSyncStatements(statements, options, querier);
6618
+ } else {
6619
+ await this.executeSqlSyncStatements(statements, options, querier);
6620
+ }
6621
+ if (options.logging) this.logger.logSchema('Schema synchronization completed');
6622
+ } catch (error) {
6623
+ if (this.dialect !== 'mongodb' && isSqlQuerier(querier)) {
6624
+ await querier.rollbackTransaction();
6625
+ }
6626
+ throw error;
6627
+ } finally{
6628
+ await querier.release();
6629
+ }
6630
+ }
6631
+ async executeMongoSyncStatements(statements, options, querier) {
6632
+ const db = querier.db;
6633
+ for (const stmt of statements){
6634
+ const cmd = JSON.parse(stmt);
6635
+ if (options.logging) this.logger.logSchema(`Executing MongoDB: ${stmt}`);
6636
+ const collectionName = cmd.name || cmd.collection;
6637
+ if (!collectionName) {
6638
+ throw new Error(`MongoDB command missing collection name: ${stmt}`);
6639
+ }
6640
+ const collection = db.collection(collectionName);
6641
+ if (cmd.action === 'createCollection') {
6642
+ await db.createCollection(cmd.name);
6643
+ if (cmd.indexes?.length) {
6644
+ for (const idx of cmd.indexes){
6645
+ const key = Object.fromEntries(idx.columns.map((c)=>[
6646
+ c,
6647
+ 1
6648
+ ]));
6649
+ await collection.createIndex(key, {
6650
+ unique: idx.unique,
6651
+ name: idx.name
6652
+ });
6653
+ }
6654
+ }
6655
+ } else if (cmd.action === 'dropCollection') {
6656
+ await collection.drop();
6657
+ } else if (cmd.action === 'createIndex') {
6658
+ await collection.createIndex(cmd.key, cmd.options);
6659
+ } else if (cmd.action === 'dropIndex') {
6660
+ await collection.dropIndex(cmd.name);
6661
+ }
6662
+ }
6663
+ }
6664
+ async executeSqlSyncStatements(statements, options, querier) {
6665
+ if (!isSqlQuerier(querier)) {
6666
+ throw new Error('Migrator requires a SQL-based querier for this dialect');
6667
+ }
6668
+ await querier.beginTransaction();
6669
+ for (const sql of statements){
6670
+ if (options.logging) this.logger.logSchema(`Executing: ${sql}`);
6671
+ await querier.run(sql);
6672
+ }
6673
+ await querier.commitTransaction();
6674
+ }
6675
+ /**
6676
+ * Get migration status
6677
+ */ async status() {
6678
+ const [pending, executed] = await Promise.all([
6679
+ this.pending().then((m)=>m.map((x)=>x.name)),
6680
+ this.executed()
6681
+ ]);
6682
+ return {
6683
+ pending,
6684
+ executed
6685
+ };
6686
+ }
6687
+ /**
6688
+ * Get migration files from the migrations directory
6689
+ */ async getMigrationFiles() {
6690
+ try {
6691
+ const files = await readdir(this.migrationsPath);
6692
+ return files.filter((f)=>/\.(ts|js|mjs)$/.test(f)).filter((f)=>!f.endsWith('.d.ts')).sort();
6693
+ } catch (error) {
6694
+ if (error.code === 'ENOENT') {
6695
+ return [];
6696
+ }
6697
+ throw error;
6698
+ }
6699
+ }
6700
+ /**
6701
+ * Load a migration from a file
6702
+ */ async loadMigration(fileName) {
6703
+ const filePath = join(this.migrationsPath, fileName);
6704
+ const fileUrl = pathToFileURL(filePath).href;
6705
+ try {
6706
+ const module = await import(fileUrl);
6707
+ const migration = module.default ?? module;
6708
+ if (this.isMigration(migration)) {
6709
+ return {
6710
+ name: this.getMigrationName(fileName),
6711
+ up: migration.up.bind(migration),
6712
+ down: migration.down.bind(migration)
6713
+ };
6714
+ }
6715
+ this.logger.logWarn(`Warning: ${fileName} is not a valid migration`);
6716
+ return undefined;
6717
+ } catch (error) {
6718
+ this.logger.logError(`Error loading migration ${fileName}: ${error.message}`, error);
6719
+ return undefined;
6720
+ }
6721
+ }
6722
+ /**
6723
+ * Check if an object is a valid migration
6724
+ */ isMigration(obj) {
6725
+ return typeof obj === 'object' && obj !== undefined && obj !== null && typeof obj.up === 'function' && typeof obj.down === 'function';
6726
+ }
6727
+ /**
6728
+ * Extract migration name from filename
6729
+ */ getMigrationName(fileName) {
6730
+ return basename(fileName, extname(fileName));
6731
+ }
6732
+ /**
6733
+ * Generate timestamp string for migration names
6734
+ */ getTimestamp() {
6735
+ const now = new Date();
6736
+ return [
6737
+ now.getFullYear(),
6738
+ String(now.getMonth() + 1).padStart(2, '0'),
6739
+ String(now.getDate()).padStart(2, '0'),
6740
+ String(now.getHours()).padStart(2, '0'),
6741
+ String(now.getMinutes()).padStart(2, '0'),
6742
+ String(now.getSeconds()).padStart(2, '0')
6743
+ ].join('');
6744
+ }
6745
+ /**
6746
+ * Convert a string to a slug for filenames
6747
+ */ slugify(text) {
6748
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
6749
+ }
6750
+ /**
6751
+ * Generate migration file content
6752
+ */ generateMigrationContent(name) {
6753
+ return /*ts*/ `import type { SqlQuerier } from 'uql-orm/migrate';
6754
+
6755
+ /**
6756
+ * Migration: ${name}
6757
+ * Created: ${new Date().toISOString()}
6758
+ */
6759
+ export default {
6760
+ async up(querier: SqlQuerier): Promise<void> {
6761
+ // Add your migration logic here
6762
+ // Example:
6763
+ // await querier.run(\`
6764
+ // CREATE TABLE "users" (
6765
+ // "id" SERIAL PRIMARY KEY,
6766
+ // "name" VARCHAR(255) NOT NULL,
6767
+ // "email" VARCHAR(255) UNIQUE NOT NULL,
6768
+ // "createdAt" TIMESTAMP DEFAULT CURRENT_TIMESTAMP
6769
+ // )
6770
+ // \`);
6771
+ },
6772
+
6773
+ async down(querier: SqlQuerier): Promise<void> {
6774
+ // Add your rollback logic here
6775
+ // Example:
6776
+ // await querier.run(\`DROP TABLE IF EXISTS "users"\`);
6777
+ },
6778
+ };
6779
+ `;
6780
+ }
6781
+ /**
6782
+ * Generate migration file content with SQL statements
6783
+ */ generateMigrationContentWithStatements(name, upStatements, downStatements) {
6784
+ const upSql = upStatements.map((s)=>/*ts*/ ` await querier.run(\`${s}\`);`).join('\n');
6785
+ const downSql = downStatements.map((s)=>/*ts*/ ` await querier.run(\`${s}\`);`).join('\n');
6786
+ return /*ts*/ `import type { SqlQuerier } from 'uql-orm/migrate';
6787
+
6788
+ /**
6789
+ * Migration: ${name}
6790
+ * Created: ${new Date().toISOString()}
6791
+ * Generated from entity definitions
6792
+ */
6793
+ export default {
6794
+ async up(querier: SqlQuerier): Promise<void> {
6795
+ ${upSql}
6796
+ },
6797
+
6798
+ async down(querier: SqlQuerier): Promise<void> {
6799
+ ${downSql}
6800
+ },
6801
+ };
6802
+ `;
6803
+ }
6804
+ }
6805
+ /**
6806
+ * Helper function to define a migration with proper typing
6807
+ */ function defineMigration(migration) {
6808
+ return migration;
6809
+ }
6810
+ /**
6811
+ * Define a migration using the type-safe builder API.
6812
+ *
6813
+ * @example
6814
+ * ```ts
6815
+ * export default defineBuilderMigration({
6816
+ * async up(m) {
6817
+ * await m.createTable('users', (table) => {
6818
+ * table.id();
6819
+ * table.string('email', 255).unique();
6820
+ * table.timestamps();
6821
+ * });
6822
+ * },
6823
+ * async down(m) {
6824
+ * await m.dropTable('users');
6825
+ * }
6826
+ * });
6827
+ * ```
6828
+ */ function defineBuilderMigration(migration) {
6829
+ return migration;
6830
+ }
6831
+
6832
+ /**
6833
+ * Stores migration state in a JSON file.
6834
+ * Useful for development or environments without a database.
6835
+ */ class JsonMigrationStorage {
6836
+ constructor(filePath = './migrations/.uql-migrations.json'){
6837
+ this.executedMigrations = [];
6838
+ this.filePath = filePath;
6839
+ }
6840
+ async ensureStorage() {
6841
+ const content = await fs.readFile(this.filePath, 'utf-8').catch(async (error)=>{
6842
+ if (error.code === 'ENOENT') {
6843
+ const initial = '[]';
6844
+ await fs.writeFile(this.filePath, initial, 'utf-8');
6845
+ return initial;
6846
+ }
6847
+ throw error;
6848
+ });
6849
+ this.executedMigrations = JSON.parse(content);
6850
+ }
6851
+ async executed() {
6852
+ await this.ensureStorage();
6853
+ return this.executedMigrations;
6854
+ }
6855
+ async logWithQuerier(_querier, migrationName) {
6856
+ await this.ensureStorage();
6857
+ if (!this.executedMigrations.includes(migrationName)) {
6858
+ this.executedMigrations.push(migrationName);
6859
+ await this.save();
6860
+ }
6861
+ }
6862
+ async unlogWithQuerier(_querier, migrationName) {
6863
+ await this.ensureStorage();
6864
+ this.executedMigrations = this.executedMigrations.filter((m)=>m !== migrationName);
6865
+ await this.save();
6866
+ }
6867
+ async save() {
6868
+ await fs.writeFile(this.filePath, JSON.stringify(this.executedMigrations, null, 2), 'utf-8');
6869
+ }
6870
+ }
6871
+
6872
+ /**
6873
+ * Service for synchronizing schemas between entities and database.
6874
+ */ class SchemaSync {
6875
+ constructor(options){
6876
+ this.options = options;
6877
+ }
6878
+ /**
6879
+ * Execute schema synchronization.
6880
+ */ async sync() {
6881
+ const direction = this.options.direction ?? 'bidirectional';
6882
+ switch(direction){
6883
+ case 'entity-to-db':
6884
+ return this.syncEntityToDb();
6885
+ case 'db-to-entity':
6886
+ return this.syncDbToEntity();
6887
+ case 'bidirectional':
6888
+ return this.syncBidirectional();
6889
+ }
6890
+ }
6891
+ /**
6892
+ * Sync from entities to database (push).
6893
+ */ async syncEntityToDb() {
6894
+ const entityAST = this.buildEntityAST();
6895
+ const dbAST = await this.buildDatabaseAST();
6896
+ const differ = new SchemaASTDiffer();
6897
+ const diff = differ.diff(entityAST, dbAST);
6898
+ // In safe mode, filter out destructive changes
6899
+ const safeDiff = this.options.safe ? this.filterDestructiveChanges(diff) : diff;
6900
+ return {
6901
+ direction: 'entity-to-db',
6902
+ success: true,
6903
+ dbChanges: safeDiff,
6904
+ entityChanges: undefined,
6905
+ conflicts: [],
6906
+ summary: this.formatSummary('entity-to-db', safeDiff),
6907
+ details: {
6908
+ db: this.getDiffCounts(safeDiff),
6909
+ entity: {
6910
+ created: 0,
6911
+ dropped: 0,
6912
+ altered: 0
6913
+ }
6914
+ }
6915
+ };
6916
+ }
6917
+ /**
6918
+ * Sync from database to entities (pull).
6919
+ */ async syncDbToEntity() {
6920
+ const entityAST = this.buildEntityAST();
6921
+ const dbAST = await this.buildDatabaseAST();
6922
+ const differ = new SchemaASTDiffer();
6923
+ // Diff in opposite direction: DB is source, entity is target
6924
+ const diff = differ.diff(dbAST, entityAST);
6925
+ // Apply safe mode if enabled
6926
+ const safeDiff = this.options.safe ? this.filterDestructiveChanges(diff) : diff;
6927
+ return {
6928
+ direction: 'db-to-entity',
6929
+ success: true,
6930
+ dbChanges: undefined,
6931
+ entityChanges: safeDiff,
6932
+ conflicts: [],
6933
+ summary: this.formatSummary('db-to-entity', safeDiff),
6934
+ details: {
6935
+ db: {
6936
+ created: 0,
6937
+ dropped: 0,
6938
+ altered: 0
6939
+ },
6940
+ entity: this.getDiffCounts(safeDiff)
6941
+ }
6942
+ };
6943
+ }
6944
+ /**
6945
+ * Bidirectional sync with conflict detection.
6946
+ */ async syncBidirectional() {
6947
+ const entityAST = this.buildEntityAST();
6948
+ const dbAST = await this.buildDatabaseAST();
6949
+ const differ = new SchemaASTDiffer();
6950
+ // Get both directions
6951
+ const entityToDb = differ.diff(entityAST, dbAST);
6952
+ const dbToEntity = differ.diff(dbAST, entityAST);
6953
+ // Detect conflicts (changes in both directions for same elements)
6954
+ const conflicts = this.detectConflicts(entityToDb, dbToEntity);
6955
+ // Filter out conflicting changes
6956
+ let safeEntityToDb = this.filterConflictingChanges(entityToDb, conflicts);
6957
+ let safeDbToEntity = this.filterConflictingChanges(dbToEntity, conflicts);
6958
+ // Apply safe mode if enabled
6959
+ if (this.options.safe) {
6960
+ safeEntityToDb = this.filterDestructiveChanges(safeEntityToDb);
6961
+ safeDbToEntity = this.filterDestructiveChanges(safeDbToEntity);
6962
+ }
6963
+ return {
6964
+ direction: 'bidirectional',
6965
+ success: conflicts.length === 0,
6966
+ dbChanges: safeEntityToDb,
6967
+ entityChanges: safeDbToEntity,
6968
+ conflicts,
6969
+ summary: this.formatBidirectionalSummary(safeEntityToDb, safeDbToEntity, conflicts),
6970
+ details: {
6971
+ db: this.getDiffCounts(safeEntityToDb),
6972
+ entity: this.getDiffCounts(safeDbToEntity)
6973
+ }
6974
+ };
6975
+ }
6976
+ /**
6977
+ * Build SchemaAST from entities.
6978
+ */ buildEntityAST() {
6979
+ const builder = new SchemaASTBuilder();
6980
+ return builder.fromEntities(this.options.entities);
6981
+ }
6982
+ /**
6983
+ * Build SchemaAST from database.
6984
+ */ async buildDatabaseAST() {
6985
+ return this.options.introspector.introspect();
6986
+ }
6987
+ /**
6988
+ * Filter destructive changes for safe mode.
6989
+ */ filterDestructiveChanges(diff) {
6990
+ return {
6991
+ ...diff,
6992
+ tablesToDrop: [],
6993
+ columnDiffs: diff.columnDiffs.filter((d)=>d.type !== 'drop'),
6994
+ indexDiffs: diff.indexDiffs.filter((d)=>d.type !== 'drop'),
6995
+ relationshipDiffs: diff.relationshipDiffs.filter((d)=>d.type !== 'drop')
6996
+ };
6997
+ }
6998
+ /**
6999
+ * Detect conflicts between bidirectional changes.
7000
+ */ detectConflicts(entityToDb, dbToEntity) {
7001
+ const conflicts = [];
7002
+ // Find column alterations that exist in both directions (type conflicts)
7003
+ const entityColAlters = new Map(entityToDb.columnDiffs.filter((d)=>d.type === 'alter').map((d)=>[
7004
+ `${d.table}.${d.column}`,
7005
+ d
7006
+ ]));
7007
+ for (const dbColDiff of dbToEntity.columnDiffs.filter((d)=>d.type === 'alter')){
7008
+ const key = `${dbColDiff.table}.${dbColDiff.column}`;
7009
+ const entityColDiff = entityColAlters.get(key);
7010
+ if (entityColDiff) {
7011
+ // Both sides want to alter the same column - conflict
7012
+ conflicts.push({
7013
+ type: 'type_mismatch',
7014
+ table: dbColDiff.table,
7015
+ column: dbColDiff.column,
7016
+ entityValue: entityColDiff.expected?.type,
7017
+ dbValue: dbColDiff.expected?.type,
7018
+ suggestion: `Column has different types. Entity: ${this.formatType(entityColDiff.expected?.type)}, DB: ${this.formatType(dbColDiff.expected?.type)}`
7019
+ });
7020
+ }
7021
+ }
7022
+ return conflicts;
7023
+ }
7024
+ /**
7025
+ * Filter out conflicting changes.
7026
+ */ filterConflictingChanges(diff, conflicts) {
7027
+ const conflictKeys = new Set(conflicts.map((c)=>`${c.table}.${c.column ?? ''}`));
7028
+ return {
7029
+ ...diff,
7030
+ columnDiffs: diff.columnDiffs.filter((d)=>!conflictKeys.has(`${d.table}.${d.column}`)),
7031
+ tablesToAlter: diff.tablesToAlter.filter((t)=>!conflictKeys.has(`${t.name}.`))
7032
+ };
7033
+ }
7034
+ /**
7035
+ * Get counts of changes from a diff.
7036
+ */ getDiffCounts(diff) {
7037
+ return {
7038
+ created: diff.tablesToCreate.length + diff.columnDiffs.filter((d)=>d.type === 'add').length,
7039
+ dropped: diff.tablesToDrop.length + diff.columnDiffs.filter((d)=>d.type === 'drop').length,
7040
+ altered: diff.columnDiffs.filter((d)=>d.type === 'alter').length
7041
+ };
7042
+ }
7043
+ /**
7044
+ * Format summary for single-direction sync.
7045
+ */ formatSummary(direction, diff) {
7046
+ const lines = [];
7047
+ if (direction === 'entity-to-db') {
7048
+ lines.push('Entity → Database Changes:');
7049
+ } else {
7050
+ lines.push('Database → Entity Changes:');
7051
+ }
7052
+ if (diff.tablesToCreate.length > 0) {
7053
+ lines.push(` + ${diff.tablesToCreate.length} table(s) to create`);
7054
+ }
7055
+ if (diff.tablesToDrop.length > 0) {
7056
+ lines.push(` - ${diff.tablesToDrop.length} table(s) to drop`);
7057
+ }
7058
+ if (diff.columnDiffs.length > 0) {
7059
+ const adds = diff.columnDiffs.filter((d)=>d.type === 'add').length;
7060
+ const drops = diff.columnDiffs.filter((d)=>d.type === 'drop').length;
7061
+ const alters = diff.columnDiffs.filter((d)=>d.type === 'alter').length;
7062
+ if (adds) lines.push(` + ${adds} column(s) to add`);
7063
+ if (drops) lines.push(` - ${drops} column(s) to drop`);
7064
+ if (alters) lines.push(` ~ ${alters} column(s) to alter`);
7065
+ }
7066
+ if (diff.indexDiffs.length > 0) {
7067
+ lines.push(` ${diff.indexDiffs.length} index change(s)`);
7068
+ }
7069
+ if (!diff.hasDifferences) {
7070
+ lines.push(' Schema is already in sync.');
7071
+ }
7072
+ return lines.join('\n');
7073
+ }
7074
+ /**
7075
+ * Format summary for bidirectional sync.
7076
+ */ formatBidirectionalSummary(entityToDb, dbToEntity, conflicts) {
7077
+ const lines = [];
7078
+ lines.push('Bidirectional Sync Report');
7079
+ lines.push('========================\n');
7080
+ if (conflicts.length > 0) {
7081
+ lines.push(`⚠️ ${conflicts.length} conflict(s) require manual resolution:\n`);
7082
+ for (const conflict of conflicts){
7083
+ lines.push(` • ${conflict.table}.${conflict.column ?? ''}: ${conflict.suggestion}`);
7084
+ }
7085
+ lines.push('');
7086
+ }
7087
+ if (entityToDb.hasDifferences) {
7088
+ lines.push('Entity → Database:');
7089
+ lines.push(this.formatSummary('entity-to-db', entityToDb).split('\n').slice(1).join('\n'));
7090
+ lines.push('');
7091
+ }
7092
+ if (dbToEntity.hasDifferences) {
7093
+ lines.push('Database → Entity:');
7094
+ lines.push(this.formatSummary('db-to-entity', dbToEntity).split('\n').slice(1).join('\n'));
7095
+ }
7096
+ if (!entityToDb.hasDifferences && !dbToEntity.hasDifferences && conflicts.length === 0) {
7097
+ lines.push('✓ Schema is in sync.');
7098
+ }
7099
+ return lines.join('\n');
7100
+ }
7101
+ /**
7102
+ * Format type for display.
7103
+ */ formatType(type) {
7104
+ if (!type || !type.category) return 'unknown';
7105
+ return type.length ? `${type.category}(${type.length})` : type.category;
7106
+ }
7107
+ }
7108
+ /**
7109
+ * Create a SchemaSync instance.
7110
+ */ function createSchemaSync(options) {
7111
+ return new SchemaSync(options);
7112
+ }
7113
+
7114
+ export { AbstractSqlSchemaIntrospector, ColumnBuilder, DatabaseMigrationStorage, DriftDetector, EntityCodeGenerator, EntityMerger, JsonMigrationStorage, MariadbSchemaIntrospector, MigrationBuilder, MigrationCodeGenerator, Migrator, MongoSchemaGenerator, MongoSchemaIntrospector, MysqlSchemaIntrospector, OperationRecorder, PostgresSchemaIntrospector, SchemaSync, SmartRelationDetector, SqlExpression, SqlSchemaGenerator, SqliteSchemaIntrospector, TableBuilder, createDriftDetector, createDryRunBuilder, createEntityCodeGenerator, createEntityMerger, createMigrationCodeGenerator, createRelationDetector, createSchemaGenerator, createSchemaSync, defineBuilderMigration, defineMigration, detectDrift, formatDefaultValue, loadConfig, t };
7115
+ //# sourceMappingURL=uql-browser.min.js.map