retold-data-service 2.0.29 → 2.0.30

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "retold-data-service",
3
- "version": "2.0.29",
3
+ "version": "2.0.30",
4
4
  "description": "Serve up a whole model!",
5
5
  "main": "source/Retold-Data-Service.js",
6
6
  "bin": {
@@ -73,7 +73,7 @@
73
73
  "meadow-connection-mysql": "^1.0.14",
74
74
  "meadow-endpoints": "^4.0.15",
75
75
  "meadow-integration": "^1.0.21",
76
- "meadow-migrationmanager": "^0.0.6",
76
+ "meadow-migrationmanager": "^0.0.7",
77
77
  "orator": "^6.0.4",
78
78
  "orator-http-proxy": "^1.0.5",
79
79
  "orator-serviceserver-restify": "^2.0.10",
@@ -511,66 +511,215 @@ module.exports = (pDataClonerService, pOratorServiceServer) =>
511
511
  // MySQL / MSSQL / PostgreSQL: async execution via connection pool
512
512
  if (tmpActiveProvider.pool)
513
513
  {
514
- // For MSSQL multi-statement batches (containing DECLARE,
515
- // cursors, dynamic SQL with QUOTENAME, etc.), pool.query()
516
- // wraps SQL in sp_executesql which cannot handle these
517
- // constructs. Use request.batch() instead, which sends
518
- // raw T-SQL without wrapping.
519
- let tmpIsBatch = (tmpProviderName === 'MSSQL') && tmpSQL.indexOf('DECLARE') >= 0
520
- && typeof(tmpActiveProvider.pool.request) === 'function';
521
-
522
- if (tmpIsBatch)
514
+ // Helper: run a single SQL statement via pool.query,
515
+ // handling both callback-style (MySQL) and promise-style
516
+ // (MSSQL / PostgreSQL) drivers.
517
+ let fRunQuery = (pSQL, fDone) =>
523
518
  {
524
- tmpActiveProvider.pool.request().batch(tmpSQL)
525
- .then(() =>
519
+ let tmpQueryResult = tmpActiveProvider.pool.query(pSQL,
520
+ (pQueryError, pResult) =>
526
521
  {
527
- tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
528
- tmpExecutedStatements.push(tmpSQL);
529
- return fExecNext(pIndex + 1);
530
- })
531
- .catch((pBatchError) =>
532
- {
533
- tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pBatchError}`);
534
- return fExecNext(pIndex + 1);
535
- });
536
- }
537
- else
538
- {
539
- // MySQL uses callbacks; MSSQL/PostgreSQL pool.query returns a promise.
540
- let tmpQueryResult = tmpActiveProvider.pool.query(tmpSQL,
541
- (pQueryError) =>
542
- {
543
- // Callback-style (MySQL) — only fires if pool.query
544
- // accepted the callback parameter
545
522
  if (pQueryError)
546
523
  {
547
- tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pQueryError}`);
524
+ return fDone(pQueryError);
548
525
  }
549
- else
550
- {
551
- tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
552
- tmpExecutedStatements.push(tmpSQL);
553
- }
554
- return fExecNext(pIndex + 1);
526
+ return fDone(null, pResult);
555
527
  });
556
528
 
557
- // Promise-style (MSSQL / PostgreSQL) — if query returned a thenable,
558
- // handle it; the callback above will not fire in this case.
559
529
  if (tmpQueryResult && typeof(tmpQueryResult.then) === 'function')
560
530
  {
561
531
  tmpQueryResult
562
- .then(() =>
532
+ .then((pResult) => fDone(null, pResult))
533
+ .catch((pError) => fDone(pError));
534
+ }
535
+ };
536
+
537
+ // For MSSQL ALTER COLUMN, drop dependent objects first,
538
+ // then alter, then restore them. Each step is a simple
539
+ // individual query that works with pool.query().
540
+ let tmpIsMSSQLAlterColumn = (tmpProviderName === 'MSSQL')
541
+ && tmpSQL.indexOf('ALTER COLUMN') >= 0;
542
+
543
+ if (tmpIsMSSQLAlterColumn)
544
+ {
545
+ // Parse table and column names from the statement:
546
+ // ALTER TABLE [TableName] ALTER COLUMN [ColName] TYPE...
547
+ let tmpMatch = tmpSQL.match(/ALTER TABLE \[([^\]]+)\] ALTER COLUMN \[([^\]]+)\] (.+)/);
548
+ if (!tmpMatch)
549
+ {
550
+ tmpFable.log.warn(`Data Cloner: Could not parse ALTER COLUMN statement: ${tmpSQL}`);
551
+ return fExecNext(pIndex + 1);
552
+ }
553
+ let tmpTblName = tmpMatch[1];
554
+ let tmpColNameParsed = tmpMatch[2];
555
+ let tmpNewType = tmpMatch[3];
556
+
557
+ // Determine the default value to re-add after ALTER.
558
+ // Look it up from the diff's target schema via the
559
+ // MigrationGenerator helpers.
560
+ let tmpMigGen = tmpMM._migrationGenerator;
561
+ let tmpDefaultValue = null;
562
+ // Find the column in the diff to get its DataType and Size
563
+ for (let t = 0; t < tmpDiff.TablesModified.length; t++)
564
+ {
565
+ let tmpMod = tmpDiff.TablesModified[t];
566
+ if (tmpMod.TableName !== tmpTblName) continue;
567
+ let tmpAllModCols = (tmpMod.ColumnsModified || []);
568
+ for (let mc = 0; mc < tmpAllModCols.length; mc++)
569
+ {
570
+ if (tmpAllModCols[mc].Column === tmpColNameParsed)
571
+ {
572
+ let tmpTargetDataType = tmpAllModCols[mc].Changes.DataType
573
+ ? tmpAllModCols[mc].Changes.DataType.To
574
+ : (tmpAllModCols[mc].DataType || null);
575
+ let tmpTargetSize = tmpAllModCols[mc].Changes.Size
576
+ ? tmpAllModCols[mc].Changes.Size.To
577
+ : (tmpAllModCols[mc].hasOwnProperty('Size') ? tmpAllModCols[mc].Size : null);
578
+ if (tmpTargetDataType)
579
+ {
580
+ let tmpFullNative = tmpMigGen._mapDataTypeToNative(tmpTargetDataType, tmpTargetSize, 'MSSQL');
581
+ tmpDefaultValue = tmpMigGen._extractDefault(tmpFullNative);
582
+ }
583
+ break;
584
+ }
585
+ }
586
+ }
587
+
588
+ // Step 1: Query default constraint names on this column
589
+ let tmpDCQuery = `SELECT dc.name FROM sys.default_constraints dc INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND dc.parent_column_id = c.column_id WHERE dc.parent_object_id = OBJECT_ID(N'${tmpTblName}') AND c.name = N'${tmpColNameParsed}'`;
590
+
591
+ // Step 2: Query index info for indexes containing this column
592
+ let tmpIxQuery = `SELECT DISTINCT i.name AS IndexName, i.is_unique AS IsUnique FROM sys.indexes i INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE i.object_id = OBJECT_ID(N'${tmpTblName}') AND c.name = N'${tmpColNameParsed}' AND i.is_primary_key = 0 AND i.type IN (1, 2)`;
593
+
594
+ // Step 3: Query index column details (for recreation)
595
+ let tmpIxColQuery = `SELECT i.name AS IndexName, c.name AS ColName, ic.is_included_column AS IsIncluded, ic.key_ordinal AS KeyOrdinal, ic.index_column_id AS ColOrder FROM sys.indexes i INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id WHERE i.object_id = OBJECT_ID(N'${tmpTblName}') AND i.is_primary_key = 0 AND i.type IN (1, 2) AND i.index_id IN (SELECT ic2.index_id FROM sys.index_columns ic2 INNER JOIN sys.columns c2 ON ic2.object_id = c2.object_id AND ic2.column_id = c2.column_id WHERE ic2.object_id = OBJECT_ID(N'${tmpTblName}') AND c2.name = N'${tmpColNameParsed}') ORDER BY i.name, ic.is_included_column, ic.key_ordinal, ic.index_column_id`;
596
+
597
+ tmpFable.log.info(`Data Cloner: MSSQL ALTER COLUMN [${tmpColNameParsed}] — querying dependent objects...`);
598
+
599
+ // Run queries in sequence: get constraints, get indexes, drop, alter, recreate
600
+ fRunQuery(tmpDCQuery, (pDCErr, pDCResult) =>
601
+ {
602
+ let tmpConstraintNames = [];
603
+ if (!pDCErr && pDCResult && pDCResult.recordset)
604
+ {
605
+ tmpConstraintNames = pDCResult.recordset.map((pRow) => pRow.name);
606
+ }
607
+
608
+ fRunQuery(tmpIxQuery, (pIxErr, pIxResult) =>
609
+ {
610
+ let tmpIndexes = [];
611
+ if (!pIxErr && pIxResult && pIxResult.recordset)
563
612
  {
564
- tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
565
- tmpExecutedStatements.push(tmpSQL);
566
- return fExecNext(pIndex + 1);
567
- })
568
- .catch((pPromiseError) =>
613
+ tmpIndexes = pIxResult.recordset;
614
+ }
615
+
616
+ fRunQuery(tmpIxColQuery, (pIxColErr, pIxColResult) =>
569
617
  {
570
- tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pPromiseError}`);
571
- return fExecNext(pIndex + 1);
618
+ let tmpIndexColumns = [];
619
+ if (!pIxColErr && pIxColResult && pIxColResult.recordset)
620
+ {
621
+ tmpIndexColumns = pIxColResult.recordset;
622
+ }
623
+
624
+ // Build the list of statements to execute in sequence:
625
+ // 1. Drop default constraints
626
+ // 2. Drop indexes
627
+ // 3. ALTER COLUMN
628
+ // 4. Recreate indexes
629
+ // 5. Re-add default constraint
630
+ let tmpPrePostStatements = [];
631
+
632
+ for (let dc = 0; dc < tmpConstraintNames.length; dc++)
633
+ {
634
+ tmpPrePostStatements.push({ phase: 'pre', sql: `ALTER TABLE [${tmpTblName}] DROP CONSTRAINT [${tmpConstraintNames[dc]}]` });
635
+ }
636
+
637
+ for (let ix = 0; ix < tmpIndexes.length; ix++)
638
+ {
639
+ tmpPrePostStatements.push({ phase: 'pre', sql: `DROP INDEX [${tmpIndexes[ix].IndexName}] ON [${tmpTblName}]` });
640
+ }
641
+
642
+ tmpPrePostStatements.push({ phase: 'alter', sql: tmpSQL });
643
+
644
+ // Recreate indexes
645
+ for (let ix = 0; ix < tmpIndexes.length; ix++)
646
+ {
647
+ let tmpIxName = tmpIndexes[ix].IndexName;
648
+ let tmpKeyCols = tmpIndexColumns
649
+ .filter((pR) => pR.IndexName === tmpIxName && !pR.IsIncluded)
650
+ .sort((a, b) => a.KeyOrdinal - b.KeyOrdinal)
651
+ .map((pR) => `[${pR.ColName}]`);
652
+ let tmpInclCols = tmpIndexColumns
653
+ .filter((pR) => pR.IndexName === tmpIxName && pR.IsIncluded)
654
+ .sort((a, b) => a.ColOrder - b.ColOrder)
655
+ .map((pR) => `[${pR.ColName}]`);
656
+ let tmpCreateIdx = (tmpIndexes[ix].IsUnique ? 'CREATE UNIQUE' : 'CREATE')
657
+ + ` INDEX [${tmpIxName}] ON [${tmpTblName}] (${tmpKeyCols.join(', ')})`;
658
+ if (tmpInclCols.length > 0)
659
+ {
660
+ tmpCreateIdx += ` INCLUDE (${tmpInclCols.join(', ')})`;
661
+ }
662
+ tmpPrePostStatements.push({ phase: 'post', sql: tmpCreateIdx });
663
+ }
664
+
665
+ // Re-add default
666
+ if (tmpDefaultValue)
667
+ {
668
+ tmpPrePostStatements.push({ phase: 'post', sql: `ALTER TABLE [${tmpTblName}] ADD DEFAULT ${tmpDefaultValue} FOR [${tmpColNameParsed}]` });
669
+ }
670
+
671
+ if (tmpConstraintNames.length > 0 || tmpIndexes.length > 0)
672
+ {
673
+ tmpFable.log.info(`Data Cloner: MSSQL ALTER COLUMN [${tmpColNameParsed}] — dropping ${tmpConstraintNames.length} constraint(s), ${tmpIndexes.length} index(es) before alter`);
674
+ }
675
+
676
+ // Execute all statements in sequence
677
+ let fRunStep = (pStepIndex) =>
678
+ {
679
+ if (pStepIndex >= tmpPrePostStatements.length)
680
+ {
681
+ tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
682
+ tmpExecutedStatements.push(tmpSQL);
683
+ return fExecNext(pIndex + 1);
684
+ }
685
+ let tmpStep = tmpPrePostStatements[pStepIndex];
686
+ fRunQuery(tmpStep.sql, (pStepErr) =>
687
+ {
688
+ if (pStepErr)
689
+ {
690
+ tmpFable.log.warn(`Data Cloner: Migration step failed (${tmpStep.phase}): ${tmpStep.sql} — ${pStepErr}`);
691
+ if (tmpStep.phase === 'alter')
692
+ {
693
+ // If the ALTER itself failed, skip post steps
694
+ tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pStepErr}`);
695
+ return fExecNext(pIndex + 1);
696
+ }
697
+ }
698
+ return fRunStep(pStepIndex + 1);
699
+ });
700
+ };
701
+
702
+ fRunStep(0);
572
703
  });
573
- }
704
+ });
705
+ });
706
+ }
707
+ else
708
+ {
709
+ // Non-MSSQL-ALTER-COLUMN: standard single-statement execution
710
+ fRunQuery(tmpSQL, (pQueryError) =>
711
+ {
712
+ if (pQueryError)
713
+ {
714
+ tmpFable.log.warn(`Data Cloner: Migration failed: ${tmpSQL} — ${pQueryError}`);
715
+ }
716
+ else
717
+ {
718
+ tmpFable.log.info(`Data Cloner: Migration applied: ${tmpSQL}`);
719
+ tmpExecutedStatements.push(tmpSQL);
720
+ }
721
+ return fExecNext(pIndex + 1);
722
+ });
574
723
  }
575
724
  }
576
725
  else