Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions .github/workflows/bridge-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Bridge tests

on:
pull_request:
paths:
- '.github/workflows/bridge-tests.yml'
- 'rust/cube/cubesqlplanner/**'
- 'rust/cube/cubenativeutils/**'
- 'packages/cubejs-backend-native/src/bridge_test_exports.rs'
- 'packages/cubejs-backend-native/test/bridge/**'
- 'packages/cubejs-backend-native/jest-bridge.config.js'

permissions:
contents: read

jobs:
bridge-tests:
runs-on: ubuntu-24.04
timeout-minutes: 40
name: Bridge tests (debug, --features bridge-test-harness)

steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.90.0
rustflags: ""
cache: false
- uses: Swatinem/rust-cache@v2
with:
workspaces: ./packages/cubejs-backend-native -> target
key: bridge-tests-${{ runner.OS }}-x86_64-unknown-linux-gnu
shared-key: bridge-tests-${{ runner.OS }}-x86_64-unknown-linux-gnu
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 22.x
- name: Set Yarn version
run: yarn policies set-version v1.22.22
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> "$GITHUB_OUTPUT"
- name: Restore yarn cache
uses: actions/cache@v4
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Yarn install
uses: nick-fields/retry@v3
env:
CUBESTORE_SKIP_POST_INSTALL: true
with:
max_attempts: 3
retry_on: error
retry_wait_seconds: 15
timeout_minutes: 20
command: yarn install --frozen-lockfile
- name: Build native (debug, --features bridge-test-harness)
working-directory: ./packages/cubejs-backend-native
run: yarn run native:build-debug-bridge-tests
- name: TypeScript compile (lerna, root)
run: yarn tsc
- name: Run bridge tests
working-directory: ./packages/cubejs-backend-native
run: yarn jest --config jest-bridge.config.js --forceExit
3 changes: 3 additions & 0 deletions packages/cubejs-backend-native/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ neon-debug = []
neon-entrypoint = []
python = ["pyo3", "pyo3-asyncio"]
async-log = ["log_nonblock"]
# Exposes `__testBridge_*` endpoints used by the bridge regression test
# harness. Off by default — never enable in production builds.
bridge-test-harness = []

[lints.clippy]
too_many_arguments = "allow"
19 changes: 19 additions & 0 deletions packages/cubejs-backend-native/jest-bridge.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const base = require('../../jest.base.config');

// Dedicated config for the Tesseract bridge regression test harness.
//
// Intentionally does NOT use `test/snapshotResolver.js` from the main jest
// config — that resolver wipes the entire `test/__snapshots__/` directory
// when running on a non-python build (see `isFallbackBuild()` check there).
// Bridge tests don't use snapshots, but the side effect would still nuke
// unrelated snapshots in this package.

/** @type {import('jest').Config} */
module.exports = {
...base,
rootDir: '.',
roots: [
'<rootDir>/dist/test/bridge/'
],
collectCoverage: false,
};
5 changes: 5 additions & 0 deletions packages/cubejs-backend-native/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,10 @@ module.exports = {
roots: [
'<rootDir>/dist/test/'
],
// Bridge tests live under `dist/test/bridge/` and require a native module
// built with `--features bridge-test-harness`. They are run separately via
// `yarn test:bridge` (see jest-bridge.config.js); excluding them here keeps
// `yarn unit` working with a regular debug build.
testPathIgnorePatterns: ['/dist/test/bridge/'],
snapshotResolver: '<rootDir>/test/snapshotResolver.js',
};
3 changes: 3 additions & 0 deletions packages/cubejs-backend-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
"native:build-release": "npm run native:build -- --release",
"native:build-debug-python": "npm run native:build -- --features python",
"native:build-release-python": "npm run native:build -- --release --features python",
"native:build-debug-bridge-tests": "npm run native:build -- --features bridge-test-harness",
"native:build-release-bridge-tests": "npm run native:build -- --release --features bridge-test-harness",
"postinstall": "post-installer || echo 'Your system is not supported by @cubejs-backend/native, some feature will be unavailable.'",
"test:server": "CUBEJS_NATIVE_INTERNAL_DEBUG=true CUBESQL_LOG_LEVEL=trace CUBESQL_PG_PORT=5555 node dist/test/server.js",
"test:server:stream": "CUBESQL_STREAM_MODE=true CUBESQL_LOG_LEVEL=error CUBESQL_PG_PORT=5555 node dist/test/server.js",
"test:python": "CUBEJS_NATIVE_INTERNAL_DEBUG=true CUBESQL_LOG_LEVEL=trace CUBESQL_PG_PORT=5555 node dist/test/python.js",
"unit": "jest --forceExit",
"test:unit": "yarn run unit",
"test:bridge": "npm run native:build-debug-bridge-tests && npm run tsc && jest --config jest-bridge.config.js --forceExit",
"test:cargo": "cargo test",
"bench": "jest --config jest-bench.config.js --forceExit",
"lint": "eslint test/ js/ --ext .ts",
Expand Down
258 changes: 258 additions & 0 deletions packages/cubejs-backend-native/src/bridge_test_exports.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
//! Test endpoints for the Tesseract bridge layer.
//!
//! These functions are exported on the native module under names prefixed with
//! `__testBridge` (e.g. `__testBridgeCompileMemberSql`). They drive real V8
//! through the production bridge code so
//! that bridge logic can be regression-tested at the unit level rather than
//! only via end-to-end JS planner tests.
//!
//! Stub implementations for trait dependencies (e.g. `BaseTools`) live in this
//! module; they should fail loudly when an unsupported code path is exercised.

