@malloydata/malloy-tests 0.0.430 → 0.0.432

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.
Files changed (2) hide show
  1. package/package.json +10 -10
  2. 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.430",
24
- "@malloydata/db-duckdb": "0.0.430",
25
- "@malloydata/db-postgres": "0.0.430",
26
- "@malloydata/db-snowflake": "0.0.430",
27
- "@malloydata/db-trino": "0.0.430",
28
- "@malloydata/malloy": "0.0.430",
29
- "@malloydata/malloy-tag": "0.0.430",
30
- "@malloydata/render": "0.0.430",
31
- "@malloydata/render-validator": "0.0.430",
23
+ "@malloydata/db-bigquery": "0.0.432",
24
+ "@malloydata/db-duckdb": "0.0.432",
25
+ "@malloydata/db-postgres": "0.0.432",
26
+ "@malloydata/db-snowflake": "0.0.432",
27
+ "@malloydata/db-trino": "0.0.432",
28
+ "@malloydata/malloy": "0.0.432",
29
+ "@malloydata/malloy-tag": "0.0.432",
30
+ "@malloydata/render": "0.0.432",
31
+ "@malloydata/render-validator": "0.0.432",
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.430"
45
+ "version": "0.0.432"
46
46
  }
@@ -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 every persist source in the model.
2476
- async function manifestForAll(): Promise<BuildManifest> {
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(query: string): Promise<string> {
2491
- const manifest = await manifestForAll();
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
- // Pending #3016 a directly-referenced sql_select never reaches the
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
- const sql = await sqlForQuery('run: via_interp -> { select: * }');
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.skip('strict mode catches a missing sql_select entry', async () => {
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.skip('a join of both kinds substitutes both', async () => {
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}