Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
ced5484
phase 1 changes
senthilb-devrev Oct 29, 2025
d065d22
phase 2 changes
senthilb-devrev Oct 29, 2025
c1f0d40
phase 3 complete and working
senthilb-devrev Oct 29, 2025
0396184
working with array + scalar resolution
senthilb-devrev Oct 30, 2025
11f30ec
working with only a scalar resolution field
senthilb-devrev Oct 30, 2025
c24c8a1
updating in meerkat browser
senthilb-devrev Oct 30, 2025
13b7f72
re-using dimensions instead of re-creating it
senthilb-devrev Oct 30, 2025
32f0d90
minor refactoring
senthilb-devrev Oct 30, 2025
c00155e
minor update
senthilb-devrev Oct 30, 2025
7600c52
unnest working as expected
senthilb-devrev Oct 31, 2025
75889b0
working properly
senthilb-devrev Nov 1, 2025
dffc096
working properly
senthilb-devrev Nov 3, 2025
4f063ff
working again
senthilb-devrev Nov 3, 2025
94a1e10
moving everything to browser too
senthilb-devrev Nov 3, 2025
2f077b1
mionr refactoring working
senthilb-devrev Nov 9, 2025
c273540
final changes after testing and copy pasting same code from browser i…
senthilb-devrev Nov 9, 2025
06662f5
minor refactoring
senthilb-devrev Nov 9, 2025
9bb01ad
udpated tests for resolution.ts
senthilb-devrev Nov 10, 2025
2b7a570
adding a test
senthilb-devrev Nov 10, 2025
d2c4fba
Merge remote-tracking branch 'refs/remotes/origin/main'
senthilb-devrev Nov 10, 2025
95beab1
ensuring we are having the same order by using row_id
senthilb-devrev Nov 10, 2025
4ae0e4e
final ordering changes for row number
senthilb-devrev Nov 10, 2025
086736b
minor
senthilb-devrev Nov 10, 2025
2f27183
final tests
senthilb-devrev Nov 10, 2025
6f27e74
fixed final tests
senthilb-devrev Nov 10, 2025
804383c
fixing test
senthilb-devrev Nov 10, 2025
61d6bde
cr comments
senthilb-devrev Nov 11, 2025
34ae940
moving code into dependent files for better readability
senthilb-devrev Nov 11, 2025
b72c658
moving to use a merged flow
senthilb-devrev Nov 11, 2025
30f63dc
changes after testing
senthilb-devrev Nov 11, 2025
1cb3e1b
fixing lint error
senthilb-devrev Nov 11, 2025
c8c7f61
cr comments
senthilb-devrev Nov 12, 2025
b236c78
changing type of resolutionConfig isArrayType
senthilb-devrev Nov 12, 2025
2f350b7
minor updates
senthilb-devrev Nov 12, 2025
46d4884
splitting resolution file into multiple generators
senthilb-devrev Nov 12, 2025
4a32ce4
minor update
senthilb-devrev Nov 12, 2025
aca247b
cr comments
senthilb-devrev Nov 12, 2025
5b33f51
updating package version for meerkat-core
senthilb-devrev Nov 12, 2025
a6351cc
added a todo
senthilb-devrev Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meerkat-browser/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devrev/meerkat-browser",
"version": "0.0.105",
"version": "0.0.106",
"dependencies": {
"tslib": "^2.3.0",
"@devrev/meerkat-core": "*",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import {
BASE_DATA_SOURCE_NAME,
ContextParams,
createBaseTableSchema,
generateResolutionJoinPaths,
generateResolutionSchemas,
generateResolvedDimensions,
Dimension,
generateRowNumberSql,
memberKeyToSafeKey,
Query,
ResolutionConfig,
ROW_ID_DIMENSION_NAME,
TableSchema,
} from '@devrev/meerkat-core';
import { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm';
import {
cubeQueryToSQL,
CubeQueryToSQLParams,
} from '../browser-cube-to-sql/browser-cube-to-sql';
import { cubeQueryToSQL } from '../browser-cube-to-sql/browser-cube-to-sql';
import { getAggregatedSql } from './steps/aggregation-step';
import { getResolvedTableSchema } from './steps/resolution-step';
import { getUnnestTableSchema } from './steps/unnest-step';

export interface CubeQueryToSQLWithResolutionParams {
connection: AsyncDuckDBConnection;
Expand All @@ -38,39 +40,102 @@ export const cubeQueryToSQLWithResolution = async ({
contextParams,
});

if (resolutionConfig.columnConfigs.length === 0) {
// If no resolution is needed, return the base SQL.
// We have columnProjections check here to ensure that, we are using the same
// order in the final query
if (
resolutionConfig.columnConfigs.length === 0 &&
columnProjections?.length === 0
) {
return baseSql;
}

// Create a table schema for the base query.
const baseTable: TableSchema = createBaseTableSchema(
if (!columnProjections) {
columnProjections = [...(query.dimensions || []), ...query.measures];
}
// This is to ensure that, only the column projection columns
// are being resolved and other definitions are ignored.
resolutionConfig.columnConfigs = resolutionConfig.columnConfigs.filter(
(config) => {
return columnProjections?.includes(config.name);
}
);
return getCubeQueryToSQLWithResolution({
connection,
baseSql,
query,
tableSchemas,
resolutionConfig,
columnProjections,
contextParams,
});
};

const getCubeQueryToSQLWithResolution = async ({
connection,
baseSql,
query,
tableSchemas,
resolutionConfig,
columnProjections,
contextParams,
}: {
connection: AsyncDuckDBConnection;
baseSql: string;
query: Query;
tableSchemas: TableSchema[];
resolutionConfig: ResolutionConfig;
columnProjections: string[];
contextParams?: ContextParams;
}): Promise<string> => {
const baseSchema: TableSchema = createBaseTableSchema(
baseSql,
tableSchemas,
resolutionConfig,
query.measures,
query.dimensions
);

const resolutionSchemas: TableSchema[] = generateResolutionSchemas(
baseSchema.dimensions.push({
name: ROW_ID_DIMENSION_NAME,
sql: generateRowNumberSql(
query,
baseSchema.dimensions,
BASE_DATA_SOURCE_NAME
),
type: 'number',
alias: ROW_ID_DIMENSION_NAME,
} as Dimension);
columnProjections.push(ROW_ID_DIMENSION_NAME);

// Doing this because we need to use the original name of the column in the base table schema.
resolutionConfig.columnConfigs.forEach((config) => {
config.name = memberKeyToSafeKey(config.name);
});

// Generate SQL with row_id and unnested arrays
const unnestTableSchema = await getUnnestTableSchema({
connection,
baseTableSchema: baseSchema,
resolutionConfig,
tableSchemas
);
contextParams,
});

const resolveParams: CubeQueryToSQLParams = {
connection: connection,
query: {
measures: [],
dimensions: generateResolvedDimensions(
query,
resolutionConfig,
columnProjections
),
joinPaths: generateResolutionJoinPaths(resolutionConfig, tableSchemas),
},
tableSchemas: [baseTable, ...resolutionSchemas],
};
const sql = await cubeQueryToSQL(resolveParams);
// Apply resolution (join with lookup tables)
const resolvedTableSchema = await getResolvedTableSchema({
connection,
baseTableSchema: unnestTableSchema,
resolutionConfig,
contextParams,
columnProjections,
});

// Re-aggregate to reverse the unnest
const aggregatedSql = await getAggregatedSql({
connection,
resolvedTableSchema,
resolutionConfig,
contextParams,
});

return sql;
return aggregatedSql;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import {
ContextParams,
getArrayTypeResolutionColumnConfigs,
getNamespacedKey,
Measure,
MEERKAT_OUTPUT_DELIMITER,
ResolutionConfig,
ROW_ID_DIMENSION_NAME,
TableSchema,
wrapWithRowIdOrderingAndExclusion,
} from '@devrev/meerkat-core';
import { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm';
import { cubeQueryToSQL } from '../../browser-cube-to-sql/browser-cube-to-sql';

/**
* Re-aggregate to reverse the unnest
*
* This function:
* 1. Groups by row_id
* 2. Uses MAX for non-array columns (they're duplicated)
* 3. Uses ARRAY_AGG for resolved array columns
*
* @param resolvedTableSchema - Schema from Phase 2 (contains all column info)
* @param resolutionConfig - Resolution configuration
* @param contextParams - Optional context parameters
* @returns Final SQL with arrays containing resolved values
*/
export const getAggregatedSql = async ({
connection,
resolvedTableSchema,
resolutionConfig,
contextParams,
}: {
connection: AsyncDuckDBConnection;
resolvedTableSchema: TableSchema;
resolutionConfig: ResolutionConfig;
contextParams?: ContextParams;
}): Promise<string> => {
const aggregationBaseTableSchema: TableSchema = resolvedTableSchema;

// Identify which columns need ARRAY_AGG vs MAX
const arrayColumns = getArrayTypeResolutionColumnConfigs(resolutionConfig);
const baseTableName = aggregationBaseTableSchema.name;

const isResolvedArrayColumn = (dimName: string) => {
return arrayColumns.some((arrayCol) => {
return dimName.includes(`${arrayCol.name}${MEERKAT_OUTPUT_DELIMITER}`);
});
};

// Create aggregation measures with proper aggregation functions
// Get row_id dimension for GROUP BY
const rowIdDimension = aggregationBaseTableSchema.dimensions.find(
(d) => d.name === ROW_ID_DIMENSION_NAME
);

if (!rowIdDimension) {
throw new Error('Row id dimension not found');
}
// Create measures with MAX or ARRAY_AGG based on column type
const aggregationMeasures: Measure[] = [];

aggregationBaseTableSchema.dimensions
.filter((dim) => dim.name !== rowIdDimension?.name)
.forEach((dim) => {
const isArrayColumn = isResolvedArrayColumn(dim.name);

// The dimension's sql field already has the correct reference (e.g., __resolved_query."__row_id")
// We just need to wrap it in the aggregation function
const columnRef =
dim.sql || `${baseTableName}."${dim.alias || dim.name}"`;

// Use ARRAY_AGG for resolved array columns, MAX for others
// Filter out null values for ARRAY_AGG using FILTER clause
const aggregationFn = isArrayColumn
? `COALESCE(ARRAY_AGG(DISTINCT ${columnRef}) FILTER (WHERE ${columnRef} IS NOT NULL), [])`
: `MAX(${columnRef})`;

aggregationMeasures.push({
name: dim.name,
sql: aggregationFn,
type: dim.type,
alias: dim.alias,
});
});

// Update the schema with aggregation measures
const schemaWithAggregation: TableSchema = {
...aggregationBaseTableSchema,
measures: aggregationMeasures,
dimensions: [rowIdDimension],
};

// Generate the final SQL
const aggregatedSql = await cubeQueryToSQL({
connection,
query: {
measures: aggregationMeasures.map((m) =>
getNamespacedKey(baseTableName, m.name)
),
dimensions: rowIdDimension
? [getNamespacedKey(baseTableName, rowIdDimension.name)]
: [],
},
tableSchemas: [schemaWithAggregation],
contextParams,
});

// Order by row_id to maintain consistent ordering before excluding it
return wrapWithRowIdOrderingAndExclusion(
aggregatedSql,
ROW_ID_DIMENSION_NAME
);
};
Loading