@memberjunction/core-entities-server 6.2.0-edge.6 → 6.2.0-edge.7

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 (159) hide show
  1. package/README.md +155 -100
  2. package/dist/custom/MJAIAgentExampleEntityServer.server.d.ts +2 -0
  3. package/dist/custom/MJAIAgentExampleEntityServer.server.d.ts.map +1 -1
  4. package/dist/custom/MJAIAgentExampleEntityServer.server.js +8 -5
  5. package/dist/custom/MJAIAgentExampleEntityServer.server.js.map +1 -1
  6. package/dist/custom/MJAIAgentNoteEntityServer.server.d.ts +2 -0
  7. package/dist/custom/MJAIAgentNoteEntityServer.server.d.ts.map +1 -1
  8. package/dist/custom/MJAIAgentNoteEntityServer.server.js +8 -5
  9. package/dist/custom/MJAIAgentNoteEntityServer.server.js.map +1 -1
  10. package/dist/custom/MJAIAgentRelationshipEntityServer.server.d.ts +17 -0
  11. package/dist/custom/MJAIAgentRelationshipEntityServer.server.d.ts.map +1 -0
  12. package/dist/custom/MJAIAgentRelationshipEntityServer.server.js +71 -0
  13. package/dist/custom/MJAIAgentRelationshipEntityServer.server.js.map +1 -0
  14. package/dist/custom/MJAIAgentSessionEntityServer.server.d.ts +8 -22
  15. package/dist/custom/MJAIAgentSessionEntityServer.server.d.ts.map +1 -1
  16. package/dist/custom/MJAIAgentSessionEntityServer.server.js +24 -1
  17. package/dist/custom/MJAIAgentSessionEntityServer.server.js.map +1 -1
  18. package/dist/custom/MJAIAgentUserPolicyEntityServer.server.d.ts +23 -0
  19. package/dist/custom/MJAIAgentUserPolicyEntityServer.server.d.ts.map +1 -0
  20. package/dist/custom/MJAIAgentUserPolicyEntityServer.server.js +53 -0
  21. package/dist/custom/MJAIAgentUserPolicyEntityServer.server.js.map +1 -0
  22. package/dist/custom/MJAIPromptEntityServer.server.d.ts.map +1 -1
  23. package/dist/custom/MJAIPromptEntityServer.server.js +2 -1
  24. package/dist/custom/MJAIPromptEntityServer.server.js.map +1 -1
  25. package/dist/custom/MJBrandKitEntityServer.server.d.ts +14 -0
  26. package/dist/custom/MJBrandKitEntityServer.server.d.ts.map +1 -0
  27. package/dist/custom/MJBrandKitEntityServer.server.js +75 -0
  28. package/dist/custom/MJBrandKitEntityServer.server.js.map +1 -0
  29. package/dist/custom/MJCompanyIntegrationEntityServer.server.d.ts.map +1 -1
  30. package/dist/custom/MJCompanyIntegrationEntityServer.server.js +1 -0
  31. package/dist/custom/MJCompanyIntegrationEntityServer.server.js.map +1 -1
  32. package/dist/custom/MJComponentEntityServer.server.d.ts +2 -0
  33. package/dist/custom/MJComponentEntityServer.server.d.ts.map +1 -1
  34. package/dist/custom/MJComponentEntityServer.server.js +5 -1
  35. package/dist/custom/MJComponentEntityServer.server.js.map +1 -1
  36. package/dist/custom/MJEntityDocumentEntityServer.server.d.ts.map +1 -1
  37. package/dist/custom/MJEntityDocumentEntityServer.server.js +1 -0
  38. package/dist/custom/MJEntityDocumentEntityServer.server.js.map +1 -1
  39. package/dist/custom/MJListDetailEntityServer.server.d.ts.map +1 -1
  40. package/dist/custom/MJListDetailEntityServer.server.js +1 -0
  41. package/dist/custom/MJListDetailEntityServer.server.js.map +1 -1
  42. package/dist/custom/MJQueryEntityServer.server.d.ts +177 -47
  43. package/dist/custom/MJQueryEntityServer.server.d.ts.map +1 -1
  44. package/dist/custom/MJQueryEntityServer.server.js +465 -162
  45. package/dist/custom/MJQueryEntityServer.server.js.map +1 -1
  46. package/dist/custom/MJQuerySQLEntityServer.server.d.ts +15 -18
  47. package/dist/custom/MJQuerySQLEntityServer.server.d.ts.map +1 -1
  48. package/dist/custom/MJQuerySQLEntityServer.server.js +34 -42
  49. package/dist/custom/MJQuerySQLEntityServer.server.js.map +1 -1
  50. package/dist/custom/MJTagEntityServer.server.d.ts +2 -0
  51. package/dist/custom/MJTagEntityServer.server.d.ts.map +1 -1
  52. package/dist/custom/MJTagEntityServer.server.js +13 -7
  53. package/dist/custom/MJTagEntityServer.server.js.map +1 -1
  54. package/dist/custom/MJUserRoutineEntityServer.server.d.ts.map +1 -1
  55. package/dist/custom/MJUserRoutineEntityServer.server.js +8 -1
  56. package/dist/custom/MJUserRoutineEntityServer.server.js.map +1 -1
  57. package/dist/custom/query-extraction/childRowWriter.d.ts +77 -0
  58. package/dist/custom/query-extraction/childRowWriter.d.ts.map +1 -0
  59. package/dist/custom/query-extraction/childRowWriter.js +277 -0
  60. package/dist/custom/query-extraction/childRowWriter.js.map +1 -0
  61. package/dist/custom/query-extraction/compositionTargets.d.ts +40 -0
  62. package/dist/custom/query-extraction/compositionTargets.d.ts.map +1 -0
  63. package/dist/custom/query-extraction/compositionTargets.js +99 -0
  64. package/dist/custom/query-extraction/compositionTargets.js.map +1 -0
  65. package/dist/custom/query-extraction/declared.d.ts +79 -0
  66. package/dist/custom/query-extraction/declared.d.ts.map +1 -0
  67. package/dist/custom/query-extraction/declared.js +91 -0
  68. package/dist/custom/query-extraction/declared.js.map +1 -0
  69. package/dist/custom/query-extraction/describeFacts.d.ts +40 -0
  70. package/dist/custom/query-extraction/describeFacts.d.ts.map +1 -0
  71. package/dist/custom/query-extraction/describeFacts.js +225 -0
  72. package/dist/custom/query-extraction/describeFacts.js.map +1 -0
  73. package/dist/custom/query-extraction/descriptionPass.d.ts +23 -0
  74. package/dist/custom/query-extraction/descriptionPass.d.ts.map +1 -0
  75. package/dist/custom/query-extraction/descriptionPass.js +95 -0
  76. package/dist/custom/query-extraction/descriptionPass.js.map +1 -0
  77. package/dist/custom/query-extraction/extractQuery.d.ts +25 -0
  78. package/dist/custom/query-extraction/extractQuery.d.ts.map +1 -0
  79. package/dist/custom/query-extraction/extractQuery.js +146 -0
  80. package/dist/custom/query-extraction/extractQuery.js.map +1 -0
  81. package/dist/custom/query-extraction/extractionTypes.d.ts +133 -0
  82. package/dist/custom/query-extraction/extractionTypes.d.ts.map +1 -0
  83. package/dist/custom/query-extraction/extractionTypes.js +2 -0
  84. package/dist/custom/query-extraction/extractionTypes.js.map +1 -0
  85. package/dist/custom/query-extraction/index.d.ts +16 -0
  86. package/dist/custom/query-extraction/index.d.ts.map +1 -1
  87. package/dist/custom/query-extraction/index.js +11 -1
  88. package/dist/custom/query-extraction/index.js.map +1 -1
  89. package/dist/custom/query-extraction/pipeline.d.ts +6 -2
  90. package/dist/custom/query-extraction/pipeline.d.ts.map +1 -1
  91. package/dist/custom/query-extraction/pipeline.js +21 -14
  92. package/dist/custom/query-extraction/pipeline.js.map +1 -1
  93. package/dist/custom/query-extraction/resolve.d.ts +7 -2
  94. package/dist/custom/query-extraction/resolve.d.ts.map +1 -1
  95. package/dist/custom/query-extraction/resolve.js +17 -14
  96. package/dist/custom/query-extraction/resolve.js.map +1 -1
  97. package/dist/custom/query-extraction/sourceBinding.d.ts +57 -0
  98. package/dist/custom/query-extraction/sourceBinding.d.ts.map +1 -0
  99. package/dist/custom/query-extraction/sourceBinding.js +104 -0
  100. package/dist/custom/query-extraction/sourceBinding.js.map +1 -0
  101. package/dist/custom/query-extraction/sync.d.ts +27 -3
  102. package/dist/custom/query-extraction/sync.d.ts.map +1 -1
  103. package/dist/custom/query-extraction/sync.js +69 -44
  104. package/dist/custom/query-extraction/sync.js.map +1 -1
  105. package/dist/custom/query-extraction/templateFacts.d.ts +62 -0
  106. package/dist/custom/query-extraction/templateFacts.d.ts.map +1 -0
  107. package/dist/custom/query-extraction/templateFacts.js +229 -0
  108. package/dist/custom/query-extraction/templateFacts.js.map +1 -0
  109. package/dist/custom/query-extraction/types.d.ts +4 -0
  110. package/dist/custom/query-extraction/types.d.ts.map +1 -1
  111. package/dist/custom/query-refresh/QueryExtractionService.d.ts +54 -0
  112. package/dist/custom/query-refresh/QueryExtractionService.d.ts.map +1 -0
  113. package/dist/custom/query-refresh/QueryExtractionService.js +207 -0
  114. package/dist/custom/query-refresh/QueryExtractionService.js.map +1 -0
  115. package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.d.ts +28 -0
  116. package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.d.ts.map +1 -0
  117. package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.js +106 -0
  118. package/dist/custom/query-refresh/QueryRefreshExtractionServerOperation.js.map +1 -0
  119. package/dist/custom/query-refresh/index.d.ts +14 -0
  120. package/dist/custom/query-refresh/index.d.ts.map +1 -0
  121. package/dist/custom/query-refresh/index.js +10 -0
  122. package/dist/custom/query-refresh/index.js.map +1 -0
  123. package/dist/custom/query-refresh/queryRebind.d.ts +33 -0
  124. package/dist/custom/query-refresh/queryRebind.d.ts.map +1 -0
  125. package/dist/custom/query-refresh/queryRebind.js +85 -0
  126. package/dist/custom/query-refresh/queryRebind.js.map +1 -0
  127. package/dist/custom/query-refresh/refreshAuthorization.d.ts +29 -0
  128. package/dist/custom/query-refresh/refreshAuthorization.d.ts.map +1 -0
  129. package/dist/custom/query-refresh/refreshAuthorization.js +66 -0
  130. package/dist/custom/query-refresh/refreshAuthorization.js.map +1 -0
  131. package/dist/custom/query-refresh/refreshFormat.d.ts +16 -0
  132. package/dist/custom/query-refresh/refreshFormat.d.ts.map +1 -0
  133. package/dist/custom/query-refresh/refreshFormat.js +67 -0
  134. package/dist/custom/query-refresh/refreshFormat.js.map +1 -0
  135. package/dist/custom/query-refresh/refreshOrder.d.ts +25 -0
  136. package/dist/custom/query-refresh/refreshOrder.d.ts.map +1 -0
  137. package/dist/custom/query-refresh/refreshOrder.js +95 -0
  138. package/dist/custom/query-refresh/refreshOrder.js.map +1 -0
  139. package/dist/custom/query-refresh/refreshSelection.d.ts +15 -0
  140. package/dist/custom/query-refresh/refreshSelection.d.ts.map +1 -0
  141. package/dist/custom/query-refresh/refreshSelection.js +99 -0
  142. package/dist/custom/query-refresh/refreshSelection.js.map +1 -0
  143. package/dist/custom/query-refresh/refreshTypes.d.ts +81 -0
  144. package/dist/custom/query-refresh/refreshTypes.d.ts.map +1 -0
  145. package/dist/custom/query-refresh/refreshTypes.js +5 -0
  146. package/dist/custom/query-refresh/refreshTypes.js.map +1 -0
  147. package/dist/custom/util.d.ts +8 -0
  148. package/dist/custom/util.d.ts.map +1 -1
  149. package/dist/custom/util.js +18 -1
  150. package/dist/custom/util.js.map +1 -1
  151. package/dist/engines/QueryEngineServer.d.ts +4 -1
  152. package/dist/engines/QueryEngineServer.d.ts.map +1 -1
  153. package/dist/engines/QueryEngineServer.js +10 -7
  154. package/dist/engines/QueryEngineServer.js.map +1 -1
  155. package/dist/index.d.ts +4 -0
  156. package/dist/index.d.ts.map +1 -1
  157. package/dist/index.js +4 -0
  158. package/dist/index.js.map +1 -1
  159. package/package.json +30 -30
