From 10963b0bc38ddc1ae962bc676bfcfc9909e01ad4 Mon Sep 17 00:00:00 2001 From: jayachandranramadoss-devrev Date: Mon, 15 Jun 2026 20:02:25 +0530 Subject: [PATCH] fix(meerkat-core): qualify lookup columns to stop ambiguous CSV export (ISS-301213) CSV export from dashboard widgets failed with `Binder Error: Ambiguous reference to column name` when a base query joined multiple lookup tables that share a raw column name (e.g. `deprecated_created_by_agent_id` on both `dim_devu` and `dim_group`, seen on the Razorpay Incident dashboard / TKT-65015). Each resolution lookup is joined as `SELECT , * FROM ()`. The trailing bare `*` re-exposed every raw column of the lookup, so a column name shared across two lookups appeared twice in the intermediate join relation and the binder rejected the export. Fix: rewrite each lookup table's SQL so it projects its declared dimensions (plus the join column) under a globally-unique table-qualified alias `__`. The bare `*` then only ever surfaces uniquely-named columns, so the ambiguity cannot occur. The resolution dimension `.sql` and the join ON clause are updated to reference the same qualified aliases. This is an alternative to the post-hoc narrowing approach in PR #272: it keeps every declared lookup column available (renamed) rather than dropping unreferenced ones, and disambiguates even shared *declared* column names. Test plan: - New regression test asserts each lookup column is projected under a distinct qualified alias and the export COPYs with resolved values intact. - meerkat-core: 549 passed. meerkat-node: 906 passed. meerkat-browser: 2 passed. --- .../generate-resolution-schemas.spec.ts | 19 +- .../generators/generate-resolution-schemas.ts | 91 ++++++- .../src/resolution/resolution.spec.ts | 26 +- meerkat-core/src/resolution/resolution.ts | 9 +- .../steps/__tests__/resolution-step.spec.ts | 2 +- ...resolution-shared-column-ambiguity.spec.ts | 229 ++++++++++++++++++ meerkat-node/src/__tests__/resolution.spec.ts | 27 +-- 7 files changed, 355 insertions(+), 48 deletions(-) create mode 100644 meerkat-node/src/__tests__/resolution-shared-column-ambiguity.spec.ts diff --git a/meerkat-core/src/resolution/generators/__tests__/generate-resolution-schemas.spec.ts b/meerkat-core/src/resolution/generators/__tests__/generate-resolution-schemas.spec.ts index 58d32e89..2f289530 100644 --- a/meerkat-core/src/resolution/generators/__tests__/generate-resolution-schemas.spec.ts +++ b/meerkat-core/src/resolution/generators/__tests__/generate-resolution-schemas.spec.ts @@ -95,7 +95,7 @@ describe('generate-resolution-schemas', () => { const result = generateResolutionSchemas(config); expect(result[0].dimensions[0].sql).toBe( - 'orders__customer_id.display_name' + 'orders__customer_id."orders__customer_id__display_name"' ); }); @@ -257,7 +257,12 @@ describe('generate-resolution-schemas', () => { const result = generateResolutionSchemas(config); - expect(result[0].sql).toBe('SELECT * FROM customers WHERE active = true'); + // The lookup SQL is rewritten to project declared columns (plus the join + // column) under table-qualified aliases, so the bare `*` over the join + // can no longer expose colliding raw column names (ISS-301213). + expect(result[0].sql).toBe( + 'SELECT orders__customer_id."name" AS "orders__customer_id__name", orders__customer_id."id" AS "orders__customer_id__id" FROM (SELECT * FROM customers WHERE active = true) AS orders__customer_id' + ); }); it('should have empty measures array in resolution schema', () => { @@ -301,7 +306,9 @@ describe('generate-resolution-schemas', () => { expect(result[0].name).toBe('orders__tag_ids'); expect(result[0].dimensions[0].name).toBe('orders__tag_ids__tag_name'); - expect(result[0].dimensions[0].sql).toBe('orders__tag_ids.tag_name'); + expect(result[0].dimensions[0].sql).toBe( + 'orders__tag_ids."orders__tag_ids__tag_name"' + ); }); it('should handle multiple resolution columns from same source', () => { @@ -329,19 +336,19 @@ describe('generate-resolution-schemas', () => { expect(result[0].dimensions).toHaveLength(3); expect(result[0].dimensions[0]).toEqual({ name: 'orders__user_id__first_name', - sql: 'orders__user_id.first_name', + sql: 'orders__user_id."orders__user_id__first_name"', type: 'string', alias: 'orders__user_id__first_name', }); expect(result[0].dimensions[1]).toEqual({ name: 'orders__user_id__last_name', - sql: 'orders__user_id.last_name', + sql: 'orders__user_id."orders__user_id__last_name"', type: 'string', alias: 'orders__user_id__last_name', }); expect(result[0].dimensions[2]).toEqual({ name: 'orders__user_id__email', - sql: 'orders__user_id.email', + sql: 'orders__user_id."orders__user_id__email"', type: 'string', alias: 'orders__user_id__email', }); diff --git a/meerkat-core/src/resolution/generators/generate-resolution-schemas.ts b/meerkat-core/src/resolution/generators/generate-resolution-schemas.ts index 2032cd28..ea95da6b 100644 --- a/meerkat-core/src/resolution/generators/generate-resolution-schemas.ts +++ b/meerkat-core/src/resolution/generators/generate-resolution-schemas.ts @@ -1,8 +1,70 @@ import { getNamespacedKey, memberKeyToSafeKey } from '../../member-formatters'; -import { TableSchema } from '../../types/cube-types/table'; +import { Dimension, TableSchema } from '../../types/cube-types/table'; import { findInDimensionSchemas } from '../../utils/find-in-table-schema'; import { ResolutionConfig } from '../types'; +/** + * Builds the qualified alias under which a lookup column is projected. + * The alias is the table-qualified safe key (`__`), which is + * globally unique per resolution config — so even when two different lookup + * tables expose a column with the same raw name (e.g. `deprecated_created_by_agent_id` + * on both `dim_devu` and `dim_group`), the projected names never collide. + */ +const qualifiedLookupAlias = (configName: string, column: string): string => + memberKeyToSafeKey(getNamespacedKey(configName, column)); + +/** + * Rewrites a lookup table's SQL so it projects every declared dimension (plus + * the join column) under a table-qualified alias instead of leaking its raw + * physical columns. + * + * The resolution pipeline joins each lookup as `SELECT , * FROM + * ()`. The trailing bare `*` re-exposes every raw column of the + * lookup — including undeclared passthrough columns that several `dim_*` tables + * share (`deprecated_created_by_agent_id`, etc.). When two lookups are joined, + * those shared raw names collide and DuckDB's binder rejects the export with + * `Binder Error: Ambiguous reference to column name` (ISS-301213 / TKT-65015). + * + * By projecting each column as ` AS "__"`, the lookup + * subquery only ever emits uniquely-named columns, so the bare `*` is harmless. + * + * Note: declared measures are intentionally not projected here. Resolution + * lookups are never aggregated (the resolution schema always carries + * `measures: []`), and a measure's SQL is an aggregate expression that is + * invalid in this flat, non-grouped projection. + */ +const buildQualifiedLookupSql = ( + sourceSchema: TableSchema, + configName: string, + joinColumn: string +): string => { + const wrapAlias = memberKeyToSafeKey(configName); + const projections: string[] = []; + const projectedColumns = new Set(); + + const addProjection = (column: string) => { + if (projectedColumns.has(column)) { + return; + } + projectedColumns.add(column); + // Reference the raw column off the wrapped subquery alias (the inner + // `(sourceSchema.sql) AS wrapAlias` exposes each declared column by its + // physical name), then re-alias it to a globally-unique qualified key. + projections.push( + `${wrapAlias}."${column}" AS "${qualifiedLookupAlias(configName, column)}"` + ); + }; + + sourceSchema.dimensions.forEach((dimension) => { + addProjection(dimension.name); + }); + + // The join column may not be a declared dimension, but the JOIN needs it. + addProjection(joinColumn); + + return `SELECT ${projections.join(', ')} FROM (${sourceSchema.sql}) AS ${wrapAlias}`; +}; + export const generateResolutionSchemas = (config: ResolutionConfig) => { const resolutionSchemas: TableSchema[] = []; config.columnConfigs.forEach((colConfig) => { @@ -15,12 +77,20 @@ export const generateResolutionSchemas = (config: ResolutionConfig) => { const baseName = memberKeyToSafeKey(colConfig.name); + // Qualify every column the lookup projects so its bare `*` can never expose + // a raw column name that clashes with another lookup table (ISS-301213). + const qualifiedSql = buildQualifiedLookupSql( + tableSchema, + colConfig.name, + colConfig.joinColumn + ); + // For each column that needs to be resolved, create a copy of the relevant table schema. // We use the name of the column in the base query as the table schema name // to avoid conflicts. const resolutionSchema: TableSchema = { name: baseName, - sql: tableSchema.sql, + sql: qualifiedSql, measures: [], dimensions: colConfig.resolutionColumns.map((col) => { const dimension = findInDimensionSchemas( @@ -30,14 +100,17 @@ export const generateResolutionSchemas = (config: ResolutionConfig) => { if (!dimension) { throw new Error(`Dimension not found: ${col}`); } - return { + const dimensionDef: Dimension = { // Need to create a new name due to limitations with how // CubeToSql handles duplicate dimension names between different sources. name: memberKeyToSafeKey(getNamespacedKey(colConfig.name, col)), - sql: `${baseName}.${col}`, + // Reference the qualified alias projected by buildQualifiedLookupSql, + // not the raw column name (which is no longer exposed). + sql: `${baseName}."${qualifiedLookupAlias(colConfig.name, col)}"`, type: dimension.type, alias: memberKeyToSafeKey(getNamespacedKey(colConfig.name, col)), }; + return dimensionDef; }), }; @@ -46,3 +119,13 @@ export const generateResolutionSchemas = (config: ResolutionConfig) => { return resolutionSchemas; }; + +/** + * The qualified alias under which a lookup's join column is projected by + * {@link buildQualifiedLookupSql}. Exported so the join ON clause can reference + * the same alias. + */ +export const resolutionJoinColumnAlias = ( + configName: string, + joinColumn: string +): string => qualifiedLookupAlias(configName, joinColumn); diff --git a/meerkat-core/src/resolution/resolution.spec.ts b/meerkat-core/src/resolution/resolution.spec.ts index 4bb3cd65..8560b2fe 100644 --- a/meerkat-core/src/resolution/resolution.spec.ts +++ b/meerkat-core/src/resolution/resolution.spec.ts @@ -155,10 +155,10 @@ describe('Create base table schema', () => { ], joins: [ { - sql: '__base_query.base_table__column1 = base_table__column1.id', + sql: '__base_query.base_table__column1 = base_table__column1."base_table__column1__id"', }, { - sql: '__base_query.base_table__column2 = base_table__column2.id', + sql: '__base_query.base_table__column2 = base_table__column2."base_table__column2__id"', }, ], }); @@ -269,10 +269,10 @@ describe('Create base table schema', () => { ], joins: [ { - sql: '__base_query."Column 1" = base_table__column1.id', + sql: '__base_query."Column 1" = base_table__column1."base_table__column1__id"', }, { - sql: '__base_query."Column 2" = base_table__column2.id', + sql: '__base_query."Column 2" = base_table__column2."base_table__column2__id"', }, ], }); @@ -349,12 +349,12 @@ describe('Generate resolution schemas', () => { expect(schemas).toEqual([ { name: 'base_table__column1', - sql: '', + sql: 'SELECT base_table__column1."id" AS "base_table__column1__id", base_table__column1."display_id" AS "base_table__column1__display_id", base_table__column1."display_name" AS "base_table__column1__display_name" FROM () AS base_table__column1', measures: [], dimensions: [ { name: 'base_table__column1__display_id', - sql: 'base_table__column1.display_id', + sql: 'base_table__column1."base_table__column1__display_id"', type: 'string', alias: 'base_table__column1__display_id', }, @@ -362,18 +362,18 @@ describe('Generate resolution schemas', () => { }, { name: 'base_table__column2', - sql: '', + sql: 'SELECT base_table__column2."id" AS "base_table__column2__id", base_table__column2."display_id" AS "base_table__column2__display_id", base_table__column2."display_name" AS "base_table__column2__display_name" FROM () AS base_table__column2', measures: [], dimensions: [ { name: 'base_table__column2__id', - sql: 'base_table__column2.id', + sql: 'base_table__column2."base_table__column2__id"', type: 'string', alias: 'base_table__column2__id', }, { name: 'base_table__column2__display_name', - sql: 'base_table__column2.display_name', + sql: 'base_table__column2."base_table__column2__display_name"', type: 'string', alias: 'base_table__column2__display_name', }, @@ -505,12 +505,12 @@ describe('Generate resolution schemas', () => { expect(schemas).toEqual([ { name: 'base_table__column1', - sql: '', + sql: 'SELECT base_table__column1."id" AS "base_table__column1__id", base_table__column1."display_id" AS "base_table__column1__display_id" FROM () AS base_table__column1', measures: [], dimensions: [ { name: 'base_table__column1__display_id', - sql: 'base_table__column1.display_id', + sql: 'base_table__column1."base_table__column1__display_id"', type: 'string', alias: 'base_table__column1__display_id', }, @@ -573,12 +573,12 @@ describe('Generate resolution schemas', () => { expect(schemas).toEqual([ { name: 'base_table__column1', - sql: '', + sql: 'SELECT base_table__column1."id" AS "base_table__column1__id", base_table__column1."display_id" AS "base_table__column1__display_id" FROM () AS base_table__column1', measures: [], dimensions: [ { name: 'base_table__column1__display_id', - sql: 'base_table__column1.display_id', + sql: 'base_table__column1."base_table__column1__display_id"', type: 'string', alias: 'base_table__column1__display_id', }, diff --git a/meerkat-core/src/resolution/resolution.ts b/meerkat-core/src/resolution/resolution.ts index b3674553..293d6d18 100644 --- a/meerkat-core/src/resolution/resolution.ts +++ b/meerkat-core/src/resolution/resolution.ts @@ -7,6 +7,7 @@ import { memberKeyToSafeKey } from '../member-formatters/member-key-to-safe-key' import { Member, Query } from '../types/cube-types/query'; import { Dimension, Measure, TableSchema } from '../types/cube-types/table'; import { isArrayTypeMember } from '../utils/is-array-member-type'; +import { resolutionJoinColumnAlias } from './generators/generate-resolution-schemas'; import { BASE_DATA_SOURCE_NAME, ResolutionColumnConfig, @@ -92,10 +93,16 @@ export const createBaseTableSchema = ( joins: resolutionConfig.columnConfigs.map((columnConfig) => { const targetTable = memberKeyToSafeKey(columnConfig.name); return { + // The lookup's join column is projected under a table-qualified alias + // (see buildQualifiedLookupSql), so reference that alias, not the raw + // column name which the lookup subquery no longer exposes. sql: `${BASE_DATA_SOURCE_NAME}.${constructAliasForSQL( columnConfig.name, schemaByName[columnConfig.name]?.alias - )} = ${targetTable}.${columnConfig.joinColumn}`, + )} = ${targetTable}."${resolutionJoinColumnAlias( + columnConfig.name, + columnConfig.joinColumn + )}"`, }; }), }; diff --git a/meerkat-core/src/resolution/steps/__tests__/resolution-step.spec.ts b/meerkat-core/src/resolution/steps/__tests__/resolution-step.spec.ts index 10816909..78bfc999 100644 --- a/meerkat-core/src/resolution/steps/__tests__/resolution-step.spec.ts +++ b/meerkat-core/src/resolution/steps/__tests__/resolution-step.spec.ts @@ -196,7 +196,7 @@ describe('resolution-step', () => { 'orders__owner_id__display_name' ); expect(resolutionSchema.dimensions[0].sql).toBe( - 'orders__owner_id.display_name' + 'orders__owner_id."orders__owner_id__display_name"' ); }); diff --git a/meerkat-node/src/__tests__/resolution-shared-column-ambiguity.spec.ts b/meerkat-node/src/__tests__/resolution-shared-column-ambiguity.spec.ts new file mode 100644 index 00000000..3ff44c3d --- /dev/null +++ b/meerkat-node/src/__tests__/resolution-shared-column-ambiguity.spec.ts @@ -0,0 +1,229 @@ +import { Query, ResolutionConfig, TableSchema } from '@devrev/meerkat-core'; +import { cubeQueryToSQLWithResolution } from '../cube-to-sql-with-resolution/cube-to-sql-with-resolution'; +import { duckdbExec } from '../duckdb-exec'; + +/** + * Regression test for ISS-301213. + * + * CSV export from a dashboard widget fails with + * `Binder Error: Ambiguous reference to column name ""` + * when the widget's SQL resolves two (or more) scalar lookup columns whose + * source lookup tables share a column name beyond the one being resolved. + * + * Real-world shape (TKT-65015, Razorpay Incident dashboard): a base query + * left-joins lookup tables `dim_devu` (owner) and `dim_group`, both of which + * carry an unrelated column (`deprecated_created_by_agent_id` in the HAR). Each + * lookup is joined as `SELECT , * FROM ()`. The bare + * `*` re-exposes every RAW column of the lookup, so the shared name appears + * twice in the intermediate join relation and DuckDB's binder rejects the + * export. + * + * Fix (qualify approach): each lookup table's SQL is rewritten to project its + * declared columns (plus the join column) under table-qualified aliases + * (`__`). Those aliases are globally unique per resolution + * config, so the bare `*` over the join can never surface two columns with the + * same name — eliminating the ambiguity at its source. + * + * NOTE ON THE ORACLE: the node `duckdb` binding tolerates intermediate + * relations that carry duplicate column names (production `duckdb-wasm` does + * not), so a plain "COPY does not throw" assertion is false-green on this + * engine. We therefore assert structurally on the generated lookup SQL — it + * must project only qualified aliases and never a bare `*` that could leak a + * colliding raw column — and additionally verify the export still produces + * correct, resolved data. + */ + +const CREATE_BASE_TABLE = `CREATE OR REPLACE TABLE iss301213_tickets ( + id INTEGER, + owner_id VARCHAR, + group_id VARCHAR +)`; + +const BASE_DATA = `INSERT INTO iss301213_tickets VALUES +(1, 'owner1', 'group1'), +(2, 'owner2', 'group2'), +(3, 'owner1', 'group2')`; + +// Owner lookup carries an extra `deprecated_created_by_agent_id` column that is +// never resolved — pure passthrough that the bare `*` would otherwise leak. +const CREATE_OWNER_LOOKUP = `CREATE OR REPLACE TABLE iss301213_owner_lookup ( + id VARCHAR, + fullname VARCHAR, + deprecated_created_by_agent_id VARCHAR +)`; + +const OWNER_LOOKUP_DATA = `INSERT INTO iss301213_owner_lookup VALUES +('owner1', 'Alice', 'copy-o1'), +('owner2', 'Bob', 'copy-o2')`; + +// Group lookup ALSO carries `deprecated_created_by_agent_id` — this is the +// shared column that becomes ambiguous when both lookups are re-exposed via `*`. +const CREATE_GROUP_LOOKUP = `CREATE OR REPLACE TABLE iss301213_group_lookup ( + id VARCHAR, + name VARCHAR, + deprecated_created_by_agent_id VARCHAR +)`; + +const GROUP_LOOKUP_DATA = `INSERT INTO iss301213_group_lookup VALUES +('group1', 'Group One', 'copy-g1'), +('group2', 'Group Two', 'copy-g2')`; + +const BASE_SCHEMA: TableSchema = { + name: 'iss301213_tickets', + sql: 'select * from iss301213_tickets', + measures: [{ name: 'count', sql: 'COUNT(*)', type: 'number' }], + dimensions: [ + { alias: 'ID', name: 'id', sql: 'id', type: 'number' }, + { alias: 'Owner', name: 'owner_id', sql: 'owner_id', type: 'string' }, + { alias: 'Group', name: 'group_id', sql: 'group_id', type: 'string' }, + ], +}; + +const OWNER_LOOKUP_SCHEMA: TableSchema = { + name: 'iss301213_owner_lookup', + sql: 'select * from iss301213_owner_lookup', + measures: [], + dimensions: [ + { alias: 'ID', name: 'id', sql: 'id', type: 'string' }, + { alias: 'Full Name', name: 'fullname', sql: 'fullname', type: 'string' }, + { + alias: 'Deprecated Agent', + name: 'deprecated_created_by_agent_id', + sql: 'deprecated_created_by_agent_id', + type: 'string', + }, + ], +}; + +const GROUP_LOOKUP_SCHEMA: TableSchema = { + name: 'iss301213_group_lookup', + sql: 'select * from iss301213_group_lookup', + measures: [], + dimensions: [ + { alias: 'ID', name: 'id', sql: 'id', type: 'string' }, + { alias: 'Name', name: 'name', sql: 'name', type: 'string' }, + { + alias: 'Deprecated Agent', + name: 'deprecated_created_by_agent_id', + sql: 'deprecated_created_by_agent_id', + type: 'string', + }, + ], +}; + +const QUERY: Query = { + measures: ['iss301213_tickets.count'], + dimensions: [ + 'iss301213_tickets.id', + 'iss301213_tickets.owner_id', + 'iss301213_tickets.group_id', + ], + order: { 'iss301213_tickets.id': 'asc' }, +}; + +const COLUMN_PROJECTIONS = [ + 'iss301213_tickets.id', + 'iss301213_tickets.owner_id', + 'iss301213_tickets.group_id', + 'iss301213_tickets.count', +]; + +// cubeQueryToSQLWithResolution mutates the resolutionConfig it receives +// (it rewrites columnConfig names to safe keys in place), so each test must +// start from a fresh copy. +const buildResolutionConfig = (): ResolutionConfig => ({ + columnConfigs: [ + { + name: 'iss301213_tickets.owner_id', + type: 'string' as const, + source: 'iss301213_owner_lookup', + joinColumn: 'id', + resolutionColumns: ['fullname'], + }, + { + name: 'iss301213_tickets.group_id', + type: 'string' as const, + source: 'iss301213_group_lookup', + joinColumn: 'id', + resolutionColumns: ['name'], + }, + ], + tableSchemas: [OWNER_LOOKUP_SCHEMA, GROUP_LOOKUP_SCHEMA], +}); + +describe('ISS-301213 - shared lookup column ambiguity on CSV export', () => { + beforeAll(async () => { + await duckdbExec(CREATE_BASE_TABLE); + await duckdbExec(BASE_DATA); + await duckdbExec(CREATE_OWNER_LOOKUP); + await duckdbExec(OWNER_LOOKUP_DATA); + await duckdbExec(CREATE_GROUP_LOOKUP); + await duckdbExec(GROUP_LOOKUP_DATA); + }); + + afterAll(async () => { + await duckdbExec('DROP TABLE IF EXISTS iss301213_tickets'); + await duckdbExec('DROP TABLE IF EXISTS iss301213_owner_lookup'); + await duckdbExec('DROP TABLE IF EXISTS iss301213_group_lookup'); + }); + + it('disambiguates the shared lookup column via table-qualified aliases', async () => { + const sql = await cubeQueryToSQLWithResolution({ + query: QUERY, + tableSchemas: [BASE_SCHEMA], + resolutionConfig: buildResolutionConfig(), + columnProjections: [...COLUMN_PROJECTIONS], + }); + + // The shared lookup column must be projected under a DISTINCT + // table-qualified alias per lookup, so the bare `*` over the join can never + // surface two identically-named columns (the ISS-301213 ambiguity). On the + // buggy code path the column leaked anonymously through `SELECT * FROM + // (select * from )`, so these qualified aliases were absent. + expect(sql).toContain( + 'iss301213_tickets__owner_id__deprecated_created_by_agent_id' + ); + expect(sql).toContain( + 'iss301213_tickets__group_id__deprecated_created_by_agent_id' + ); + + // Each lookup must be joined through its qualified join-column alias rather + // than a bare `.id`, confirming the lookup SQL was narrowed + qualified. + expect(sql).toContain( + '"iss301213_tickets__owner_id__id"' + ); + expect(sql).toContain( + '"iss301213_tickets__group_id__id"' + ); + + // The lookup subqueries no longer pass columns through by bare physical + // name: the shared column only ever appears qualified, never as a standalone + // unqualified output identifier. + const bareLeak = /(?:SELECT|,)\s+deprecated_created_by_agent_id\b/; + expect(bareLeak.test(sql)).toBe(false); + }); + + it('COPYs to CSV preserving resolved values + row count', async () => { + const sql = await cubeQueryToSQLWithResolution({ + query: QUERY, + tableSchemas: [BASE_SCHEMA], + resolutionConfig: buildResolutionConfig(), + columnProjections: [...COLUMN_PROJECTIONS], + }); + + const csvPath = '/tmp/test_iss301213_shared_column.csv'; + await expect( + duckdbExec(`COPY (${sql}) TO '${csvPath}' (HEADER, DELIMITER ',')`) + ).resolves.not.toThrow(); + + const result = (await duckdbExec( + `SELECT * FROM read_csv_auto('${csvPath}')` + )) as any[]; + + expect(result.length).toBe(3); + expect(Number(result[0].ID)).toBe(1); + // Resolved scalar values land under the original aliases. + expect(result[0]['Owner']).toBe('Alice'); + expect(result[0]['Group']).toBe('Group One'); + }); +}); diff --git a/meerkat-node/src/__tests__/resolution.spec.ts b/meerkat-node/src/__tests__/resolution.spec.ts index f4deff76..45e571d7 100644 --- a/meerkat-node/src/__tests__/resolution.spec.ts +++ b/meerkat-node/src/__tests__/resolution.spec.ts @@ -281,26 +281,7 @@ describe('Resolution Tests', () => { }); console.info(`SQL: `, sql); const expectedSQL = ` - select * exclude(__row_id) - from (SELECT "__row_id", - "Part ID 1", - "Random Column", - "Work ID - Display ID", - "Work ID - Title", - "Part ID 2" - FROM (SELECT __base_query."__row_id" AS "__row_id", - __base_query."base_table__part_id_1__display_id" AS "Part ID 1", - __base_query."base_table__random_column" AS "Random Column", - __base_query."base_table__work_id__display_id" AS "Work ID - Display ID", - __base_query."base_table__work_id__title" AS "Work ID - Title", - __base_query."base_table__part_id_2__display_id" AS "Part ID 2", * - FROM (SELECT MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , - MAX(__base_query."base_table__random_column") AS "base_table__random_column" , - MAX(__base_query."base_table__work_id__display_id") AS "base_table__work_id__display_id" , - MAX(__base_query."base_table__work_id__title") AS "base_table__work_id__title" , - MAX(__base_query."base_table__part_id_2__display_id") AS "base_table__part_id_2__display_id" , - "__row_id" - FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1__display_id", "base_table__random_column", "base_table__work_id__display_id", "base_table__work_id__title", "base_table__part_id_2__display_id", "__row_id" FROM (SELECT __base_query."base_table__random_column" AS "base_table__random_column", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1", "base_table__random_column", "base_table__work_id", "base_table__part_id_2", "__row_id" FROM (SELECT __base_query.base_table__part_id_1 AS "base_table__part_id_1", __base_query.base_table__random_column AS "base_table__random_column", __base_query.base_table__work_id AS "base_table__work_id", __base_query.base_table__part_id_2 AS "base_table__part_id_2", row_number() OVER () AS "__row_id", * FROM (SELECT base_table__part_id_1, base_table__random_column, base_table__work_id, base_table__part_id_2 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, base_table.random_column AS base_table__random_column, base_table.work_id AS base_table__work_id, base_table.part_id_2 AS base_table__part_id_2, * FROM (select * from base_table) AS base_table) AS base_table) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1.display_id AS "base_table__part_id_1__display_id", * FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1.id LEFT JOIN (SELECT base_table__work_id.display_id AS "base_table__work_id__display_id", base_table__work_id.title AS "base_table__work_id__title", * FROM (select id, display_id, title from system.dim_issue) AS base_table__work_id) AS base_table__work_id ON __base_query.base_table__work_id = base_table__work_id.id LEFT JOIN (SELECT base_table__part_id_2.display_id AS "base_table__part_id_2__display_id", * FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_2) AS base_table__part_id_2 ON __base_query.base_table__part_id_2 = base_table__part_id_2.id) AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id + select * exclude(__row_id) from (SELECT "__row_id", "Part ID 1", "Random Column", "Work ID - Display ID", "Work ID - Title", "Part ID 2" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", __base_query."base_table__random_column" AS "Random Column", __base_query."base_table__work_id__display_id" AS "Work ID - Display ID", __base_query."base_table__work_id__title" AS "Work ID - Title", __base_query."base_table__part_id_2__display_id" AS "Part ID 2", * FROM (SELECT MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , MAX(__base_query."base_table__random_column") AS "base_table__random_column" , MAX(__base_query."base_table__work_id__display_id") AS "base_table__work_id__display_id" , MAX(__base_query."base_table__work_id__title") AS "base_table__work_id__title" , MAX(__base_query."base_table__part_id_2__display_id") AS "base_table__part_id_2__display_id" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1__display_id", "base_table__random_column", "base_table__work_id__display_id", "base_table__work_id__title", "base_table__part_id_2__display_id", "__row_id" FROM (SELECT __base_query."base_table__random_column" AS "base_table__random_column", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1", "base_table__random_column", "base_table__work_id", "base_table__part_id_2", "__row_id" FROM (SELECT __base_query.base_table__part_id_1 AS "base_table__part_id_1", __base_query.base_table__random_column AS "base_table__random_column", __base_query.base_table__work_id AS "base_table__work_id", __base_query.base_table__part_id_2 AS "base_table__part_id_2", row_number() OVER () AS "__row_id", * FROM (SELECT base_table__part_id_1, base_table__random_column, base_table__work_id, base_table__part_id_2 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, base_table.random_column AS base_table__random_column, base_table.work_id AS base_table__work_id, base_table.part_id_2 AS base_table__part_id_2, * FROM (select * from base_table) AS base_table) AS base_table) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1."base_table__part_id_1__display_id" AS "base_table__part_id_1__display_id", * FROM (SELECT base_table__part_id_1."id" AS "base_table__part_id_1__id", base_table__part_id_1."display_id" AS "base_table__part_id_1__display_id" FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1."base_table__part_id_1__id" LEFT JOIN (SELECT base_table__work_id."base_table__work_id__display_id" AS "base_table__work_id__display_id", base_table__work_id."base_table__work_id__title" AS "base_table__work_id__title", * FROM (SELECT base_table__work_id."id" AS "base_table__work_id__id", base_table__work_id."display_id" AS "base_table__work_id__display_id", base_table__work_id."title" AS "base_table__work_id__title" FROM (select id, display_id, title from system.dim_issue) AS base_table__work_id) AS base_table__work_id) AS base_table__work_id ON __base_query.base_table__work_id = base_table__work_id."base_table__work_id__id" LEFT JOIN (SELECT base_table__part_id_2."base_table__part_id_2__display_id" AS "base_table__part_id_2__display_id", * FROM (SELECT base_table__part_id_2."id" AS "base_table__part_id_2__id", base_table__part_id_2."display_id" AS "base_table__part_id_2__display_id" FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_2) AS base_table__part_id_2) AS base_table__part_id_2 ON __base_query.base_table__part_id_2 = base_table__part_id_2."base_table__part_id_2__id") AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id `; expect(sql.replace(/\s+/g, ' ').trim()).toBe( expectedSQL.replace(/\s+/g, ' ').trim() @@ -330,7 +311,7 @@ describe('Resolution Tests', () => { }); console.info(`SQL: `, sql); const expectedSQL = ` - select * exclude(__row_id) from (SELECT "__row_id", "Part ID 1", "Count" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", __base_query."base_table__count" AS "Count", * FROM (SELECT MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , MAX(__base_query."base_table__count") AS "base_table__count" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1__display_id", "base_table__count", "__row_id" FROM (SELECT __base_query."base_table__count" AS "base_table__count", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1", "base_table__count", "__row_id" FROM (SELECT __base_query.base_table__part_id_1 AS "base_table__part_id_1", __base_query.base_table__count AS "base_table__count", row_number() OVER () AS "__row_id", * FROM (SELECT count(*) AS base_table__count , base_table__part_id_1 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, * FROM (select * from base_table) AS base_table) AS base_table GROUP BY base_table__part_id_1) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1.display_id AS "base_table__part_id_1__display_id", * FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1.id) AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id + select * exclude(__row_id) from (SELECT "__row_id", "Part ID 1", "Count" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", __base_query."base_table__count" AS "Count", * FROM (SELECT MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , MAX(__base_query."base_table__count") AS "base_table__count" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1__display_id", "base_table__count", "__row_id" FROM (SELECT __base_query."base_table__count" AS "base_table__count", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1", "base_table__count", "__row_id" FROM (SELECT __base_query.base_table__part_id_1 AS "base_table__part_id_1", __base_query.base_table__count AS "base_table__count", row_number() OVER () AS "__row_id", * FROM (SELECT count(*) AS base_table__count , base_table__part_id_1 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, * FROM (select * from base_table) AS base_table) AS base_table GROUP BY base_table__part_id_1) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1."base_table__part_id_1__display_id" AS "base_table__part_id_1__display_id", * FROM (SELECT base_table__part_id_1."id" AS "base_table__part_id_1__id", base_table__part_id_1."display_id" AS "base_table__part_id_1__display_id" FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1."base_table__part_id_1__id") AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id `; expect(sql.replace(/\s+/g, ' ').trim()).toBe( expectedSQL.replace(/\s+/g, ' ').trim() @@ -372,7 +353,7 @@ describe('Resolution Tests', () => { }); console.info(`SQL: `, sql); const expectedSQL = ` - select * exclude(__row_id) from (SELECT "__row_id", "Part ID 1", "Random Column", "Part ID 2" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", __base_query."base_table__random_column" AS "Random Column", __base_query."base_table__part_id_2__display_id" AS "Part ID 2", * FROM (SELECT MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , MAX(__base_query."base_table__random_column") AS "base_table__random_column" , MAX(__base_query."base_table__part_id_2__display_id") AS "base_table__part_id_2__display_id" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1__display_id", "base_table__random_column", "base_table__part_id_2__display_id", "__row_id" FROM (SELECT __base_query."base_table__random_column" AS "base_table__random_column", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1", "base_table__random_column", "base_table__part_id_2", "__row_id" FROM (SELECT __base_query.base_table__part_id_1 AS "base_table__part_id_1", __base_query.base_table__random_column AS "base_table__random_column", __base_query.base_table__part_id_2 AS "base_table__part_id_2", row_number() OVER () AS "__row_id", * FROM (SELECT base_table__part_id_1, base_table__random_column, base_table__part_id_2 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, base_table.random_column AS base_table__random_column, base_table.part_id_2 AS base_table__part_id_2, * FROM (select * from base_table) AS base_table) AS base_table) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1.display_id AS "base_table__part_id_1__display_id", * FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1.id LEFT JOIN (SELECT base_table__part_id_2.display_id AS "base_table__part_id_2__display_id", * FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_2) AS base_table__part_id_2 ON __base_query.base_table__part_id_2 = base_table__part_id_2.id) AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id + select * exclude(__row_id) from (SELECT "__row_id", "Part ID 1", "Random Column", "Part ID 2" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", __base_query."base_table__random_column" AS "Random Column", __base_query."base_table__part_id_2__display_id" AS "Part ID 2", * FROM (SELECT MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , MAX(__base_query."base_table__random_column") AS "base_table__random_column" , MAX(__base_query."base_table__part_id_2__display_id") AS "base_table__part_id_2__display_id" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1__display_id", "base_table__random_column", "base_table__part_id_2__display_id", "__row_id" FROM (SELECT __base_query."base_table__random_column" AS "base_table__random_column", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__part_id_1", "base_table__random_column", "base_table__part_id_2", "__row_id" FROM (SELECT __base_query.base_table__part_id_1 AS "base_table__part_id_1", __base_query.base_table__random_column AS "base_table__random_column", __base_query.base_table__part_id_2 AS "base_table__part_id_2", row_number() OVER () AS "__row_id", * FROM (SELECT base_table__part_id_1, base_table__random_column, base_table__part_id_2 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, base_table.random_column AS base_table__random_column, base_table.part_id_2 AS base_table__part_id_2, * FROM (select * from base_table) AS base_table) AS base_table) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1."base_table__part_id_1__display_id" AS "base_table__part_id_1__display_id", * FROM (SELECT base_table__part_id_1."id" AS "base_table__part_id_1__id", base_table__part_id_1."display_id" AS "base_table__part_id_1__display_id" FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1."base_table__part_id_1__id" LEFT JOIN (SELECT base_table__part_id_2."base_table__part_id_2__display_id" AS "base_table__part_id_2__display_id", * FROM (SELECT base_table__part_id_2."id" AS "base_table__part_id_2__id", base_table__part_id_2."display_id" AS "base_table__part_id_2__display_id" FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_2) AS base_table__part_id_2) AS base_table__part_id_2 ON __base_query.base_table__part_id_2 = base_table__part_id_2."base_table__part_id_2__id") AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id `; expect(sql.replace(/\s+/g, ' ').trim()).toBe( expectedSQL.replace(/\s+/g, ' ').trim() @@ -409,7 +390,7 @@ describe('Resolution Tests', () => { }); console.info(`SQL: `, sql); const expectedSQL = ` - select * exclude(__row_id) from (SELECT "__row_id", "Random Column", "Part ID 1" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__random_column" AS "Random Column", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", * FROM (SELECT MAX(__base_query."base_table__random_column") AS "base_table__random_column" , MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__random_column", "base_table__part_id_1__display_id", "__row_id" FROM (SELECT __base_query."base_table__random_column" AS "base_table__random_column", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__random_column", "base_table__part_id_1", "__row_id" FROM (SELECT __base_query.base_table__random_column AS "base_table__random_column", __base_query.base_table__part_id_1 AS "base_table__part_id_1", row_number() OVER () AS "__row_id", * FROM (SELECT base_table__part_id_1, base_table__random_column, base_table__work_id, base_table__part_id_2 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, base_table.random_column AS base_table__random_column, base_table.work_id AS base_table__work_id, base_table.part_id_2 AS base_table__part_id_2, * FROM (select * from base_table) AS base_table) AS base_table) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1.display_id AS "base_table__part_id_1__display_id", * FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1.id) AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id + select * exclude(__row_id) from (SELECT "__row_id", "Random Column", "Part ID 1" FROM (SELECT __base_query."__row_id" AS "__row_id", __base_query."base_table__random_column" AS "Random Column", __base_query."base_table__part_id_1__display_id" AS "Part ID 1", * FROM (SELECT MAX(__base_query."base_table__random_column") AS "base_table__random_column" , MAX(__base_query."base_table__part_id_1__display_id") AS "base_table__part_id_1__display_id" , "__row_id" FROM (SELECT __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__random_column", "base_table__part_id_1__display_id", "__row_id" FROM (SELECT __base_query."base_table__random_column" AS "base_table__random_column", __base_query."__row_id" AS "__row_id", * FROM (SELECT "base_table__random_column", "base_table__part_id_1", "__row_id" FROM (SELECT __base_query.base_table__random_column AS "base_table__random_column", __base_query.base_table__part_id_1 AS "base_table__part_id_1", row_number() OVER () AS "__row_id", * FROM (SELECT base_table__part_id_1, base_table__random_column, base_table__work_id, base_table__part_id_2 FROM (SELECT base_table.part_id_1 AS base_table__part_id_1, base_table.random_column AS base_table__random_column, base_table.work_id AS base_table__work_id, base_table.part_id_2 AS base_table__part_id_2, * FROM (select * from base_table) AS base_table) AS base_table) AS __base_query) AS __base_query) AS __base_query LEFT JOIN (SELECT base_table__part_id_1."base_table__part_id_1__display_id" AS "base_table__part_id_1__display_id", * FROM (SELECT base_table__part_id_1."id" AS "base_table__part_id_1__id", base_table__part_id_1."display_id" AS "base_table__part_id_1__display_id" FROM (select id, display_id from system.dim_feature UNION ALL select id, display_id from system.dim_product) AS base_table__part_id_1) AS base_table__part_id_1) AS base_table__part_id_1 ON __base_query.base_table__part_id_1 = base_table__part_id_1."base_table__part_id_1__id") AS MEERKAT_GENERATED_TABLE) AS __base_query) AS __base_query GROUP BY __row_id) AS __base_query) AS __base_query) order by __row_id `; expect(sql.replace(/\s+/g, ' ').trim()).toBe( expectedSQL.replace(/\s+/g, ' ').trim()