@malloydata/malloy 0.0.428 → 0.0.430

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 (42) hide show
  1. package/dist/api/foundation/build_targets.d.ts +50 -0
  2. package/dist/api/foundation/build_targets.js +221 -0
  3. package/dist/api/foundation/core.d.ts +54 -10
  4. package/dist/api/foundation/core.js +80 -17
  5. package/dist/api/foundation/index.d.ts +1 -1
  6. package/dist/api/foundation/runtime.d.ts +18 -1
  7. package/dist/api/foundation/runtime.js +35 -0
  8. package/dist/api/foundation/types.d.ts +93 -6
  9. package/dist/index.d.ts +1 -1
  10. package/dist/lang/ast/field-space/dynamic-space.js +20 -3
  11. package/dist/lang/ast/query-elements/query-arrow.d.ts +1 -1
  12. package/dist/lang/ast/query-elements/query-arrow.js +10 -4
  13. package/dist/lang/ast/query-elements/query-base.d.ts +1 -1
  14. package/dist/lang/ast/query-elements/query-base.js +1 -6
  15. package/dist/lang/ast/query-elements/query-raw.d.ts +1 -1
  16. package/dist/lang/ast/query-elements/query-raw.js +4 -2
  17. package/dist/lang/ast/query-elements/query-reference.d.ts +1 -1
  18. package/dist/lang/ast/query-elements/query-reference.js +9 -3
  19. package/dist/lang/ast/query-elements/query-refine.d.ts +1 -1
  20. package/dist/lang/ast/query-elements/query-refine.js +10 -4
  21. package/dist/lang/ast/source-elements/query-source.js +1 -1
  22. package/dist/lang/ast/source-elements/sql-source.js +2 -2
  23. package/dist/lang/ast/source-query-elements/source-query-element.d.ts +27 -0
  24. package/dist/lang/ast/source-query-elements/source-query-element.js +33 -0
  25. package/dist/lang/ast/source-query-elements/sq-arrow.d.ts +2 -0
  26. package/dist/lang/ast/source-query-elements/sq-arrow.js +12 -6
  27. package/dist/lang/ast/source-query-elements/sq-refine.d.ts +2 -0
  28. package/dist/lang/ast/source-query-elements/sq-refine.js +12 -6
  29. package/dist/lang/ast/source-query-elements/sq-source.d.ts +1 -0
  30. package/dist/lang/ast/source-query-elements/sq-source.js +6 -3
  31. package/dist/lang/ast/sql-elements/sql-string.d.ts +1 -1
  32. package/dist/lang/ast/sql-elements/sql-string.js +25 -5
  33. package/dist/lang/ast/statements/import-statement.js +7 -10
  34. package/dist/lang/ast/types/query-element.d.ts +15 -1
  35. package/dist/lang/parse-log.d.ts +2 -0
  36. package/dist/lang/test/test-translator.d.ts +17 -0
  37. package/dist/lang/test/test-translator.js +46 -2
  38. package/dist/model/persist_utils.d.ts +61 -10
  39. package/dist/model/persist_utils.js +172 -63
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +4 -4
@@ -35,7 +35,7 @@ class SQLString extends malloy_element_1.MalloyElement {
35
35
  el.logError('invalid-sql-source-interpolation', 'This element is not legal inside an SQL string');
36
36
  }
37
37
  }
