@malloy-publisher/server 0.0.211 → 0.0.212
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/dist/server.mjs
CHANGED
|
@@ -239287,11 +239287,21 @@ function flattenDependsOn(node) {
|
|
|
239287
239287
|
return node.dependsOn.map((d) => d.sourceID);
|
|
239288
239288
|
}
|
|
239289
239289
|
function* iterGraphSources(graph, sources) {
|
|
239290
|
+
const seen = new Set;
|
|
239291
|
+
function* visit(node) {
|
|
239292
|
+
if (seen.has(node.sourceID))
|
|
239293
|
+
return;
|
|
239294
|
+
for (const dep of node.dependsOn) {
|
|
239295
|
+
yield* visit(dep);
|
|
239296
|
+
}
|
|
239297
|
+
seen.add(node.sourceID);
|
|
239298
|
+
const source = sources[node.sourceID];
|
|
239299
|
+
if (source)
|
|
239300
|
+
yield source;
|
|
239301
|
+
}
|
|
239290
239302
|
for (const level of graph.nodes) {
|
|
239291
239303
|
for (const node of level) {
|
|
239292
|
-
|
|
239293
|
-
if (source)
|
|
239294
|
-
yield source;
|
|
239304
|
+
yield* visit(node);
|
|
239295
239305
|
}
|
|
239296
239306
|
}
|
|
239297
239307
|
}
|
package/package.json
CHANGED
|
@@ -45,6 +45,74 @@ describe("iterGraphSources", () => {
|
|
|
45
45
|
);
|
|
46
46
|
expect(names).toEqual(["a", "b"]);
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("walks each root's nested dependsOn tree, deps before dependents", () => {
|
|
50
|
+
// root -> mid -> leaf, with only `root` at the graph's node level — this
|
|
51
|
+
// mirrors malloy getBuildPlan(): terminal persist sources are the nodes,
|
|
52
|
+
// and every transitive persist dependency is nested in dependsOn. All
|
|
53
|
+
// three must be yielded (so all get built), leaf-first so a downstream
|
|
54
|
+
// build reads its upstream's freshly materialized table.
|
|
55
|
+
const root = fakeSource({ name: "root", buildId: "br" });
|
|
56
|
+
const mid = fakeSource({ name: "mid", buildId: "bm" });
|
|
57
|
+
const leaf = fakeSource({ name: "leaf", buildId: "bl" });
|
|
58
|
+
const graph = {
|
|
59
|
+
connectionName: "duckdb",
|
|
60
|
+
nodes: [
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
sourceID: "root@m",
|
|
64
|
+
dependsOn: [
|
|
65
|
+
{
|
|
66
|
+
sourceID: "mid@m",
|
|
67
|
+
dependsOn: [{ sourceID: "leaf@m", dependsOn: [] }],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
],
|
|
73
|
+
} as unknown as MalloyBuildGraph;
|
|
74
|
+
|
|
75
|
+
const names = [
|
|
76
|
+
...iterGraphSources(graph, {
|
|
77
|
+
"root@m": root,
|
|
78
|
+
"mid@m": mid,
|
|
79
|
+
"leaf@m": leaf,
|
|
80
|
+
}),
|
|
81
|
+
].map((s) => s.name);
|
|
82
|
+
expect(names).toEqual(["leaf", "mid", "root"]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("deduplicates a shared (diamond) dependency across roots", () => {
|
|
86
|
+
// r1 and r2 both depend on `shared`; it must be yielded exactly once and
|
|
87
|
+
// before both dependents.
|
|
88
|
+
const r1 = fakeSource({ name: "r1", buildId: "b1" });
|
|
89
|
+
const r2 = fakeSource({ name: "r2", buildId: "b2" });
|
|
90
|
+
const shared = fakeSource({ name: "shared", buildId: "bs" });
|
|
91
|
+
const graph = {
|
|
92
|
+
connectionName: "duckdb",
|
|
93
|
+
nodes: [
|
|
94
|
+
[
|
|
95
|
+
{
|
|
96
|
+
sourceID: "r1@m",
|
|
97
|
+
dependsOn: [{ sourceID: "shared@m", dependsOn: [] }],
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
sourceID: "r2@m",
|
|
101
|
+
dependsOn: [{ sourceID: "shared@m", dependsOn: [] }],
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
],
|
|
105
|
+
} as unknown as MalloyBuildGraph;
|
|
106
|
+
|
|
107
|
+
const names = [
|
|
108
|
+
...iterGraphSources(graph, {
|
|
109
|
+
"r1@m": r1,
|
|
110
|
+
"r2@m": r2,
|
|
111
|
+
"shared@m": shared,
|
|
112
|
+
}),
|
|
113
|
+
].map((s) => s.name);
|
|
114
|
+
expect(names).toEqual(["shared", "r1", "r2"]);
|
|
115
|
+
});
|
|
48
116
|
});
|
|
49
117
|
|
|
50
118
|
describe("deriveAnnotationFields", () => {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
AtomicField,
|
|
3
3
|
BuildGraph as MalloyBuildGraph,
|
|
4
|
+
BuildNode,
|
|
4
5
|
MalloyConfig,
|
|
5
6
|
Connection as MalloyConnection,
|
|
6
7
|
PersistSource,
|
|
@@ -94,19 +95,46 @@ export function flattenDependsOn(node: {
|
|
|
94
95
|
}
|
|
95
96
|
|
|
96
97
|
/**
|
|
97
|
-
* Yield
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* planning and build loops.
|
|
98
|
+
* Yield every persist source of one graph in dependency order — each source's
|
|
99
|
+
* persist dependencies before the source itself — resolving each node's
|
|
100
|
+
* sourceID against the sources map and skipping nodes whose source is absent.
|
|
101
|
+
* Centralizes the graph→source walk shared by the planning and build loops.
|
|
102
|
+
*
|
|
103
|
+
* <p>Malloy's {@code getBuildPlan()} puts only the <em>root</em> persist sources
|
|
104
|
+
* (the terminals nothing else consumes) in {@code graph.nodes}; every transitive
|
|
105
|
+
* persist dependency is nested under a root in its recursive {@code dependsOn}
|
|
106
|
+
* tree (and present in {@code sources}). Walking only {@code graph.nodes} —
|
|
107
|
+
* which this used to do — therefore silently skips every intermediate persist
|
|
108
|
+
* source, so it never gets materialized (it only got a table by coincidence when
|
|
109
|
+
* it shared a buildId with a root). We post-order DFS the {@code dependsOn} tree
|
|
110
|
+
* so dependencies are built first (a downstream build can then read its upstream
|
|
111
|
+
* source's freshly materialized table), deduplicating shared (diamond)
|
|
112
|
+
* dependencies by sourceID so each is yielded once. This mirrors the canonical
|
|
113
|
+
* malloy-cli reference build loop (its {@code flattenBuildNodes} +
|
|
114
|
+
* dedup-by-sourceID; see malloydata/malloy-cli src/malloy/build_graph.ts).
|
|
101
115
|
*/
|
|
102
116
|
export function* iterGraphSources(
|
|
103
117
|
graph: MalloyBuildGraph,
|
|
104
118
|
sources: Record<string, PersistSource>,
|
|
105
119
|
): Iterable<PersistSource> {
|
|
120
|
+
const seen = new Set<string>();
|
|
121
|
+
|
|
122
|
+
function* visit(node: BuildNode): Iterable<PersistSource> {
|
|
123
|
+
if (seen.has(node.sourceID)) return;
|
|
124
|
+
// Dependencies first: a source built later in the run resolves its
|
|
125
|
+
// upstream references against the tables built earlier (see the Manifest
|
|
126
|
+
// threading in executeInstructedBuild).
|
|
127
|
+
for (const dep of node.dependsOn) {
|
|
128
|
+
yield* visit(dep);
|
|
129
|
+
}
|
|
130
|
+
seen.add(node.sourceID);
|
|
131
|
+
const source = sources[node.sourceID];
|
|
132
|
+
if (source) yield source;
|
|
133
|
+
}
|
|
134
|
+
|
|
106
135
|
for (const level of graph.nodes) {
|
|
107
136
|
for (const node of level) {
|
|
108
|
-
|
|
109
|
-
if (source) yield source;
|
|
137
|
+
yield* visit(node);
|
|
110
138
|
}
|
|
111
139
|
}
|
|
112
140
|
}
|
|
@@ -718,6 +718,66 @@ describe("executeInstructedBuild", () => {
|
|
|
718
718
|
expect(entries["carried0"].physicalTableName).toBe("carried_tbl");
|
|
719
719
|
});
|
|
720
720
|
|
|
721
|
+
it("builds an intermediate persist source nested under a root node", async () => {
|
|
722
|
+
// Mirrors malloy getBuildPlan(): only the terminal `root` is a graph
|
|
723
|
+
// node; its persist dependency `mid` is nested in dependsOn (not a node).
|
|
724
|
+
// Before the iterGraphSources fix, `mid` was silently never built even
|
|
725
|
+
// though the caller instructed it. Both must now build, `mid` first.
|
|
726
|
+
const runSQL = sinon.stub().resolves();
|
|
727
|
+
const connection = { runSQL } as unknown as MalloyConnection;
|
|
728
|
+
const root = fakeSource({ name: "root", buildId: "brootaaaaaaaaaa" });
|
|
729
|
+
const mid = fakeSource({ name: "mid", buildId: "bmidbbbbbbbbbbb" });
|
|
730
|
+
const compiled = {
|
|
731
|
+
graphs: [
|
|
732
|
+
{
|
|
733
|
+
connectionName: "duckdb",
|
|
734
|
+
nodes: [
|
|
735
|
+
[
|
|
736
|
+
{
|
|
737
|
+
sourceID: "root",
|
|
738
|
+
dependsOn: [{ sourceID: "mid", dependsOn: [] }],
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
sources: { root, mid },
|
|
745
|
+
connectionDigests: { duckdb: "dig" },
|
|
746
|
+
connections: new Map([["duckdb", connection]]),
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
const entries = await callExecute(
|
|
750
|
+
compiled,
|
|
751
|
+
[
|
|
752
|
+
{
|
|
753
|
+
buildId: "brootaaaaaaaaaa",
|
|
754
|
+
materializedTableId: "mt-r",
|
|
755
|
+
physicalTableName: "root_v1",
|
|
756
|
+
realization: "COPY",
|
|
757
|
+
},
|
|
758
|
+
{
|
|
759
|
+
buildId: "bmidbbbbbbbbbbb",
|
|
760
|
+
materializedTableId: "mt-m",
|
|
761
|
+
physicalTableName: "mid_v1",
|
|
762
|
+
realization: "COPY",
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
{},
|
|
766
|
+
);
|
|
767
|
+
|
|
768
|
+
expect(entries["bmidbbbbbbbbbbb"].physicalTableName).toBe("mid_v1");
|
|
769
|
+
expect(entries["brootaaaaaaaaaa"].physicalTableName).toBe("root_v1");
|
|
770
|
+
// The dependency's CREATE precedes its dependent's (dependency order).
|
|
771
|
+
const creates = runSQL
|
|
772
|
+
.getCalls()
|
|
773
|
+
.map((c) => c.args[0] as string)
|
|
774
|
+
.filter((s) => s.startsWith("CREATE TABLE"));
|
|
775
|
+
const midIdx = creates.findIndex((s) => s.includes("mid_v1"));
|
|
776
|
+
const rootIdx = creates.findIndex((s) => s.includes("root_v1"));
|
|
777
|
+
expect(midIdx).toBeGreaterThanOrEqual(0);
|
|
778
|
+
expect(rootIdx).toBeGreaterThan(midIdx);
|
|
779
|
+
});
|
|
780
|
+
|
|
721
781
|
it("throws when an instructed graph's connection is missing", async () => {
|
|
722
782
|
const s1 = fakeSource({ name: "s1", buildId: "b1aaaaaaaaaaaaaa" });
|
|
723
783
|
const compiled = compiledWith({ s1 }, [["s1"]], new Map());
|
|
@@ -30,6 +30,35 @@ describe("quoteIdentifier", () => {
|
|
|
30
30
|
});
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
+
// Cross-repo conformance (PR ms2data/service#5272 review item 1). The publisher
|
|
34
|
+
// keys identifier quoting off the Malloy `dialectName`; the control plane keys
|
|
35
|
+
// the SAME fact off the publisher *connection type* in
|
|
36
|
+
// `PhysicalTableName.BACKTICK_TYPES` ({bigquery, mysql, databricks}). The two
|
|
37
|
+
// must stay byte-compatible — a name the publisher CREATEs with backticks the CP
|
|
38
|
+
// must DROP with backticks — but they live in different repos under different key
|
|
39
|
+
// vocabularies (`standardsql` the dialect vs `bigquery` the connection type), so
|
|
40
|
+
// drift produces malformed DDL with no compile error. This table pins the full
|
|
41
|
+
// connection-type → dialect → quote-char correspondence; if it changes, update
|
|
42
|
+
// `PhysicalTableName.BACKTICK_TYPES` in the control plane in lockstep.
|
|
43
|
+
describe("dialect/connection-type quoting conformance", () => {
|
|
44
|
+
const QUOTING = [
|
|
45
|
+
{ connectionType: "bigquery", dialect: "standardsql", quote: "`" },
|
|
46
|
+
{ connectionType: "mysql", dialect: "mysql", quote: "`" },
|
|
47
|
+
{ connectionType: "databricks", dialect: "databricks", quote: "`" },
|
|
48
|
+
{ connectionType: "postgres", dialect: "postgres", quote: '"' },
|
|
49
|
+
{ connectionType: "snowflake", dialect: "snowflake", quote: '"' },
|
|
50
|
+
{ connectionType: "trino", dialect: "trino", quote: '"' },
|
|
51
|
+
{ connectionType: "duckdb", dialect: "duckdb", quote: '"' },
|
|
52
|
+
{ connectionType: "motherduck", dialect: "duckdb", quote: '"' },
|
|
53
|
+
] as const;
|
|
54
|
+
|
|
55
|
+
for (const { connectionType, dialect, quote } of QUOTING) {
|
|
56
|
+
it(`${connectionType} (${dialect}) quotes with ${quote}`, () => {
|
|
57
|
+
expect(quoteIdentifier("x", dialect)).toBe(`${quote}x${quote}`);
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
33
62
|
describe("quoteTablePath", () => {
|
|
34
63
|
it("quotes each segment of a container path independently", () => {
|
|
35
64
|
expect(
|
package/src/service/quoting.ts
CHANGED
|
@@ -10,7 +10,10 @@ export function bareTableName(tableName: string): string {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
// Dialects whose identifier quote character is a backtick; everything else uses
|
|
13
|
-
// the SQL-standard double quote. Keyed by Malloy `dialectName`.
|
|
13
|
+
// the SQL-standard double quote. Keyed by Malloy `dialectName`. The control
|
|
14
|
+
// plane encodes the same fact keyed by connection type
|
|
15
|
+
// (`PhysicalTableName.BACKTICK_TYPES` = {bigquery, mysql, databricks}); the two
|
|
16
|
+
// must stay byte-compatible. See the conformance table in quoting.spec.ts.
|
|
14
17
|
const BACKTICK_DIALECTS = new Set(["standardsql", "mysql", "databricks"]);
|
|
15
18
|
|
|
16
19
|
/**
|