uql-orm 0.7.3 → 0.7.4

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