use cubenativeutils::wrappers::neon::neon_guarded_funcion_call;
use cubenativeutils::wrappers::object::{NativeArray, NativeFunction, NativeStruct, NativeType};
use cubenativeutils::wrappers::serializer::NativeSerialize;
use cubenativeutils::wrappers::{inner_types::InnerTypes, NativeContextHolder, NativeObjectHandle};
use cubenativeutils::CubeError;
use cubesqlplanner::cube_bridge::base_tools::BaseTools;
use cubesqlplanner::cube_bridge::driver_tools::DriverTools;
use cubesqlplanner::cube_bridge::filter_params_callback::{
FilterParamsCallback, NativeFilterParamsCallback,
};
use cubesqlplanner::cube_bridge::join_definition::JoinDefinition;
use cubesqlplanner::cube_bridge::join_hints::JoinHintItem;
use cubesqlplanner::cube_bridge::member_sql::{
FilterGroupItem, FilterParamsItem, MemberSql, NativeMemberSql, SqlTemplate, SqlTemplateArgs,
};
use cubesqlplanner::cube_bridge::pre_aggregation_obj::PreAggregationObj;
use cubesqlplanner::cube_bridge::security_context::{NativeSecurityContext, SecurityContext};
use cubesqlplanner::cube_bridge::sql_templates_render::SqlTemplatesRender;
use cubesqlplanner::cube_bridge::sql_utils::SqlUtils;
use neon::prelude::*;
use std::any::Any;
use std::rc::Rc;

struct StubBaseTools;

fn stub_err(method: &str) -> CubeError {
CubeError::internal(format!(
"StubBaseTools::{} called from bridge test harness — \
this test path requires a real BaseTools implementation",
method
))
}

impl BaseTools for StubBaseTools {
fn as_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
fn driver_tools(&self, _external: bool) -> Result<Rc<dyn DriverTools>, CubeError> {
Err(stub_err("driver_tools"))
}
fn sql_templates(&self) -> Result<Rc<dyn SqlTemplatesRender>, CubeError> {
Err(stub_err("sql_templates"))
}
fn sql_utils_for_rust(&self) -> Result<Rc<dyn SqlUtils>, CubeError> {
Err(stub_err("sql_utils_for_rust"))
}
fn generate_time_series(
&self,
_granularity: String,
_date_range: Vec<String>,
) -> Result<Vec<Vec<String>>, CubeError> {
Err(stub_err("generate_time_series"))
}
fn generate_custom_time_series(
&self,
_granularity: String,
_date_range: Vec<String>,
_origin: String,
) -> Result<Vec<Vec<String>>, CubeError> {
Err(stub_err("generate_custom_time_series"))
}
fn get_allocated_params(&self) -> Result<Vec<String>, CubeError> {
Err(stub_err("get_allocated_params"))
}
fn all_cube_members(&self, _path: String) -> Result<Vec<String>, CubeError> {
Err(stub_err("all_cube_members"))
}
fn interval_and_minimal_time_unit(&self, _interval: String) -> Result<Vec<String>, CubeError> {
Err(stub_err("interval_and_minimal_time_unit"))
}
fn get_pre_aggregation_by_name(
&self,
_cube_name: String,
_name: String,
) -> Result<Rc<dyn PreAggregationObj>, CubeError> {
Err(stub_err("get_pre_aggregation_by_name"))
}
fn pre_aggregation_table_name(
&self,
_cube_name: String,
_name: String,
) -> Result<String, CubeError> {
Err(stub_err("pre_aggregation_table_name"))
}
fn join_tree_for_hints(
&self,
_hints: Vec<JoinHintItem>,
) -> Result<Rc<dyn JoinDefinition>, CubeError> {
Err(stub_err("join_tree_for_hints"))
}
}

fn handles_to_array<IT: InnerTypes>(
items: Vec<NativeObjectHandle<IT>>,
context: NativeContextHolder<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let arr = context.empty_array()?;
for (i, item) in items.into_iter().enumerate() {
arr.set(i as u32, item)?;
}
Ok(NativeObjectHandle::new(arr.into_object()))
}

