diff --git a/packages/cubejs-backend-native/js/index.ts b/packages/cubejs-backend-native/js/index.ts index b5dbf3cdc4ad2..5c4c24f25f7a5 100644 --- a/packages/cubejs-backend-native/js/index.ts +++ b/packages/cubejs-backend-native/js/index.ts @@ -463,6 +463,50 @@ export const buildSqlAndParams = (cubeEvaluator: any): any[] => { return native.buildSqlAndParams(cubeEvaluator); }; +/** + * JS-side wrapper around the long-lived Tesseract model `JsBox`. The + * `handle` field is the opaque `JsBox` produced by + * the Rust side; it stays public for tests / debug introspection but + * production callers should go through the methods. Lifetime is + * managed by the JS GC — when no reference to a `TesseractModel` + * survives, the underlying Rust `Model` is finalized through + * `NativeRustHandle`'s Drop chain. + */ +export class TesseractModel { + /** @internal — opaque JsBox handle, do not interpret in JS. */ + public readonly handle: unknown; + + /** @internal — instances are produced by `prepareModel`. */ + public constructor(handle: unknown) { + this.handle = handle; + } + + /** + * Same shape as the standalone `buildSqlAndParams` but skips the + * per-request `cubeEvaluator` roundtrip for structural lookups — + * the planner reads the compiled model from this handle instead. + */ + public buildSqlAndParams(queryParams: any): any[] { + const native = loadNative(); + return native.modelBuildSqlAndParams(this.handle, queryParams); + } +} + +/** + * Build the long-lived Tesseract model from a JS `SchemaSource` + * wrapper around `CubeEvaluator`. Returns a `TesseractModel` whose + * methods route per-request calls to the cached model handle. Returns + * `null` when the native module doesn't expose `prepareModel` + * (older build) so callers can detect the feature. + */ +export const prepareModel = (schemaSource: unknown): TesseractModel | null => { + const native = loadNative(); + if (typeof native.prepareModel !== 'function') { + return null; + } + return new TesseractModel(native.prepareModel(schemaSource)); +}; + export type ResultRow = Record; export const parseCubestoreResultMessage = async (message: ArrayBuffer): Promise => { diff --git a/packages/cubejs-backend-native/src/bridge_test_exports.rs b/packages/cubejs-backend-native/src/bridge_test_exports.rs index 98fb704fb2835..a46978d461edc 100644 --- a/packages/cubejs-backend-native/src/bridge_test_exports.rs +++ b/packages/cubejs-backend-native/src/bridge_test_exports.rs @@ -734,6 +734,13 @@ fn invoke_cube_definition(b: &NativeCubeDefinition) -> Invok r.record("sql_table", b.sql_table()); r.record("sql", b.sql()); r.record("default_filters", b.default_filters()); + r.record("measures", b.measures()); + r.record("dimensions", b.dimensions()); + r.record("segments", b.segments()); + r.record("joins", b.joins()); + r.record("pre_aggregations", b.pre_aggregations()); + r.record("access_policies", b.access_policies()); + r.record("included_members", b.included_members()); r } @@ -746,6 +753,7 @@ fn invoke_dimension_definition(b: &NativeDimensionDefinition r.record("time_shift", b.time_shift()); r.record("filter", b.filter()); r.record("mask_sql", b.mask_sql()); + r.record("granularities", b.granularities()); r } @@ -803,6 +811,10 @@ fn invoke_pre_aggregation_description( r.record("segment_references", b.segment_references()); r.record("rollup_references", b.rollup_references()); r.record("time_dimension_references", b.time_dimension_references()); + r.record("build_range_start", b.build_range_start()); + r.record("build_range_end", b.build_range_end()); + r.record("indexes", b.indexes()); + r.record("refresh_key", b.refresh_key()); r } @@ -991,6 +1003,62 @@ fn rust_box_unwrap(cx: FunctionContext) -> JsResult { ) } +/// Inspector used by the JS roundtrip test: opens a model handle +/// returned by `prepareModel` and reports the cubes it contains. +/// Production code reaches the model through downcast — this endpoint +/// just makes the introspection visible to JS for tests. +#[derive(serde::Serialize)] +struct ModelView { + cubes: Vec, +} + +#[derive(serde::Serialize)] +struct CubeView { + name: String, + is_view: bool, + measure_count: usize, + dimension_count: usize, + segment_count: usize, + join_count: usize, + pre_aggregation_count: usize, + access_policy_count: usize, +} + +fn model_describe_inner( + _context: NativeContextHolder, + obj: NativeObjectHandle, +) -> Result { + let rust_box = obj.into_rust_box()?; + let model = rust_box + .handle() + .downcast::()?; + let mut cubes = Vec::with_capacity(model.cubes.len()); + for cube in model.cubes_iter() { + cubes.push(CubeView { + name: cube.name.to_string(), + is_view: cube.is_view, + measure_count: cube.measures.len(), + dimension_count: cube.dimensions.len(), + segment_count: cube.segments.len(), + join_count: cube.joins.len(), + pre_aggregation_count: cube.pre_aggregations.len(), + access_policy_count: cube.access_policies.len(), + }); + } + // Stable order so the JS test can match by index. + cubes.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(ModelView { cubes }) +} + +fn model_describe(cx: FunctionContext) -> JsResult { + neon_guarded_funcion_call( + cx, + |context_holder: NativeContextHolder<_>, obj: NativeObjectHandle<_>| { + model_describe_inner(context_holder, obj) + }, + ) +} + pub fn register_module(cx: &mut ModuleContext) -> NeonResult<()> { cx.export_function("__testBridgeCompileMemberSql", compile_member_sql)?; cx.export_function("__testBridgeParseArgsNames", parse_args_names)?; @@ -1005,5 +1073,6 @@ pub fn register_module(cx: &mut ModuleContext) -> NeonResult<()> { cx.export_function("__testBridgeRustBoxCreate", rust_box_create)?; cx.export_function("__testBridgeRustBoxCreateAlt", rust_box_create_alt)?; cx.export_function("__testBridgeRustBoxUnwrap", rust_box_unwrap)?; + cx.export_function("__testBridgeModelDescribe", model_describe)?; Ok(()) } diff --git a/packages/cubejs-backend-native/src/node_export.rs b/packages/cubejs-backend-native/src/node_export.rs index c5bdc1cc687d4..6b08ae587b2de 100644 --- a/packages/cubejs-backend-native/src/node_export.rs +++ b/packages/cubejs-backend-native/src/node_export.rs @@ -22,9 +22,14 @@ use crate::stream::{OnCloseHandler, OnDrainHandler}; use crate::tokio_runtime_node; use crate::transport::NodeBridgeTransport; use crate::utils::{batch_to_rows, NonDebugInRelease}; +use cubenativeutils::wrappers::inner_types::InnerTypes; use cubenativeutils::wrappers::neon::neon_guarded_funcion_call; -use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::object::{NativeRustBox, NativeType}; +use cubenativeutils::wrappers::rust_handle::NativeRustHandle; +use cubenativeutils::wrappers::{NativeContextHolder, NativeObjectHandle}; use cubesqlplanner::cube_bridge::base_query_options::NativeBaseQueryOptions; +use cubesqlplanner::cube_bridge::schema_source::{NativeSchemaSource, SchemaSource}; +use cubesqlplanner::model::{Model, SchemaModelBuilder}; use cubesqlplanner::planner::base_query::BaseQuery; use std::rc::Rc; use std::sync::Arc; @@ -789,6 +794,48 @@ fn build_sql_and_params(cx: FunctionContext) -> JsResult { ) } +/// Build the Tesseract domain Model from a JS `SchemaSource` and +/// return a `JsBox` that JS can keep across calls. +/// +/// This is the entry point for the model-caching flow: JS calls +/// `prepareModel(schemaSource)` once after schema compilation; later +/// `buildSqlAndParams` calls go through `modelBuildSqlAndParams` with +/// that handle. +fn prepare_model_inner( + context_holder: NativeContextHolder, + source: NativeSchemaSource, +) -> Result, cubenativeutils::CubeError> { + let source: Rc = Rc::new(source); + let model = SchemaModelBuilder::new(source).build()?; + let handle = NativeRustHandle::new(model); + let rust_box = context_holder.rust_box(handle)?; + Ok(NativeObjectHandle::new(rust_box.into_object())) +} + +fn prepare_model(cx: FunctionContext) -> JsResult { + neon_guarded_funcion_call(cx, prepare_model_inner) +} + +/// Same signature as `buildSqlAndParams` but takes the model handle +/// returned by `prepareModel` as the first argument. The handle is +/// downcast back to `Model` so future planner code can read structure +/// from it; for now we just validate the round-trip and delegate to +/// the existing per-request `BaseQuery` flow. +fn model_build_sql_and_params_inner( + context_holder: NativeContextHolder, + model_handle: NativeObjectHandle, + options: NativeBaseQueryOptions, +) -> Result, cubenativeutils::CubeError> { + let rust_box = model_handle.into_rust_box()?; + let _model = rust_box.handle().downcast::()?; + let base_query = BaseQuery::try_new(context_holder.clone(), Rc::new(options))?; + base_query.build_sql_and_params() +} + +fn model_build_sql_and_params(cx: FunctionContext) -> JsResult { + neon_guarded_funcion_call(cx, model_build_sql_and_params_inner) +} + fn debug_js_to_clrepr_to_js(mut cx: FunctionContext) -> JsResult { let arg = cx.argument::(0)?; let arg_clrep = CLRepr::from_js_ref(arg, &mut cx)?; @@ -811,6 +858,8 @@ pub fn register_module_exports( //============ sql planner exports =================== cx.export_function("buildSqlAndParams", build_sql_and_params)?; + cx.export_function("prepareModel", prepare_model)?; + cx.export_function("modelBuildSqlAndParams", model_build_sql_and_params)?; //========= sql orchestrator exports ================= crate::orchestrator::register_module(&mut cx)?; diff --git a/packages/cubejs-backend-native/test/bridge/bridge-fixtures.ts b/packages/cubejs-backend-native/test/bridge/bridge-fixtures.ts index 67fb875c3af11..aad1eef9b6037 100644 --- a/packages/cubejs-backend-native/test/bridge/bridge-fixtures.ts +++ b/packages/cubejs-backend-native/test/bridge/bridge-fixtures.ts @@ -167,6 +167,12 @@ export const cubeDefinitionFixture = (): unknown => ({ // sqlAlias, isView, isCalendar, joinMap optional // sql_table, sql optional getters defaultFilters: [viewFilterDefinitionFixture()], + // measures/dimensions/segments are required vec fields + measures: [measureDefinitionFixture()], + dimensions: [dimensionDefinitionFixture()], + segments: [segmentDefinitionFixture()], + // joins/preAggregations/accessPolicy/includedMembers are optional vec + // getters — omitted, the getters return None }); export const dimensionDefinitionFixture = (): unknown => ({ diff --git a/packages/cubejs-backend-native/test/bridge/helpers.ts b/packages/cubejs-backend-native/test/bridge/helpers.ts index 17b4969cd032c..75bfdd6d6a13d 100644 --- a/packages/cubejs-backend-native/test/bridge/helpers.ts +++ b/packages/cubejs-backend-native/test/bridge/helpers.ts @@ -152,3 +152,26 @@ export function createRustBoxProbeAlt(note: string): unknown { export function unwrapRustBoxProbe(handle: unknown): RustBoxProbeView { return native.__testBridgeRustBoxUnwrap(handle); } + +export interface ModelCubeView { + name: string; + is_view: boolean; + measure_count: number; + dimension_count: number; + segment_count: number; + join_count: number; + pre_aggregation_count: number; + access_policy_count: number; +} + +export interface ModelView { + cubes: ModelCubeView[]; +} + +export function prepareModelRaw(schemaSource: unknown): unknown { + return native.prepareModel(schemaSource); +} + +export function describeModel(handle: unknown): ModelView { + return native.__testBridgeModelDescribe(handle); +} diff --git a/packages/cubejs-backend-native/test/bridge/model-roundtrip.test.ts b/packages/cubejs-backend-native/test/bridge/model-roundtrip.test.ts new file mode 100644 index 0000000000000..a420dc24292dc --- /dev/null +++ b/packages/cubejs-backend-native/test/bridge/model-roundtrip.test.ts @@ -0,0 +1,112 @@ +import { + bridgeHarnessAvailable, + describeModel, + prepareModelRaw as prepareModel, +} from './helpers'; + +const describeBridge = bridgeHarnessAvailable ? describe : describe.skip; + +/** + * Minimal stand-in for the production `SchemaSource` (from + * cubejs-schema-compiler). The Rust side only consumes `primaryKeys` + * and `cubes()`; each cube shape mirrors what the real + * `SchemaSource.cubes()` wrapper exposes after prepareCompiler runs. + */ +function makeSchemaSource(cubes: any[], primaryKeys: Record = {}) { + return { + primaryKeys, + cubes: () => cubes, + }; +} + +function makeCube(overrides: Partial = {}): any { + return { + name: 'Users', + sqlAlias: undefined, + isView: false, + calendar: false, + measures: [], + dimensions: [], + segments: [], + joins: [], + preAggregations: [], + accessPolicy: [], + includedMembers: [], + ...overrides, + }; +} + +describeBridge('bridge: model roundtrip via prepareModel / __testBridgeModelDescribe', () => { + it('returns the cubes in the model with member counts', () => { + const source = makeSchemaSource( + [ + makeCube({ + name: 'Users', + measures: [ + { name: 'count', type: 'count', ownedByCube: true }, + { name: 'total', type: 'sum', ownedByCube: true }, + ], + dimensions: [ + { name: 'id', type: 'number', primaryKey: true, ownedByCube: true }, + { name: 'status', type: 'string', ownedByCube: true }, + ], + }), + makeCube({ + name: 'Orders', + measures: [{ name: 'count', type: 'count', ownedByCube: true }], + dimensions: [{ name: 'id', type: 'number', primaryKey: true, ownedByCube: true }], + }), + ], + { + Users: ['id'], + Orders: ['id'], + } + ); + + const handle = prepareModel(source); + const view = describeModel(handle); + + // SchemaModelBuilder iterates by insertion order from JS, but the + // describe helper sorts alphabetically. + expect(view.cubes.map(c => c.name)).toEqual(['Orders', 'Users']); + + const users = view.cubes.find(c => c.name === 'Users')!; + expect(users.measure_count).toBe(2); + expect(users.dimension_count).toBe(2); + expect(users.is_view).toBe(false); + + const orders = view.cubes.find(c => c.name === 'Orders')!; + expect(orders.measure_count).toBe(1); + expect(orders.dimension_count).toBe(1); + }); + + it('the handle survives multiple describe calls', () => { + const source = makeSchemaSource( + [makeCube({ name: 'Users', measures: [{ name: 'count', type: 'count' }] })], + { Users: [] } + ); + const handle = prepareModel(source); + for (let i = 0; i < 50; i += 1) { + const view = describeModel(handle); + expect(view.cubes).toHaveLength(1); + expect(view.cubes[0].measure_count).toBe(1); + } + }); + + it('different handles refer to independent models', () => { + const a = prepareModel( + makeSchemaSource([makeCube({ name: 'A' })]) + ); + const b = prepareModel( + makeSchemaSource([makeCube({ name: 'B' }), makeCube({ name: 'C' })]) + ); + + expect(describeModel(a).cubes.map(c => c.name)).toEqual(['A']); + expect(describeModel(b).cubes.map(c => c.name)).toEqual(['B', 'C']); + }); + + it('describeModel rejects non-RustBox arguments and wrong-type boxes', () => { + expect(() => describeModel({})).toThrow(/Object is not a Rust box/); + expect(() => describeModel(null)).toThrow(/Object is not a Rust box/); + }); +}); diff --git a/packages/cubejs-backend-native/test/bridge/object-bridges-coverage.test.ts b/packages/cubejs-backend-native/test/bridge/object-bridges-coverage.test.ts index 629ae33d5945a..527978be9d017 100644 --- a/packages/cubejs-backend-native/test/bridge/object-bridges-coverage.test.ts +++ b/packages/cubejs-backend-native/test/bridge/object-bridges-coverage.test.ts @@ -88,11 +88,18 @@ const BRIDGES: BridgeSpec[] = [ { name: 'cubeDefinition', expected: [ + 'access_policies', 'default_filters', + 'dimensions', + 'included_members', 'is_calendar', 'is_view', 'join_map', + 'joins', + 'measures', 'name', + 'pre_aggregations', + 'segments', 'sql', 'sql_alias', 'sql_table', @@ -121,13 +128,16 @@ const BRIDGES: BridgeSpec[] = [ name: 'dimensionDefinition', expected: [ 'add_group_by_references', + 'alias_member', 'case', 'dimension_type', 'filter', + 'granularities', 'latitude', 'longitude', 'mask_sql', 'multi_stage', + 'name', 'owned_by_cube', 'primary_key', 'propagate_filters_to_sub_query', @@ -168,7 +178,7 @@ const BRIDGES: BridgeSpec[] = [ { name: 'filterGroup', expected: [] }, { name: 'filterParams', expected: [] }, { name: 'geoItem', expected: ['sql'] }, - { name: 'granularityDefinition', expected: ['interval', 'offset', 'origin', 'sql'] }, + { name: 'granularityDefinition', expected: ['interval', 'name', 'offset', 'origin', 'sql'] }, { name: 'joinDefinition', expected: ['joins', 'multiplication_factor', 'root'] }, { name: 'joinGraph', expected: ['build_join'] }, { name: 'joinItem', expected: ['from', 'join', 'original_from', 'original_to', 'to'] }, @@ -177,6 +187,7 @@ const BRIDGES: BridgeSpec[] = [ name: 'measureDefinition', expected: [ 'add_group_by_references', + 'alias_member', 'case', 'drill_filters', 'filter', @@ -186,6 +197,7 @@ const BRIDGES: BridgeSpec[] = [ 'mask_sql', 'measure_type', 'multi_stage', + 'name', 'order_by', 'owned_by_cube', 'reduce_by_references', @@ -212,17 +224,25 @@ const BRIDGES: BridgeSpec[] = [ name: 'preAggregationDescription', expected: [ 'allow_non_strict_date_range_match', + 'build_range_end', + 'build_range_start', 'dimension_references', 'external', 'granularity', + 'indexes', 'measure_references', 'name', + 'owned_by_cube', + 'partition_granularity', 'pre_aggregation_type', + 'refresh_key', 'rollup_references', + 'scheduled_refresh', 'segment_references', 'sql_alias', 'time_dimension_reference', 'time_dimension_references', + 'use_original_sql_pre_aggregations', ], }, { @@ -231,7 +251,7 @@ const BRIDGES: BridgeSpec[] = [ }, { name: 'preAggregationTimeDimension', expected: ['dimension', 'granularity'] }, { name: 'securityContext', expected: [] }, - { name: 'segmentDefinition', expected: ['owned_by_cube', 'segment_type', 'sql'] }, + { name: 'segmentDefinition', expected: ['name', 'owned_by_cube', 'segment_type', 'sql'] }, { name: 'sqlUtils', expected: [] }, { name: 'structWithSqlMember', expected: ['sql'] }, { name: 'timeShiftDefinition', expected: ['interval', 'name', 'sql', 'timeshift_type'] }, diff --git a/packages/cubejs-backend-shared/src/env.ts b/packages/cubejs-backend-shared/src/env.ts index 3cf619bd906e1..5e6be32648704 100644 --- a/packages/cubejs-backend-shared/src/env.ts +++ b/packages/cubejs-backend-shared/src/env.ts @@ -317,6 +317,7 @@ const variables: Record any> = { .asInt(), nativeSqlPlanner: () => get('CUBEJS_TESSERACT_SQL_PLANNER').default('false').asBool(), nativeSqlPlannerPreAggregations: () => get('CUBEJS_TESSERACT_PRE_AGGREGATIONS').default('false').asBool(), + tesseractNativeModel: () => get('CUBEJS_TESSERACT_NATIVE_MODEL').default('false').asBool(), transpilationWorkerThreads: () => { const enabled = get('CUBEJS_TRANSPILATION_WORKER_THREADS') .default('true') diff --git a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts index b86c36102d24b..b692d6dc572d0 100644 --- a/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts +++ b/packages/cubejs-schema-compiler/src/compiler/CubeEvaluator.ts @@ -16,7 +16,10 @@ import { ViewDefaultValueFilter, ViewIncludedMember } from './CubeSymbols'; +import { getEnv } from '@cubejs-backend/shared'; +import { prepareModel as nativePrepareModel, TesseractModel } from '@cubejs-backend/native'; import { UserError } from './UserError'; +import { SchemaSource } from './SchemaSource'; import { BaseQuery, PreAggregationDefinitionExtended } from '../adapter'; import type { CubeValidator } from './CubeValidator'; import type { ErrorReporter } from './ErrorReporter'; @@ -195,6 +198,18 @@ export class CubeEvaluator extends CubeSymbols { public byFileName: Record = {}; + /** + * Handle to the Rust-side Tesseract domain model built from this + * evaluator. Populated at the end of `compile()` only when + * `CUBEJS_TESSERACT_NATIVE_MODEL` is enabled; the planner is not yet + * routed through it. Lifetime is managed by the JS GC: when this + * `CubeEvaluator` is replaced (next schema reload), the old + * `TesseractModel` loses references and gets finalized — the + * underlying Rust `Model` and its `MemberSql` roots are then dropped + * via the standard `Finalize` path. + */ + public tesseractModel: TesseractModel | null = null; + private isRbacEnabledCache: boolean | null = null; public constructor( @@ -230,6 +245,32 @@ export class CubeEvaluator extends CubeSymbols { return [v.name, primaryKeyNamesToSymbols]; }) ); + + this.tesseractModel = this.tryPrepareTesseractModel(errorReporter); + } + + /** + * Build the Tesseract domain model wrapper. Gated behind + * `CUBEJS_TESSERACT_NATIVE_MODEL` (off by default). Returns `null` + * when the flag is off, when the native module doesn't expose + * `prepareModel` (older build), or when the build throws — schema + * compilation must not fail just because the caching layer can't + * initialise. + */ + private tryPrepareTesseractModel(errorReporter: ErrorReporter): TesseractModel | null { + if (!getEnv('tesseractNativeModel')) { + return null; + } + try { + const source = new SchemaSource(this); + return nativePrepareModel(source); + } catch (e: any) { + errorReporter.warning({ + message: `Tesseract domain model preparation skipped: ${e?.message ?? e}`, + loc: null, + }); + return null; + } } protected prepareCube(cube, errorReporter: ErrorReporter): EvaluatedCube { @@ -699,7 +740,10 @@ export class CubeEvaluator extends CubeSymbols { protected preparePreAggregations(cube: any, errorReporter: ErrorReporter) { if (cube.preAggregations) { // eslint-disable-next-line no-restricted-syntax - for (const preAggregation of Object.values(cube.preAggregations) as any) { + for (const [preAggName, preAggregation] of Object.entries(cube.preAggregations) as Array<[string, any]>) { + // Tesseract bridge consumes pre-aggregations as an array; stamp + // the name so it survives the Record → Array conversion. + preAggregation.name = preAggName; // preAggregation is actually (PreAggregationDefinitionRollup | PreAggregationDefinitionOriginalSql) if (preAggregation.timeDimension) { preAggregation.timeDimensionReference = preAggregation.timeDimension; @@ -799,7 +843,7 @@ export class CubeEvaluator extends CubeSymbols { errorReporter.error(`View '${cube.name}' defines own member '${cube.name}.${memberName}'. Please move this member definition to one of the cubes.`); } - members[memberName] = { ...members[memberName], ownedByCube }; + members[memberName] = { ...members[memberName], ownedByCube, name: memberName }; if (aliasMember) { members[memberName].aliasMember = aliasMember; } diff --git a/packages/cubejs-schema-compiler/src/compiler/SchemaSource.ts b/packages/cubejs-schema-compiler/src/compiler/SchemaSource.ts new file mode 100644 index 0000000000000..ccc0afa97d5f9 --- /dev/null +++ b/packages/cubejs-schema-compiler/src/compiler/SchemaSource.ts @@ -0,0 +1,63 @@ +import { CubeEvaluator } from './CubeEvaluator'; + +/** + * Build-phase wrapper exposed to the Rust side as the data source for + * the Tesseract domain model. Kept intentionally separate from + * `CubeEvaluator`: that one stays the runtime/lookup bridge, this one + * is consumed once per schema by `ModelBuilder` on the Rust side. + * + * Each returned cube is a thin prototype wrapper: own properties + * override `measures` / `dimensions` / `segments` / `preAggregations` + * (Record → Array form required by the Rust bridge), while everything + * else (sql, getters like `maskSql`, ...) is inherited from the + * underlying evaluated cube via the prototype chain so getters keep + * working. Dimensions and pre-aggregations get the same prototype + * trick to surface nested Records (granularities, indexes) as arrays + * with `name` stamped on each entry. + */ +export class SchemaSource { + public constructor(private readonly cubeEvaluator: CubeEvaluator) {} + + public get primaryKeys(): Record { + return this.cubeEvaluator.primaryKeys; + } + + public cubes(): any[] { + return Object.values(this.cubeEvaluator.evaluatedCubes).map(cube => { + const wrapper = Object.create(cube); + wrapper.measures = Object.values(cube.measures || {}); + wrapper.dimensions = Object.values(cube.dimensions || {}).map(SchemaSource.wrapDimension); + wrapper.segments = Object.values(cube.segments || {}); + wrapper.preAggregations = Object.values(cube.preAggregations || {}).map(SchemaSource.wrapPreAggregation); + return wrapper; + }); + } + + private static wrapDimension(dim: any): any { + if (!dim.granularities) { + return dim; + } + const wrapped = Object.create(dim); + wrapped.granularities = Object.entries(dim.granularities).map(([name, gran]: [string, any]) => { + if (gran.name === undefined) { + gran.name = name; + } + return gran; + }); + return wrapped; + } + + private static wrapPreAggregation(preAgg: any): any { + if (!preAgg.indexes) { + return preAgg; + } + const wrapped = Object.create(preAgg); + wrapped.indexes = Object.entries(preAgg.indexes).map(([name, idx]: [string, any]) => { + if (idx.name === undefined) { + idx.name = name; + } + return idx; + }); + return wrapped; + } +} diff --git a/packages/cubejs-schema-compiler/src/compiler/index.ts b/packages/cubejs-schema-compiler/src/compiler/index.ts index ff62cfaa59ddc..f029891380ffc 100644 --- a/packages/cubejs-schema-compiler/src/compiler/index.ts +++ b/packages/cubejs-schema-compiler/src/compiler/index.ts @@ -11,3 +11,4 @@ export { PreAggregationInfo, EvaluatedCube, } from './CubeEvaluator'; +export { SchemaSource } from './SchemaSource'; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_condition_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_condition_definition.rs new file mode 100644 index 0000000000000..5131837e95cfd --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_condition_definition.rs @@ -0,0 +1,17 @@ +use super::member_sql::{MemberSql, NativeMemberSql}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +/// One condition under `accessPolicy[].conditions[]` — wraps the +/// `if` predicate callable. +#[nativebridge::native_bridge] +pub trait AccessConditionDefinition { + #[nbridge(field, rename = "if")] + fn predicate(&self) -> Result, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_filter_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_filter_definition.rs new file mode 100644 index 0000000000000..89a65b9f86011 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_filter_definition.rs @@ -0,0 +1,30 @@ +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeArray; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +/// `accessPolicy[].rowLevel.filters[]` shape. Either a leaf (member + +/// operator + values) or an `and`/`or` recursive group. +#[derive(Serialize, Deserialize, Debug)] +pub struct AccessFilterDefinitionStatic { + /// Resolved by `prepareAccessPolicy` from the source `member` field. + #[serde(rename = "memberReference")] + pub member_reference: Option, + pub operator: Option, + #[serde(default)] + pub values: Vec, +} + +#[nativebridge::native_bridge(AccessFilterDefinitionStatic)] +pub trait AccessFilterDefinition { + #[nbridge(field, vec, optional)] + fn and(&self) -> Result>>, CubeError>; + #[nbridge(field, vec, optional)] + fn or(&self) -> Result>>, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_policy_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_policy_definition.rs new file mode 100644 index 0000000000000..9843fcdc341ac --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/access_policy_definition.rs @@ -0,0 +1,40 @@ +use super::access_condition_definition::{ + AccessConditionDefinition, NativeAccessConditionDefinition, +}; +use super::member_level_access_definition::{ + MemberLevelAccessDefinition, NativeMemberLevelAccessDefinition, +}; +use super::row_level_access_definition::{ + NativeRowLevelAccessDefinition, RowLevelAccessDefinition, +}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeArray; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +#[derive(Serialize, Deserialize, Debug)] +pub struct AccessPolicyDefinitionStatic { + pub role: Option, + pub group: Option, + #[serde(default)] + pub groups: Vec, +} + +/// Access policy declared on a cube. +#[nativebridge::native_bridge(AccessPolicyDefinitionStatic)] +pub trait AccessPolicyDefinition { + #[nbridge(field, optional, rename = "memberLevel")] + fn member_level(&self) -> Result>, CubeError>; + #[nbridge(field, optional, rename = "memberMasking")] + fn member_masking(&self) -> Result>, CubeError>; + #[nbridge(field, optional, rename = "rowLevel")] + fn row_level(&self) -> Result>, CubeError>; + #[nbridge(field, vec, optional)] + fn conditions(&self) -> Result>>, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_definition.rs index a7014c2aacbeb..e36fa9a94a2b0 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_definition.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_definition.rs @@ -1,5 +1,14 @@ +use super::access_policy_definition::{AccessPolicyDefinition, NativeAccessPolicyDefinition}; +use super::cube_join_definition::{CubeJoinDefinition, NativeCubeJoinDefinition}; +use super::dimension_definition::{DimensionDefinition, NativeDimensionDefinition}; +use super::measure_definition::{MeasureDefinition, NativeMeasureDefinition}; use super::member_sql::{MemberSql, NativeMemberSql}; +use super::pre_aggregation_description::{ + NativePreAggregationDescription, PreAggregationDescription, +}; +use super::segment_definition::{NativeSegmentDefinition, SegmentDefinition}; use super::view_filter_definition::{NativeViewFilterDefinition, ViewFilterDefinition}; +use super::view_included_member::{NativeViewIncludedMember, ViewIncludedMember}; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, }; @@ -42,4 +51,19 @@ pub trait CubeDefinition { fn sql(&self) -> Result>, CubeError>; #[nbridge(field, optional, vec)] fn default_filters(&self) -> Result>>, CubeError>; + #[nbridge(field, vec)] + fn measures(&self) -> Result>, CubeError>; + #[nbridge(field, vec)] + fn dimensions(&self) -> Result>, CubeError>; + #[nbridge(field, vec)] + fn segments(&self) -> Result>, CubeError>; + #[nbridge(field, vec, optional)] + fn joins(&self) -> Result>>, CubeError>; + #[nbridge(field, vec, optional, rename = "preAggregations")] + fn pre_aggregations(&self) + -> Result>>, CubeError>; + #[nbridge(field, vec, optional, rename = "accessPolicy")] + fn access_policies(&self) -> Result>>, CubeError>; + #[nbridge(field, vec, optional, rename = "includedMembers")] + fn included_members(&self) -> Result>>, CubeError>; } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_join_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_join_definition.rs new file mode 100644 index 0000000000000..7d84e94c37339 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/cube_join_definition.rs @@ -0,0 +1,28 @@ +use super::member_sql::{MemberSql, NativeMemberSql}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +/// Raw join declared on a cube (post-`prepareJoins`). Distinct from +/// the graph-traversal `JoinItem` / `JoinItemDefinition` bridges used +/// at query time. +#[derive(Serialize, Deserialize, Debug)] +pub struct CubeJoinDefinitionStatic { + /// Name of the join's target cube. + pub name: String, + /// Normalized to `belongsTo` | `hasMany` | `hasOne` by + /// `prepareJoins`. + pub relationship: String, +} + +#[nativebridge::native_bridge(CubeJoinDefinitionStatic)] +pub trait CubeJoinDefinition { + #[nbridge(field)] + fn sql(&self) -> Result, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs index 65a73c350a8ab..0128ecf4add08 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/dimension_definition.rs @@ -1,5 +1,6 @@ use super::case_variant::CaseVariant; use super::geo_item::{GeoItem, NativeGeoItem}; +use super::granularity_definition::{GranularityDefinition, NativeGranularityDefinition}; use super::member_sql::{MemberSql, NativeMemberSql}; use super::multi_stage_filter::{MultiStageFilterReferences, NativeMultiStageFilterReferences}; use crate::cube_bridge::timeshift_definition::{NativeTimeShiftDefinition, TimeShiftDefinition}; @@ -16,6 +17,10 @@ use std::rc::Rc; #[derive(Serialize, Deserialize, Debug, nativebridge::NativeBridgeStatic)] pub struct DimensionDefinitionStatic { + /// Local name of the dimension on its cube. Populated by + /// `prepareMembers` on the JS side. + #[serde(default)] + pub name: String, #[serde(rename = "type")] pub dimension_type: String, #[serde(rename = "ownedByCube")] @@ -31,6 +36,8 @@ pub struct DimensionDefinitionStatic { pub values: Option>, #[serde(rename = "primaryKey")] pub primary_key: Option, + #[serde(rename = "aliasMember")] + pub alias_member: Option, } #[nativebridge::native_bridge(DimensionDefinitionStatic, with_static_meta)] @@ -55,4 +62,7 @@ pub trait DimensionDefinition { #[nbridge(field, optional)] fn mask_sql(&self) -> Result>, CubeError>; + + #[nbridge(field, vec, optional)] + fn granularities(&self) -> Result>>, CubeError>; } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/granularity_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/granularity_definition.rs index afde8d097ad06..68a5bf12cd1e4 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/granularity_definition.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/granularity_definition.rs @@ -13,6 +13,12 @@ use std::rc::Rc; Deserialize, Serialize, Clone, Debug, PartialEq, Eq, Hash, nativebridge::NativeBridgeStatic, )] pub struct GranularityDefinitionStatic { + /// Local name of the granularity on its time dimension. Stamped + /// in `SchemaSource.cubes()` so the bridge can return granularities + /// as an array (the field is otherwise the Record key only). + #[serde(default)] + pub name: String, + #[serde(default)] pub interval: String, pub origin: Option, pub offset: Option, diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs index b9b54f3b5a5ef..b9e9273656d1e 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/measure_definition.rs @@ -37,6 +37,11 @@ pub struct RollingWindow { #[derive(Serialize, Deserialize, Debug, nativebridge::NativeBridgeStatic)] pub struct MeasureDefinitionStatic { + /// Local name of the measure on its cube. Populated by + /// `prepareMembers` on the JS side so the bridge can return + /// measures as an array (`name` is the Record key otherwise). + #[serde(default)] + pub name: String, #[serde(rename = "type")] pub measure_type: String, #[serde(rename = "ownedByCube")] @@ -53,6 +58,8 @@ pub struct MeasureDefinitionStatic { pub time_shift_references: Option>, #[serde(rename = "rollingWindow")] pub rolling_window: Option, + #[serde(rename = "aliasMember")] + pub alias_member: Option, } #[nativebridge::native_bridge(MeasureDefinitionStatic, with_static_meta)] diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_level_access_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_level_access_definition.rs new file mode 100644 index 0000000000000..1f2b3466a0cc1 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/member_level_access_definition.rs @@ -0,0 +1,21 @@ +use cubenativeutils::wrappers::serializer::{NativeDeserialize, NativeSerialize}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +/// Shared shape for `memberLevel` and `memberMasking` on an access +/// policy. After `prepareAccessPolicy` includes / excludes are resolved +/// into qualified-name lists. +#[derive(Serialize, Deserialize, Debug)] +pub struct MemberLevelAccessDefinitionStatic { + #[serde(rename = "includesMembers", default)] + pub includes_members: Vec, + #[serde(rename = "excludesMembers", default)] + pub excludes_members: Vec, +} + +#[nativebridge::native_bridge(MemberLevelAccessDefinitionStatic)] +pub trait MemberLevelAccessDefinition {} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs index 19759f1796dcd..a7a4e24bf6631 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/mod.rs @@ -5,6 +5,9 @@ //! callbacks, security context, and so on. Tesseract reads these //! types as input; no business logic lives here. +pub mod access_condition_definition; +pub mod access_filter_definition; +pub mod access_policy_definition; pub mod base_query_options; pub mod base_tools; pub mod case_definition; @@ -15,6 +18,7 @@ pub mod case_switch_else_item; pub mod case_switch_item; pub mod case_variant; pub mod cube_definition; +pub mod cube_join_definition; pub mod dimension_definition; pub mod driver_tools; pub mod evaluator; @@ -31,14 +35,19 @@ pub mod join_item_definition; pub mod measure_definition; pub mod member_definition; pub mod member_expression; +pub mod member_level_access_definition; pub mod member_order_by; pub mod member_sql; pub mod multi_stage_filter; pub mod multi_stage_grain; pub mod options_member; pub mod pre_aggregation_description; +pub mod pre_aggregation_index_definition; pub mod pre_aggregation_obj; pub mod pre_aggregation_time_dimension; +pub mod refresh_key_definition; +pub mod row_level_access_definition; +pub mod schema_source; pub mod security_context; pub mod segment_definition; pub mod sql_templates_render; @@ -47,3 +56,4 @@ pub mod string_or_sql; pub mod struct_with_sql_member; pub mod timeshift_definition; pub mod view_filter_definition; +pub mod view_included_member; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_description.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_description.rs index f0e42e962f8d4..c0ab4e16525a3 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_description.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_description.rs @@ -1,7 +1,11 @@ use super::member_sql::{MemberSql, NativeMemberSql}; +use super::pre_aggregation_index_definition::{ + NativePreAggregationIndexDefinition, PreAggregationIndexDefinition, +}; use super::pre_aggregation_time_dimension::{ NativePreAggregationTimeDimension, PreAggregationTimeDimension, }; +use super::refresh_key_definition::{NativeRefreshKeyDefinition, RefreshKeyDefinition}; use cubenativeutils::wrappers::serializer::{ NativeDeserialize, NativeDeserializer, NativeSerialize, }; @@ -24,6 +28,14 @@ pub struct PreAggregationDescriptionStatic { pub external: Option, #[serde(rename = "allowNonStrictDateRangeMatch")] pub allow_non_strict_date_range_match: Option, + #[serde(rename = "scheduledRefresh")] + pub scheduled_refresh: Option, + #[serde(rename = "useOriginalSqlPreAggregations")] + pub use_original_sql_pre_aggregations: Option, + #[serde(rename = "partitionGranularity")] + pub partition_granularity: Option, + #[serde(rename = "ownedByCube")] + pub owned_by_cube: Option, } #[nativebridge::native_bridge(PreAggregationDescriptionStatic, with_static_meta)] @@ -47,4 +59,12 @@ pub trait PreAggregationDescription { fn time_dimension_references( &self, ) -> Result>>, CubeError>; + #[nbridge(field, optional, rename = "refreshRangeStart")] + fn build_range_start(&self) -> Result>, CubeError>; + #[nbridge(field, optional, rename = "refreshRangeEnd")] + fn build_range_end(&self) -> Result>, CubeError>; + #[nbridge(field, vec, optional)] + fn indexes(&self) -> Result>>, CubeError>; + #[nbridge(field, optional, rename = "refreshKey")] + fn refresh_key(&self) -> Result>, CubeError>; } diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_index_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_index_definition.rs new file mode 100644 index 0000000000000..8837cbae06873 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/pre_aggregation_index_definition.rs @@ -0,0 +1,23 @@ +use cubenativeutils::wrappers::serializer::{NativeDeserialize, NativeSerialize}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +#[derive(Serialize, Deserialize, Debug)] +pub struct PreAggregationIndexDefinitionStatic { + /// Stamped by `SchemaSource.cubes()` from the Record key. + #[serde(default)] + pub name: String, + #[serde(default)] + pub columns: Vec, + /// `regular` | `aggregate`. Optional in the schema — defaults to + /// `regular` if missing. + #[serde(rename = "type")] + pub index_type: Option, +} + +#[nativebridge::native_bridge(PreAggregationIndexDefinitionStatic)] +pub trait PreAggregationIndexDefinition {} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/refresh_key_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/refresh_key_definition.rs new file mode 100644 index 0000000000000..ff460ba230d94 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/refresh_key_definition.rs @@ -0,0 +1,29 @@ +use super::member_sql::{MemberSql, NativeMemberSql}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +/// Raw refresh-key object on a pre-aggregation. The three schema +/// variants (`Sql` / `Every` / `Immutable`) share this single shape; +/// the model builder picks the variant by which fields are present. +#[derive(Serialize, Deserialize, Debug)] +pub struct RefreshKeyDefinitionStatic { + pub every: Option, + pub timezone: Option, + pub incremental: Option, + #[serde(rename = "updateWindow")] + pub update_window: Option, + pub immutable: Option, +} + +#[nativebridge::native_bridge(RefreshKeyDefinitionStatic)] +pub trait RefreshKeyDefinition { + #[nbridge(field, optional)] + fn sql(&self) -> Result>, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/row_level_access_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/row_level_access_definition.rs new file mode 100644 index 0000000000000..f3b4c9fe13f88 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/row_level_access_definition.rs @@ -0,0 +1,16 @@ +use super::access_filter_definition::{AccessFilterDefinition, NativeAccessFilterDefinition}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeArray; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use std::any::Any; +use std::rc::Rc; + +#[nativebridge::native_bridge] +pub trait RowLevelAccessDefinition { + #[nbridge(field, vec)] + fn filters(&self) -> Result>, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/schema_source.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/schema_source.rs new file mode 100644 index 0000000000000..6aefabfd99683 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/schema_source.rs @@ -0,0 +1,29 @@ +use super::cube_definition::{CubeDefinition, NativeCubeDefinition}; +use cubenativeutils::wrappers::serializer::{ + NativeDeserialize, NativeDeserializer, NativeSerialize, +}; +use cubenativeutils::wrappers::NativeArray; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Serialize, Deserialize, Debug)] +pub struct SchemaSourceStatic { + #[serde(rename = "primaryKeys")] + pub primary_keys: HashMap>, +} + +/// Build-phase bridge: feeds `model::ModelBuilder` from JS. +/// +/// Separate from `CubeEvaluator` on purpose. `CubeEvaluator` is the +/// runtime bridge (path lookups, per-query helpers); `SchemaSource` +/// exists only for one-shot schema enumeration during model build. +#[nativebridge::native_bridge(SchemaSourceStatic)] +pub trait SchemaSource { + #[nbridge(vec)] + fn cubes(&self) -> Result>, CubeError>; +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/segment_definition.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/segment_definition.rs index 41ba60c8ca2b0..610907bfa4898 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/segment_definition.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/segment_definition.rs @@ -11,6 +11,10 @@ use std::rc::Rc; #[derive(Serialize, Deserialize, Debug, nativebridge::NativeBridgeStatic)] pub struct SegmentDefinitionStatic { + /// Local name of the segment on its cube. Populated by + /// `prepareMembers` on the JS side. + #[serde(default)] + pub name: String, #[serde(rename = "type")] pub segment_type: Option, #[serde(rename = "ownedByCube")] diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/view_included_member.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/view_included_member.rs new file mode 100644 index 0000000000000..ad209bbc06b16 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/cube_bridge/view_included_member.rs @@ -0,0 +1,23 @@ +use cubenativeutils::wrappers::serializer::{NativeDeserialize, NativeSerialize}; +use cubenativeutils::wrappers::NativeContextHolder; +use cubenativeutils::wrappers::NativeObjectHandle; +use cubenativeutils::CubeError; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::rc::Rc; + +#[derive(Serialize, Deserialize, Debug)] +pub struct ViewIncludedMemberStatic { + /// `measures` | `dimensions` | `segments` | `hierarchies`. + #[serde(rename = "type")] + pub member_kind: String, + /// Path to the source member on the underlying cube + /// (`"Cube.member"`). + #[serde(rename = "memberPath")] + pub member_path: String, + /// Local name surfaced by the view. + pub name: String, +} + +#[nativebridge::native_bridge(ViewIncludedMemberStatic)] +pub trait ViewIncludedMember {} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/lib.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/lib.rs index 527435a4879ee..d2afc12928b56 100644 --- a/rust/cube/cubesqlplanner/cubesqlplanner/src/lib.rs +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/lib.rs @@ -1,5 +1,6 @@ pub mod cube_bridge; pub mod logical_plan; +pub mod model; pub mod physical_plan; pub mod physical_plan_builder; pub mod planner; diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/model/access_policy.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/model/access_policy.rs new file mode 100644 index 0000000000000..a4282b10782d9 --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/model/access_policy.rs @@ -0,0 +1,54 @@ +use super::expression::Expression; +use super::path::MemberPath; + +#[derive(Clone)] +pub struct AccessPolicy { + pub role: Option, + pub group: Option, + pub groups: Vec, + + pub conditions: Vec, + pub row_level: Option, + pub member_level: Option, + pub member_masking: Option, +} + +#[derive(Clone)] +pub struct AccessCondition { + /// `if` callback — evaluated against request context to decide + /// whether the policy applies to a given query. + pub predicate: Expression, +} + +#[derive(Clone)] +pub struct RowLevelAccess { + pub filters: Vec, +} + +#[derive(Clone)] +pub enum AccessFilter { + Member { + member: MemberPath, + operator: String, + values: Vec, + }, + And(Vec), + Or(Vec), +} + +/// Resolved member-level access. `includes` and `excludes` are the +/// `includesMembers` / `excludesMembers` arrays the schema-compiler +/// builds — `'*'` is already expanded to a concrete member list and +/// the missing field becomes an empty vec, so callers don't have to +/// re-resolve the "all" wildcard. +#[derive(Clone)] +pub struct MemberLevelAccess { + pub includes: Vec, + pub excludes: Vec, +} + +#[derive(Clone)] +pub struct MemberMasking { + pub includes: Vec, + pub excludes: Vec, +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/model/builder.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/model/builder.rs new file mode 100644 index 0000000000000..e214c5358287b --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/model/builder.rs @@ -0,0 +1,784 @@ +use super::access_policy::{ + AccessCondition, AccessFilter, AccessPolicy, MemberLevelAccess, MemberMasking, RowLevelAccess, +}; +use super::case::{ + Case, CaseLabel, CaseSwitch, CaseSwitchWhen, CaseVariant as ModelCaseVariant, CaseWhen, +}; +use super::cube::{Cube, SqlSource}; +use super::dimension::{Dimension, DimensionType, Granularity}; +use super::expression::Expression; +use super::join::{Join, Relationship}; +use super::measure::{ + Measure, MeasureOrderBy, MeasureType, MultiStageKind, MultiStageSpec, OrderDirection, + RollingWindowKind, RollingWindowSpec, TimeShiftDirection, TimeShiftSpec, +}; +use super::model::{Model, ModelBuilder}; +use super::path::{CubeName, MemberPath}; +use super::pre_aggregation::{ + EveryInterval, Index, IndexKind, OriginalSqlSpec, PreAggregation, PreAggregationKind, + RefreshKey, RollupSpec, RollupTimeDimension, +}; +use super::segment::Segment; +use super::view::{IncludedMember, IncludedMemberKind, ViewSpec}; +use crate::cube_bridge::access_policy_definition::AccessPolicyDefinition; +use crate::cube_bridge::case_variant::CaseVariant as BridgeCaseVariant; +use crate::cube_bridge::cube_definition::CubeDefinition; +use crate::cube_bridge::cube_join_definition::CubeJoinDefinition; +use crate::cube_bridge::dimension_definition::DimensionDefinition; +use crate::cube_bridge::measure_definition::{ + MeasureDefinition, RollingWindow, TimeShiftReference, +}; +use crate::cube_bridge::pre_aggregation_description::PreAggregationDescription; +use crate::cube_bridge::schema_source::SchemaSource; +use crate::cube_bridge::segment_definition::SegmentDefinition; +use crate::cube_bridge::string_or_sql::StringOrSql; +use crate::cube_bridge::view_included_member::ViewIncludedMember; +use cubenativeutils::CubeError; +use std::collections::HashMap; +use std::rc::Rc; + +/// Builds a `Model` from a `SchemaSource` snapshot of the JS schema. +/// +/// Currently populates the cube skeleton plus measures, dimensions, and +/// segments with their basic fields (path, kind, sql, mask_sql, +/// owned_by_cube, primary_key, multi_stage flag, sub_query, values, +/// alias_member). Cases, multi-stage specs, rolling windows, time +/// shifts, filters, order_by, joins, pre-aggregations, access policies, +/// and view spec are populated in follow-up iterations. +pub struct SchemaModelBuilder { + source: Rc, +} + +impl SchemaModelBuilder { + pub fn new(source: Rc) -> Self { + Self { source } + } + + pub fn build(&self) -> Result { + let mut model = ModelBuilder::new(); + let primary_keys = &self.source.static_data().primary_keys; + for definition in self.source.cubes()? { + let cube = self.build_cube(definition, primary_keys)?; + model.add_cube(Rc::new(cube)); + } + model.build() + } + + fn build_cube( + &self, + definition: Rc, + primary_keys: &HashMap>, + ) -> Result { + let static_data = definition.static_data(); + let cube_name = CubeName::new(static_data.name.clone()); + let source = self.build_source(&definition)?; + let primary_keys = primary_keys + .get(&static_data.name) + .cloned() + .unwrap_or_default(); + + let measures = definition + .measures()? + .into_iter() + .map(|m| { + let measure = Self::build_measure(&cube_name, m)?; + Ok((measure.path.name().to_string(), Rc::new(measure))) + }) + .collect::, CubeError>>()?; + + let dimensions = definition + .dimensions()? + .into_iter() + .map(|d| { + let dimension = Self::build_dimension(&cube_name, d)?; + Ok((dimension.path.name().to_string(), Rc::new(dimension))) + }) + .collect::, CubeError>>()?; + + let segments = definition + .segments()? + .into_iter() + .map(|s| { + let segment = Self::build_segment(&cube_name, s)?; + Ok((segment.path.name().to_string(), Rc::new(segment))) + }) + .collect::, CubeError>>()?; + + let joins = definition + .joins()? + .unwrap_or_default() + .into_iter() + .map(|j| Self::build_join(&cube_name, j)) + .collect::, CubeError>>()?; + + let pre_aggregations = definition + .pre_aggregations()? + .unwrap_or_default() + .into_iter() + .map(|p| { + let pa = Self::build_pre_aggregation(p)?; + Ok((pa.name.clone(), Rc::new(pa))) + }) + .collect::, CubeError>>()?; + + let access_policies = definition + .access_policies()? + .unwrap_or_default() + .into_iter() + .map(Self::build_access_policy) + .collect::, CubeError>>()?; + + let is_view = static_data.is_view.unwrap_or(false); + let view = if is_view { + Some(Self::build_view_spec(&definition)?) + } else { + None + }; + + Ok(Cube { + name: cube_name, + sql_alias: static_data.sql_alias.clone(), + source, + + measures, + dimensions, + segments, + joins, + pre_aggregations, + access_policies, + + primary_keys, + + is_view, + calendar: static_data.is_calendar.unwrap_or(false), + + view, + }) + } + + fn build_source( + &self, + definition: &Rc, + ) -> Result, CubeError> { + if let Some(table) = definition.sql_table()? { + return Ok(Some(SqlSource::Table(Expression::new(table)))); + } + if let Some(query) = definition.sql()? { + return Ok(Some(SqlSource::Query(Expression::new(query)))); + } + Ok(None) + } + + fn build_measure( + cube: &CubeName, + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let case = definition + .case()? + .as_ref() + .map(Self::build_case_variant) + .transpose()?; + let measure_type = MeasureType::parse(&static_data.measure_type)?; + let multi_stage = Self::build_multi_stage_spec(&static_data, &measure_type)?; + let rolling_window = static_data + .rolling_window + .as_ref() + .map(Self::build_rolling_window) + .transpose()?; + let time_shifts = static_data + .time_shift_references + .as_deref() + .map(Self::build_time_shifts) + .transpose()? + .unwrap_or_default(); + let filters = definition + .filters()? + .unwrap_or_default() + .into_iter() + .map(|f| Ok::<_, CubeError>(Expression::new(f.sql()?))) + .collect::, _>>()?; + let drill_filters = definition + .drill_filters()? + .unwrap_or_default() + .into_iter() + .map(|f| Ok::<_, CubeError>(Expression::new(f.sql()?))) + .collect::, _>>()?; + let order_by = definition + .order_by()? + .unwrap_or_default() + .into_iter() + .map(Self::build_order_by) + .collect::, _>>()?; + let alias_member = static_data + .alias_member + .as_deref() + .map(MemberPath::parse) + .transpose()?; + Ok(Measure { + path: MemberPath::new(cube.clone(), static_data.name.clone()), + measure_type, + sql: definition.sql()?.map(Expression::new), + case, + mask_sql: definition.mask_sql()?.map(Expression::new), + owned_by_cube: static_data.owned_by_cube.unwrap_or(false), + primary_key: false, + multi_stage, + rolling_window, + time_shifts, + filters, + drill_filters, + order_by, + alias_member, + }) + } + + fn build_multi_stage_spec( + static_data: &crate::cube_bridge::measure_definition::MeasureDefinitionStatic, + measure_type: &MeasureType, + ) -> Result, CubeError> { + if !static_data.multi_stage.unwrap_or(false) { + return Ok(None); + } + // A multi-stage `rank` measure is a filtering stage; every other + // multi-stage measure type aggregates. + let kind = match measure_type { + MeasureType::Rank => MultiStageKind::Filtering, + _ => MultiStageKind::Aggregating, + }; + let reduce_by = Self::parse_paths(&static_data.reduce_by_references)?; + let group_by = Self::parse_paths(&static_data.group_by_references)?; + let add_group_by = Self::parse_paths(&static_data.add_group_by_references)?; + let time_shifts = static_data + .time_shift_references + .as_deref() + .map(Self::build_time_shifts) + .transpose()? + .unwrap_or_default(); + Ok(Some(MultiStageSpec { + kind, + reduce_by, + group_by, + add_group_by, + time_shifts, + })) + } + + fn parse_paths(refs: &Option>) -> Result, CubeError> { + refs.as_deref() + .map(|v| v.iter().map(|p| MemberPath::parse(p)).collect()) + .unwrap_or_else(|| Ok(Vec::new())) + } + + fn build_rolling_window(window: &RollingWindow) -> Result { + let kind = window + .rolling_type + .as_deref() + .map(|t| match t { + "time" => Ok(RollingWindowKind::Time), + "row" => Ok(RollingWindowKind::Row), + other => Err(CubeError::user(format!( + "Unknown rolling window kind: {other}" + ))), + }) + .transpose()?; + Ok(RollingWindowSpec { + trailing: window.trailing.clone(), + leading: window.leading.clone(), + offset: window.offset.clone(), + kind, + granularity: window.granularity.clone(), + }) + } + + fn build_time_shifts(refs: &[TimeShiftReference]) -> Result, CubeError> { + refs.iter().map(Self::build_time_shift).collect() + } + + fn build_time_shift(reference: &TimeShiftReference) -> Result { + let direction = reference + .shift_type + .as_deref() + .map(|d| match d { + "next" => Ok(TimeShiftDirection::Next), + "prior" => Ok(TimeShiftDirection::Prior), + other => Err(CubeError::user(format!( + "Unknown time shift direction: {other}" + ))), + }) + .transpose()?; + let time_dimension = reference + .time_dimension + .as_deref() + .map(MemberPath::parse) + .transpose()?; + Ok(TimeShiftSpec { + interval: reference.interval.clone(), + name: reference.name.clone(), + direction, + time_dimension, + }) + } + + fn build_order_by( + definition: Rc, + ) -> Result { + let direction = match definition.dir()?.as_str() { + "asc" | "ASC" => OrderDirection::Asc, + "desc" | "DESC" => OrderDirection::Desc, + other => return Err(CubeError::user(format!("Unknown order direction: {other}"))), + }; + Ok(MeasureOrderBy { + sql: Expression::new(definition.sql()?), + direction, + }) + } + + fn build_case_variant(variant: &BridgeCaseVariant) -> Result { + match variant { + BridgeCaseVariant::Case(def) => { + let when = def + .when()? + .into_iter() + .map(|item| -> Result { + Ok(CaseWhen { + sql: Expression::new(item.sql()?), + label: Self::build_case_label(item.label()?), + }) + }) + .collect::, _>>()?; + let else_label = Some(Self::build_case_label(def.else_label()?.label()?)); + Ok(ModelCaseVariant::Predicate(Case { when, else_label })) + } + BridgeCaseVariant::CaseSwitch(def) => { + let selector = Expression::new(def.switch()?); + let when = def + .when()? + .into_iter() + .map(|item| -> Result { + Ok(CaseSwitchWhen { + value: item.static_data().value.clone(), + label: Expression::new(item.sql()?), + }) + }) + .collect::, _>>()?; + let else_label = Some(Expression::new(def.else_sql()?.sql()?)); + Ok(ModelCaseVariant::Switch(CaseSwitch { + selector, + when, + else_label, + })) + } + } + } + + fn build_case_label(label: StringOrSql) -> CaseLabel { + match label { + StringOrSql::String(s) => CaseLabel::String(s), + StringOrSql::MemberSql(member) => { + // StructWithSqlMember holds a `sql` callable behind another + // trait — surface it as Expression. This swallows the + // `sql()` Result; we'd rather fail at build time, but the + // bridge surface returns it eagerly. + match member.sql() { + Ok(sql) => CaseLabel::Sql(Expression::new(sql)), + Err(_) => CaseLabel::String(String::new()), + } + } + } + } + + fn build_dimension( + cube: &CubeName, + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let case = definition + .case()? + .as_ref() + .map(Self::build_case_variant) + .transpose()?; + let latitude = definition + .latitude()? + .map(|g| Ok::<_, CubeError>(Expression::new(g.sql()?))) + .transpose()?; + let longitude = definition + .longitude()? + .map(|g| Ok::<_, CubeError>(Expression::new(g.sql()?))) + .transpose()?; + let add_group_by = Self::parse_paths(&static_data.add_group_by_references)?; + let time_shifts = definition + .time_shift()? + .unwrap_or_default() + .into_iter() + .map(Self::build_dimension_time_shift) + .collect::, _>>()?; + let alias_member = static_data + .alias_member + .as_deref() + .map(MemberPath::parse) + .transpose()?; + let granularities = definition + .granularities()? + .unwrap_or_default() + .into_iter() + .map(Self::build_granularity) + .map(|g| g.map(|gr| (gr.name.clone(), gr))) + .collect::, CubeError>>()?; + Ok(Dimension { + path: MemberPath::new(cube.clone(), static_data.name.clone()), + dimension_type: DimensionType::parse(&static_data.dimension_type)?, + sql: definition.sql()?.map(Expression::new), + case, + mask_sql: definition.mask_sql()?.map(Expression::new), + latitude, + longitude, + primary_key: static_data.primary_key.unwrap_or(false), + owned_by_cube: static_data.owned_by_cube.unwrap_or(false), + sub_query: static_data.sub_query.unwrap_or(false), + propagate_filters_to_sub_query: static_data + .propagate_filters_to_sub_query + .unwrap_or(false), + values: static_data.values.clone().unwrap_or_default(), + multi_stage: static_data.multi_stage.unwrap_or(false), + add_group_by, + time_shifts, + granularities, + alias_member, + }) + } + + fn build_refresh_key( + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let sql = definition.sql()?.map(Expression::new); + // `immutable: true` takes precedence; otherwise an `sql` + // callback means `Sql`-variant (with optional `every` cadence), + // and a bare `every` (no sql) means `Every`-variant. + if static_data.immutable.unwrap_or(false) { + return Ok(RefreshKey::Immutable); + } + if let Some(sql) = sql { + return Ok(RefreshKey::Sql { + sql, + every: static_data + .every + .as_deref() + .map(|s| EveryInterval(s.to_string())), + }); + } + if let Some(every) = static_data.every.as_deref() { + return Ok(RefreshKey::Every { + every: EveryInterval(every.to_string()), + timezone: static_data.timezone.clone(), + incremental: static_data.incremental.unwrap_or(false), + update_window: static_data + .update_window + .as_deref() + .map(|s| EveryInterval(s.to_string())), + }); + } + Err(CubeError::user( + "refresh_key must define one of `sql`, `every`, or `immutable: true`".to_string(), + )) + } + + fn build_granularity( + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let interval = if static_data.interval.is_empty() { + None + } else { + Some(static_data.interval.clone()) + }; + Ok(Granularity { + name: static_data.name.clone(), + interval, + offset: static_data.offset.clone(), + origin: static_data.origin.clone(), + sql: definition.sql()?.map(Expression::new), + }) + } + + fn build_dimension_time_shift( + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let direction = static_data + .timeshift_type + .as_deref() + .map(|d| match d { + "next" => Ok(TimeShiftDirection::Next), + "prior" => Ok(TimeShiftDirection::Prior), + other => Err(CubeError::user(format!( + "Unknown time shift direction: {other}" + ))), + }) + .transpose()?; + Ok(TimeShiftSpec { + interval: static_data.interval.clone(), + name: static_data.name.clone(), + direction, + // Dimension-side time shift does not carry the source time + // dimension on its definition; the planner pairs it with + // the owning time dimension at query time. + time_dimension: None, + }) + } + + fn build_segment( + cube: &CubeName, + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + Ok(Segment { + path: MemberPath::new(cube.clone(), static_data.name.clone()), + sql: Expression::new(definition.sql()?), + owned_by_cube: static_data.owned_by_cube.unwrap_or(false), + }) + } + + fn build_join( + cube: &CubeName, + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + Ok(Join { + from: cube.clone(), + to: CubeName::new(static_data.name.clone()), + relationship: Relationship::parse(&static_data.relationship)?, + sql: Expression::new(definition.sql()?), + }) + } + + fn build_access_policy( + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let member_level = definition + .member_level()? + .map(|m| -> Result { + let s = m.static_data(); + Ok(MemberLevelAccess { + includes: s + .includes_members + .iter() + .map(|p| MemberPath::parse(p)) + .collect::>()?, + excludes: s + .excludes_members + .iter() + .map(|p| MemberPath::parse(p)) + .collect::>()?, + }) + }) + .transpose()?; + let member_masking = definition + .member_masking()? + .map(|m| -> Result { + let s = m.static_data(); + Ok(MemberMasking { + includes: s + .includes_members + .iter() + .map(|p| MemberPath::parse(p)) + .collect::>()?, + excludes: s + .excludes_members + .iter() + .map(|p| MemberPath::parse(p)) + .collect::>()?, + }) + }) + .transpose()?; + let conditions = definition + .conditions()? + .unwrap_or_default() + .into_iter() + .map(|c| -> Result { + Ok(AccessCondition { + predicate: Expression::new(c.predicate()?), + }) + }) + .collect::, _>>()?; + let row_level = definition + .row_level()? + .map(|r| -> Result { + let filters = r + .filters()? + .into_iter() + .map(Self::build_access_filter) + .collect::, _>>()?; + Ok(RowLevelAccess { filters }) + }) + .transpose()?; + Ok(AccessPolicy { + role: static_data.role.clone(), + group: static_data.group.clone(), + groups: static_data.groups.clone(), + conditions, + row_level, + member_level, + member_masking, + }) + } + + fn build_access_filter( + definition: Rc, + ) -> Result { + if let Some(and) = definition.and()? { + let nested = and + .into_iter() + .map(Self::build_access_filter) + .collect::, _>>()?; + return Ok(AccessFilter::And(nested)); + } + if let Some(or) = definition.or()? { + let nested = or + .into_iter() + .map(Self::build_access_filter) + .collect::, _>>()?; + return Ok(AccessFilter::Or(nested)); + } + let static_data = definition.static_data(); + let member_ref = static_data.member_reference.as_deref().ok_or_else(|| { + CubeError::user( + "Access filter leaf must specify `memberReference` (or use and/or grouping)" + .to_string(), + ) + })?; + Ok(AccessFilter::Member { + member: MemberPath::parse(member_ref)?, + operator: static_data.operator.clone().unwrap_or_default(), + values: static_data.values.clone(), + }) + } + + fn build_view_spec(definition: &Rc) -> Result { + let included_members = definition + .included_members()? + .unwrap_or_default() + .into_iter() + .filter_map(|m| Self::build_included_member(m).transpose()) + .collect::, CubeError>>()?; + let join_map = definition + .static_data() + .join_map + .as_ref() + .map(|rows| { + rows.iter() + .map(|row| row.iter().map(|n| CubeName::new(n.clone())).collect()) + .collect() + }) + .unwrap_or_default(); + Ok(ViewSpec { + included_members, + join_map, + }) + } + + fn build_included_member( + definition: Rc, + ) -> Result, CubeError> { + let static_data = definition.static_data(); + let kind = match static_data.member_kind.as_str() { + "measures" => IncludedMemberKind::Measure, + "dimensions" => IncludedMemberKind::Dimension, + "segments" => IncludedMemberKind::Segment, + // Hierarchies are presentation-only metadata and are not + // modeled — a view that includes one contributes no SQL member. + "hierarchies" => return Ok(None), + other => { + return Err(CubeError::user(format!( + "Unknown included member kind: {other}" + ))) + } + }; + Ok(Some(IncludedMember { + kind, + source: MemberPath::parse(&static_data.member_path)?, + name: static_data.name.clone(), + })) + } + + fn build_pre_aggregation( + definition: Rc, + ) -> Result { + let static_data = definition.static_data(); + let kind = PreAggregationKind::parse(&static_data.pre_aggregation_type)?; + let (rollup, original_sql) = if kind.is_rollup_family() { + let time_dimensions = definition + .time_dimension_references()? + .unwrap_or_default() + .into_iter() + .map(|td| -> Result { + Ok(RollupTimeDimension { + dimension: Expression::new(td.dimension()?), + granularity: td.static_data().granularity.clone(), + }) + }) + .collect::, _>>()?; + let rollup = RollupSpec { + measures: definition.measure_references()?.map(Expression::new), + dimensions: definition.dimension_references()?.map(Expression::new), + segments: definition.segment_references()?.map(Expression::new), + rollups: definition.rollup_references()?.map(Expression::new), + time_dimensions, + granularity: static_data.granularity.clone(), + }; + (Some(rollup), None) + } else { + let spec = OriginalSqlSpec { + partition_granularity: static_data.partition_granularity.clone(), + time_dimension: definition.time_dimension_reference()?.map(Expression::new), + }; + (None, Some(spec)) + }; + + let indexes = definition + .indexes()? + .unwrap_or_default() + .into_iter() + .map(|i| -> Result<(String, Index), CubeError> { + let s = i.static_data(); + let kind = s.index_type.as_deref().map(IndexKind::parse).transpose()?; + Ok(( + s.name.clone(), + Index { + name: s.name.clone(), + columns: s.columns.clone(), + kind, + }, + )) + }) + .collect::, _>>()?; + + let refresh_key = definition + .refresh_key()? + .map(Self::build_refresh_key) + .transpose()?; + + Ok(PreAggregation { + name: static_data.name.clone(), + kind, + sql_alias: static_data.sql_alias.clone(), + external: static_data.external, + scheduled_refresh: static_data.scheduled_refresh, + refresh_key, + use_original_sql_pre_aggregations: static_data + .use_original_sql_pre_aggregations + .unwrap_or(false), + allow_non_strict_date_range_match: static_data + .allow_non_strict_date_range_match + .unwrap_or(false), + indexes, + owned_by_cube: static_data.owned_by_cube.unwrap_or(false), + rollup, + original_sql, + build_range_start: definition.build_range_start()?.map(Expression::new), + build_range_end: definition.build_range_end()?.map(Expression::new), + }) + } +} diff --git a/rust/cube/cubesqlplanner/cubesqlplanner/src/model/case.rs b/rust/cube/cubesqlplanner/cubesqlplanner/src/model/case.rs new file mode 100644 index 0000000000000..a551bd3e794cc --- /dev/null +++ b/rust/cube/cubesqlplanner/cubesqlplanner/src/model/case.rs @@ -0,0 +1,42 @@ +use super::expression::Expression; + +#[derive(Clone)] +pub enum CaseLabel { + String(String), + Sql(Expression), +} + +/// Predicate-style `case`: ordered `WHEN THEN