@memberjunction/codegen-lib 5.4.1 → 5.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +65 -2
  2. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  3. package/dist/Angular/angular-codegen.js +26 -12
  4. package/dist/Angular/angular-codegen.js.map +1 -1
  5. package/dist/Angular/related-entity-components.js +2 -2
  6. package/dist/Angular/related-entity-components.js.map +1 -1
  7. package/dist/Config/config.d.ts +10 -0
  8. package/dist/Config/config.d.ts.map +1 -1
  9. package/dist/Config/config.js +10 -0
  10. package/dist/Config/config.js.map +1 -1
  11. package/dist/Database/codeGenDatabaseProvider.d.ts +544 -0
  12. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -0
  13. package/dist/Database/codeGenDatabaseProvider.js +29 -0
  14. package/dist/Database/codeGenDatabaseProvider.js.map +1 -0
  15. package/dist/Database/manage-metadata.d.ts +165 -60
  16. package/dist/Database/manage-metadata.d.ts.map +1 -1
  17. package/dist/Database/manage-metadata.js +592 -483
  18. package/dist/Database/manage-metadata.js.map +1 -1
  19. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts +53 -0
  20. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.d.ts.map +1 -0
  21. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js +112 -0
  22. package/dist/Database/providers/postgresql/PostgreSQLCodeGenConnection.js.map +1 -0
  23. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +344 -0
  24. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -0
  25. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +1567 -0
  26. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -0
  27. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts +42 -0
  28. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.d.ts.map +1 -0
  29. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js +84 -0
  30. package/dist/Database/providers/sqlserver/SQLServerCodeGenConnection.js.map +1 -0
  31. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +372 -0
  32. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -0
  33. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +1483 -0
  34. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -0
  35. package/dist/Database/reorder-columns.d.ts +2 -2
  36. package/dist/Database/reorder-columns.d.ts.map +1 -1
  37. package/dist/Database/reorder-columns.js +9 -9
  38. package/dist/Database/reorder-columns.js.map +1 -1
  39. package/dist/Database/sql.d.ts +10 -5
  40. package/dist/Database/sql.d.ts.map +1 -1
  41. package/dist/Database/sql.js +44 -228
  42. package/dist/Database/sql.js.map +1 -1
  43. package/dist/Database/sql_codegen.d.ts +31 -29
  44. package/dist/Database/sql_codegen.d.ts.map +1 -1
  45. package/dist/Database/sql_codegen.js +209 -842
  46. package/dist/Database/sql_codegen.js.map +1 -1
  47. package/dist/Misc/action_subclasses_codegen.js +3 -2
  48. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  49. package/dist/Misc/entity_subclasses_codegen.d.ts +4 -4
  50. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  51. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  52. package/dist/Misc/graphql_server_codegen.d.ts +6 -1
  53. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  54. package/dist/Misc/graphql_server_codegen.js +33 -35
  55. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  56. package/dist/Misc/sql_logging.d.ts +2 -2
  57. package/dist/Misc/sql_logging.d.ts.map +1 -1
  58. package/dist/Misc/sql_logging.js +1 -1
  59. package/dist/Misc/sql_logging.js.map +1 -1
  60. package/dist/Misc/system_integrity.d.ts +6 -6
  61. package/dist/Misc/system_integrity.d.ts.map +1 -1
  62. package/dist/Misc/system_integrity.js +33 -8
  63. package/dist/Misc/system_integrity.js.map +1 -1
  64. package/dist/Misc/temp_batch_file.d.ts.map +1 -1
  65. package/dist/Misc/temp_batch_file.js +4 -1
  66. package/dist/Misc/temp_batch_file.js.map +1 -1
  67. package/dist/index.d.ts +5 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +5 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/runCodeGen.d.ts +30 -75
  72. package/dist/runCodeGen.d.ts.map +1 -1
  73. package/dist/runCodeGen.js +123 -215
  74. package/dist/runCodeGen.js.map +1 -1
  75. package/package.json +18 -15
