Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,21 @@
import {
BASE_DATA_SOURCE_NAME,
ContextParams,
getAggregatedSql as coreGetAggregatedSql,
getResolvedTableSchema as coreGetResolvedTableSchema,
getUnnestTableSchema as coreGetUnnestTableSchema,
createBaseTableSchema,
generateResolutionJoinPaths,
generateResolutionSchemas,
generateResolvedDimensions,
Dimension,
generateRowNumberSql,
memberKeyToSafeKey,
Query,
ResolutionConfig,
ROW_ID_DIMENSION_NAME,
shouldSkipResolution,
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';

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

if (resolutionConfig.columnConfigs.length === 0) {
// If no resolution is needed, return the base SQL.
// Check if resolution should be skipped
if (shouldSkipResolution(resolutionConfig, query, columnProjections)) {
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);
}
);

const baseSchema: TableSchema = createBaseTableSchema(
baseSql,
tableSchemas,
resolutionConfig,
query.measures,
query.dimensions
);

const resolutionSchemas: TableSchema[] = generateResolutionSchemas(
const rowIdDimension: Dimension = {
name: ROW_ID_DIMENSION_NAME,
sql: generateRowNumberSql(
query,
baseSchema.dimensions,
BASE_DATA_SOURCE_NAME
),
type: 'number',
alias: ROW_ID_DIMENSION_NAME,
};
baseSchema.dimensions.push(rowIdDimension);
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 coreGetUnnestTableSchema({
baseTableSchema: baseSchema,
resolutionConfig,
tableSchemas
);
contextParams,
cubeQueryToSQL: async (params) => cubeQueryToSQL({ connection, ...params }),
});

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 coreGetResolvedTableSchema({
baseTableSchema: unnestTableSchema,
resolutionConfig,
contextParams,
columnProjections,
cubeQueryToSQL: async (params) => cubeQueryToSQL({ connection, ...params }),
});

// Re-aggregate to reverse the unnest
const aggregatedSql = await coreGetAggregatedSql({
resolvedTableSchema,
resolutionConfig,
contextParams,
cubeQueryToSQL: async (params) => cubeQueryToSQL({ connection, ...params }),
});

return sql;
return aggregatedSql;
};
2 changes: 1 addition & 1 deletion meerkat-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@devrev/meerkat-core",
"version": "0.0.105",
"version": "0.0.106",
"dependencies": {
"tslib": "^2.3.0"
},
Expand Down
1 change: 1 addition & 0 deletions meerkat-core/src/constants/exports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const ROW_ID_DIMENSION_NAME = '__row_id';
Original file line number Diff line number Diff line change
@@ -1,130 +1,142 @@
import { Dimension } from '../../types/cube-types/table';
import { isArrayTypeMember } from '../../utils/is-array-member-type';
import { arrayFieldUnNestModifier, DimensionModifier, getModifiedSqlExpression, MODIFIERS, shouldUnnest } from "../sql-expression-modifiers";
import {
arrayFieldUnNestModifier,
shouldUnnest,
} from '../modifiers/array-unnest-modifier';
import {
getModifiedSqlExpression,
MODIFIERS,
} from '../sql-expression-modifiers';
import { DimensionModifier } from '../types';

jest.mock("../../utils/is-array-member-type", () => {
jest.mock('../../utils/is-array-member-type', () => {
return {
isArrayTypeMember: jest.fn()
}
isArrayTypeMember: jest.fn(),
};
});

const QUERY = {
measures: ["test_measure"],
dimensions: ["test_dimension"]
}
measures: ['test_measure'],
dimensions: ['test_dimension'],
};