@@ -4,21 +4,56 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
4
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { BaseEntity, LogError, } from "@memberjunction/core";
7
+ import { BaseEntity, BaseEntityResult, DatabaseProviderBase, LogError, ProviderBase, ProviderType, } from "@memberjunction/core";
8
8
  import { ComputeQueryFingerprintAsync, MJQueryEntityExtended, QueryEngine, } from "@memberjunction/core-entities";
9
9
  import { EscapeSQLString, Float32VectorToBase64, RegisterClass, MJGlobal, UUIDsEqual } from "@memberjunction/global";
10
- import { EmbedTextLocalHelper } from "./util.js";
11
- import { RunExtractionPipeline, CleanupQueryData, ConvertTSQLToPostgreSQL, } from "./query-extraction/index.js";
12
- import { SQLParser } from "@memberjunction/sql-parser";
13
- import { GetDialect } from "@memberjunction/sql-dialect";
14
- import { resolveDbPlatformFromEnv } from "@memberjunction/generic-database-provider";
10
+ import { GenericDatabaseProvider, UserCache } from "@memberjunction/generic-database-provider";
11
+ import { CompositionFullPath } from "@memberjunction/sql-parser";
12
+ import { EmbedTextLocalHelper, IsLocalEmbeddingStaleHelper } from "./util.js";
13
+ import { CleanupQueryData, ConvertTSQLToPostgreSQL, CollectDeclaredQueryChildren, HasDeclaredQueryChildren, FindReplacedRows, QueueReplacedRowDeletion, QueryChildNameKey, QueryEntityKey, NO_DECLARED_QUERY_CHILDREN, ComputeQueryExtraction, WriteExtractedChildRows, ApplyFieldFacts, RunDescriptionPass, SQLUsesTemplate, ReadCompositionTargetsAsync, MergeCompositionCandidates, } from "./query-extraction/index.js";
15
14
  import { ApplyApprovalRulesAsync, ApprovalRefusalResult } from "./approvalStamp.js";