fn template_to_native<IT: InnerTypes>(
template: &SqlTemplate,
context: NativeContextHolder<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
match template {
SqlTemplate::String(s) => s.to_native(context),
SqlTemplate::StringVec(strings) => strings.to_native(context),
}
}

fn filter_params_to_native<IT: InnerTypes>(
items: &[FilterParamsItem],
context: NativeContextHolder<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let serialized = items
.iter()
.map(|itm| itm.to_native(context.clone()))
.collect::<Result<Vec<_>, _>>()?;
handles_to_array(serialized, context)
}

fn filter_group_to_native<IT: InnerTypes>(
group: &FilterGroupItem,
context: NativeContextHolder<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let result = context.empty_struct()?;
result.set_field(
"filter_params",
filter_params_to_native(&group.filter_params, context.clone())?,
)?;
Ok(NativeObjectHandle::new(result.into_object()))
}

fn args_to_native<IT: InnerTypes>(
args: &SqlTemplateArgs,
context: NativeContextHolder<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let result = context.empty_struct()?;
result.set_field(
"symbol_paths",
args.symbol_paths.to_native(context.clone())?,
)?;
result.set_field(
"filter_params",
filter_params_to_native(&args.filter_params, context.clone())?,
)?;
let groups = args
.filter_groups
.iter()
.map(|g| filter_group_to_native(g, context.clone()))
.collect::<Result<Vec<_>, _>>()?;
result.set_field("filter_groups", handles_to_array(groups, context.clone())?)?;
let security_context = context.empty_struct()?;
security_context.set_field(
"values",
args.security_context.values.to_native(context.clone())?,
)?;
result.set_field(
"security_context",
NativeObjectHandle::new(security_context.into_object()),
)?;
Ok(NativeObjectHandle::new(result.into_object()))
}

fn compile_member_sql_inner<IT: InnerTypes>(
context_holder: NativeContextHolder<IT>,
js_fn: NativeObjectHandle<IT>,
security_context_obj: NativeObjectHandle<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let member_sql = NativeMemberSql::try_new(js_fn)?;
let security_context: Rc<dyn SecurityContext> =
Rc::new(NativeSecurityContext::try_new(security_context_obj)?);
let base_tools: Rc<dyn BaseTools> = Rc::new(StubBaseTools);

let (template, args) = member_sql.compile_template_sql(base_tools, security_context)?;

let result = context_holder.empty_struct()?;
result.set_field(
"template",
template_to_native(&template, context_holder.clone())?,
)?;
result.set_field("args", args_to_native(&args, context_holder.clone())?)?;
Ok(NativeObjectHandle::new(result.into_object()))
}

fn compile_member_sql(cx: FunctionContext) -> JsResult<JsValue> {
neon_guarded_funcion_call(
cx,
|context_holder: NativeContextHolder<_>,
js_fn: NativeObjectHandle<_>,
security_context_obj: NativeObjectHandle<_>| {
compile_member_sql_inner(context_holder, js_fn, security_context_obj)
},
)
}

fn parse_args_names_inner<IT: InnerTypes>(
context_holder: NativeContextHolder<IT>,
js_fn: NativeObjectHandle<IT>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let func = js_fn.to_function()?;
let names = func.args_names()?;
names.to_native(context_holder)
}

fn parse_args_names(cx: FunctionContext) -> JsResult<JsValue> {
neon_guarded_funcion_call(
cx,
|context_holder: NativeContextHolder<_>, js_fn: NativeObjectHandle<_>| {
parse_args_names_inner(context_holder, js_fn)
},
)
}

fn invoke_filter_params_callback_inner<IT: InnerTypes>(
context_holder: NativeContextHolder<IT>,
js_fn: NativeObjectHandle<IT>,
args: Vec<String>,
) -> Result<NativeObjectHandle<IT>, CubeError> {
let callback = NativeFilterParamsCallback::new(js_fn);
let result = callback.call(&args)?;
result.to_native(context_holder)
}

fn invoke_filter_params_callback(cx: FunctionContext) -> JsResult<JsValue> {
neon_guarded_funcion_call(
cx,
|context_holder: NativeContextHolder<_>,
js_fn: NativeObjectHandle<_>,
args: Vec<String>| {
invoke_filter_params_callback_inner(context_holder, js_fn, args)
},
)
}

pub fn register_module(cx: &mut ModuleContext) -> NeonResult<()> {
cx.export_function("__testBridgeCompileMemberSql", compile_member_sql)?;
cx.export_function("__testBridgeParseArgsNames", parse_args_names)?;
cx.export_function(
"__testBridgeInvokeFilterParamsCallback",
invoke_filter_params_callback,
)?;
Ok(())
}
2 changes: 2 additions & 0 deletions packages/cubejs-backend-native/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
extern crate findshlibs;

pub mod auth;
#[cfg(feature = "bridge-test-harness")]
pub mod bridge_test_exports;
pub mod channel;
pub mod config;
pub mod cross;
Expand Down
Loading
Loading