@lightdash/warehouses 0.3455.0 → 0.3457.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export * from './ssh/sshTunnel';
2
2
  export * from './types';
3
+ export * from './validateWarehouseColumnReferences';
3
4
  export * from './warehouseClientFromCredentials';
4
5
  export * from './warehouseClients/AthenaWarehouseClient';
5
6
  export * from './warehouseClients/BigqueryWarehouseClient';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,kCAAkC,CAAC;AACjD,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,8CAA8C,CAAC;AAC7D,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,SAAS,CAAC;AACxB,cAAc,qCAAqC,CAAC;AACpD,cAAc,kCAAkC,CAAC;AACjD,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,8CAA8C,CAAC;AAC7D,cAAc,0CAA0C,CAAC;AACzD,cAAc,4CAA4C,CAAC;AAC3D,cAAc,4CAA4C,CAAC;AAC3D,cAAc,2CAA2C,CAAC;AAC1D,cAAc,6CAA6C,CAAC;AAC5D,cAAc,yCAAyC,CAAC;AACxD,cAAc,+BAA+B,CAAC"}
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./ssh/sshTunnel"), exports);
18
18
  __exportStar(require("./types"), exports);
19
+ __exportStar(require("./validateWarehouseColumnReferences"), exports);
19
20
  __exportStar(require("./warehouseClientFromCredentials"), exports);
20
21
  __exportStar(require("./warehouseClients/AthenaWarehouseClient"), exports);
21
22
  __exportStar(require("./warehouseClients/BigqueryWarehouseClient"), exports);
