@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.
- package/lib/index.css +2 -2
- package/lib/index.css.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts +14 -0
- package/lib/legend-ai/LegendAIDocEnrichment.d.ts.map +1 -1
- package/lib/legend-ai/LegendAIDocEnrichment.js +140 -17
- package/lib/legend-ai/LegendAIDocEnrichment.js.map +1 -1
- package/lib/legend-ai/LegendAITypes.d.ts +30 -1
- package/lib/legend-ai/LegendAITypes.d.ts.map +1 -1
- package/lib/legend-ai/LegendAITypes.js +31 -0
- package/lib/legend-ai/LegendAITypes.js.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts +2 -2
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.d.ts.map +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js +1 -1
- package/lib/legend-ai/LegendAI_LegendApplicationPlugin_Extension.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChat.js +62 -38
- package/lib/legend-ai/components/LegendAIChat.js.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts +5 -0
- package/lib/legend-ai/components/LegendAIChatHelpers.d.ts.map +1 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js +8 -1
- package/lib/legend-ai/components/LegendAIChatHelpers.js.map +1 -1
- package/lib/legend-ai/index.d.ts +3 -2
- package/lib/legend-ai/index.d.ts.map +1 -1
- package/lib/legend-ai/index.js +3 -2
- package/lib/legend-ai/index.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts +27 -2
- package/lib/legend-ai/stores/LegendAIChatProcessors.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIChatProcessors.js +329 -86
- package/lib/legend-ai/stores/LegendAIChatProcessors.js.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts +11 -0
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js +53 -19
- package/lib/legend-ai/stores/LegendAIJoinAnalysis.js.map +1 -1
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts +4 -1
- package/lib/legend-ai/stores/LegendAISqlHelpers.d.ts.map +1 -1
- package/lib/legend-ai/stores/LegendAISqlHelpers.js +25 -2
- package/lib/legend-ai/stores/LegendAISqlHelpers.js.map +1 -1
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js +4 -4
- package/lib/legend-ai/stores/LegendAISqlJoinSanitizers.js.map +1 -1
- package/package.json +3 -3
- package/src/legend-ai/LegendAIDocEnrichment.ts +189 -23
- package/src/legend-ai/LegendAITypes.ts +54 -1
- package/src/legend-ai/LegendAI_LegendApplicationPlugin_Extension.ts +2 -0
- package/src/legend-ai/components/LegendAIChat.tsx +114 -78
- package/src/legend-ai/components/LegendAIChatHelpers.ts +9 -3
- package/src/legend-ai/index.ts +7 -0
- package/src/legend-ai/stores/LegendAIChatProcessors.ts +476 -105
- package/src/legend-ai/stores/LegendAIJoinAnalysis.ts +82 -19
- package/src/legend-ai/stores/LegendAISqlHelpers.ts +32 -2
- 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
|
-
|
|
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
|
|
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
|
|
88
|
-
*
|
|
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
|
|
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
|
-
|
|
96
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
451
|
+
if (accessPointCalls(sql).length < 2) {
|
|
452
452
|
return sql;
|
|
453
453
|
}
|
|
454
454
|
const bareInputPattern =
|