@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
@@ -5,6 +5,7 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.checkPersistAnnotation = checkPersistAnnotation;
8
+ exports.walkPersistentDependencies = walkPersistentDependencies;
8
9
  exports.findPersistentDependencies = findPersistentDependencies;
9
10
  exports.minimalBuildGraph = minimalBuildGraph;
10
11
  const malloy_types_1 = require("./malloy_types");
@@ -50,15 +51,26 @@ function isPersistent(sourceID, modelDef, tagParseLog) {
50
51
  return value.persist;
51
52
  }
52
53
  /**
53
- * Find persistent dependencies for a source or query, returning a nested DAG.
54
+ * Walk everything a source or query references, and emit the sources that
55
+ * matter to persistence — in dependency order, each one after everything it
56
+ * depends on.
54
57
  *
55
- * Walks the full dependency tree but only includes persistent sources in the
56
- * result. Non-persistent sources are "flattened out" - their persistent
57
- * dependencies bubble up to become direct dependencies of the caller.
58
+ * A source is emitted if it is persistent something to build — or if it
59
+ * is the *route* to something persistent. Everything else is dropped; a
60
+ * source with nothing persistent beneath it is of no interest to anybody.
58
61
  *
59
- * Example: source_c (persist) -> source_b (NOT persist) -> source_a (persist)
60
- * Returns: [{sourceID: source_a, dependsOn: []}]
61
- * (source_b is flattened out, source_a becomes direct dependency)
62
+ * One rule decides both halves, applied on the way back up:
63
+ *
64
+ * keep me if I am persistent, or if any child survived
65
+ *
66
+ * The second clause needs no lookahead. A child only survived under this same
67
+ * rule, so a surviving child *is* the proof that something persistent lies
68
+ * below. For `a(persist) → b → c(persist) → d`, d has nothing beneath it
69
+ * and is dropped, c is kept for being persistent, b is kept for leading to c.
70
+ *
71
+ * Because a node arrives only after everything it depends on, a consumer can
72
+ * fold the stream in a single pass with no second lookup — whether it wants
73
+ * every source touched, or only the ones that are tables.
62
74
  *
63
75
  * ## The 6 Dependency Paths in the IR
64
76
  *
@@ -75,58 +87,91 @@ function isPersistent(sourceID, modelDef, tagParseLog) {
75
87
  * Note: CompositeSourceDef.sources[] is ignored - composite sources and
76
88
  * persistence may be incompatible features.
77
89
  *
78
- * @param root The source or query to find dependencies for
90
+ * Joins are followed whether or not a query activates them, so what comes back
91
+ * is what a source *could* depend on. An unused join is a dependency the SQL
92
+ * will not contain — an over-approximation, which costs ordering freedom and
93
+ * never correctness.
94
+ *
95
+ * @param roots The sources and queries to walk, sharing one memo
79
96
  * @param modelDef The model definition containing the source registry
80
- * @returns Array of BuildNode representing the persistent dependency DAG
97
+ * @param tagParseLog Collects errors from parsing `#@` annotations
81
98
  */
