diff --git a/meerkat-browser/src/ast/dimension.ts b/meerkat-browser/src/ast/dimension.ts new file mode 100644 index 00000000..8e42b539 --- /dev/null +++ b/meerkat-browser/src/ast/dimension.ts @@ -0,0 +1,32 @@ +import { validateDimension } from '@devrev/meerkat-core'; +import { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm'; +import { parseQueryToAST } from './query-to-ast'; +import { getAvailableFunctions, isParseError } from './utils'; + +/** + * Validates the query can be used as a dimension by parsing it to an AST and checking its structure + * @param connection - DuckDB connection instance + * @param query - The query string to validate + * @returns Promise - Whether the dimension is valid + */ +export const validateDimensionQuery = async ({ + connection, + query, + validFunctions, +}: { + connection: AsyncDuckDBConnection; + query: string; + validFunctions?: string[]; +}): Promise => { + const parsedSerialization = await parseQueryToAST(query, connection); + + if (isParseError(parsedSerialization)) { + throw new Error(parsedSerialization.error_message ?? 'Unknown error'); + } + + // Only fetch valid functions if not provided + const availableFunctions = + validFunctions ?? (await getAvailableFunctions(connection, 'scalar')); + + return validateDimension(parsedSerialization, availableFunctions); +}; diff --git a/meerkat-browser/src/ast/index.ts b/meerkat-browser/src/ast/index.ts new file mode 100644 index 00000000..92301f1b --- /dev/null +++ b/meerkat-browser/src/ast/index.ts @@ -0,0 +1,2 @@ +export * from './dimension'; +export * from './query-to-ast'; diff --git a/meerkat-browser/src/ast/query-to-ast.ts b/meerkat-browser/src/ast/query-to-ast.ts new file mode 100644 index 00000000..f8286c16 --- /dev/null +++ b/meerkat-browser/src/ast/query-to-ast.ts @@ -0,0 +1,29 @@ +import { + astSerializerQuery, + deserializeQuery, + ParsedSerialization, +} from '@devrev/meerkat-core'; +import { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm'; + +/** + * Converts a query to an AST + * @param query - The query string to convert + * @param connection - The DuckDB connection instance + * @returns The AST as a JSON object + */ +export const parseQueryToAST = async ( + query: string, + connection: AsyncDuckDBConnection +): Promise => { + try { + const serializedQuery = astSerializerQuery(query); + const arrowResult = await connection.query(serializedQuery); + + const parsedOutputQuery = arrowResult.toArray().map((row) => row.toJSON()); + const deserializedQuery = deserializeQuery(parsedOutputQuery); + + return JSON.parse(deserializedQuery); + } catch (error) { + throw new Error('Failed to parse query to AST'); + } +}; diff --git a/meerkat-browser/src/ast/utils.ts b/meerkat-browser/src/ast/utils.ts new file mode 100644 index 00000000..ee1ad5d0 --- /dev/null +++ b/meerkat-browser/src/ast/utils.ts @@ -0,0 +1,18 @@ +import { ParsedSerialization } from '@devrev/meerkat-core'; +import { AsyncDuckDBConnection } from '@duckdb/duckdb-wasm'; + +export const isParseError = (data: ParsedSerialization): boolean => { + return !!data.error; +}; + +// Helper function to get available functions from DuckDB based on function type +export const getAvailableFunctions = async ( + connection: AsyncDuckDBConnection, + function_type: 'scalar' | 'aggregate' +): Promise => { + const result = await connection.query( + `SELECT distinct function_name FROM duckdb_functions() WHERE function_type = '${function_type}'` + ); + + return result.toArray().map((row) => row.toJSON().function_name); +}; diff --git a/meerkat-browser/src/index.ts b/meerkat-browser/src/index.ts index 07df6623..878b1e9e 100644 --- a/meerkat-browser/src/index.ts +++ b/meerkat-browser/src/index.ts @@ -1,3 +1,4 @@ export * from './browser-cube-to-sql/browser-cube-to-sql'; export { convertCubeStringToTableSchema }; import { convertCubeStringToTableSchema } from '@devrev/meerkat-core'; +export { validateDimensionQuery } from './ast'; diff --git a/meerkat-core/src/ast-serializer/ast-serializer.ts b/meerkat-core/src/ast-serializer/ast-serializer.ts new file mode 100644 index 00000000..ff167a51 --- /dev/null +++ b/meerkat-core/src/ast-serializer/ast-serializer.ts @@ -0,0 +1,3 @@ +export const astSerializerQuery = (query: string) => { + return `SELECT json_serialize_sql('${query}')`; +}; diff --git a/meerkat-core/src/ast-validator/dimension-validator.spec.ts b/meerkat-core/src/ast-validator/dimension-validator.spec.ts new file mode 100644 index 00000000..228dd20c --- /dev/null +++ b/meerkat-core/src/ast-validator/dimension-validator.spec.ts @@ -0,0 +1,393 @@ +import { + ExpressionType, + ParsedExpression, + QueryNodeType, + ResultModifierType, + TableReferenceType, +} from '../types/duckdb-serialization-types'; +import { ExpressionClass } from '../types/duckdb-serialization-types/serialization/Expression'; +import { AggregateHandling } from '../types/duckdb-serialization-types/serialization/QueryNode'; +import { + validateDimension, + validateExpressionNode, +} from './dimension-validator'; +import { ParsedSerialization } from './types'; + +const EMPTY_VALID_FUNCTIONS = new Set(); +const VALID_FUNCTIONS = new Set(['contains', 'round', 'power']); + +const COLUMN_REF_NODE: ParsedExpression = { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: 'alias', + query_location: 0, + column_names: ['column_name'], +}; + +const INVALID_NODE: ParsedExpression = { + class: ExpressionClass.INVALID, + type: ExpressionType.INVALID, + alias: '', + query_location: 0, +}; + +const PARSED_SERIALIZATION: ParsedSerialization = { + error: false, + statements: [ + { + node: { + type: QueryNodeType.SELECT_NODE, + modifiers: [], + cte_map: { + map: [], + }, + select_list: [COLUMN_REF_NODE], + from_table: { + type: TableReferenceType.BASE_TABLE, + alias: '', + sample: null, + }, + group_expressions: [], + group_sets: [], + aggregate_handling: AggregateHandling.STANDARD_HANDLING, + having: null, + sample: null, + qualify: null, + }, + }, + ], +}; + +describe('validateDimension', () => { + it('should throw error if the statement if there is no statement', () => { + expect(() => + validateDimension( + { + error: false, + statements: [], + }, + [] + ) + ).toThrow('No statement found'); + }); + + it('should throw error if no statement is found', () => { + expect(() => + validateDimension( + { + error: false, + statements: [ + { + node: { + type: QueryNodeType.CTE_NODE, + modifiers: [], + cte_map: { + map: [], + }, + }, + }, + ], + }, + [] + ) + ).toThrow('Statement must be a SELECT node'); + }); + + it('should throw error if select list is not exactly one expression', () => { + expect(() => + validateDimension( + { + error: false, + statements: [ + { + node: { + type: QueryNodeType.SELECT_NODE, + modifiers: [], + cte_map: { + map: [], + }, + select_list: [], + }, + }, + ], + }, + [] + ) + ).toThrow('SELECT must contain exactly one expression'); + }); + + it('should return true if the statement is valid', () => { + expect(validateDimension(PARSED_SERIALIZATION, [])).toBe(true); + }); + + it('should throw error if the expression is invalid', () => { + expect(() => + validateDimension( + { + ...PARSED_SERIALIZATION, + statements: [ + { + ...PARSED_SERIALIZATION.statements[0], + node: { + ...PARSED_SERIALIZATION.statements[0].node, + select_list: [INVALID_NODE], + }, + }, + ], + }, + ['contains'] + ) + ).toThrow('Invalid expression type: INVALID'); + }); +}); + +describe('validateExpressionNode for dimension expressions', () => { + it('should return true for node type COLUMN_REF', () => { + const COLUMN_REF_NODE: ParsedExpression = { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 0, + column_names: ['column_name'], + }; + + expect(validateExpressionNode(COLUMN_REF_NODE, EMPTY_VALID_FUNCTIONS)).toBe( + true + ); + }); + + it('should return true for node type COLUMN_REF with alias', () => { + expect(validateExpressionNode(COLUMN_REF_NODE, EMPTY_VALID_FUNCTIONS)).toBe( + true + ); + }); + + it('should return true for node type VALUE_CONSTANT', () => { + const VALUE_CONSTANT_NODE: ParsedExpression = { + class: ExpressionClass.CONSTANT, + type: ExpressionType.VALUE_CONSTANT, + alias: '', + query_location: 0, + value: '1', + }; + + expect( + validateExpressionNode(VALUE_CONSTANT_NODE, EMPTY_VALID_FUNCTIONS) + ).toBe(true); + }); + + it('should return true for node type OPERATOR_CAST', () => { + const OPERATOR_CAST_NODE: ParsedExpression = { + class: ExpressionClass.CAST, + type: ExpressionType.OPERATOR_CAST, + alias: '', + query_location: 7, + child: { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 12, + column_names: ['column_name'], + }, + cast_type: { + id: 1, + }, + try_cast: false, + }; + + expect( + validateExpressionNode(OPERATOR_CAST_NODE, EMPTY_VALID_FUNCTIONS) + ).toBe(true); + }); + + it('should return true for node type OPERATOR_COALESCE', () => { + const OPERATOR_COALESCE_NODE: ParsedExpression = { + class: ExpressionClass.OPERATOR, + type: ExpressionType.OPERATOR_COALESCE, + alias: '', + query_location: 18446744073709552000, + children: [ + { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 16, + column_names: ['column_name'], + }, + { + class: ExpressionClass.CONSTANT, + type: ExpressionType.VALUE_CONSTANT, + alias: '', + query_location: 38, + value: { + type: { + id: 'INTEGER', + type_info: null, + }, + is_null: false, + value: 0, + }, + }, + ], + }; + + expect( + validateExpressionNode(OPERATOR_COALESCE_NODE, EMPTY_VALID_FUNCTIONS) + ).toBe(true); + }); + + it('should return true for node type FUNCTION with ROUND function and if it contains in validFunctions', () => { + const CASE_EXPR_NODE: ParsedExpression = { + class: ExpressionClass.FUNCTION, + type: ExpressionType.FUNCTION, + alias: '', + query_location: 7, + function_name: 'round', + schema: '', + children: [ + { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 13, + column_names: ['column_name'], + }, + { + class: ExpressionClass.CONSTANT, + type: ExpressionType.VALUE_CONSTANT, + alias: '', + query_location: 41, + value: { + type: { + id: 'INTEGER', + type_info: null, + }, + is_null: false, + value: 1, + }, + }, + ], + filter: null, + order_bys: { + type: ResultModifierType.ORDER_MODIFIER, + orders: [], + }, + distinct: false, + is_operator: false, + export_state: false, + catalog: '', + }; + + expect(validateExpressionNode(CASE_EXPR_NODE, VALID_FUNCTIONS)).toBe(true); + }); + + it('should throw error for node type FUNCTION with ROUND function and if it not contains in validFunctions', () => { + const CASE_EXPR_NODE: ParsedExpression = { + class: ExpressionClass.FUNCTION, + type: ExpressionType.FUNCTION, + alias: '', + query_location: 7, + function_name: 'round', + schema: '', + children: [ + { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 13, + column_names: ['column_name'], + }, + { + class: ExpressionClass.CONSTANT, + type: ExpressionType.VALUE_CONSTANT, + alias: '', + query_location: 41, + value: { + type: { + id: 'INTEGER', + type_info: null, + }, + is_null: false, + value: 1, + }, + }, + ], + filter: null, + order_bys: { + type: ResultModifierType.ORDER_MODIFIER, + orders: [], + }, + distinct: false, + is_operator: false, + export_state: false, + catalog: '', + }; + + expect(() => + validateExpressionNode(CASE_EXPR_NODE, new Set(['contains'])) + ).toThrowError('Invalid function: round'); + }); + + it('should return true for node type CASE', () => { + const CASE_EXPR_NODE: ParsedExpression = { + class: ExpressionClass.CASE, + type: ExpressionType.CASE_EXPR, + alias: '', + query_location: 7, + case_checks: [ + { + when_expr: { + class: ExpressionClass.COMPARISON, + type: ExpressionType.COMPARE_GREATERTHAN, + alias: '', + query_location: 35, + left: { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 17, + column_names: ['actual_close_date'], + }, + right: { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 37, + column_names: ['created_date'], + }, + }, + then_expr: { + class: ExpressionClass.COLUMN_REF, + type: ExpressionType.COLUMN_REF, + alias: '', + query_location: 55, + column_names: ['actual_close_date'], + }, + }, + ], + else_expr: { + class: ExpressionClass.CONSTANT, + type: ExpressionType.VALUE_CONSTANT, + alias: '', + query_location: 18446744073709552000, + value: { + type: { + id: 'NULL', + type_info: null, + }, + is_null: true, + }, + }, + }; + + expect(validateExpressionNode(CASE_EXPR_NODE, EMPTY_VALID_FUNCTIONS)).toBe( + true + ); + }); + + it('should throw error for node type INVALID', () => { + expect(() => + validateExpressionNode(INVALID_NODE, EMPTY_VALID_FUNCTIONS) + ).toThrowError('Invalid expression type'); + }); +}); diff --git a/meerkat-core/src/ast-validator/dimension-validator.ts b/meerkat-core/src/ast-validator/dimension-validator.ts new file mode 100644 index 00000000..e9e3d556 --- /dev/null +++ b/meerkat-core/src/ast-validator/dimension-validator.ts @@ -0,0 +1,88 @@ +import { ParsedExpression } from '../types/duckdb-serialization-types'; +import { + isCaseExpression, + isCoalesceExpression, + isColumnRefExpression, + isFunctionExpression, + isOperatorCast, + isValueConstantExpression, +} from '../types/utils'; +import { ParsedSerialization } from './types'; + +/** + * Validates an individual expression node + */ +export const validateExpressionNode = ( + node: ParsedExpression, + validFunctions: Set +): boolean => { + // Column references and value constants + if (isColumnRefExpression(node) || isValueConstantExpression(node)) { + return true; + } + + // Operator cast + if (isOperatorCast(node)) { + return validateExpressionNode(node.child, validFunctions); + } + + // Coalesce expression + if (isCoalesceExpression(node)) { + return node.children.every((child) => + validateExpressionNode(child, validFunctions) + ); + } + + // Function expression + if (isFunctionExpression(node)) { + if (!validFunctions.has(node.function_name)) { + throw new Error(`Invalid function: ${node.function_name}`); + } + return node.children.every((child) => + validateExpressionNode(child, validFunctions) + ); + } + + // Case expression + if (isCaseExpression(node)) { + return ( + node.case_checks.every((check) => + validateExpressionNode(check.then_expr, validFunctions) + ) && validateExpressionNode(node.else_expr, validFunctions) + ); + } + + throw new Error(`Invalid expression type: ${node.type}`); +}; + +/** + * Validates if the parsed serialization represents a valid dimension + */ +export const validateDimension = ( + parsedSerialization: ParsedSerialization, + validFunctions: string[] +): boolean => { + const statement = parsedSerialization.statements?.[0]; + if (!statement) { + throw new Error('No statement found'); + } + + if (statement.node.type !== 'SELECT_NODE') { + throw new Error('Statement must be a SELECT node'); + } + + const selectList = statement.node.select_list; + if (!selectList?.length || selectList.length !== 1) { + throw new Error('SELECT must contain exactly one expression'); + } + + const validFunctionSet = new Set(validFunctions); + + // Validate the expression + const expression = selectList[0]; + if (!validateExpressionNode(expression, validFunctionSet)) { + throw new Error('Expression contains invalid functions or operators'); + } + + return true; +}; diff --git a/meerkat-core/src/ast-validator/index.ts b/meerkat-core/src/ast-validator/index.ts new file mode 100644 index 00000000..058191df --- /dev/null +++ b/meerkat-core/src/ast-validator/index.ts @@ -0,0 +1,2 @@ +export { validateDimension } from './dimension-validator'; +export * from './types'; diff --git a/meerkat-core/src/ast-validator/types.ts b/meerkat-core/src/ast-validator/types.ts new file mode 100644 index 00000000..27142757 --- /dev/null +++ b/meerkat-core/src/ast-validator/types.ts @@ -0,0 +1,9 @@ +import { SelectStatement } from '../types/duckdb-serialization-types'; + +export interface ParsedSerialization { + statements: SelectStatement[]; + error?: boolean; + error_message?: string; + error_type?: string; + position?: string; +} diff --git a/meerkat-core/src/cube-filter-transformer/not/not.ts b/meerkat-core/src/cube-filter-transformer/not/not.ts index d9a414da..5798e5ec 100644 --- a/meerkat-core/src/cube-filter-transformer/not/not.ts +++ b/meerkat-core/src/cube-filter-transformer/not/not.ts @@ -1,10 +1,10 @@ import { - ConjunctionExpression, ExpressionClass, ExpressionType, + OperatorExpression, } from '../../types/duckdb-serialization-types/index'; -export const notDuckdbCondition = (): ConjunctionExpression => { +export const notDuckdbCondition = (): OperatorExpression => { return { class: ExpressionClass.OPERATOR, type: ExpressionType.OPERATOR_NOT, diff --git a/meerkat-core/src/index.ts b/meerkat-core/src/index.ts index 9d5df04c..f793a869 100644 --- a/meerkat-core/src/index.ts +++ b/meerkat-core/src/index.ts @@ -1,12 +1,14 @@ export * from './ast-builder/ast-builder'; export * from './ast-deserializer/ast-deserializer'; +export * from './ast-serializer/ast-serializer'; +export * from './ast-validator'; export { detectApplyContextParamsToBaseSQL } from './context-params/context-params-ast'; export * from './cube-measure-transformer/cube-measure-transformer'; export * from './cube-to-duckdb/cube-filter-to-duckdb'; export { applyFilterParamsToBaseSQL, detectAllFilterParamsFromSQL, - getFilterParamsAST + getFilterParamsAST, } from './filter-params/filter-params-ast'; export { getFilterParamsSQL } from './get-filter-params-sql/get-filter-params-sql'; export { getFinalBaseSQL } from './get-final-base-sql/get-final-base-sql'; @@ -20,4 +22,3 @@ export * from './utils/cube-to-table-schema'; export * from './utils/get-possible-nodes'; export { meerkatPlaceholderReplacer } from './utils/meerkat-placeholder-replacer'; export { memberKeyToSafeKey } from './utils/member-key-to-safe-key'; - diff --git a/meerkat-core/src/types/duckdb-serialization-types/serialization/ParsedExpression.ts b/meerkat-core/src/types/duckdb-serialization-types/serialization/ParsedExpression.ts index 47977bfd..8667b85b 100644 --- a/meerkat-core/src/types/duckdb-serialization-types/serialization/ParsedExpression.ts +++ b/meerkat-core/src/types/duckdb-serialization-types/serialization/ParsedExpression.ts @@ -30,47 +30,65 @@ export type ParsedExpression = | WindowExpression; export interface BetweenExpression extends BaseParsedExpression { + type: ExpressionType.COMPARE_BETWEEN | ExpressionType.COMPARE_NOT_BETWEEN; input: ParsedExpression; lower: ParsedExpression; upper: ParsedExpression; } export interface CaseExpression extends BaseParsedExpression { + type: ExpressionType.CASE_EXPR; case_checks: CacheCheck[]; else_expr: BaseParsedExpression; } export interface CastExpression extends BaseParsedExpression { + type: ExpressionType.OPERATOR_CAST; child: ParsedExpression; cast_type: LogicalType; try_cast: boolean; } export interface CollateExpression extends BaseParsedExpression { + type: ExpressionType.COLLATE; child: ParsedExpression; collation: string; } export interface ColumnRefExpression extends BaseParsedExpression { + type: ExpressionType.COLUMN_REF; column_names: string[]; } export interface ComparisonExpression extends BaseParsedExpression { + type: + | ExpressionType.COMPARE_EQUAL + | ExpressionType.COMPARE_NOTEQUAL + | ExpressionType.COMPARE_LESSTHAN + | ExpressionType.COMPARE_GREATERTHAN + | ExpressionType.COMPARE_LESSTHANOREQUALTO + | ExpressionType.COMPARE_GREATERTHANOREQUALTO; left: ParsedExpression; right: ParsedExpression; } export interface ConjunctionExpression extends BaseParsedExpression { + type: + | ExpressionType.CONJUNCTION_AND + | ExpressionType.CONJUNCTION_OR + | ExpressionType.OPERATOR_NOT; children: ParsedExpression[]; } export interface ConstantExpression extends BaseParsedExpression { + type: ExpressionType.VALUE_CONSTANT; value: Value; } export type DefaultExpression = BaseParsedExpression; export interface FunctionExpression extends BaseParsedExpression { + type: ExpressionType.FUNCTION; function_name: string; schema: string; children: ParsedExpression[]; @@ -83,23 +101,32 @@ export interface FunctionExpression extends BaseParsedExpression { } export interface LambdaExpression extends BaseParsedExpression { + type: ExpressionType.LAMBDA; lhs: ParsedExpression; expr: ParsedExpression | null; } export interface OperatorExpression extends BaseParsedExpression { + type: + | ExpressionType.OPERATOR_NOT + | ExpressionType.OPERATOR_NULLIF + | ExpressionType.OPERATOR_IS_NULL + | ExpressionType.OPERATOR_IS_NOT_NULL; children: ParsedExpression[]; } export interface ParameterExpression extends BaseParsedExpression { + type: ExpressionType.VALUE_PARAMETER; identifier: string; } export interface PositionalReferenceExpression extends BaseParsedExpression { + type: ExpressionType.POSITIONAL_REFERENCE; index: number; } export interface StarExpression extends BaseParsedExpression { + type: ExpressionType.STAR; relation_name: string; exclude_list: Set | Array; replace_list: Set | Array; @@ -116,6 +143,7 @@ export enum SubqueryType { } export interface SubqueryExpression extends BaseParsedExpression { + type: ExpressionType.SUBQUERY; subquery_type: SubqueryType; subquery: SelectStatement; child?: ParsedExpression; @@ -135,6 +163,19 @@ export enum WindowBoundary { } export interface WindowExpression extends BaseParsedExpression { + type: + | ExpressionType.WINDOW_AGGREGATE + | ExpressionType.WINDOW_RANK + | ExpressionType.WINDOW_RANK_DENSE + | ExpressionType.WINDOW_NTILE + | ExpressionType.WINDOW_PERCENT_RANK + | ExpressionType.WINDOW_CUME_DIST + | ExpressionType.WINDOW_ROW_NUMBER + | ExpressionType.WINDOW_FIRST_VALUE + | ExpressionType.WINDOW_LAST_VALUE + | ExpressionType.WINDOW_LEAD + | ExpressionType.WINDOW_LAG + | ExpressionType.WINDOW_NTH_VALUE; function_name: string; schema: string; catalog: string; diff --git a/meerkat-core/src/types/utils.ts b/meerkat-core/src/types/utils.ts new file mode 100644 index 00000000..d6bcf190 --- /dev/null +++ b/meerkat-core/src/types/utils.ts @@ -0,0 +1,47 @@ +import { + CaseExpression, + CastExpression, + ColumnRefExpression, + ConstantExpression, + FunctionExpression, + OperatorExpression, +} from './duckdb-serialization-types/serialization/ParsedExpression'; + +import { ExpressionType } from './duckdb-serialization-types'; +import { ParsedExpression } from './duckdb-serialization-types/serialization/ParsedExpression'; + +export const isColumnRefExpression = ( + node: ParsedExpression +): node is ColumnRefExpression => { + return node.type === ExpressionType.COLUMN_REF; +}; + +export const isValueConstantExpression = ( + node: ParsedExpression +): node is ConstantExpression => { + return node.type === ExpressionType.VALUE_CONSTANT; +}; + +export const isOperatorCast = ( + node: ParsedExpression +): node is CastExpression => { + return node.type === ExpressionType.OPERATOR_CAST; +}; + +export const isCoalesceExpression = ( + node: ParsedExpression +): node is OperatorExpression => { + return node.type === ExpressionType.OPERATOR_COALESCE; +}; + +export const isCaseExpression = ( + node: ParsedExpression +): node is CaseExpression => { + return node.type === ExpressionType.CASE_EXPR; +}; + +export const isFunctionExpression = ( + node: ParsedExpression +): node is FunctionExpression => { + return node.type === ExpressionType.FUNCTION; +}; diff --git a/meerkat-core/src/utils/base-ast.ts b/meerkat-core/src/utils/base-ast.ts index 6487f265..1ba1e5cf 100644 --- a/meerkat-core/src/utils/base-ast.ts +++ b/meerkat-core/src/utils/base-ast.ts @@ -1,5 +1,11 @@ -import { AggregateHandling, QueryNodeType } from '../types/duckdb-serialization-types/serialization/QueryNode'; -import { ExpressionClass, ExpressionType } from '../types/duckdb-serialization-types/serialization/Expression'; +import { + ExpressionClass, + ExpressionType, +} from '../types/duckdb-serialization-types/serialization/Expression'; +import { + AggregateHandling, + QueryNodeType, +} from '../types/duckdb-serialization-types/serialization/QueryNode'; import { SelectStatement } from '../types/duckdb-serialization-types/serialization/Statement'; import { TableReferenceType } from '../types/duckdb-serialization-types/serialization/TableRef'; @@ -22,7 +28,6 @@ export const getBaseAST = (): SelectStatement => { exclude_list: [], replace_list: [], columns: false, - expr: null, }, ], from_table: {