@finos/legend-lego 2.0.217 → 2.0.218

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 (50) hide show
  1. package/lib/index.css +2 -2
  2. package/lib/index.css.map +1 -1
  3. package/lib/legend-ai/LegendAIDocEnrichment.d.ts +14 -0
  4. package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
  5. package/lib/legend-ai/LegendAIDocEnrichment.js +140 -17
  6. package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
  7. package/lib/legend-ai/LegendAITypes.d.ts +30 -1
  8. package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
  9. package/lib/legend-ai/LegendAITypes.js +31 -0
  10. package/lib/legend-ai/LegendAITypes.js.map +1 -1
  11. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +2 -2
  12. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
  13. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +1 -1
  14. package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
  15. package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
  16. package/lib/legend-ai/components/LegendAIChat.js +62 -38
  17. package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
  18. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +5 -0
  19. package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
  20. package/lib/legend-ai/components/LegendAIChatHelpers.js +8 -1
  21. package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
  22. package/lib/legend-ai/index.d.ts +3 -2
  23. package/lib/legend-ai/index.d.ts.map +1 -1
  24. package/lib/legend-ai/index.js +3 -2
  25. package/lib/legend-ai/index.js.map +1 -1
  26. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +27 -2
  27. package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
  28. package/lib/legend-ai/stores/LegendAIChatProcessors.js +329 -86
  29. package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
  30. package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +11 -0
  31. package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -1
  32. package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +53 -19
  33. package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -1
  34. package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +4 -1
  35. package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -1
  36. package/lib/legend-ai/stores/LegendAISqlHelpers.js +25 -2
  37. package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -1
  38. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +4 -4
  39. package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -1
  40. package/package.json +3 -3
  41. package/src/legend-ai/LegendAIDocEnrichment.ts +189 -23
  42. package/src/legend-ai/LegendAITypes.ts +54 -1
  43. package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +2 -0
  44. package/src/legend-ai/components/LegendAIChat.tsx +114 -78
  45. package/src/legend-ai/components/LegendAIChatHelpers.ts +9 -3
  46. package/src/legend-ai/index.ts +7 -0
  47. package/src/legend-ai/stores/LegendAIChatProcessors.ts +476 -105
  48. package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +82 -19
  49. package/src/legend-ai/stores/LegendAISqlHelpers.ts +32 -2
  50. package/src/legend-ai/stores/LegendAISqlJoinSanitizers.ts +4 -4
@@ -19,7 +19,7 @@ import {
19
19
  TDSServiceSourceType,
20
20
  } from '../LegendAITypes.js';
