@memberjunction/codegen-lib 6.1.4 → 6.2.0-edge.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/dist/Angular/angular-codegen.d.ts +6 -0
  2. package/dist/Angular/angular-codegen.d.ts.map +1 -1
  3. package/dist/Angular/angular-codegen.js +26 -14
  4. package/dist/Angular/angular-codegen.js.map +1 -1
  5. package/dist/Config/config.d.ts +45 -2
  6. package/dist/Config/config.d.ts.map +1 -1
  7. package/dist/Config/config.js +116 -39
  8. package/dist/Config/config.js.map +1 -1
  9. package/dist/Config/db-connection.d.ts +4 -0
  10. package/dist/Config/db-connection.d.ts.map +1 -1
  11. package/dist/Config/db-connection.js +11 -3
  12. package/dist/Config/db-connection.js.map +1 -1
  13. package/dist/Config/pg-connection.d.ts +2 -0
  14. package/dist/Config/pg-connection.d.ts.map +1 -1
  15. package/dist/Config/pg-connection.js +5 -1
  16. package/dist/Config/pg-connection.js.map +1 -1
  17. package/dist/Config/schema-output.d.ts +8 -0
  18. package/dist/Config/schema-output.d.ts.map +1 -1
  19. package/dist/Config/schema-output.js +19 -7
  20. package/dist/Config/schema-output.js.map +1 -1
  21. package/dist/Database/__tests__/exclude-schema-permissions.test.js +3 -3
  22. package/dist/Database/codeGenDatabaseProvider.d.ts +40 -1
  23. package/dist/Database/codeGenDatabaseProvider.d.ts.map +1 -1
  24. package/dist/Database/codeGenDatabaseProvider.js +65 -7
  25. package/dist/Database/codeGenDatabaseProvider.js.map +1 -1
  26. package/dist/Database/dbSchema.d.ts +4 -0
  27. package/dist/Database/dbSchema.d.ts.map +1 -1
  28. package/dist/Database/dbSchema.js +17 -9
  29. package/dist/Database/dbSchema.js.map +1 -1
  30. package/dist/Database/entity-field-change-tracking.d.ts +4 -0
  31. package/dist/Database/entity-field-change-tracking.d.ts.map +1 -1
  32. package/dist/Database/entity-field-change-tracking.js +12 -4
  33. package/dist/Database/entity-field-change-tracking.js.map +1 -1
  34. package/dist/Database/exclude-tables.d.ts +2 -0
  35. package/dist/Database/exclude-tables.d.ts.map +1 -1
  36. package/dist/Database/exclude-tables.js +5 -1
  37. package/dist/Database/exclude-tables.js.map +1 -1
  38. package/dist/Database/field-metadata-lock.d.ts +8 -6
  39. package/dist/Database/field-metadata-lock.d.ts.map +1 -1
  40. package/dist/Database/field-metadata-lock.js +29 -25
  41. package/dist/Database/field-metadata-lock.js.map +1 -1
  42. package/dist/Database/heal-schema-params.d.ts +14 -2
  43. package/dist/Database/heal-schema-params.d.ts.map +1 -1
  44. package/dist/Database/heal-schema-params.js +21 -5
  45. package/dist/Database/heal-schema-params.js.map +1 -1
  46. package/dist/Database/manage-metadata.d.ts +227 -4
  47. package/dist/Database/manage-metadata.d.ts.map +1 -1
  48. package/dist/Database/manage-metadata.js +883 -384
  49. package/dist/Database/manage-metadata.js.map +1 -1
  50. package/dist/Database/materializationAnalysis.d.ts +61 -2
  51. package/dist/Database/materializationAnalysis.d.ts.map +1 -1
  52. package/dist/Database/materializationAnalysis.js +43 -23
  53. package/dist/Database/materializationAnalysis.js.map +1 -1
  54. package/dist/Database/materializationBroadRender.d.ts +5 -3
  55. package/dist/Database/materializationBroadRender.d.ts.map +1 -1
  56. package/dist/Database/materializationBroadRender.js +17 -13
  57. package/dist/Database/materializationBroadRender.js.map +1 -1
  58. package/dist/Database/materializationDrift.d.ts +2 -0
  59. package/dist/Database/materializationDrift.d.ts.map +1 -1
  60. package/dist/Database/materializationDrift.js +5 -1
  61. package/dist/Database/materializationDrift.js.map +1 -1
  62. package/dist/Database/materializationParamClassifier.d.ts +17 -2
  63. package/dist/Database/materializationParamClassifier.d.ts.map +1 -1
  64. package/dist/Database/materializationParamClassifier.js +22 -10
  65. package/dist/Database/materializationParamClassifier.js.map +1 -1
  66. package/dist/Database/materializationParamVerifier.d.ts +2 -0
  67. package/dist/Database/materializationParamVerifier.d.ts.map +1 -1
  68. package/dist/Database/materializationParamVerifier.js +18 -14
  69. package/dist/Database/materializationParamVerifier.js.map +1 -1
  70. package/dist/Database/materializationSqlAst.d.ts +19 -1
  71. package/dist/Database/materializationSqlAst.d.ts.map +1 -1
  72. package/dist/Database/materializationSqlAst.js +53 -17
  73. package/dist/Database/materializationSqlAst.js.map +1 -1
  74. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts +18 -0
  75. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.d.ts.map +1 -1
  76. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js +76 -30
  77. package/dist/Database/providers/postgresql/PostgreSQLCodeGenProvider.js.map +1 -1
  78. package/dist/Database/providers/postgresql/metadataSupportObjects.d.ts +2 -0
  79. package/dist/Database/providers/postgresql/metadataSupportObjects.d.ts.map +1 -1
  80. package/dist/Database/providers/postgresql/metadataSupportObjects.js +5 -1
  81. package/dist/Database/providers/postgresql/metadataSupportObjects.js.map +1 -1
  82. package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts +10 -0
  83. package/dist/Database/providers/postgresql/viewDependencyCapture.d.ts.map +1 -1
  84. package/dist/Database/providers/postgresql/viewDependencyCapture.js +25 -5
  85. package/dist/Database/providers/postgresql/viewDependencyCapture.js.map +1 -1
  86. package/dist/Database/providers/postgresql/viewFallback.d.ts +11 -7
  87. package/dist/Database/providers/postgresql/viewFallback.d.ts.map +1 -1
  88. package/dist/Database/providers/postgresql/viewFallback.js +18 -12
  89. package/dist/Database/providers/postgresql/viewFallback.js.map +1 -1
  90. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts +9 -0
  91. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.d.ts.map +1 -1
  92. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js +27 -13
  93. package/dist/Database/providers/sqlserver/SQLServerCodeGenProvider.js.map +1 -1
  94. package/dist/Database/reconcileFieldLevelSecurity.d.ts +2 -0
  95. package/dist/Database/reconcileFieldLevelSecurity.d.ts.map +1 -1
  96. package/dist/Database/reconcileFieldLevelSecurity.js +5 -1
  97. package/dist/Database/reconcileFieldLevelSecurity.js.map +1 -1
  98. package/dist/Database/reorder-columns.d.ts +2 -0
  99. package/dist/Database/reorder-columns.d.ts.map +1 -1
  100. package/dist/Database/reorder-columns.js +5 -1
  101. package/dist/Database/reorder-columns.js.map +1 -1
  102. package/dist/Database/schema-filters.d.ts +6 -0
  103. package/dist/Database/schema-filters.d.ts.map +1 -1
  104. package/dist/Database/schema-filters.js +17 -5
  105. package/dist/Database/schema-filters.js.map +1 -1
  106. package/dist/Database/schema-scope.d.ts +4 -0
  107. package/dist/Database/schema-scope.d.ts.map +1 -1
  108. package/dist/Database/schema-scope.js +13 -5
  109. package/dist/Database/schema-scope.js.map +1 -1
  110. package/dist/Database/search-guardrails.d.ts +52 -0
  111. package/dist/Database/search-guardrails.d.ts.map +1 -1
  112. package/dist/Database/search-guardrails.js +77 -18
  113. package/dist/Database/search-guardrails.js.map +1 -1
  114. package/dist/Database/sql.d.ts +20 -0
  115. package/dist/Database/sql.d.ts.map +1 -1
  116. package/dist/Database/sql.js +72 -32
  117. package/dist/Database/sql.js.map +1 -1
  118. package/dist/Database/sql_codegen.d.ts +92 -0
  119. package/dist/Database/sql_codegen.d.ts.map +1 -1
  120. package/dist/Database/sql_codegen.js +197 -113
  121. package/dist/Database/sql_codegen.js.map +1 -1
  122. package/dist/EntityNameScanner/EntityNameScanner.d.ts +16 -0
  123. package/dist/EntityNameScanner/EntityNameScanner.d.ts.map +1 -1
  124. package/dist/EntityNameScanner/EntityNameScanner.js +47 -15
  125. package/dist/EntityNameScanner/EntityNameScanner.js.map +1 -1
  126. package/dist/EntityNameScanner/HtmlEntityNameScanner.d.ts +6 -0
  127. package/dist/EntityNameScanner/HtmlEntityNameScanner.d.ts.map +1 -1
  128. package/dist/EntityNameScanner/HtmlEntityNameScanner.js +19 -7
  129. package/dist/EntityNameScanner/HtmlEntityNameScanner.js.map +1 -1
  130. package/dist/EntityNameScanner/MetadataNameScanner.d.ts +6 -0
  131. package/dist/EntityNameScanner/MetadataNameScanner.d.ts.map +1 -1
  132. package/dist/EntityNameScanner/MetadataNameScanner.js +19 -7
  133. package/dist/EntityNameScanner/MetadataNameScanner.js.map +1 -1
  134. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts +6 -0
  135. package/dist/Manifest/GenerateClassRegistrationsManifest.d.ts.map +1 -1
  136. package/dist/Manifest/GenerateClassRegistrationsManifest.js +16 -4
  137. package/dist/Manifest/GenerateClassRegistrationsManifest.js.map +1 -1
  138. package/dist/Misc/action_subclasses_codegen.d.ts +4 -0
  139. package/dist/Misc/action_subclasses_codegen.d.ts.map +1 -1
  140. package/dist/Misc/action_subclasses_codegen.js +11 -3
  141. package/dist/Misc/action_subclasses_codegen.js.map +1 -1
  142. package/dist/Misc/advanced_generation.d.ts +48 -2
  143. package/dist/Misc/advanced_generation.d.ts.map +1 -1
  144. package/dist/Misc/advanced_generation.js +53 -15
  145. package/dist/Misc/advanced_generation.js.map +1 -1
  146. package/dist/Misc/codegen-reporter.d.ts +53 -0
  147. package/dist/Misc/codegen-reporter.d.ts.map +1 -1
  148. package/dist/Misc/codegen-reporter.js +106 -22
  149. package/dist/Misc/codegen-reporter.js.map +1 -1
  150. package/dist/Misc/createNewUser.d.ts +6 -0
  151. package/dist/Misc/createNewUser.d.ts.map +1 -1
  152. package/dist/Misc/createNewUser.js +5 -1
  153. package/dist/Misc/createNewUser.js.map +1 -1
  154. package/dist/Misc/entity_subclasses_codegen.d.ts +8 -0
  155. package/dist/Misc/entity_subclasses_codegen.d.ts.map +1 -1
  156. package/dist/Misc/entity_subclasses_codegen.js +52 -36
  157. package/dist/Misc/entity_subclasses_codegen.js.map +1 -1
  158. package/dist/Misc/externalTypeMapping.d.ts +2 -0
  159. package/dist/Misc/externalTypeMapping.d.ts.map +1 -1
  160. package/dist/Misc/externalTypeMapping.js +5 -1
  161. package/dist/Misc/externalTypeMapping.js.map +1 -1
  162. package/dist/Misc/file-write.d.ts +2 -0
  163. package/dist/Misc/file-write.d.ts.map +1 -1
  164. package/dist/Misc/file-write.js +5 -1
  165. package/dist/Misc/file-write.js.map +1 -1
  166. package/dist/Misc/graphql_server_codegen.d.ts +14 -0
  167. package/dist/Misc/graphql_server_codegen.d.ts.map +1 -1
  168. package/dist/Misc/graphql_server_codegen.js +51 -27
  169. package/dist/Misc/graphql_server_codegen.js.map +1 -1
  170. package/dist/Misc/remote_operations_codegen.d.ts +12 -0
  171. package/dist/Misc/remote_operations_codegen.d.ts.map +1 -1
  172. package/dist/Misc/remote_operations_codegen.js +21 -5
  173. package/dist/Misc/remote_operations_codegen.js.map +1 -1
  174. package/dist/Misc/runCommand.d.ts +6 -0
  175. package/dist/Misc/runCommand.d.ts.map +1 -1
  176. package/dist/Misc/runCommand.js +16 -4
  177. package/dist/Misc/runCommand.js.map +1 -1
  178. package/dist/Misc/schema-emit.d.ts +28 -0
  179. package/dist/Misc/schema-emit.d.ts.map +1 -1
  180. package/dist/Misc/schema-emit.js +75 -23
  181. package/dist/Misc/schema-emit.js.map +1 -1
  182. package/dist/Misc/sql_logging.d.ts +44 -11
  183. package/dist/Misc/sql_logging.d.ts.map +1 -1
  184. package/dist/Misc/sql_logging.js +132 -51
  185. package/dist/Misc/sql_logging.js.map +1 -1
  186. package/dist/Misc/sql_text.d.ts +22 -0
  187. package/dist/Misc/sql_text.d.ts.map +1 -0
  188. package/dist/Misc/sql_text.js +46 -0
  189. package/dist/Misc/sql_text.js.map +1 -0
  190. package/dist/Misc/status_logging.d.ts +36 -0
  191. package/dist/Misc/status_logging.d.ts.map +1 -1
  192. package/dist/Misc/status_logging.js +95 -23
  193. package/dist/Misc/status_logging.js.map +1 -1
  194. package/dist/Misc/system_integrity.js +4 -4
  195. package/dist/Misc/system_integrity.js.map +1 -1
  196. package/dist/Misc/temp_batch_file.d.ts +12 -0
  197. package/dist/Misc/temp_batch_file.d.ts.map +1 -1
  198. package/dist/Misc/temp_batch_file.js +35 -11
  199. package/dist/Misc/temp_batch_file.js.map +1 -1
  200. package/dist/Misc/util.d.ts +33 -0
  201. package/dist/Misc/util.d.ts.map +1 -1
  202. package/dist/Misc/util.js +53 -13
  203. package/dist/Misc/util.js.map +1 -1
  204. package/dist/index.d.ts +1 -1
  205. package/dist/index.d.ts.map +1 -1
  206. package/dist/index.js +1 -1
  207. package/dist/index.js.map +1 -1
  208. package/dist/plugins/index.d.ts +11 -1
  209. package/dist/plugins/index.d.ts.map +1 -1
  210. package/dist/plugins/index.js +9 -1
  211. package/dist/plugins/index.js.map +1 -1
  212. package/dist/runCodeGen.d.ts +6 -0
  213. package/dist/runCodeGen.d.ts.map +1 -1
  214. package/dist/runCodeGen.js +150 -138
  215. package/dist/runCodeGen.js.map +1 -1
  216. package/package.json +29 -29
@@ -1,53 +1,54 @@
1
1
  import { SQLServerDialect, PostgreSQLDialect } from '@memberjunction/sql-dialect';
2
2
  import { CodeGenDatabaseProvider } from './codeGenDatabaseProvider.js';
3
- import { analyzeQueryForMaterialization, detectAggregationKeyColumns, detectAdditiveMeasures, MATERIALIZATION_SURROGATE_COLUMN } from './materializationAnalysis.js';
4
- import { evaluateMaterializationDrift } from './materializationDrift.js';
5
- import { classifyQueryParameters, buildHeldValues } from './materializationParamClassifier.js';
6
- import { buildBroadRowFilterSQL } from './materializationBroadRender.js';
3
+ import { AnalyzeQueryForMaterialization, DetectAggregationKeyColumns, DetectAdditiveMeasures, MATERIALIZATION_SURROGATE_COLUMN } from './materializationAnalysis.js';
4
+ import { EvaluateMaterializationDrift } from './materializationDrift.js';
5
+ import { ClassifyQueryParameters, BuildHeldValues } from './materializationParamClassifier.js';
6
+ import { BuildBroadRowFilterSQL } from './materializationBroadRender.js';
7
7
  import { QueryParameterProcessor } from '@memberjunction/query-processor';
8
8
  // Side-effect import — registers `SQLServerCodeGenProvider` with `MJGlobal.ClassFactory`
9
9
  // under the `'sqlserver'` key via its `@RegisterClass` decorator. Without this import,
10
10
  // `ClassFactory.CreateInstance(CodeGenDatabaseProvider, 'sqlserver')` returns nothing
11
11
  // and the SS code path silently fails.
12
12
  import './providers/sqlserver/SQLServerCodeGenProvider.js';
13
- import { configInfo, currentWorkingDirectory, dbPlatform, getSettingValue, mj_core_schema, outputDir } from '../Config/config.js';
13
+ import { configInfo, currentWorkingDirectory, DbPlatform, GetSettingValue, MjCoreSchema, OutputDir } from '../Config/config.js';
14
14
  import { CodeNameFromString, EntityFieldInfo, EntityInfo, ExternalDataSourceReadRouter, ExtractActualDefaultValue, LogError, LogStatus, Metadata, RunQuerySQLFilterManager, SeverityType } from "@memberjunction/core";
15
15
  import { MJEntityFieldSchema } from "@memberjunction/core-entities";
16
- import { logError, logMessage, logStatus, startSpinner, updateSpinner, succeedSpinner } from "../Misc/status_logging.js";
16
+ import { logError, LogMessage, logStatus, StartSpinner, UpdateSpinner, SucceedSpinner } from "../Misc/status_logging.js";
17
17
  import { SQLUtilityBase } from "./sql.js";
18
- import { applyIncludeSchemaScope } from "./schema-scope.js";
19
- import { buildHealSchemaRoutineParams, getAuthoredExcludeSchemas, snapshotAuthoredExcludeSchemas } from "./heal-schema-params.js";
20
- import { AdvancedGeneration, isPlausibleEntityName } from "../Misc/advanced_generation.js";
18
+ import { ApplyIncludeSchemaScope } from "./schema-scope.js";
19
+ import { BuildHealSchemaRoutineParams, GetAuthoredExcludeSchemas, SnapshotAuthoredExcludeSchemas } from "./heal-schema-params.js";
20
+ import { AdvancedGeneration, IsPlausibleEntityName } from "../Misc/advanced_generation.js";
21
21
  import { CodeGenReporter } from "../Misc/codegen-reporter.js";
22
- import { applySearchableFieldsCap, entityLevelEnableBlockedReason, isNarrativeFieldName, normalizePredicate, normalizeSmartFieldResultShape, } from "./search-guardrails.js";
23
- import { mapExternalNativeTypeToMJ } from "../Misc/externalTypeMapping.js";
22
+ import { ApplySearchableFieldsCap, DefaultPredicateFor, EntityLevelEnableBlockedReason, IsNarrativeFieldName, MAX_SEARCHABLE_FIELDS_PER_ENTITY, NAME_LIKE_FIELD_NAMES, NormalizePredicate, NormalizeSmartFieldResultShape, } from "./search-guardrails.js";
23
+ import { MapExternalNativeTypeToMJ } from "../Misc/externalTypeMapping.js";
24
24
  import { SQLParser } from "@memberjunction/sql-parser";
25
25
  import { createDisplayName, generatePluralName, MJGlobal, ResolveSingleEntityResourceTarget, stripTrailingChars, UUIDsEqual } from "@memberjunction/global";
26
26
  import { v4 as uuidv4, v5 as uuidv5 } from 'uuid';
27
27
  import * as fs from 'fs';
28
28
  import path from 'path';
29
- import { canonicalJSONStringify, deepEqualJSON } from "../Misc/util.js";
29
+ import { CanonicalJSONStringify, DeepEqualJSON } from "../Misc/util.js";
30
+ import { TrimTrailingStatementTerminators } from "../Misc/sql_text.js";
30
31
  import { SQLLogging } from "../Misc/sql_logging.js";
31
- import { computeFieldMetadataUpdate } from "./field-metadata-lock.js";
32
+ import { ComputeFieldMetadataUpdate } from "./field-metadata-lock.js";
32
33
  import { TRACKED_FIELD_COLUMNS, DISPLAYNAME_REOPEN_REASONS, TYPE_REOPEN_REASONS, diffEntityFieldSnapshots, } from './entity-field-change-tracking.js';
33
- export { TRACKED_FIELD_COLUMNS, DISPLAYNAME_REOPEN_REASONS, TYPE_REOPEN_REASONS, diffEntityFieldSnapshots, } from './entity-field-change-tracking.js';
34
+ export { TRACKED_FIELD_COLUMNS, DISPLAYNAME_REOPEN_REASONS, TYPE_REOPEN_REASONS, DiffEntityFieldSnapshots, diffEntityFieldSnapshots, } from './entity-field-change-tracking.js';
34
35
  export class ValidatorResult {
35
36
  constructor() {
36
- this.entityName = "";
37
- this.sourceCheckConstraint = "";
38
- this.functionText = "";
39
- this.functionName = "";
40
- this.functionDescription = "";
37
+ this.entityName = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
38
+ this.sourceCheckConstraint = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
39
+ this.functionText = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
40
+ this.functionName = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
41
+ this.functionDescription = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
41
42
  /**
42
43
  * The ID value in the Generated Codes entity that was created for this validator.
43
44
  */
44
- this.generatedCodeId = "";
45
+ this.generatedCodeId = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
45
46
  /**
46
47
  * The ID for the AI Model that was used to generate the code
47
48
  */
48
- this.aiModelID = "";
49
- this.wasGenerated = true;
50
- this.success = false;
49
+ this.aiModelID = ""; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
50
+ this.wasGenerated = true; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
51
+ this.success = false; // case-violation-ok-legacy-back-compat: object literals are assigned to this class, so an accessor stub changes what they must supply
51
52
  }
52
53
  }