82
- function findPersistentDependencies(root, modelDef, tagParseLog = []) {
83
- const visited = new Set();
84
- function processSourceID(sourceID) {
85
- if (visited.has(sourceID)) {
86
- return [];
99
+ function* walkPersistentDependencies(roots, modelDef, tagParseLog = []) {
100
+ // Memoized, not merely visited: a source reached a second time reports the
101
+ // same answer it gave the first time, so every dependent records the edge
102
+ // rather than only the first one to arrive. The memo spans every root, so a
103
+ // subtree two model objects both reach is walked once and yielded once.
104
+ const done = new Map();
105
+ const openIDs = new Set();
106
+ function* processSourceID(sourceID) {
107
+ const memo = done.get(sourceID);
108
+ if (memo !== undefined) {
109
+ return memo;
87
110
  }
88
- visited.add(sourceID);
89
- const sourceDef = (0, source_def_utils_1.resolveSourceID)(modelDef, sourceID);
90
- if (!sourceDef) {
111
+ // A source cannot reach itself, but a malformed registry could; stop rather
112
+ // than recur forever.
113
+ if (openIDs.has(sourceID)) {
91
114
  return [];
92
115
  }
93
- const childDeps = processSourceDef(sourceDef);
94
- const persistent = isPersistent(sourceID, modelDef, tagParseLog);
95
- if (persistent) {
96
- return [{ sourceID, dependsOn: childDeps }];
116
+ openIDs.add(sourceID);
117
+ const sourceDef = (0, source_def_utils_1.resolveSourceID)(modelDef, sourceID);
118
+ let result = [];
119
+ if (sourceDef) {
120
+ const dependsOn = yield* processSourceDef(sourceDef);
121
+ const persistent = isPersistent(sourceID, modelDef, tagParseLog);
122
+ // Kept for being a table, or for being the road to one. Yielded after
123
+ // its dependencies, which is what lets a consumer fold in one pass.
124
+ if (persistent || dependsOn.length > 0) {
125
+ yield { sourceID, persistent, dependsOn };
126
+ result = [sourceID];
127
+ }
97
128
  }
98
- else {
99
- return childDeps;
129
+ openIDs.delete(sourceID);
130
+ done.set(sourceID, result);
131
+ return result;
132
+ }
133
+ /** Concatenate what several paths found, without repeating a source. */
134
+ function* gather(parts) {
135
+ const found = [];
136
+ const seen = new Set();
137
+ for (const part of parts) {
138
+ for (const id of yield* part) {
139
+ if (!seen.has(id)) {
140
+ seen.add(id);
141
+ found.push(id);
142
+ }
143
+ }
100
144
  }
145
+ return found;
101
146
  }
102
- function processSourceDef(source) {
103
- const results = [];
147
+ function* processSourceDef(source) {
148
+ const parts = [];
104
149
  // Path 4: PersistableSourceDef.extends
105
150
  if ((0, malloy_types_1.isPersistableSourceDef)(source) && source.extends) {
106
- results.push(...processSourceID(source.extends));
151
+ parts.push(processSourceID(source.extends));
107
152
  }
108
153
  // Path 6: QuerySourceDef.query
109
154
  if (source.type === 'query_source') {
110
- results.push(...processQuery(source.query));
155
+ parts.push(processQuery(source.query));
111
156
  }
112
157
  // Path 5: SQLSourceDef.selectSegments[]
113
158
  if (source.type === 'sql_select' && source.selectSegments) {
114
159
  for (const segment of source.selectSegments) {
115
- results.push(...processSQLSegment(segment));
160
+ parts.push(processSQLSegment(segment));
116
161
  }
117
162
  }
118
163
  // Path 3: SourceDef.fields[] - joins defined on the source
119
164
  for (const field of source.fields) {
120
165
  if ((0, malloy_types_1.isJoined)(field) && (0, malloy_types_1.isSourceDef)(field)) {
121
- results.push(...processJoinedSource(field));
166
+ parts.push(processJoinedSource(field));
122
167
  }
123
168
  }
124
- return results;
169
+ return yield* gather(parts);
125
170
  }
126
- function processQuery(query) {
127
- const results = [];
171
+ function* processQuery(query) {
172
+ const parts = [];
128
173
  // Path 1: Query.structRef
129
- results.push(...processStructRef(query.structRef));
174
+ parts.push(processStructRef(query.structRef));
130
175
  // Path 2: Query.pipeline[].extendSource[]
131
176
  for (const segment of query.pipeline) {
132
177
  if (segment.type === 'reduce' ||
@@ -136,80 +181,140 @@ function findPersistentDependencies(root, modelDef, tagParseLog = []) {
136
181
  if (querySegment.extendSource) {
137
182
  for (const field of querySegment.extendSource) {
138
183
  if ((0, malloy_types_1.isJoined)(field) && (0, malloy_types_1.isSourceDef)(field)) {
139
- results.push(...processJoinedSource(field));
184
+ parts.push(processJoinedSource(field));
140
185
  }
141
186
  }
142
187
  }
143
188
  }
144
189
  }
145
- return results;
190
+ return yield* gather(parts);
146
191
  }
147
- function processJoinedSource(source) {
192
+ function* processJoinedSource(source) {
148
193
  // If it has an sourceID, go through the registry
149
194
  if ((0, malloy_types_1.isPersistableSourceDef)(source) && source.sourceID) {
150
- return processSourceID(source.sourceID);
195
+ return yield* processSourceID(source.sourceID);
151
196
  }
152
197
  // Otherwise walk through it transparently
153
- return processSourceDef(source);
198
+ return yield* processSourceDef(source);
154
199
  }
155
- function processStructRef(ref) {
200
+ function* processStructRef(ref) {
156
201
  if (typeof ref === 'string') {
157
202
  const source = resolveSource(modelDef, ref);
158
203
  if (!source)
159
204
  return [];
160
205
  if ((0, malloy_types_1.isPersistableSourceDef)(source) && source.sourceID) {
161
- return processSourceID(source.sourceID);
206
+ return yield* processSourceID(source.sourceID);
162
207
  }
163
- return processSourceDef(source);
208
+ return yield* processSourceDef(source);
164
209
  }
165
210
  else if ((0, malloy_types_1.isSourceDef)(ref)) {
166
211
  if ((0, malloy_types_1.isPersistableSourceDef)(ref) && ref.sourceID) {
167
- return processSourceID(ref.sourceID);
212
+ return yield* processSourceID(ref.sourceID);
168
213
  }
169
- return processSourceDef(ref);
214
+ return yield* processSourceDef(ref);
170
215
  }
171
216
  return [];
172
217
  }
173
- function processSQLSegment(segment) {
218
+ function* processSQLSegment(segment) {
174
219
  if ((0, malloy_types_1.isSegmentSQL)(segment)) {
175
220
  return [];
176
221
  }
177
222
  else if ((0, malloy_types_1.isSegmentSource)(segment)) {
178
223
  if ((0, malloy_types_1.isPersistableSourceDef)(segment) && segment.sourceID) {
179
- return processSourceID(segment.sourceID);
224
+ return yield* processSourceID(segment.sourceID);
180
225
  }
181
- return processSourceDef(segment);
226
+ return yield* processSourceDef(segment);
182
227
  }
183
228
  else {
184
229
  // It's a Query
185
- return processQuery(segment);
230
+ return yield* processQuery(segment);
186
231
  }
187
232
  }
188
- // Entry point: handle both SourceDef and Query
189
- // Query has required 'structRef', SourceDef does not
190
- if ('structRef' in root) {
191
- return processQuery(root);
192
- }
193
- else {
194
- // If the root source itself is persistable and has a sourceID, process it through
195
- // processSourceID so it gets included in the result if persistent
233
+ // Entry point: a Query has a required 'structRef', a SourceDef does not.
234
+ return yield* gather(roots.map(root => {
235
+ if ('structRef' in root) {
236
+ return processQuery(root);
237
+ }
238
+ // A root that is persistable and named goes through processSourceID so
239
+ // it appears in its own right if it is persistent.
196
240
  if ((0, malloy_types_1.isPersistableSourceDef)(root) && root.sourceID) {
197
241
  return processSourceID(root.sourceID);
198
242
  }
199
243
  return processSourceDef(root);
244
+ }));
245
+ }
246
+ /**
247
+ * The persistent sources a source or query depends on, as a nested DAG.
248
+ *
249
+ * A source that is not itself persistent never appears; its persistent
250
+ * dependencies become direct dependencies of whoever referenced it. So
251
+ * `c (persist) → b → a (persist)` yields `[{sourceID: a, dependsOn: []}]`.
252
+ *
253
+ * @deprecated The nested shape exists for `Model.getBuildPlan()`, which is on
254
+ * its way out. Consume {@link walkPersistentDependencies} directly.
255
+ */
256
+ function findPersistentDependencies(root, modelDef, tagParseLog = []) {
257
+ var _a, _b;
258
+ // What each source contributes to whoever referenced it: itself if it is a
259
+ // table, otherwise whatever it was a route to. Post-order means every
260
+ // dependency is already in the map by the time its dependent arrives.
261
+ const contributes = new Map();
262
+ const walk = walkPersistentDependencies([root], modelDef, tagParseLog);
263
+ let step = walk.next();
264
+ while (!step.done) {
265
+ const node = step.value;
266
+ // Two of a source's references can lead to one table — a join and an
267
+ // extend of the same thing, say. That is one dependency, listed once.
268
+ const dependsOn = [];
269
+ const seen = new Set();
270
+ for (const id of node.dependsOn) {
271
+ for (const dep of (_a = contributes.get(id)) !== null && _a !== void 0 ? _a : []) {
272
+ if (!seen.has(dep)) {
273
+ seen.add(dep);
274
+ dependsOn.push(dep);
275
+ }
276
+ }
277
+ }
278
+ contributes.set(node.sourceID, node.persistent ? [{ sourceID: node.sourceID, dependsOn }] : dependsOn);
279
+ step = walk.next();
200
280
  }
281
+ // The generator's return value is what the root itself reached — the only
282
+ // thing a `for...of` would throw away.
283
+ //
284
+ // Deduplicated because two routes can land on one table: a source reached
285
+ // through two different `#@ -persist` wrappers contributes the same node
286
+ // twice, and this function has always returned each node once.
287
+ const roots = [];
288
+ const rootSeen = new Set();
289
+ for (const id of step.value) {
290
+ for (const node of (_b = contributes.get(id)) !== null && _b !== void 0 ? _b : []) {
291
+ if (!rootSeen.has(node)) {
292
+ rootSeen.add(node);
293
+ roots.push(node);
294
+ }
295
+ }
296
+ }
297
+ return roots;
201
298
  }
202
299
  /**
203
300
  * Collect all sourceIDs from a BuildNode forest (for analysis only).
301
+ *
302
+ * A forest shares nodes — one memoized node is reachable from every dependent
303
+ * that reads it — so the walk tracks which nodes it has already descended into.
204
304
  */
205
305
  function collectAllSourceIDs(nodes) {
206
306
  const result = new Set();
207
- for (const node of nodes) {
307
+ const seen = new Set();
308
+ function visit(node) {
309
+ if (seen.has(node))
310
+ return;
311
+ seen.add(node);
208
312
  result.add(node.sourceID);
209
- for (const id of collectAllSourceIDs(node.dependsOn)) {
210
- result.add(id);
211
- }
313
+ for (const dep of node.dependsOn)
314
+ visit(dep);
212
315
  }
316
+ for (const node of nodes)
317
+ visit(node);
213
318
  return result;
214
319
  }
215
320
  /**
@@ -217,14 +322,18 @@ function collectAllSourceIDs(nodes) {
217
322
  */
218
323
  function collectAllDependedOn(nodes) {
219
324
  const result = new Set();
220
- for (const node of nodes) {
325
+ const seen = new Set();
326
+ function visit(node) {
327
+ if (seen.has(node))
328
+ return;
329
+ seen.add(node);
221
330
  for (const dep of node.dependsOn) {
222
331
  result.add(dep.sourceID);
223
- }
224
- for (const id of collectAllDependedOn(node.dependsOn)) {
225
- result.add(id);
332
+ visit(dep);
226
333
  }
227
334
  }
335
+ for (const node of nodes)
336
+ visit(node);
228
337
  return result;
229
338
  }
230
339
  /**
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const MALLOY_VERSION = "0.0.428";
1
+ export declare const MALLOY_VERSION = "0.0.430";
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MALLOY_VERSION = void 0;
4
4
  // generated with 'generate-version-file' script; do not edit manually
5
- exports.MALLOY_VERSION = '0.0.428';
5
+ exports.MALLOY_VERSION = '0.0.430';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@malloydata/malloy",
3
- "version": "0.0.428",
3
+ "version": "0.0.430",
4
4
  "license": "MIT",
5
5
  "exports": {
6
6
  ".": "./dist/index.js",
@@ -51,9 +51,9 @@
51
51
  "generate-version-file": "VERSION=$(npm pkg get version --workspaces=false | tr -d \\\")\necho \"// generated with 'generate-version-file' script; do not edit manually\\nexport const MALLOY_VERSION = '$VERSION';\" > src/version.ts"
52
52
  },
53
53
  "dependencies": {
54
- "@malloydata/malloy-filter": "0.0.428",
55
- "@malloydata/malloy-interfaces": "0.0.428",
56
- "@malloydata/malloy-tag": "0.0.428",
54
+ "@malloydata/malloy-filter": "0.0.430",
55
+ "@malloydata/malloy-interfaces": "0.0.430",
56
+ "@malloydata/malloy-tag": "0.0.430",
57
57
  "@noble/hashes": "^1.8.0",
58
58
  "antlr4ts": "^0.5.0-alpha.4",
59
59
  "assert": "^2.0.0",