15
+ /** Whether `provider` can read views and create entity objects, which is all extraction's writes need. */
16
+ function IsExtractionProvider(provider) {
17
+ if (provider === null || typeof provider !== 'object')
18
+ return false;
19
+ return typeof Reflect.get(provider, 'RunView') === 'function' && typeof Reflect.get(provider, 'GetEntityObject') === 'function';
20
+ }
16
21
  /**
17
- * Server-side query entity with embedding generation, SQL extraction pipeline,
18
- * and dialect auto-conversion. Extends `MJQueryEntityExtended` which provides
19
- * child-relationship getters and business logic available on both client and server.
22
+ * Server-side query entity with embedding generation, SQL extraction, and dialect auto-conversion.
23
+ * Extends `MJQueryEntityExtended`, which provides the child-relationship getters and business
24
+ * logic available on both client and server.
25
+ *
26
+ * **Extraction.** A save of a new query, of a query whose SQL changed, or with
27
+ * {@link ExtractionOverride} `Force`, establishes the query's facts (`ComputeQueryExtraction`):
28
+ * its parameters, compositions and the entities it reads from MJ's parser, and its output fields
29
+ * from the database server's description of each render. Save-time rule errors and repeated
30
+ * output names refuse a new or edited query, with the reasons in the save's result. Once the save
31
+ * of the query row succeeds, the child rows are brought in line with the facts as the system user,
32
+ * keeping what people authored (`WriteExtractedChildRows`), and the query's `UsesTemplate`,
33
+ * `ExtractedSQLHash`, `ExtractionStatus` and `ExtractionMessage` record the run. Empty
34
+ * descriptions are then filled by an LLM after the save's transaction commits, unless the save
35
+ * skips AI actions.
20
36
  */