53
54
  export class ManageMetadataBase {
@@ -97,7 +98,7 @@ export class ManageMetadataBase {
97
98
  */
98
99
  get dbProvider() {
99
100
  if (!this._dbProvider) {
100
- const platform = dbPlatform();
101
+ const platform = DbPlatform();
101
102
  const provider = MJGlobal.Instance.ClassFactory.CreateInstance(CodeGenDatabaseProvider, platform);
102
103
  // CreateInstance returns the abstract base class on a missed key (it
103
104
  // doesn't throw), so disambiguate by constructor identity: anything
@@ -190,7 +191,7 @@ export class ManageMetadataBase {
190
191
  */
191
192
  applyTimeEntityFieldSequenceSQL(entityID) {
192
193
  // COALESCE is valid on both platforms and is the form the docs and the gate describe.
193
- return `(SELECT COALESCE(MAX(${this.qi('Sequence')}), 0) + 1 FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ${this.qi('EntityID')} = '${entityID}')`;
194
+ return `(SELECT COALESCE(MAX(${this.qi('Sequence')}), 0) + 1 FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ${this.qi('EntityID')} = '${entityID}')`;
194
195
  }
195
196
  /**
196
197
  * Wraps a SELECT query with a row limit.
@@ -212,7 +213,7 @@ export class ManageMetadataBase {
212
213
  * (avoids hardcoding runtime UUIDs that may differ on a fresh database).
213
214
  */
214
215
  entityIdSubquery(tableName, schemaName) {
215
- const schema = mj_core_schema();
216
+ const schema = MjCoreSchema();
216
217
  return `(${this.selectTop(1, 'ID', `FROM ${this.qs(schema, 'vwEntities')} WHERE BaseTable = '${tableName}' AND SchemaName = '${schemaName}'`)})`;
217
218
  }
218
219
  /**
@@ -262,22 +263,38 @@ export class ManageMetadataBase {
262
263
  /**
263
264
  * Globally scoped list of entities that have been created during the metadata management process.
264
265
  */
265
- static get newEntityList() {
266
+ static get NewEntityList() {
266
267
  return this._newEntityList;
267
268
  }
268
- static set newEntityList(value) {
269
+ static set NewEntityList(value) {
269
270
  this._newEntityList = value;
270
271
  }
272
+ /** @deprecated Use {@link NewEntityList}. */
273
+ static get newEntityList() {
274
+ return this.NewEntityList;
275
+ }
276
+ /** @deprecated Use {@link NewEntityList}. */
277
+ static set newEntityList(value) {
278
+ this.NewEntityList = value;
279
+ }
271
280
  static { this._modifiedEntityList = []; }
272
281
  /**
273
282
  * Globally scoped list of entities that have been modified during the metadata management process.
274
283
  */
275
- static get modifiedEntityList() {
284
+ static get ModifiedEntityList() {
276
285
  return this._modifiedEntityList;
277
286
  }
278
- static set modifiedEntityList(value) {
287
+ static set ModifiedEntityList(value) {
279
288
  this._modifiedEntityList = value;
280
289
  }
290
+ /** @deprecated Use {@link ModifiedEntityList}. */
291
+ static get modifiedEntityList() {
292
+ return this.ModifiedEntityList;
293
+ }
294
+ /** @deprecated Use {@link ModifiedEntityList}. */
295
+ static set modifiedEntityList(value) {
296
+ this.ModifiedEntityList = value;
297
+ }
281
298
  static { this._newFieldSet = new Set(); }
282
299
  static { this._changedFields = new Map(); }
283
300
  static { this._changedFieldReport = []; }
@@ -289,13 +306,17 @@ export class ManageMetadataBase {
289
306
  }
290
307
  return `${eid}:${fld}`;
291
308
  }
292
- static registerNewField(entityID, name) {
309
+ static RegisterNewField(entityID, name) {
293
310
  const key = this.fieldKey(entityID, name);
294
311
  if (key) {
295
312
  this._newFieldSet.add(key);
296
313
  }
297
314
  }
298
- static registerFieldChange(entityID, name, reasons) {
315
+ /** @deprecated Use {@link RegisterNewField}. */
316
+ static registerNewField(entityID, name) {
317
+ return this.RegisterNewField(entityID, name);
318
+ }
319
+ static RegisterFieldChange(entityID, name, reasons) {
299
320
  const key = this.fieldKey(entityID, name);
300
321
  if (key) {
301
322
  let set = this._changedFields.get(key);
@@ -308,17 +329,29 @@ export class ManageMetadataBase {
308
329
  }
309
330
  }
310
331
  }
311
- static isFieldNew(entityID, name) {
332
+ /** @deprecated Use {@link RegisterFieldChange}. */
333
+ static registerFieldChange(entityID, name, reasons) {
334
+ return this.RegisterFieldChange(entityID, name, reasons);
335
+ }
336
+ static IsFieldNew(entityID, name) {
312
337
  const key = this.fieldKey(entityID, name);
313
338
  return key ? this._newFieldSet.has(key) : false;
314
339
  }
315
- static fieldChangeReasons(entityID, name) {
340
+ /** @deprecated Use {@link IsFieldNew}. */
341
+ static isFieldNew(entityID, name) {
342
+ return this.IsFieldNew(entityID, name);
343
+ }
344
+ static FieldChangeReasons(entityID, name) {
316
345
  const key = this.fieldKey(entityID, name);
317
346
  const set = key ? this._changedFields.get(key) : undefined;
318
347
  return set ?? new Set();
319
348
  }
320
- static isDisplayNameReopened(entityID, name) {
321
- const reasons = this.fieldChangeReasons(entityID, name);
349
+ /** @deprecated Use {@link FieldChangeReasons}. */
350
+ static fieldChangeReasons(entityID, name) {
351
+ return this.FieldChangeReasons(entityID, name);
352
+ }
353
+ static IsDisplayNameReopened(entityID, name) {
354
+ const reasons = this.FieldChangeReasons(entityID, name);
322
355
  for (const r of reasons) {
323
356
  if (DISPLAYNAME_REOPEN_REASONS.has(r)) {
324
357
  return true;
@@ -326,8 +359,12 @@ export class ManageMetadataBase {
326
359
  }
327
360
  return false;
328
361
  }
329
- static isTypeReopened(entityID, name) {
330
- const reasons = this.fieldChangeReasons(entityID, name);
362
+ /** @deprecated Use {@link IsDisplayNameReopened}. */
363
+ static isDisplayNameReopened(entityID, name) {
364
+ return this.IsDisplayNameReopened(entityID, name);
365
+ }
366
+ static IsTypeReopened(entityID, name) {
367
+ const reasons = this.FieldChangeReasons(entityID, name);
331
368
  for (const r of reasons) {
332
369
  if (TYPE_REOPEN_REASONS.has(r)) {
333
370
  return true;
@@ -335,20 +372,40 @@ export class ManageMetadataBase {
335
372
  }
336
373
  return false;
337
374
  }
338
- static get newFieldCount() {
375
+ /** @deprecated Use {@link IsTypeReopened}. */
376
+ static isTypeReopened(entityID, name) {
377
+ return this.IsTypeReopened(entityID, name);
378
+ }
379
+ static get NewFieldCount() {
339
380
  return this._newFieldSet.size;
340
381
  }
341
- static get changedFieldCount() {
382
+ /** @deprecated Use {@link NewFieldCount}. */
383
+ static get newFieldCount() {
384
+ return this.NewFieldCount;
385
+ }
386
+ static get ChangedFieldCount() {
342
387
  return this._changedFields.size;
343
388
  }
344
- static get changedFieldReport() {
389
+ /** @deprecated Use {@link ChangedFieldCount}. */
390
+ static get changedFieldCount() {
391
+ return this.ChangedFieldCount;
392
+ }
393
+ static get ChangedFieldReport() {
345
394
  return this._changedFieldReport;
346
395
  }
347
- static clearFieldTracking() {
396
+ /** @deprecated Use {@link ChangedFieldReport}. */
397
+ static get changedFieldReport() {
398
+ return this.ChangedFieldReport;
399
+ }
400
+ static ClearFieldTracking() {
348
401
  this._newFieldSet.clear();
349
402
  this._changedFields.clear();
350
403
  this._changedFieldReport = [];
351
404
  }
405
+ /** @deprecated Use {@link ClearFieldTracking}. */
406
+ static clearFieldTracking() {
407
+ return this.ClearFieldTracking();
408
+ }
352
409
  /**
353
410
  * Takes a snapshot of EntityField rows from vwEntityFields for diffing.
354
411
  * Only queries columns in TRACKED_FIELD_COLUMNS (Sequence is excluded).
@@ -360,7 +417,7 @@ export class ManageMetadataBase {
360
417
  : '';
361
418
  const cols = TRACKED_FIELD_COLUMNS.map(c => this.qi(c)).join(', ');
362
419
  const sSQL = `SELECT ${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Entity')} AS ${this.qi('EntityName')}, ${this.qi('Name')}, ${cols}
363
- FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
420
+ FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
364
421
  ${filter}`;
365
422
  const result = await this.runQuery(pool, sSQL);
366
423
  const map = new Map();
@@ -390,9 +447,13 @@ export class ManageMetadataBase {
390
447
  * the dead class keeps a live `@RegisterClass` registration on disk. The old monolith could
391
448
  * not drift this way: it was rewritten in full every run.
392
449
  */
393
- static get deletedEntitySchemaList() {
450
+ static get DeletedEntitySchemaList() {
394
451
  return this._deletedEntitySchemaList;
395
452
  }
453
+ /** @deprecated Use {@link DeletedEntitySchemaList}. */
454
+ static get deletedEntitySchemaList() {
455
+ return this.DeletedEntitySchemaList;
456
+ }
396
457
  static { this._entitiesRequiringViewRegen = []; }
397
458
  /**
398
459
  * Entities that had late-phase changes requiring their base views to be
@@ -411,9 +472,13 @@ export class ManageMetadataBase {
411
472
  /**
412
473
  * Globally scoped list of validators that have been generated during the metadata management process.
413
474
  */
414
- static get generatedValidators() {
475
+ static get GeneratedValidators() {
415
476
  return this._generatedValidators;
416
477
  }
478
+ /** @deprecated Use {@link GeneratedValidators}. */
479
+ static get generatedValidators() {
480
+ return this.GeneratedValidators;
481
+ }
417
482
  static { this._softPKFKConfigCache = null; }
418
483
  static { this._softPKFKConfigPath = ''; }
419
484
  /**
@@ -428,16 +493,20 @@ export class ManageMetadataBase {
428
493
  * Deterministic + event-driven (no mtime/TOCTOU race). The CLI `Run()` path does not call this, so its
429
494
  * load-once-per-process behavior is unchanged.
430
495
  */
431
- static invalidateSoftPKFKConfigCache() {
496
+ static InvalidateSoftPKFKConfigCache() {
432
497
  ManageMetadataBase._softPKFKConfigCache = null;
433
498
  ManageMetadataBase._softPKFKConfigPath = '';
434
499
  }
500
+ /** @deprecated Use {@link InvalidateSoftPKFKConfigCache}. */
501
+ static invalidateSoftPKFKConfigCache() {
502
+ return this.InvalidateSoftPKFKConfigCache();
503
+ }
435
504
  /**
436
505
  * Loads and caches the soft PK/FK configuration from the additionalSchemaInfo file.
437
506
  * Cached per process to avoid repeated I/O within a CodeGen run; the in-process (RSU) path calls
438
507
  * {@link invalidateSoftPKFKConfigCache} at the start of each run so a rewritten file is picked up.
439
508
  */
440
- static getSoftPKFKConfig() {
509
+ static GetSoftPKFKConfig() {
441
510
  // Return cached config if path hasn't changed
442
511
  const configPath = configInfo.additionalSchemaInfo
443
512
  ? path.join(currentWorkingDirectory, configInfo.additionalSchemaInfo)
@@ -463,6 +532,10 @@ export class ManageMetadataBase {
463
532
  return null;
464
533
  }
465
534
  }
535
+ /** @deprecated Use {@link GetSoftPKFKConfig}. */
536
+ static getSoftPKFKConfig() {
537
+ return this.GetSoftPKFKConfig();
538
+ }
466
539
  /**
467
540
  * Extracts a flat array of table configs from the config file, handling both formats:
468
541
  * 1. Schema-as-key (template format): { "dbo": [{ "TableName": "Orders", ... }] }
@@ -668,35 +741,47 @@ export class ManageMetadataBase {
668
741
  async processOrganicKeyConfig(pool) {
669
742
  const config = ManageMetadataBase.getSoftPKFKConfig();
670
743
  if (!config)
671
- return { success: true, createdCount: 0, updatedCount: 0 };
744
+ return { success: true, createdCount: 0, updatedCount: 0, failedCount: 0 };
672
745
  const allOrganicKeys = this.extractOrganicKeysFromConfig(config);
673
746
  if (allOrganicKeys.length === 0)
674
- return { success: true, createdCount: 0, updatedCount: 0 };
675
- const schema = mj_core_schema();
747
+ return { success: true, createdCount: 0, updatedCount: 0, failedCount: 0 };
748
+ const schema = MjCoreSchema();
676
749
  let createdCount = 0;
677
750
  let updatedCount = 0;
751
+ let failedCount = 0;
678
752
  for (const tableConfig of allOrganicKeys) {
679
753
  // Resolve the owning entity
680
- const ownerResult = await this.runQueryWithParams(pool, `
681
- ${this.selectTop(1, 'ID, Name', `FROM ${this.qs(schema, 'vwEntities')}
682
- WHERE (BaseTable = @TableName AND SchemaName = @SchemaName)
683
- OR Name = @TableName`, 'CASE WHEN BaseTable = @TableName AND SchemaName = @SchemaName THEN 0 ELSE 1 END')}
684
- `, { 'TableName': tableConfig.TableName, 'SchemaName': tableConfig.SchemaName });
685
- if (ownerResult.recordset.length === 0) {
686
- logError(` > Organic keys config: entity "${tableConfig.SchemaName}.${tableConfig.TableName}" not found — skipping`);
754
+ const owner = await this.findOrganicKeyEntity(pool, tableConfig.SchemaName, tableConfig.TableName);
755
+ if (!owner) {
756
+ // Every key declared on this table goes unapplied, so each one counts as a failure.
757
+ failedCount += tableConfig.OrganicKeys.length;
758
+ logError(` > Organic keys config: entity "${tableConfig.SchemaName}.${tableConfig.TableName}" not found — skipping its ${tableConfig.OrganicKeys.length} organic key(s)`);
687
759
  continue;
688
760
  }
689
- const ownerEntityId = ownerResult.recordset[0].ID;
690
- const ownerEntityName = ownerResult.recordset[0].Name;
761
+ const ownerEntityId = owner.ID;
762
+ const ownerEntityName = owner.Name;
691
763
  for (const okConfig of tableConfig.OrganicKeys) {
692
764
  try {
765
+ // Resolve every related entity BEFORE writing anything for this key. Step 1 creates
766
+ // bridge views (in the database and the migration log) and step 2 records the key, so
767
+ // discovering a missing related entity in step 3 would leave an orphan view and a key
768
+ // with a mapping missing.
769
+ const relatedEntities = await this.resolveOrganicKeyRelatedEntities(pool, okConfig);
770
+ if (!relatedEntities) {
771
+ failedCount++;
772
+ continue;
773
+ }
693
774
  // Step 1: Create transitive views if defined
694
775
  for (const re of okConfig.RelatedEntities) {
695
776
  if (re.TransitiveView) {
696
777
  const viewSchema = re.TransitiveView.SchemaName || re.SchemaName;
697
778
  const viewFullName = `${viewSchema}.${re.TransitiveView.Name}`;
698
- const viewSQL = `CREATE OR ALTER VIEW ${this.qs(viewSchema, re.TransitiveView.Name)} AS\n${re.TransitiveView.SQL}`;
699
- await this.LogSQLAndExecute(pool, viewSQL, `Create transitive bridge view ${viewFullName} for organic key "${okConfig.Name}" on ${ownerEntityName}`);
779
+ const viewSQL = this.dbProvider.generateCreateOrReplaceViewSQL(viewSchema, re.TransitiveView.Name, re.TransitiveView.SQL);
780
+ // T-SQL requires CREATE [OR ALTER] VIEW to be the ONLY statement in its batch — first as
781
+ // well as last — and the metadata DML logged just before it (entity-config UPDATEs, the
782
+ // previous key's INSERTs) has no trailing GO. requiresOwnBatch puts the provider's
783
+ // separator on both sides in the migration file ('' on PostgreSQL: nothing is added).
784
+ await this.logSQLAndExecute(pool, viewSQL, `Create transitive bridge view ${viewFullName} for organic key "${okConfig.Name}" on ${ownerEntityName}`, false, true, this.dbProvider.BatchSeparator, true);
700
785
  // Auto-populate TransitiveObject from the view definition
701
786
  re.TransitiveObject = viewFullName;
702
787
  }
@@ -716,7 +801,7 @@ export class ManageMetadataBase {
716
801
  Sequence = ${okConfig.Sequence ?? 0},
717
802
  Status = 'Active'
718
803
  WHERE ID = '${organicKeyId}'`;
719
- await this.LogSQLAndExecute(pool, updateSQL, `Update organic key "${okConfig.Name}" on ${ownerEntityName}`);
804
+ await this.logSQLAndExecute(pool, updateSQL, `Update organic key "${okConfig.Name}" on ${ownerEntityName}`);
720
805
  updatedCount++;
721
806
  logStatus(` > Organic key: Updated "${okConfig.Name}" on ${ownerEntityName}`);
722
807
  }
@@ -726,7 +811,7 @@ export class ManageMetadataBase {
726
811
  const insertSQL = `INSERT INTO ${this.qs(schema, 'EntityOrganicKey')}
727
812
  (EntityID, Name, ${okConfig.Description ? 'Description, ' : ''}MatchFieldNames, NormalizationStrategy, ${okConfig.CustomNormalizationExpression ? 'CustomNormalizationExpression, ' : ''}Sequence, Status)
728
813
  VALUES (${ownerSubquery}, '${okConfig.Name}', ${okConfig.Description ? `'${okConfig.Description.replace(/'/g, "''")}', ` : ''}'${matchFieldNames}', '${okConfig.NormalizationStrategy || 'LowerCaseTrim'}', ${okConfig.CustomNormalizationExpression ? `'${okConfig.CustomNormalizationExpression.replace(/'/g, "''")}', ` : ''}${okConfig.Sequence ?? 0}, 'Active')`;
729
- await this.LogSQLAndExecute(pool, insertSQL, `Insert organic key "${okConfig.Name}" on ${ownerEntityName}`);
814
+ await this.logSQLAndExecute(pool, insertSQL, `Insert organic key "${okConfig.Name}" on ${ownerEntityName}`);
730
815
  createdCount++;
731
816
  logStatus(` > Organic key: Created "${okConfig.Name}" on ${ownerEntityName}`);
732
817
  // Fetch the new ID
@@ -734,18 +819,9 @@ export class ManageMetadataBase {
734
819
  organicKeyId = newKey.recordset[0].ID;
735
820
  }
736
821
  // Step 3: Upsert EntityOrganicKeyRelatedEntity for each related entity
737
- for (const reConfig of okConfig.RelatedEntities) {
738
- const relResult = await this.runQueryWithParams(pool, `
739
- ${this.selectTop(1, 'ID, Name', `FROM ${this.qs(schema, 'vwEntities')}
740
- WHERE (BaseTable = @TableName AND SchemaName = @SchemaName)
741
- OR Name = @TableName`, 'CASE WHEN BaseTable = @TableName AND SchemaName = @SchemaName THEN 0 ELSE 1 END')}
742
- `, { 'TableName': reConfig.TableName, 'SchemaName': reConfig.SchemaName });
743
- if (relResult.recordset.length === 0) {
744
- logError(` > Organic key "${okConfig.Name}": related entity "${reConfig.SchemaName}.${reConfig.TableName}" not found — skipping`);
745
- continue;
746
- }
747
- const relEntityId = relResult.recordset[0].ID;
748
- const relEntityName = relResult.recordset[0].Name;
822
+ for (const { config: reConfig, entity: relEntity } of relatedEntities) {
823
+ const relEntityId = relEntity.ID;
824
+ const relEntityName = relEntity.Name;
749
825
  // Check if this related entity mapping already exists
750
826
  const existingRel = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(schema, 'EntityOrganicKeyRelatedEntity')} WHERE EntityOrganicKeyID = @KeyID AND RelatedEntityID = @RelEntityID`, { 'KeyID': organicKeyId, 'RelEntityID': relEntityId });
751
827
  // Build field values
@@ -765,7 +841,7 @@ export class ManageMetadataBase {
765
841
  DisplayLocation = '${reConfig.DisplayLocation || 'After Field Tabs'}',
766
842
  Sequence = ${reConfig.Sequence ?? 0}
767
843
  WHERE ID = '${relId}'`;
768
- await this.LogSQLAndExecute(pool, updateRelSQL, `Update organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
844
+ await this.logSQLAndExecute(pool, updateRelSQL, `Update organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
769
845
  }
770
846
  else {
771
847
  // Use subqueries for FK references so the logged SQL is portable across databases
@@ -785,18 +861,55 @@ export class ManageMetadataBase {
785
861
  ${reConfig.DisplayName ? `'${reConfig.DisplayName.replace(/'/g, "''")}'` : 'NULL'},
786
862
  '${reConfig.DisplayLocation || 'After Field Tabs'}',
787
863
  ${reConfig.Sequence ?? 0})`;
788
- await this.LogSQLAndExecute(pool, insertRelSQL, `Insert organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
864
+ await this.logSQLAndExecute(pool, insertRelSQL, `Insert organic key related entity: "${okConfig.Name}" → ${relEntityName}`);
789
865
  }
790
866
  logStatus(` > Organic key "${okConfig.Name}": ${existingRel.recordset.length > 0 ? 'updated' : 'created'} → ${relEntityName} (${isDirect ? 'direct' : 'transitive'})`);
791
867
  }
792
868
  }
793
869
  catch (err) {
870
+ // Keep going so one bad key doesn't block the others — but count it: a key that failed
871
+ // here (bridge-view DDL the database rejected, a refused view drop, …) is missing from
872
+ // the database, and the run must not report success for it.
873
+ failedCount++;
794
874
  const errMessage = err instanceof Error ? err.message : String(err);
795
875
  logError(` > Organic key config: Failed to process "${okConfig.Name}" on ${ownerEntityName}: ${errMessage}`);
796
876
  }
797
877
  }
798
878
  }
799
- return { success: true, createdCount, updatedCount };
879
+ return { success: failedCount === 0, createdCount, updatedCount, failedCount };
880
+ }
881
+ /**
882
+ * Finds the entity an organic-key config names: by base table + schema, else by entity name.
883
+ * Returns `null` when neither matches.
884
+ */
885
+ async findOrganicKeyEntity(pool, schemaName, tableName) {
886
+ const result = await this.runQueryWithParams(pool, `
887
+ ${this.selectTop(1, 'ID, Name', `FROM ${this.qs(MjCoreSchema(), 'vwEntities')}
888
+ WHERE (BaseTable = @TableName AND SchemaName = @SchemaName)
889
+ OR Name = @TableName`, 'CASE WHEN BaseTable = @TableName AND SchemaName = @SchemaName THEN 0 ELSE 1 END')}
890
+ `, { 'TableName': tableName, 'SchemaName': schemaName });
891
+ const row = result.recordset[0];
892
+ return row ? { ID: row.ID, Name: row.Name } : null;
893
+ }
894
+ /**
895
+ * Resolves every related entity of an organic key, in config order. Logs each one that doesn't
896
+ * resolve and returns `null` if any is missing, so the caller can skip the key before writing any
897
+ * of it.
898
+ */
899
+ async resolveOrganicKeyRelatedEntities(pool, okConfig) {
900
+ const resolved = [];
901
+ let missing = 0;
902
+ for (const reConfig of okConfig.RelatedEntities) {
903
+ const entity = await this.findOrganicKeyEntity(pool, reConfig.SchemaName, reConfig.TableName);
904
+ if (entity) {
905
+ resolved.push({ config: reConfig, entity });
906
+ }
907
+ else {
908
+ missing++;
909
+ logError(` > Organic key "${okConfig.Name}": related entity "${reConfig.SchemaName}.${reConfig.TableName}" not found — the key is not applied`);
910
+ }
911
+ }
912
+ return missing === 0 ? resolved : null;
800
913
  }
801
914
  /**
802
915
  * Builds the entity lookup behind {@link processISARelationshipConfig}: match on Name first, else
@@ -840,7 +953,7 @@ export class ManageMetadataBase {
840
953
  if (relationships.length === 0)
841
954
  return { success: true, updatedCount: 0 };
842
955
  let updatedCount = 0;
843
- const schema = mj_core_schema();
956
+ const schema = MjCoreSchema();
844
957
  for (const rel of relationships) {
845
958
  try {
846
959
  // Look up the parent entity — try by Name first, then by BaseTable within the given schema
@@ -884,7 +997,7 @@ export class ManageMetadataBase {
884
997
  SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()},
885
998
  ${this.qi('ParentID')} = '${parentId}'
886
999
  WHERE ${this.qi('ID')} = '${childId}'`;
887
- await this.LogSQLAndExecute(pool, updateSQL, `Set IS-A ParentID for "${childName}" → "${parentName}"`);
1000
+ await this.logSQLAndExecute(pool, updateSQL, `Set IS-A ParentID for "${childName}" → "${parentName}"`);
888
1001
  if (existingParentId) {
889
1002
  logStatus(` > IS-A: Updated "${childName}" ParentID from previous value to "${parentName}"`);
890
1003
  }
@@ -932,7 +1045,7 @@ export class ManageMetadataBase {
932
1045
  * so a broken declaration fails before it produces generated code.
933
1046
  */
934
1047
  async validateISARelationships(pool) {
935
- const schema = mj_core_schema();
1048
+ const schema = MjCoreSchema();
936
1049
  let errorCount = 0;
937
1050
  try {
938
1051
  const results = await this.runQuery(pool, this.buildISAValidationSQL(schema));
@@ -1023,7 +1136,7 @@ export class ManageMetadataBase {
1023
1136
  if (entityConfigs.length === 0)
1024
1137
  return { success: true, updatedCount: 0 };
1025
1138
  let updatedCount = 0;
1026
- const schema = mj_core_schema();
1139
+ const schema = MjCoreSchema();
1027
1140
  const reservedKeys = new Set(['BaseTable', 'SchemaName']);
1028
1141
  for (const ec of entityConfigs) {
1029
1142
  try {
@@ -1088,7 +1201,7 @@ export class ManageMetadataBase {
1088
1201
  const updateSQL = `UPDATE ${this.qs(schema, 'Entity')}
1089
1202
  SET ${setClauses.join(', ')}
1090
1203
  WHERE ${this.qi('ID')} = '${entityId}'`;
1091
- await this.LogSQLAndExecute(pool, updateSQL, `Update attributes on "${entityName}" from additionalSchemaInfo`);
1204
+ await this.logSQLAndExecute(pool, updateSQL, `Update attributes on "${entityName}" from additionalSchemaInfo`);
1092
1205
  const attrSummary = differingAttrs.map(a => a.summary).join(', ');
1093
1206
  logStatus(` > Entities config: Set ${attrSummary} on "${entityName}"`);
1094
1207
  updatedCount++;
@@ -1114,7 +1227,7 @@ export class ManageMetadataBase {
1114
1227
  if (virtualEntities.length === 0)
1115
1228
  return { success: true, createdCount: 0 };
1116
1229
  let createdCount = 0;
1117
- const schema = mj_core_schema();
1230
+ const schema = MjCoreSchema();
1118
1231
  for (const ve of virtualEntities) {
1119
1232
  const viewSchema = ve.SchemaName || schema;
1120
1233
  const viewName = ve.ViewName;
@@ -1190,7 +1303,7 @@ export class ManageMetadataBase {
1190
1303
  // Refresh so entity.Fields reflect this run's field sync before we snapshot the shape.
1191
1304
  const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
1192
1305
  await md.Refresh();
1193
- const coreSchema = mj_core_schema();
1306
+ const coreSchema = MjCoreSchema();
1194
1307
  const esc = (s) => s.replace(/'/g, "''");
1195
1308
  const lit = (s) => (s ? `'${esc(s)}'` : 'NULL');
1196
1309
  let processedCount = 0;
@@ -1253,8 +1366,8 @@ export class ManageMetadataBase {
1253
1366
  // includeBatchSeparator: each is a single GO-free batch (executed via ds.query), but the
1254
1367
  // migration file needs a GO between statements for Flyway/sqlcmd. Pass the provider's
1255
1368
  // separator, not the 'GO' default: on PostgreSQL it is '' and a literal GO breaks replay.
1256
- await this.LogSQLAndExecute(pool, tableSQL, `Create materialized table for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
1257
- await this.LogSQLAndExecute(pool, viewSQL, `Create wrapper view for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
1369
+ await this.logSQLAndExecute(pool, tableSQL, `Create materialized table for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
1370
+ await this.logSQLAndExecute(pool, viewSQL, `Create wrapper view for base-view materialization of entity ${entity.Name}`, false, true, this.dbProvider.BatchSeparator);
1258
1371
  // 4) Upsert the MJ: Materialized Results row, keyed on (SourceType, SourceEntityID).
1259
1372
  const existing = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(coreSchema, 'MaterializedResult')} WHERE SourceType = @SourceType AND SourceEntityID = @EntityID`, { SourceType: 'EntityBaseView', EntityID: entity.ID });
1260
1373
  if (existing.recordset.length > 0) {
@@ -1262,7 +1375,7 @@ export class ManageMetadataBase {
1262
1375
  SET SchemaName='${esc(matSchema)}', TableName='${esc(tableName)}', ViewName='${esc(viewName)}',
1263
1376
  RefreshSchedule=${lit(decl.RefreshSchedule)}, IntendedWorkload=${lit(decl.IntendedWorkload)}
1264
1377
  WHERE ID='${existing.recordset[0].ID}'`;
1265
- await this.LogSQLAndExecute(pool, sqlUpd, `Update MJ: Materialized Results for base-view materialization of ${entity.Name}`);
1378
+ await this.logSQLAndExecute(pool, sqlUpd, `Update MJ: Materialized Results for base-view materialization of ${entity.Name}`);
1266
1379
  }
1267
1380
  else {
1268
1381
  const newId = this.createNewUUID();
@@ -1274,7 +1387,7 @@ export class ManageMetadataBase {
1274
1387
  VALUES ( '${newId}', 'EntityBaseView', '${entity.ID}', '${esc(matSchema)}', '${esc(tableName)}', '${esc(viewName)}',
1275
1388
  'None', 'FullRebuild', ${lit(decl.RefreshSchedule)}, 'Building', ${lit(decl.IntendedWorkload)},
1276
1389
  ${this.utcNow()}, ${this.utcNow()} )`;
1277
- await this.LogSQLAndExecute(pool, sqlIns, `Insert MJ: Materialized Results for base-view materialization of ${entity.Name}`);
1390
+ await this.logSQLAndExecute(pool, sqlIns, `Insert MJ: Materialized Results for base-view materialization of ${entity.Name}`);
1278
1391
  }
1279
1392
  processedCount++;
1280
1393
  logStatus(` > Base-view materialization ready for entity "${entity.Name}" → [${matSchema}].[${viewName}]`);
@@ -1288,7 +1401,7 @@ export class ManageMetadataBase {
1288
1401
  * fields, or a precise refusal (per-parameter reasons) so non-qualifying queries are skipped loudly.
1289
1402
  */
1290
1403
  async classifyParameterizedQueryForMaterialization(pool, queryId, queryName, outputColumns, currentUser) {
1291
- const coreSchema = mj_core_schema();
1404
+ const coreSchema = MjCoreSchema();
1292
1405
  const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
1293
1406
  // Load the query template + parameter definitions (typed entities for the renderer).
1294
1407
  const qRes = await this.runQueryWithParams(pool, `SELECT SQL, UsesTemplate FROM ${this.qs(coreSchema, 'Query')} WHERE ID = @QID`, { QID: queryId });
@@ -1320,31 +1433,31 @@ export class ManageMetadataBase {
1320
1433
  // Phase 2 is shipped: enable Bucket-1 row-filter broad materialization. The provider now auto-injects
1321
1434
  // the read-time predicate from the persisted ReadFilterSpec (bound params), so a RowFilterBroad
1322
1435
  // materialization is safe to mint. `allowRowFilterBroad` remains the build-level kill switch.
1323
- const classification = classifyQueryParameters({ queryName, params: paramDefs, outputColumns, dialect, render, allowRowFilterBroad: true });
1324
- if (!classification.qualification.qualifies) {
1325
- const detail = classification.perParam.map((pp) => `${pp.name}: ${pp.verdict.reason}`).join('; ');
1326
- return { qualifies: false, reason: `${classification.qualification.reason ?? 'parameters not materializable'}${detail ? ` — [${detail}]` : ''}` };
1436
+ const classification = ClassifyQueryParameters({ queryName, params: paramDefs, outputColumns, dialect, render, allowRowFilterBroad: true });
1437
+ if (!classification.Qualification.qualifies) {
1438
+ const detail = classification.PerParam.map((pp) => `${pp.name}: ${pp.verdict.reason}`).join('; ');
1439
+ return { qualifies: false, reason: `${classification.Qualification.reason ?? 'parameters not materializable'}${detail ? ` — [${detail}]` : ''}` };
1327
1440
  }
1328
- if (classification.qualification.paramMode !== 'RowFilterBroad') {
1329
- return { qualifies: false, reason: `parameterization mode '${classification.qualification.paramMode}' is not supported for materialization in v1 (only RowFilterBroad)` };
1441
+ if (classification.Qualification.paramMode !== 'RowFilterBroad') {
1442
+ return { qualifies: false, reason: `parameterization mode '${classification.Qualification.paramMode}' is not supported for materialization in v1 (only RowFilterBroad)` };
1330
1443
  }
1331
1444
  // Build the broad source SQL: render a concrete instance (held values) then strip the row-filter predicates.
1332
1445
  let renderedHeld;
1333
1446
  try {
1334
- renderedHeld = render(buildHeldValues(paramDefs));
1447
+ renderedHeld = render(BuildHeldValues(paramDefs));
1335
1448
  }
1336
1449
  catch (e) {
1337
1450
  return { qualifies: false, reason: `could not render the query to build broad SQL: ${e instanceof Error ? e.message : String(e)}` };
1338
1451
  }
1339
- const expectedRemovals = classification.qualification.rowFilterColumns.length;
1340
- const broad = buildBroadRowFilterSQL(renderedHeld, classification.qualification.rowFilterColumns, dialect, expectedRemovals);
1341
- if (broad.removedCount === 0) {
1342
- return { qualifies: false, reason: `expected to strip row-filter predicate(s) on [${classification.qualification.rowFilterColumns.join(', ')}] but none were removed from the rendered SQL — refusing to avoid a wrongly-filtered materialization` };
1452
+ const expectedRemovals = classification.Qualification.rowFilterColumns.length;
1453
+ const broad = BuildBroadRowFilterSQL(renderedHeld, classification.Qualification.rowFilterColumns, dialect, expectedRemovals);
1454
+ if (broad.RemovedCount === 0) {
1455
+ return { qualifies: false, reason: `expected to strip row-filter predicate(s) on [${classification.Qualification.rowFilterColumns.join(', ')}] but none were removed from the rendered SQL — refusing to avoid a wrongly-filtered materialization` };
1343
1456
  }
1344
- if (broad.ambiguous) {
1345
- return { qualifies: false, reason: `expected to strip exactly ${expectedRemovals} row-filter parameter predicate(s) on [${classification.qualification.rowFilterColumns.join(', ')}] but matched ${broad.removedCount} — the parameter predicate(s) cannot be cleanly isolated from other static or same-named predicates on those columns, so a broad materialization would include or exclude rows the live query never would. Refusing (query stays live-only).` };
1457
+ if (broad.Ambiguous) {
1458
+ return { qualifies: false, reason: `expected to strip exactly ${expectedRemovals} row-filter parameter predicate(s) on [${classification.Qualification.rowFilterColumns.join(', ')}] but matched ${broad.RemovedCount} — the parameter predicate(s) cannot be cleanly isolated from other static or same-named predicates on those columns, so a broad materialization would include or exclude rows the live query never would. Refusing (query stays live-only).` };
1346
1459
  }
1347
- return { qualifies: true, rowFilterColumns: classification.qualification.rowFilterColumns, broadSQL: broad.sql, readFilterSpec: classification.qualification.readFilterSpec };
1460
+ return { qualifies: true, rowFilterColumns: classification.Qualification.rowFilterColumns, broadSQL: broad.Sql, readFilterSpec: classification.Qualification.readFilterSpec };
1348
1461
  }
1349
1462
  /**
1350
1463
  * Processes query materialization (CodeGen materialization phase, sub-step C — plan §4.2).
@@ -1363,7 +1476,7 @@ export class ManageMetadataBase {
1363
1476
  * Parameterized queries are skipped with a log (deferred to Phase 2).
1364
1477
  */
1365
1478
  async processQueryMaterializations(pool, currentUser) {
1366
- const coreSchema = mj_core_schema();
1479
+ const coreSchema = MjCoreSchema();
1367
1480
  // Gate the whole method on the IsMaterialized column existing — the flagged-query SELECT below filters on it,
1368
1481
  // and it (like the MaterializedResult table) is added by the materialization Foundation migration. On a DB
1369
1482
  // where that migration hasn't run yet the SELECT would throw and abort the entire codegen pass; with no such
@@ -1427,7 +1540,7 @@ export class ManageMetadataBase {
1427
1540
  // Detected BEFORE the shape analysis so the surrogate column can be typed for the keyed case.
1428
1541
  const detectSQL = paramMode === 'RowFilterBroad' ? (broadSQL ?? '') : querySQL;
1429
1542
  const matDialect = this.dbProvider.PlatformKey === 'postgresql' ? new PostgreSQLDialect() : new SQLServerDialect();
1430
- const detectedKeyColumns = detectSQL ? detectAggregationKeyColumns({ sql: detectSQL, dialect: matDialect, fields }) : null;
1543
+ const detectedKeyColumns = detectSQL ? DetectAggregationKeyColumns({ sql: detectSQL, dialect: matDialect, fields }) : null;
1431
1544
  // An EXTERNAL query refreshes via rebuildFromExternalQuery, which fetches all rows through the EDS
1432
1545
  // driver and writes a SYNTHETIC row-index surrogate — it cannot compute the per-group combined-key
1433
1546
  // hash and ignores KeyColumns entirely. So minting a keyed (hash-surrogate) entity for an external
@@ -1453,7 +1566,7 @@ export class ManageMetadataBase {
1453
1566
  // The materialized table's column shape is the query's output columns — independent of
1454
1567
  // parameterization (a row-filter query is materialized broad over the same columns). Pass
1455
1568
  // isParameterized:false so the analyzer yields the shape; the param gate above already ran.
1456
- const analysis = analyzeQueryForMaterialization({ queryName, isParameterized: false, fields, surrogateSQLType });
1569
+ const analysis = AnalyzeQueryForMaterialization({ queryName, isParameterized: false, fields, surrogateSQLType });
1457
1570
  if (!analysis.qualifies) {
1458
1571
  logStatus(` > Skipping materialization of query "${queryName}": ${analysis.reason}`);
1459
1572
  continue;
@@ -1471,7 +1584,7 @@ export class ManageMetadataBase {
1471
1584
  const isKeyedSingleSource = isKeyed && !!detectSQL && SQLParser.ExtractTableRefs(detectSQL, matDialect).length === 1;
1472
1585
  const refreshStrategy = !isKeyedSingleSource
1473
1586
  ? 'FullRebuild'
1474
- : detectAdditiveMeasures(detectSQL)
1587
+ : DetectAdditiveMeasures(detectSQL)
1475
1588
  ? 'Incremental'
1476
1589
  : 'DirtyGroupRecompute';
1477
1590
  // RLS downgrade gate (§6.2, ships day one) — a materialized query entity does NOT inherit its
@@ -1510,10 +1623,10 @@ export class ManageMetadataBase {
1510
1623
  logError(` > REFUSING to materialize query "${queryName}": its derived table [${coreSchema}].[${tableName}] (from CodeName "${codeName}") is already owned by a different materialization (SourceType=${foreignOwner.SourceType}, QueryID=${foreignOwner.QueryID ?? 'n/a'}). Two sources whose names normalize to the same CodeName would clobber each other's snapshot — rename one so their CodeNames differ. Skipping.`);
1511
1624
  continue;
1512
1625
  }
1513
- const tableSQL = this.dbProvider.generateMaterializedTableSQL(coreSchema, tableName, analysis.columns);
1626
+ const tableSQL = this.dbProvider.generateMaterializedTableSQL(coreSchema, tableName, analysis.Columns);
1514
1627
  const viewSQL = this.dbProvider.generateMaterializedWrapperViewSQL(coreSchema, viewName, tableName);
1515
- await this.LogSQLAndExecute(pool, tableSQL, `Create materialized table for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
1516
- await this.LogSQLAndExecute(pool, viewSQL, `Create wrapper view for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
1628
+ await this.logSQLAndExecute(pool, tableSQL, `Create materialized table for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
1629
+ await this.logSQLAndExecute(pool, viewSQL, `Create wrapper view for query "${queryName}"`, false, true, this.dbProvider.BatchSeparator);
1517
1630
  // 3) Mint the read-only Virtual Entity over the wrapper view (idempotent by view).
1518
1631
  const existingVE = await this.runQueryWithParams(pool, `SELECT ID FROM ${this.qs(coreSchema, 'vwEntities')} WHERE BaseView = @V AND SchemaName = @S`, { V: viewName, S: coreSchema });
1519
1632
  let generatedEntityId = existingVE.recordset[0]?.ID;
@@ -1523,7 +1636,7 @@ export class ManageMetadataBase {
1523
1636
  Name: queryName,
1524
1637
  BaseView: viewName,
1525
1638
  SchemaName: coreSchema,
1526
- PrimaryKeyFieldName: analysis.surrogateColumnName,
1639
+ PrimaryKeyFieldName: analysis.SurrogateColumnName,
1527
1640
  Description: `Materialized result of query "${queryName}".`,
1528
1641
  });
1529
1642
  generatedEntityId = createRes.recordset?.[0]?.[''] || createRes.recordset?.[0]?.ID || createRes.recordset?.[0]?.Column0;
@@ -1562,7 +1675,7 @@ export class ManageMetadataBase {
1562
1675
  SET GeneratedEntityID=${idLit(generatedEntityId)}, SchemaName='${esc(coreSchema)}', TableName='${esc(tableName)}', ViewName='${esc(viewName)}',
1563
1676
  ${this.qi('ParamMode')}='${paramMode}', ${this.qi('RowFilterColumns')}=${rowFilterColumnsLit}, ${this.qi('BroadSQL')}=${broadSQLLit}, ${this.qi('ReadFilterSpec')}=${readFilterSpecLit}, ${this.qi('KeyColumns')}=${keyColumnsLit}, ${this.qi('RefreshStrategy')}='${refreshStrategy}'
1564
1677
  WHERE ID='${matResultId}'`;
1565
- await this.LogSQLAndExecute(pool, sqlUpd, `Update MJ: Materialized Results for query "${queryName}"`);
1678
+ await this.logSQLAndExecute(pool, sqlUpd, `Update MJ: Materialized Results for query "${queryName}"`);
1566
1679
  }
1567
1680
  else {
1568
1681
  matResultId = this.createNewUUID();
@@ -1572,13 +1685,13 @@ export class ManageMetadataBase {
1572
1685
  ${c('ParamMode')}, ${c('RowFilterColumns')}, ${c('BroadSQL')}, ${c('ReadFilterSpec')}, ${c('KeyColumns')}, ${c('RefreshStrategy')}, ${c('Status')}, ${c('__mj_CreatedAt')}, ${c('__mj_UpdatedAt')} )
1573
1686
  VALUES ( '${matResultId}', 'Query', ${idLit(generatedEntityId)}, '${esc(coreSchema)}', '${esc(tableName)}', '${esc(viewName)}',
1574
1687
  '${paramMode}', ${rowFilterColumnsLit}, ${broadSQLLit}, ${readFilterSpecLit}, ${keyColumnsLit}, '${refreshStrategy}', 'Building', ${this.utcNow()}, ${this.utcNow()} )`;
1575
- await this.LogSQLAndExecute(pool, sqlIns, `Insert MJ: Materialized Results for query "${queryName}"`);
1688
+ await this.logSQLAndExecute(pool, sqlIns, `Insert MJ: Materialized Results for query "${queryName}"`);
1576
1689
  // Link the new materialization to its source Query via the join table.
1577
1690
  const joinId = this.createNewUUID();
1578
1691
  const sqlJoinIns = `INSERT INTO ${this.qs(coreSchema, 'MaterializedResultQuery')} (
1579
1692
  ${c('ID')}, ${c('MaterializedResultID')}, ${c('QueryID')}, ${c('__mj_CreatedAt')}, ${c('__mj_UpdatedAt')} )
1580
1693
  VALUES ( '${joinId}', '${matResultId}', '${queryId}', ${this.utcNow()}, ${this.utcNow()} )`;
1581
- await this.LogSQLAndExecute(pool, sqlJoinIns, `Link MJ: Materialized Results to Query "${queryName}" via join row`);
1694
+ await this.logSQLAndExecute(pool, sqlJoinIns, `Link MJ: Materialized Results to Query "${queryName}" via join row`);
1582
1695
  }
1583
1696
  processedCount++;
1584
1697
  }
@@ -1595,7 +1708,7 @@ export class ManageMetadataBase {
1595
1708
  * auto-rebuilds. Already-held (`DriftHold`) and `Disabled` rows are skipped.
1596
1709
  */
1597
1710
  async detectMaterializationDrift(pool) {
1598
- const coreSchema = mj_core_schema();
1711
+ const coreSchema = MjCoreSchema();
1599
1712
  // Gate on MaterializedResult existing — the SELECT below reads it. Absent (materialization migration not yet
1600
1713
  // applied on this DB) ⇒ no materializations exist to check for drift; skip cleanly rather than throwing and
1601
1714
  // aborting the codegen pass.
@@ -1700,14 +1813,14 @@ export class ManageMetadataBase {
1700
1813
  // would only stop FUTURE refreshes while leaving the already-populated rows queryable via raw SQL
1701
1814
  // over the wrapper view. Emptying (not dropping) removes the leaked data without breaking any object
1702
1815
  // dependency; the wrapper view remains but returns nothing. Then DriftHold so refresh never refills it.
1703
- await this.LogSQLAndExecute(pool, `DELETE FROM ${this.qs(r.SchemaName, r.TableName)}`, `Empty external RLS base-view mirror "${r.TableName}" (leak guard — mirror exposed RLS-refused rows)`);
1704
- await this.LogSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag external RLS base-view materialization "${r.TableName}" as DriftHold (leak guard)`);
1816
+ await this.logSQLAndExecute(pool, `DELETE FROM ${this.qs(r.SchemaName, r.TableName)}`, `Empty external RLS base-view mirror "${r.TableName}" (leak guard — mirror exposed RLS-refused rows)`);
1817
+ await this.logSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag external RLS base-view materialization "${r.TableName}" as DriftHold (leak guard)`);
1705
1818
  logError(` > RLS LEAK GUARD: base-view materialization "${r.TableName}" mirrors an EXTERNAL row-restricted entity ("${bvEntity.Name}" — role RLS and/or an API-key row filter) → mirror EMPTIED + DriftHold (refresh stopped). It exposed rows the live path refuses under RLS.`);
1706
1819
  return true;
1707
1820
  }
1708
1821
  }
1709
1822
  const facts = await this.gatherDriftFacts(pool, md, r, coreSchema);
1710
- const verdict = evaluateMaterializationDrift(facts);
1823
+ const verdict = EvaluateMaterializationDrift(facts);
1711
1824
  if (verdict.drift) {
1712
1825
  // Fail-closed on ANY hold of a QUERY materialization: once held, the row drops out of
1713
1826
  // detectMaterializationDrift's `Status NOT IN ('DriftHold', ...)` scan, so the C1 RLS re-check and the
@@ -1719,7 +1832,7 @@ export class ManageMetadataBase {
1719
1832
  if (r.SourceType === 'Query' && r.GeneratedEntityID) {
1720
1833
  await this.revokeMaterializedEntityReadAccess(pool, r.GeneratedEntityID, r.TableName, `drift hold — ${verdict.reason ?? 'shape/provenance drift'}`);
1721
1834
  }
1722
- await this.LogSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag materialization "${r.TableName}" as DriftHold`);
1835
+ await this.logSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag materialization "${r.TableName}" as DriftHold`);
1723
1836
  logStatus(` > DRIFT: materialization "${r.TableName}" → DriftHold — ${verdict.reason}`);
1724
1837
  return true;
1725
1838
  }
@@ -1753,7 +1866,7 @@ export class ManageMetadataBase {
1753
1866
  if (r.GeneratedEntityID) {
1754
1867
  await this.revokeMaterializedEntityReadAccess(pool, r.GeneratedEntityID, r.TableName, reason);
1755
1868
  }
1756
- await this.LogSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag materialization "${r.TableName}" as DriftHold (${label.toLowerCase()})`);
1869
+ await this.logSQLAndExecute(pool, `UPDATE ${this.qs(coreSchema, 'MaterializedResult')} SET ${this.qi('Status')}='DriftHold' WHERE ID='${r.ID}'`, `Flag materialization "${r.TableName}" as DriftHold (${label.toLowerCase()})`);
1757
1870
  logError(` > ${label}: materialization "${r.TableName}" → read access revoked + DriftHold — ${reason}`);
1758
1871
  }
1759
1872
  /** Gathers the drift-relevant existence facts for one materialization against current metadata. */
@@ -1864,7 +1977,7 @@ export class ManageMetadataBase {
1864
1977
  * @param pool - the ConnectionPool object to use for querying and updating the database
1865
1978
  * @returns
1866
1979
  */
1867
- async manageMetadata(pool, currentUser) {
1980
+ async ManageMetadata(pool, currentUser) {
1868
1981
  const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
1869
1982
  // Auto-exclude platform-specific system schemas.
1870
1983
  // We mutate configInfo.excludeSchemas directly so that all downstream code
@@ -1885,7 +1998,7 @@ export class ManageMetadataBase {
1885
1998
  // machinery — when the provider supplies DDL (PostgreSQL), we install it
1886
1999
  // idempotently here rather than depending on migrations to have shipped
1887
2000
  // it. Nothing below can work without them, so a failure here is fatal.
1888
- const supportObjectsSQL = this.dbProvider.getMetadataSupportObjectsSQL(mj_core_schema());
2001
+ const supportObjectsSQL = this.dbProvider.getMetadataSupportObjectsSQL(MjCoreSchema());
1889
2002
  if (supportObjectsSQL) {
1890
2003
  try {
1891
2004
  await pool.query(supportObjectsSQL);
@@ -1899,7 +2012,7 @@ export class ManageMetadataBase {
1899
2012
  // Authored exclude list (sys, staging, …) must be captured BEFORE includeSchemas is
1900
2013
  // compiled into excludeSchemas. Heal EXEC statements use that original list plus
1901
2014
  // @IncludedSchemaNames — never the sibling snapshot of this machine's database.
1902
- snapshotAuthoredExcludeSchemas(configInfo.excludeSchemas);
2015
+ SnapshotAuthoredExcludeSchemas(configInfo.excludeSchemas);
1903
2016
  // Resolve the opt-in `includeSchemas` positive scope into excludeSchemas, BEFORE the exclude
1904
2017
  // snapshot below and before createNewEntities() runs. The universe is queried from the DATABASE
1905
2018
  // rather than taken from loaded metadata on purpose: createNewEntities() discovers tables with no
@@ -1909,12 +2022,12 @@ export class ManageMetadataBase {
1909
2022
  // every run after, orphaning its entity records). No-op when includeSchemas is unset.
1910
2023
  if (configInfo.includeSchemas && configInfo.includeSchemas.length > 0) {
1911
2024
  try {
1912
- const schemaSQL = `SELECT DISTINCT ${this.qi('SchemaName')} FROM ${this.qs(mj_core_schema(), 'vwSQLTablesAndEntities')}`;
2025
+ const schemaSQL = `SELECT DISTINCT ${this.qi('SchemaName')} FROM ${this.qs(MjCoreSchema(), 'vwSQLTablesAndEntities')}`;
1913
2026
  const schemaResult = await this.runQuery(pool, schemaSQL);
1914
2027
  const allSchemas = (schemaResult.recordset ?? [])
1915
2028
  .map((r) => r.SchemaName)
1916
2029
  .filter((s) => !!s);
1917
- const newlyExcluded = applyIncludeSchemaScope(allSchemas, configInfo);
2030
+ const newlyExcluded = ApplyIncludeSchemaScope(allSchemas, configInfo);
1918
2031
  logStatus(` Applied includeSchemas scope [${configInfo.includeSchemas.join(', ')}] — excluded ${newlyExcluded.length} other schema(s) present in the database`);
1919
2032
  }
1920
2033
  catch (e) {
@@ -1964,12 +2077,12 @@ export class ManageMetadataBase {
1964
2077
  start = new Date();
1965
2078
  logStatus(' Recompiling base views...');
1966
2079
  const sqlUtility = MJGlobal.Instance.ClassFactory.CreateInstance(SQLUtilityBase);
1967
- const adminSchema = getSettingValue('mj_core_schema', '__mj');
1968
- const schemasToExclude = getSettingValue('recompile_mj_views', true)
2080
+ const adminSchema = GetSettingValue('mj_core_schema', '__mj');
2081
+ const schemasToExclude = GetSettingValue('recompile_mj_views', true)
1969
2082
  ? excludeSchemas.filter((s) => s !== adminSchema)
1970
2083
  : excludeSchemas;
1971
2084
  if (!await sqlUtility.recompileAllBaseViews(pool, schemasToExclude, true, ManageMetadataBase._newEntityList /*exclude the newly created entities from the above step the first time we run as those views don't exist yet*/)) {
1972
- logMessage(' Warning: Non-Fatal error recompiling base views', SeverityType.Warning, false);
2085
+ LogMessage(' Warning: Non-Fatal error recompiling base views', SeverityType.Warning, false);
1973
2086
  // many times the former versions of base views will NOT succesfully recompile, so don't consider that scenario to be a
1974
2087
  // failure for this entire function
1975
2088
  }
@@ -1980,7 +2093,7 @@ export class ManageMetadataBase {
1980
2093
  // Also skip deleting unneeded fields on this first pass — base views haven't been regenerated yet,
1981
2094
  // so virtual fields (which come from view JOINs) would be incorrectly identified as orphaned and deleted.
1982
2095
  // Deletion runs on the second pass (in sql_codegen.ts) after views are current.
1983
- if (!await this.manageEntityFields(pool, excludeSchemas, false, false, currentUser, true, true)) {
2096
+ if (!await this.ManageEntityFields(pool, excludeSchemas, false, false, currentUser, true, true)) {
1984
2097
  logError(' Error managing entity fields');
1985
2098
  bSuccess = false;
1986
2099
  }
@@ -2062,6 +2175,10 @@ export class ManageMetadataBase {
2062
2175
  if (organicKeyResult.createdCount > 0 || organicKeyResult.updatedCount > 0) {
2063
2176
  logStatus(` > Organic keys: ${organicKeyResult.createdCount} created, ${organicKeyResult.updatedCount} updated from config`);
2064
2177
  }
2178
+ if (!organicKeyResult.success) {
2179
+ logError(` Error processing organic keys: ${organicKeyResult.failedCount} key(s) failed and were not applied — see the errors above`);
2180
+ bSuccess = false;
2181
+ }
2065
2182
  // Config-driven base-view materialization — emit the physical table + wrapper view and
2066
2183
  // attach an "MJ: Materialized Results" row to the existing entity. Runs AFTER entity fields
2067
2184
  // are managed so the snapshot shape reflects the current base view (plan §4.1).
@@ -2089,6 +2206,10 @@ export class ManageMetadataBase {
2089
2206
  await this.checkEntityFieldSequenceIntegrity(pool, excludeSchemas);
2090
2207
  return bSuccess;
2091
2208
  }
2209
+ /** @deprecated Use {@link ManageMetadata}. */
2210
+ async manageMetadata(pool, currentUser) {
2211
+ return this.ManageMetadata(pool, currentUser);
2212
+ }
2092
2213
  /**
2093
2214
  * INTEGRITY CHECK — in a well-formed entity every base (non-virtual) field sequences BEFORE the
2094
2215
  * virtual/related fields, so the EntityField order matches the base view's `SELECT [base].*, <joins>`
@@ -2102,10 +2223,10 @@ export class ManageMetadataBase {
2102
2223
  async checkEntityFieldSequenceIntegrity(pool, excludeSchemas) {
2103
2224
  try {
2104
2225
  const excl = excludeSchemas.length > 0
2105
- ? `WHERE ${this.qi('EntityID')} NOT IN (SELECT ${this.qi('ID')} FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE ${this.qi('SchemaName')} IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))`
2226
+ ? `WHERE ${this.qi('EntityID')} NOT IN (SELECT ${this.qi('ID')} FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE ${this.qi('SchemaName')} IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))`
2106
2227
  : '';
2107
2228
  const sSQL = `SELECT ${this.qi('EntityID')}, ${this.qi('Entity')}, ${this.qi('Name')}, ${this.qi('Sequence')}, ${this.qi('IsVirtual')}
2108
- FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
2229
+ FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
2109
2230
  ${excl}
2110
2231
  ORDER BY ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('Name')}`;
2111
2232
  const result = await this.runQuery(pool, sSQL);
@@ -2157,7 +2278,7 @@ export class ManageMetadataBase {
2157
2278
  // virtual entities are records defined in the entity metadata and do NOT define a distinct base table
2158
2279
  // but they do specify a base view. We DO NOT generate a base view for a virtual entity, we simply use it to figure
2159
2280
  // out the fields that should be in the entity definition and add/update/delete the entity definition to match what's in the view when this runs
2160
- const sql = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE VirtualEntity = ${this.boolLit(true)}`;
2281
+ const sql = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntities')} WHERE VirtualEntity = ${this.boolLit(true)}`;
2161
2282
  const virtualEntitiesResult = await this.runQuery(pool, sql);
2162
2283
  const virtualEntities = virtualEntitiesResult.recordset;
2163
2284
  let anyUpdates = false;
@@ -2217,7 +2338,7 @@ export class ManageMetadataBase {
2217
2338
  async materializedResultTableExists(pool) {
2218
2339
  if (this._materializedResultTableExists === null) {
2219
2340
  const sql = `SELECT COUNT(*) AS TblExists FROM INFORMATION_SCHEMA.TABLES ` +
2220
- `WHERE TABLE_SCHEMA = '${mj_core_schema()}' AND TABLE_NAME = 'MaterializedResult'`;
2341
+ `WHERE TABLE_SCHEMA = '${MjCoreSchema()}' AND TABLE_NAME = 'MaterializedResult'`;
2221
2342
  const result = await this.runQuery(pool, sql);
2222
2343
  const row = (result.recordset?.[0] ?? {});
2223
2344
  const cnt = row.TblExists ?? row.tblexists ?? 0;
@@ -2235,7 +2356,7 @@ export class ManageMetadataBase {
2235
2356
  if (!(await this.entityHasExternalDataSourceColumn(pool))) {
2236
2357
  return { success: true, anyUpdates: false, relationshipsUpdated: false };
2237
2358
  }
2238
- const sql = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE ExternalDataSourceID IS NOT NULL`;
2359
+ const sql = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntities')} WHERE ExternalDataSourceID IS NOT NULL`;
2239
2360
  const result = await this.runQuery(pool, sql);
2240
2361
  const externalEntities = result.recordset;
2241
2362
  if (!externalEntities || externalEntities.length === 0) {
@@ -2352,16 +2473,16 @@ export class ManageMetadataBase {
2352
2473
  }
2353
2474
  // Map each introspected column into the veField shape that manageSingleVirtualEntityField consumes.
2354
2475
  const eeFields = obj.Columns.map(c => {
2355
- const t = mapExternalNativeTypeToMJ(c.NativeType);
2476
+ const t = MapExternalNativeTypeToMJ(c.NativeType);
2356
2477
  return { FieldName: c.Name, Type: t.Type, Length: t.Length, Precision: t.Precision, Scale: t.Scale, AllowsNull: c.Nullable, IsPrimaryKey: c.IsPrimaryKey };
2357
2478
  });
2358
2479
  const md = new Metadata(); // global-provider-ok: codegen runs offline against a single provider
2359
2480
  const entity = md.EntityByName(externalEntity.Name);
2360
2481
  if (entity) {
2361
2482
  // remove EntityFields no longer present in the remote object
2362
- const sqlRemove = this.buildExternalFieldRemoveSQL(mj_core_schema(), entity.Fields, eeFields.map(ef => ef.FieldName));
2483
+ const sqlRemove = this.buildExternalFieldRemoveSQL(MjCoreSchema(), entity.Fields, eeFields.map(ef => ef.FieldName));
2363
2484
  if (sqlRemove) {
2364
- await this.LogSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from external entity ${externalEntity.Name}`);
2485
+ await this.logSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from external entity ${externalEntity.Name}`);
2365
2486
  bUpdated = true;
2366
2487
  }
2367
2488
  // create/update each remote column. Use the real PK info from introspection; fall back
@@ -2392,8 +2513,8 @@ export class ManageMetadataBase {
2392
2513
  }
2393
2514
  }
2394
2515
  if (bUpdated) {
2395
- const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${externalEntity.ID}'`;
2396
- await this.LogSQLAndExecute(pool, sqlUpdate, `SQL text to update external entity updated date for ${externalEntity.Name}`);
2516
+ const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${externalEntity.ID}'`;
2517
+ await this.logSQLAndExecute(pool, sqlUpdate, `SQL text to update external entity updated date for ${externalEntity.Name}`);
2397
2518
  }
2398
2519
  return { success: bSuccess, updatedEntity: bUpdated, relationshipsUpdated: bRelationshipsUpdated };
2399
2520
  }
@@ -2450,7 +2571,7 @@ export class ManageMetadataBase {
2450
2571
  if (alreadySet) {
2451
2572
  continue;
2452
2573
  }
2453
- sqlStatements.push(`UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
2574
+ sqlStatements.push(`UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
2454
2575
  SET RelatedEntityID='${relatedEntity.ID}',
2455
2576
  RelatedEntityFieldName='${refColumn.replace(/'/g, "''")}',
2456
2577
  IsSoftForeignKey=1
@@ -2460,7 +2581,7 @@ export class ManageMetadataBase {
2460
2581
  if (sqlStatements.length === 0) {
2461
2582
  return false;
2462
2583
  }
2463
- await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set external foreign keys for ${externalEntity.Name}`);
2584
+ await this.logSQLBatchAndExecute(pool, sqlStatements, `Set external foreign keys for ${externalEntity.Name}`);
2464
2585
  return true;
2465
2586
  }
2466
2587
  async manageSingleVirtualEntity(pool, virtualEntity) {
@@ -2484,9 +2605,9 @@ export class ManageMetadataBase {
2484
2605
  removeList.push(f.ID);
2485
2606
  }
2486
2607
  if (removeList.length > 0) {
2487
- const sqlRemove = `DELETE FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID IN (${removeList.map(removeId => `'${removeId}'`).join(',')})`;
2608
+ const sqlRemove = `DELETE FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID IN (${removeList.map(removeId => `'${removeId}'`).join(',')})`;
2488
2609
  // this removes the fields that shouldn't be there anymore
2489
- await this.LogSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from entity ${virtualEntity.Name}`);
2610
+ await this.logSQLAndExecute(pool, sqlRemove, `SQL text to remove fields from entity ${virtualEntity.Name}`);
2490
2611
  bUpdated = true;
2491
2612
  }
2492
2613
  // check to see if any of the fields in the virtual entity have Pkey attribute set. If not, we will default to the first field
@@ -2506,8 +2627,8 @@ export class ManageMetadataBase {
2506
2627
  }
2507
2628
  if (bUpdated) {
2508
2629
  // finally make sure we update the UpdatedAt field for the entity if we made changes to its fields
2509
- const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${virtualEntity.ID}'`;
2510
- await this.LogSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity updated date for ${virtualEntity.Name}`);
2630
+ const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${virtualEntity.ID}'`;
2631
+ await this.logSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity updated date for ${virtualEntity.Name}`);
2511
2632
  }
2512
2633
  return { success: bSuccess, updatedEntity: bUpdated };
2513
2634
  }
@@ -2575,7 +2696,7 @@ export class ManageMetadataBase {
2575
2696
  field.Sequence !== fieldSequence) {
2576
2697
  // the field needs to be updated, so update it
2577
2698
  const sqlUpdate = `UPDATE
2578
- ${this.qs(mj_core_schema(), 'EntityField')}
2699
+ ${this.qs(MjCoreSchema(), 'EntityField')}
2579
2700
  SET
2580
2701
  Sequence=${fieldSequence},
2581
2702
  Type='${veField.Type}',
@@ -2586,7 +2707,7 @@ export class ManageMetadataBase {
2586
2707
  Scale=${veField.Scale}
2587
2708
  WHERE
2588
2709
  ID = '${field.ID}'`; // don't need to update the __mj_UpdatedAt field here, that happens automatically via the trigger
2589
- await this.LogSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
2710
+ await this.logSQLAndExecute(pool, sqlUpdate, `SQL text to update virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
2590
2711
  didUpdate = true;
2591
2712
  }
2592
2713
  }
@@ -2596,7 +2717,7 @@ export class ManageMetadataBase {
2596
2717
  // Apply-time Sequence: see applyTimeEntityFieldSequenceSQL. A literal here would be
2597
2718
  // replayed verbatim from the CodeGen capture and collide on a fresh install.
2598
2719
  const q = (n) => this.qi(n);
2599
- const sqlAdd = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
2720
+ const sqlAdd = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityField')} (
2600
2721
  ${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
2601
2722
  ${q('Length')}, ${q('Precision')}, ${q('Scale')},
2602
2723
  ${q('Sequence')}, ${q('IsPrimaryKey')}, ${q('IsUnique')},
@@ -2606,7 +2727,7 @@ export class ManageMetadataBase {
2606
2727
  ${this.applyTimeEntityFieldSequenceSQL(entity.ID)}, ${this.boolLit(wantPrimaryKey)}, ${this.boolLit(wantUnique)},
2607
2728
  ${this.utcNow()}, ${this.utcNow()}
2608
2729
  )`;
2609
- await this.LogSQLAndExecute(pool, sqlAdd, `SQL text to add virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
2730
+ await this.logSQLAndExecute(pool, sqlAdd, `SQL text to add virtual entity field ${veField.FieldName} for entity ${virtualEntity.Name}`);
2610
2731
  ManageMetadataBase.registerNewField(entity.ID, veField.FieldName);
2611
2732
  didUpdate = true;
2612
2733
  }
@@ -2702,7 +2823,7 @@ export class ManageMetadataBase {
2702
2823
  return { decorated: false, skipped: false };
2703
2824
  }
2704
2825
  // Apply results to EntityField records
2705
- const schema = mj_core_schema();
2826
+ const schema = MjCoreSchema();
2706
2827
  let anyUpdated = false;
2707
2828
  // Apply primary keys
2708
2829
  anyUpdated = await this.applyLLMPrimaryKeys(pool, entity, result.primaryKeys, schema) || anyUpdated;
@@ -2714,7 +2835,7 @@ export class ManageMetadataBase {
2714
2835
  anyUpdated = await this.applyVEFieldCategories(pool, entity, result) || anyUpdated;
2715
2836
  if (anyUpdated) {
2716
2837
  const sqlUpdate = `UPDATE ${this.qs(schema, 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${entity.ID}'`;
2717
- await this.LogSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${entity.Name} after LLM decoration`);
2838
+ await this.logSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${entity.Name} after LLM decoration`);
2718
2839
  }
2719
2840
  return { decorated: anyUpdated, skipped: false };
2720
2841
  }
@@ -2768,7 +2889,7 @@ export class ManageMetadataBase {
2768
2889
  return false;
2769
2890
  }
2770
2891
  // Load VE EntityField rows from DB (we need the ID and auto-update flags)
2771
- const schema = mj_core_schema();
2892
+ const schema = MjCoreSchema();
2772
2893
  const fieldsSQL = `
2773
2894
  SELECT ID, Name, Category, AutoUpdateCategory, AutoUpdateDisplayName, AutoUpdateExtendedType, GeneratedFormSection, DisplayName, ExtendedType, CodeType, ValueListType
2774
2895
  FROM ${this.qs(schema, 'EntityField')}
@@ -2832,7 +2953,7 @@ export class ManageMetadataBase {
2832
2953
  WHERE EntityID='${entity.ID}' AND Name='${pk}'`);
2833
2954
  logStatus(` ✓ Set PK for ${entity.Name}.${pk} (LLM-identified)`);
2834
2955
  }
2835
- await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified PKs for ${entity.Name}: ${validPKs.join(', ')}`);
2956
+ await this.logSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified PKs for ${entity.Name}: ${validPKs.join(', ')}`);
2836
2957
  return true;
2837
2958
  }
2838
2959
  /**
@@ -2877,7 +2998,7 @@ export class ManageMetadataBase {
2877
2998
  if (sqlStatements.length === 0) {
2878
2999
  return false;
2879
3000
  }
2880
- await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified FKs for ${entity.Name}`);
3001
+ await this.logSQLBatchAndExecute(pool, sqlStatements, `Set LLM-identified FKs for ${entity.Name}`);
2881
3002
  return true;
2882
3003
  }
2883
3004
  /**
@@ -2917,7 +3038,7 @@ export class ManageMetadataBase {
2917
3038
  if (sqlStatements.length === 0) {
2918
3039
  return false;
2919
3040
  }
2920
- await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set LLM-generated descriptions for ${entity.Name} (${sqlStatements.length} fields)`);
3041
+ await this.logSQLBatchAndExecute(pool, sqlStatements, `Set LLM-generated descriptions for ${entity.Name} (${sqlStatements.length} fields)`);
2921
3042
  return true;
2922
3043
  }
2923
3044
  /**
@@ -3042,7 +3163,7 @@ export class ManageMetadataBase {
3042
3163
  // unquoted reference folds to lower case on PostgreSQL — `column "length" does not exist`.
3043
3164
  // SQL Server's case-insensitive resolution hid this for as long as IS-A ran only there.
3044
3165
  const existsResult = await this.runQueryWithParams(pool, `SELECT ${this.qi('ID')}, ${this.qi('IsVirtual')}, ${this.qi('Type')}, ${this.qi('Length')}, ${this.qi('Precision')}, ${this.qi('Scale')}, ${this.qi('AllowsNull')}, ${this.qi('AllowUpdateAPI')}
3045
- FROM ${this.qs(mj_core_schema(), 'EntityField')}
3166
+ FROM ${this.qs(MjCoreSchema(), 'EntityField')}
3046
3167
  WHERE ${this.qi('EntityID')} = @EntityID AND ${this.qi('Name')} = @FieldName`, { 'EntityID': childEntity.ID, 'FieldName': parentField.Name });
3047
3168
  if (existsResult.recordset.length > 0) {
3048
3169
  // Field already exists — update it to ensure it's marked as a virtual IS-A field
@@ -3055,7 +3176,7 @@ export class ManageMetadataBase {
3055
3176
  existingRow.AllowsNull !== parentField.AllowsNull ||
3056
3177
  !existingRow.AllowUpdateAPI;
3057
3178
  if (needsUpdate) {
3058
- const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
3179
+ const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
3059
3180
  SET ${this.qi('IsVirtual')}=${this.boolLit(true)},
3060
3181
  ${this.qi('Type')}='${parentField.Type}',
3061
3182
  ${this.qi('Length')}=${parentField.Length},
@@ -3064,7 +3185,7 @@ export class ManageMetadataBase {
3064
3185
  ${this.qi('AllowsNull')}=${this.boolLit(parentField.AllowsNull)},
3065
3186
  ${this.qi('AllowUpdateAPI')}=${this.boolLit(true)}
3066
3187
  WHERE ${this.qi('ID')}='${existingRow.ID}'`;
3067
- await this.LogSQLAndExecute(pool, sqlUpdate, `Update IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
3188
+ await this.logSQLAndExecute(pool, sqlUpdate, `Update IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
3068
3189
  bUpdated = true;
3069
3190
  }
3070
3191
  }
@@ -3074,7 +3195,7 @@ export class ManageMetadataBase {
3074
3195
  // Apply-time Sequence (see applyTimeEntityFieldSequenceSQL); reordered by
3075
3196
  // updateExistingEntityFieldsFromSchema afterwards.
3076
3197
  const q = (n) => this.qi(n);
3077
- const sqlInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')} (
3198
+ const sqlInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityField')} (
3078
3199
  ${q('ID')}, ${q('EntityID')}, ${q('Name')}, ${q('Type')}, ${q('AllowsNull')},
3079
3200
  ${q('Length')}, ${q('Precision')}, ${q('Scale')},
3080
3201
  ${q('Sequence')}, ${q('IsVirtual')}, ${q('AllowUpdateAPI')},
@@ -3086,7 +3207,7 @@ export class ManageMetadataBase {
3086
3207
  ${parentField.Length}, ${parentField.Precision}, ${parentField.Scale},
3087
3208
  ${this.applyTimeEntityFieldSequenceSQL(childEntity.ID)}, ${this.boolLit(true)}, ${this.boolLit(true)}, ${this.boolLit(false)}, ${this.boolLit(false)},
3088
3209
  ${this.utcNow()}, ${this.utcNow()})`;
3089
- await this.LogSQLAndExecute(pool, sqlInsert, `Create IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
3210
+ await this.logSQLAndExecute(pool, sqlInsert, `Create IS-A parent field ${parentField.Name} on ${childEntity.Name}`);
3090
3211
  ManageMetadataBase.registerNewField(childEntity.ID, parentField.Name);
3091
3212
  bUpdated = true;
3092
3213
  }
@@ -3098,13 +3219,13 @@ export class ManageMetadataBase {
3098
3219
  !f.IsPrimaryKey && !f.Name.startsWith('__mj_') &&
3099
3220
  !currentParentFieldNames.has(f.Name.toLowerCase()));
3100
3221
  for (const staleField of staleFields) {
3101
- const sqlDelete = `DELETE FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID='${staleField.ID}'`;
3102
- await this.LogSQLAndExecute(pool, sqlDelete, `Remove stale IS-A parent field ${staleField.Name} from ${childEntity.Name}`);
3222
+ const sqlDelete = `DELETE FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID='${staleField.ID}'`;
3223
+ await this.logSQLAndExecute(pool, sqlDelete, `Remove stale IS-A parent field ${staleField.Name} from ${childEntity.Name}`);
3103
3224
  bUpdated = true;
3104
3225
  }
3105
3226
  if (bUpdated) {
3106
- const sqlUpdate = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${childEntity.ID}'`;
3107
- await this.LogSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${childEntity.Name} after IS-A field sync`);
3227
+ const sqlUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()} WHERE ID='${childEntity.ID}'`;
3228
+ await this.logSQLAndExecute(pool, sqlUpdate, `Update entity timestamp for ${childEntity.Name} after IS-A field sync`);
3108
3229
  }
3109
3230
  return { success: true, updated: bUpdated };
3110
3231
  }
@@ -3155,16 +3276,16 @@ export class ManageMetadataBase {
3155
3276
  try {
3156
3277
  // STEP 1 - search for all foreign keys in the vwEntityFields view, we use the RelatedEntityID field to determine our FKs
3157
3278
  const sSQL = `SELECT *
3158
- FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
3279
+ FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
3159
3280
  WHERE
3160
3281
  RelatedEntityID IS NOT NULL AND
3161
3282
  IsVirtual = ${this.boolLit(false)} AND
3162
- EntityID NOT IN (SELECT ID FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
3283
+ EntityID NOT IN (SELECT ID FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
3163
3284
  ORDER BY RelatedEntityID, EntityID, Sequence, ID`;
3164
3285
  const entityFieldsResult = await this.runQuery(pool, sSQL);
3165
3286
  const entityFields = entityFieldsResult.recordset;
3166
3287
  // Get the relationship counts for each entity
3167
- const sSQLRelationshipCount = `SELECT ${this.qi('EntityID')}, COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(mj_core_schema(), 'EntityRelationship')} GROUP BY ${this.qi('EntityID')}`;
3288
+ const sSQLRelationshipCount = `SELECT ${this.qi('EntityID')}, COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')} GROUP BY ${this.qi('EntityID')}`;
3168
3289
  const relationshipCountsResult = await this.runQuery(pool, sSQLRelationshipCount);
3169
3290
  const relationshipCounts = relationshipCountsResult.recordset;
3170
3291
  const relationshipCountMap = new Map();
@@ -3173,7 +3294,7 @@ export class ManageMetadataBase {
3173
3294
  relationshipCountMap.set(rc.EntityID, Number(rc.Count));
3174
3295
  }
3175
3296
  // get all relationships in one query for performance improvement
3176
- const sSQLRelationship = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityRelationship')}`;
3297
+ const sSQLRelationship = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')}`;
3177
3298
  const allRelationshipsResult = await this.runQuery(pool, sSQLRelationship);
3178
3299
  const allRelationships = allRelationshipsResult.recordset;
3179
3300
  // Group FK fields by entity pair (ParentEntityID -> ChildEntityID) so we can
@@ -3199,12 +3320,12 @@ export class ManageMetadataBase {
3199
3320
  }
3200
3321
  batchCount++;
3201
3322
  if (batchCount % batchItems === 0 && batchSQL.length > 0) {
3202
- await this.LogSQLAndExecute(pool, batchSQL);
3323
+ await this.logSQLAndExecute(pool, batchSQL);
3203
3324
  batchSQL = '';
3204
3325
  }
3205
3326
  }
3206
3327
  if (batchSQL.length > 0) {
3207
- await this.LogSQLAndExecute(pool, batchSQL);
3328
+ await this.logSQLAndExecute(pool, batchSQL);
3208
3329
  }
3209
3330
  // NOTE: Stale relationship cleanup is intentionally NOT done here because this
3210
3331
  // method runs BEFORE deleteUnneededEntityFields(). Stale EntityField records
@@ -3245,8 +3366,8 @@ export class ManageMetadataBase {
3245
3366
  const relCount = relationshipCountMap.get(parentEntityID) || 0;
3246
3367
  const sequence = relCount + 1;
3247
3368
  const newEntityRelationshipUUID = this.createNewUUID();
3248
- const checkQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${newEntityRelationshipUUID}'`;
3249
- const insertSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityRelationship')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('RelatedEntityID')}, ${this.qi('RelatedEntityJoinField')}, ${this.qi('Type')}, ${this.qi('BundleInAPI')}, ${this.qi('DisplayInForm')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
3369
+ const checkQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${newEntityRelationshipUUID}'`;
3370
+ const insertSQL = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityRelationship')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('RelatedEntityID')}, ${this.qi('RelatedEntityJoinField')}, ${this.qi('Type')}, ${this.qi('BundleInAPI')}, ${this.qi('DisplayInForm')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
3250
3371
  VALUES ('${newEntityRelationshipUUID}', '${f.RelatedEntityID}', '${f.EntityID}', '${f.Name}', 'One To Many', ${this.boolLit(true)}, ${this.boolLit(true)}, ${sequence}, ${this.utcNow()}, ${this.utcNow()})`;
3251
3372
  relationshipCountMap.set(parentEntityID, sequence);
3252
3373
  return `
@@ -3308,7 +3429,7 @@ export class ManageMetadataBase {
3308
3429
  logStatus(` > Updating EntityRelationship join field: ${oldJoinField} -> ${newJoinField} (ID: ${r.ID})`);
3309
3430
  sql += `
3310
3431
  /* Update EntityRelationship join field from '${oldJoinField}' to '${newJoinField}' */
3311
- UPDATE ${this.qs(mj_core_schema(), 'EntityRelationship')}
3432
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityRelationship')}
3312
3433
  SET ${this.qi('RelatedEntityJoinField')} = '${newJoinField}',
3313
3434
  ${this.qi('__mj_UpdatedAt')} = ${this.utcNow()}
3314
3435
  WHERE ${this.qi('ID')} = '${r.ID}';
@@ -3330,18 +3451,18 @@ export class ManageMetadataBase {
3330
3451
  * @param pool - database connection
3331
3452
  * @param excludeSchemas - schemas to exclude from FK field lookup
3332
3453
  */
3333
- async cleanupStaleEntityRelationships(pool, excludeSchemas) {
3454
+ async CleanupStaleEntityRelationships(pool, excludeSchemas) {
3334
3455
  try {
3335
3456
  const sSQL = `SELECT *
3336
- FROM ${this.qs(mj_core_schema(), 'vwEntityFields')}
3457
+ FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')}
3337
3458
  WHERE
3338
3459
  RelatedEntityID IS NOT NULL AND
3339
3460
  IsVirtual = ${this.boolLit(false)} AND
3340
- EntityID NOT IN (SELECT ID FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
3461
+ EntityID NOT IN (SELECT ID FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE SchemaName IN (${excludeSchemas.map(s => `'${s}'`).join(',')}))
3341
3462
  ORDER BY RelatedEntityID, EntityID, Sequence, ID`;
3342
3463
  const entityFieldsResult = await this.runQuery(pool, sSQL);
3343
3464
  const entityFields = entityFieldsResult.recordset;
3344
- const sSQLRelationship = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityRelationship')}`;
3465
+ const sSQLRelationship = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')}`;
3345
3466
  const allRelationshipsResult = await this.runQuery(pool, sSQLRelationship);
3346
3467
  const allRelationships = allRelationshipsResult.recordset;
3347
3468
  await this.removeStaleOneToManyRelationships(pool, allRelationships, entityFields);
@@ -3352,6 +3473,10 @@ export class ManageMetadataBase {
3352
3473
  return false;
3353
3474
  }
3354
3475
  }
3476
+ /** @deprecated Use {@link CleanupStaleEntityRelationships}. */
3477
+ async cleanupStaleEntityRelationships(pool, excludeSchemas) {
3478
+ return this.CleanupStaleEntityRelationships(pool, excludeSchemas);
3479
+ }
3355
3480
  /**
3356
3481
  * Removes stale One-To-Many EntityRelationship records whose RelatedEntityJoinField no longer
3357
3482
  * corresponds to a valid FK field in the database. Only removes relationships where
@@ -3374,10 +3499,10 @@ export class ManageMetadataBase {
3374
3499
  logStatus(` > Removing stale EntityRelationship: ${r.Entity} -> ${r.RelatedEntity} via '${r.RelatedEntityJoinField}' (ID: ${r.ID})`);
3375
3500
  deleteSQL += `
3376
3501
  /* Remove stale EntityRelationship: ${r.Entity} -> ${r.RelatedEntity} (FK field '${r.RelatedEntityJoinField}' no longer exists) */
3377
- DELETE FROM ${this.qs(mj_core_schema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${r.ID}';
3502
+ DELETE FROM ${this.qs(MjCoreSchema(), 'EntityRelationship')} WHERE ${this.qi('ID')} = '${r.ID}';
3378
3503
  `;
3379
3504
  }
3380
- await this.LogSQLAndExecute(pool, deleteSQL, 'Remove stale One-To-Many EntityRelationships');
3505
+ await this.logSQLAndExecute(pool, deleteSQL, 'Remove stale One-To-Many EntityRelationships');
3381
3506
  }
3382
3507
  }
3383
3508
  /**
@@ -3388,7 +3513,7 @@ export class ManageMetadataBase {
3388
3513
  */
3389
3514
  async checkAndRemoveMetadataForDeletedTables(pool, excludeSchemas) {
3390
3515
  try {
3391
- const sql = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntitiesWithMissingBaseTables')}${this.dbProvider.getEntitiesWithMissingBaseTablesFilter()}`;
3516
+ const sql = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntitiesWithMissingBaseTables')}${this.dbProvider.getEntitiesWithMissingBaseTablesFilter()}`;
3392
3517
  const entitiesResult = await this.runQuery(pool, sql);
3393
3518
  const entities = entitiesResult.recordset;
3394
3519
  if (entities && entities.length > 0) {
@@ -3411,8 +3536,8 @@ export class ManageMetadataBase {
3411
3536
  // discardResult: this routine only performs deletions — nothing below reads rows
3412
3537
  // from it — and on PostgreSQL it is declared RETURNS SETOF record, which cannot be
3413
3538
  // invoked through the default `SELECT * FROM` form at all.
3414
- const sqlDelete = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spDeleteEntityWithCoreDependencies', [`'${e.ID}'`], ['EntityID'], true);
3415
- await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove entity ${e.Name}`);
3539
+ const sqlDelete = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spDeleteEntityWithCoreDependencies', [`'${e.ID}'`], ['EntityID'], true);
3540
+ await this.logSQLAndExecute(pool, sqlDelete, `SQL text to remove entity ${e.Name}`);
3416
3541
  logStatus(` > Removed metadata for table ${e.SchemaName}.${e.BaseTable}`);
3417
3542
  // Recorded only after the delete succeeds, so a failed removal (logged below for
3418
3543
  // an admin to handle) does not mark a schema dirty for an entity still present.
@@ -3449,9 +3574,9 @@ export class ManageMetadataBase {
3449
3574
  // Object type codes: P = Stored Procedure, V = View, FN = Scalar Function, IF/TF = Table-Valued Function
3450
3575
  const upperType = type.toUpperCase();
3451
3576
  const sqlDelete = this.dbProvider.dropObjectSQL(upperType, schemaName, name);
3452
- await this.LogSQLAndExecute(pool, sqlDelete, `SQL text to remove ${type} ${schemaName}.${name}`);
3577
+ await this.logSQLAndExecute(pool, sqlDelete, `SQL text to remove ${type} ${schemaName}.${name}`);
3453
3578
  // next up, we need to clean up the cache of saved DB objects that may exist for this entity in the appropriate sub-directory.
3454
- const sqlOutputDir = outputDir('SQL', true);
3579
+ const sqlOutputDir = OutputDir('SQL', true);
3455
3580
  if (sqlOutputDir) {
3456
3581
  // now do the same thing for the /schema directory within the provided directory
3457
3582
  const fType = type === 'procedure' ? 'sp' : type === 'view' ? 'view' : 'full_text_search_function';
@@ -3525,7 +3650,7 @@ export class ManageMetadataBase {
3525
3650
  * which is the typical "no schema changes since last run" case in Pass 2.
3526
3651
  * `undefined` (default) preserves prior full-scan behavior.
3527
3652
  */
3528
- async manageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields = false, entityFilter) {
3653
+ async ManageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields = false, entityFilter) {
3529
3654
  let bSuccess = true;
3530
3655
  const startTime = new Date();
3531
3656
  // Fast-exit: an explicit empty filter means "no entities changed in Pass 1, so Pass 2 has nothing to do"
@@ -3578,9 +3703,9 @@ export class ManageMetadataBase {
3578
3703
  // Fix: derive virtual field AllowsNull from the FK column that drives the JOIN.
3579
3704
  // The virtual field name = FK field name minus trailing "ID" (e.g. ClassID → Class).
3580
3705
  if (this.dbProvider.NeedsVirtualFieldNullabilityFix) {
3581
- const fixSQL = this.dbProvider.getFixVirtualFieldNullabilitySQL(mj_core_schema());
3706
+ const fixSQL = this.dbProvider.getFixVirtualFieldNullabilitySQL(MjCoreSchema());
3582
3707
  if (fixSQL) {
3583
- await this.LogSQLAndExecute(pool, fixSQL, 'SQL to fix virtual field nullability');
3708
+ await this.logSQLAndExecute(pool, fixSQL, 'SQL to fix virtual field nullability');
3584
3709
  }
3585
3710
  }
3586
3711
  // AN: 14-June-2025 - we are now running this AFTER we create new entity fields from schema
@@ -3649,17 +3774,37 @@ export class ManageMetadataBase {
3649
3774
  // Advanced Generation - Smart field identification and form layout
3650
3775
  if (!skipAdvancedGeneration) {
3651
3776
  const step7StartTime = new Date();
3652
- startSpinner('Applying AI-powered advanced generation (smart fields, form layout)...');
3777
+ StartSpinner('Applying AI-powered advanced generation (smart fields, form layout)...');
3653
3778
  if (!await this.applyAdvancedGeneration(pool, excludeSchemas, currentUser)) {
3654
3779
  logError('Error applying advanced generation features');
3655
3780
  // Don't fail the entire process - advanced generation is optional
3656
3781
  }
3657
3782
  const step7Elapsed = ((new Date().getTime() - step7StartTime.getTime()) / 1000).toFixed(1);
3658
- succeedSpinner(`Advanced generation completed (${step7Elapsed}s)`);
3783
+ SucceedSpinner(`Advanced generation completed (${step7Elapsed}s)`);
3784
+ }
3785
+ // Deterministic search-flag hygiene. Deliberately OUTSIDE the `skipAdvancedGeneration`
3786
+ // guard above: advanced generation is off by default, and an entity created under that
3787
+ // default is precisely the one that ends up flagged searchable with nothing searchable on
3788
+ // it. Ordered after it so the model's choices, where it did run, are already applied and
3789
+ // the seed only fills a genuine gap. See buildSearchFlagHygieneSQL.
3790
+ if (!await this.applySearchFlagHygiene(pool, excludeSchemas)) {
3791
+ logError('Error applying search flag hygiene');
3792
+ // FATAL, unlike advanced generation. This pass compares before it writes, so a failure
3793
+ // means the probes could not run — and because nothing is written in that case, the pass
3794
+ // failing is indistinguishable from the pass having nothing to do: no SQL, no artifact,
3795
+ // every gate green. Advanced generation can degrade quietly because it only ever ADDS
3796
+ // model-suggested metadata; this one turns search OFF on entities and is the only thing
3797
+ // keeping AllowUserSearchAPI honest, so a run where it did not execute must not be
3798
+ // reported as a clean run.
3799
+ bSuccess = false;
3659
3800
  }
3660
3801
  logStatus(` Total time to manage entity fields: ${(new Date().getTime() - startTime.getTime()) / 1000} seconds`);
3661
3802
  return bSuccess;
3662
3803
  }
3804
+ /** @deprecated Use {@link ManageEntityFields}. */
3805
+ async manageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields = false, entityFilter) {
3806
+ return this.ManageEntityFields(pool, excludeSchemas, skipCreatedAtUpdatedAtDeletedAtFieldValidation, skipEntityFieldValues, currentUser, skipAdvancedGeneration, skipDeleteUnneededFields, entityFilter);
3807
+ }
3663
3808
  /**
3664
3809
  * This method ensures that the __mj_DeletedAt field exists in each entity that has DeleteType=Soft. If the field does not exist, it is created.
3665
3810
  */
@@ -3668,7 +3813,7 @@ export class ManageMetadataBase {
3668
3813
  const sqlEntities = `SELECT
3669
3814
  *
3670
3815
  FROM
3671
- ${this.qs(mj_core_schema(), 'vwEntities')}
3816
+ ${this.qs(MjCoreSchema(), 'vwEntities')}
3672
3817
  WHERE
3673
3818
  VirtualEntity=${this.boolLit(false)} AND
3674
3819
  DeleteType='Soft' AND
@@ -3729,7 +3874,7 @@ export class ManageMetadataBase {
3729
3874
  }
3730
3875
  let totalPKs = 0;
3731
3876
  let totalFKs = 0;
3732
- const schema = mj_core_schema();
3877
+ const schema = MjCoreSchema();
3733
3878
  // Config supports two formats:
3734
3879
  // 1. Schema-as-key (template format): { "dbo": [{ "TableName": "Orders", ... }] }
3735
3880
  // 2. Flat tables array (legacy format): { "tables": [{ "SchemaName": "dbo", "TableName": "Orders", ... }] }
@@ -3768,7 +3913,7 @@ export class ManageMetadataBase {
3768
3913
  ${this.qi('IsPrimaryKey')} = ${this.boolLit(true)},
3769
3914
  ${this.qi('IsSoftPrimaryKey')} = ${this.boolLit(true)}
3770
3915
  WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${pk.FieldName}'`;
3771
- const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft PK for ${tableSchema}.${tableName}.${pk.FieldName}`);
3916
+ const result = await this.logSQLAndExecute(pool, sSQL, `Set soft PK for ${tableSchema}.${tableName}.${pk.FieldName}`);
3772
3917
  if (result !== null) {
3773
3918
  logStatus(` ✓ Set IsPrimaryKey=1, IsSoftPrimaryKey=1 for ${tableName}.${pk.FieldName}`);
3774
3919
  totalPKs++;
@@ -3812,7 +3957,7 @@ export class ManageMetadataBase {
3812
3957
  ${this.qi('IsSoftForeignKey')} = ${this.boolLit(true)},
3813
3958
  ${this.qi('AutoUpdateRelatedEntityInfo')} = ${this.boolLit(false)}
3814
3959
  WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = '${fk.FieldName}'`;
3815
- const result = await this.LogSQLAndExecute(pool, sSQL, `Set soft FK for ${tableSchema}.${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
3960
+ const result = await this.logSQLAndExecute(pool, sSQL, `Set soft FK for ${tableSchema}.${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
3816
3961
  if (result !== null) {
3817
3962
  logStatus(` ✓ Set soft FK for ${tableName}.${fk.FieldName} → ${fk.RelatedTable}.${fk.RelatedField}`);
3818
3963
  totalFKs++;
@@ -3843,7 +3988,7 @@ export class ManageMetadataBase {
3843
3988
  return true;
3844
3989
  try {
3845
3990
  // Load all existing EntityFieldValue rows for dedup checking
3846
- const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
3991
+ const efvSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
3847
3992
  const allEFVResult = await this.runQuery(pool, efvSQL);
3848
3993
  const allEntityFieldValues = allEFVResult.recordset;
3849
3994
  let totalApplied = 0;
@@ -3869,7 +4014,7 @@ export class ManageMetadataBase {
3869
4014
  * EntityFieldValue rows already exist (from CHECK constraints) and skips if so.
3870
4015
  */
3871
4016
  async applyValueListsForTable(pool, schemaName, tableName, fields, allEntityFieldValues) {
3872
- const schema = mj_core_schema();
4017
+ const schema = MjCoreSchema();
3873
4018
  let applied = 0;
3874
4019
  let skipped = 0;
3875
4020
  // Look up entity ID
@@ -3915,7 +4060,7 @@ export class ManageMetadataBase {
3915
4060
  const newType = fieldCfg.ValueListType;
3916
4061
  if (currentValueListType !== newType.toLowerCase()) {
3917
4062
  const updateSQL = `UPDATE ${this.qs(schema, 'EntityField')} SET ValueListType='${newType}' WHERE ID='${entityFieldID}'`;
3918
- await this.LogSQLAndExecute(pool, updateSQL, `Set soft ValueListType='${newType}' for ${schemaName}.${tableName}.${fieldCfg.FieldName}`);
4063
+ await this.logSQLAndExecute(pool, updateSQL, `Set soft ValueListType='${newType}' for ${schemaName}.${tableName}.${fieldCfg.FieldName}`);
3919
4064
  }
3920
4065
  logStatus(` ✓ Applied soft value-list for ${tableName}.${fieldCfg.FieldName} (${sortedValues.length} values, type=${newType})`);
3921
4066
  return 'applied';
@@ -3937,7 +4082,7 @@ export class ManageMetadataBase {
3937
4082
  const sqlEntities = `SELECT
3938
4083
  *
3939
4084
  FROM
3940
- ${this.qs(mj_core_schema(), 'vwEntities')}
4085
+ ${this.qs(MjCoreSchema(), 'vwEntities')}
3941
4086
  WHERE
3942
4087
  VirtualEntity = ${this.boolLit(false)} AND
3943
4088
  ${externalEntityClause}
@@ -4001,7 +4146,7 @@ export class ManageMetadataBase {
4001
4146
  // and the subsequent UPDATE referencing that column run in separate batches.
4002
4147
  // SQL Server compiles an entire batch before executing, so without the separator
4003
4148
  // the UPDATE fails with "Invalid column name" since the column doesn't exist yet at compile time.
4004
- await this.LogSQLAndExecute(pool, stmt, `SQL text to add special date field ${fieldName} to entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
4149
+ await this.logSQLAndExecute(pool, stmt, `SQL text to add special date field ${fieldName} to entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
4005
4150
  }
4006
4151
  }
4007
4152
  else {
@@ -4014,7 +4159,7 @@ export class ManageMetadataBase {
4014
4159
  // modifying the column, and finally adding the default constraint back in.
4015
4160
  await this.dropExistingDefaultConstraint(pool, entity, fieldName);
4016
4161
  const sql = this.dbProvider.alterColumnTypeAndNullabilitySQL(entity.SchemaName, entity.BaseTable, fieldName, this.timestampType, allowNull);
4017
- await this.LogSQLAndExecute(pool, sql, `SQL text to update special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
4162
+ await this.logSQLAndExecute(pool, sql, `SQL text to update special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
4018
4163
  if (!allowNull)
4019
4164
  await this.createDefaultConstraintForSpecialDateField(pool, entity, fieldName);
4020
4165
  }
@@ -4044,7 +4189,7 @@ export class ManageMetadataBase {
4044
4189
  async createDefaultConstraintForSpecialDateField(pool, entity, fieldName) {
4045
4190
  try {
4046
4191
  const sqlAddDefaultConstraint = this.dbProvider.addDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName, this.utcNow());
4047
- await this.LogSQLAndExecute(pool, sqlAddDefaultConstraint, `SQL text to add default constraint for special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
4192
+ await this.logSQLAndExecute(pool, sqlAddDefaultConstraint, `SQL text to add default constraint for special date field ${fieldName} in entity ${entity.SchemaName}.${entity.BaseTable}`);
4048
4193
  }
4049
4194
  catch (e) {
4050
4195
  logError(e);
@@ -4071,7 +4216,7 @@ export class ManageMetadataBase {
4071
4216
  try {
4072
4217
  const sqlDropDefaultConstraint = this.dbProvider.dropDefaultConstraintSQL(entity.SchemaName, entity.BaseTable, fieldName);
4073
4218
  // DECLARE-bearing block: close its batch in the replayable log (see SQLLogging.appendToSQLLogFile).
4074
- await this.LogSQLAndExecute(pool, sqlDropDefaultConstraint, `SQL text to drop default existing default constraints in entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
4219
+ await this.logSQLAndExecute(pool, sqlDropDefaultConstraint, `SQL text to drop default existing default constraints in entity ${entity.SchemaName}.${entity.BaseTable}`, false, true, this.dbProvider.BatchSeparator);
4075
4220
  }
4076
4221
  catch (e) {
4077
4222
  logError(e);
@@ -4096,15 +4241,15 @@ export class ManageMetadataBase {
4096
4241
  // "Unclosed quotation mark" (SQL Server) at the very end of an otherwise-complete
4097
4242
  // pass. '' doubling is correct on both supported dialects.
4098
4243
  const esc = (s) => s.replace(/'/g, "''");
4099
- const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntities')} WHERE Name = '${esc(e)}'`);
4244
+ const dataResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntities')} WHERE Name = '${esc(e)}'`);
4100
4245
  const data = dataResult.recordset;
4101
- const fieldsResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} WHERE EntityID='${data[0].ID}'`);
4246
+ const fieldsResult = await this.runQuery(pool, `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')} WHERE EntityID='${data[0].ID}'`);
4102
4247
  const fields = fieldsResult.recordset;
4103
4248
  // Use new API to generate entity description
4104
4249
  const result = await ag.generateEntityDescription(e, data[0].BaseTable, fields.map((f) => ({ Name: f.Name, Type: f.Type, IsNullable: f.AllowsNull, Description: f.Description })), currentUser);
4105
4250
  if (result?.entityDescription && result.entityDescription.length > 0) {
4106
- const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'Entity')} SET ${this.qi('Description')} = '${esc(result.entityDescription)}', ${this.qi('AutoUpdateDescription')} = ${this.boolLit(false)} WHERE ${this.qi('Name')} = '${esc(e)}'`;
4107
- await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
4251
+ const sSQL = `UPDATE ${this.qs(MjCoreSchema(), 'Entity')} SET ${this.qi('Description')} = '${esc(result.entityDescription)}', ${this.qi('AutoUpdateDescription')} = ${this.boolLit(false)} WHERE ${this.qi('Name')} = '${esc(e)}'`;
4252
+ await this.logSQLAndExecute(pool, sSQL, `SQL text to update entity description for entity ${e}`);
4108
4253
  }
4109
4254
  else {
4110
4255
  console.warn(' >>> Advanced Generation Error: LLM returned invalid result, skipping entity description for entity ' + e);
@@ -4125,9 +4270,9 @@ export class ManageMetadataBase {
4125
4270
  const sql = `SELECT
4126
4271
  ef.ID, ef.Name
4127
4272
  FROM
4128
- ${this.qs(mj_core_schema(), 'vwEntityFields')} ef
4273
+ ${this.qs(MjCoreSchema(), 'vwEntityFields')} ef
4129
4274
  INNER JOIN
4130
- ${this.qs(mj_core_schema(), 'vwEntities')} e
4275
+ ${this.qs(MjCoreSchema(), 'vwEntities')} e
4131
4276
  ON
4132
4277
  ef.EntityID = e.ID
4133
4278
  WHERE
@@ -4142,8 +4287,8 @@ export class ManageMetadataBase {
4142
4287
  const namingOptions = configInfo.entityNaming?.normalizeFieldNames !== false ? this.getEntityNamingOptions() : undefined;
4143
4288
  const sDisplayName = stripTrailingChars(createDisplayName(field.Name, namingOptions), 'ID', true).trim();
4144
4289
  if (sDisplayName.length > 0 && sDisplayName.toLowerCase().trim() !== field.Name.toLowerCase().trim()) {
4145
- const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'EntityField')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()}, DisplayName = '${sDisplayName}' WHERE ID = '${field.ID}'`;
4146
- await this.LogSQLAndExecute(pool, sSQL, `SQL text to update display name for field ${field.Name}`);
4290
+ const sSQL = `UPDATE ${this.qs(MjCoreSchema(), 'EntityField')} SET ${this.qi(EntityInfo.UpdatedAtFieldName)}=${this.utcNow()}, DisplayName = '${sDisplayName}' WHERE ID = '${field.ID}'`;
4291
+ await this.logSQLAndExecute(pool, sSQL, `SQL text to update display name for field ${field.Name}`);
4147
4292
  }
4148
4293
  }
4149
4294
  return true;
@@ -4163,12 +4308,12 @@ export class ManageMetadataBase {
4163
4308
  */
4164
4309
  async setDefaultColumnWidthWhereNeeded(pool, excludeSchemas) {
4165
4310
  try {
4166
- const heal = buildHealSchemaRoutineParams({
4167
- authoredExclude: getAuthoredExcludeSchemas(excludeSchemas),
4311
+ const heal = BuildHealSchemaRoutineParams({
4312
+ authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
4168
4313
  includeSchemas: configInfo.includeSchemas,
4169
4314
  });
4170
- const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spSetDefaultColumnWidthWhereNeeded', heal.values, heal.names);
4171
- await this.LogSQLAndExecute(pool, sSQL, `SQL text to set default column width where needed`, true);
4315
+ const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spSetDefaultColumnWidthWhereNeeded', heal.Values, heal.Names);
4316
+ await this.logSQLAndExecute(pool, sSQL, `SQL text to set default column width where needed`, true);
4172
4317
  return true;
4173
4318
  }
4174
4319
  catch (e) {
@@ -4188,7 +4333,7 @@ export class ManageMetadataBase {
4188
4333
  * @returns {string} - The SQL statement to retrieve pending entity fields.
4189
4334
  */
4190
4335
  getPendingEntityFieldsSELECTSQL(entityIDs, excludeSchemas) {
4191
- const schema = mj_core_schema();
4336
+ const schema = MjCoreSchema();
4192
4337
  return this.dbProvider.getPendingEntityFieldsSQL(schema, entityIDs, excludeSchemas);
4193
4338
  }
4194
4339
  /**
@@ -4231,7 +4376,7 @@ export class ManageMetadataBase {
4231
4376
  const quotedDefaultValue = (parsedDefaultValue == null || parsedDefaultValue.trim().length === 0)
4232
4377
  ? 'NULL'
4233
4378
  : (parsedDefaultValue.trim().toLowerCase() === 'null' ? 'NULL' : `'${escapedParsedDefault}'`);
4234
- const conflictCheck = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID = '${newEntityFieldUUID}' OR (EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')`;
4379
+ const conflictCheck = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID = '${newEntityFieldUUID}' OR (EntityID = '${n.EntityID}' AND Name = '${n.FieldName}')`;
4235
4380
  const guard = this.dbProvider.wrapInsertWithConflictGuard(conflictCheck);
4236
4381
  // Sequence is an apply-time expression, never a literal: see applyTimeEntityFieldSequenceSQL.
4237
4382
  // `n.Sequence` (MAX+100000+ordinal from the pending SELECT) is only that query's ORDER BY key
@@ -4239,7 +4384,7 @@ export class ManageMetadataBase {
4239
4384
  const sequenceExpr = this.applyTimeEntityFieldSequenceSQL(n.EntityID);
4240
4385
  return `
4241
4386
  ${guard.prefix}
4242
- INSERT INTO ${this.qs(mj_core_schema(), 'EntityField')}
4387
+ INSERT INTO ${this.qs(MjCoreSchema(), 'EntityField')}
4243
4388
  (
4244
4389
  ${this.qi('ID')},
4245
4390
  ${this.qi('EntityID')},
@@ -4354,7 +4499,7 @@ export class ManageMetadataBase {
4354
4499
  for (let i = 0; i < inserts.length; i += CHUNK_SIZE) {
4355
4500
  const chunk = inserts.slice(i, i + CHUNK_SIZE);
4356
4501
  try {
4357
- await this.LogSQLBatchAndExecute(pool, chunk, `SQL text to insert ${chunk.length} new entity field(s)`);
4502
+ await this.logSQLBatchAndExecute(pool, chunk, `SQL text to insert ${chunk.length} new entity field(s)`);
4358
4503
  // an error blows up the transaction (all-or-nothing), which is what we want
4359
4504
  }
4360
4505
  catch (e) {
@@ -4388,10 +4533,10 @@ export class ManageMetadataBase {
4388
4533
  * @param relatedEntityNameFieldMap
4389
4534
  * @returns
4390
4535
  */
4391
- async updateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
4536
+ async UpdateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
4392
4537
  try {
4393
- const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateEntityFieldRelatedEntityNameFieldMap', [`'${entityFieldID}'`, `'${relatedEntityNameFieldMap}'`], ['EntityFieldID', 'RelatedEntityNameFieldMap']);
4394
- await this.LogSQLAndExecute(pool, sSQL, `SQL text to update entity field related entity name field map for entity field ID ${entityFieldID}`);
4538
+ const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateEntityFieldRelatedEntityNameFieldMap', [`'${entityFieldID}'`, `'${relatedEntityNameFieldMap}'`], ['EntityFieldID', 'RelatedEntityNameFieldMap']);
4539
+ await this.logSQLAndExecute(pool, sSQL, `SQL text to update entity field related entity name field map for entity field ID ${entityFieldID}`);
4395
4540
  return true;
4396
4541
  }
4397
4542
  catch (e) {
@@ -4399,14 +4544,18 @@ export class ManageMetadataBase {
4399
4544
  return false;
4400
4545
  }
4401
4546
  }
4547
+ /** @deprecated Use {@link UpdateEntityFieldRelatedEntityNameFieldMap}. */
4548
+ async updateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap) {
4549
+ return this.UpdateEntityFieldRelatedEntityNameFieldMap(pool, entityFieldID, relatedEntityNameFieldMap);
4550
+ }
4402
4551
  async updateExistingEntitiesFromSchema(pool, excludeSchemas) {
4403
4552
  try {
4404
- const heal = buildHealSchemaRoutineParams({
4405
- authoredExclude: getAuthoredExcludeSchemas(excludeSchemas),
4553
+ const heal = BuildHealSchemaRoutineParams({
4554
+ authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
4406
4555
  includeSchemas: configInfo.includeSchemas,
4407
4556
  });
4408
- const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateExistingEntitiesFromSchema', heal.values, heal.names);
4409
- const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to update existing entities from schema`, true);
4557
+ const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateExistingEntitiesFromSchema', heal.Values, heal.Names);
4558
+ const result = await this.logSQLAndExecute(pool, sSQL, `SQL text to update existing entities from schema`, true);
4410
4559
  // result contains the updated entities, and there is a property of each row called Name which has the entity name that was modified
4411
4560
  // add these to the modified entity list if they're not already in there
4412
4561
  if (result && result.length > 0) {
@@ -4422,30 +4571,34 @@ export class ManageMetadataBase {
4422
4571
  /**
4423
4572
  * Adds a list of entity names to the modified entity list if they're not already in there
4424
4573
  */
4425
- static addNewEntitiesToModifiedList(entityNames) {
4574
+ static AddNewEntitiesToModifiedList(entityNames) {
4426
4575
  const distinctEntityNames = [...new Set(entityNames)];
4427
4576
  const newlyModifiedEntityNames = distinctEntityNames.filter((e) => !ManageMetadataBase._modifiedEntityList.includes(e));
4428
4577
  // now make sure that each of these entity names is in the modified entity list
4429
4578
  ManageMetadataBase._modifiedEntityList = ManageMetadataBase._modifiedEntityList.concat(newlyModifiedEntityNames);
4430
4579
  }
4580
+ /** @deprecated Use {@link AddNewEntitiesToModifiedList}. */
4581
+ static addNewEntitiesToModifiedList(entityNames) {
4582
+ return this.AddNewEntitiesToModifiedList(entityNames);
4583
+ }
4431
4584
  async updateExistingEntityFieldsFromSchema(pool, excludeSchemas, entityIDs) {
4432
4585
  try {
4433
4586
  // One SP call regardless of scope: pass the entire entity ID list as a comma-delimited
4434
4587
  // string (mirrors the @ExcludedSchemaNames pattern). The SP fans the list out into
4435
4588
  // a table variable via STRING_SPLIT and joins once. This avoids both per-entity
4436
4589
  // round-trips (slow) and parallel calls (page-level lock contention on EntityField).
4437
- const heal = buildHealSchemaRoutineParams({
4438
- authoredExclude: getAuthoredExcludeSchemas(excludeSchemas),
4590
+ const heal = BuildHealSchemaRoutineParams({
4591
+ authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
4439
4592
  includeSchemas: configInfo.includeSchemas,
4440
4593
  entityIDs,
4441
4594
  });
4442
- const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateExistingEntityFieldsFromSchema', heal.values, heal.names);
4595
+ const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateExistingEntityFieldsFromSchema', heal.Values, heal.Names);
4443
4596
  const isScoped = entityIDs !== undefined && entityIDs.length > 0;
4444
4597
  const label = isScoped
4445
4598
  ? `SQL text to update existing entity fields from schema (${entityIDs.length} scoped entities)`
4446
4599
  : `SQL text to update existing entity fields from schema`;
4447
4600
  const before = await this.snapshotEntityFields(pool, entityIDs);
4448
- await this.LogSQLAndExecute(pool, sSQL, label, true);
4601
+ await this.logSQLAndExecute(pool, sSQL, label, true);
4449
4602
  const after = await this.snapshotEntityFields(pool, entityIDs);
4450
4603
  const changes = diffEntityFieldSnapshots(before, after, (e, n) => ManageMetadataBase.isFieldNew(e, n));
4451
4604
  if (changes.length > 0) {
@@ -4476,12 +4629,12 @@ export class ManageMetadataBase {
4476
4629
  */
4477
4630
  async updateSchemaInfoFromDatabase(pool, excludeSchemas) {
4478
4631
  try {
4479
- const heal = buildHealSchemaRoutineParams({
4480
- authoredExclude: getAuthoredExcludeSchemas(excludeSchemas),
4632
+ const heal = BuildHealSchemaRoutineParams({
4633
+ authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
4481
4634
  includeSchemas: configInfo.includeSchemas,
4482
4635
  });
4483
- const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spUpdateSchemaInfoFromDatabase', heal.values, heal.names);
4484
- const result = await this.LogSQLAndExecute(pool, sSQL, `SQL text to sync schema info from database schemas`, true);
4636
+ const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spUpdateSchemaInfoFromDatabase', heal.Values, heal.Names);
4637
+ const result = await this.logSQLAndExecute(pool, sSQL, `SQL text to sync schema info from database schemas`, true);
4485
4638
  if (result && result.length > 0) {
4486
4639
  logStatus(` > Updated/created ${result.length} SchemaInfo records`);
4487
4640
  this.cacheSchemaInfoRecords(result);
@@ -4511,7 +4664,7 @@ export class ManageMetadataBase {
4511
4664
  */
4512
4665
  async loadSchemaInfoRecords(pool) {
4513
4666
  try {
4514
- const sSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'SchemaInfo')}`;
4667
+ const sSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'SchemaInfo')}`;
4515
4668
  const result = await this.runQuery(pool, sSQL);
4516
4669
  if (result?.recordset?.length > 0) {
4517
4670
  this.cacheSchemaInfoRecords(result.recordset);
@@ -4577,17 +4730,17 @@ export class ManageMetadataBase {
4577
4730
  // string (mirrors the @ExcludedSchemaNames pattern). The SP fans the list out into
4578
4731
  // a table variable via STRING_SPLIT and filters once. Avoids both per-entity
4579
4732
  // round-trips and parallel calls (page-level lock contention on EntityField).
4580
- const heal = buildHealSchemaRoutineParams({
4581
- authoredExclude: getAuthoredExcludeSchemas(excludeSchemas),
4733
+ const heal = BuildHealSchemaRoutineParams({
4734
+ authoredExclude: GetAuthoredExcludeSchemas(excludeSchemas),
4582
4735
  includeSchemas: configInfo.includeSchemas,
4583
4736
  entityIDs,
4584
4737
  });
4585
- const sSQL = this.dbProvider.callRoutineSQL(mj_core_schema(), 'spDeleteUnneededEntityFields', heal.values, heal.names);
4738
+ const sSQL = this.dbProvider.callRoutineSQL(MjCoreSchema(), 'spDeleteUnneededEntityFields', heal.Values, heal.Names);
4586
4739
  const isScoped = entityIDs !== undefined && entityIDs.length > 0;
4587
4740
  const label = isScoped
4588
4741
  ? `SQL text to delete unneeded entity fields (${entityIDs.length} scoped entities)`
4589
4742
  : `SQL text to delete unneeded entity fields`;
4590
- const result = await this.LogSQLAndExecute(pool, sSQL, label, true);
4743
+ const result = await this.logSQLAndExecute(pool, sSQL, label, true);
4591
4744
  // result contains the DELETED entity fields. Get a distinct list of entity names
4592
4745
  // and add them to the modified entity list if they're not already in there.
4593
4746
  if (result && result.length > 0) {
@@ -4607,13 +4760,13 @@ export class ManageMetadataBase {
4607
4760
  // for the field and sync that up with the EntityFieldValue table. If it is not a simple series of OR statements, we will not be able to parse it and we'll
4608
4761
  // just ignore it.
4609
4762
  const filter = this.dbProvider.getCheckConstraintsSchemaFilter(excludeSchemas);
4610
- const sSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFieldsWithCheckConstraints')}${filter}`;
4763
+ const sSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntityFieldsWithCheckConstraints')}${filter}`;
4611
4764
  const resultResult = await this.runQuery(pool, sSQL);
4612
4765
  const result = resultResult.recordset;
4613
- const efvSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
4766
+ const efvSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'EntityFieldValue')} ORDER BY EntityFieldID, Sequence, Value, ID`;
4614
4767
  const allEntityFieldValuesResult = await this.runQuery(pool, efvSQL);
4615
4768
  const allEntityFieldValues = allEntityFieldValuesResult.recordset;
4616
- const efSQL = `SELECT * FROM ${this.qs(mj_core_schema(), 'vwEntityFields')} ORDER BY EntityID, Sequence, Name, ID`;
4769
+ const efSQL = `SELECT * FROM ${this.qs(MjCoreSchema(), 'vwEntityFields')} ORDER BY EntityID, Sequence, Name, ID`;
4617
4770
  const allEntityFieldsResult = await this.runQuery(pool, efSQL);
4618
4771
  const allEntityFields = allEntityFieldsResult.recordset;
4619
4772
  const generationPromises = [];
@@ -4631,25 +4784,30 @@ export class ManageMetadataBase {
4631
4784
  for (const r of columnLevelResults) {
4632
4785
  // now, for each of the constraints we get back here, loop through and evaluate if they're simple and if they're simple, parse and sync with entity field values for that field
4633
4786
  if (r.ConstraintDefinition && r.ConstraintDefinition.length > 0) {
4634
- const parsedValues = this.parseCheckConstraintValues(r.ConstraintDefinition, r.ColumnName, r.EntityName);
4787
+ // parse first, then decide whether THIS field should carry the list (bit, PK singleton guards).
4788
+ // UUIDsEqual, not ===: PG returns lowercase UUIDs and SQL Server uppercase, and a missed
4789
+ // lookup here fails OPEN (the exclusions stop applying), so a case mismatch would be silent.
4790
+ const field = allEntityFields.find((f) => UUIDsEqual(f.ID, r.EntityFieldID));
4791
+ const parsedValues = ManageMetadataBase.valueListForField(field, this.parseCheckConstraintValues(r.ConstraintDefinition, r.ColumnName, r.EntityName));
4635
4792
  if (parsedValues) {
4636
4793
  if (!skipDBUpdate) {
4637
4794
  // we only do this part if we are not skiping the database update as this code will sync values from the CHECK
4638
4795
  // with the EntityFieldValues in the database.
4639
- // Sort values alphabetically to ensure consistent sequences across all databases
4640
- // This guarantees the same value always gets the same sequence number regardless of
4641
- // how SQL Server returns CHECK constraint values (which can vary)
4642
- parsedValues.sort();
4796
+ // Sort the values to ensure consistent sequences across all databases. This guarantees the
4797
+ // same value always gets the same sequence number regardless of how SQL Server returns
4798
+ // CHECK constraint values (which can vary). An all-numeric list sorts numerically so the
4799
+ // dropdown reads 1, 2, 10 rather than the lexical 1, 10, 2 — still fully deterministic.
4800
+ ManageMetadataBase.sortCheckConstraintValues(parsedValues);
4643
4801
  // we have parsed values from the check constraint, so sync them with the entity field values
4644
4802
  await this.syncEntityFieldValues(pool, r.EntityFieldID, parsedValues, allEntityFieldValues);
4645
4803
  // finally, make sure the ValueListType column within the EntityField table is set to "List" because for check constraints we only allow the values specified in the list.
4646
4804
  // check to see if the ValueListType is already set to "List", if not, update it
4647
- const sSQLCheck = `SELECT ValueListType FROM ${this.qs(mj_core_schema(), 'EntityField')} WHERE ID='${r.EntityFieldID}'`;
4805
+ const sSQLCheck = `SELECT ValueListType FROM ${this.qs(MjCoreSchema(), 'EntityField')} WHERE ID='${r.EntityFieldID}'`;
4648
4806
  const checkResultResult = await this.runQuery(pool, sSQLCheck);
4649
4807
  const checkResult = checkResultResult.recordset;
4650
4808
  if (checkResult && checkResult.length > 0 && checkResult[0].ValueListType.trim().toLowerCase() !== 'list') {
4651
- const sSQL = `UPDATE ${this.qs(mj_core_schema(), 'EntityField')} SET ValueListType='List' WHERE ID='${r.EntityFieldID}'`;
4652
- await this.LogSQLAndExecute(pool, sSQL, `SQL text to update ValueListType for entity field ID ${r.EntityFieldID}`);
4809
+ const sSQL = `UPDATE ${this.qs(MjCoreSchema(), 'EntityField')} SET ValueListType='List' WHERE ID='${r.EntityFieldID}'`;
4810
+ await this.logSQLAndExecute(pool, sSQL, `SQL text to update ValueListType for entity field ID ${r.EntityFieldID}`);
4653
4811
  }
4654
4812
  }
4655
4813
  else {
@@ -4691,7 +4849,7 @@ export class ManageMetadataBase {
4691
4849
  * @param pool
4692
4850
  * @param currentUser
4693
4851
  */
4694
- async loadGeneratedCode(pool, currentUser) {
4852
+ async LoadGeneratedCode(pool, currentUser) {
4695
4853
  try {
4696
4854
  // right now we're just doing validator functions which are handled here
4697
4855
  return await this.manageEntityFieldValuesAndValidatorFunctions(pool, [], currentUser, true);
@@ -4701,6 +4859,10 @@ export class ManageMetadataBase {
4701
4859
  return false;
4702
4860
  }
4703
4861
  }
4862
+ /** @deprecated Use {@link LoadGeneratedCode}. */
4863
+ async loadGeneratedCode(pool, currentUser) {
4864
+ return this.LoadGeneratedCode(pool, currentUser);
4865
+ }
4704
4866
  async runValidationGeneration(r, allEntityFields, generateNewCode, currentUser) {
4705
4867
  const generatedFunction = await this.generateValidatorFunctionFromCheckConstraint(r, allEntityFields, currentUser, generateNewCode);
4706
4868
  if (generatedFunction?.success) {
@@ -4781,8 +4943,8 @@ export class ManageMetadataBase {
4781
4943
  for (const ev of existingValues) {
4782
4944
  if (!possibleValues.find(v => v === ev.Value)) {
4783
4945
  // delete the value from the database
4784
- const sSQLDelete = `DELETE FROM ${this.qs(mj_core_schema(), 'EntityFieldValue')} WHERE ID='${ev.ID}'`;
4785
- await this.LogSQLAndExecute(ds, sSQLDelete, `SQL text to delete entity field value ID ${ev.ID}`);
4946
+ const sSQLDelete = `DELETE FROM ${this.qs(MjCoreSchema(), 'EntityFieldValue')} WHERE ID='${ev.ID}'`;
4947
+ await this.logSQLAndExecute(ds, sSQLDelete, `SQL text to delete entity field value ID ${ev.ID}`);
4786
4948
  numRemoved++;
4787
4949
  }
4788
4950
  }
@@ -4793,11 +4955,11 @@ export class ManageMetadataBase {
4793
4955
  // Generate a UUID for this new EntityFieldValue record
4794
4956
  const newId = uuidv4();
4795
4957
  // add the value to the database with explicit ID
4796
- const sSQLInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'EntityFieldValue')}
4958
+ const sSQLInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntityFieldValue')}
4797
4959
  (${this.qi('ID')}, ${this.qi('EntityFieldID')}, ${this.qi('Sequence')}, ${this.qi('Value')}, ${this.qi('Code')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')})
4798
4960
  VALUES
4799
4961
  ('${newId}', '${entityFieldID}', ${1 + possibleValues.indexOf(v)}, '${v}', '${v}', ${this.utcNow()}, ${this.utcNow()})`;
4800
- await this.LogSQLAndExecute(ds, sSQLInsert, `SQL text to insert entity field value with ID ${newId}`);
4962
+ await this.logSQLAndExecute(ds, sSQLInsert, `SQL text to insert entity field value with ID ${newId}`);
4801
4963
  numAdded++;
4802
4964
  }
4803
4965
  }
@@ -4809,8 +4971,8 @@ export class ManageMetadataBase {
4809
4971
  // DB returns a string for Sequence instead of a number.
4810
4972
  if (ev && ev.Sequence != 1 + possibleValues.indexOf(v)) {
4811
4973
  // update the sequence to match the order in the possible values list, if it doesn't already match
4812
- const sSQLUpdate = `UPDATE ${this.qs(mj_core_schema(), 'EntityFieldValue')} SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID='${ev.ID}'`;
4813
- await this.LogSQLAndExecute(ds, sSQLUpdate, `SQL text to update entity field value sequence`);
4974
+ const sSQLUpdate = `UPDATE ${this.qs(MjCoreSchema(), 'EntityFieldValue')} SET Sequence=${1 + possibleValues.indexOf(v)} WHERE ID='${ev.ID}'`;
4975
+ await this.logSQLAndExecute(ds, sSQLUpdate, `SQL text to update entity field value sequence`);
4814
4976
  numUpdated++;
4815
4977
  }
4816
4978
  }
@@ -4827,10 +4989,24 @@ export class ManageMetadataBase {
4827
4989
  return false;
4828
4990
  }
4829
4991
  }
4992
+ /**
4993
+ * Parses a column-level CHECK constraint into the list of values it permits, or returns null when the
4994
+ * constraint is not a simple value list (a range, a `LEN()` predicate, a mixed condition, and so on).
4995
+ *
4996
+ * Both of the ways `IN (...)` is rendered by SQL Server are handled. String, date and GUID literals come
4997
+ * back quoted — `([Status]='Active' OR [Status]='Inactive')` — but numeric and `bit` literals come back
4998
+ * **unquoted and parenthesized**: `([Level]=(3) OR [Level]=(2) OR [Level]=(1))`. Matching only the quoted
4999
+ * form left a numeric IN-list with no value list at all, and so no dropdown in Explorer either (#3978).
5000
+ * PostgreSQL's `= ANY (ARRAY[...])` rendering is handled separately, by parsePgArrayConstraint.
5001
+ *
5002
+ * A single-value list is captured too: `CHECK (Status='Active')` — which is also how SQL Server renders
5003
+ * `IN ('Active')` — permits exactly one value, which is a one-item list.
5004
+ *
5005
+ * This is a parser, not a policy: it reports what the constraint permits. WHICH fields get a value list
5006
+ * out of that is decided after the fact, by valueListForField.
5007
+ */
4830
5008
  parseCheckConstraintValues(constraintDefinition, fieldName, entityName) {
4831
- // This regex checks for the overall structure including field name and 'OR' sequences
4832
- // SQL Server uses [FieldName]='Value' quoting, PostgreSQL uses "FieldName" or unquoted FieldName
4833
- // We handle both: [FieldName], "FieldName", or bare FieldName
5009
+ // Normalize N'literal' to 'literal' so one pattern handles both.
4834
5010
  // Note: Assuming fieldName does not contain regex special characters; otherwise, it needs to be escaped as well.
4835
5011
  const processedConstraint = constraintDefinition.replace(/(^|[=(\s])N'([^']*)'/g, "$1'$2'");
4836
5012
  // PostgreSQL ANY (ARRAY[...]) pattern:
@@ -4843,46 +5019,92 @@ export class ManageMetadataBase {
4843
5019
  // Build a regex fragment that matches the field name in any quoting style:
4844
5020
  // [FieldName] (SQL Server) or "FieldName" (PostgreSQL) or bare FieldName
4845
5021
  const quotedField = `(?:\\[${fieldName}\\]|"${fieldName}"|${fieldName})`;
4846
- // Check for nested pattern: (Field IS NULL OR (Field='Value1' OR ...))
4847
- const nestedNullRegex = new RegExp(`^\\(${quotedField} IS NULL OR \\(${quotedField}='[^']+'(?: OR ${quotedField}='[^']+?')+\\)\\)$`);
4848
- if (nestedNullRegex.test(processedConstraint)) {
4849
- // Extract values from nested pattern - same extraction logic works
4850
- const valueRegex = new RegExp(`${quotedField}='([^']+)\'`, 'g');
4851
- let match;
4852
- const possibleValues = [];
4853
- while ((match = valueRegex.exec(processedConstraint)) !== null) {
4854
- if (match.index === valueRegex.lastIndex) {
4855
- valueRegex.lastIndex++;
4856
- }
4857
- if (match[1]) {
4858
- possibleValues.push(match[1]);
4859
- }
5022
+ // One literal, either quoting style. The unquoted numeric forms are the ones sys.check_constraints
5023
+ // actually stores: (3) (-1) (1.00) (2.5) (1.0000000000000000e+030) and (12345678901234567890.) —
5024
+ // an integral literal with a trailing point once the precision is large enough.
5025
+ const literal = `(?:'[^']+'|${ManageMetadataBase.NUMERIC_CHECK_LITERAL})`;
5026
+ const assignment = `${quotedField}=${literal}`;
5027
+ // Nested pattern: (Field IS NULL OR (Field=<literal> OR ...)) — how `X IS NULL OR X IN (...)` renders
5028
+ const nestedNullRegex = new RegExp(`^\\(${quotedField} IS NULL OR \\(${assignment}(?: OR ${assignment})*\\)\\)$`);
5029
+ // Standard pattern, with an optional trailing IS NULL: (Field=<literal> OR ... [OR Field IS NULL])
5030
+ const structureRegex = new RegExp(`^\\(${assignment}(?: OR ${assignment})*(?: OR ${quotedField} IS NULL)?\\)$`);
5031
+ if (!nestedNullRegex.test(processedConstraint) && !structureRegex.test(processedConstraint)) {
5032
+ return null;
5033
+ }
5034
+ // Extract the values: group 1 is a quoted literal, group 2 an unquoted numeric one.
5035
+ const valueRegex = new RegExp(`${quotedField}=(?:'([^']+)'|(${ManageMetadataBase.NUMERIC_CHECK_LITERAL}))`, 'g');
5036
+ const possibleValues = [];
5037
+ let match;
5038
+ while ((match = valueRegex.exec(processedConstraint)) !== null) {
5039
+ // This is necessary to avoid infinite loops with zero-width matches
5040
+ if (match.index === valueRegex.lastIndex) {
5041
+ valueRegex.lastIndex++;
5042
+ }
5043
+ const value = match[1] ?? (match[2] ? ManageMetadataBase.normalizeNumericCheckLiteral(match[2]) : undefined);
5044
+ if (value && value.length > 0) {
5045
+ possibleValues.push(value);
4860
5046
  }
4861
- return possibleValues.length > 0 ? possibleValues : null;
4862
5047
  }
4863
- // Check for standard pattern with optional trailing IS NULL
4864
- const structureRegex = new RegExp(`^\\(${quotedField}='[^']+'(?: OR ${quotedField}='[^']+?')+(?: OR ${quotedField} IS NULL)?\\)$`);
4865
- if (!structureRegex.test(processedConstraint)) {
5048
+ // An empty result is not a value list — returning [] here would set ValueListType='List' on a field
5049
+ // with no EntityFieldValue rows behind it.
5050
+ return possibleValues.length > 0 ? possibleValues : null;
5051
+ }
5052
+ /**
5053
+ * The value list to actually store for a field, or null when a parsed list should NOT become
5054
+ * EntityFieldValue rows (and so should not become a dropdown in Explorer either).
5055
+ *
5056
+ * Runs AFTER parsing rather than gating it, so each exclusion is no broader than its own reason and
5057
+ * neither one can drop a list that was captured before #3978:
5058
+ *
5059
+ * - `bit`: `IN (0,1)` is vacuous — bit already permits exactly 0 and 1 — and `= 1` is a validator,
5060
+ * not a two-item dropdown over what renders as a checkbox. Every bit list is rendered unquoted, so
5061
+ * none of them was reachable before this change and nothing regresses.
5062
+ * - a PRIMARY KEY carrying a SINGLE value: `CHECK (ID=1)` is the single-row-table guard MJ's own
5063
+ * sequence tables use — a structural invariant, not a set of values a user picks from. A
5064
+ * MULTI-value list on a primary key is left alone: `CHECK (Code IN ('US','CA'))` on a natural-key
5065
+ * PK is a real domain list, it was captured before this change, and CodeGen also runs over customer
5066
+ * schemas where natural-key primary keys are ordinary.
5067
+ *
5068
+ * A field whose metadata row was not found keeps whatever parsed, which is the pre-#3978 behaviour.
5069
+ */
5070
+ static valueListForField(field, parsedValues) {
5071
+ if (!parsedValues || !field) {
5072
+ return parsedValues;
5073
+ }
5074
+ if (field.Type?.trim().toLowerCase() === 'bit') {
4866
5075
  return null;
4867
5076
  }
4868
- else {
4869
- // Regular expression to match the values within the single quotes specifically for the field
4870
- const valueRegex = new RegExp(`${quotedField}='([^']+)\'`, 'g');
4871
- let match;
4872
- const possibleValues = [];
4873
- // Use regex to find matches and extract the values
4874
- while ((match = valueRegex.exec(processedConstraint)) !== null) {
4875
- // This is necessary to avoid infinite loops with zero-width matches
4876
- if (match.index === valueRegex.lastIndex) {
4877
- valueRegex.lastIndex++;
4878
- }
4879
- // The first captured group contains the value
4880
- if (match[1]) {
4881
- possibleValues.push(match[1]);
4882
- }
4883
- }
4884
- return possibleValues;
5077
+ if (field.IsPrimaryKey && parsedValues.length === 1) {
5078
+ return null;
4885
5079
  }
5080
+ return parsedValues;
5081
+ }
5082
+ /**
5083
+ * Sorts a parsed value list in place, so the sequence a value gets is stable across databases and runs.
5084
+ * An all-numeric list is compared as numbers (1, 2, 10); anything else keeps the default lexical order.
5085
+ *
5086
+ * Keyed off the VALUES, not the column type — so a string column whose list happens to be all-numeric
5087
+ * (`CHECK (Code IN ('1','2','10'))` on nvarchar) would re-sequence too. Measured over every column CHECK
5088
+ * constraint in a live 6.x database (MJ core + BizApps accounting/orders): of the 431 lists the previous
5089
+ * parser already captured, ZERO re-sequence under this comparison. If one ever turns up and the churn is
5090
+ * unwanted, gate `allNumeric` on the field being numeric as well.
5091
+ */
5092
+ static sortCheckConstraintValues(values) {
5093
+ const allNumeric = values.length > 0 && values.every(v => /^-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?$/.test(v));
5094
+ return allNumeric ? values.sort((a, b) => Number(a) - Number(b)) : values.sort();
5095
+ }
5096
+ /**
5097
+ * A numeric literal as SQL Server renders it inside a CHECK constraint: parenthesized, unquoted, and
5098
+ * possibly signed, fractional or exponential — `(3)`, `(-1)`, `(1.00)`, `(2.5)`, `(1.0e+030)`.
5099
+ */
5100
+ static { this.NUMERIC_CHECK_LITERAL = `\\(-?\\d+(?:\\.\\d*)?(?:[eE][+-]?\\d+)?\\)`; }
5101
+ /**
5102
+ * Turns a matched numeric literal into the value to store: strips the wrapping parentheses, and the
5103
+ * trailing point SQL Server adds to a large integral literal (`(12345678901234567890.)`).
5104
+ */
5105
+ static normalizeNumericCheckLiteral(literal) {
5106
+ const inner = literal.slice(1, -1);
5107
+ return inner.endsWith('.') ? inner.slice(0, -1) : inner;
4886
5108
  }
4887
5109
  /**
4888
5110
  * Parses PostgreSQL ANY (ARRAY[...]) CHECK constraint syntax.
@@ -4945,7 +5167,7 @@ export class ManageMetadataBase {
4945
5167
  // it — user-visible AND editable, letting someone mutate the snapshot the refresher overwrites. The
4946
5168
  // wrapper view is already excluded (it carries the minted entity, so EntityID IS NOT NULL). The outer
4947
5169
  // view is aliased `t` so the correlated MaterializedResult subquery can't resolve columns ambiguously.
4948
- const coreSchema = mj_core_schema();
5170
+ const coreSchema = MjCoreSchema();
4949
5171
  // Gate the MaterializedResult reference on the table actually existing. Referencing it unconditionally
4950
5172
  // would throw — breaking new-entity creation for EVERY entity — on any DB where MaterializedResult
4951
5173
  // isn't present yet (e.g. the PostgreSQL parallel world's object-availability lag). If it's absent we
@@ -5089,7 +5311,7 @@ export class ManageMetadataBase {
5089
5311
  const result = await ag.generateEntityName(newEntity.TableName, currentUser);
5090
5312
  // Checked here as well as inside generateEntityName: a subclass or a stub can return anything, and
5091
5313
  // a non-name that reaches the INSERT fails it (see createNewEntity's catch for why that used to be silent).
5092
- if (result && isPlausibleEntityName(result.entityName)) {
5314
+ if (result && IsPlausibleEntityName(result.entityName)) {
5093
5315
  return this.markupEntityName(newEntity.SchemaName, result.entityName);
5094
5316
  }
5095
5317
  else {
@@ -5154,7 +5376,7 @@ export class ManageMetadataBase {
5154
5376
  return configInfo.newEntityDefaults?.NameRulesBySchema?.find(r => {
5155
5377
  let schemaNameToUse = r.SchemaName;
5156
5378
  if (schemaNameToUse?.trim().toLowerCase() === '${mj_core_schema}') {
5157
- schemaNameToUse = mj_core_schema();
5379
+ schemaNameToUse = MjCoreSchema();
5158
5380
  }
5159
5381
  return schemaNameToUse.trim().toLowerCase() === schemaName.trim().toLowerCase();
5160
5382
  });
@@ -5204,7 +5426,7 @@ export class ManageMetadataBase {
5204
5426
  // process a single new entity
5205
5427
  let newEntityName = await this.createNewEntityName(newEntity, currentUser);
5206
5428
  const newEntityDisplayName = this.createNewEntityDisplayName(newEntity, newEntityName);
5207
- const { name: uniqueName, suffix: uniqueSuffix } = this.resolveUniqueEntityName(newEntityName, newEntity.SchemaName, md.Entities.map(e => e.Name));
5429
+ const { name: uniqueName, suffix: uniqueSuffix } = this.ResolveUniqueEntityName(newEntityName, newEntity.SchemaName, md.Entities.map(e => e.Name));
5208
5430
  if (uniqueName !== newEntityName) {
5209
5431
  LogError(` >>>> WARNING: Entity name already exists, so using ${uniqueName} instead. If you did not intend for this, please rename the ${newEntity.SchemaName}.${newEntity.TableName} table in the database.`);
5210
5432
  }
@@ -5213,7 +5435,7 @@ export class ManageMetadataBase {
5213
5435
  const isNewSchema = await this.isSchemaNew(pool, newEntity.SchemaName);
5214
5436
  const newEntityID = this.createNewUUID();
5215
5437
  const sSQLInsert = this.createNewEntityInsertSQL(newEntityID, newEntityName, newEntity, suffix, newEntityDisplayName);
5216
- await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to create new entity ${newEntityName}`);
5438
+ await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to create new entity ${newEntityName}`);
5217
5439
  // if we get here we created a new entity safely, otherwise we get exception
5218
5440
  // add it to the new entity list
5219
5441
  ManageMetadataBase.newEntityList.push(newEntityName);
@@ -5246,10 +5468,10 @@ export class ManageMetadataBase {
5246
5468
  if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
5247
5469
  // only do this if the configuration setting is set to add new entities to applications for schema names
5248
5470
  for (const appUUID of apps) {
5249
- const sSQLInsertApplicationEntity = `INSERT INTO ${this.qs(mj_core_schema(), 'ApplicationEntity')}
5471
+ const sSQLInsertApplicationEntity = `INSERT INTO ${this.qs(MjCoreSchema(), 'ApplicationEntity')}
5250
5472
  (${this.qi('ApplicationID')}, ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
5251
- ('${appUUID}', '${newEntityID}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(mj_core_schema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
5252
- await this.LogSQLAndExecute(pool, sSQLInsertApplicationEntity, `SQL generated to add new entity ${newEntityName} to application ID: '${appUUID}'`);
5473
+ ('${appUUID}', '${newEntityID}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(MjCoreSchema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
5474
+ await this.logSQLAndExecute(pool, sSQLInsertApplicationEntity, `SQL generated to add new entity ${newEntityName} to application ID: '${appUUID}'`);
5253
5475
  }
5254
5476
  }
5255
5477
  else {
@@ -5268,7 +5490,7 @@ export class ManageMetadataBase {
5268
5490
  const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
5269
5491
  if (RoleID) {
5270
5492
  const sSQLInsertPermission = this.buildEntityPermissionInsertSQL(newEntityID, RoleID, p.CanRead, p.CanCreate, p.CanUpdate, p.CanDelete);
5271
- await this.LogSQLAndExecute(pool, sSQLInsertPermission, `SQL generated to add new permission for entity ${newEntityName} for role ${p.RoleName}`);
5493
+ await this.logSQLAndExecute(pool, sSQLInsertPermission, `SQL generated to add new permission for entity ${newEntityName} for role ${p.RoleName}`);
5272
5494
  }
5273
5495
  else
5274
5496
  LogError(` >>>> ERROR: Unable to find Role ID for role ${p.RoleName} to add permissions for new entity ${newEntityName}`);
@@ -5302,7 +5524,7 @@ export class ManageMetadataBase {
5302
5524
  async isSchemaNew(pool, schemaName) {
5303
5525
  // check to see if there are any entities in the db with this schema name
5304
5526
  // Quote the alias so PostgreSQL preserves PascalCase (unquoted aliases are lowercased in PG)
5305
- const sSQL = `SELECT COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE ${this.qi('SchemaName')} = '${schemaName}'`;
5527
+ const sSQL = `SELECT COUNT(*) AS ${this.qi('Count')} FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE ${this.qi('SchemaName')} = '${schemaName}'`;
5306
5528
  const resultResult = await this.runQuery(pool, sSQL);
5307
5529
  const result = resultResult.recordset;
5308
5530
  // Use Number() because PG returns COUNT(*) as bigint, which the pg driver delivers as a string
@@ -5334,7 +5556,7 @@ export class ManageMetadataBase {
5334
5556
  // Guard the INSERT with an existence check so the emitted (and re-runnable) migration SQL is
5335
5557
  // idempotent. drop-schema clears an app's ApplicationEntity/EntityPermission/Entity rows but NOT
5336
5558
  // its Application row, so replaying this block would otherwise collide on the Application PK.
5337
- const appCheckQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'Application')} WHERE ${this.qi('ID')} = '${appID}'`;
5559
+ const appCheckQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'Application')} WHERE ${this.qi('ID')} = '${appID}'`;
5338
5560
  // Schema-named bucket apps are plumbing (entity links, role grants, SchemaAutoAddNewEntities),
5339
5561
  // not products — hide them from new users. Application.DefaultForNewUser defaults to 1 in the
5340
5562
  // DB, so omitting the column here is what put raw '__mj_*'-named apps in every new user's
@@ -5343,10 +5565,10 @@ export class ManageMetadataBase {
5343
5565
  // statement in PG's `DO $$ ... $$` block, and the identifier auto-quoter skips dollar-quoted
5344
5566
  // blocks wholesale (they can legally contain arbitrary text), so nothing downstream will quote
5345
5567
  // these for us. Bare `ID` reaches PG folded to `id` and the INSERT fails on every run.
5346
- const appInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'Application')} (${this.qi('ID')}, ${this.qi('Name')}, ${this.qi('Description')}, ${this.qi('SchemaAutoAddNewEntities')}, ${this.qi('Path')}, ${this.qi('AutoUpdatePath')}, ${this.qi('DefaultForNewUser')})
5568
+ const appInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'Application')} (${this.qi('ID')}, ${this.qi('Name')}, ${this.qi('Description')}, ${this.qi('SchemaAutoAddNewEntities')}, ${this.qi('Path')}, ${this.qi('AutoUpdatePath')}, ${this.qi('DefaultForNewUser')})
5347
5569
  VALUES ('${appID}', '${appName}', 'Generated for schema', '${schemaName}', '${path}', ${this.dialect.BooleanLiteral(true)}, ${this.dialect.BooleanLiteral(false)})`;
5348
5570
  const sSQL = this.conditionalInsert(appCheckQuery, appInsert);
5349
- await this.LogSQLAndExecute(pool, sSQL, `SQL generated to create new application ${appName}`);
5571
+ await this.logSQLAndExecute(pool, sSQL, `SQL generated to create new application ${appName}`);
5350
5572
  LogStatus(`Created new application ${appName} with Path: ${path}`);
5351
5573
  // Auto-assign default roles to the new application
5352
5574
  await this.addDefaultRolesForApplication(pool, appID, appName);
@@ -5374,12 +5596,12 @@ export class ManageMetadataBase {
5374
5596
  // This covers BOTH re-running the block after drop-schema (which leaves ApplicationRole rows
5375
5597
  // intact) AND the collision with the Integration role that manageMetadata recreates
5376
5598
  // out-of-band — either path would otherwise violate UQ_ApplicationRole_App_Role.
5377
- const roleCheckQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'ApplicationRole')} WHERE ${this.qi('ApplicationID')} = '${appId}' AND ${this.qi('RoleID')} = '${role.ID}'`;
5378
- const roleInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'ApplicationRole')}
5599
+ const roleCheckQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'ApplicationRole')} WHERE ${this.qi('ApplicationID')} = '${appId}' AND ${this.qi('RoleID')} = '${role.ID}'`;
5600
+ const roleInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'ApplicationRole')}
5379
5601
  (${this.qi('ApplicationID')}, ${this.qi('RoleID')}, ${this.qi('CanAccess')}, ${this.qi('CanAdmin')}) VALUES
5380
5602
  ('${appId}', '${role.ID}', ${this.boolLit(roleDef.CanAccess)}, ${this.boolLit(roleDef.CanAdmin)})`;
5381
5603
  const sSQLInsert = this.conditionalInsert(roleCheckQuery, roleInsert);
5382
- await this.LogSQLAndExecute(pool, sSQLInsert, `Adding role ${roleDef.RoleName} to application ${appName}`);
5604
+ await this.logSQLAndExecute(pool, sSQLInsert, `Adding role ${roleDef.RoleName} to application ${appName}`);
5383
5605
  }
5384
5606
  else {
5385
5607
  LogError(`Unable to find Role '${roleDef.RoleName}' for application ${appName}`);
@@ -5387,14 +5609,14 @@ export class ManageMetadataBase {
5387
5609
  }
5388
5610
  }
5389
5611
  async applicationExists(pool, applicationName) {
5390
- const sSQL = `SELECT ID FROM ${this.qs(mj_core_schema(), 'Application')} WHERE Name = '${applicationName}'`;
5612
+ const sSQL = `SELECT ID FROM ${this.qs(MjCoreSchema(), 'Application')} WHERE Name = '${applicationName}'`;
5391
5613
  const resultResult = await this.runQuery(pool, sSQL);
5392
5614
  const result = resultResult.recordset;
5393
5615
  return result && result.length > 0 ? result[0].ID.length > 0 : false;
5394
5616
  }
5395
5617
  async getApplicationIDForSchema(pool, schemaName) {
5396
5618
  // get all the apps each time from DB as we might be adding, don't use Metadata here for that reason
5397
- const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM ${this.qs(mj_core_schema(), 'vwApplications')}`;
5619
+ const sSQL = `SELECT ID, Name, SchemaAutoAddNewEntities FROM ${this.qs(MjCoreSchema(), 'vwApplications')}`;
5398
5620
  const resultResult = await this.runQuery(pool, sSQL);
5399
5621
  const result = resultResult.recordset;
5400
5622
  if (!result || result.length === 0) {
@@ -5436,10 +5658,10 @@ export class ManageMetadataBase {
5436
5658
  if (apps && apps.length > 0) {
5437
5659
  if (configInfo.newEntityDefaults.AddToApplicationWithSchemaName) {
5438
5660
  for (const appUUID of apps) {
5439
- const sSQLInsert = `INSERT INTO ${this.qs(mj_core_schema(), 'ApplicationEntity')}
5661
+ const sSQLInsert = `INSERT INTO ${this.qs(MjCoreSchema(), 'ApplicationEntity')}
5440
5662
  (${this.qi('ApplicationID')}, ${this.qi('EntityID')}, ${this.qi('Sequence')}, ${this.qi('__mj_CreatedAt')}, ${this.qi('__mj_UpdatedAt')}) VALUES
5441
- ('${appUUID}', '${entityId}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(mj_core_schema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
5442
- await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add entity ${entityName} to application ID: '${appUUID}'`);
5663
+ ('${appUUID}', '${entityId}', (SELECT COALESCE(MAX(${this.qi('Sequence')}),0)+1 FROM ${this.qs(MjCoreSchema(), 'ApplicationEntity')} WHERE ${this.qi('ApplicationID')} = '${appUUID}'), ${this.utcNow()}, ${this.utcNow()})`;
5664
+ await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to add entity ${entityName} to application ID: '${appUUID}'`);
5443
5665
  }
5444
5666
  }
5445
5667
  }
@@ -5465,7 +5687,7 @@ export class ManageMetadataBase {
5465
5687
  * valid on both SQL Server and PostgreSQL, so no provider branch is needed.
5466
5688
  */
5467
5689
  buildEntityPermissionInsertSQL(entityId, roleId, canRead, canCreate, canUpdate, canDelete) {
5468
- const table = this.qs(mj_core_schema(), 'EntityPermission');
5690
+ const table = this.qs(MjCoreSchema(), 'EntityPermission');
5469
5691
  const entityLit = this.uuidLit(entityId);
5470
5692
  const roleLit = this.uuidLit(roleId);
5471
5693
  return `INSERT INTO ${table}
@@ -5490,7 +5712,7 @@ export class ManageMetadataBase {
5490
5712
  const RoleID = md.Roles.find(r => r.Name.trim().toLowerCase() === p.RoleName.trim().toLowerCase())?.ID;
5491
5713
  if (RoleID) {
5492
5714
  const sSQLInsert = this.buildEntityPermissionInsertSQL(entityId, RoleID, p.CanRead, p.CanCreate, p.CanUpdate, p.CanDelete);
5493
- await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add permission for entity ${entityName} for role ${p.RoleName}`);
5715
+ await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to add permission for entity ${entityName} for role ${p.RoleName}`);
5494
5716
  }
5495
5717
  else {
5496
5718
  LogError(` >>>> ERROR: Unable to find Role ID for role ${p.RoleName} to add permissions for entity ${entityName}`);
@@ -5652,7 +5874,7 @@ export class ManageMetadataBase {
5652
5874
  continue;
5653
5875
  }
5654
5876
  const sSQLInsert = this.buildEntityPermissionInsertSQL(entityId, roleId, true, false, false, false);
5655
- await this.LogSQLAndExecute(pool, sSQLInsert, `SQL generated to add read permission for materialized entity ${entityName} for role ${p.RoleName}`);
5877
+ await this.logSQLAndExecute(pool, sSQLInsert, `SQL generated to add read permission for materialized entity ${entityName} for role ${p.RoleName}`);
5656
5878
  }
5657
5879
  }
5658
5880
  /**
@@ -5662,8 +5884,8 @@ export class ManageMetadataBase {
5662
5884
  * human authors protection. Non-destructive (rows are kept, just flipped) so the grant can be restored.
5663
5885
  */
5664
5886
  async revokeMaterializedEntityReadAccess(pool, entityId, entityLabel, reason) {
5665
- const sql = `UPDATE ${this.qs(mj_core_schema(), 'EntityPermission')} SET ${this.qi('CanRead')}=${this.boolLit(false)} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`;
5666
- await this.LogSQLAndExecute(pool, sql, `Revoke read access on materialized entity "${entityLabel}" (${reason})`);
5887
+ const sql = `UPDATE ${this.qs(MjCoreSchema(), 'EntityPermission')} SET ${this.qi('CanRead')}=${this.boolLit(false)} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`;
5888
+ await this.logSQLAndExecute(pool, sql, `Revoke read access on materialized entity "${entityLabel}" (${reason})`);
5667
5889
  }
5668
5890
  /**
5669
5891
  * ROLE-RLS LAYER ONLY — true if any of the entity's role permissions carries a non-empty `ReadRLSFilterID`.
@@ -5734,7 +5956,7 @@ export class ManageMetadataBase {
5734
5956
  for (const table of ['APIKeyScope', 'APIApplicationScope']) {
5735
5957
  if (!(await this.columnExistsInCoreSchema(pool, table, 'RowFilterID')))
5736
5958
  continue;
5737
- const res = await this.runQuery(pool, `SELECT DISTINCT ${this.qi('ResourcePattern')} AS ResourcePattern FROM ${this.qs(mj_core_schema(), table)} WHERE ${this.qi('RowFilterID')} IS NOT NULL`);
5959
+ const res = await this.runQuery(pool, `SELECT DISTINCT ${this.qi('ResourcePattern')} AS ResourcePattern FROM ${this.qs(MjCoreSchema(), table)} WHERE ${this.qi('RowFilterID')} IS NOT NULL`);
5738
5960
  for (const row of res.recordset ?? []) {
5739
5961
  const r = row;
5740
5962
  const pattern = (r.ResourcePattern ?? r.resourcepattern) ?? '';
@@ -5767,7 +5989,7 @@ export class ManageMetadataBase {
5767
5989
  if (cached !== undefined)
5768
5990
  return cached;
5769
5991
  const sql = `SELECT COUNT(*) AS ColExists FROM INFORMATION_SCHEMA.COLUMNS ` +
5770
- `WHERE TABLE_SCHEMA = '${mj_core_schema()}' AND TABLE_NAME = '${table}' AND COLUMN_NAME = '${column}'`;
5992
+ `WHERE TABLE_SCHEMA = '${MjCoreSchema()}' AND TABLE_NAME = '${table}' AND COLUMN_NAME = '${column}'`;
5771
5993
  const result = await this.runQuery(pool, sql);
5772
5994
  const row = (result.recordset?.[0] ?? {});
5773
5995
  const cnt = row.ColExists ?? row.colexists ?? 0;
@@ -5787,14 +6009,14 @@ export class ManageMetadataBase {
5787
6009
  if (sourceEntities.length === 0)
5788
6010
  return 0; // no sources to intersect against — the RLS/empty gate handles this
5789
6011
  const roleCanReadAllSources = (roleId) => sourceEntities.every((e) => e.Permissions.some((p) => UUIDsEqual(p.RoleID, roleId) && p.CanRead));
5790
- const grants = await this.runQuery(pool, `SELECT ${this.qi('RoleID')} FROM ${this.qs(mj_core_schema(), 'EntityPermission')} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`);
6012
+ const grants = await this.runQuery(pool, `SELECT ${this.qi('RoleID')} FROM ${this.qs(MjCoreSchema(), 'EntityPermission')} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`);
5791
6013
  let revoked = 0;
5792
6014
  for (const g of grants.recordset) {
5793
6015
  const roleId = g.RoleID;
5794
6016
  if (!roleId)
5795
6017
  continue;
5796
6018
  if (!roleCanReadAllSources(roleId)) {
5797
- await this.LogSQLAndExecute(pool, `UPDATE ${this.qs(mj_core_schema(), 'EntityPermission')} SET ${this.qi('CanRead')}=${this.boolLit(false)} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('RoleID')}='${roleId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`, `Narrow read grant on materialized entity "${entityLabel}": role ${roleId} can no longer read every source (C2 intersection re-narrowed)`);
6019
+ await this.logSQLAndExecute(pool, `UPDATE ${this.qs(MjCoreSchema(), 'EntityPermission')} SET ${this.qi('CanRead')}=${this.boolLit(false)} WHERE ${this.qi('EntityID')}='${entityId}' AND ${this.qi('RoleID')}='${roleId}' AND ${this.qi('CanRead')}=${this.boolLit(true)}`, `Narrow read grant on materialized entity "${entityLabel}": role ${roleId} can no longer read every source (C2 intersection re-narrowed)`);
5798
6020
  revoked++;
5799
6021
  }
5800
6022
  }
@@ -5824,7 +6046,7 @@ export class ManageMetadataBase {
5824
6046
  * earlier in this run (which are not in metadata yet)
5825
6047
  * @returns the free name and the suffix used to reach it ('' when the name was already free)
5826
6048
  */
5827
- resolveUniqueEntityName(desiredName, schemaName, takenNames) {
6049
+ ResolveUniqueEntityName(desiredName, schemaName, takenNames) {
5828
6050
  const taken = new Set([...takenNames, ...ManageMetadataBase.newEntityList].map(n => n.toLowerCase()));
5829
6051
  const isTaken = (candidate) => taken.has(candidate.toLowerCase());
5830
6052
  if (!isTaken(desiredName)) {
@@ -5843,13 +6065,17 @@ export class ManageMetadataBase {
5843
6065
  }
5844
6066
  return { name, suffix };
5845
6067
  }
6068
+ /** @deprecated Use {@link ResolveUniqueEntityName}. */
6069
+ resolveUniqueEntityName(desiredName, schemaName, takenNames) {
6070
+ return this.ResolveUniqueEntityName(desiredName, schemaName, takenNames);
6071
+ }
5846
6072
  createNewEntityInsertSQL(newEntityUUID, newEntityName, newEntity, newEntitySuffix, newEntityDisplayName) {
5847
6073
  const newEntityDefaults = configInfo.newEntityDefaults;
5848
6074
  const newEntityDescriptionEscaped = newEntity.EntityDescription ? `'${newEntity.EntityDescription.replace(/'/g, "''")}'` : null;
5849
6075
  const allowCaching = this.resolveAllowCachingForSchema(newEntity.SchemaName);
5850
6076
  const q = (name) => this.qi(name);
5851
6077
  const sSQLInsert = `
5852
- INSERT INTO ${this.qs(mj_core_schema(), 'Entity')} (
6078
+ INSERT INTO ${this.qs(MjCoreSchema(), 'Entity')} (
5853
6079
  ${q('ID')},
5854
6080
  ${q('Name')},
5855
6081
  ${q('DisplayName')},
@@ -5910,7 +6136,7 @@ export class ManageMetadataBase {
5910
6136
  const match = overrides.find(entry => {
5911
6137
  let candidate = entry.SchemaName;
5912
6138
  if (candidate?.trim().toLowerCase() === '${mj_core_schema}') {
5913
- candidate = mj_core_schema();
6139
+ candidate = MjCoreSchema();
5914
6140
  }
5915
6141
  return candidate.trim().toLowerCase() === schemaName.trim().toLowerCase();
5916
6142
  });
@@ -5971,7 +6197,7 @@ export class ManageMetadataBase {
5971
6197
  e.FullTextSearchEnabled,
5972
6198
  e.AutoUpdateFullTextSearch
5973
6199
  FROM
5974
- ${this.qs(mj_core_schema(), 'vwEntities')} e
6200
+ ${this.qs(MjCoreSchema(), 'vwEntities')} e
5975
6201
  WHERE
5976
6202
  ${whereClause}
5977
6203
  ORDER BY
@@ -6020,7 +6246,7 @@ export class ManageMetadataBase {
6020
6246
  ef.AutoUpdateFullTextSearch,
6021
6247
  ef.${this.qi('Length')} as MaxLength
6022
6248
  FROM
6023
- ${this.qs(mj_core_schema(), 'vwEntityFields')} ef
6249
+ ${this.qs(MjCoreSchema(), 'vwEntityFields')} ef
6024
6250
  WHERE
6025
6251
  ef.EntityID IN (${entityIds})
6026
6252
  ORDER BY
@@ -6038,7 +6264,7 @@ export class ManageMetadataBase {
6038
6264
  es.Name,
6039
6265
  es.Value
6040
6266
  FROM
6041
- ${this.qs(mj_core_schema(), 'EntitySetting')} es
6267
+ ${this.qs(MjCoreSchema(), 'EntitySetting')} es
6042
6268
  WHERE
6043
6269
  es.EntityID IN (${entityIds})
6044
6270
  AND es.Name = 'FieldCategoryInfo'
@@ -6113,13 +6339,13 @@ export class ManageMetadataBase {
6113
6339
  }
6114
6340
  }
6115
6341
  const pct = Math.round((processedCount / total) * 100);
6116
- updateSpinner(`Advanced generation: ${processedCount}/${total} entities (${pct}%)${errorCount > 0 ? ` — ${errorCount} error(s)` : ''}`);
6342
+ UpdateSpinner(`Advanced generation: ${processedCount}/${total} entities (${pct}%)${errorCount > 0 ? ` — ${errorCount} error(s)` : ''}`);
6117
6343
  // Credential circuit tripped (e.g. keyless / mis-credentialed env) — stop issuing further
6118
6344
  // AI calls; the remaining entities would only produce doomed round-trips. One clear message
6119
6345
  // was already logged when the circuit opened.
6120
6346
  if (ag.AICircuitOpen) {
6121
6347
  const skipped = total - processedCount;
6122
- updateSpinner(`Advanced generation: AI credential circuit open — skipping remaining ${skipped} entit${skipped === 1 ? 'y' : 'ies'} (check AI credentials).`);
6348
+ UpdateSpinner(`Advanced generation: AI credential circuit open — skipping remaining ${skipped} entit${skipped === 1 ? 'y' : 'ies'} (check AI credentials).`);
6123
6349
  break;
6124
6350
  }
6125
6351
  }
@@ -6290,7 +6516,7 @@ export class ManageMetadataBase {
6290
6516
  // Execute all updates in one batch
6291
6517
  if (sqlStatements.length > 0) {
6292
6518
  try {
6293
- await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set field properties for entity`, false);
6519
+ await this.logSQLBatchAndExecute(pool, sqlStatements, `Set field properties for entity`, false);
6294
6520
  }
6295
6521
  catch (ex) {
6296
6522
  logError('Error executing combined smart field SQL: ', ex);
@@ -6318,7 +6544,7 @@ export class ManageMetadataBase {
6318
6544
  const proposedSearchableCount = (result.searchableFields ?? []).length;
6319
6545
  const proposedAllowUserSearch = result.allowUserSearch === true;
6320
6546
  // 1. Shape cleanup (defensive — identifyFields() also runs this).
6321
- const cleaned = normalizeSmartFieldResultShape(result);
6547
+ const cleaned = NormalizeSmartFieldResultShape(result);
6322
6548
  result.searchableFields = cleaned.searchableFields ?? [];
6323
6549
  result.searchPredicates = cleaned.searchPredicates ?? [];
6324
6550
  result.allowUserSearch = cleaned.allowUserSearch;
@@ -6339,14 +6565,14 @@ export class ManageMetadataBase {
6339
6565
  droppedIneligibleCount += 1;
6340
6566
  continue;
6341
6567
  }
6342
- if (isNarrativeFieldName(name) && !ftxEnabled) {
6568
+ if (IsNarrativeFieldName(name) && !ftxEnabled) {
6343
6569
  droppedNarrativeCount += 1;
6344
6570
  continue;
6345
6571
  }
6346
6572
  eligible.push(name);
6347
6573
  }
6348
6574
  // 3. Per-entity cap.
6349
- const { accepted, dropped: droppedByCap } = applySearchableFieldsCap(eligible);
6575
+ const { accepted, dropped: droppedByCap } = ApplySearchableFieldsCap(eligible);
6350
6576
  result.searchableFields = accepted;
6351
6577
  // 4. Predicate normalization. Build a fresh searchPredicates list keyed
6352
6578
  // on the accepted fields. The LLM's proposed predicates are the input
@@ -6361,7 +6587,7 @@ export class ManageMetadataBase {
6361
6587
  const normalizedPredicates = [];
6362
6588
  let predicatesRewrittenCount = 0;
6363
6589
  for (const fieldName of accepted) {
6364
- const { predicate, rewritten } = normalizePredicate({
6590
+ const { predicate, rewritten } = NormalizePredicate({
6365
6591
  fieldName,
6366
6592
  proposed: proposedByName.get(fieldName),
6367
6593
  isInFullTextSearchFields: ftsFieldSet.has(fieldName),
@@ -6436,7 +6662,7 @@ export class ManageMetadataBase {
6436
6662
  // Set the winner when it isn't flagged yet (and auto-update is allowed).
6437
6663
  if (winner && winner.AutoUpdateIsNameField && winner.ID && !winner.IsNameField) {
6438
6664
  sqlStatements.push(`
6439
- UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
6665
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6440
6666
  SET IsNameField = ${this.boolLit(true)}
6441
6667
  WHERE ID = '${winner.ID}'
6442
6668
  AND AutoUpdateIsNameField = ${this.boolLit(true)}
@@ -6448,7 +6674,7 @@ export class ManageMetadataBase {
6448
6674
  continue;
6449
6675
  }
6450
6676
  sqlStatements.push(`
6451
- UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
6677
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6452
6678
  SET IsNameField = ${this.boolLit(false)}
6453
6679
  WHERE ID = '${f.ID}'
6454
6680
  AND AutoUpdateIsNameField = ${this.boolLit(true)}
@@ -6597,7 +6823,7 @@ export class ManageMetadataBase {
6597
6823
  for (const field of defaultInViewFields) {
6598
6824
  if (!field.DefaultInView) {
6599
6825
  sqlStatements.push(`
6600
- UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
6826
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6601
6827
  SET DefaultInView = ${this.boolLit(true)}
6602
6828
  WHERE ID = '${field.ID}'
6603
6829
  AND AutoUpdateDefaultInView = ${this.boolLit(true)}
@@ -6635,7 +6861,7 @@ export class ManageMetadataBase {
6635
6861
  }
6636
6862
  if (!field.IncludeInUserSearchAPI) {
6637
6863
  sqlStatements.push(`
6638
- UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
6864
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6639
6865
  SET IncludeInUserSearchAPI = ${this.boolLit(true)}
6640
6866
  WHERE ID = '${field.ID}'
6641
6867
  AND AutoUpdateIncludeInUserSearchAPI = ${this.boolLit(true)}
@@ -6643,6 +6869,279 @@ export class ManageMetadataBase {
6643
6869
  }
6644
6870
  }
6645
6871
  }
6872
+ /**
6873
+ * Build the two deterministic search-flag hygiene statements.
6874
+ *
6875
+ * These exist because the LLM-driven smart-field pipeline cannot be relied on to run at all:
6876
+ * `AdvancedGeneration.enabled` reads `enableAdvancedGeneration ?? false`, so on a default
6877
+ * configuration none of `applySearchableFieldUpdates` / `applyEntitySearchConfig` ever
6878
+ * executes — while every new entity is still INSERTed with `AllowUserSearchAPI = 1`
6879
+ * (see `createNewEntityInsertSQL`). The result is an entity flagged searchable with nothing
6880
+ * searchable on it, which is not a harmless default: `UserSearchString` against such an entity
6881
+ * is a documented no-op (MJ#4581/#4582), so the data provider ignores the term and returns the
6882
+ * UNFILTERED table. Global search then fans out to it on every keystroke and discards every row.
6883
+ *
6884
+ * Both statements are deterministic, need no model, and are safe to run on every pass:
6885
+ *
6886
+ * 1. **Seed** — an entity with NO searchable field gets its name-like columns flagged
6887
+ * (`NAME_LIKE_FIELD_NAMES`: Name, Title, FirstName, LastName, …). Those are what a person
6888
+ * types into a search box, so they are the one defensible default. Only fires when the
6889
+ * entity has nothing flagged at all, so it fills a gap rather than overriding anyone —
6890
+ * including the LLM, which has already run by this point when it is enabled.
6891
+ *
6892
+ * Note this keys on the field's NAME, not on `IsNameField`. That flag looks like the
6893
+ * obvious source and is the wrong one: measured against a real database, seeding from it
6894
+ * would flag 54 fields of which 41 are VIRTUAL — the denormalized FK display columns
6895
+ * CodeGen puts on views (`Action`, `Agent`, `Artifact`). Those are computed by JOIN, so a
6896
+ * LIKE against them cannot seek any index, and flagging them would push 49 junction
6897
+ * entities into the global-search fan-out with unindexable predicates — the exact cost
6898
+ * `search-guardrails.ts` exists to prevent. It also picked up identifiers (`RecordID`,
6899
+ * `Token`, `ExternalSystemRecordID`) and a `Description`, which `isNarrativeFieldName`
6900
+ * rejects on the LLM path. And it would still not have fixed `MJ: Employees`, whose only
6901
+ * `IsNameField` is the virtual `FirstLast`; keying on the name reaches its real
6902
+ * `FirstName` / `LastName` columns, which is what the reported bug needed.
6903
+ * 2. **Clear** — an entity STILL left with no searchable field has `AllowUserSearchAPI` turned
6904
+ * off, so it drops out of the search fan-out instead of contributing noise.
6905
+ *
6906
+ * Both honor the `AutoUpdate*` opt-outs, which is how an operator pins a hand-made decision —
6907
+ * and is exactly how the curated entries in `metadata/entities/.entity-search-exclusions.json`
6908
+ * protect themselves (they set `AutoUpdateAllowUserSearchAPI` to false alongside the flag).
6909
+ *
6910
+ * Full-text-search entities are exempt from both. An FTS entity is searchable through its
6911
+ * INDEX: `createViewUserSearchSQL` takes the full-text branch before it ever reads
6912
+ * `IncludeInUserSearchAPI`, so those flags are dead metadata there and the entity is a
6913
+ * perfectly valid search target without them.
6914
+ *
6915
+ * Written as `UPDATE ... WHERE <key> IN (subquery)` rather than `UPDATE ... FROM ... JOIN`,
6916
+ * which is T-SQL-only. The subquery form is ANSI and runs unchanged on both platforms.
6917
+ */
6918
+ buildSearchFlagHygieneSQL(excludeSchemas) {
6919
+ const coreSchema = MjCoreSchema();
6920
+ const entity = this.qs(coreSchema, 'Entity');
6921
+ const entityField = this.qs(coreSchema, 'EntityField');
6922
+ const yes = this.boolLit(true);
6923
+ const no = this.boolLit(false);
6924
+ // Same set the LLM path's isNameLikeFieldName() tests, lowered for a case-insensitive
6925
+ // comparison that does not depend on the database collation.
6926
+ const nameLikeList = NAME_LIKE_FIELD_NAMES.map(n => `'${n.toLowerCase()}'`).join(',');
6927
+ // Every name in NAME_LIKE_FIELD_NAMES resolves to the same predicate, so one literal covers
6928
+ // the whole seed. This MUST be set explicitly: `EntityField.UserSearchPredicateAPI` defaults
6929
+ // to 'Contains' in the database, which is `LIKE '%term%'` — the unindexable scan the
6930
+ // guardrails exist to prevent. Seeding the flag without the predicate would have made every
6931
+ // seeded entity a full scan on every keystroke.
6932
+ const seedPredicate = DefaultPredicateFor(NAME_LIKE_FIELD_NAMES[0]);
6933
+ // The entity-shape guardrails the LLM path applies (`entityLevelEnableBlockedReason`).
6934
+ // A log / audit / run-history table grows without bound and a detail / line-item child is
6935
+ // reached through its parent; neither is a global-search target, whichever columns it has.
6936
+ // Expressed as SQL rather than reusing the regex helpers because this runs in the database.
6937
+ const shapeSuffixes = [
6938
+ 'Logs', 'Log', 'Runs', 'Run', 'Run History', 'Run Steps', 'Run Messages', 'Execution Logs',
6939
+ 'Details', 'Detail', 'Lines', 'Line', 'Items', 'Item', 'Steps', 'Step',
6940
+ 'Params', 'Param', 'Mappings', 'Mapping',
6941
+ ];
6942
+ // WORD boundaries, not raw suffixes. `LIKE '%Lines'` is a case-insensitive endsWith under
6943
+ // the default collation, so it matched `Pipelines`, `Guidelines`, `Timelines`, `Airlines`,
6944
+ // `Deadlines` and `Baselines`; `LIKE '%Logs'` matched `Catalogs`, `Dialogs` and `Blogs`;
6945
+ // `LIKE '%Audit%'` matched `Auditors`. MJ's own metadata was not exempt — `MJ: ML Training
6946
+ // Pipelines` was caught by `'%Lines'`. Those entities were then excluded from the seed and
6947
+ // swept up by the clear (which carries no shape filter, deliberately — see below), so a
6948
+ // guardrail meaning "do not bother seeding these" silently turned search OFF on them.
6949
+ //
6950
+ // Matching the final WORD keeps every intended shape — `Order Details`, `Audit Logs`,
6951
+ // `MJ: AI Agent Runs` — while `Catalogs`, `Pipelines` and `Auditors` fall through to the
6952
+ // seed as ordinary entities.
6953
+ const nameEndsWithWord = (word) => `(e.${this.qi('Name')} = '${word}' OR e.${this.qi('Name')} LIKE '% ${word}')`;
6954
+ const nameContainsWord = (word) => `(e.${this.qi('Name')} = '${word}'`
6955
+ + ` OR e.${this.qi('Name')} LIKE '${word} %'`
6956
+ + ` OR e.${this.qi('Name')} LIKE '% ${word}'`
6957
+ + ` OR e.${this.qi('Name')} LIKE '% ${word} %')`;
6958
+ const shapeClauses = shapeSuffixes
6959
+ .map(sfx => nameEndsWithWord(sfx))
6960
+ .concat([nameContainsWord('Audit'), nameContainsWord('Record Change')])
6961
+ .join(' OR ');
6962
+ const entityShapeFilter = `AND NOT (${shapeClauses})`;
6963
+ // Schema names are configuration, not literals we control: double any apostrophe rather
6964
+ // than interpolating it straight into the statement.
6965
+ const schemaFilter = excludeSchemas.length > 0
6966
+ ? `AND e.${this.qi('SchemaName')} NOT IN (${excludeSchemas.map(sc => `'${sc.replace(/'/g, "''")}'`).join(',')})`
6967
+ : '';
6968
+ // "This entity has nothing a user search can match." Mirrors
6969
+ // `EntityInfo.HasSearchFields` and the runtime screen in EntitySearchProvider.
6970
+ const noSearchableField = `NOT EXISTS (
6971
+ SELECT 1 FROM ${entityField} f2
6972
+ WHERE f2.${this.qi('EntityID')} = e.${this.qi('ID')}
6973
+ AND f2.${this.qi('IncludeInUserSearchAPI')} = ${yes}
6974
+ )`;
6975
+ // An FTS entity is searchable through its index, with no per-field flags involved.
6976
+ const notFullText = `AND ${this.coalesce(`e.${this.qi('FullTextSearchEnabled')}`, no)} = ${no}`;
6977
+ // The eligibility predicate here mirrors `isFieldEligibleForUserSearch` (and the runtime
6978
+ // `isTextSearchableType` it was written against): not the primary key, a bounded text
6979
+ // column. A name field that is neither is left alone rather than flagged uselessly.
6980
+ // The candidate SELECTs are built once and used twice: once as the UPDATE's subquery, once
6981
+ // as the probe `applySearchFlagHygiene` runs to decide whether to emit the UPDATE at all.
6982
+ // Sharing the text is the point — a probe whose predicates could drift from the statement
6983
+ // it guards would either suppress a needed write or emit a no-op one.
6984
+ const seedCandidateSQL = `
6985
+ SELECT ranked.${this.qi('ID')} FROM (
6986
+ SELECT f.${this.qi('ID')},
6987
+ ROW_NUMBER() OVER (
6988
+ PARTITION BY f.${this.qi('EntityID')}
6989
+ ORDER BY f.${this.qi('Sequence')}, f.${this.qi('Name')}
6990
+ ) AS rn
6991
+ FROM ${entityField} f
6992
+ INNER JOIN ${entity} e ON e.${this.qi('ID')} = f.${this.qi('EntityID')}
6993
+ WHERE LOWER(f.${this.qi('Name')}) IN (${nameLikeList})
6994
+ AND f.${this.qi('AutoUpdateIncludeInUserSearchAPI')} = ${yes}
6995
+ AND f.${this.qi('IncludeInUserSearchAPI')} = ${no}
6996
+ AND ${this.coalesce(`f.${this.qi('IsPrimaryKey')}`, no)} = ${no}
6997
+ AND ${this.coalesce(`f.${this.qi('IsVirtual')}`, no)} = ${no}
6998
+ AND LOWER(f.${this.qi('Type')}) IN ('nvarchar','varchar','char','nchar')
6999
+ AND ${this.coalesce(`f.${this.qi('Length')}`, '0')} <> -1
7000
+ AND e.${this.qi('VirtualEntity')} = ${no}
7001
+ AND e.${this.qi('AllowUserSearchAPI')} = ${yes}
7002
+ ${notFullText}
7003
+ ${entityShapeFilter}
7004
+ ${schemaFilter}
7005
+ AND ${noSearchableField}
7006
+ ) ranked
7007
+ WHERE ranked.rn <= ${MAX_SEARCHABLE_FIELDS_PER_ENTITY}`;
7008
+ // The clear carries NO entity-shape filter, and that is deliberate rather than an
7009
+ // oversight: a log / run / detail table is exactly what should drop out of the search
7010
+ // fan-out, so the shapes the seed refuses to touch are meant to fall through to here.
7011
+ //
7012
+ // That composition is load-bearing and worth stating, because it means the shape list
7013
+ // above does not merely withhold help — it DECIDES which entities get search turned off.
7014
+ // A name wrongly matched there is not "left alone", it is disabled. Which is why the
7015
+ // matching is word-boundary (see nameEndsWithWord) and why widening that list is a
7016
+ // destructive change, not a conservative one.
7017
+ const clearWhere = `e.${this.qi('AllowUserSearchAPI')} = ${yes}
7018
+ AND e.${this.qi('AutoUpdateAllowUserSearchAPI')} = ${yes}
7019
+ AND e.${this.qi('VirtualEntity')} = ${no}
7020
+ ${notFullText}
7021
+ ${schemaFilter}
7022
+ AND ${noSearchableField}`;
7023
+ const clearCandidateSQL = `
7024
+ SELECT e.${this.qi('ID')}
7025
+ FROM ${entity} e
7026
+ WHERE ${clearWhere}`;
7027
+ const seedSQL = `
7028
+ UPDATE ${entityField}
7029
+ SET ${this.qi('IncludeInUserSearchAPI')} = ${yes},
7030
+ ${this.qi('UserSearchPredicateAPI')} = '${seedPredicate}'
7031
+ WHERE ${this.qi('ID')} IN (${seedCandidateSQL}
7032
+ )`;
7033
+ const clearSQL = `
7034
+ UPDATE ${entity}
7035
+ SET ${this.qi('AllowUserSearchAPI')} = ${no}
7036
+ WHERE ${this.qi('ID')} IN (${clearCandidateSQL}
7037
+ )`;
7038
+ // Derived-table alias is required by T-SQL and accepted by PostgreSQL, so one form serves
7039
+ // both. COUNT(*) rather than EXISTS because the count also feeds the status line.
7040
+ const seedProbeSQL = `SELECT COUNT(*) AS ${this.qi('Cnt')} FROM (${seedCandidateSQL}
7041
+ ) probe`;
7042
+ // The clear probe returns NAMES, not a count. Disabling search is the destructive half of
7043
+ // this pass and it is effectively one-way: getting it back means flagging a field by hand
7044
+ // AND pinning AutoUpdateAllowUserSearchAPI = 0, or the next run undoes the repair. An
7045
+ // operator told "cleared 14 entities" has no way to learn which 14 without going to the
7046
+ // database; naming them in the run output is the difference between an auditable change
7047
+ // and a silent one. Shares `clearWhere` with the UPDATE so the two cannot disagree.
7048
+ const clearProbeSQL = `
7049
+ SELECT e.${this.qi('Name')} AS ${this.qi('Name')}
7050
+ FROM ${entity} e
7051
+ WHERE ${clearWhere}
7052
+ ORDER BY e.${this.qi('Name')}`;
7053
+ return { seedSQL, clearSQL, seedProbeSQL, clearProbeSQL };
7054
+ }
7055
+ /**
7056
+ * Run the deterministic search-flag hygiene pass (see {@link buildSearchFlagHygieneSQL}).
7057
+ *
7058
+ * Runs OUTSIDE `applyAdvancedGeneration` and therefore regardless of whether advanced
7059
+ * generation is enabled — which is the whole point, since it is off by default and an entity
7060
+ * created under that default is exactly the one that needs this. Ordered after it so the
7061
+ * model's choices, when it did run, are already in place and the seed only fills a real gap.
7062
+ *
7063
+ * COMPARE FIRST, then write — the T20 contract every-run config writers are held to (see
7064
+ * `__tests__/idempotency/config-writers-compare-first.test.ts`). The two UPDATEs converge on
7065
+ * their own, because the first pass makes `noSearchableField` false for every row it touches,
7066
+ * so re-running them changes nothing. That is not sufficient: `LogSQLBatchAndExecute` writes
7067
+ * whatever it is handed into the run's `CodeGen_Run_*.sql` capture whether or not a row moves,
7068
+ * and the drift gate's warm-twice stage fails on ANY capture surviving a second run. Emitting
7069
+ * unconditionally therefore reddened the gate on every PR while the data was perfectly
7070
+ * idempotent — the data converged, the log did not. So each statement is emitted only when its
7071
+ * own candidate probe finds work.
7072
+ */
7073
+ async applySearchFlagHygiene(pool, excludeSchemas) {
7074
+ try {
7075
+ const { seedSQL, clearSQL, seedProbeSQL, clearProbeSQL } = this.buildSearchFlagHygieneSQL(excludeSchemas);
7076
+ // Order matters: seeding first means an entity whose name field was just flagged is no
7077
+ // longer a candidate for having its AllowUserSearchAPI cleared. Reversed, the pass would
7078
+ // disable search on an entity it was about to make searchable.
7079
+ //
7080
+ // 4th arg is `isRecurringScript`, NOT a throw flag — passing `false` here is the default
7081
+ // and is spelled out only to make the intent explicit. Errors are caught below.
7082
+ const seedCount = await this.searchFlagHygieneCandidateCount(pool, seedProbeSQL);
7083
+ if (seedCount > 0) {
7084
+ logStatus(` Search-flag hygiene: seeding ${seedCount} name field(s)`);
7085
+ await this.logSQLBatchAndExecute(pool, [seedSQL], 'Deterministic search-flag hygiene — seed name fields', false);
7086
+ }
7087
+ // Probed AFTER the seed has run, not alongside it. Seeding changes which entities still
7088
+ // have nothing searchable, so a clear probe taken before it would count entities the seed
7089
+ // was about to repair and emit a statement that then matched nothing — reintroducing the
7090
+ // stray capture file this is here to avoid.
7091
+ const clearNames = await this.searchFlagHygieneClearCandidates(pool, clearProbeSQL);
7092
+ if (clearNames.length > 0) {
7093
+ // NAME them. This is the destructive half and it is effectively one-way for an
7094
+ // operator who does not know it happened.
7095
+ const shown = clearNames.slice(0, 25).join(', ');
7096
+ const more = clearNames.length > 25 ? `, ... and ${clearNames.length - 25} more` : '';
7097
+ logStatus(` Search-flag hygiene: turning AllowUserSearchAPI OFF on ${clearNames.length} entity(ies): ${shown}${more}`);
7098
+ await this.logSQLBatchAndExecute(pool, [clearSQL], 'Deterministic search-flag hygiene — clear AllowUserSearchAPI', false);
7099
+ }
7100
+ return true;
7101
+ }
7102
+ catch (ex) {
7103
+ // A probe that cannot run is NOT the same as "nothing to do", and must not be allowed to
7104
+ // look like it. Before this pass compared first, a malformed statement was appended to
7105
+ // the CodeGen_Run capture by SQLLogging BEFORE it was executed, so a broken pass left a
7106
+ // surviving artifact and reddened the drift gate. Probing first removes that signal: the
7107
+ // throw now happens before anything is written, so without a loud failure here the pass
7108
+ // would silently do nothing while every gate stayed green — the exact silent no-op this
7109
+ // whole change exists to eliminate, one level up in the fixer. The caller fails the run.
7110
+ logError('Search-flag hygiene FAILED — search flags were NOT reconciled on this run', ex);
7111
+ return false;
7112
+ }
7113
+ }
7114
+ /**
7115
+ * Row count for one of the search-flag hygiene probes (see {@link applySearchFlagHygiene}).
7116
+ *
7117
+ * Reads the first column of the first row positionally rather than by name, because the alias
7118
+ * comes back cased differently across drivers. A probe that returns nothing is treated as no
7119
+ * work, which is the safe direction: the statement is skipped rather than emitted blind.
7120
+ */
7121
+ async searchFlagHygieneCandidateCount(pool, probeSQL) {
7122
+ const result = await this.runQuery(pool, probeSQL);
7123
+ const row = result?.recordset?.[0];
7124
+ if (!row) {
7125
+ return 0;
7126
+ }
7127
+ const count = Number(Object.values(row)[0]);
7128
+ return Number.isFinite(count) ? count : 0;
7129
+ }
7130
+ /**
7131
+ * Names of the entities the clear would disable (see {@link applySearchFlagHygiene}).
7132
+ *
7133
+ * Valid only AFTER the seed has run, since seeding changes which entities still have nothing
7134
+ * searchable. Names rather than a count because this is the destructive half: the run output
7135
+ * is the only place an operator can see which entities lost search, and getting it back is
7136
+ * manual.
7137
+ */
7138
+ async searchFlagHygieneClearCandidates(pool, probeSQL) {
7139
+ const result = await this.runQuery(pool, probeSQL);
7140
+ const rows = result?.recordset ?? [];
7141
+ return rows
7142
+ .map(r => String(Object.values(r)[0] ?? '').trim())
7143
+ .filter(n => n.length > 0);
7144
+ }
6646
7145
  /**
6647
7146
  * Returns true if the field is a sensible target for LIKE-based user search.
6648
7147
  * Mirrors the runtime guard in GenericDatabaseProvider.isTextSearchableType /
@@ -6684,7 +7183,7 @@ export class ManageMetadataBase {
6684
7183
  // Only update if the current value differs from the recommended predicate
6685
7184
  if (field.UserSearchPredicateAPI !== sp.predicate) {
6686
7185
  sqlStatements.push(`
6687
- UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
7186
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6688
7187
  SET UserSearchPredicateAPI = '${sp.predicate}'
6689
7188
  WHERE ID = '${field.ID}'
6690
7189
  AND AutoUpdateUserSearchPredicate = ${this.boolLit(true)}
@@ -6715,7 +7214,7 @@ export class ManageMetadataBase {
6715
7214
  // the field-level guardrails, and refuses log/audit and detail/line-
6716
7215
  // item-shaped entities. Anything blocked here gets a one-line note in
6717
7216
  // the CodeGen run report so the team can audit proposals.
6718
- const blocked = entityLevelEnableBlockedReason({
7217
+ const blocked = EntityLevelEnableBlockedReason({
6719
7218
  entityName: entity.Name,
6720
7219
  confidence: result.confidence,
6721
7220
  acceptedSearchableFieldsCount: (result.searchableFields ?? []).length,
@@ -6731,7 +7230,7 @@ export class ManageMetadataBase {
6731
7230
  }
6732
7231
  if (newValue !== currentValue) {
6733
7232
  sqlStatements.push(`
6734
- UPDATE ${this.qs(mj_core_schema(), 'Entity')}
7233
+ UPDATE ${this.qs(MjCoreSchema(), 'Entity')}
6735
7234
  SET AllowUserSearchAPI = ${this.boolLit(newValue)}
6736
7235
  WHERE ID = '${entity.ID}'
6737
7236
  AND AutoUpdateAllowUserSearchAPI = ${this.boolLit(true)}
@@ -6774,7 +7273,7 @@ export class ManageMetadataBase {
6774
7273
  const currentValue = !!entity.FullTextSearchEnabled;
6775
7274
  if (newValue !== currentValue) {
6776
7275
  sqlStatements.push(`
6777
- UPDATE ${this.qs(mj_core_schema(), 'Entity')}
7276
+ UPDATE ${this.qs(MjCoreSchema(), 'Entity')}
6778
7277
  SET FullTextSearchEnabled = ${this.boolLit(newValue)}
6779
7278
  WHERE ID = '${entity.ID}'
6780
7279
  AND AutoUpdateFullTextSearch = ${this.boolLit(true)}
@@ -6796,7 +7295,7 @@ export class ManageMetadataBase {
6796
7295
  }
6797
7296
  if (!field.FullTextSearchEnabled) {
6798
7297
  sqlStatements.push(`
6799
- UPDATE ${this.qs(mj_core_schema(), 'EntityField')}
7298
+ UPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6800
7299
  SET FullTextSearchEnabled = ${this.boolLit(true)}
6801
7300
  WHERE ID = '${field.ID}'
6802
7301
  AND AutoUpdateFullTextSearch = ${this.boolLit(true)}
@@ -6841,7 +7340,7 @@ export class ManageMetadataBase {
6841
7340
  * Respects the AutoUpdateSupportsGeoCoding flag — if 0, the value is locked.
6842
7341
  */
6843
7342
  async detectAndSetGeoCodingSupport(pool, entity) {
6844
- const schema = mj_core_schema();
7343
+ const schema = MjCoreSchema();
6845
7344
  // Check if the entity's AutoUpdateSupportsGeoCoding flag allows us to modify it
6846
7345
  const autoUpdateResult = await pool.query(`
6847
7346
  SELECT ${this.qi('AutoUpdateSupportsGeoCoding')}, ${this.qi('SupportsGeoCoding')}
@@ -6864,7 +7363,7 @@ export class ManageMetadataBase {
6864
7363
  const shouldSupportGeo = existingGeoCount > 0;
6865
7364
  const currentValue = row.SupportsGeoCoding ? true : false;
6866
7365
  if (shouldSupportGeo !== currentValue) {
6867
- await this.LogSQLAndExecute(pool, `
7366
+ await this.logSQLAndExecute(pool, `
6868
7367
  UPDATE ${this.qs(schema, 'Entity')}
6869
7368
  SET ${this.qi('SupportsGeoCoding')} = ${this.boolLit(shouldSupportGeo)}
6870
7369
  WHERE ${this.qi('ID')} = '${entity.ID}' AND ${this.qi('AutoUpdateSupportsGeoCoding')} = ${this.boolLit(true)}
@@ -6938,14 +7437,14 @@ export class ManageMetadataBase {
6938
7437
  continue;
6939
7438
  }
6940
7439
  const lockCtx = {
6941
- isNewEntity: ctx.isNewEntity,
6942
- isNewField: ManageMetadataBase.isFieldNew(entity.ID, field.Name),
6943
- descriptionReopened: ManageMetadataBase.isDisplayNameReopened(entity.ID, field.Name),
6944
- typeReopened: ManageMetadataBase.isTypeReopened(entity.ID, field.Name),
6945
- existingCategories,
7440
+ IsNewEntity: ctx.isNewEntity,
7441
+ IsNewField: ManageMetadataBase.isFieldNew(entity.ID, field.Name),
7442
+ DescriptionReopened: ManageMetadataBase.isDisplayNameReopened(entity.ID, field.Name),
7443
+ TypeReopened: ManageMetadataBase.isTypeReopened(entity.ID, field.Name),
7444
+ ExistingCategories: existingCategories,
6946
7445
  };
6947
- const update = computeFieldMetadataUpdate(field, fieldCategory, lockCtx, this.validateExtendedType.bind(this), (val, fn) => this.sanitizeCodeType(val, fn ?? field.Name, entity.Name));
6948
- for (const skip of update.skipped) {
7446
+ const update = ComputeFieldMetadataUpdate(field, fieldCategory, lockCtx, this.validateExtendedType.bind(this), (val, fn) => this.sanitizeCodeType(val, fn ?? field.Name, entity.Name));
7447
+ for (const skip of update.Skipped) {
6949
7448
  if (skip.reason === 'locked') {
6950
7449
  reporter.counter('ai.fieldsLocked');
6951
7450
  }
@@ -6972,7 +7471,7 @@ export class ManageMetadataBase {
6972
7471
  setClauses.push(`CodeType = ${codeVal}`);
6973
7472
  }
6974
7473
  if (setClauses.length > 0) {
6975
- sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE ${this.qs(mj_core_schema(), 'EntityField')}
7474
+ sqlStatements.push(`\n-- UPDATE Entity Field Category Info ${entity.Name}.${field.Name} \nUPDATE ${this.qs(MjCoreSchema(), 'EntityField')}
6976
7475
  SET
6977
7476
  ${setClauses.join(',\n ')}
6978
7477
  WHERE
@@ -6981,7 +7480,7 @@ WHERE
6981
7480
  }
6982
7481
  if (sqlStatements.length > 0) {
6983
7482
  try {
6984
- await this.LogSQLBatchAndExecute(pool, sqlStatements, `Set categories for ${sqlStatements.length} fields`, false);
7483
+ await this.logSQLBatchAndExecute(pool, sqlStatements, `Set categories for ${sqlStatements.length} fields`, false);
6985
7484
  }
6986
7485
  catch (ex) {
6987
7486
  logError('Error Applying Field Categories', ex);
@@ -6994,19 +7493,19 @@ WHERE
6994
7493
  async applyEntityIcon(pool, entityId, entityIcon, entityName) {
6995
7494
  if (!entityIcon || entityIcon.trim().length === 0)
6996
7495
  return;
6997
- const checkSQL = `SELECT Icon FROM ${this.qs(mj_core_schema(), 'Entity')} WHERE ID = '${entityId}'`;
7496
+ const checkSQL = `SELECT Icon FROM ${this.qs(MjCoreSchema(), 'Entity')} WHERE ID = '${entityId}'`;
6998
7497
  const entityCheck = await this.runQuery(pool, checkSQL);
6999
7498
  if (entityCheck.recordset.length > 0) {
7000
7499
  const currentIcon = entityCheck.recordset[0].Icon;
7001
7500
  if (!currentIcon || currentIcon.trim().length === 0) {
7002
7501
  const escapedIcon = entityIcon.replace(/'/g, "''");
7003
7502
  const updateSQL = `
7004
- UPDATE ${this.qs(mj_core_schema(), 'Entity')}
7503
+ UPDATE ${this.qs(MjCoreSchema(), 'Entity')}
7005
7504
  SET ${this.qi('Icon')} = '${escapedIcon}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
7006
7505
  WHERE ${this.qi('ID')} = '${entityId}'
7007
7506
  `;
7008
7507
  try {
7009
- await this.LogSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
7508
+ await this.logSQLAndExecute(pool, updateSQL, `Set entity icon to ${entityIcon}`, false);
7010
7509
  logStatus(` Set entity icon: ${entityIcon}`);
7011
7510
  }
7012
7511
  catch (ex) {
@@ -7021,26 +7520,26 @@ WHERE
7021
7520
  async applyCategoryInfoSettings(pool, entityId, categoryInfo, _entityName) {
7022
7521
  if (!categoryInfo || Object.keys(categoryInfo).length === 0)
7023
7522
  return;
7024
- const canonicalInfo = canonicalJSONStringify(categoryInfo, 2);
7523
+ const canonicalInfo = CanonicalJSONStringify(categoryInfo, 2);
7025
7524
  const infoJSON = canonicalInfo.replace(/'/g, "''");
7026
7525
  const ENTITY_SETTING_NAMESPACE = '6ba7b810-9dad-11d1-80b4-00c04fd430c8';
7027
7526
  // Upsert FieldCategoryInfo (new format)
7028
- const checkNewSQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
7527
+ const checkNewSQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
7029
7528
  const existingNew = await this.runQuery(pool, checkNewSQL);
7030
7529
  if (existingNew.recordset.length > 0) {
7031
7530
  let isMatch = false;
7032
7531
  try {
7033
7532
  const rawVal = existingNew.recordset[0].Value;
7034
7533
  const parsed = typeof rawVal === 'string' ? JSON.parse(rawVal) : rawVal;
7035
- isMatch = deepEqualJSON(parsed, categoryInfo);
7534
+ isMatch = DeepEqualJSON(parsed, categoryInfo);
7036
7535
  }
7037
7536
  catch {
7038
7537
  isMatch = false;
7039
7538
  }
7040
7539
  if (!isMatch) {
7041
7540
  try {
7042
- await this.LogSQLAndExecute(pool, `
7043
- UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
7541
+ await this.logSQLAndExecute(pool, `
7542
+ UPDATE ${this.qs(MjCoreSchema(), 'EntitySetting')}
7044
7543
  SET ${this.qi('Value')} = '${infoJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
7045
7544
  WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'
7046
7545
  `, `Update FieldCategoryInfo setting for entity`, false);
@@ -7053,11 +7552,11 @@ WHERE
7053
7552
  else {
7054
7553
  const newId = uuidv5(`${entityId}|FieldCategoryInfo`, ENTITY_SETTING_NAMESPACE);
7055
7554
  try {
7056
- const checkQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
7057
- const insertSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
7555
+ const checkQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryInfo'`;
7556
+ const insertSQL = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
7058
7557
  VALUES ('${newId}', '${entityId}', 'FieldCategoryInfo', '${infoJSON}', ${this.utcNow()}, ${this.utcNow()})`;
7059
7558
  const sSQL = this.conditionalInsert(checkQuery, insertSQL);
7060
- await this.LogSQLAndExecute(pool, sSQL, `Insert FieldCategoryInfo setting for entity`, false);
7559
+ await this.logSQLAndExecute(pool, sSQL, `Insert FieldCategoryInfo setting for entity`, false);
7061
7560
  }
7062
7561
  catch (ex) {
7063
7562
  logError('Error Applying Category Info Settings: Part 2', ex);
@@ -7070,24 +7569,24 @@ WHERE
7070
7569
  iconsOnly[category] = info.icon;
7071
7570
  }
7072
7571
  }
7073
- const canonicalIcons = canonicalJSONStringify(iconsOnly, 2);
7572
+ const canonicalIcons = CanonicalJSONStringify(iconsOnly, 2);
7074
7573
  const iconsJSON = canonicalIcons.replace(/'/g, "''");
7075
- const checkLegacySQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
7574
+ const checkLegacySQL = `SELECT ${this.qi('ID')}, ${this.qi('Value')} FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
7076
7575
  const existingLegacy = await this.runQuery(pool, checkLegacySQL);
7077
7576
  if (existingLegacy.recordset.length > 0) {
7078
7577
  let isMatch = false;
7079
7578
  try {
7080
7579
  const rawVal = existingLegacy.recordset[0].Value;
7081
7580
  const parsed = typeof rawVal === 'string' ? JSON.parse(rawVal) : rawVal;
7082
- isMatch = deepEqualJSON(parsed, iconsOnly);
7581
+ isMatch = DeepEqualJSON(parsed, iconsOnly);
7083
7582
  }
7084
7583
  catch {
7085
7584
  isMatch = false;
7086
7585
  }
7087
7586
  if (!isMatch) {
7088
7587
  try {
7089
- await this.LogSQLAndExecute(pool, `
7090
- UPDATE ${this.qs(mj_core_schema(), 'EntitySetting')}
7588
+ await this.logSQLAndExecute(pool, `
7589
+ UPDATE ${this.qs(MjCoreSchema(), 'EntitySetting')}
7091
7590
  SET ${this.qi('Value')} = '${iconsJSON}', ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
7092
7591
  WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'
7093
7592
  `, `Update FieldCategoryIcons setting (legacy)`, false);
@@ -7100,11 +7599,11 @@ WHERE
7100
7599
  else {
7101
7600
  const newId = uuidv5(`${entityId}|FieldCategoryIcons`, ENTITY_SETTING_NAMESPACE);
7102
7601
  try {
7103
- const checkQuery = `SELECT 1 FROM ${this.qs(mj_core_schema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
7104
- const insertSQL = `INSERT INTO ${this.qs(mj_core_schema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
7602
+ const checkQuery = `SELECT 1 FROM ${this.qs(MjCoreSchema(), 'EntitySetting')} WHERE ${this.qi('EntityID')} = '${entityId}' AND ${this.qi('Name')} = 'FieldCategoryIcons'`;
7603
+ const insertSQL = `INSERT INTO ${this.qs(MjCoreSchema(), 'EntitySetting')} (${this.qi('ID')}, ${this.qi('EntityID')}, ${this.qi('Name')}, ${this.qi('Value')}, ${this.qi(EntityInfo.CreatedAtFieldName)}, ${this.qi(EntityInfo.UpdatedAtFieldName)})
7105
7604
  VALUES ('${newId}', '${entityId}', 'FieldCategoryIcons', '${iconsJSON}', ${this.utcNow()}, ${this.utcNow()})`;
7106
7605
  const sSQL = this.conditionalInsert(checkQuery, insertSQL);
7107
- await this.LogSQLAndExecute(pool, sSQL, `Insert FieldCategoryIcons setting (legacy)`, false);
7606
+ await this.logSQLAndExecute(pool, sSQL, `Insert FieldCategoryIcons setting (legacy)`, false);
7108
7607
  }
7109
7608
  catch (ex) {
7110
7609
  logError('Error Applying Category Info Settings: Part 4', ex);
@@ -7117,12 +7616,12 @@ WHERE
7117
7616
  */
7118
7617
  async applyEntityImportance(pool, entityId, importance, entityName) {
7119
7618
  const updateSQL = `
7120
- UPDATE ${this.qs(mj_core_schema(), 'ApplicationEntity')}
7619
+ UPDATE ${this.qs(MjCoreSchema(), 'ApplicationEntity')}
7121
7620
  SET ${this.qi('DefaultForNewUser')} = ${this.boolLit(importance.defaultForNewUser)}, ${this.qi(EntityInfo.UpdatedAtFieldName)} = ${this.utcNow()}
7122
7621
  WHERE ${this.qi('EntityID')} = '${entityId}'
7123
7622
  `;
7124
7623
  try {
7125
- await this.LogSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${importance.defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
7624
+ await this.logSQLAndExecute(pool, updateSQL, `Set DefaultForNewUser=${importance.defaultForNewUser} for NEW entity (category: ${importance.entityCategory}, confidence: ${importance.confidence})`, false);
7126
7625
  logStatus(` Entity importance (NEW Entity): ${importance.entityCategory} (defaultForNewUser: ${importance.defaultForNewUser}, confidence: ${importance.confidence})`);
7127
7626
  logStatus(` Reasoning: ${importance.reasoning}`);
7128
7627
  }
@@ -7140,8 +7639,8 @@ WHERE
7140
7639
  * @param isRecurringScript - if set to true tells the logger that the provided SQL represents a recurring script meaning it is something that is executed, generally, for all CodeGen runs. In these cases, the Config settings can result in omitting these recurring scripts from being logged because the configuration environment may have those recurring scripts already set to run after all run-specific migrations get run.
7141
7640
  * @returns - The result of the query execution.
7142
7641
  */
7143
- async LogSQLAndExecute(pool, query, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO') {
7144
- return await SQLLogging.LogSQLAndExecute(pool, this.qsql(query), description, isRecurringScript, includeBatchSeparator, batchSeparator);
7642
+ async logSQLAndExecute(pool, query, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO', requiresOwnBatch = false) {
7643
+ return await SQLLogging.LogSQLAndExecute(pool, this.qsql(query), description, isRecurringScript, includeBatchSeparator, batchSeparator, requiresOwnBatch);
7145
7644
  }
7146
7645
  /**
7147
7646
  * Logs and executes a sequence of SQL statements as a single batch.
@@ -7159,17 +7658,17 @@ WHERE
7159
7658
  * Empty / whitespace-only statements are filtered out. Returns
7160
7659
  * `undefined` (no execution) when nothing remains.
7161
7660
  */
7162
- async LogSQLBatchAndExecute(pool, statements, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO') {
7661
+ async logSQLBatchAndExecute(pool, statements, description, isRecurringScript = false, includeBatchSeparator = false, batchSeparator = 'GO') {
7163
7662
  const terminated = [];
7164
7663
  for (const s of statements) {
7165
- const trimmed = (s ?? '').replace(/[\s;]+$/g, '');
7664
+ const trimmed = TrimTrailingStatementTerminators(s ?? '');
7166
7665
  if (trimmed.length === 0)
7167
7666
  continue;
7168
7667
  terminated.push(`${trimmed};`);
7169
7668
  }
7170
7669
  if (terminated.length === 0)
7171
7670
  return undefined;
7172
- return await this.LogSQLAndExecute(pool, terminated.join('\n'), description, isRecurringScript, includeBatchSeparator, batchSeparator);
7671
+ return await this.logSQLAndExecute(pool, terminated.join('\n'), description, isRecurringScript, includeBatchSeparator, batchSeparator);
7173
7672
  }
7174
7673
  }
7175
7674
  //# sourceMappingURL=manage-metadata.js.map