38
- sqlPhrases() {
38
+ sqlPhrases(forConnection) {
39
39
  const ret = [];
40
40
  let valid = true;
41
41
  for (const el of this.elements) {
@@ -47,19 +47,39 @@ class SQLString extends malloy_element_1.MalloyElement {
47
47
  const source = el.getSource();
48
48
  if (source) {
49
49
  const sourceDef = source.getSourceDef(undefined);
50
- if ((0, malloy_types_1.isPersistableSourceDef)(sourceDef)) {
50
+ const connectionMismatch = sourceDef.connection !== forConnection;
51
+ const persistable = (0, malloy_types_1.isPersistableSourceDef)(sourceDef);
52
+ if (!connectionMismatch && persistable) {
51
53
  ret.push(sourceDef);
52
54
  continue;
53
55
  }
56
+ valid = false;
57
+ // A source can be wrong in both ways at once, and both are worth
58
+ // saying, so claim the report and then say everything.
59
+ if (el.sqClaimError()) {
60
+ if (connectionMismatch) {
61
+ el.logError('sql-source-connection-mismatch', `Source is on connection ${sourceDef.connection}, but this SQL is for ${forConnection}`);
62
+ }
63
+ if (!persistable) {
64
+ el.logError('invalid-sql-source-interpolation', 'Source is not persistable, cannot be used in SQL');
65
+ }
66
+ }
67
+ }
68
+ else {
69
+ el.sqLog('failed-to-expand-sql-source', 'Cannot expand into a query');
70
+ valid = false;
54
71
  }
55
- el.sqLog('failed-to-expand-sql-source', 'Cannot expand into a query');
56
- valid = false;
57
72
  }
58
73
  else {
59
74
  // Not a source - try as a query
60
75
  const queryObject = el.getQuery();
61
76
  if (queryObject) {
62
- ret.push(queryObject.query(false));
77
+ const queryComp = queryObject.queryComp(false, false);
78
+ if (queryComp.inputStruct.connection !== forConnection) {
79
+ el.sqLog('sql-source-connection-mismatch', `Query is on connection ${queryComp.inputStruct.connection}, but this SQL is for ${forConnection}`);
80
+ valid = false;
81
+ }
82
+ ret.push(queryComp.query);
63
83
  }
64
84
  else {
65
85
  el.sqLog('failed-to-expand-sql-source', 'Cannot expand into a query');
@@ -10,13 +10,6 @@ const malloy_types_1 = require("../../../model/malloy_types");
10
10
  const source_def_utils_1 = require("../../../model/source_def_utils");
11
11
  const persist_utils_1 = require("../../../model/persist_utils");
12
12
  const utils_1 = require("../../../model/utils");
13
- /** Walk BuildNode tree and collect all sourceIDs */
14
- function collectSourceIDs(nodes, into) {
15
- for (const node of nodes) {
16
- into.add(node.sourceID);
17
- collectSourceIDs(node.dependsOn, into);
18
- }
19
- }
20
13
  class ImportSourceName extends malloy_element_1.MalloyElement {
21
14
  constructor(text) {
22
15
  super();
@@ -160,10 +153,14 @@ class ImportStatement extends malloy_element_1.ListOf {
160
153
  const importMe = { ...sourceEntry };
161
154
  importMe.as = dstName;
162
155
  doc.setEntry(dstName, { entry: importMe, exported: false });
163
- // Collect dependencies for persistable sources
156
+ // Every source the walk touched, routes included — not just the
157
+ // persistent ones. A `#@ -persist` wrapper that adds a join is
158
+ // not a table, but it can be the only way to reach the tables
159
+ // under it, and this model has to be able to take the same route.
164
160
  if ((0, malloy_types_1.isSourceDef)(importMe) && (0, malloy_types_1.isPersistableSourceDef)(importMe)) {
165
- const deps = (0, persist_utils_1.findPersistentDependencies)(importMe, importedModel);
166
- collectSourceIDs(deps, neededSourceIDs);
161
+ for (const found of (0, persist_utils_1.walkPersistentDependencies)([importMe], importedModel)) {
162
+ neededSourceIDs.add(found.sourceID);
163
+ }
167
164
  }
168
165
  }
169
166
  }
@@ -2,7 +2,21 @@ import type { MalloyElement } from './malloy-element';
2
2
  import type { Query } from '../../../model/malloy_types';
3
3
  import type { QueryComp } from './query-comp';
4
4
  export interface QueryElement extends MalloyElement {
5
- queryComp(isRefOk: boolean): QueryComp;
5
+ /**
6
+ * Both arguments say what looseness the caller will accept.
7
+ *
8
+ * @param isRefOk `structRef` may be left as the name of a source. False
9
+ * means expand it, which is what a caller writing this query into
10
+ * something that must stand on its own needs.
11
+ * @param isPartialOk The pipeline may contain `partial` segments, whose
12
+ * query class is not decided yet. Only the base of a refinement can
13
+ * accept those, because the refinement is what decides the class.
14
+ * False means the pipeline is finished and the compiler can read it —
15
+ * any partial found is made a `reduce` and reported as
16
+ * `ambiguous-view-type`, so passing false decides where that error
17
+ * lands and when it is logged relative to the rest of the pipeline.
18
+ */
19
+ queryComp(isRefOk: boolean, isPartialOk: boolean): QueryComp;
6
20
  query(isRefOk?: boolean): Query;
7
21
  }
8
22
  export declare function isQueryElement(e: MalloyElement): e is QueryElement;
@@ -248,11 +248,13 @@ type MessageParameterTypes = {
248
248
  'unexpected-source-property': string;
249
249
  'aggregate-in-source-filter': string;
250
250
  'invalid-connection-for-sql-source': string;
251
+ 'sql-source-connection-mismatch': string;
251
252
  'failed-to-fetch-sql-source-schema': string;
252
253
  'invalid-sql-source': string;
253
254
  'non-top-level-sql-source': string;
254
255
  'failed-to-fetch-table-schema': string;
255
256
  'invalid-connection-for-table-source': string;
257
+ 'join-connection-mismatch': string;
256
258
  'join-on-primary-key-type-mismatch': string;
257
259
  'join-primary-key-not-found': string;
258
260
  'join-with-without-primary-key': string;
@@ -19,6 +19,13 @@ export declare function pretty(thing: unknown): string;
19
19
  export declare function humanify(value: unknown): string;
20
20
  export declare const TEST_DIALECT = "duckdb";
21
21
  export declare const aTableDef: TableSourceDef;
22
+ /**
23
+ * A second connection speaking the same dialect as `_db_`. Two sources can
24
+ * then compile to identical SQL on different connections, which isolates the
25
+ * connection check from the dialect check, and is exactly the case where
26
+ * persistence could forget that a build target is a table on a connection.
27
+ */
28
+ export declare const db2TableDef: TableSourceDef;
22
29
  export declare const bqTableDef: SourceDef;
23
30
  export declare const pgTableDef: SourceDef;
24
31
  /**
@@ -128,4 +135,14 @@ export declare function warningMessage(message: string | RegExp): {
128
135
  message: string | RegExp;
129
136
  severity: LogSeverity;
130
137
  };
138
+ /**
139
+ * Drain a translation's SQL schema requests, answering each with the schema of
140
+ * `aTable` on the connection which asked, until it stops asking. A `sql()`
141
+ * source needs a schema before the translation can finish, and nothing in a
142
+ * TestTranslator provides one.
143
+ *
144
+ * @param selectStr What to record as the block's SELECT. Defaults to the
145
+ * statement which was requested; pass a value to hand back something else.
146
+ */
147
+ export declare function answerSQLSchemaRequests(translator: TestTranslator, selectStr?: string): void;
131
148
  export {};
@@ -5,7 +5,7 @@
5
5
  * SPDX-License-Identifier: MIT
6
6
  */
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.BetaExpression = exports.TestTranslator = exports.TestChildTranslator = exports.mockSchema = exports.pgTableDef = exports.bqTableDef = exports.aTableDef = exports.TEST_DIALECT = void 0;
8
+ exports.BetaExpression = exports.TestTranslator = exports.TestChildTranslator = exports.mockSchema = exports.pgTableDef = exports.bqTableDef = exports.db2TableDef = exports.aTableDef = exports.TEST_DIALECT = void 0;
9
9
  exports.pretty = pretty;
10
10
  exports.humanify = humanify;
11
11
  exports.getExplore = getExplore;
@@ -24,6 +24,7 @@ exports.error = error;
24
24
  exports.warning = warning;
25
25
  exports.errorMessage = errorMessage;
26
26
  exports.warningMessage = warningMessage;
27
+ exports.answerSQLSchemaRequests = answerSQLSchemaRequests;
27
28
  const util_1 = require("util");
28
29
  const malloy_types_1 = require("../../model/malloy_types");
29
30
  const ast_1 = require("../ast");
@@ -162,6 +163,16 @@ exports.aTableDef = {
162
163
  { type: 'timestamptz', name: 'atstz' }, // duckdb supports timestamptz
163
164
  ],
164
165
  };
166
+ /**
167
+ * A second connection speaking the same dialect as `_db_`. Two sources can
168
+ * then compile to identical SQL on different connections, which isolates the
169
+ * connection check from the dialect check, and is exactly the case where
170
+ * persistence could forget that a build target is a table on a connection.
171
+ */
172
+ exports.db2TableDef = {
173
+ ...exports.aTableDef,
174
+ connection: '_db2_',
175
+ };
165
176
  // BigQuery-compatible table definition (no timestamptz support)
166
177
  exports.bqTableDef = {
167
178
  type: 'table',
@@ -188,6 +199,7 @@ exports.pgTableDef = {
188
199
  */
189
200
  exports.mockSchema = [
190
201
  exports.aTableDef,
202
+ exports.db2TableDef,
191
203
  exports.bqTableDef,
192
204
  exports.pgTableDef,
193
205
  {
@@ -331,11 +343,15 @@ class TestTranslator extends parse_malloy_1.MalloyTranslator {
331
343
  this.testSrc = testSrc;
332
344
  this.allDialectsEnabled = true;
333
345
  /*
334
- * There are two connections:
346
+ * There are four connections:
335
347
  * _db_ - duckdb dialect, with the following tables ...
336
348
  * aTable, malloytest.carriers, malloytest.flights, malloytest.airports
349
+ * _db2_ - duckdb dialect, with one table, aTable. A second connection
350
+ * on the same dialect as _db_, for cross-connection tests.
337
351
  * _bq_ - bigquery/standardsql dialect, with one table
338
352
  * aTable
353
+ * _pg_ - postgres dialect, with one table
354
+ * aTable
339
355
  *
340
356
  * The "aTable" table is a mocked table with one column of each type.
341
357
  * The _bq_ version does not have the timestamptz column, and when
@@ -361,6 +377,7 @@ class TestTranslator extends parse_malloy_1.MalloyTranslator {
361
377
  ...(0, utils_1.mkModelDef)(testURI, testURI),
362
378
  contents: {
363
379
  _db_: { type: 'connection', name: '_db_' },
380
+ _db2_: { type: 'connection', name: '_db2_' },
364
381
  _bq_: { type: 'connection', name: '_bq_' },
365
382
  _pg_: { type: 'connection', name: '_pg_' },
366
383
  a: { ...exports.aTableDef, primaryKey: 'astr', name: 'a' },
@@ -417,6 +434,7 @@ class TestTranslator extends parse_malloy_1.MalloyTranslator {
417
434
  this.schemaZone.define(`${actualSchema.connection}:${actualSchema.tablePath}`, actualSchema);
418
435
  }
419
436
  this.connectionDialectZone.define('_db_', exports.TEST_DIALECT);
437
+ this.connectionDialectZone.define('_db2_', exports.TEST_DIALECT);
420
438
  this.connectionDialectZone.define('_bq_', 'standardsql');
421
439
  this.connectionDialectZone.define('_pg_', 'postgres');
422
440
  for (const flag of (_e = options.compilerFlags) !== null && _e !== void 0 ? _e : []) {
@@ -694,4 +712,30 @@ function errorMessage(message) {
694
712
  function warningMessage(message) {
695
713
  return { message, severity: 'warn' };
696
714
  }
715
+ /**
716
+ * Drain a translation's SQL schema requests, answering each with the schema of
717
+ * `aTable` on the connection which asked, until it stops asking. A `sql()`
718
+ * source needs a schema before the translation can finish, and nothing in a
719
+ * TestTranslator provides one.
720
+ *
721
+ * @param selectStr What to record as the block's SELECT. Defaults to the
722
+ * statement which was requested; pass a value to hand back something else.
723
+ */
724
+ function answerSQLSchemaRequests(translator, selectStr) {
725
+ for (;;) {
726
+ const compileSQL = translator.translate().compileSQL;
727
+ if (compileSQL === undefined)
728
+ return;
729
+ const key = (0, sql_block_1.sqlKey)(compileSQL.connection, compileSQL.selectStr);
730
+ const schema = {
731
+ type: 'sql_select',
732
+ name: key,
733
+ dialect: exports.TEST_DIALECT,
734
+ connection: compileSQL.connection,
735
+ selectStr: selectStr !== null && selectStr !== void 0 ? selectStr : compileSQL.selectStr,
736
+ fields: exports.aTableDef.fields,
737
+ };
738
+ translator.update({ compileSQL: { [key]: schema } });
739
+ }
740
+ }
697
741
  //# sourceMappingURL=test-translator.js.map
@@ -1,4 +1,4 @@
1
- import type { ModelDef, SourceDef, Query } from './malloy_types';
1
+ import type { ModelDef, SourceDef, Query, SourceID } from './malloy_types';
2
2
  import type { LogMessage } from '../lang';
3
3
  import type { BuildNode } from '../api/foundation/types';
4
4
  /**
@@ -10,15 +10,50 @@ export declare function checkPersistAnnotation(source: SourceDef): {
10
10
  log: LogMessage[];
11
11
  };
12
12
  /**
13
- * Find persistent dependencies for a source or query, returning a nested DAG.
13
+ * One persistable source the walk found, and the sources it directly
14
+ * references.
14
15
  *
15
- * Walks the full dependency tree but only includes persistent sources in the
16
- * result. Non-persistent sources are "flattened out" - their persistent
17
- * dependencies bubble up to become direct dependencies of the caller.
16
+ * Edges are sourceIDs rather than nested nodes, so there is no shared object
17
+ * to be careful about and nothing to recurse through — a consumer folds the
18
+ * stream in one pass.
19
+ */
20
+ export interface PersistNode {
21
+ sourceID: SourceID;
22
+ /**
23
+ * Whether this source is itself something to build. `false` means it is a
24
+ * *route*: it materializes nothing, but it is how the walk reached something
25
+ * that does.
26
+ */
27
+ persistent: boolean;
28
+ /** The sources it reaches directly, each of which was yielded already */
29
+ dependsOn: SourceID[];
30
+ }
31
+ /**
32
+ * A walk in progress: yields a {@link PersistNode} per source kept, and
33
+ * finally returns the sources the roots themselves reached.
34
+ */
35
+ export type PersistWalk = Generator<PersistNode, SourceID[]>;
36
+ /**
37
+ * Walk everything a source or query references, and emit the sources that
38
+ * matter to persistence — in dependency order, each one after everything it
39
+ * depends on.
40
+ *
41
+ * A source is emitted if it is persistent — something to build — or if it
42
+ * is the *route* to something persistent. Everything else is dropped; a
43
+ * source with nothing persistent beneath it is of no interest to anybody.
44
+ *
45
+ * One rule decides both halves, applied on the way back up:
46
+ *
47
+ * keep me if I am persistent, or if any child survived
18
48
  *
19
- * Example: source_c (persist) -> source_b (NOT persist) -> source_a (persist)
20
- * Returns: [{sourceID: source_a, dependsOn: []}]
21
- * (source_b is flattened out, source_a becomes direct dependency)
49
+ * The second clause needs no lookahead. A child only survived under this same
50
+ * rule, so a surviving child *is* the proof that something persistent lies
51
+ * below. For `a(persist) b c(persist) d`, d has nothing beneath it
52
+ * and is dropped, c is kept for being persistent, b is kept for leading to c.
53
+ *
54
+ * Because a node arrives only after everything it depends on, a consumer can
55
+ * fold the stream in a single pass with no second lookup — whether it wants
56
+ * every source touched, or only the ones that are tables.
22
57
  *
23
58
  * ## The 6 Dependency Paths in the IR
24
59
  *
@@ -35,9 +70,25 @@ export declare function checkPersistAnnotation(source: SourceDef): {
35
70
  * Note: CompositeSourceDef.sources[] is ignored - composite sources and
36
71
  * persistence may be incompatible features.
37
72
  *
38
- * @param root The source or query to find dependencies for
73
+ * Joins are followed whether or not a query activates them, so what comes back
74
+ * is what a source *could* depend on. An unused join is a dependency the SQL
75
+ * will not contain — an over-approximation, which costs ordering freedom and
76
+ * never correctness.
77
+ *
78
+ * @param roots The sources and queries to walk, sharing one memo
39
79
  * @param modelDef The model definition containing the source registry
40
- * @returns Array of BuildNode representing the persistent dependency DAG
80
+ * @param tagParseLog Collects errors from parsing `#@` annotations
81
+ */
82
+ export declare function walkPersistentDependencies(roots: (SourceDef | Query)[], modelDef: ModelDef, tagParseLog?: LogMessage[]): PersistWalk;
83
+ /**
84
+ * The persistent sources a source or query depends on, as a nested DAG.
85
+ *
86
+ * A source that is not itself persistent never appears; its persistent
87
+ * dependencies become direct dependencies of whoever referenced it. So
88
+ * `c (persist) → b → a (persist)` yields `[{sourceID: a, dependsOn: []}]`.
89
+ *
90
+ * @deprecated The nested shape exists for `Model.getBuildPlan()`, which is on
91
+ * its way out. Consume {@link walkPersistentDependencies} directly.
41
92
  */
42
93
  export declare function findPersistentDependencies(root: SourceDef | Query, modelDef: ModelDef, tagParseLog?: LogMessage[]): BuildNode[];
43
94
  /**