@malloydata/malloy-tests 0.0.430 → 0.0.431
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -10
- package/src/core/persist.spec.ts +180 -12
package/package.json
CHANGED
|
@@ -20,15 +20,15 @@
|
|
|
20
20
|
"malloyc": "ts-node ../scripts/malloy-to-json"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@malloydata/db-bigquery": "0.0.
|
|
24
|
-
"@malloydata/db-duckdb": "0.0.
|
|
25
|
-
"@malloydata/db-postgres": "0.0.
|
|
26
|
-
"@malloydata/db-snowflake": "0.0.
|
|
27
|
-
"@malloydata/db-trino": "0.0.
|
|
28
|
-
"@malloydata/malloy": "0.0.
|
|
29
|
-
"@malloydata/malloy-tag": "0.0.
|
|
30
|
-
"@malloydata/render": "0.0.
|
|
31
|
-
"@malloydata/render-validator": "0.0.
|
|
23
|
+
"@malloydata/db-bigquery": "0.0.431",
|
|
24
|
+
"@malloydata/db-duckdb": "0.0.431",
|
|
25
|
+
"@malloydata/db-postgres": "0.0.431",
|
|
26
|
+
"@malloydata/db-snowflake": "0.0.431",
|
|
27
|
+
"@malloydata/db-trino": "0.0.431",
|
|
28
|
+
"@malloydata/malloy": "0.0.431",
|
|
29
|
+
"@malloydata/malloy-tag": "0.0.431",
|
|
30
|
+
"@malloydata/render": "0.0.431",
|
|
31
|
+
"@malloydata/render-validator": "0.0.431",
|
|
32
32
|
"events": "^3.3.0",
|
|
33
33
|
"jsdom": "^22.1.0",
|
|
34
34
|
"luxon": "^3.7.2",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"overrides": {
|
|
43
43
|
"typescript": "^6.0.3"
|
|
44
44
|
},
|
|
45
|
-
"version": "0.0.
|
|
45
|
+
"version": "0.0.431"
|
|
46
46
|
}
|
package/src/core/persist.spec.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
BuildManifest,
|
|
11
11
|
BuildPlan,
|
|
12
12
|
BuildTarget,
|
|
13
|
+
VirtualMap,
|
|
13
14
|
} from '@malloydata/malloy';
|
|
14
15
|
import {
|
|
15
16
|
ConnectionRuntime,
|
|
@@ -2305,6 +2306,31 @@ describe('source persistence', () => {
|
|
|
2305
2306
|
).rejects.toThrow('not found in manifest');
|
|
2306
2307
|
});
|
|
2307
2308
|
|
|
2309
|
+
it('the throw names the source when it still has an identity', async () => {
|
|
2310
|
+
const manifest = createManifest();
|
|
2311
|
+
manifest.strict = true;
|
|
2312
|
+
|
|
2313
|
+
await expect(
|
|
2314
|
+
runtimeWithManifest(manifest).loadQuery(strictModelCode).getSQL()
|
|
2315
|
+
).rejects.toThrow(/Persisted source 'by_carrier@/);
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
it('and does not guess a name when the reference has none', async () => {
|
|
2319
|
+
// An inline extend has had its sourceID deleted; `as` and `extends`
|
|
2320
|
+
// survive but name the base, so neither stands in for it.
|
|
2321
|
+
const manifest = createManifest();
|
|
2322
|
+
manifest.strict = true;
|
|
2323
|
+
const inlineExtend = `${CARRIER_STATS_PERSIST_MODEL}
|
|
2324
|
+
run: carrier_stats extend { dimension: loud is upper(carrier) } -> {
|
|
2325
|
+
group_by: loud
|
|
2326
|
+
}
|
|
2327
|
+
`;
|
|
2328
|
+
|
|
2329
|
+
await expect(
|
|
2330
|
+
runtimeWithManifest(manifest).loadQuery(inlineExtend).getSQL()
|
|
2331
|
+
).rejects.toThrow(/Persisted source not found in manifest/);
|
|
2332
|
+
});
|
|
2333
|
+
|
|
2308
2334
|
it('strict manifest with loadError surfaces the load error in the throw', async () => {
|
|
2309
2335
|
// Simulates the runtime auto-read path producing an empty manifest
|
|
2310
2336
|
// because the file was unparseable. The strict throw should mention
|
|
@@ -2472,12 +2498,14 @@ describe('source persistence', () => {
|
|
|
2472
2498
|
""")
|
|
2473
2499
|
`;
|
|
2474
2500
|
|
|
2475
|
-
// A manifest holding a built table for
|
|
2476
|
-
|
|
2501
|
+
// A manifest holding a built table for each named persist source, or for
|
|
2502
|
+
// every one of them when no names are given.
|
|
2503
|
+
async function manifestFor(names?: string[]): Promise<BuildManifest> {
|
|
2477
2504
|
const model = await wrapTestModel(tstRuntime, TWO_KINDS).model.getModel();
|
|
2478
2505
|
const digest = await getDigest();
|
|
2479
2506
|
const manifest = createManifest();
|
|
2480
2507
|
for (const source of Object.values(model.getBuildPlan().sources)) {
|
|
2508
|
+
if (names && !names.includes(source.name)) continue;
|
|
2481
2509
|
addManifestEntry(
|
|
2482
2510
|
manifest,
|
|
2483
2511
|
source.makeBuildId(digest, source.getSQL()),
|
|
@@ -2487,8 +2515,11 @@ describe('source persistence', () => {
|
|
|
2487
2515
|
return manifest;
|
|
2488
2516
|
}
|
|
2489
2517
|
|
|
2490
|
-
async function sqlForQuery(
|
|
2491
|
-
|
|
2518
|
+
async function sqlForQuery(
|
|
2519
|
+
query: string,
|
|
2520
|
+
built?: string[]
|
|
2521
|
+
): Promise<string> {
|
|
2522
|
+
const manifest = await manifestFor(built);
|
|
2492
2523
|
return runtimeWithManifest(manifest)
|
|
2493
2524
|
.loadQuery(`${TWO_KINDS}\n${query}`)
|
|
2494
2525
|
.getSQL();
|
|
@@ -2500,11 +2531,7 @@ describe('source persistence', () => {
|
|
|
2500
2531
|
expect(sql).not.toContain('COUNT(');
|
|
2501
2532
|
});
|
|
2502
2533
|
|
|
2503
|
-
|
|
2504
|
-
// manifest. The two passing tests around these are the controls: the
|
|
2505
|
-
// other persistable kind substitutes, and the same source substitutes
|
|
2506
|
-
// when interpolation reaches it, so the entry and its key are right.
|
|
2507
|
-
it.skip('sql_select reads its built table', async () => {
|
|
2534
|
+
it('sql_select reads its built table', async () => {
|
|
2508
2535
|
const sql = await sqlForQuery('run: ss -> { select: * }');
|
|
2509
2536
|
expect(sql).toContain('cached.ss_table');
|
|
2510
2537
|
expect(sql).not.toContain('COUNT(');
|
|
@@ -2512,12 +2539,14 @@ describe('source persistence', () => {
|
|
|
2512
2539
|
|
|
2513
2540
|
it('sql_select reached by interpolation reads its built table', async () => {
|
|
2514
2541
|
// The manifest key and the entry are right — this path finds them.
|
|
2515
|
-
|
|
2542
|
+
// Only `ss` is built, so via_interp has to reach it rather than being
|
|
2543
|
+
// substituted itself.
|
|
2544
|
+
const sql = await sqlForQuery('run: via_interp -> { select: * }', ['ss']);
|
|
2516
2545
|
expect(sql).toContain('cached.ss_table');
|
|
2517
2546
|
expect(sql).not.toContain('COUNT(');
|
|
2518
2547
|
});
|
|
2519
2548
|
|
|
2520
|
-
it
|
|
2549
|
+
it('strict mode catches a missing sql_select entry', async () => {
|
|
2521
2550
|
// Strict mode's promise is that a persist source with no manifest entry
|
|
2522
2551
|
// throws rather than silently compiling to inline SQL.
|
|
2523
2552
|
const strict = createManifest();
|
|
@@ -2529,7 +2558,54 @@ describe('source persistence', () => {
|
|
|
2529
2558
|
).rejects.toThrow(/not found in manifest/);
|
|
2530
2559
|
});
|
|
2531
2560
|
|
|
2532
|
-
it
|
|
2561
|
+
it('reads the built table, not the SQL that built it', async () => {
|
|
2562
|
+
// Substitution is only real if the rows come from the table. Build one
|
|
2563
|
+
// whose contents disagree with the source's SQL: a query that reads the
|
|
2564
|
+
// table says 'stale', one that recomputes says 'live'. Before #3016 the
|
|
2565
|
+
// sql_select leg always said 'live' — it looked fresh because it was
|
|
2566
|
+
// never reading what had been built.
|
|
2567
|
+
const rwConn = new DuckDBConnection({
|
|
2568
|
+
name: tstDB,
|
|
2569
|
+
databasePath: ':memory:',
|
|
2570
|
+
workingDirectory: 'test/data/duckdb',
|
|
2571
|
+
});
|
|
2572
|
+
try {
|
|
2573
|
+
const modelCode = `${PERSIST_ANNOTATION}
|
|
2574
|
+
#@ persist
|
|
2575
|
+
source: ss is ${tstDB}.sql("""SELECT 'live' as tag""")
|
|
2576
|
+
|
|
2577
|
+
run: ss -> { select: * }
|
|
2578
|
+
`;
|
|
2579
|
+
const rwRuntime = new SingleConnectionRuntime({
|
|
2580
|
+
connection: rwConn,
|
|
2581
|
+
urlReader: testFileSpace,
|
|
2582
|
+
});
|
|
2583
|
+
const model = await rwRuntime.loadModel(modelCode).getModel();
|
|
2584
|
+
const source = Object.values(model.getBuildPlan().sources).find(
|
|
2585
|
+
s => s.name === 'ss'
|
|
2586
|
+
)!;
|
|
2587
|
+
const buildId = source.makeBuildId(rwConn.getDigest(), source.getSQL());
|
|
2588
|
+
|
|
2589
|
+
await rwConn.runSQL(
|
|
2590
|
+
"CREATE TABLE ss_freshness AS SELECT 'stale' as tag"
|
|
2591
|
+
);
|
|
2592
|
+
const manifest = createManifest();
|
|
2593
|
+
addManifestEntry(manifest, buildId, 'ss_freshness');
|
|
2594
|
+
rwRuntime.buildManifest = manifest;
|
|
2595
|
+
|
|
2596
|
+
const built = await rwRuntime.loadQuery(modelCode).run();
|
|
2597
|
+
const recomputed = await rwRuntime
|
|
2598
|
+
.loadQuery(modelCode, {buildManifest: EMPTY_BUILD_MANIFEST})
|
|
2599
|
+
.run();
|
|
2600
|
+
|
|
2601
|
+
expect(built.data.toObject()).toEqual([{tag: 'stale'}]);
|
|
2602
|
+
expect(recomputed.data.toObject()).toEqual([{tag: 'live'}]);
|
|
2603
|
+
} finally {
|
|
2604
|
+
await rwConn.close();
|
|
2605
|
+
}
|
|
2606
|
+
});
|
|
2607
|
+
|
|
2608
|
+
it('a join of both kinds substitutes both', async () => {
|
|
2533
2609
|
const sql = await sqlForQuery(`
|
|
2534
2610
|
run: flights extend {
|
|
2535
2611
|
join_one: q is qs on carrier = q.carrier
|
|
@@ -2544,6 +2620,98 @@ describe('source persistence', () => {
|
|
|
2544
2620
|
});
|
|
2545
2621
|
});
|
|
2546
2622
|
|
|
2623
|
+
describe('BuildID SQL carries what changes the SQL', () => {
|
|
2624
|
+
// The BuildID must ignore the manifest and nothing else. A virtualMap
|
|
2625
|
+
// decides what table a virtual source reads, so a BuildID computed without
|
|
2626
|
+
// it cannot be compiled at all — which is what both sides used to do.
|
|
2627
|
+
const VIRTUAL_MODEL = `${PERSIST_ANNOTATION}
|
|
2628
|
+
##! experimental.virtual_source
|
|
2629
|
+
type: ff is { carrier :: string }
|
|
2630
|
+
source: vf is ${tstDB}.virtual('vflights')::ff
|
|
2631
|
+
|
|
2632
|
+
#@ persist
|
|
2633
|
+
source: by_carrier is vf -> { group_by: carrier }
|
|
2634
|
+
|
|
2635
|
+
run: by_carrier -> { select: * }
|
|
2636
|
+
`;
|
|
2637
|
+
|
|
2638
|
+
const virtualMap: VirtualMap = new Map([
|
|
2639
|
+
[tstDB, new Map([['vflights', 'malloytest.flights']])],
|
|
2640
|
+
]);
|
|
2641
|
+
|
|
2642
|
+
function virtualRuntime() {
|
|
2643
|
+
const rt = new SingleConnectionRuntime({
|
|
2644
|
+
connection: tstRuntime.connection,
|
|
2645
|
+
urlReader: testFileSpace,
|
|
2646
|
+
});
|
|
2647
|
+
rt.virtualMap = virtualMap;
|
|
2648
|
+
return rt;
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
it('a persisted source over a virtual source can be planned', async () => {
|
|
2652
|
+
const rt = virtualRuntime();
|
|
2653
|
+
const model = await rt.loadModel(VIRTUAL_MODEL).getModel();
|
|
2654
|
+
const {connections} = await rt.getBuildTargets(model);
|
|
2655
|
+
expect(connections[0].targets[0].sql).toContain('malloytest.flights');
|
|
2656
|
+
});
|
|
2657
|
+
|
|
2658
|
+
it('a given with an inline default keeps both sides on one key', async () => {
|
|
2659
|
+
// The other half of the rule: `resolvedGivens` also changes the SQL, but
|
|
2660
|
+
// only the compiler can produce one, so neither side resolves givens.
|
|
2661
|
+
// Resolve on one side only and this throws — the builder emits
|
|
2662
|
+
// `'admin'='admin'` where the compiler folds the same expression.
|
|
2663
|
+
const model = `${PERSIST_ANNOTATION}
|
|
2664
|
+
##! experimental.givens
|
|
2665
|
+
given:
|
|
2666
|
+
ROLE :: string is "admin"
|
|
2667
|
+
inline IS_ADMIN :: boolean is $ROLE = 'admin'
|
|
2668
|
+
${FLIGHTS_SOURCE}
|
|
2669
|
+
|
|
2670
|
+
#@ persist
|
|
2671
|
+
source: gated is flights -> {
|
|
2672
|
+
where: $IS_ADMIN
|
|
2673
|
+
group_by: carrier
|
|
2674
|
+
}
|
|
2675
|
+
|
|
2676
|
+
run: gated -> { select: * }
|
|
2677
|
+
`;
|
|
2678
|
+
const rt = new SingleConnectionRuntime({
|
|
2679
|
+
connection: tstRuntime.connection,
|
|
2680
|
+
urlReader: testFileSpace,
|
|
2681
|
+
});
|
|
2682
|
+
const {connections} = await rt.getBuildTargets(
|
|
2683
|
+
await rt.loadModel(model).getModel()
|
|
2684
|
+
);
|
|
2685
|
+
const target = connections[0].targets[0];
|
|
2686
|
+
|
|
2687
|
+
const manifest = createManifest();
|
|
2688
|
+
addManifestEntry(manifest, target.buildId, 'cached.gated');
|
|
2689
|
+
manifest.strict = true;
|
|
2690
|
+
rt.buildManifest = manifest;
|
|
2691
|
+
|
|
2692
|
+
const sql = await rt.loadQuery(model).getSQL();
|
|
2693
|
+
expect(sql).toContain('cached.gated');
|
|
2694
|
+
});
|
|
2695
|
+
|
|
2696
|
+
it('and the query finds the table the plan named', async () => {
|
|
2697
|
+
// The whole point of the pair: the key the builder writes is the key the
|
|
2698
|
+
// compiler recomputes. Build from getBuildTargets, then query.
|
|
2699
|
+
const rt = virtualRuntime();
|
|
2700
|
+
const model = await rt.loadModel(VIRTUAL_MODEL).getModel();
|
|
2701
|
+
const {connections} = await rt.getBuildTargets(model);
|
|
2702
|
+
const target = connections[0].targets[0];
|
|
2703
|
+
|
|
2704
|
+
const manifest = createManifest();
|
|
2705
|
+
addManifestEntry(manifest, target.buildId, 'cached.virtual_by_carrier');
|
|
2706
|
+
manifest.strict = true;
|
|
2707
|
+
rt.buildManifest = manifest;
|
|
2708
|
+
|
|
2709
|
+
const sql = await rt.loadQuery(VIRTUAL_MODEL).getSQL();
|
|
2710
|
+
expect(sql).toContain('cached.virtual_by_carrier');
|
|
2711
|
+
expect(sql).not.toContain('malloytest.flights');
|
|
2712
|
+
});
|
|
2713
|
+
});
|
|
2714
|
+
|
|
2547
2715
|
describe('per-call buildManifest on ModelMaterializer.loadQuery', () => {
|
|
2548
2716
|
const MODEL = `${PERSIST_ANNOTATION}
|
|
2549
2717
|
${FLIGHTS_SOURCE}
|