@@ -0,0 +1,9 @@
1
+ import { type Explore, type ExploreError, type WarehouseClient } from '@lightdash/common';
2
+ type WarehouseColumnValidationClient = Pick<WarehouseClient, 'getFieldQuoteChar' | 'runQuery'>;
3
+ export declare const validateWarehouseColumnReferences: ({ explores, client, tags, }: {
4
+ explores: (Explore | ExploreError)[];
5
+ client: WarehouseColumnValidationClient;
6
+ tags: Record<string, string>;
7
+ }) => Promise<(Explore | ExploreError)[]>;
8
+ export {};
9
+ //# sourceMappingURL=validateWarehouseColumnReferences.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateWarehouseColumnReferences.d.ts","sourceRoot":"","sources":["../src/validateWarehouseColumnReferences.ts"],"names":[],"mappings":"AAAA,OAAO,EAKH,KAAK,OAAO,EACZ,KAAK,YAAY,EACjB,KAAK,eAAe,EACvB,MAAM,mBAAmB,CAAC;AAG3B,KAAK,+BAA+B,GAAG,IAAI,CACvC,eAAe,EACf,mBAAmB,GAAG,UAAU,CACnC,CAAC;AAmKF,eAAO,MAAM,iCAAiC,GAAU,6BAIrD;IACC,QAAQ,EAAE,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CAAC;IACrC,MAAM,EAAE,+BAA+B,CAAC;IACxC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC,KAAG,OAAO,CAAC,CAAC,OAAO,GAAG,YAAY,CAAC,EAAE,CA0CrC,CAAC"}
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateWarehouseColumnReferences = void 0;
4
+ const common_1 = require("@lightdash/common");
5
+ const processPromisesInBatches_1 = require("./utils/processPromisesInBatches");
6
+ const getTableReferences = (explores) => {
7
+ const referencesByTable = new Map();
8
+ explores.forEach((explore) => {
9
+ if ((0, common_1.isExploreError)(explore))
10
+ return;
11
+ Object.values(explore.tables).forEach((table) => {
12
+ if (table.sqlTable.includes('${'))
13
+ return;
14
+ const key = `${table.name}\0${table.sqlTable}`;
15
+ const tableReference = referencesByTable.get(key) ?? {
16
+ modelName: table.name,
17
+ sqlTable: table.sqlTable,
18
+ columnReferences: new Set(),
19
+ };
20
+ [
21
+ ...Object.values(table.dimensions),
22
+ ...Object.values(table.metrics),
23
+ ]
24
+ .flatMap((field) => (0, common_1.getTableColumnReferences)(field.sql))
25
+ .forEach((columnReference) => tableReference.columnReferences.add(columnReference));
26
+ referencesByTable.set(key, tableReference);
27
+ });
28
+ });
29
+ return [...referencesByTable.values()].filter(({ columnReferences }) => columnReferences.size > 0);
30
+ };
31
+ const getProbeSql = ({ sqlTable, modelName, columnReferences, fieldQuoteChar, }) => {
32
+ const tableAlias = `${fieldQuoteChar}${modelName}${fieldQuoteChar}`;
33
+ const select = columnReferences.length === 0
34
+ ? '1'
35
+ : columnReferences
36
+ .map((reference) => `${tableAlias}.${reference}`)
37
+ .join(', ');
38
+ return `SELECT ${select} FROM ${sqlTable} AS ${tableAlias} WHERE 1 = 0`;
39
+ };
40
+ const VALIDATION_CONCURRENCY = 5;
41
+ const findInvalidColumnReferences = async ({ client, tableReferences, tags, }) => {
42
+ const fieldQuoteChar = client.getFieldQuoteChar();
43
+ const invalidReferencesByTable = await (0, processPromisesInBatches_1.processPromisesInBatches)(tableReferences, VALIDATION_CONCURRENCY, async (tableReference) => {
44
+ const sharedProbeArgs = {
45
+ sqlTable: tableReference.sqlTable,
46
+ modelName: tableReference.modelName,
47
+ fieldQuoteChar,
48
+ };
49
+ const columnReferences = [...tableReference.columnReferences];
50
+ try {
51
+ await client.runQuery(getProbeSql({
52
+ ...sharedProbeArgs,
53
+ columnReferences,
54
+ }), tags);
55
+ return [];
56
+ }
57
+ catch {
58
+ // Probe separately only when the batched query fails.
59
+ }
60
+ try {
61
+ await client.runQuery(getProbeSql({
62
+ ...sharedProbeArgs,
63
+ columnReferences: [],
64
+ }), tags);
65
+ }
66
+ catch {
67
+ // Relation failures do not prove that its columns are invalid.
68
+ return [];
69
+ }
70
+ const invalidReferences = await (0, processPromisesInBatches_1.processPromisesInBatches)(columnReferences, VALIDATION_CONCURRENCY, async (columnReference) => {
71
+ try {
72
+ await client.runQuery(getProbeSql({
73
+ ...sharedProbeArgs,
74
+ columnReferences: [columnReference],
75
+ }), tags);
76
+ return null;
77
+ }
78
+ catch (error) {
79
+ try {
80
+ await client.runQuery(getProbeSql({
81
+ ...sharedProbeArgs,
82
+ columnReferences: [],
83
+ }), tags);
84
+ return {
85
+ modelName: tableReference.modelName,
86
+ columnReference,
87
+ error: (0, common_1.getErrorMessage)(error),
88
+ };
89
+ }
90
+ catch {
91
+ // The relation became unavailable mid-probe.
92
+ return null;
93
+ }
94
+ }
95
+ });
96
+ return invalidReferences.filter((reference) => reference !== null);
97
+ });
98
+ return invalidReferencesByTable.flat();
99
+ };
100
+ const validateWarehouseColumnReferences = async ({ explores, client, tags, }) => {
101
+ const invalidReferences = await findInvalidColumnReferences({
102
+ client,
103
+ tableReferences: getTableReferences(explores),
104
+ tags,
105
+ });
106
+ if (invalidReferences.length === 0)
107
+ return explores;
108
+ const warningExploreByModel = new Map();
109
+ explores.forEach((explore) => {
110
+ if ((0, common_1.isExploreError)(explore))
111
+ return;
112
+ Object.values(explore.tables).forEach((table) => {
113
+ const current = warningExploreByModel.get(table.name);
114
+ if (current === undefined || explore.name === table.name) {
115
+ warningExploreByModel.set(table.name, explore.name);
116
+ }
117
+ });
118
+ });
119
+ const warningsByExplore = new Map();
120
+ invalidReferences.forEach(({ modelName, columnReference, error }) => {
121
+ const exploreName = warningExploreByModel.get(modelName);
122
+ if (exploreName === undefined)
123
+ return;
124
+ const warnings = warningsByExplore.get(exploreName) ?? [];
125
+ warnings.push({
126
+ type: common_1.InlineErrorType.WAREHOUSE_COLUMN_ERROR,
127
+ message: `Warehouse rejected column reference \${TABLE}.${columnReference} in model "${modelName}": ${error}`,
128
+ });
129
+ warningsByExplore.set(exploreName, warnings);
130
+ });
131
+ return explores.map((explore) => {
132
+ if ((0, common_1.isExploreError)(explore))
133
+ return explore;
134
+ const warnings = warningsByExplore.get(explore.name);
135
+ if (warnings === undefined)
136
+ return explore;
137
+ return {
138
+ ...explore,
139
+ warnings: [...(explore.warnings ?? []), ...warnings],
140
+ };
141
+ });
142
+ };
143
+ exports.validateWarehouseColumnReferences = validateWarehouseColumnReferences;
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=validateWarehouseColumnReferences.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"validateWarehouseColumnReferences.test.d.ts","sourceRoot":"","sources":["../src/validateWarehouseColumnReferences.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const common_1 = require("@lightdash/common");
4
+ const validateWarehouseColumnReferences_1 = require("./validateWarehouseColumnReferences");
5
+ const explore = {
6
+ name: 'orders',
7
+ label: 'Orders',
8
+ tags: [],
9
+ baseTable: 'orders',
10
+ joinedTables: [],
11
+ targetDatabase: common_1.SupportedDbtAdapter.POSTGRES,
12
+ tables: {
13
+ orders: {
14
+ name: 'orders',
15
+ label: 'Orders',
16
+ database: 'postgres',
17
+ schema: 'jaffle',
18
+ sqlTable: '"postgres"."jaffle"."orders"',
19
+ lineageGraph: {},
20
+ dimensions: {
21
+ amount: {
22
+ fieldType: common_1.FieldType.DIMENSION,
23
+ type: common_1.DimensionType.NUMBER,
24
+ name: 'amount',
25
+ label: 'Amount',
26
+ table: 'orders',
27
+ tableLabel: 'Orders',
28
+ // eslint-disable-next-line no-template-curly-in-string
29
+ sql: '${TABLE}.amount',
30
+ compiledSql: '"orders".amount',
31
+ tablesReferences: ['orders'],
32
+ hidden: false,
33
+ },
34
+ },
35
+ metrics: {
36
+ stale_metric: {
37
+ fieldType: common_1.FieldType.METRIC,
38
+ type: common_1.MetricType.AVERAGE,
39
+ name: 'stale_metric',
40
+ label: 'Stale metric',
41
+ table: 'orders',
42
+ tableLabel: 'Orders',
43
+ // eslint-disable-next-line no-template-curly-in-string
44
+ sql: '${TABLE}.rolling_30d_avg_sales',
45
+ compiledSql: 'AVG("orders".rolling_30d_avg_sales)',
46
+ tablesReferences: ['orders'],
47
+ hidden: false,
48
+ },
49
+ },
50
+ },
51
+ },
52
+ };
53
+ const queryResult = { fields: {}, rows: [] };
54
+ const tags = { query_context: 'cli' };
55
+ describe('validateWarehouseColumnReferences', () => {
56
+ it('returns explores unchanged when the warehouse accepts all references', async () => {
57
+ const runQuery = vi.fn(async () => queryResult);
58
+ const result = await (0, validateWarehouseColumnReferences_1.validateWarehouseColumnReferences)({
59
+ explores: [explore],
60
+ client: { getFieldQuoteChar: () => '"', runQuery },
61
+ tags,
62
+ });
63
+ expect(result).toEqual([explore]);
64
+ expect(runQuery).toHaveBeenCalledTimes(1);
65
+ expect(runQuery).toHaveBeenNthCalledWith(1, 'SELECT "orders".amount, "orders".rolling_30d_avg_sales FROM "postgres"."jaffle"."orders" AS "orders" WHERE 1 = 0', tags);
66
+ });
67
+ it('attaches one warning for each reference rejected by the warehouse', async () => {
68
+ const runQuery = vi.fn(async (sql) => {
69
+ if (sql.includes('rolling_30d_avg_sales')) {
70
+ throw new Error('column orders.rolling_30d_avg_sales does not exist');
71
+ }
72
+ return queryResult;
73
+ });
74
+ const [result] = await (0, validateWarehouseColumnReferences_1.validateWarehouseColumnReferences)({
75
+ explores: [explore],
76
+ client: { getFieldQuoteChar: () => '"', runQuery },
77
+ tags,
78
+ });
79
+ expect(result).toEqual({
80
+ ...explore,
81
+ warnings: [
82
+ {
83
+ type: common_1.InlineErrorType.WAREHOUSE_COLUMN_ERROR,
84
+ message:
85
+ // eslint-disable-next-line no-template-curly-in-string
86
+ 'Warehouse rejected column reference ${TABLE}.rolling_30d_avg_sales in model "orders": column orders.rolling_30d_avg_sales does not exist',
87
+ },
88
+ ],
89
+ });
90
+ expect(runQuery).toHaveBeenCalledTimes(5);
91
+ });
92
+ it('skips column validation when the relation probe fails', async () => {
93
+ const runQuery = vi.fn(async () => {
94
+ throw new Error('warehouse unavailable');
95
+ });
96
+ const result = await (0, validateWarehouseColumnReferences_1.validateWarehouseColumnReferences)({
97
+ explores: [explore],
98
+ client: { getFieldQuoteChar: () => '"', runQuery },
99
+ tags,
100
+ });
101
+ expect(result).toEqual([explore]);
102
+ expect(runQuery).toHaveBeenCalledTimes(2);
103
+ });
104
+ it('preserves pre-existing warnings and appends warehouse warnings after them', async () => {
105
+ const existingWarning = {
106
+ type: common_1.InlineErrorType.FIELD_ERROR,
107
+ message: 'Dimension "broken_dimension" failed to compile',
108
+ };
109
+ const exploreWithExistingWarning = {
110
+ ...explore,
111
+ warnings: [existingWarning],
112
+ };
113
+ const runQuery = vi.fn(async (sql) => {
114
+ if (sql.includes('rolling_30d_avg_sales')) {
115
+ throw new Error('column orders.rolling_30d_avg_sales does not exist');
116
+ }
117
+ return queryResult;
118
+ });
119
+ const [result] = await (0, validateWarehouseColumnReferences_1.validateWarehouseColumnReferences)({
120
+ explores: [exploreWithExistingWarning],
121
+ client: { getFieldQuoteChar: () => '"', runQuery },
122
+ tags,
123
+ });
124
+ expect(result).toEqual({
125
+ ...exploreWithExistingWarning,
126
+ warnings: [
127
+ existingWarning,
128
+ {
129
+ type: common_1.InlineErrorType.WAREHOUSE_COLUMN_ERROR,
130
+ message:
131
+ // eslint-disable-next-line no-template-curly-in-string
132
+ 'Warehouse rejected column reference ${TABLE}.rolling_30d_avg_sales in model "orders": column orders.rolling_30d_avg_sales does not exist',
133
+ },
134
+ ],
135
+ });
136
+ });
137
+ it('skips explore errors and templated table references without probing', async () => {
138
+ const exploreError = {
139
+ name: 'broken_explore',
140
+ label: 'Broken explore',
141
+ errors: [
142
+ {
143
+ type: common_1.InlineErrorType.METADATA_PARSE_ERROR,
144
+ message: 'Failed to compile explore',
145
+ },
146
+ ],
147
+ };
148
+ const templatedExplore = {
149
+ ...explore,
150
+ name: 'templated_orders',
151
+ tables: {
152
+ orders: {
153
+ ...explore.tables.orders,
154
+ // eslint-disable-next-line no-template-curly-in-string
155
+ sqlTable: '"postgres"."${ld.schema}"."orders"',
156
+ },
157
+ },
158
+ };
159
+ const runQuery = vi.fn(async () => queryResult);
160
+ const result = await (0, validateWarehouseColumnReferences_1.validateWarehouseColumnReferences)({
161
+ explores: [exploreError, templatedExplore],
162
+ client: { getFieldQuoteChar: () => '"', runQuery },
163
+ tags,
164
+ });
165
+ expect(result).toEqual([exploreError, templatedExplore]);
166
+ expect(runQuery).not.toHaveBeenCalled();
167
+ });
168
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightdash/warehouses",
3
- "version": "0.3455.0",
3
+ "version": "0.3457.0",
4
4
  "description": "Warehouse connectors for Lightdash",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,7 +30,7 @@
30
30
  "snowflake-sdk": "2.3.4",
31
31
  "ssh2": "1.14.0",
32
32
  "trino-client": "0.2.9",
33
- "@lightdash/common": "0.3455.0"
33
+ "@lightdash/common": "0.3457.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/big.js": "6.2.2",