21
37
  let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtended {
38
+ constructor() {
39
+ super(...arguments);
40
+ /**
41
+ * What the next save does about extraction: `Auto` (the default) extracts when the query is new
42
+ * or its SQL changed; `Skip` does not extract, so the save-time rules are not applied and the
43
+ * child rows, the Extraction columns and `UsesTemplate` stay as they are (for bulk loaders and
44
+ * fixtures that write those themselves); `Force` extracts even when the SQL did not change.
45
+ * Reset to `Auto` after each save.
46
+ */
47
+ this.ExtractionOverride = 'Auto';
48
+ /** The SQL a re-extraction reads instead of the cached platform variant (set by {@link RerunExtraction}). */
49
+ this.platformSQLOverride = null;
50
+ /** The facts an explicit refresh already established, which its save writes instead of extracting again. */
51
+ this.presetExtraction = null;
52
+ /** Whether the running save is an explicit refresh ({@link ApplyRefreshAsync}). */
53
+ this.refreshing = false;
54
+ /** What the last save's child-row writes changed; `null` when it wrote none. */
55
+ this.lastWriteSummary = null;
56
+ }
22
57
  // ─── Embedding Methods ───────────────────────────────────────────────────────
23
58
  /**
24
59
  * Simple proxy to local helper method for embeddings. Needed for BaseEntity sub-classes that want to use embeddings built into BaseEntity
@@ -26,6 +61,10 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
26
61
  async EmbedTextLocal(textToEmbed) {
27
62
  return EmbedTextLocalHelper(this, textToEmbed);
28
63
  }
64
+ /** A stored vector from a model other than the default local one is re-embedded on save. */
65
+ async IsEmbeddingStale(embeddingModelID) {
66
+ return IsLocalEmbeddingStaleHelper(this, embeddingModelID);
67
+ }
29
68
  /**
30
69
  * Generates an embedding from composite text (Name + UserQuestion + Description) for richer semantic search.
31
70
  * Stores the vector in EmbeddingVector (JSON) and EmbeddingVectorBinary (float32 bytes, base64), and the
@@ -35,8 +74,17 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
35
74
  * embedder error — all three columns are cleared, so a query whose text changed never keeps a vector
36
75
  * computed from its old text. An embedder error is logged and does not fail the save; the query simply
37
76
  * has no semantic match until its next successful embedding.
77
+ *
78
+ * Does nothing when {@link SkipEmbeddings} is set (a bulk load computes them later), so the stored
79
+ * vector is left as it is.
80
+ *
81
+ * @param textChanged - false when the text is unchanged and only the stored vector's model is stale:
82
+ * a failed embed then keeps the existing vector, which still matches the text.
38
83
  */
39
- async GenerateCompositeEmbedding() {
84
+ async GenerateCompositeEmbedding(textChanged = true) {
85
+ if (this.SkipEmbeddings) {
86
+ return;
87
+ }
40
88
  const parts = [
41
89
  this.Name || '',
42
90
  this.UserQuestion || '',
@@ -59,7 +107,9 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
59
107
  catch (error) {
60
108
  LogError(`[MJQueryEntityServer] Embedding refresh failed for query "${this.Name}": ${error instanceof Error ? error.message : String(error)}`);
61
109
  }
62
- this.clearEmbedding();
110
+ if (textChanged) {
111
+ this.clearEmbedding();
112
+ }
63
113
  }
64
114
  /** Clears the stored vector (both forms) and the model it was computed with. */
65
115
  clearEmbedding() {
@@ -67,8 +117,22 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
67
117
  this.EmbeddingVectorBinary = null;
68
118
  this.EmbeddingModelID = null;
69
119
  }
120
+ /** Re-embeds when the embedded text changed, or when the stored vector came from a model other than the default local one. */
121
+ async refreshEmbeddingAsync() {
122
+ if (this.SkipEmbeddings)
123
+ return;
124
+ const textChanged = !this.IsSaved || ['Name', 'Description', 'UserQuestion'].some(f => this.GetFieldByName(f).Dirty);
125
+ if (textChanged || (this.EmbeddingVector != null && await this.IsEmbeddingStale(this.EmbeddingModelID))) {
126
+ await this.GenerateCompositeEmbedding(textChanged);
127
+ }
128
+ }
70
129
  // ─── Save / Delete Overrides ─────────────────────────────────────────────────
71
130
  async Save(options) {
131
+ // Over a client provider (a process that loads this package but saves over GraphQL), the server
132
+ // the save reaches applies the approval rules and extracts; doing it here too would write the
133
+ // child rows twice, the second time from the client.
134
+ if (this.savesOverNetwork)
135
+ return super.Save(options);
72
136
  let approving;
73
137
  try {
74
138
  // Approving is a server act; a ReplayOnly save writes nothing, so it has nothing to stamp.
@@ -79,47 +143,185 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
79
143
  return false;
80
144
  }
81
145
  try {
82
- const sqlField = this.GetFieldByName('SQL');
83
- const nameField = this.GetFieldByName('Name');
84
- const descriptionField = this.GetFieldByName('Description');
85
- const userQuestionField = this.GetFieldByName('UserQuestion');
86
- const shouldExtractData = !this.IsSaved || sqlField.Dirty;
87
- const shouldGenerateEmbedding = !this.IsSaved || nameField.Dirty || descriptionField.Dirty || userQuestionField.Dirty;
88
- // Generate embedding from composite text for better semantic search
89
- if (shouldGenerateEmbedding) {
90
- await this.GenerateCompositeEmbedding();
91
- }
92
- else if (!this.Description || this.Description.trim().length === 0) {
93
- this.EmbeddingVector = null;
94
- this.EmbeddingVectorBinary = null;
95
- this.EmbeddingModelID = null;
96
- }
97
- // Save the query first without AI processing
98
- const saveResult = await super.Save(options);
99
- if (!saveResult) {
100
- return false;
101
- }
102
- // Extract and sync parameters AFTER saving, outside of any transaction
103
- if (shouldExtractData && this.SQL && this.SQL.trim().length > 0) {
104
- await this.extractAndSyncDataAsync(options?.SkipEntityAIActions === true);
105
- await this.autoConvertDialectsAsync();
106
- }
107
- else if (!this.SQL || this.SQL.trim().length === 0) {
108
- this.UsesTemplate = false;
109
- await this.cleanupEmptyQueryAsync();
110
- }
111
- // The parameter and dialect rows are rewritten after the Query row, so the approved
112
- // fingerprint is taken last, from the definition as stored.
113
- if (approving) {
114
- await this.stampApprovalFingerprintAsync(options);
115
- }
116
- return true;
146
+ return await this.saveWithExtractionAsync(options, approving);
117
147
  }
118
148
  catch (e) {
119
149
  LogError('Failed to save query:', e);
120
150
  this.LatestResult?.Errors.push(e);
121
151
  return false;
122
152
  }
153
+ finally {
154
+ this.ExtractionOverride = 'Auto';
155
+ this.platformSQLOverride = null;
156
+ this.presetExtraction = null;
157
+ this.refreshing = false;
158
+ }
159
+ }
160
+ /** Whether this object saves through a network (client) provider rather than to a database. */
161
+ get savesOverNetwork() {
162
+ const provider = this.ProviderToUse;
163
+ return provider instanceof ProviderBase && provider.ProviderType === ProviderType.Network;
164
+ }
165
+ /**
166
+ * The save: facts first (a refusal writes nothing), then the query row (a graph save with any
167
+ * declared children), then the child rows extraction derives, then the approval fingerprint.
168
+ */
169
+ async saveWithExtractionAsync(options, approving) {
170
+ const declared = await this.PrepareDeclaredChildrenAsync(options);
171
+ const plan = this.planExtraction(options, HasDeclaredQueryChildren(declared));
172
+ const extraction = plan.Run ? await this.computeExtractionAsync(plan.Mode) : null;
173
+ if (extraction?.Refusal) {
174
+ this.RegisterResultHistoryEntry(this.refusalResult(extraction.Refusal));
175
+ return false;
176
+ }
177
+ this.applyRunColumns(extraction);
178
+ if (extraction)
179
+ this.applyFactsToDeclaredFields(extraction);
180
+ await this.refreshEmbeddingAsync();
181
+ const postCommit = this.capturePostCommitToken();
182
+ this.lastWriteSummary = null;
183
+ if (!await super.Save(options)) {
184
+ return false;
185
+ }
186
+ if (extraction) {
187
+ await this.writeExtractionAsync(extraction, options, postCommit);
188
+ // A refresh brings the derived rows in line with the SQL; it never writes SQL, a dialect variant's included.
189
+ if (!this.refreshing)
190
+ await this.autoConvertDialectsAsync();
191
+ }
192
+ else if (!this.hasSQL) {
193
+ await this.cleanupEmptyQueryAsync();
194
+ }
195
+ // The child and dialect rows are rewritten after the Query row, so the approved fingerprint is
196
+ // taken last, from the definition as stored.
197
+ if (approving) {
198
+ await this.stampApprovalFingerprintAsync(options);
199
+ }
200
+ return true;
201
+ }
202
+ get hasSQL() {
203
+ return !!this.SQL && this.SQL.trim().length > 0;
204
+ }
205
+ /**
206
+ * Whether this save extracts, and under which rules: a new or edited query extracts under the
207
+ * save-time rules; a save that writes declared child rows, or a `Force` override, re-extracts an
208
+ * unchanged query under the refresh rules, so the facts of rows it writes are corrected again.
209
+ */
210
+ planExtraction(options, writesDeclaredChildren) {
211
+ const edited = !this.IsSaved || this.GetFieldByName('SQL').Dirty;
212
+ if (options?.ReplayOnly || !this.hasSQL || this.ExtractionOverride === 'Skip') {
213
+ return { Run: false, Mode: 'Save' };
214
+ }
215
+ return { Run: edited || writesDeclaredChildren || this.ExtractionOverride === 'Force', Mode: edited ? 'Save' : 'Refresh' };
216
+ }
217
+ /**
218
+ * Sets the columns a save records about the SQL before the row is written. `UsesTemplate` is a
219
+ * lexical fact and follows every SQL change; it is never changed by a failed extraction. A save
220
+ * with {@link ExtractionOverride} `Skip` leaves it as the caller set it, like the other
221
+ * extraction results.
222
+ */
223
+ applyRunColumns(extraction) {
224
+ if (extraction) {
225
+ this.UsesTemplate = extraction.UsesTemplate;
226
+ this.ExtractedSQLHash = extraction.Hash;
227
+ this.ExtractionStatus = extraction.Status;
228
+ this.ExtractionMessage = extraction.Messages.length > 0 ? extraction.Messages.join('\n') : null;
229
+ }
230
+ else if (!this.hasSQL) {
231
+ this.UsesTemplate = false;
232
+ }
233
+ else if (this.ExtractionOverride !== 'Skip' && (!this.IsSaved || this.GetFieldByName('SQL').Dirty)) {
234
+ this.UsesTemplate = this.lexicalUsesTemplate();
235
+ }
236
+ }
237
+ /**
238
+ * Gives each field row this save declares the facts extraction established for it, before the
239
+ * graph is written. Facts follow the SQL and the database server whatever a caller declares, so
240
+ * a declaration that carries facts (types, nullability, sequence, source) for another platform is
241
+ * corrected before it is written instead of being written and then corrected, and a new declared
242
+ * row, which may declare only its name and annotations, gets the facts its columns require.
243
+ */
244
+ applyFactsToDeclaredFields(extraction) {
245
+ if (!extraction.Fields)
246
+ return;
247
+ const facts = new Map(extraction.Fields.map(f => [f.Name.toLowerCase(), f]));
248
+ for (const row of this.DeclaredFields?.Items ?? []) {
249
+ const fact = facts.get((row.Name ?? '').toLowerCase());
250
+ if (fact)
251
+ ApplyFieldFacts(row, fact);
252
+ }
253
+ }
254
+ /** Whether the SQL holds a template tag outside comments, read by the lexer. */
255
+ lexicalUsesTemplate() {
256
+ return SQLUsesTemplate(this.SQL ?? '', this.extractionPlatform);
257
+ }
258
+ /** The save's result entry for a refused save, carrying the reasons. */
259
+ refusalResult(reason) {
260
+ const result = new BaseEntityResult();
261
+ result.Success = false;
262
+ result.Type = this.IsSaved ? 'update' : 'create';
263
+ result.Message = reason;
264
+ result.StartedAt = new Date();
265
+ result.EndedAt = new Date();
266
+ return result;
267
+ }
268
+ /**
269
+ * The declared-rows-win hook, run before the save writes anything.
270
+ *
271
+ * A caller can save a query together with child rows it declares, through the `Parameters`,
272
+ * `Fields` and `Entities` collections. When a NEW declared row has the key of a stored row with a
273
+ * different ID whose `DetectionMethod` is not `Manual` (a row extraction created), the stored row
274
+ * is deleted inside the same graph save, before the declared row is inserted. The declared row
275
+ * keeps the ID the caller gave it, which is what makes a metadata file that names its child IDs
276
+ * converge instead of colliding with rows extraction wrote earlier. A stored `Manual` row is never
277
+ * deleted here; a declared row that collides with one fails the save on the database's own
278
+ * uniqueness rule where there is one.
279
+ *
280
+ * Extraction then runs after `super.Save()`, sees the declared rows already written, and treats
281
+ * them like any stored row: their facts are corrected from the SQL, their authored annotations
282
+ * kept.
283
+ *
284
+ * @param options - The save's options. A `ReplayOnly` save, or one that skips related
285
+ * collections, writes no declared rows, so nothing is declared.
286
+ * @returns The keys of the child rows this save declares.
287
+ */
288
+ async PrepareDeclaredChildrenAsync(options) {
289
+ if (options?.ReplayOnly || options?.SkipRelatedCollections) {
290
+ return NO_DECLARED_QUERY_CHILDREN;
291
+ }
292
+ const collections = { Parameters: this.DeclaredParameters, Fields: this.DeclaredFields, Entities: this.DeclaredEntities };
293
+ const declared = CollectDeclaredQueryChildren(collections);
294
+ if (this.IsSaved && HasDeclaredQueryChildren(declared)) {
295
+ await this.queueReplacedRowDeletionsAsync();
296
+ }
297
+ return declared;
298
+ }
299
+ /**
300
+ * Reads the stored child rows from the database (never a cache, which inside a transaction can
301
+ * miss rows written earlier in it) and queues the extraction-created rows each new declared row
302
+ * replaces for deletion in the graph save.
303
+ */
304
+ async queueReplacedRowDeletionsAsync() {
305
+ const filter = `QueryID='${EscapeSQLString(this.ID)}'`;
306
+ const [parameters, fields, entities] = await this.RunViewProviderToUse.RunViews([
307
+ { EntityName: 'MJ: Query Parameters', ExtraFilter: filter, ResultType: 'entity_object', BypassCache: true, IgnoreMaxRows: true },
308
+ { EntityName: 'MJ: Query Fields', ExtraFilter: filter, ResultType: 'entity_object', BypassCache: true, IgnoreMaxRows: true },
309
+ { EntityName: 'MJ: Query Entities', ExtraFilter: filter, ResultType: 'entity_object', BypassCache: true, IgnoreMaxRows: true },
310
+ ], this.ContextCurrentUser);
311
+ if (!parameters?.Success || !fields?.Success || !entities?.Success) {
312
+ throw new Error(`could not read the query's stored child rows: ${parameters?.ErrorMessage || fields?.ErrorMessage || entities?.ErrorMessage}`);
313
+ }
314
+ const newItems = (items) => items.filter(i => !i.IsSaved);
315
+ const byName = (row) => QueryChildNameKey(row.Name);
316
+ for (const row of FindReplacedRows(newItems(this.DeclaredParameters.Items), parameters.Results, byName)) {
317
+ QueueReplacedRowDeletion(this.DeclaredParameters, row);
318
+ }
319
+ for (const row of FindReplacedRows(newItems(this.DeclaredFields.Items), fields.Results, byName)) {
320
+ QueueReplacedRowDeletion(this.DeclaredFields, row);
321
+ }
322
+ for (const row of FindReplacedRows(newItems(this.DeclaredEntities.Items), entities.Results, QueryEntityKey)) {
323
+ QueueReplacedRowDeletion(this.DeclaredEntities, row);
324
+ }
123
325
  }
124
326
  /**
125
327
  * Stores the fingerprint of the definition just approved: this row's SQL, template flag and
@@ -151,11 +353,7 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
151
353
  }
152
354
  async Delete(options) {
153
355
  try {
154
- const deleteResult = await super.Delete(options);
155
- if (!deleteResult) {
156
- return false;
157
- }
158
- return deleteResult;
356
+ return await super.Delete(options);
159
357
  }
160
358
  catch (e) {
161
359
  LogError('Failed to delete query:', e);
@@ -163,131 +361,251 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
163
361
  return false;
164
362
  }
165
363
  }
166
- // ─── Pipeline Delegation ─────────────────────────────────────────────────────
364
+ // ─── Extraction ──────────────────────────────────────────────────────────────
167
365
  /**
168
- * Returns the database platform of the connected database.
169
- * Uses the DB_PLATFORM env var (same source as MJServer's provider selection).
366
+ * The platform the query is extracted for: the platform of the provider this entity saves
367
+ * through, so two providers on different platforms in one process each extract for their own.
170
368
  */
171
- get currentPlatform() {
172
- return resolveDbPlatformFromEnv() ?? 'sqlserver';
369
+ get extractionPlatform() {
370
+ const provider = this.ProviderToUse;
371
+ return provider instanceof DatabaseProviderBase ? provider.PlatformKey : 'sqlserver';
372
+ }
373
+ /** The provider as extraction reads and writes through it; `null` when it cannot read views and create entity objects. */
374
+ get extractionProvider() {
375
+ const provider = this.ProviderToUse;
376
+ return IsExtractionProvider(provider) ? provider : null;
173
377
  }
174
378
  /**
175
- * Resolves the SQL to use for extraction based on the current database platform.
176
- *
177
- * Resolution order:
178
- * 1. MJ: Query SQLs record matching the current platform's dialect → use that SQL
179
- * 2. Base Query.SQL field → fall back if no dialect variant exists
180
- *
181
- * After resolution, validates the SQL is compatible with the current platform's
182
- * dialect by attempting a parse. Logs a warning if the SQL appears to be for a
183
- * different dialect (e.g., T-SQL bracket identifiers on a PostgreSQL database).
379
+ * The user derived child rows are read and written as: the system user, once the caller's save
380
+ * of the query is authorized (the rows mirror SQL the caller was allowed to change, like CodeGen
381
+ * output). The caller when no system user is loaded.
184
382
  */
185
- resolveExtractionSQL() {
186
- const platform = this.currentPlatform;
187
- // 1. Check for a dialect-specific QuerySQL record
188
- const platformSQL = this.GetPlatformSQL(platform);
189
- // If GetPlatformSQL returned something different from the base SQL, use it
190
- const resolvedSQL = platformSQL || this.SQL;
191
- if (!resolvedSQL || resolvedSQL.trim().length === 0) {
192
- return '';
193
- }
194
- // Validate dialect compatibility
195
- this.validateDialectCompatibility(resolvedSQL, platform);
196
- return resolvedSQL;
383
+ get extractionUser() {
384
+ if (this.refreshing)
385
+ return this.ContextCurrentUser;
386
+ return UserCache.Instance?.GetSystemUser?.() ?? this.ContextCurrentUser;
387
+ }
388
+ /** The SQL extraction reads: a re-extraction's override, else the query's SQL for the platform. */
389
+ get extractionSQL() {
390
+ return this.platformSQLOverride ?? (this.GetPlatformSQL(this.extractionPlatform) || this.SQL || '');
391
+ }
392
+ /** Establishes the query's facts (nothing is written); an explicit refresh's facts when it already has them. */
393
+ async computeExtractionAsync(mode) {
394
+ if (this.presetExtraction)
395
+ return this.presetExtraction;
396
+ const provider = this.ProviderToUse;
397
+ await this.ensureQueryEngineLoadedAsync();
398
+ const targets = await this.readCompositionTargetsAsync();
399
+ return ComputeQueryExtraction({
400
+ QueryID: this.IsSaved ? this.ID : null,
401
+ QueryName: this.Name,
402
+ QueryPath: CompositionFullPath({ Name: this.Name, CategoryPath: this.CategoryID ? this.BuildCategoryPath(this.CategoryID) : '' }),
403
+ SQL: this.extractionSQL,
404
+ Platform: this.extractionPlatform,
405
+ Mode: mode,
406
+ IsExternal: !!this.ExternalDataSourceID,
407
+ KnownParameters: await this.knownParametersAsync(),
408
+ ParameterHints: this.ParameterHints,
409
+ ContextUser: this.ContextCurrentUser,
410
+ Entities: provider instanceof DatabaseProviderBase ? provider.Entities : [],
411
+ Queries: MergeCompositionCandidates(targets.Candidates, QueryEngine.Instance.Queries),
412
+ CompositionDependencies: targets.InlineDependencies,
413
+ Describe: provider instanceof GenericDatabaseProvider ? (sql) => provider.DescribeStatement(sql, this.ContextCurrentUser) : null,
414
+ });
197
415
  }
198
416
  /**
199
- * Best-effort dialect validation: attempts to parse the SQL with the current platform's
200
- * dialect. If parsing throws, logs a warning — the SQL may be for a different dialect.
201
- *
202
- * This is a heuristic, not a guarantee: node-sql-parser is permissive on many
203
- * dialect-specific functions (ISNULL, GETDATE pass in both dialects), so a passing
204
- * parse does not prove the SQL is correct for this dialect. But a failing parse is a
205
- * strong signal of incompatibility.
417
+ * The queries the SQL composes, read on this save's provider with every cache bypassed, so a
418
+ * target saved earlier in the same transaction (one metadata push creating both queries) is
419
+ * found before the shared query cache hears of it. Nothing is read for SQL that composes
420
+ * nothing, or on a provider that cannot run views.
206
421
  */
207
- validateDialectCompatibility(sql, platform) {
422
+ async readCompositionTargetsAsync() {
423
+ const provider = this.ProviderToUse;
424
+ if (!(provider instanceof DatabaseProviderBase))
425
+ return { Candidates: [], InlineDependencies: [] };
426
+ const engine = QueryEngine.Instance;
427
+ const cachedIDs = new Set(engine.Queries.map(q => q.ID.toLowerCase()));
428
+ return ReadCompositionTargetsAsync(this.extractionSQL, this.extractionPlatform, provider, this.extractionUser, cachedIDs, engine.SQLDialects);
429
+ }
430
+ /** Loads QueryEngine when this process has not yet (a CLI or sync context); compositions resolve against it. */
431
+ async ensureQueryEngineLoadedAsync() {
432
+ if (QueryEngine.Instance.Loaded)
433
+ return;
434
+ try {
435
+ await QueryEngine.Instance.Config(false, this.ContextCurrentUser);
436
+ }
437
+ catch (e) {
438
+ LogError(`[MJQueryEntityServer] QueryEngine could not be loaded for extraction of "${this.Name}": ${e instanceof Error ? e.message : String(e)}`);
439
+ }
440
+ }
441
+ /**
442
+ * The parameter rows extraction knows before it runs: the stored rows, read from the database
443
+ * (never the cache), overlaid by the rows this save declares.
444
+ */
445
+ async knownParametersAsync() {
446
+ const byName = new Map();
447
+ for (const row of await this.storedParametersAsync())
448
+ byName.set(row.Name.toLowerCase(), row);
449
+ for (const row of this.DeclaredParameters?.Items ?? [])
450
+ byName.set(row.Name.toLowerCase(), row);
451
+ return [...byName.values()].map(row => ({
452
+ Name: row.Name, Type: row.Type, IsRequired: row.IsRequired === true, IsServerSet: row.IsServerSet === true,
453
+ SampleValue: row.SampleValue, DefaultValue: row.DefaultValue, ValidationFilters: row.ValidationFilters,
454
+ IsAuthored: row.DetectionMethod === 'Manual',
455
+ }));
456
+ }
457
+ async storedParametersAsync() {
458
+ const provider = this.extractionProvider;
459
+ if (!this.IsSaved || !provider)
460
+ return [];
461
+ const result = await provider.RunView({
462
+ EntityName: 'MJ: Query Parameters', ExtraFilter: `QueryID='${EscapeSQLString(this.ID)}'`, ResultType: 'entity_object', BypassCache: true,
463
+ }, this.extractionUser);
464
+ if (!result.Success)
465
+ throw new Error(`could not read the query's stored parameters: ${result.ErrorMessage}`);
466
+ return result.Results ?? [];
467
+ }
468
+ /**
469
+ * Writes the derived child rows as the system user, inside the caller's transaction when there
470
+ * is one, then queues the description pass. A failed write is recorded on the query
471
+ * (`ExtractionStatus` `Error`) rather than failing a save whose row is already written.
472
+ */
473
+ async writeExtractionAsync(extraction, options, postCommit) {
474
+ const provider = this.extractionProvider;
208
475
  try {
209
- const dialect = GetDialect(platform);
210
- // Attempt to parse — will throw on strong dialect markers
211
- // (e.g., [bracket] identifiers fail on PG, :: casts fail on T-SQL)
212
- SQLParser.ExtractSelectColumns(sql, dialect);
476
+ if (!provider)
477
+ throw new Error('the query is not saved through a database provider');
478
+ const summary = await WriteExtractedChildRows({ QueryID: this.ID, Provider: provider, User: this.extractionUser, EntityName: id => this.entityNameOf(id) }, extraction);
479
+ this.lastWriteSummary = summary;
480
+ this.queueDescriptionPass(summary, options, postCommit);
481
+ }
482
+ catch (e) {
483
+ // An explicit refresh runs in its own transaction, which a failed write rolls back.
484
+ if (this.refreshing)
485
+ throw e;
486
+ await this.recordWriteFailureAsync(e);
213
487
  }
214
- catch {
215
- const otherDialect = platform === 'postgresql' ? 'T-SQL (SQL Server)' : 'PostgreSQL';
216
- console.warn(`[MJQueryEntityServer] Query "${this.Name}" — SQL may be incompatible with the ` +
217
- `current database platform "${platform}". The SQL appears to use ${otherDialect} syntax. ` +
218
- `Consider adding a dialect-specific variant via MJ: Query SQLs.`);
488
+ }
489
+ /** An entity's name from the saving provider's catalog, for reporting; the ID when it is not there. */
490
+ entityNameOf(entityID) {
491
+ const provider = this.ProviderToUse;
492
+ const entities = provider instanceof DatabaseProviderBase ? provider.Entities : [];
493
+ return entities.find(e => UUIDsEqual(e.ID, entityID))?.Name ?? entityID;
494
+ }
495
+ async recordWriteFailureAsync(error) {
496
+ const reason = `The extracted child rows could not be written: ${error instanceof Error ? error.message : String(error)}`;
497
+ LogError(`[MJQueryEntityServer] Query "${this.Name}": ${reason}`);
498
+ this.ExtractionStatus = 'Error';
499
+ this.ExtractionMessage = this.ExtractionMessage ? `${this.ExtractionMessage}\n${reason}` : reason;
500
+ if (!await super.Save()) {
501
+ LogError(`[MJQueryEntityServer] Query "${this.Name}": the extraction failure could not be recorded: ${this.LatestResult?.CompleteMessage}`);
219
502
  }
220
503
  }
504
+ /** The post-commit token of the caller's transaction, captured before the save writes anything. */
505
+ capturePostCommitToken() {
506
+ const provider = this.ProviderToUse;
507
+ return provider instanceof DatabaseProviderBase ? provider.CapturePostCommitToken() : undefined;
508
+ }
221
509
  /**
222
- * Builds the QuerySyncContext from this entity instance for use by the extraction pipeline.
223
- * Resolves the SQL to the appropriate dialect variant for the connected database.
510
+ * Queues the LLM description pass to run after the caller's transaction commits, when some row
511
+ * has no description and the save does not skip AI actions. No LLM call is made inside the
512
+ * transaction. A save-triggered extraction that changed no child row does not queue it: the
513
+ * rows are as the last run left them, so a repeated save (a re-push of unchanged metadata) does
514
+ * not call the LLM again, and a pass that failed is not retried, and recorded, on every save.
515
+ * An explicit refresh that asks for the pass always queues it.
224
516
  */
225
- buildSyncContext(skipAIEnrichment = false) {
226
- return {
227
- queryID: this.ID,
228
- queryName: this.Name,
229
- sql: this.resolveExtractionSQL(),
230
- isSaved: this.IsSaved,
231
- contextUser: this.ContextCurrentUser,
232
- metadataProvider: this.ProviderToUse,
233
- runViewProvider: this.RunViewProviderToUse,
234
- platform: this.currentPlatform,
235
- parameterHints: this.ParameterHints,
236
- SkipAIEnrichment: skipAIEnrichment,
237
- };
517
+ queueDescriptionPass(summary, options, postCommit) {
518
+ const provider = this.ProviderToUse;
519
+ const undescribed = summary.FieldsWithoutDescription.length + summary.ParametersWithoutDescription.length;
520
+ if (options?.SkipEntityAIActions || undescribed === 0 || !(provider instanceof DatabaseProviderBase))
521
+ return;
522
+ if (!this.refreshing && summary.Changes.length === 0)
523
+ return;
524
+ const request = { QueryID: this.ID, QueryName: this.Name, SQL: this.extractionSQL, Provider: provider, User: this.extractionUser };
525
+ provider.RunAfterCommit(() => RunDescriptionPass(request), `query description pass for "${this.Name}"`, postCommit);
238
526
  }
239
527
  /**
240
- * Re-runs the extraction pipeline on this query. Called by MJQuerySQLEntityServer
241
- * when a dialect variant matching the current platform is saved — this handles
242
- * the timing issue where the parent Query is saved before its QuerySQL children
243
- * exist in the cache.
528
+ * Re-extracts this query: a save with {@link ExtractionOverride} `Force`. Called by
529
+ * `MJQuerySQLEntityServer` when a dialect variant for the current platform is saved.
244
530
  *
245
- * @param skipAIEnrichment Skip the LLM enrichment stage, as `SkipEntityAIActions` does for Save.
531
+ * @param skipAIEnrichment Skip the description pass, as `SkipEntityAIActions` does for a save.
532
+ * @param platformSQL The SQL to extract from (the variant just saved), instead of the cached
533
+ * platform variant, which inside a transaction may not hold it yet.
246
534
  */
247
- async RerunExtraction(skipAIEnrichment = false) {
248
- await this.extractAndSyncDataAsync(skipAIEnrichment);
535
+ async RerunExtraction(skipAIEnrichment = false, platformSQL) {
536
+ this.ExtractionOverride = 'Force';
537
+ this.platformSQLOverride = platformSQL ?? null;
538
+ if (!await this.Save({ SkipEntityAIActions: skipAIEnrichment })) {
539
+ LogError(`[MJQueryEntityServer] Re-extraction of query "${this.Name}" failed: ${this.LatestResult?.CompleteMessage}`);
540
+ }
249
541
  }
542
+ // ─── Explicit refresh ────────────────────────────────────────────────────────
250
543
  /**
251
- * Runs the extraction pipeline and saves the UsesTemplate flag.
252
- * On error, sets UsesTemplate=false and attempts to save so the query remains usable.
544
+ * Establishes this saved query's facts under the refresh rules (a save-time rule error is
545
+ * recorded, never a refusal), as the save would, and writes nothing. The facts are read on the
546
+ * provider this entity was loaded with, for its platform.
547
+ */
548
+ async ComputeRefreshExtractionAsync() {
549
+ return this.computeExtractionAsync('Refresh');
550
+ }
551
+ /**
552
+ * The child-row changes `extraction` would make, worked out against the rows stored now and
553
+ * read as this entity's context user. Nothing is written.
554
+ */
555
+ async PreviewRefreshAsync(extraction, entityName) {
556
+ const provider = this.extractionProvider;
557
+ if (!provider)
558
+ throw new Error('the query is not loaded through a database provider');
559
+ return WriteExtractedChildRows({ QueryID: this.ID, Provider: provider, User: this.ContextCurrentUser, DryRun: true, EntityName: entityName ?? (id => this.entityNameOf(id)) }, extraction);
560
+ }
561
+ /**
562
+ * Writes `extraction` (from {@link ComputeRefreshExtractionAsync}): a save with
563
+ * {@link ExtractionOverride} `Force` that records the run on the query and brings the child
564
+ * rows in line with the facts. Unlike a save-triggered extraction, the child rows are written
565
+ * as this entity's context user, so each write is checked against that user's permissions, and
566
+ * a failed write fails the refresh instead of being recorded on the query. SQL, dialect
567
+ * variants and the embedding are never written.
253
568
  *
254
- * @param skipAIEnrichment Skip the LLM enrichment stage (the save asked to skip AI actions).
569
+ * @param withAI Queue the LLM description pass for rows still without a description.
570
+ * @returns What the child-row writes changed.
571
+ * @throws Error when the save or a child-row write fails; the caller's transaction should be rolled back.
255
572
  */
256
- async extractAndSyncDataAsync(skipAIEnrichment) {
573
+ async ApplyRefreshAsync(extraction, withAI) {
574
+ this.ExtractionOverride = 'Force';
575
+ this.presetExtraction = extraction;
576
+ this.refreshing = true;
577
+ // A refresh changes the derived rows, not the text the embedding is computed from.
578
+ const skipEmbeddings = this.SkipEmbeddings;
579
+ this.SkipEmbeddings = true;
257
580
  try {
258
- const ctx = this.buildSyncContext(skipAIEnrichment);
259
- const pipelineResult = await RunExtractionPipeline(ctx);
260
- this.UsesTemplate = pipelineResult.UsesTemplate;
261
- // Save the query again to persist UsesTemplate and any changes from AI processing
262
- const updateResult = await super.Save();
263
- if (!updateResult) {
264
- LogError('Failed to save query after AI processing completed');
581
+ if (!await this.Save({ SkipEntityAIActions: !withAI })) {
582
+ throw new Error(this.LatestResult?.CompleteMessage || `the refresh of query "${this.Name}" could not be saved`);
265
583
  }
266
584
  }
267
- catch (e) {
268
- LogError('Error in async AI processing:', e);
269
- this.UsesTemplate = false;
270
- try {
271
- await super.Save();
272
- }
273
- catch (saveError) {
274
- LogError('Failed to save query after AI processing error:', saveError);
275
- }
585
+ finally {
586
+ this.SkipEmbeddings = skipEmbeddings;
276
587
  }
588
+ return this.lastWriteSummary;
277
589
  }
278
590
  /**
279
- * Cleans up all extraction data when SQL is empty, then saves the updated UsesTemplate flag.
591
+ * Cleans up all extraction data when SQL is empty.
280
592
  */
281
593
  async cleanupEmptyQueryAsync() {
594
+ const provider = this.extractionProvider;
595
+ if (!provider)
596
+ return;
282
597
  try {
283
- const ctx = this.buildSyncContext();
598
+ const ctx = {
599
+ queryID: this.ID,
600
+ queryName: this.Name,
601
+ sql: '',
602
+ isSaved: this.IsSaved,
603
+ contextUser: this.extractionUser,
604
+ metadataProvider: provider,
605
+ runViewProvider: provider,
606
+ platform: this.extractionPlatform,
607
+ };
284
608
  await CleanupQueryData(ctx);
285
- // Also remove dialect records (not managed by the pipeline)
286
- await this.removeAllQuerySQLRecordsAsync();
287
- const updateResult = await super.Save();
288
- if (!updateResult) {
289
- LogError('Failed to save query after cleanup');
290
- }
291
609
  }
292
610
  catch (e) {
293
611
  LogError('Error in async cleanup:', e);
@@ -352,7 +670,9 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
352
670
  * Creates or updates a QuerySQL record for the given dialect.
353
671
  */
354
672
  async upsertQuerySQLRecord(targetDialect, convertedSQL) {
355
- const md = this.ProviderToUse;
673
+ const md = this.extractionProvider;
674
+ if (!md)
675
+ return;
356
676
  // Look up existing record from QueryEngine cache instead of a RunView call
357
677
  const existing = QueryEngine.Instance.QuerySQLs.find(qs => UUIDsEqual(qs.QueryID, this.ID) && UUIDsEqual(qs.SQLDialectID, targetDialect.ID));
358
678
  let record;
@@ -370,23 +690,6 @@ let MJQueryEntityServer = class MJQueryEntityServer extends MJQueryEntityExtende
370
690
  console.warn(`Query "${this.Name}" - Failed to save QuerySQL record for dialect "${targetDialect.Name}"`);
371
691
  }
372
692
  }
373
- /**
374
- * Removes all QuerySQL records for this query. Called during cleanup operations.
375
- */
376
- async removeAllQuerySQLRecordsAsync() {
377
- try {
378
- if (!this.IsSaved)
379
- return;
380
- const records = QueryEngine.Instance.QuerySQLs.filter(qs => UUIDsEqual(qs.QueryID, this.ID));
381
- const deletePromises = records.map(r => r.Delete());
382
- if (deletePromises.length > 0) {
383
- await Promise.all(deletePromises);
384
- }
385
- }
386
- catch (e) {
387
- console.warn(`Query "${this.Name}" - Failed to remove QuerySQL records:`, e);
388
- }
389
- }
390
693
  };
391
694
  MJQueryEntityServer = __decorate([
392
695
  RegisterClass(BaseEntity, 'MJ: Queries')