@@ -2424,3 +2424,191 @@ describe("Field Mapping Value Transformation", () => {
24242424 // But the column metadata should use the exposed name
24252425 } ) ;
24262426} ) ;
2427+
2428+ describe ( "Internal-only column blocking" , ( ) => {
2429+ /**
2430+ * These tests verify that internal columns (tenant columns, required filter columns)
2431+ * that are NOT exposed in tableSchema.columns are blocked from user queries in
2432+ * SELECT, ORDER BY, GROUP BY, and HAVING clauses, but allowed in system-generated
2433+ * tenant isolation guards.
2434+ */
2435+
2436+ // Schema with hidden tenant columns (not exposed in public columns)
2437+ const hiddenTenantSchema : TableSchema = {
2438+ name : "hidden_tenant_runs" ,
2439+ clickhouseName : "trigger_dev.task_runs_v2" ,
2440+ columns : {
2441+ // Public columns - these are exposed to users
2442+ id : { name : "id" , ...column ( "String" ) } ,
2443+ status : { name : "status" , ...column ( "String" ) } ,
2444+ task_identifier : { name : "task_identifier" , ...column ( "String" ) } ,
2445+ created_at : { name : "created_at" , ...column ( "DateTime64" ) } ,
2446+ } ,
2447+ // Tenant columns are NOT in the public columns above
2448+ tenantColumns : {
2449+ organizationId : "organization_id" ,
2450+ projectId : "project_id" ,
2451+ environmentId : "environment_id" ,
2452+ } ,
2453+ } ;
2454+
2455+ // Schema with hidden required filter column
2456+ const hiddenFilterSchema : TableSchema = {
2457+ name : "hidden_filter_runs" ,
2458+ clickhouseName : "trigger_dev.task_runs_v2" ,
2459+ columns : {
2460+ id : { name : "id" , ...column ( "String" ) } ,
2461+ status : { name : "status" , ...column ( "String" ) } ,
2462+ created_at : { name : "created_at" , ...column ( "DateTime64" ) } ,
2463+ } ,
2464+ tenantColumns : {
2465+ organizationId : "organization_id" ,
2466+ projectId : "project_id" ,
2467+ environmentId : "environment_id" ,
2468+ } ,
2469+ requiredFilters : [
2470+ { column : "engine" , value : "V2" } , // 'engine' is not in public columns
2471+ ] ,
2472+ } ;
2473+
2474+ function createHiddenTenantContext ( ) : PrinterContext {
2475+ const schema = createSchemaRegistry ( [ hiddenTenantSchema ] ) ;
2476+ return createPrinterContext ( {
2477+ organizationId : "org_test" ,
2478+ projectId : "proj_test" ,
2479+ environmentId : "env_test" ,
2480+ schema,
2481+ } ) ;
2482+ }
2483+
2484+ function createHiddenFilterContext ( ) : PrinterContext {
2485+ const schema = createSchemaRegistry ( [ hiddenFilterSchema ] ) ;
2486+ return createPrinterContext ( {
2487+ organizationId : "org_test" ,
2488+ projectId : "proj_test" ,
2489+ environmentId : "env_test" ,
2490+ schema,
2491+ } ) ;
2492+ }
2493+
2494+ describe ( "should block internal-only columns in SELECT" , ( ) => {
2495+ it ( "should throw error when selecting hidden tenant column (organization_id)" , ( ) => {
2496+ const ctx = createHiddenTenantContext ( ) ;
2497+ expect ( ( ) => {
2498+ printQuery ( "SELECT id, organization_id FROM hidden_tenant_runs" , ctx ) ;
2499+ } ) . toThrow ( QueryError ) ;
2500+ expect ( ( ) => {
2501+ printQuery ( "SELECT id, organization_id FROM hidden_tenant_runs" , ctx ) ;
2502+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2503+ } ) ;
2504+
2505+ it ( "should throw error when selecting hidden tenant column (project_id)" , ( ) => {
2506+ const ctx = createHiddenTenantContext ( ) ;
2507+ expect ( ( ) => {
2508+ printQuery ( "SELECT project_id FROM hidden_tenant_runs" , ctx ) ;
2509+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2510+ } ) ;
2511+
2512+ it ( "should throw error when selecting hidden tenant column (environment_id)" , ( ) => {
2513+ const ctx = createHiddenTenantContext ( ) ;
2514+ expect ( ( ) => {
2515+ printQuery ( "SELECT environment_id FROM hidden_tenant_runs" , ctx ) ;
2516+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2517+ } ) ;
2518+
2519+ it ( "should throw error when selecting hidden required filter column" , ( ) => {
2520+ const ctx = createHiddenFilterContext ( ) ;
2521+ expect ( ( ) => {
2522+ printQuery ( "SELECT id, engine FROM hidden_filter_runs" , ctx ) ;
2523+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2524+ } ) ;
2525+ } ) ;
2526+
2527+ describe ( "should block internal-only columns in ORDER BY" , ( ) => {
2528+ it ( "should throw error when ordering by hidden tenant column" , ( ) => {
2529+ const ctx = createHiddenTenantContext ( ) ;
2530+ expect ( ( ) => {
2531+ printQuery ( "SELECT id, status FROM hidden_tenant_runs ORDER BY organization_id" , ctx ) ;
2532+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2533+ } ) ;
2534+
2535+ it ( "should throw error when ordering by hidden required filter column" , ( ) => {
2536+ const ctx = createHiddenFilterContext ( ) ;
2537+ expect ( ( ) => {
2538+ printQuery ( "SELECT id, status FROM hidden_filter_runs ORDER BY engine" , ctx ) ;
2539+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2540+ } ) ;
2541+ } ) ;
2542+
2543+ describe ( "should block internal-only columns in GROUP BY" , ( ) => {
2544+ it ( "should throw error when grouping by hidden tenant column" , ( ) => {
2545+ const ctx = createHiddenTenantContext ( ) ;
2546+ expect ( ( ) => {
2547+ printQuery ( "SELECT count(*) FROM hidden_tenant_runs GROUP BY organization_id" , ctx ) ;
2548+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2549+ } ) ;
2550+
2551+ it ( "should throw error when grouping by hidden required filter column" , ( ) => {
2552+ const ctx = createHiddenFilterContext ( ) ;
2553+ expect ( ( ) => {
2554+ printQuery ( "SELECT count(*) FROM hidden_filter_runs GROUP BY engine" , ctx ) ;
2555+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2556+ } ) ;
2557+ } ) ;
2558+
2559+ describe ( "should block internal-only columns in HAVING" , ( ) => {
2560+ it ( "should throw error when using hidden column in HAVING" , ( ) => {
2561+ const ctx = createHiddenTenantContext ( ) ;
2562+ expect ( ( ) => {
2563+ printQuery (
2564+ "SELECT status, count(*) as cnt FROM hidden_tenant_runs GROUP BY status HAVING organization_id = 'x'" ,
2565+ ctx
2566+ ) ;
2567+ } ) . toThrow ( / n o t a v a i l a b l e f o r q u e r y i n g / i) ;
2568+ } ) ;
2569+ } ) ;
2570+
2571+ describe ( "should allow tenant guard in WHERE clause" , ( ) => {
2572+ it ( "should successfully execute query with tenant isolation (hidden tenant columns work internally)" , ( ) => {
2573+ const ctx = createHiddenTenantContext ( ) ;
2574+ // This should succeed - the tenant guard is injected internally and should work
2575+ const { sql } = printQuery ( "SELECT id, status FROM hidden_tenant_runs" , ctx ) ;
2576+
2577+ // Verify tenant guards are present in WHERE clause
2578+ expect ( sql ) . toContain ( "organization_id" ) ;
2579+ expect ( sql ) . toContain ( "project_id" ) ;
2580+ expect ( sql ) . toContain ( "environment_id" ) ;
2581+ // But NOT in SELECT
2582+ expect ( sql ) . toMatch ( / S E L E C T \s + i d , \s * s t a t u s \s + F R O M / i) ;
2583+ } ) ;
2584+
2585+ it ( "should successfully execute query with required filter (hidden filter columns work internally)" , ( ) => {
2586+ const ctx = createHiddenFilterContext ( ) ;
2587+ const { sql, params } = printQuery ( "SELECT id, status FROM hidden_filter_runs" , ctx ) ;
2588+
2589+ // Verify required filter is present in WHERE clause
2590+ expect ( sql ) . toContain ( "engine" ) ;
2591+ // The value V2 is parameterized, check that it's in the params
2592+ expect ( Object . values ( params ) ) . toContain ( "V2" ) ;
2593+ } ) ;
2594+ } ) ;
2595+
2596+ describe ( "should allow exposed tenant columns" , ( ) => {
2597+ // In task_runs schema, tenant columns ARE exposed in the public columns
2598+ it ( "should allow selecting exposed tenant column" , ( ) => {
2599+ // task_runs schema exposes organization_id in its columns
2600+ const { sql } = printQuery ( "SELECT id, organization_id FROM task_runs" ) ;
2601+ expect ( sql ) . toContain ( "SELECT id, organization_id" ) ;
2602+ } ) ;
2603+
2604+ it ( "should allow ordering by exposed tenant column" , ( ) => {
2605+ const { sql } = printQuery ( "SELECT id, status FROM task_runs ORDER BY organization_id" ) ;
2606+ expect ( sql ) . toContain ( "ORDER BY organization_id" ) ;
2607+ } ) ;
2608+
2609+ it ( "should allow grouping by exposed tenant column" , ( ) => {
2610+ const { sql } = printQuery ( "SELECT organization_id, count(*) FROM task_runs GROUP BY organization_id" ) ;
2611+ expect ( sql ) . toContain ( "GROUP BY organization_id" ) ;
2612+ } ) ;
2613+ } ) ;
2614+ } ) ;
0 commit comments