@@ -3,12 +3,13 @@ import { logError, logStatus, logWarning, startSpinner, updateSpinner, succeedSp
3
3
  import * as fs from 'fs';
4
4
  import path from 'path';
5
5
  import { SQLUtilityBase } from './sql.js';
6
- import sql from 'mssql';
7
- import { autoIndexForeignKeys, configInfo, customSqlScripts, dbDatabase, mjCoreSchema, MAX_INDEX_NAME_LENGTH } from '../Config/config.js';
6
+ import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
7
+ import { SQLServerCodeGenProvider } from './providers/sqlserver/SQLServerCodeGenProvider.js';
8
+ import { autoIndexForeignKeys, configInfo, customSqlScripts, mjCoreSchema } from '../Config/config.js';
8
9
  import { ManageMetadataBase } from './manage-metadata.js';
9
10
  import { UserCache } from '@memberjunction/sqlserver-dataprovider';
10
- import { combineFiles, logIf, sortBySequenceAndCreatedAt } from '../Misc/util.js';
11
- import { MJGlobal } from '@memberjunction/global';
11
+ import { combineFiles, logIf } from '../Misc/util.js';
12
+ import { MJGlobal, UUIDsEqual } from '@memberjunction/global';
12
13
  import { SQLLogging } from '../Misc/sql_logging.js';
13
14
  import { TempBatchFile } from '../Misc/temp_batch_file.js';
14
15
  export const SPType = {
@@ -21,9 +22,44 @@ export const SPType = {
21
22
  * databases. The base class implements support for SQL Server. In future versions of MJ, we will break out an abstract base class that has the skeleton of the logic and then the SQL Server version will be a sub-class
22
23
  * of that abstract base class and other databases will be sub-classes of the abstract base class as well.
23
24
  */
25
+ /**
26
+ * Creates the appropriate CodeGen database provider based on the configured database type.
27
+ * Falls back to SQLServerCodeGenProvider for backward compatibility.
28
+ *
29
+ * @param dbType The database platform type from configuration
30
+ * @returns A CodeGenDatabaseProvider instance for the specified platform
31
+ */
32
+ function createCodeGenProvider(dbType) {
33
+ switch (dbType) {
34
+ case 'postgresql':
35
+ // Dynamic import is avoided - the PostgreSQL provider should be
36
+ // registered via MJGlobal ClassFactory for proper decoupling.
37
+ // For now, attempt to resolve from ClassFactory.
38
+ try {
39
+ const pgProvider = MJGlobal.Instance.ClassFactory.CreateInstance(CodeGenDatabaseProvider, 'PostgreSQLCodeGenProvider');
40
+ if (pgProvider) {
41
+ return pgProvider;
42
+ }
43
+ }
44
+ catch {
45
+ // Fall through to error
46
+ }
47
+ throw new Error('PostgreSQL CodeGen provider not found. Ensure @memberjunction/postgresql-dataprovider ' +
48
+ 'is installed and its CodeGen provider is registered before running CodeGen.');
49
+ case 'mssql':
50
+ default:
51
+ return new SQLServerCodeGenProvider();
52
+ }
53
+ }
24
54
  export class SQLCodeGenBase {
25
55
  constructor() {
26
56
  this._sqlUtilityObject = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
57
+ /**
58
+ * The database-specific code generation provider. Defaults to SQL Server.
59
+ * Override this property or set it before calling generation methods to use
60
+ * a different database platform (e.g., PostgreSQL).
61
+ */
62
+ this._dbProvider = createCodeGenProvider(configInfo.dbType);
27
63
  /**
28
64
  * Array of entity names that qualify for forced regeneration based on the whereClause filter
29
65
  */
@@ -50,6 +86,12 @@ export class SQLCodeGenBase {
50
86
  get SQLUtilityObject() {
51
87
  return this._sqlUtilityObject;
52
88
  }
89
+ get DBProvider() {
90
+ return this._dbProvider;
91
+ }
92
+ set DBProvider(value) {
93
+ this._dbProvider = value;
94
+ }
53
95
  async manageSQLScriptsAndExecution(pool, entities, directory, currentUser) {
54
96
  try {
55
97
  // Build list of entities qualified for forced regeneration if entityWhereClause is provided
@@ -57,12 +99,15 @@ export class SQLCodeGenBase {
57
99
  this.filterEntitiesQualifiedForRegeneration = true; // Enable filtering
58
100
  try {
59
101
  const whereClause = configInfo.forceRegeneration.entityWhereClause;
102
+ const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
103
+ const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
104
+ const quotedWhereClause = this._dbProvider.quoteSQLForExecution(whereClause);
60
105
  const query = `
61
- SELECT Name
62
- FROM [${mjCoreSchema}].[Entity]
63
- WHERE ${whereClause}
106
+ SELECT ${qi('Name')}
107
+ FROM ${qs(mjCoreSchema, 'Entity')}
108
+ WHERE ${quotedWhereClause}
64
109
  `;
65
- const result = await pool.request().query(query);
110
+ const result = await pool.query(query);
66
111
  this.entitiesQualifiedForForcedRegeneration = result.recordset.map((r) => r.Name);
67
112
  logStatus(`Force regeneration filter enabled: ${this.entitiesQualifiedForForcedRegeneration.length} entities qualified based on entityWhereClause: ${whereClause}`);
68
113
  }
@@ -107,7 +152,7 @@ export class SQLCodeGenBase {
107
152
  // First, separate entities that need cascade delete regeneration from others
108
153
  const entitiesWithoutCascadeRegeneration = includedEntities.filter(e => !this.entitiesNeedingDeleteSPRegeneration.has(e.ID));
109
154
  const entitiesForCascadeRegeneration = this.orderedEntitiesForDeleteSPRegeneration
110
- .map(id => includedEntities.find(e => e.ID === id))
155
+ .map(id => includedEntities.find(e => UUIDsEqual(e.ID, id)))
111
156
  .filter(e => e !== undefined);
112
157
  // Generate SQL for entities that don't need cascade delete regeneration
113
158
  const genResult = await this.generateAndExecuteEntitySQLToSeparateFiles({
@@ -208,6 +253,17 @@ export class SQLCodeGenBase {
208
253
  succeedSpinner('Entity fields metadata updated');
209
254
  }
210
255
  // no logStatus/timer for this because manageEntityFields() has its own internal logging for this including the total, so it is redundant to log it here
256
+ // STEP 3.5 - Clean up stale entity relationships now that entity fields are fully synced.
257
+ // This must run AFTER manageEntityFields deletes stale fields (for dropped columns),
258
+ // otherwise the stale EntityField records make the relationships appear valid.
259
+ startSpinner('Cleaning up stale entity relationships...');
260
+ if (!await manageMD.cleanupStaleEntityRelationships(pool, configInfo.excludeSchemas)) {
261
+ failSpinner('Failed to clean up stale entity relationships');
262
+ overallSuccess = false;
263
+ }
264
+ else {
265
+ succeedSpinner('Stale entity relationships cleaned up');
266
+ }
211
267
  // STEP 4- Apply permissions, executing all .permissions files
212
268
  startSpinner('Applying permissions...');
213
269
  const step4StartTime = new Date();
@@ -278,7 +334,7 @@ export class SQLCodeGenBase {
278
334
  const fileBuffer = fs.readFileSync(fullPath);
279
335
  const fileContents = fileBuffer.toString();
280
336
  try {
281
- await pool.request().query(fileContents);
337
+ await pool.query(fileContents);
282
338
  }
283
339
  catch (e) {
284
340
  logError(`Error executing permissions file ${fullPath} for entity ${e.Name}: ${e}`);
@@ -447,7 +503,8 @@ export class SQLCodeGenBase {
447
503
  }
448
504
  try {
449
505
  const viewName = entity.BaseView ? entity.BaseView : `vw${entity.CodeName}`;
450
- const result = await pool.request().query(`SELECT OBJECT_DEFINITION(OBJECT_ID('[${entity.SchemaName}].[${viewName}]')) AS ViewDefinition`);
506
+ const viewDefSQL = this._dbProvider.getViewDefinitionSQL(entity.SchemaName, viewName);
507
+ const result = await pool.query(viewDefSQL);
451
508
  const dbDefinition = result.recordset?.[0]?.ViewDefinition;
452
509
  if (!dbDefinition) {
453
510
  // View doesn't exist in DB yet — it's new, will be handled by existing new entity logic
@@ -482,7 +539,8 @@ export class SQLCodeGenBase {
482
539
  // Start from the SELECT keyword
483
540
  let body = viewSQL.substring(asMatch.index + asMatch[0].indexOf('SELECT'));
484
541
  // Trim trailing GO and anything after it (permissions, etc.)
485
- const goMatch = body.match(/\r?\nGO\b/i);
542
+ const batchSep = this._dbProvider.BatchSeparator;
543
+ const goMatch = body.match(new RegExp(`\\r?\\n${batchSep}\\b`, 'i'));
486
544
  if (goMatch && goMatch.index !== undefined) {
487
545
  body = body.substring(0, goMatch.index);
488
546
  }
@@ -573,7 +631,7 @@ export class SQLCodeGenBase {
573
631
  this.logSQLForNewOrModifiedEntity(options.entity, s, 'Index for Foreign Keys for ' + options.entity.BaseTable, options.enableSQLLoggingForNewOrModifiedEntities);
574
632
  files.push(filePath);
575
633
  }
576
- sRet += s + '\nGO\n';
634
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
577
635
  }
578
636
  // BASE VIEW AND RELATED TVFs
579
637
  // Only generate if BaseViewGenerated is true (respects custom views where it's false)
@@ -597,7 +655,7 @@ export class SQLCodeGenBase {
597
655
  files.push(filePath);
598
656
  }
599
657
  // Add function SQL to output BEFORE the view
600
- sRet += s + '\nGO\n';
658
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
601
659
  }
602
660
  }
603
661
  // Generate the base view (which may reference the TVFs created above)
@@ -612,12 +670,12 @@ export class SQLCodeGenBase {
612
670
  this.logSQLForNewOrModifiedEntity(options.entity, s, `Base View SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
613
671
  files.push(filePath);
614
672
  }
615
- sRet += s + '\nGO\n';
673
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
616
674
  }
617
675
  // always generate permissions for the base view
618
676
  const s = this.generateSingleEntitySQLFileHeader(options.entity, 'Permissions for ' + options.entity.BaseView) + this.generateViewPermissions(options.entity);
619
677
  if (s.length > 0)
620
- permissionsSQL += s + '\nGO\n';
678
+ permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
621
679
  if (options.writeFiles) {
622
680
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('view', options.entity.SchemaName, options.entity.BaseView, true, true));
623
681
  this.writeFileIfChanged(filePath, s);
@@ -627,7 +685,7 @@ export class SQLCodeGenBase {
627
685
  // now, append the permissions to the return string IF we did NOT generate the base view - because if we generated the base view, that
628
686
  // means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
629
687
  if (!options.entity.BaseViewGenerated)
630
- sRet += s + '\nGO\n';
688
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
631
689
  // CREATE SP
632
690
  if (options.entity.AllowCreateAPI && !options.entity.VirtualEntity) {
633
691
  const spName = this.getSPName(options.entity, SPType.Create);
@@ -642,11 +700,11 @@ export class SQLCodeGenBase {
642
700
  this.logSQLForNewOrModifiedEntity(options.entity, s, `spCreate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
643
701
  files.push(filePath);
644
702
  }
645
- sRet += s + '\nGO\n';
703
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
646
704
  }
647
705
  const s = this.generateSPPermissions(options.entity, spName, SPType.Create) + '\n\n';
648
706
  if (s.length > 0)
649
- permissionsSQL += s + '\nGO\n';
707
+ permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
650
708
  if (options.writeFiles) {
651
709
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
652
710
  this.writeFileIfChanged(filePath, s);
@@ -656,7 +714,7 @@ export class SQLCodeGenBase {
656
714
  // now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
657
715
  // means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
658
716
  if (!options.entity.spCreateGenerated)
659
- sRet += s + '\nGO\n';
717
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
660
718
  }
661
719
  // UPDATE SP
662
720
  if (options.entity.AllowUpdateAPI && !options.entity.VirtualEntity) {
@@ -672,11 +730,11 @@ export class SQLCodeGenBase {
672
730
  this.logSQLForNewOrModifiedEntity(options.entity, s, `spUpdate SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
673
731
  files.push(filePath);
674
732
  }
675
- sRet += s + '\nGO\n';
733
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
676
734
  }
677
735
  const s = this.generateSPPermissions(options.entity, spName, SPType.Update) + '\n\n';
678
736
  if (s.length > 0)
679
- permissionsSQL += s + '\nGO\n';
737
+ permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
680
738
  if (options.writeFiles) {
681
739
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
682
740
  this.writeFileIfChanged(filePath, s);
@@ -686,7 +744,7 @@ export class SQLCodeGenBase {
686
744
  // now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
687
745
  // means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
688
746
  if (!options.entity.spUpdateGenerated)
689
- sRet += s + '\nGO\n';
747
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
690
748
  }
691
749
  // DELETE SP
692
750
  if (options.entity.AllowDeleteAPI && !options.entity.VirtualEntity) {
@@ -708,11 +766,11 @@ export class SQLCodeGenBase {
708
766
  this.logSQLForNewOrModifiedEntity(options.entity, s, `spDelete SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities, baseViewChanged);
709
767
  files.push(filePath);
710
768
  }
711
- sRet += s + '\nGO\n';
769
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
712
770
  }
713
771
  const s = this.generateSPPermissions(options.entity, spName, SPType.Delete) + '\n\n';
714
772
  if (s.length > 0)
715
- permissionsSQL += s + '\nGO\n';
773
+ permissionsSQL += s + '\n' + this._dbProvider.BatchSeparator + '\n';
716
774
  if (options.writeFiles) {
717
775
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('sp', options.entity.SchemaName, spName, true, true));
718
776
  this.writeFileIfChanged(filePath, s);
@@ -722,7 +780,7 @@ export class SQLCodeGenBase {
722
780
  // now, append the permissions to the return string IF we did NOT generate the proc - because if we generated the proc, that
723
781
  // means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
724
782
  if (!options.entity.spDeleteGenerated)
725
- sRet += s + '\nGO\n';
783
+ sRet += s + '\n' + this._dbProvider.BatchSeparator + '\n';
726
784
  }
727
785
  // check to see if the options.entity supports full text search or not
728
786
  if (options.entity.FullTextSearchEnabled || (configInfo.forceRegeneration?.enabled && configInfo.forceRegeneration?.fullTextSearch)) {
@@ -736,11 +794,11 @@ export class SQLCodeGenBase {
736
794
  this.logSQLForNewOrModifiedEntity(options.entity, ft.sql, `Full Text Search SQL for ${options.entity.Name}`, options.enableSQLLoggingForNewOrModifiedEntities);
737
795
  files.push(filePath);
738
796
  }
739
- sRet += ft.sql + '\nGO\n';
797
+ sRet += ft.sql + '\n' + this._dbProvider.BatchSeparator + '\n';
740
798
  }
741
799
  const sP = this.generateFullTextSearchFunctionPermissions(options.entity, ft.functionName) + '\n\n';
742
800
  if (sP.length > 0)
743
- permissionsSQL += sP + '\nGO\n';
801
+ permissionsSQL += sP + '\n' + this._dbProvider.BatchSeparator + '\n';
744
802
  const filePath = path.join(options.directory, this.SQLUtilityObject.getDBObjectFileName('full_text_search_function', options.entity.SchemaName, options.entity.BaseTable, true, true));
745
803
  if (options.writeFiles) {
746
804
  this.writeFileIfChanged(filePath, sP);
@@ -750,7 +808,7 @@ export class SQLCodeGenBase {
750
808
  // now, append the permissions to the return string IF we did NOT generate the function - because if we generated the function, that
751
809
  // means we already generated the permissions for it above and it is part of sRet already, but we always save it to a file, (per above line)
752
810
  if (!options.entity.FullTextSearchFunctionGenerated)
753
- sRet += sP + '\nGO\n';
811
+ sRet += sP + '\n' + this._dbProvider.BatchSeparator + '\n';
754
812
  }
755
813
  return { sql: sRet, permissionsSQL: permissionsSQL, files: files };
756
814
  }
@@ -760,14 +818,7 @@ export class SQLCodeGenBase {
760
818
  }
761
819
  }
762
820
  getSPName(entity, type) {
763
- switch (type) {
764
- case SPType.Create:
765
- return entity.spCreate && entity.spCreate.length > 0 ? entity.spCreate : 'spCreate' + entity.BaseTableCodeName;
766
- case SPType.Update:
767
- return entity.spUpdate && entity.spUpdate.length > 0 ? entity.spUpdate : 'spUpdate' + entity.BaseTableCodeName;
768
- case SPType.Delete:
769
- return entity.spDelete && entity.spDelete.length > 0 ? entity.spDelete : 'spDelete' + entity.BaseTableCodeName;
770
- }
821
+ return this._dbProvider.getCRUDRoutineName(entity, type);
771
822
  }
772
823
  getEntityPermissionFileNames(entity) {
773
824
  const files = [];
@@ -833,108 +884,43 @@ export class SQLCodeGenBase {
833
884
  return sOutput;
834
885
  }
835
886
  async generateEntityFullTextSearchSQL(pool, entity) {
836
- let sql = '';
837
- const catalogName = entity.FullTextCatalog && entity.FullTextCatalog.length > 0 ? entity.FullTextCatalog : dbDatabase + '_FullTextCatalog';
838
- if (entity.FullTextCatalogGenerated) {
839
- // this situation means we have a generated catalog and the user has provided a name specific to THIS entity
840
- sql += ` -- CREATE THE FULL TEXT CATALOG FOR THE ENTITY, IF NOT ALREADY CREATED
841
- IF NOT EXISTS (
842
- SELECT *
843
- FROM sys.fulltext_catalogs
844
- WHERE name = '${catalogName}'
845
- )
846
- CREATE FULLTEXT CATALOG ${catalogName};
847
- GO
848
- `;
849
- }
887
+ const searchFields = entity.Fields.filter(f => f.FullTextSearchEnabled);
888
+ // Validate search fields exist when needed
889
+ if (entity.FullTextIndexGenerated && searchFields.length === 0)
890
+ throw new Error(`FullTextIndexGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
891
+ if (entity.FullTextSearchFunctionGenerated && searchFields.length === 0)
892
+ throw new Error(`FullTextSearchFunctionGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
893
+ // Get the primary key index name (needed for full text index)
894
+ let primaryKeyIndexName = '';
850
895
  if (entity.FullTextIndexGenerated) {
851
- const fullTextFields = entity.Fields.filter(f => f.FullTextSearchEnabled).map(f => `${f.Name} LANGUAGE 'English'`).join(', ');
852
- if (fullTextFields.length === 0)
853
- throw new Error(`FullTextIndexGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
854
- // drop and recreate the full text index
855
- const entity_pk_name = await this.getEntityPrimaryKeyIndexName(pool, entity);
856
- sql += ` -- DROP AND RECREATE THE FULL TEXT INDEX
857
- IF EXISTS (
858
- SELECT *
859
- FROM sys.fulltext_indexes
860
- WHERE object_id = OBJECT_ID('${entity.SchemaName}.${entity.BaseTable}')
861
- )
862
- BEGIN
863
- DROP FULLTEXT INDEX ON [${entity.SchemaName}].[${entity.BaseTable}];
864
- END
865
- GO
866
-
867
- IF NOT EXISTS (
868
- SELECT *
869
- FROM sys.fulltext_indexes
870
- WHERE object_id = OBJECT_ID('${entity.SchemaName}.${entity.BaseTable}')
871
- )
872
- BEGIN
873
- CREATE FULLTEXT INDEX ON [${entity.SchemaName}].[${entity.BaseTable}]
874
- (
875
- ${fullTextFields}
876
- )
877
- KEY INDEX ${entity_pk_name}
878
- ON ${catalogName};
879
- END
880
- GO
881
- `;
896
+ primaryKeyIndexName = await this.getEntityPrimaryKeyIndexName(pool, entity);
882
897
  }
883
- const functionName = entity.FullTextSearchFunction && entity.FullTextSearchFunction.length > 0 ? entity.FullTextSearchFunction : `fnSearch${entity.CodeName}`;
898
+ // If the function name is not set, save it to the DB
899
+ const functionName = entity.FullTextSearchFunction && entity.FullTextSearchFunction.length > 0
900
+ ? entity.FullTextSearchFunction
901
+ : `fnSearch${entity.CodeName}`;
902
+ if (entity.FullTextSearchFunctionGenerated && (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0)) {
903
+ const md = new Metadata();
904
+ const u = UserCache.Instance.Users[0];
905
+ if (!u)
906
+ throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
907
+ const e = await md.GetEntityObject('MJ: Entities', u);
908
+ await e.Load(entity.ID);
909
+ e.FullTextSearchFunction = functionName;
910
+ if (!await e.Save())
911
+ throw new Error(`Could not update the FullTextSearchFunction for entity ${entity.Name}`);
912
+ }
913
+ // Delegate SQL generation to the provider
914
+ const result = this._dbProvider.generateFullTextSearch(entity, searchFields, primaryKeyIndexName);
915
+ // Add permissions if function was generated
884
916
  if (entity.FullTextSearchFunctionGenerated) {
885
- const fullTextFieldsSimple = entity.Fields.filter(f => f.FullTextSearchEnabled).map(f => '[' + f.Name + ']').join(', ');
886
- if (fullTextFieldsSimple.length === 0)
887
- throw new Error(`FullTextSearchFunctionGenerated is true for entity ${entity.Name}, but no fields are marked as FullTextSearchEnabled`);
888
- if (!entity.FullTextSearchFunction || entity.FullTextSearchFunction.length === 0) {
889
- // update this in the DB
890
- const md = new Metadata();
891
- const u = UserCache.Instance.Users[0];
892
- if (!u)
893
- throw new Error('Could not find the first user in the cache, cant generate the full text search function without a user');
894
- const e = await md.GetEntityObject('MJ: Entities', u);
895
- await e.Load(entity.ID);
896
- e.FullTextSearchFunction = functionName;
897
- if (!await e.Save())
898
- throw new Error(`Could not update the FullTextSearchFunction for entity ${entity.Name}`);
899
- }
900
- const pkeyList = entity.PrimaryKeys.map(pk => '[' + pk.Name + ']').join(', ');
901
- // drop and recreate the full text search function
902
- sql += ` -- DROP AND RECREATE THE FULL TEXT SEARCH FUNCTION
903
- -- Create an inline table-valued function to perform full-text search
904
- -- Drop the function if it already exists
905
- IF OBJECT_ID('${entity.SchemaName}.${functionName}', 'IF') IS NOT NULL
906
- DROP FUNCTION ${entity.SchemaName}.${functionName};
907
- GO
908
- CREATE FUNCTION ${entity.SchemaName}.${functionName} (@searchTerm NVARCHAR(255))
909
- RETURNS TABLE
910
- AS
911
- RETURN (
912
- SELECT ${pkeyList}
913
- FROM [${entity.SchemaName}].[${entity.BaseTable}]
914
- WHERE CONTAINS((${fullTextFieldsSimple}), @searchTerm)
915
- )
916
- GO
917
- `;
918
- sql += this.generateFullTextSearchFunctionPermissions(entity, functionName) + '\n\nGO\n';
917
+ result.sql += this.generateFullTextSearchFunctionPermissions(entity, functionName) + '\n\n' + this._dbProvider.BatchSeparator + '\n';
919
918
  }
920
- return { sql, functionName };
919
+ return result;
921
920
  }
922
921
  async getEntityPrimaryKeyIndexName(pool, entity) {
923
- const sSQL = ` SELECT
924
- i.name AS IndexName
925
- FROM
926
- sys.indexes i
927
- INNER JOIN
928
- sys.objects o ON i.object_id = o.object_id
929
- INNER JOIN
930
- sys.key_constraints kc ON i.object_id = kc.parent_object_id AND
931
- i.index_id = kc.unique_index_id
932
- WHERE
933
- o.name = '${entity.BaseTable}' AND
934
- o.schema_id = SCHEMA_ID('${entity.SchemaName}') AND
935
- kc.type = 'PK';
936
- `;
937
- const resultResult = await pool.request().query(sSQL);
922
+ const sSQL = this._dbProvider.getPrimaryKeyIndexNameSQL(entity.SchemaName, entity.BaseTable);
923
+ const resultResult = await pool.query(sSQL);
938
924
  const result = resultResult.recordset;
939
925
  if (result && result.length > 0)
940
926
  return result[0].IndexName;
@@ -942,56 +928,18 @@ export class SQLCodeGenBase {
942
928
  throw new Error(`Could not find primary key index for entity ${entity.Name}`);
943
929
  }
944
930
  generateAllEntitiesSQLFileHeader() {
945
- return `-----------------------------------------------------------------
946
- -- SQL Code Generation for Entities
947
- --
948
- -- This file contains the SQL code for the entities in the database
949
- -- that are included in the API and have generated SQL elements like views and
950
- -- stored procedures.
951
- --
952
- -- It is generated by the MemberJunction CodeGen tool.
953
- -- It is not intended to be edited by hand.
954
- -----------------------------------------------------------------
955
- `;
931
+ return this._dbProvider.generateAllEntitiesSQLFileHeader();
956
932
  }
957
933
  generateSingleEntitySQLFileHeader(entity, itemName) {
958
- return `-----------------------------------------------------------------
959
- -- SQL Code Generation
960
- -- Entity: ${entity.Name}
961
- -- Item: ${itemName}
962
- --
963
- -- This was generated by the MemberJunction CodeGen tool.
964
- -- This file should NOT be edited by hand.
965
- -----------------------------------------------------------------
966
- `;
934
+ return this._dbProvider.generateSQLFileHeader(entity, itemName);
967
935
  }
968
936
  /**
969
- * Generates the SQL for creating indexes for the foreign keys in the entity
970
- * @param pool
971
- * @param entity
937
+ * Generates the SQL for creating indexes for the foreign keys in the entity.
938
+ * Delegates to the database provider for platform-specific index DDL.
972
939
  */
973
940
  generateIndexesForForeignKeys(pool, entity) {
974
- // iterate through all of the fields in the entity that are foreign keys and generate an index for each one
975
- let sOutput = '';
976
- for (const f of entity.Fields) {
977
- if (f.RelatedEntity && f.RelatedEntity.length > 0) {
978
- // we have an fkey, so generate the create index
979
- let indexName = `IDX_AUTO_MJ_FKEY_${entity.BaseTableCodeName}_${f.CodeName}`; // use code names in case the table and/or field names have special characters or spaces/etc
980
- if (indexName.length > MAX_INDEX_NAME_LENGTH)
981
- indexName = indexName.substring(0, MAX_INDEX_NAME_LENGTH); // truncate to max length if necessary
982
- if (sOutput.length > 0)
983
- sOutput += '\n\n'; // do this way so we don't end up with a trailing newline at end of the string/file
984
- sOutput += `-- Index for foreign key ${f.Name} in table ${entity.BaseTable}
985
- IF NOT EXISTS (
986
- SELECT 1
987
- FROM sys.indexes
988
- WHERE name = '${indexName}'
989
- AND object_id = OBJECT_ID('[${entity.SchemaName}].[${entity.BaseTable}]')
990
- )
991
- CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.Name}]);`;
992
- }
993
- }
994
- return sOutput;
941
+ const indexStatements = this._dbProvider.generateForeignKeyIndexes(entity);
942
+ return indexStatements.join('\n\n');
995
943
  }
996
944
  /**
997
945
  * Detects self-referential foreign keys in an entity (e.g., ParentTaskID pointing back to Task table)
@@ -999,7 +947,7 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
999
947
  */
1000
948
  detectRecursiveForeignKeys(entity) {
1001
949
  return entity.Fields.filter(field => field.RelatedEntityID != null &&
1002
- field.RelatedEntityID === entity.ID);
950
+ UUIDsEqual(field.RelatedEntityID, entity.ID));
1003
951
  }
1004
952
  /**
1005
953
  * Generates an inline Table Value Function for calculating root ID for a recursive FK field
@@ -1007,67 +955,7 @@ CREATE INDEX ${indexName} ON [${entity.SchemaName}].[${entity.BaseTable}] ([${f.
1007
955
  * Returns SQL to create the function including DROP IF EXISTS
1008
956
  */
1009
957
  generateRootIDFunction(entity, field) {
1010
- const primaryKey = entity.FirstPrimaryKey.Name;
1011
- const primaryKeyType = entity.FirstPrimaryKey.SQLFullType;
1012
- const schemaName = entity.SchemaName;
1013
- const tableName = entity.BaseTable;
1014
- const fieldName = field.Name;
1015
- const functionName = `fn${entity.BaseTable}${fieldName}_GetRootID`;
1016
- return `------------------------------------------------------------
1017
- ----- ROOT ID FUNCTION FOR: [${tableName}].[${fieldName}]
1018
- ------------------------------------------------------------
1019
- IF OBJECT_ID('[${schemaName}].[${functionName}]', 'IF') IS NOT NULL
1020
- DROP FUNCTION [${schemaName}].[${functionName}];
1021
- GO
1022
-
1023
- CREATE FUNCTION [${schemaName}].[${functionName}]
1024
- (
1025
- @RecordID ${primaryKeyType},
1026
- @ParentID ${primaryKeyType}
1027
- )
1028
- RETURNS TABLE
1029
- AS
1030
- RETURN
1031
- (
1032
- WITH CTE_RootParent AS (
1033
- -- Anchor: Start from @ParentID if not null, otherwise start from @RecordID
1034
- SELECT
1035
- [${primaryKey}],
1036
- [${fieldName}],
1037
- [${primaryKey}] AS [RootParentID],
1038
- 0 AS [Depth]
1039
- FROM
1040
- [${schemaName}].[${tableName}]
1041
- WHERE
1042
- [${primaryKey}] = COALESCE(@ParentID, @RecordID)
1043
-
1044
- UNION ALL
1045
-
1046
- -- Recursive: Keep going up the hierarchy until ${fieldName} is NULL
1047
- -- Includes depth counter to prevent infinite loops from circular references
1048
- SELECT
1049
- c.[${primaryKey}],
1050
- c.[${fieldName}],
1051
- c.[${primaryKey}] AS [RootParentID],
1052
- p.[Depth] + 1 AS [Depth]
1053
- FROM
1054
- [${schemaName}].[${tableName}] c
1055
- INNER JOIN
1056
- CTE_RootParent p ON c.[${primaryKey}] = p.[${fieldName}]
1057
- WHERE
1058
- p.[Depth] < 100 -- Prevent infinite loops, max 100 levels
1059
- )
1060
- SELECT TOP 1
1061
- [RootParentID] AS RootID
1062
- FROM
1063
- CTE_RootParent
1064
- WHERE
1065
- [${fieldName}] IS NULL
1066
- ORDER BY
1067
- [RootParentID]
1068
- );
1069
- GO
1070
- `;
958
+ return this._dbProvider.generateRootIDFunction(entity, field);
1071
959
  }
1072
960
  /**
1073
961
  * Generates all inline Table Value Functions for an entity's recursive foreign keys
@@ -1085,30 +973,33 @@ GO
1085
973
  * Generates the SELECT clause additions for root fields from TVFs
1086
974
  * Example: , root_ParentID.RootID AS [RootParentID]
1087
975
  */
1088
- generateRootFieldSelects(recursiveFKs, classNameFirstChar) {
1089
- return recursiveFKs.map(field => {
976
+ generateRootFieldSelects(entity, recursiveFKs, classNameFirstChar) {
977
+ if (recursiveFKs.length === 0)
978
+ return '';
979
+ let sOutput = '';
980
+ for (let i = 0; i < recursiveFKs.length; i++) {
981
+ const field = recursiveFKs[i];
1090
982
  const alias = `root_${field.Name}`;
1091
- const columnName = `Root${field.Name}`;
1092
- return `,\n ${alias}.RootID AS [${columnName}]`;
1093
- }).join('');
983
+ sOutput += `,\n ${this._dbProvider.generateRootFieldSelect(entity, field, alias)}`;
984
+ }
985
+ return sOutput;
1094
986
  }
1095
987
  /**
1096
988
  * Generates OUTER APPLY joins to inline Table Value Functions for root ID calculation
1097
989
  * Each recursive FK gets an OUTER APPLY that calls its corresponding function
1098
990
  */
1099
991
  generateRootIDJoins(recursiveFKs, classNameFirstChar, entity) {
1100
- if (recursiveFKs.length === 0) {
992
+ if (recursiveFKs.length === 0)
1101
993
  return '';
1102
- }
1103
- const primaryKey = entity.FirstPrimaryKey.Name;
1104
- const schemaName = entity.SchemaName;
1105
- const tableName = entity.BaseTable;
1106
- const joins = recursiveFKs.map(field => {
1107
- const functionName = `fn${tableName}${field.Name}_GetRootID`;
994
+ let sOutput = '';
995
+ for (let i = 0; i < recursiveFKs.length; i++) {
996
+ const field = recursiveFKs[i];
1108
997
  const alias = `root_${field.Name}`;
1109
- return `OUTER APPLY\n [${schemaName}].[${functionName}]([${classNameFirstChar}].[${primaryKey}], [${classNameFirstChar}].[${field.Name}]) AS ${alias}`;
1110
- }).join('\n');
1111
- return '\n' + joins;
998
+ if (sOutput.length > 0)
999
+ sOutput += '\n';
1000
+ sOutput += this._dbProvider.generateRootFieldJoin(entity, field, alias);
1001
+ }
1002
+ return '\n' + sOutput;
1112
1003
  }
1113
1004
  /**
1114
1005
  * @deprecated Use generateRootIDJoins instead - kept for backward compatibility during migration
@@ -1137,7 +1028,7 @@ GO
1137
1028
  // Skip PKs (shared with child), timestamps, and virtual fields (view-computed)
1138
1029
  if (field.IsPrimaryKey || field.Name.startsWith('__mj_') || field.IsVirtual)
1139
1030
  continue;
1140
- fieldExpressions.push(` ${alias}.[${field.Name}]`);
1031
+ fieldExpressions.push(` ${alias}.${this._dbProvider.Dialect.QuoteIdentifier(field.Name)}`);
1141
1032
  }
1142
1033
  }
1143
1034
  if (fieldExpressions.length === 0)
@@ -1162,8 +1053,10 @@ GO
1162
1053
  // First parent joins to child table alias; deeper parents chain to previous parent alias
1163
1054
  const sourceAlias = i === 0 ? classNameFirstChar : `__mj_isa_p${i}`;
1164
1055
  // Build PK-to-PK join condition (supports composite keys)
1165
- const joinConditions = entity.PrimaryKeys.map(pk => `[${sourceAlias}].[${pk.Name}] = ${parentAlias}.[${pk.Name}]`).join(' AND ');
1166
- joins.push(`INNER JOIN\n [${parent.SchemaName}].[${parent.BaseTable}] AS ${parentAlias}\n ON\n ${joinConditions}`);
1056
+ const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
1057
+ const joinConditions = entity.PrimaryKeys.map(pk => `${qi(sourceAlias)}.${qi(pk.Name)} = ${parentAlias}.${qi(pk.Name)}`).join(' AND ');
1058
+ const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
1059
+ joins.push(`INNER JOIN\n ${qs(parent.SchemaName, parent.BaseTable)} AS ${parentAlias}\n ON\n ${joinConditions}`);
1167
1060
  }
1168
1061
  return joins.join('\n');
1169
1062
  }
@@ -1173,45 +1066,30 @@ GO
1173
1066
  const relatedFieldsString = await this.generateBaseViewRelatedFieldsString(pool, entity.Fields);
1174
1067
  const relatedFieldsJoinString = this.generateBaseViewJoins(entity, entity.Fields);
1175
1068
  const permissions = this.generateViewPermissions(entity);
1069
+ const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
1176
1070
  const whereClause = entity.DeleteType === 'Soft' ? `WHERE
1177
- ${classNameFirstChar}.[${EntityInfo.DeletedAtFieldName}] IS NULL
1071
+ ${qi(classNameFirstChar)}.${qi(EntityInfo.DeletedAtFieldName)} IS NULL
1178
1072
  ` : '';
1179
1073
  // Detect recursive foreign keys and generate TVF joins and root field selects
1180
1074
  const recursiveFKs = this.detectRecursiveForeignKeys(entity);
1181
- const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(recursiveFKs, classNameFirstChar) : '';
1075
+ const rootFields = recursiveFKs.length > 0 ? this.generateRootFieldSelects(entity, recursiveFKs, classNameFirstChar) : '';
1182
1076
  const rootJoins = recursiveFKs.length > 0 ? this.generateRootIDJoins(recursiveFKs, classNameFirstChar, entity) : '';
1183
1077
  // IS-A parent entity JOINs — walk ParentID chain, JOIN to each parent's base table
1184
1078
  const parentFieldsString = this.generateParentEntityFieldSelects(entity);
1185
1079
  const parentJoinsString = this.generateParentEntityJoins(entity, classNameFirstChar);
1186
- return `
1187
- ------------------------------------------------------------
1188
- ----- BASE VIEW FOR ENTITY: ${entity.Name}
1189
- ----- SCHEMA: ${entity.SchemaName}
1190
- ----- BASE TABLE: ${entity.BaseTable}
1191
- ----- PRIMARY KEY: ${entity.PrimaryKeys.map(pk => pk.Name).join(', ')}
1192
- ------------------------------------------------------------
1193
- IF OBJECT_ID('[${entity.SchemaName}].[${viewName}]', 'V') IS NOT NULL
1194
- DROP VIEW [${entity.SchemaName}].[${viewName}];
1195
- GO
1196
-
1197
- CREATE VIEW [${entity.SchemaName}].[${viewName}]
1198
- AS
1199
- SELECT
1200
- ${classNameFirstChar}.*${parentFieldsString}${relatedFieldsString.length > 0 ? ',' : ''}${relatedFieldsString}${rootFields}
1201
- FROM
1202
- [${entity.SchemaName}].[${entity.BaseTable}] AS ${classNameFirstChar}${parentJoinsString ? '\n' + parentJoinsString : ''}${relatedFieldsJoinString ? '\n' + relatedFieldsJoinString : ''}${rootJoins}
1203
- ${whereClause}GO${permissions}
1204
- `;
1080
+ const context = {
1081
+ entity,
1082
+ relatedFieldsSelect: relatedFieldsString,
1083
+ relatedFieldsJoins: relatedFieldsJoinString,
1084
+ parentFieldsSelect: parentFieldsString,
1085
+ parentJoins: parentJoinsString,
1086
+ rootFieldsSelect: rootFields,
1087
+ rootJoins: rootJoins,
1088
+ };
1089
+ return this._dbProvider.generateBaseView(context) + permissions;
1205
1090
  }
1206
1091
  generateViewPermissions(entity) {
1207
- let sOutput = '';
1208
- for (let i = 0; i < entity.Permissions.length; i++) {
1209
- const ep = entity.Permissions[i];
1210
- if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
1211
- sOutput += (sOutput === '' ? `GRANT SELECT ON [${entity.SchemaName}].[${entity.BaseView}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
1212
- }
1213
- }
1214
- return (sOutput == '' ? '' : '\n') + sOutput;
1092
+ return this._dbProvider.generateViewPermissions(entity);
1215
1093
  }
1216
1094
  generateBaseViewJoins(entity, entityFields) {
1217
1095
  let sOutput = '';
@@ -1225,7 +1103,10 @@ ${whereClause}GO${permissions}
1225
1103
  // This generates the JOIN clause; the actual field aliases are added separately in generateBaseViewFields()
1226
1104
  if (ef.RelatedEntityID && ef._RelatedEntityJoinFieldMappings && ef._RelatedEntityJoinFieldMappings.length > 0 && ef._RelatedEntityTableAlias) {
1227
1105
  sOutput += sOutput == '' ? '' : '\n';
1228
- sOutput += `${ef.AllowsNull ? 'LEFT OUTER' : 'INNER'} JOIN\n ${'[' + ef.RelatedEntitySchemaName + '].'}[${ef._RelatedEntityNameFieldIsVirtual ? ef.RelatedEntityBaseView : ef.RelatedEntityBaseTable}] AS ${ef._RelatedEntityTableAlias}\n ON\n [${classNameFirstChar}].[${ef.Name}] = ${ef._RelatedEntityTableAlias}.[${ef.RelatedEntityFieldName}]`;
1106
+ const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
1107
+ const qs = this._dbProvider.Dialect.QuoteSchema.bind(this._dbProvider.Dialect);
1108
+ const relatedTable = ef._RelatedEntityNameFieldIsVirtual ? ef.RelatedEntityBaseView : ef.RelatedEntityBaseTable;
1109
+ sOutput += `${ef.AllowsNull ? 'LEFT OUTER' : 'INNER'} JOIN\n ${qs(ef.RelatedEntitySchemaName, relatedTable)} AS ${ef._RelatedEntityTableAlias}\n ON\n ${qi(classNameFirstChar)}.${qi(ef.Name)} = ${ef._RelatedEntityTableAlias}.${qi(ef.RelatedEntityFieldName)}`;
1229
1110
  }
1230
1111
  }
1231
1112
  return sOutput;
@@ -1302,7 +1183,7 @@ ${whereClause}GO${permissions}
1302
1183
  }
1303
1184
  // 2. Handle configured additional fields
1304
1185
  if (config.fields && config.fields.length > 0) {
1305
- const currentEntity = md.Entities.find(e => e.ID === ef.EntityID);
1186
+ const currentEntity = md.Entities.find(e => UUIDsEqual(e.ID, ef.EntityID));
1306
1187
  for (const fieldConfig of config.fields) {
1307
1188
  const fieldName = fieldConfig.field;
1308
1189
  const alias = fieldConfig.alias || this.generateDefaultAlias(ef.Name, fieldName);
@@ -1335,7 +1216,8 @@ ${whereClause}GO${permissions}
1335
1216
  ef._RelatedEntityTableAlias = ef.RelatedEntityClassName + '_' + ef.Name;
1336
1217
  ef._RelatedEntityNameFieldIsVirtual = anyFieldIsVirtual;
1337
1218
  for (const mapping of ef._RelatedEntityJoinFieldMappings) {
1338
- sOutput += `${fieldCount === 0 ? '' : ','}\n ${ef._RelatedEntityTableAlias}.[${mapping.sourceField}] AS [${mapping.alias}]`;
1219
+ const qi = this._dbProvider.Dialect.QuoteIdentifier.bind(this._dbProvider.Dialect);
1220
+ sOutput += `${fieldCount === 0 ? '' : ','}\n ${ef._RelatedEntityTableAlias}.${qi(mapping.sourceField)} AS ${qi(mapping.alias)}`;
1339
1221
  fieldCount++;
1340
1222
  }
1341
1223
  }
@@ -1388,215 +1270,19 @@ ${whereClause}GO${permissions}
1388
1270
  return false;
1389
1271
  }
1390
1272
  generateSPPermissions(entity, spName, type) {
1391
- let sOutput = '';
1392
- for (let i = 0; i < entity.Permissions.length; i++) {
1393
- const ep = entity.Permissions[i];
1394
- if ((type == SPType.Create && ep.CanCreate) ||
1395
- (type == SPType.Update && ep.CanUpdate) ||
1396
- (type == SPType.Delete && ep.CanDelete)) {
1397
- if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
1398
- sOutput += (sOutput === '' ? `GRANT EXECUTE ON [${entity.SchemaName}].[${spName}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
1399
- }
1400
- }
1401
- }
1402
- return (sOutput == '' ? '' : '\n') + sOutput;
1273
+ return this._dbProvider.generateCRUDPermissions(entity, spName, type);
1403
1274
  }
1404
1275
  generateFullTextSearchFunctionPermissions(entity, functionName) {
1405
- let sOutput = '';
1406
- for (let i = 0; i < entity.Permissions.length; i++) {
1407
- const ep = entity.Permissions[i];
1408
- if (ep.CanRead) {
1409
- if (ep.RoleSQLName && ep.RoleSQLName.length > 0) {
1410
- sOutput += (sOutput === '' ? `GRANT SELECT ON [${entity.SchemaName}].[${functionName}] TO ` : ', ') + `[${ep.RoleSQLName}]`;
1411
- }
1412
- }
1413
- }
1414
- return (sOutput == '' ? '' : '\n') + sOutput;
1276
+ return this._dbProvider.generateFullTextSearchPermissions(entity, functionName);
1415
1277
  }
1416
1278
  generateSPCreate(entity) {
1417
- const spName = entity.spCreate ? entity.spCreate : `spCreate${entity.BaseTableCodeName}`;
1418
- const firstKey = entity.FirstPrimaryKey;
1419
- //double exclamations used on the firstKey.DefaultValue property otherwise the type of this variable is 'number | ""';
1420
- const primaryKeyAutomatic = firstKey.AutoIncrement; // Only exclude auto-increment fields, allow manual override for all other PKs including UUIDs with defaults
1421
- const efString = this.createEntityFieldsParamString(entity.Fields, false); // Always pass false for isUpdate since this is generateSPCreate
1422
- const permissions = this.generateSPPermissions(entity, spName, SPType.Create);
1423
- let preInsertCode = '';
1424
- let outputCode = '';
1425
- let selectInsertedRecord = '';
1426
- let additionalFieldList = '';
1427
- let additionalValueList = '';
1428
- if (entity.FirstPrimaryKey.AutoIncrement) {
1429
- selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = SCOPE_IDENTITY()`;
1430
- }
1431
- else if (entity.FirstPrimaryKey.Type.toLowerCase().trim() === 'uniqueidentifier' && entity.PrimaryKeys.length === 1) {
1432
- // our primary key is a uniqueidentifier. Now we support optional override:
1433
- // - If PKEY is provided (not NULL), use it
1434
- // - If PKEY is NULL and there's a default value, let the database use it
1435
- // - If PKEY is NULL and no default value, generate NEWID()
1436
- const hasDefaultValue = entity.FirstPrimaryKey.DefaultValue && entity.FirstPrimaryKey.DefaultValue.trim().length > 0;
1437
- if (hasDefaultValue) {
1438
- // Has default value - use conditional logic to either include the field or let DB use default
1439
- preInsertCode = `DECLARE @InsertedRow TABLE ([${entity.FirstPrimaryKey.Name}] UNIQUEIDENTIFIER)
1440
-
1441
- IF @${entity.FirstPrimaryKey.Name} IS NOT NULL
1442
- BEGIN
1443
- -- User provided a value, use it
1444
- INSERT INTO [${entity.SchemaName}].[${entity.BaseTable}]
1445
- (
1446
- [${entity.FirstPrimaryKey.Name}],
1447
- ${this.createEntityFieldsInsertString(entity, entity.Fields, '', true)}
1448
- )
1449
- OUTPUT INSERTED.[${entity.FirstPrimaryKey.Name}] INTO @InsertedRow
1450
- VALUES
1451
- (
1452
- @${entity.FirstPrimaryKey.Name},
1453
- ${this.createEntityFieldsInsertString(entity, entity.Fields, '@', true)}
1454
- )
1455
- END
1456
- ELSE
1457
- BEGIN
1458
- -- No value provided, let database use its default (e.g., NEWSEQUENTIALID())
1459
- INSERT INTO [${entity.SchemaName}].[${entity.BaseTable}]
1460
- (
1461
- ${this.createEntityFieldsInsertString(entity, entity.Fields, '', true)}
1462
- )
1463
- OUTPUT INSERTED.[${entity.FirstPrimaryKey.Name}] INTO @InsertedRow
1464
- VALUES
1465
- (
1466
- ${this.createEntityFieldsInsertString(entity, entity.Fields, '@', true)}
1467
- )
1468
- END`;
1469
- // Clear these as we're handling the INSERT in preInsertCode
1470
- additionalFieldList = '';
1471
- additionalValueList = '';
1472
- outputCode = '';
1473
- selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = (SELECT [${entity.FirstPrimaryKey.Name}] FROM @InsertedRow)`;
1474
- }
1475
- else {
1476
- // No default value - we calculate the ID upfront, so no need for OUTPUT clause
1477
- preInsertCode = `DECLARE @ActualID UNIQUEIDENTIFIER = ISNULL(@${entity.FirstPrimaryKey.Name}, NEWID())`;
1478
- additionalFieldList = ',\n [' + entity.FirstPrimaryKey.Name + ']';
1479
- additionalValueList = ',\n @ActualID';
1480
- outputCode = ''; // No OUTPUT clause needed
1481
- // We already know the ID, so just select using it directly
1482
- selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE [${entity.FirstPrimaryKey.Name}] = @ActualID`;
1483
- }
1484
- }
1485
- else {
1486
- selectInsertedRecord = `SELECT * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE `;
1487
- let isFirst = true;
1488
- for (let k of entity.PrimaryKeys) {
1489
- if (!isFirst)
1490
- selectInsertedRecord += ' AND ';
1491
- selectInsertedRecord += `[${k.Name}] = @${k.CodeName}`;
1492
- isFirst = false;
1493
- }
1494
- }
1495
- return `
1496
- ------------------------------------------------------------
1497
- ----- CREATE PROCEDURE FOR ${entity.BaseTable}
1498
- ------------------------------------------------------------
1499
- IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1500
- DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1501
- GO
1502
-
1503
- CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
1504
- ${efString}
1505
- AS
1506
- BEGIN
1507
- SET NOCOUNT ON;
1508
- ${preInsertCode}${preInsertCode.includes('INSERT INTO') ? '' : `
1509
- INSERT INTO
1510
- [${entity.SchemaName}].[${entity.BaseTable}]
1511
- (
1512
- ${this.createEntityFieldsInsertString(entity, entity.Fields, '')}${additionalFieldList}
1513
- )
1514
- ${outputCode}VALUES
1515
- (
1516
- ${this.createEntityFieldsInsertString(entity, entity.Fields, '@')}${additionalValueList}
1517
- )`}
1518
- -- return the new record from the base view, which might have some calculated fields
1519
- ${selectInsertedRecord}
1520
- END
1521
- GO${permissions}
1522
- `;
1279
+ return this._dbProvider.generateCRUDCreate(entity);
1523
1280
  }
1524
1281
  generateUpdatedAtTrigger(entity) {
1525
- const updatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.toLowerCase().trim());
1526
- if (!updatedAtField)
1527
- return '';
1528
- const triggerStatement = `
1529
- ------------------------------------------------------------
1530
- ----- TRIGGER FOR ${EntityInfo.UpdatedAtFieldName} field for the ${entity.BaseTable} table
1531
- ------------------------------------------------------------
1532
- IF OBJECT_ID('[${entity.SchemaName}].[trgUpdate${entity.BaseTableCodeName}]', 'TR') IS NOT NULL
1533
- DROP TRIGGER [${entity.SchemaName}].[trgUpdate${entity.BaseTableCodeName}];
1534
- GO
1535
- CREATE TRIGGER [${entity.SchemaName}].trgUpdate${entity.BaseTableCodeName}
1536
- ON [${entity.SchemaName}].[${entity.BaseTable}]
1537
- AFTER UPDATE
1538
- AS
1539
- BEGIN
1540
- SET NOCOUNT ON;
1541
- UPDATE
1542
- [${entity.SchemaName}].[${entity.BaseTable}]
1543
- SET
1544
- ${EntityInfo.UpdatedAtFieldName} = GETUTCDATE()
1545
- FROM
1546
- [${entity.SchemaName}].[${entity.BaseTable}] AS _organicTable
1547
- INNER JOIN
1548
- INSERTED AS I ON
1549
- ${entity.PrimaryKeys.map(k => `_organicTable.[${k.Name}] = I.[${k.Name}]`).join(' AND ')};
1550
- END;
1551
- GO`;
1552
- return triggerStatement;
1282
+ return this._dbProvider.generateTimestampTrigger(entity);
1553
1283
  }
1554
1284
  generateSPUpdate(entity) {
1555
- const spName = entity.spUpdate ? entity.spUpdate : `spUpdate${entity.BaseTableCodeName}`;
1556
- const efParamString = this.createEntityFieldsParamString(entity.Fields, true);
1557
- const permissions = this.generateSPPermissions(entity, spName, SPType.Update);
1558
- const hasUpdatedAtField = entity.Fields.find(f => f.Name.toLowerCase().trim() === EntityInfo.UpdatedAtFieldName.trim().toLowerCase()) !== undefined;
1559
- const updatedAtTrigger = hasUpdatedAtField ? this.generateUpdatedAtTrigger(entity) : '';
1560
- let selectUpdatedRecord = `SELECT
1561
- *
1562
- FROM
1563
- [${entity.SchemaName}].[${entity.BaseView}]
1564
- WHERE
1565
- ${entity.PrimaryKeys.map(k => `[${k.Name}] = @${k.CodeName}`).join(' AND ')}
1566
- `;
1567
- return `
1568
- ------------------------------------------------------------
1569
- ----- UPDATE PROCEDURE FOR ${entity.BaseTable}
1570
- ------------------------------------------------------------
1571
- IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1572
- DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1573
- GO
1574
-
1575
- CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
1576
- ${efParamString}
1577
- AS
1578
- BEGIN
1579
- SET NOCOUNT ON;
1580
- UPDATE
1581
- [${entity.SchemaName}].[${entity.BaseTable}]
1582
- SET
1583
- ${this.createEntityFieldsUpdateString(entity.Fields)}
1584
- WHERE
1585
- ${entity.PrimaryKeys.map(k => `[${k.Name}] = @${k.CodeName}`).join(' AND ')}
1586
-
1587
- -- Check if the update was successful
1588
- IF @@ROWCOUNT = 0
1589
- -- Nothing was updated, return no rows, but column structure from base view intact, semantically correct this way.
1590
- SELECT TOP 0 * FROM [${entity.SchemaName}].[${entity.BaseView}] WHERE 1=0
1591
- ELSE
1592
- -- Return the updated record so the caller can see the updated values and any calculated fields
1593
- ${selectUpdatedRecord}
1594
- END
1595
- GO
1596
- ${permissions}
1597
- GO
1598
- ${updatedAtTrigger}
1599
- `;
1285
+ return this._dbProvider.generateCRUDUpdate(entity);
1600
1286
  }
1601
1287
  /**
1602
1288
  * Formats a default value for use in SQL, handling special cases like SQL functions
@@ -1605,209 +1291,20 @@ ${updatedAtTrigger}
1605
1291
  * @returns Properly formatted default value for SQL
1606
1292
  */
1607
1293
  formatDefaultValue(defaultValue, needsQuotes) {
1608
- if (!defaultValue || defaultValue.trim().length === 0) {
1609
- return 'NULL';
1610
- }
1611
- let trimmedValue = defaultValue.trim();
1612
- const lowerValue = trimmedValue.toLowerCase();
1613
- // SQL functions that should not be quoted
1614
- const sqlFunctions = [
1615
- 'newid()',
1616
- 'newsequentialid()',
1617
- 'getdate()',
1618
- 'getutcdate()',
1619
- 'sysdatetime()',
1620
- 'sysdatetimeoffset()',
1621
- 'current_timestamp',
1622
- 'user_name()',
1623
- 'suser_name()',
1624
- 'system_user'
1625
- ];
1626
- // Check if this is a SQL function
1627
- for (const func of sqlFunctions) {
1628
- if (lowerValue.includes(func)) {
1629
- // Remove outer parentheses if they exist (e.g., "(getutcdate())" -> "getutcdate()")
1630
- if (trimmedValue.startsWith('(') && trimmedValue.endsWith(')')) {
1631
- trimmedValue = trimmedValue.substring(1, trimmedValue.length - 1);
1632
- }
1633
- return trimmedValue;
1634
- }
1635
- }
1636
- // If the value already has quotes, remove them first
1637
- let cleanValue = trimmedValue;
1638
- if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
1639
- cleanValue = cleanValue.substring(1, cleanValue.length - 1);
1640
- }
1641
- // Add quotes if needed
1642
- if (needsQuotes) {
1643
- return `'${cleanValue}'`;
1644
- }
1645
- return cleanValue;
1294
+ return this._dbProvider.formatDefaultValue(defaultValue, needsQuotes);
1646
1295
  }
1647
1296
  createEntityFieldsParamString(entityFields, isUpdate) {
1648
- let sOutput = '', isFirst = true;
1649
- for (let i = 0; i < entityFields.length; ++i) {
1650
- const ef = entityFields[i];
1651
- const autoGeneratedPrimaryKey = ef.AutoIncrement; // Only exclude auto-increment fields from params
1652
- if ((ef.AllowUpdateAPI || (ef.IsPrimaryKey && isUpdate) || (ef.IsPrimaryKey && !autoGeneratedPrimaryKey && !isUpdate)) &&
1653
- !ef.IsVirtual &&
1654
- (!ef.IsPrimaryKey || !autoGeneratedPrimaryKey || isUpdate) &&
1655
- !ef.IsSpecialDateField) {
1656
- if (!isFirst)
1657
- sOutput += ',\n ';
1658
- else
1659
- isFirst = false;
1660
- // Check if we need a default value
1661
- let defaultParamValue = '';
1662
- if (!isUpdate && ef.IsPrimaryKey && !ef.AutoIncrement) {
1663
- // For primary keys (non-auto-increment), make them optional with NULL default
1664
- // This allows callers to omit the PK and let the DB/sproc handle it
1665
- defaultParamValue = ' = NULL';
1666
- }
1667
- else if (!isUpdate && ef.HasDefaultValue && !ef.AllowsNull) {
1668
- // For non-nullable fields with database defaults, make the parameter optional
1669
- // This allows callers to pass NULL and let the database default be used
1670
- defaultParamValue = ' = NULL';
1671
- }
1672
- sOutput += `@${ef.CodeName} ${ef.SQLFullType}${defaultParamValue}`;
1673
- }
1674
- }
1675
- return sOutput;
1297
+ return this._dbProvider.generateCRUDParamString(entityFields, isUpdate);
1676
1298
  }
1677
1299
  createEntityFieldsInsertString(entity, entityFields, prefix, excludePrimaryKey = false) {
1678
- const autoGeneratedPrimaryKey = entity.FirstPrimaryKey.AutoIncrement; // Only exclude auto-increment PKs from insert
1679
- let sOutput = '', isFirst = true;
1680
- for (let i = 0; i < entityFields.length; ++i) {
1681
- const ef = entityFields[i];
1682
- // we only want fields that are (a) not primary keys, or if a pkey, not an auto-increment pkey and (b) not virtual fields and (c) updateable fields and (d) not auto-increment fields if they're not pkeys)
1683
- // ALSO: if excludePrimaryKey is true, skip all primary key fields
1684
- if ((excludePrimaryKey && ef.IsPrimaryKey) || (ef.IsPrimaryKey && autoGeneratedPrimaryKey) || ef.IsVirtual || !ef.AllowUpdateAPI || ef.AutoIncrement) {
1685
- continue; // skip this field
1686
- }
1687
- if (!isFirst)
1688
- sOutput += ',\n ';
1689
- else
1690
- isFirst = false;
1691
- if (prefix !== '' && ef.IsSpecialDateField) {
1692
- if (ef.IsCreatedAtField || ef.IsUpdatedAtField)
1693
- sOutput += `GETUTCDATE()`; // we set the inserted row value to the current date for created and updated at fields
1694
- else
1695
- sOutput += `NULL`; // we don't set the deleted at field on an insert, only on a delete
1696
- }
1697
- else if ((prefix && prefix !== '') && !ef.IsPrimaryKey && ef.IsUniqueIdentifier && ef.HasDefaultValue && !ef.AllowsNull) {
1698
- // this is the VALUE side (prefix not null/blank), is NOT a primary key, and is a uniqueidentifier column with a default value and does NOT allow NULL
1699
- // We need to handle both NULL and the special value '00000000-0000-0000-0000-000000000000' for backward compatibility
1700
- // Existing code uses the special value to indicate "use the default", so we preserve that behavior
1701
- const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
1702
- sOutput += `CASE @${ef.CodeName} WHEN '00000000-0000-0000-0000-000000000000' THEN ${formattedDefault} ELSE ISNULL(@${ef.CodeName}, ${formattedDefault}) END`;
1703
- }
1704
- else {
1705
- let sVal = '';
1706
- if (!prefix || prefix.length === 0) {
1707
- // Column name side
1708
- sVal = '[' + ef.Name + ']'; // always put field names in brackets so that if reserved words are being used for field names in a table like "USER" and so on, they still work
1709
- }
1710
- else {
1711
- // Value/parameter side
1712
- sVal = prefix + ef.CodeName;
1713
- // If this field has a default value and doesn't allow NULL, wrap with ISNULL
1714
- // For UniqueIdentifier fields, also handle the special value '00000000-0000-0000-0000-000000000000' for backward compatibility
1715
- if (ef.HasDefaultValue && !ef.AllowsNull) {
1716
- const formattedDefault = this.formatDefaultValue(ef.DefaultValue, ef.NeedsQuotes);
1717
- if (ef.IsUniqueIdentifier) {
1718
- // Handle both NULL and the special UUID value for backward compatibility with existing code
1719
- sVal = `CASE ${sVal} WHEN '00000000-0000-0000-0000-000000000000' THEN ${formattedDefault} ELSE ISNULL(${sVal}, ${formattedDefault}) END`;
1720
- }
1721
- else {
1722
- sVal = `ISNULL(${sVal}, ${formattedDefault})`;
1723
- }
1724
- }
1725
- }
1726
- sOutput += sVal;
1727
- }
1728
- }
1729
- return sOutput;
1300
+ return this._dbProvider.generateInsertFieldString(entity, entityFields, prefix, excludePrimaryKey);
1730
1301
  }
1731
1302
  createEntityFieldsUpdateString(entityFields) {
1732
- let sOutput = '', isFirst = true;
1733
- for (let i = 0; i < entityFields.length; ++i) {
1734
- const ef = entityFields[i];
1735
- if (!ef.IsPrimaryKey &&
1736
- !ef.IsVirtual &&
1737
- ef.AllowUpdateAPI &&
1738
- !ef.AutoIncrement &&
1739
- !ef.IsSpecialDateField) {
1740
- if (!isFirst)
1741
- sOutput += ',\n ';
1742
- else
1743
- isFirst = false;
1744
- sOutput += `[${ef.Name}] = @${ef.CodeName}`; // always put field names in brackets for field names that have spaces or use reserved words. Also, we use CodeName for the param name, which is the field name unless it has spaces
1745
- }
1746
- }
1747
- return sOutput;
1303
+ return this._dbProvider.generateUpdateFieldString(entityFields);
1748
1304
  }
1749
1305
  async generateSPDelete(entity, pool) {
1750
- const spName = entity.spDelete ? entity.spDelete : `spDelete${entity.BaseTableCodeName}`;
1751
- const sCascadeDeletes = await this.generateCascadeDeletes(entity, pool);
1752
- const permissions = this.generateSPPermissions(entity, spName, SPType.Delete);
1753
- let sVariables = '';
1754
- let sSelect = '';
1755
- for (let k of entity.PrimaryKeys) {
1756
- if (sVariables !== '')
1757
- sVariables += ', ';
1758
- sVariables += `@${k.CodeName} ${k.SQLFullType}`;
1759
- if (sSelect !== '')
1760
- sSelect += ', ';
1761
- sSelect += `@${k.CodeName} AS [${k.Name}]`;
1762
- }
1763
- // next up, create the delete code which is based on the type of delete the entity is set to
1764
- // start off by creating the where clause first and then prepend the delete or update statement to it
1765
- let deleteCode = ` WHERE
1766
- ${entity.PrimaryKeys.map(k => `[${k.Name}] = @${k.CodeName}`).join(' AND ')}
1767
- `;
1768
- if (entity.DeleteType === 'Hard') {
1769
- deleteCode = ` DELETE FROM
1770
- [${entity.SchemaName}].[${entity.BaseTable}]
1771
- ${deleteCode}`;
1772
- }
1773
- else {
1774
- deleteCode = ` UPDATE
1775
- [${entity.SchemaName}].[${entity.BaseTable}]
1776
- SET
1777
- ${EntityInfo.DeletedAtFieldName} = GETUTCDATE()
1778
- ${deleteCode} AND ${EntityInfo.DeletedAtFieldName} IS NULL -- don't update the record if it's already been deleted via a soft delete`;
1779
- }
1780
- // Build the NULL select statement for when no rows are affected
1781
- let sNullSelect = '';
1782
- for (let k of entity.PrimaryKeys) {
1783
- if (sNullSelect !== '')
1784
- sNullSelect += ', ';
1785
- sNullSelect += `NULL AS [${k.Name}]`;
1786
- }
1787
- return `
1788
- ------------------------------------------------------------
1789
- ----- DELETE PROCEDURE FOR ${entity.BaseTable}
1790
- ------------------------------------------------------------
1791
- IF OBJECT_ID('[${entity.SchemaName}].[${spName}]', 'P') IS NOT NULL
1792
- DROP PROCEDURE [${entity.SchemaName}].[${spName}];
1793
- GO
1794
-
1795
- CREATE PROCEDURE [${entity.SchemaName}].[${spName}]
1796
- ${sVariables}
1797
- AS
1798
- BEGIN
1799
- SET NOCOUNT ON;${sCascadeDeletes}
1800
-
1801
- ${deleteCode}
1802
-
1803
- -- Check if the delete was successful
1804
- IF @@ROWCOUNT = 0
1805
- SELECT ${sNullSelect} -- Return NULL for all primary key fields to indicate no record was deleted
1806
- ELSE
1807
- SELECT ${sSelect} -- Return the primary key values to indicate we successfully deleted the record
1808
- END
1809
- GO${permissions}
1810
- `;
1306
+ const cascadeSQL = await this.generateCascadeDeletes(entity, pool);
1307
+ return this._dbProvider.generateCRUDDelete(entity, cascadeSQL);
1811
1308
  }
1812
1309
  async generateCascadeDeletes(entity, pool) {
1813
1310
  let sOutput = '';
@@ -1816,7 +1313,7 @@ GO${permissions}
1816
1313
  // Find all fields in other entities that are foreign keys to this entity
1817
1314
  for (const e of md.Entities) {
1818
1315
  for (const ef of e.Fields) {
1819
- if (ef.RelatedEntityID === entity.ID && ef.IsVirtual === false) {
1316
+ if (UUIDsEqual(ef.RelatedEntityID, entity.ID) && ef.IsVirtual === false) {
1820
1317
  const cascadeSql = await this.generateSingleCascadeOperation(entity, e, ef, pool);
1821
1318
  if (cascadeSql !== '') {
1822
1319
  if (sOutput !== '')
@@ -1878,129 +1375,26 @@ GO${permissions}
1878
1375
  return '';
1879
1376
  }
1880
1377
  generateCascadeCursorOperation(parentEntity, relatedEntity, fkField, operation) {
1881
- // Build the WHERE clause for matching foreign key(s)
1882
- // TODO: Future enhancement to support composite foreign keys
1883
- const whereClause = `[${fkField.CodeName}] = @${parentEntity.FirstPrimaryKey.CodeName}`;
1884
- // Generate unique cursor name using entity code name AND FK field name
1885
- // This ensures uniqueness when an entity has multiple FKs pointing to the same parent
1886
- // (e.g., AIPromptRun.ParentID and AIPromptRun.RerunFromPromptRunID both reference AIPromptRun)
1887
- const cursorName = `cascade_${operation}_${relatedEntity.CodeName}_${fkField.CodeName}_cursor`;
1888
- // Use a combined prefix that includes both entity name and FK field name to ensure unique variable names
1889
- const variablePrefix = `${relatedEntity.CodeName}_${fkField.CodeName}`;
1890
- // Determine which SP to call
1891
- const spType = operation === 'delete' ? SPType.Delete : SPType.Update;
1892
- const spName = this.getSPName(relatedEntity, spType);
1893
- if (operation === 'update') {
1894
- // For update, we need to include all updateable fields
1895
- // Use the combined prefix to ensure uniqueness across multiple FKs to same entity
1896
- const updateParams = this.buildUpdateCursorParameters(relatedEntity, fkField, variablePrefix);
1897
- const spCallParams = updateParams.allParams;
1898
- return `
1899
- -- Cascade update on ${relatedEntity.BaseTable} using cursor to call ${spName}
1900
- ${updateParams.declarations}
1901
- DECLARE ${cursorName} CURSOR FOR
1902
- SELECT ${updateParams.selectFields}
1903
- FROM [${relatedEntity.SchemaName}].[${relatedEntity.BaseTable}]
1904
- WHERE ${whereClause}
1905
-
1906
- OPEN ${cursorName}
1907
- FETCH NEXT FROM ${cursorName} INTO ${updateParams.fetchInto}
1908
-
1909
- WHILE @@FETCH_STATUS = 0
1910
- BEGIN
1911
- -- Set the FK field to NULL
1912
- SET @${variablePrefix}_${fkField.CodeName} = NULL
1913
-
1914
- -- Call the update SP for the related entity
1915
- EXEC [${relatedEntity.SchemaName}].[${spName}] ${spCallParams}
1916
-
1917
- FETCH NEXT FROM ${cursorName} INTO ${updateParams.fetchInto}
1918
- END
1919
-
1920
- CLOSE ${cursorName}
1921
- DEALLOCATE ${cursorName}`;
1922
- }
1923
- // For delete operation, use a simpler prefix for primary keys only
1924
- const pkComponents = this.buildPrimaryKeyComponents(relatedEntity, variablePrefix);
1925
- return `
1926
- -- Cascade delete from ${relatedEntity.BaseTable} using cursor to call ${spName}
1927
- DECLARE ${pkComponents.varDeclarations}
1928
- DECLARE ${cursorName} CURSOR FOR
1929
- SELECT ${pkComponents.selectFields}
1930
- FROM [${relatedEntity.SchemaName}].[${relatedEntity.BaseTable}]
1931
- WHERE ${whereClause}
1932
-
1933
- OPEN ${cursorName}
1934
- FETCH NEXT FROM ${cursorName} INTO ${pkComponents.fetchInto}
1935
-
1936
- WHILE @@FETCH_STATUS = 0
1937
- BEGIN
1938
- -- Call the delete SP for the related entity, which handles its own cascades
1939
- EXEC [${relatedEntity.SchemaName}].[${spName}] ${pkComponents.spParams}
1940
-
1941
- FETCH NEXT FROM ${cursorName} INTO ${pkComponents.fetchInto}
1942
- END
1943
-
1944
- CLOSE ${cursorName}
1945
- DEALLOCATE ${cursorName}`;
1378
+ return this._dbProvider.generateSingleCascadeOperation({
1379
+ parentEntity,
1380
+ relatedEntity,
1381
+ fkField,
1382
+ operation,
1383
+ });
1946
1384
  }
1947
1385
  buildPrimaryKeyComponents(entity, prefix = '') {
1948
- let varDeclarations = '';
1949
- let selectFields = '';
1950
- let fetchInto = '';
1951
- let spParams = '';
1952
- const varPrefix = prefix || 'Related';
1953
- for (const pk of entity.PrimaryKeys) {
1954
- if (varDeclarations !== '')
1955
- varDeclarations += ', ';
1956
- varDeclarations += `@${varPrefix}${pk.CodeName} ${pk.SQLFullType}`;
1957
- if (selectFields !== '')
1958
- selectFields += ', ';
1959
- selectFields += `[${pk.Name}]`;
1960
- if (fetchInto !== '')
1961
- fetchInto += ', ';
1962
- fetchInto += `@${varPrefix}${pk.CodeName}`;
1963
- if (spParams !== '')
1964
- spParams += ', ';
1965
- // Use named parameters: @ParamName = @VariableValue
1966
- spParams += `@${pk.CodeName} = @${varPrefix}${pk.CodeName}`;
1967
- }
1968
- return { varDeclarations, selectFields, fetchInto, spParams };
1386
+ const result = this._dbProvider.buildPrimaryKeyComponents(entity, prefix || undefined);
1387
+ return {
1388
+ varDeclarations: result.varDeclarations,
1389
+ selectFields: result.selectFields,
1390
+ fetchInto: result.fetchInto,
1391
+ spParams: result.routineParams,
1392
+ };
1969
1393
  }
1970
1394
  buildUpdateCursorParameters(entity, _fkField, prefix = '') {
1971
- let declarations = '';
1972
- let selectFields = '';
1973
- let fetchInto = '';
1974
- let allParams = '';
1975
- const varPrefix = prefix || entity.CodeName;
1976
- // First, handle primary keys with the entity-specific prefix
1977
- const pkComponents = this.buildPrimaryKeyComponents(entity, varPrefix);
1978
- // Add primary key declarations to the declarations string
1979
- // Need to add DECLARE keyword since buildPrimaryKeyComponents doesn't include it
1980
- declarations = pkComponents.varDeclarations.split(', ').map(decl => `DECLARE ${decl}`).join('\n ');
1981
- selectFields = pkComponents.selectFields;
1982
- fetchInto = pkComponents.fetchInto;
1983
- allParams = pkComponents.spParams;
1984
- // Then, add all updateable fields with the same prefix
1985
- const sortedFields = sortBySequenceAndCreatedAt(entity.Fields);
1986
- for (const ef of sortedFields) {
1987
- if (!ef.IsPrimaryKey && !ef.IsVirtual && ef.AllowUpdateAPI && !ef.AutoIncrement && !ef.IsSpecialDateField) {
1988
- if (declarations !== '')
1989
- declarations += '\n ';
1990
- declarations += `DECLARE @${varPrefix}_${ef.CodeName} ${ef.SQLFullType}`;
1991
- if (selectFields !== '')
1992
- selectFields += ', ';
1993
- selectFields += `[${ef.Name}]`;
1994
- if (fetchInto !== '')
1995
- fetchInto += ', ';
1996
- fetchInto += `@${varPrefix}_${ef.CodeName}`;
1997
- if (allParams !== '')
1998
- allParams += ', ';
1999
- // Use named parameters: @ParamName = @VariableValue
2000
- allParams += `@${ef.CodeName} = @${varPrefix}_${ef.CodeName}`;
2001
- }
2002
- }
2003
- return { declarations, selectFields, fetchInto, allParams };
1395
+ // This method is now only called by generateCascadeCursorOperation which
1396
+ // delegates to the provider. Kept for backward compatibility.
1397
+ throw new Error('buildUpdateCursorParameters should be handled by the database provider');
2004
1398
  }
2005
1399
  /**
2006
1400
  * Checks if a field is part of a composite (multi-column) unique constraint.
@@ -2009,36 +1403,9 @@ GO${permissions}
2009
1403
  * during cascade operations, since setting to NULL could violate uniqueness.
2010
1404
  */
2011
1405
  async isFieldInCompositeUniqueConstraint(pool, schemaName, tableName, columnName) {
2012
- const query = `
2013
- SELECT i.index_id
2014
- FROM sys.indexes i
2015
- INNER JOIN sys.tables t ON i.object_id = t.object_id
2016
- INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
2017
- WHERE i.is_unique = 1
2018
- AND i.is_primary_key = 0
2019
- AND s.name = @schemaName
2020
- AND t.name = @tableName
2021
- -- This index contains the specified column
2022
- AND EXISTS (
2023
- SELECT 1
2024
- FROM sys.index_columns ic
2025
- INNER JOIN sys.columns c ON ic.object_id = c.object_id AND c.column_id = ic.column_id
2026
- WHERE ic.object_id = i.object_id
2027
- AND ic.index_id = i.index_id
2028
- AND c.name = @columnName
2029
- )
2030
- -- This index has more than one column (composite)
2031
- AND (SELECT COUNT(*)
2032
- FROM sys.index_columns ic2
2033
- WHERE ic2.object_id = i.object_id
2034
- AND ic2.index_id = i.index_id) > 1
2035
- `;
1406
+ const query = this._dbProvider.getCompositeUniqueConstraintCheckSQL(schemaName, tableName, columnName);
2036
1407
  try {
2037
- const result = await pool.request()
2038
- .input('schemaName', sql.NVarChar, schemaName)
2039
- .input('tableName', sql.NVarChar, tableName)
2040
- .input('columnName', sql.NVarChar, columnName)
2041
- .query(query);
1408
+ const result = await pool.query(query);
2042
1409
  return result.recordset.length > 0;
2043
1410
  }
2044
1411
  catch (error) {
@@ -2056,10 +1423,10 @@ GO${permissions}
2056
1423
  // Find all fields in other entities that are foreign keys to this entity
2057
1424
  for (const e of md.Entities) {
2058
1425
  for (const ef of e.Fields) {
2059
- if (ef.RelatedEntityID === entity.ID && ef.IsVirtual === false) {
1426
+ if (UUIDsEqual(ef.RelatedEntityID, entity.ID) && ef.IsVirtual === false) {
2060
1427
  // Skip self-referential foreign keys (e.g., ParentID pointing to same entity)
2061
1428
  // These don't create inter-entity dependencies for ordering purposes
2062
- if (e.ID === entity.ID) {
1429
+ if (UUIDsEqual(e.ID, entity.ID)) {
2063
1430
  continue;
2064
1431
  }
2065
1432
  // Check if this would generate a cascade operation
@@ -2114,9 +1481,9 @@ GO${permissions}
2114
1481
  // Log the dependency map
2115
1482
  logStatus(`Cascade delete dependency map built:`);
2116
1483
  for (const [dependedOnEntityId, dependentEntityIds] of this.cascadeDeleteDependencies) {
2117
- const dependedOnEntity = entities.find(e => e.ID === dependedOnEntityId);
1484
+ const dependedOnEntity = entities.find(e => UUIDsEqual(e.ID, dependedOnEntityId));
2118
1485
  const dependentNames = Array.from(dependentEntityIds)
2119
- .map(id => entities.find(e => e.ID === id)?.Name || id)
1486
+ .map(id => entities.find(e => UUIDsEqual(e.ID, id))?.Name || id)
2120
1487
  .join(', ');
2121
1488
  logStatus(` ${dependedOnEntity?.Name || dependedOnEntityId} is depended on by: ${dependentNames}`);
2122
1489
  }
@@ -2188,7 +1555,7 @@ GO${permissions}
2188
1555
  logStatus(`Identified ${entitiesNeedingRegeneration.size} entities requiring delete SP regeneration due to cascade dependencies`);
2189
1556
  // Store the entity IDs that need regeneration (only if spDeleteGenerated=true)
2190
1557
  for (const entityId of entitiesNeedingRegeneration) {
2191
- const entity = entities.find(e => e.ID === entityId);
1558
+ const entity = entities.find(e => UUIDsEqual(e.ID, entityId));
2192
1559
  if (entity && entity.spDeleteGenerated) {
2193
1560
  this.entitiesNeedingDeleteSPRegeneration.add(entityId);
2194
1561
  logStatus(` - Marked ${entity.Name} for delete SP regeneration (cascade dependency)`);
@@ -2202,7 +1569,7 @@ GO${permissions}
2202
1569
  if (this.orderedEntitiesForDeleteSPRegeneration.length > 0) {
2203
1570
  logStatus(`Ordered entities for delete SP regeneration:`);
2204
1571
  this.orderedEntitiesForDeleteSPRegeneration.forEach((entityId, index) => {
2205
- const entity = entities.find(e => e.ID === entityId);
1572
+ const entity = entities.find(e => UUIDsEqual(e.ID, entityId));
2206
1573
  logStatus(` ${index + 1}. ${entity?.Name || entityId}`);
2207
1574
  });
2208
1575
  }
@@ -2250,7 +1617,7 @@ GO${permissions}
2250
1617
  }
2251
1618
  if (visiting.has(entityId)) {
2252
1619
  // Circular dependency detected - mark it but don't fail
2253
- const entity = entities.find(e => e.ID === entityId);
1620
+ const entity = entities.find(e => UUIDsEqual(e.ID, entityId));
2254
1621
  logStatus(`Warning: Circular cascade delete dependency detected involving ${entity?.Name || entityId}`);
2255
1622
  circularDeps.add(entityId);
2256
1623
  return false; // Signal circular dependency but continue processing
@@ -2277,7 +1644,7 @@ GO${permissions}
2277
1644
  if (!success && circularDeps.has(entityId)) {
2278
1645
  // Entity is part of circular dependency - add it anyway in arbitrary order
2279
1646
  // The SQL will still be generated, just not in perfect dependency order
2280
- logStatus(` - Adding ${entities.find(e => e.ID === entityId)?.Name || entityId} despite circular dependency`);
1647
+ logStatus(` - Adding ${entities.find(e => UUIDsEqual(e.ID, entityId))?.Name || entityId} despite circular dependency`);
2281
1648
  visited.add(entityId);
2282
1649
  ordered.push(entityId);
2283
1650
  }