|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | +// |
| 3 | +// ADR-0062 D6 — the NativeSQLStrategy must DECLINE federated (external-datasource) |
| 4 | +// objects: its hand-compiled `FROM "<object>"` / bare column refs bypass the |
| 5 | +// driver's physical-table resolution (remoteName/remoteSchema/columnMap) and |
| 6 | +// would query the wrong table. Declining routes the query to the lower-priority |
| 7 | +// ObjectQL aggregate path, which goes through the driver's getBuilder (correct). |
| 8 | + |
| 9 | +import { describe, it, expect } from 'vitest'; |
| 10 | +import { NativeSQLStrategy } from '../strategies/native-sql-strategy.js'; |
| 11 | +import type { StrategyContext } from '../strategies/types.js'; |
| 12 | + |
| 13 | +const baseCaps = { nativeSql: true, objectqlAggregate: true, inMemory: false }; |
| 14 | + |
| 15 | +function ctxFor(cube: any, isExternalObject?: (o: string) => boolean): StrategyContext { |
| 16 | + return { |
| 17 | + getCube: () => cube, |
| 18 | + queryCapabilities: () => baseCaps, |
| 19 | + executeRawSql: async () => [], |
| 20 | + ...(isExternalObject ? { isExternalObject } : {}), |
| 21 | + } as unknown as StrategyContext; |
| 22 | +} |
| 23 | + |
| 24 | +const query = { cube: 'c', measures: ['cnt'], dimensions: ['region'] } as any; |
| 25 | + |
| 26 | +describe('NativeSQLStrategy external-object gate (ADR-0062 D6)', () => { |
| 27 | + const strategy = new NativeSQLStrategy(); |
| 28 | + |
| 29 | + it('DECLINES when the base object is external', () => { |
| 30 | + const cube = { name: 'c', sql: 'ext_customer', dimensions: {}, measures: {} }; |
| 31 | + const ctx = ctxFor(cube, (o) => o === 'ext_customer'); |
| 32 | + expect(strategy.canHandle(query, ctx)).toBe(false); |
| 33 | + }); |
| 34 | + |
| 35 | + it('ACCEPTS a managed object (native-SQL still used)', () => { |
| 36 | + const cube = { name: 'c', sql: 'account', dimensions: {}, measures: {} }; |
| 37 | + const ctx = ctxFor(cube, () => false); |
| 38 | + expect(strategy.canHandle(query, ctx)).toBe(true); |
| 39 | + }); |
| 40 | + |
| 41 | + it('DECLINES when a JOINED object is external (the join would hit the wrong table)', () => { |
| 42 | + const cube = { name: 'c', sql: 'orders', joins: { customer: { name: 'ext_customer' } }, dimensions: {}, measures: {} }; |
| 43 | + const ctx = ctxFor(cube, (o) => o === 'ext_customer'); |
| 44 | + expect(strategy.canHandle(query, ctx)).toBe(false); |
| 45 | + }); |
| 46 | + |
| 47 | + it('is purely additive — with no isExternalObject hook it behaves as before (accept)', () => { |
| 48 | + const cube = { name: 'c', sql: 'ext_customer', dimensions: {}, measures: {} }; |
| 49 | + const ctx = ctxFor(cube); // no hook |
| 50 | + expect(strategy.canHandle(query, ctx)).toBe(true); |
| 51 | + }); |
| 52 | +}); |
0 commit comments