21
21
  import {
22
- AP_CALL_PATTERN,
22
+ accessPointCalls,
23
23
  buildColumnByNameIndex,
24
24
  buildServiceByPIdIndex,
25
25
  sharedColumnNames,
@@ -62,14 +62,14 @@ function hasSharedRequiredKey(
62
62
 
63
63
  // Resolves the access points referenced by a cross-`p()` join SQL to their
64
64
  // schemas via `findServiceForPId`, de-duplicated by pId.
65
- function resolveJoinedAccessPoints(
65
+ export function resolveJoinedAccessPoints(
66
66
  sql: string,
67
67
  services: TDSServiceSchema[],
68
68
  ): TDSServiceSchema[] {
69
69
  const serviceByPId = buildServiceByPIdIndex(services);
70
70
  const resolved: TDSServiceSchema[] = [];
71
71
  const seenPIds = new Set<string>();
72
- for (const match of sql.matchAll(AP_CALL_PATTERN)) {
72
+ for (const match of accessPointCalls(sql)) {
73
73
  const pId = match.groups?.pId;
74
74
  if (pId === undefined || seenPIds.has(pId)) {
75
75
  continue;
@@ -83,17 +83,55 @@ function resolveJoinedAccessPoints(
83
83
  return resolved;
84
84
  }
85
85
 
86
+ // Previews the first few of a column's values for a message, so the grounded
87
+ // and live-probe paths show the same number of examples.
88
+ export function previewValues(values: string[]): string {
89
+ return values.slice(0, SAMPLE_VALUE_PREVIEW_COUNT).join(', ');
90
+ }
91
+
92
+ function previewSampleValues(sampleValues: string | undefined): string {
93
+ return previewValues(
94
+ (sampleValues ?? '')
95
+ .split(',')
96
+ .map((value) => value.trim())
97
+ .filter((value) => value.length > 0),
98
+ );
99
+ }
100
+
101
+ // The shared "disjoint join" explanation, used for both grounded-sample and
102
+ // live-probe disjointness so the two paths phrase the finding identically.
103
+ export function buildDisjointJoinMessage(
104
+ apATitle: string,
105
+ apBTitle: string,
106
+ column: string,
107
+ previewA: string,
108
+ previewB: string,
109
+ ): string {
110
+ return `**${apATitle}** and **${apBTitle}** cover different data. Their shared column "${column}" has no values in common (e.g. ${previewA} vs ${previewB}), so there are no rows to join.`;
111
+ }
112
+
86
113
  /**
87
- * Explains why a cross-access-point join returned 0 rows from grounded sample
88
- * values. Returns undefined when this is not a cross-access-point join.
114
+ * Explains a cross-access-point join proven empty by disjoint sample values.
115
+ * `requireComplete` + `restrictToColumns` (the join keys) make it safe pre-execution.
89
116
  */
90
- export function buildCrossJoinZeroRowExplanation(
117
+ export function detectDisjointJoinUniverses(
91
118
  sql: string,
92
119
  services: TDSServiceSchema[],
120
+ options?: { requireComplete?: boolean; restrictToColumns?: Set<string> },
93
121
  ): string | undefined {
94
122
  const involved = resolveJoinedAccessPoints(sql, services);
95
- const apA = involved[0];
96
- const apB = involved[1];
123
+ return detectDisjointJoinUniversesFor(involved[0], involved[1], options);
124
+ }
125
+
126
+ // Takes the already resolved pair so a caller that needs it as well does not
127
+ // scan the statement twice.
128
+ function detectDisjointJoinUniversesFor(
129
+ apA: TDSServiceSchema | undefined,
130
+ apB: TDSServiceSchema | undefined,
131
+ options?: { requireComplete?: boolean; restrictToColumns?: Set<string> },
132
+ ): string | undefined {
133
+ const requireComplete = options?.requireComplete ?? false;
134
+ const restrictToColumns = options?.restrictToColumns;
97
135
  if (apA === undefined || apB === undefined) {
98
136
  return undefined;
99
137
  }
@@ -101,27 +139,52 @@ export function buildCrossJoinZeroRowExplanation(
101
139
  const colsB = buildColumnByNameIndex(apB.columns);
102
140
  for (const name of sharedColumnNames(apA, apB)) {
103
141
  const key = name.toLowerCase();
142
+ if (restrictToColumns !== undefined && !restrictToColumns.has(key)) {
143
+ continue;
144
+ }
104
145
  const colA = colsA.get(key);
105
146
  const colB = colsB.get(key);
147
+ if (
148
+ requireComplete &&
149
+ (colA?.sampleValuesComplete !== true ||
150
+ colB?.sampleValuesComplete !== true)
151
+ ) {
152
+ continue;
153
+ }
106
154
  const setA = parseSampleValueSet(colA?.sampleValues);
107
155
  const setB = parseSampleValueSet(colB?.sampleValues);
108
156
  if (setA.size === 0 || setB.size === 0) {
109
157
  continue;
110
158
  }
111
159
  if (![...setA].some((v) => setB.has(v))) {
112
- const previewA = colA?.sampleValues
113
- ?.split(',')
114
- .slice(0, SAMPLE_VALUE_PREVIEW_COUNT)
115
- .map((v) => v.trim())
116
- .join(', ');
117
- const previewB = colB?.sampleValues
118
- ?.split(',')
119
- .slice(0, SAMPLE_VALUE_PREVIEW_COUNT)
120
- .map((v) => v.trim())
121
- .join(', ');
122
- return `The join executed successfully but returned **0 rows**: **${apA.title}** and **${apB.title}** cover different data. Their shared column "${name}" has no values in common (e.g. ${previewA} vs ${previewB}), so there are no rows to join.`;
160
+ return buildDisjointJoinMessage(
161
+ apA.title,
162
+ apB.title,
163
+ name,
164
+ previewSampleValues(colA?.sampleValues),
165
+ previewSampleValues(colB?.sampleValues),
166
+ );
123
167
  }
124
168
  }
169
+ return undefined;
170
+ }
171
+
172
+ /**
173
+ * Explains why a cross-access-point join returned 0 rows from grounded sample
174
+ * values. Returns undefined when this is not a cross-access-point join.
175
+ */
176
+ export function buildCrossJoinZeroRowExplanation(
177
+ sql: string,
178
+ services: TDSServiceSchema[],
179
+ ): string | undefined {
180
+ const [apA, apB] = resolveJoinedAccessPoints(sql, services);
181
+ const disjoint = detectDisjointJoinUniversesFor(apA, apB);
182
+ if (disjoint !== undefined) {
183
+ return `The join executed successfully but returned **0 rows**: ${disjoint}`;
184
+ }
185
+ if (apA === undefined || apB === undefined) {
186
+ return undefined;
187
+ }
125
188
  return `The join executed successfully but returned **0 rows**: no rows share the join key between **${apA.title}** and **${apB.title}**. These access points may cover different products or time periods.`;
126
189
  }
127
190
 
@@ -16,13 +16,43 @@
16
16
 
17
17
  import type { TDSColumnSchema, TDSServiceSchema } from '../LegendAITypes.js';
18
18
 
19
- export const AP_CALL_PATTERN = /(?<!\w)p\(\s*'(?<pId>[^']+)'\s*\)/g;
19
+ const AP_CALL_PATTERN = /(?<!\w)p\(\s*'(?<pId>[^']+)'\s*\)/g;
20
20
  export const HAS_LIMIT_PATTERN = /\bLIMIT\s+\d+/i;
21
21
 
22
+ /** Lists every `p('...')` access point call in a SQL string. */
23
+ export function accessPointCalls(sql: string): RegExpMatchArray[] {
24
+ return [...sql.matchAll(AP_CALL_PATTERN)];
25
+ }
26
+
27
+ const QUALIFIED_COLUMN_PATTERN = String.raw`(?:(?:"[^"]+"|\w+)\.)?"([^"]+)"`;
28
+ const JOIN_KEY_EQUALITY_PATTERN = new RegExp(
29
+ String.raw`${QUALIFIED_COLUMN_PATTERN}\s*=\s*${QUALIFIED_COLUMN_PATTERN}`,
30
+ 'g',
31
+ );
32
+
33
+ // Lowercased equi-join key names — same quoted column on both sides of `=`
34
+ // (e.g. `a."id" = b."id"`); a filter (`= 'literal'`) never matches both sides.
35
+ export function extractJoinKeyColumns(sql: string): Set<string> {
36
+ const keys = new Set<string>();
37
+ for (const [, left, right] of sql.matchAll(JOIN_KEY_EQUALITY_PATTERN)) {
38
+ const key = left?.toLowerCase();
39
+ if (key !== undefined && key === right?.toLowerCase()) {
40
+ keys.add(key);
41
+ }
42
+ }
43
+ return keys;
44
+ }
45
+
46
+ // The access point's name as used in `p('<dataProductPath>.<name>')` and in a
47
+ // message's `queriedAccessPoints` — the service pattern without its leading `/`.
48
+ export function accessPointName(service: TDSServiceSchema): string {
49
+ return service.pattern.replace(/^\//u, '');
50
+ }
51
+
22
52
  export function servicePId(service: TDSServiceSchema): string | undefined {
23
53
  return service.dataProductPath === undefined
24
54
  ? undefined
25
- : `${service.dataProductPath}.${service.pattern.replace(/^\//, '')}`;
55
+ : `${service.dataProductPath}.${accessPointName(service)}`;
26
56
  }
27
57
 
28
58
  export function pureRelationColumnRef(column: string): string {
@@ -17,7 +17,7 @@
17
17
  import { escapeRegExp, isNonNullable } from '@finos/legend-shared';
18
18
  import type { TDSServiceSchema } from '../LegendAITypes.js';
19
19
  import {
20
- AP_CALL_PATTERN,
20
+ accessPointCalls,
21
21
  HAS_LIMIT_PATTERN,
22
22
  buildColumnByNameIndex,
23
23
  buildServiceByPIdIndex,
@@ -229,7 +229,7 @@ export function sanitizeJoinDuplicateColumns(
229
229
  if (!JOIN_PATTERN.test(sql) || COMPLEX_SQL_SHAPE_PATTERN.test(sql)) {
230
230
  return sql;
231
231
  }
232
- if ([...sql.matchAll(AP_CALL_PATTERN)].length < 2) {
232
+ if (accessPointCalls(sql).length < 2) {
233
233
  return sql;
234
234
  }
235
235
 
@@ -420,7 +420,7 @@ export function boundCrossAccessPointJoinDrivingSide(sql: string): string {
420
420
  if (!JOIN_PATTERN.test(sql) || COMPLEX_SQL_SHAPE_PATTERN.test(sql)) {
421
421
  return sql;
422
422
  }
423
- if ([...sql.matchAll(AP_CALL_PATTERN)].length < 2) {
423
+ if (accessPointCalls(sql).length < 2) {
424
424
  return sql;
425
425
  }
426
426
  const openIdx = topLevelFromOpenParenIndex(sql);
@@ -448,7 +448,7 @@ export function wrapBareJoinAccessPoints(
448
448
  if (!JOIN_PATTERN.test(sql) || COMPLEX_SQL_SHAPE_PATTERN.test(sql)) {
449
449
  return sql;
450
450
  }
451
- if ([...sql.matchAll(AP_CALL_PATTERN)].length < 2) {
451
+ if (accessPointCalls(sql).length < 2) {
452
452
  return sql;
453
453
  }
454
454
  const bareInputPattern =