describe("Dimension Modifier", () => {
describe("arrayFieldUnNestModifier", () => {
it("should return the correct unnested SQL expression", () => {
describe('Dimension Modifier', () => {
describe('arrayFieldUnNestModifier', () => {
it('should return the correct unnested SQL expression', () => {
const modifier: DimensionModifier = {
sqlExpression: "some_array_field",
sqlExpression: 'some_array_field',
dimension: {} as Dimension,
key: "test_key",
query: QUERY
key: 'test_key',
query: QUERY,
};
expect(arrayFieldUnNestModifier(modifier)).toBe("array[unnest(some_array_field)]");
expect(arrayFieldUnNestModifier(modifier)).toBe(
'array[unnest(some_array_field)]'
);
});
});

describe("shouldUnnest", () => {
it("should return true when dimension is array type and has shouldUnnestGroupBy modifier", () => {
describe('shouldUnnest', () => {
it('should return true when dimension is array type and has shouldUnnestGroupBy modifier', () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(true);
const modifier: DimensionModifier = {
sqlExpression: "some_expression",
dimension: {
type: "array",
modifier: { shouldUnnestGroupBy: true }
sqlExpression: 'some_expression',
dimension: {
type: 'array',
modifier: { shouldUnnestGroupBy: true },
} as Dimension,
key: "test_key",
query: QUERY
key: 'test_key',
query: QUERY,
};
expect(shouldUnnest(modifier)).toBe(true);
});

it("should return false when dimension is not array type", () => {
it('should return false when dimension is not array type', () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(false);
const modifier: DimensionModifier = {
sqlExpression: "some_expression",
dimension: {
type: "string",
modifier: { shouldUnnestGroupBy: true }
sqlExpression: 'some_expression',
dimension: {
type: 'string',
modifier: { shouldUnnestGroupBy: true },
} as Dimension,
key: "test_key",
query: QUERY
key: 'test_key',
query: QUERY,
};
expect(shouldUnnest(modifier)).toBe(false);
});

it("should return false when dimension doesn't have shouldUnnestGroupBy modifier", () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(true);
const modifier: DimensionModifier = {
sqlExpression: "some_expression",
dimension: {
type: "array",
modifier: {}
sqlExpression: 'some_expression',
dimension: {
type: 'array',
modifier: {},
} as Dimension,
key: 'test_key',
query: QUERY,
};
expect(shouldUnnest(modifier)).toBe(false);
});
it('should return false when dimension when modifier undefined', () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(true);
const modifier: DimensionModifier = {
sqlExpression: 'some_expression',
dimension: {
type: 'array',
} as Dimension,
key: "test_key",
query: QUERY
key: 'test_key',
query: QUERY,
};
expect(shouldUnnest(modifier)).toBe(false);
});
it("should return false when dimension when modifier undefined", () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(true);
const modifier: DimensionModifier = {
sqlExpression: "some_expression",
dimension: {
type: "array",
} as Dimension,
key: "test_key",
query: QUERY
};
expect(shouldUnnest(modifier)).toBe(false);
});
});

describe("getModifiedSqlExpression", () => {
it("should not modify if no modifiers passed", () => {
describe('getModifiedSqlExpression', () => {
it('should not modify if no modifiers passed', () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(true);
const input = {
sqlExpression: "array_field",
sqlExpression: 'array_field',
dimension: {
type: "array",
modifier: { shouldUnnestGroupBy: true }
type: 'array',
modifier: { shouldUnnestGroupBy: true },
} as Dimension,
query: QUERY,
key: "test_key",
modifiers: []
key: 'test_key',
modifiers: [],
};
expect(getModifiedSqlExpression(input)).toBe("array_field");
expect(getModifiedSqlExpression(input)).toBe('array_field');
});
it("should apply the modifier when conditions are met", () => {
it('should apply the modifier when conditions are met', () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(true);
const input = {
sqlExpression: "array_field",
sqlExpression: 'array_field',
dimension: {
type: "array",
modifier: { shouldUnnestGroupBy: true }
type: 'array',
modifier: { shouldUnnestGroupBy: true },
} as Dimension,
query: QUERY,
key: "test_key",
modifiers: MODIFIERS
key: 'test_key',
modifiers: MODIFIERS,
};
expect(getModifiedSqlExpression(input)).toBe("array[unnest(array_field)]");
expect(getModifiedSqlExpression(input)).toBe(
'array[unnest(array_field)]'
);
});

it("should not apply the modifier when conditions are not met", () => {
it('should not apply the modifier when conditions are not met', () => {
(isArrayTypeMember as jest.Mock).mockReturnValue(false);
const input = {
sqlExpression: "non_array_field",
sqlExpression: 'non_array_field',
dimension: {
type: "string",
modifier: {}
type: 'string',
modifier: {},
} as Dimension,
query: QUERY,
key: "test_key",
modifiers: MODIFIERS
key: 'test_key',
modifiers: MODIFIERS,
};
expect(getModifiedSqlExpression(input)).toBe("non_array_field");
expect(getModifiedSqlExpression(input)).toBe('non_array_field');
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
findInDimensionSchema,
findInMeasureSchema,
} from '../utils/find-in-table-schema';
import { getModifiedSqlExpression, Modifier } from './sql-expression-modifiers';
import { getModifiedSqlExpression } from './sql-expression-modifiers';
import { Modifier } from './types';

export const getDimensionProjection = ({
key,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export { getWrappedBaseQueryWithProjections } from './get-wrapped-base-query-with-projections';
export {
getModifiedSqlExpression,
MODIFIERS,
} from './sql-expression-modifiers';
export type { DimensionModifier, Modifier } from './types';
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { isArrayTypeMember } from '../../utils/is-array-member-type';
import { DimensionModifier, Modifier } from '../types';

export const arrayFlattenModifier = ({
sqlExpression,
}: DimensionModifier): string => {
// Ensure NULL or empty arrays produce at least one row with NULL value
// This prevents rows from being dropped when arrays are NULL or empty
// COALESCE handles NULL, and len() = 0 check handles empty arrays []
return `unnest(CASE WHEN ${sqlExpression} IS NULL OR len(COALESCE(${sqlExpression}, [])) = 0 THEN [NULL] ELSE ${sqlExpression} END)`;
};

export const shouldFlattenArray = ({
dimension,
}: DimensionModifier): boolean => {
const isArrayType = isArrayTypeMember(dimension.type);
const shouldFlattenArray = dimension.modifier?.shouldFlattenArray;
return !!(isArrayType && shouldFlattenArray);
};

export const arrayFlattenModifierConfig: Modifier = {
name: 'shouldFlattenArray',
matcher: shouldFlattenArray,
modifier: arrayFlattenModifier,
};
Loading