@ingestron/pack-databricks 1.0.1 → 2.0.0

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.
@@ -0,0 +1,108 @@
1
+ // Values that differ in a PySpark transform's file: Python reads the pipeline's
2
+ // configuration with spark.conf.get (spec/behaviour.md, 3.3). Decorator arguments
3
+ // are evaluated when the file is loaded, so each value they need is read once
4
+ // into a module-level constant and put in an f-string, as Databricks' "Create
5
+ // tables in a for loop" example builds `name=`. Reads for the transform's
6
+ // inputs stay inside the function body.
7
+ /** A double-quoted Python text on one line, as the pack writes them with JSON. */
8
+ const quoted = /"(?:[^"\\\n]|\\.)*"/g;
9
+ /** A marker, with the variable's name. */
10
+ const marker = /⟦([^⟦⟧]+)⟧/;
11
+ /** The function that defines the table: everything before it runs when the file loads. */
12
+ const wrapper = /^def ingestron_/m;
13
+ /** The comment above the module-level constants. */
14
+ const header = "# Values that differ between environments, from the pipeline's configuration.";
15
+ /** The configuration key a variable is read under. */
16
+ export function configurationKey(name) {
17
+ return `ingestron.${name}`;
18
+ }
19
+ /** The module-level constant a variable is read into. */
20
+ function constantOf(name) {
21
+ return `ingestron_${name}`;
22
+ }
23
+ /**
24
+ * The file with each marker inside a double-quoted text read from the configuration:
25
+ * before the table's function, `"⟦schema_silver⟧.totals"` becomes
26
+ * `f"{ingestron_schema_silver}.totals"` with
27
+ * `ingestron_schema_silver = spark.conf.get("ingestron.schema_silver")` above the
28
+ * decorators; inside it, `spark.conf.get("ingestron.schema_bronze") + ".orders"`.
29
+ * Undefined when a marker is anywhere else, which no Python expression can stand for.
30
+ */
31
+ export function pythonValues(text) {
32
+ const at = Math.max(text.search(wrapper), 0);
33
+ const names = [];
34
+ const loaded = text
35
+ .slice(0, at)
36
+ .replace(quoted, (found) => (marker.test(found) ? formatted(found, names) : found));
37
+ const body = text
38
+ .slice(at)
39
+ .replace(quoted, (found) => (marker.test(found) ? joined(found) : found));
40
+ const folded = `${loaded}${body}`;
41
+ if (marker.test(folded))
42
+ return undefined;
43
+ if (names.length === 0)
44
+ return folded;
45
+ const reads = [...new Set(names)].map((name) => `${constantOf(name)} = spark.conf.get("${configurationKey(name)}")`);
46
+ return folded.replace(/^@dp\./m, `${[header, ...reads].join("\n")}\n\n\n@dp.`);
47
+ }
48
+ /** A text as an f-string, each marker as its constant, braces in the text doubled. */
49
+ function formatted(found, names) {
50
+ const parts = found.slice(1, -1).split(new RegExp(marker.source));
51
+ const inner = parts.map((part, index) => {
52
+ if (index % 2 === 0)
53
+ return part.replaceAll("{", "{{").replaceAll("}", "}}");
54
+ names.push(part);
55
+ return `{${constantOf(part)}}`;
56
+ });
57
+ return `f"${inner.join("")}"`;
58
+ }
59
+ function joined(found) {
60
+ const parts = found.slice(1, -1).split(new RegExp(marker.source));
61
+ return parts
62
+ .map((part, index) => index % 2 === 1 ? `spark.conf.get("${configurationKey(part)}")` : part && `"${part}"`)
63
+ .filter(Boolean)
64
+ .join(" + ");
65
+ }
66
+ /**
67
+ * The file as the pipeline runs it, as the pack would have written it with the values:
68
+ * the module-level reads removed and each f-string that uses them a plain text, then
69
+ * each spark.conf.get of an `ingestron.*` key in the body joined into the texts around it.
70
+ */
71
+ export function pythonWithValues(text, values) {
72
+ const constants = {};
73
+ const block = new RegExp(`^${header.replace(/[.()]/g, "\\$&")}\\n((?:\\w+ = spark\\.conf\\.get\\("[^"]+"\\)\\n)+)\\n\\n`, "m");
74
+ const unread = text.replace(block, (found, lines) => {
75
+ for (const [, constant, key] of lines.matchAll(/^(\w+) = spark\.conf\.get\("([^"]+)"\)$/gm))
76
+ if (values[key] !== undefined)
77
+ constants[constant] = values[key];
78
+ return lines.trim().split("\n").length === Object.keys(constants).length ? "" : found;
79
+ });
80
+ const plain = unread.replace(/f"((?:[^"\\\n]|\\.)*)"/g, (found, inner) => Object.keys(constants).some((name) => inner.includes(`{${name}}`))
81
+ ? `"${unformatted(inner, constants)}"`
82
+ : found);
83
+ return plain
84
+ .split("\n")
85
+ .map((line) => withReads(line, values))
86
+ .join("\n");
87
+ }
88
+ function unformatted(inner, constants) {
89
+ return inner.replace(/\{\{|\}\}|\{(\w+)\}/g, (found, name) => {
90
+ if (found === "{{")
91
+ return "{";
92
+ if (found === "}}")
93
+ return "}";
94
+ const value = name === undefined ? undefined : constants[name];
95
+ return value === undefined ? found : JSON.stringify(value).slice(1, -1);
96
+ });
97
+ }
98
+ /** A body line's spark.conf.get reads as their values, joined into the texts beside them. */
99
+ function withReads(line, values) {
100
+ if (!line.includes('spark.conf.get("ingestron.'))
101
+ return line;
102
+ let merged = line.replace(/spark\.conf\.get\("([^"]+)"\)/g, (found, key) => values[key] === undefined ? found : JSON.stringify(values[key]));
103
+ for (let previous = ""; previous !== merged;) {
104
+ previous = merged;
105
+ merged = merged.replace(/"((?:[^"\\]|\\.)*)" \+ "((?:[^"\\]|\\.)*)"/, '"$1$2"');
106
+ }
107
+ return merged;
108
+ }
@@ -0,0 +1,29 @@
1
+ import { type Data } from "./bundle.js";
2
+ import type { Plan } from "./fold-plan.js";
3
+ import { type Variable } from "./fold-values.js";
4
+ /** Each target's settings, and the deploy-time value each target's host is, if any. */
5
+ export interface Targets {
6
+ settings: Record<string, Data>;
7
+ /** NAME of the deploy-time value that is a target's whole host, by target. */
8
+ hosts: Record<string, string>;
9
+ }
10
+ /** The settings of every target, before the variables are set. */
11
+ export declare function targetSettings(plan: Plan): Targets;
12
+ /** How the refresh job's schedule is paused in each target. */
13
+ export type Pause = {
14
+ kind: "same";
15
+ } | {
16
+ kind: "presets";
17
+ presets: Record<string, "PAUSED" | "UNPAUSED">;
18
+ } | {
19
+ kind: "variable";
20
+ variable: Variable;
21
+ };
22
+ /**
23
+ * When the schedule's pause differs: a preset only where the target's mode does not
24
+ * pause it by itself, or a variable when a preset would also change a continuous
25
+ * pipeline. Databricks sets `continuous: false` on every pipeline under a PAUSED
26
+ * preset, and development mode does the same unless an UNPAUSED preset replaces its
27
+ * own PAUSED one.
28
+ */
29
+ export declare function pauseOf(plan: Plan): Pause;
@@ -0,0 +1,97 @@
1
+ // Each target's own settings in the folded bundle: what Databricks takes per
2
+ // target (mode, workspace, run_as, permissions) as each target's values, and
3
+ // presets for the tags and schedule pause that differ (spec/behaviour.md, 2.3).
4
+ import { splitDeployText } from "@ingestron/definitions";
5
+ import { targetOf } from "./bundle.js";
6
+ import { hasContinuous } from "./connect.js";
7
+ import { mapStrings } from "./walk.js";
8
+ import { deployProblem, deployVariable, placeVariable, supply, } from "./fold-values.js";
9
+ /** The settings of every target, before the variables are set. */
10
+ export function targetSettings(plan) {
11
+ const hosts = {};
12
+ const settings = Object.fromEntries(plan.targets.map((target, index) => {
13
+ const model = plan.models[target];
14
+ const own = targetOf(model);
15
+ const host = hostOf(plan, target, own);
16
+ if (host)
17
+ hosts[target] = host;
18
+ const tags = plan.tags[target];
19
+ const data = {
20
+ mode: own.mode,
21
+ ...(index === 0 && { default: true }),
22
+ ...withVariables(plan, target, own),
23
+ ...(Object.keys(tags).length > 0 && {
24
+ presets: { tags: withVariables(plan, target, tags) },
25
+ }),
26
+ };
27
+ return [target, data];
28
+ }));
29
+ return { settings, hosts };
30
+ }
31
+ /**
32
+ * A host supplied when deploying: Databricks takes no variable there, so the target
33
+ * leaves the host out and the pipeline sets DATABRICKS_HOST.
34
+ */
35
+ function hostOf(plan, target, settings) {
36
+ const workspace = settings.workspace;
37
+ const model = plan.models[target];
38
+ const parts = splitDeployText(workspace?.host ?? "", model.deployValues ?? []);
39
+ const value = parts.find((part) => typeof part !== "string");
40
+ if (!value || typeof value === "string")
41
+ return undefined;
42
+ delete workspace.host;
43
+ if (Object.keys(workspace).length === 0)
44
+ delete settings.workspace;
45
+ if (parts.length === 1)
46
+ return value.name;
47
+ plan.problems.push(deployProblem(`The workspace host in ${target} holds \${envvar:${value.name}} within other text; Databricks takes a host supplied when deploying only as a whole, from DATABRICKS_HOST.`, model, value.name));
48
+ return undefined;
49
+ }
50
+ /** Each deploy-time value in a target's own settings becomes its variable, which the target leaves unset. */
51
+ function withVariables(plan, target, node) {
52
+ return mapStrings(node, (text) => replaced(plan, target, plan.models[target], text));
53
+ }
54
+ function replaced(plan, target, model, text) {
55
+ return splitDeployText(text, model.deployValues ?? [])
56
+ .map((part) => {
57
+ if (typeof part === "string")
58
+ return part;
59
+ const variable = deployVariable(plan.variables, part.name);
60
+ plan.problems.push(...supply(variable, target, model));
61
+ return `\${var.${variable.name}}`;
62
+ })
63
+ .join("");
64
+ }
65
+ /**
66
+ * When the schedule's pause differs: a preset only where the target's mode does not
67
+ * pause it by itself, or a variable when a preset would also change a continuous
68
+ * pipeline. Databricks sets `continuous: false` on every pipeline under a PAUSED
69
+ * preset, and development mode does the same unless an UNPAUSED preset replaces its
70
+ * own PAUSED one.
71
+ */
72
+ export function pauseOf(plan) {
73
+ const schedules = plan.targets.map((target) => plan.models[target].platform.schedule);
74
+ if (schedules.some((schedule) => !schedule))
75
+ return { kind: "same" };
76
+ if (new Set(schedules.map((schedule) => schedule.paused)).size === 1)
77
+ return { kind: "same" };
78
+ const presets = {};
79
+ for (const target of plan.targets) {
80
+ const { mode, schedule } = plan.models[target].platform;
81
+ if (mode === "production" && schedule.paused)
82
+ presets[target] = "PAUSED";
83
+ if (mode === "development" && !schedule.paused)
84
+ presets[target] = "UNPAUSED";
85
+ }
86
+ if (!Object.keys(presets).some((target) => hasContinuous(plan.models[target])))
87
+ return { kind: "presets", presets };
88
+ const values = Object.fromEntries(plan.targets.map((target) => [
89
+ target,
90
+ plan.models[target].platform.schedule.paused ? "PAUSED" : "UNPAUSED",
91
+ ]));
92
+ const place = {
93
+ name: "pause_status",
94
+ description: "Whether the refresh job's schedule is paused.",
95
+ };
96
+ return { kind: "variable", variable: placeVariable(plan.variables, place, values) };
97
+ }
@@ -0,0 +1,17 @@
1
+ import { type EnvironmentsProblem, type ResolvedModel } from "@ingestron/definitions";
2
+ import { type Variables } from "./fold-values.js";
3
+ import type { Place } from "./fold-names.js";
4
+ /** What folding a text needs, and where it reports problems. */
5
+ export interface TextFold {
6
+ targets: string[];
7
+ models: Readonly<Record<string, ResolvedModel>>;
8
+ variables: Variables;
9
+ problems: EnvironmentsProblem[];
10
+ }
11
+ /** The folded text for one place, given each target's text; `place` names a new variable. */
12
+ export declare function foldText(fold: TextFold, values: Record<string, string>, place: Place): string;
13
+ /**
14
+ * Code folded token by token: each run of tokens that differs is one value. Undefined when
15
+ * the targets' code does not have the same number of tokens, so differs in more than values.
16
+ */
17
+ export declare function foldCode(fold: TextFold, values: Record<string, string>, place: (count: number) => Place): string | undefined;
@@ -0,0 +1,87 @@
1
+ // Folding one text that differs between targets: a variable for the whole
2
+ // value, or, where a target supplies part of it when deploying, a variable for
3
+ // that part with the rest literal. Code is folded token by token.
4
+ import { splitDeployText, } from "@ingestron/definitions";
5
+ import { deployProblem, deployVariable, marker, placeVariable, setIn, supply, } from "./fold-values.js";
6
+ /** The folded text for one place, given each target's text; `place` names a new variable. */
7
+ export function foldText(fold, values, place) {
8
+ const parts = (target) => splitDeployText(values[target], fold.models[target].deployValues ?? []);
9
+ const supplying = fold.targets.filter((target) => parts(target).some((part) => typeof part !== "string"));
10
+ if (supplying.length === 0)
11
+ return marker(placeVariable(fold.variables, place, values).name);
12
+ const [first, ...others] = supplying;
13
+ const model = fold.models[first];
14
+ const pattern = parts(first);
15
+ const names = pattern.flatMap((part) => (typeof part === "string" ? [] : [part.name]));
16
+ const other = others.find((target) => values[target] !== values[first]);
17
+ if (other) {
18
+ fold.problems.push(deployProblem(`This value is "${values[first]}" in ${first} and "${values[other]}" in ${other}; a value supplied when deploying must be written the same way in every environment that supplies it.`, model, names[0]));
19
+ return values[first];
20
+ }
21
+ for (const target of supplying)
22
+ for (const name of names)
23
+ fold.problems.push(...supply(deployVariable(fold.variables, name), target, fold.models[target]));
24
+ const literal = fold.targets.filter((target) => !supplying.includes(target));
25
+ for (const target of literal)
26
+ fold.problems.push(...capture(fold, values, target, pattern, first));
27
+ return pattern
28
+ .map((part) => typeof part === "string" ? part : marker(deployVariable(fold.variables, part.name).name))
29
+ .join("");
30
+ }
31
+ /** A literal target sets the one deploy-time value to its text between the literal parts. */
32
+ function capture(fold, values, target, pattern, supplier) {
33
+ const text = values[target];
34
+ const at = pattern.findIndex((part) => typeof part !== "string");
35
+ const name = pattern[at].name;
36
+ const model = fold.models[supplier];
37
+ if (pattern.filter((part) => typeof part !== "string").length > 1)
38
+ return [
39
+ deployProblem(`This value is "${text}" in ${target} and "${values[supplier]}" in ${supplier}, which holds more than one value supplied when deploying; write it the same way in every environment.`, model, name),
40
+ ];
41
+ const before = pattern.slice(0, at).join("");
42
+ const after = pattern.slice(at + 1).join("");
43
+ const fits = text.length >= before.length + after.length && text.startsWith(before) && text.endsWith(after);
44
+ if (!fits)
45
+ return [
46
+ deployProblem(`This value is "${text}" in ${target}, which does not have the form "${values[supplier]}" of ${supplier}, so \${envvar:${name}} cannot stand for part of it.`, model, name),
47
+ ];
48
+ const middle = text.slice(before.length, text.length - after.length);
49
+ return setIn(deployVariable(fold.variables, name), target, middle, fold.models[target]);
50
+ }
51
+ /** Words, runs of space, and single other characters; a deploy-time value is one token. */
52
+ function tokens(text, model) {
53
+ return splitDeployText(text, model.deployValues ?? []).flatMap((part) => typeof part === "string"
54
+ ? (part.match(/[A-Za-z0-9_]+|\s+|[^A-Za-z0-9_\s]/g) ?? [])
55
+ : [part.text]);
56
+ }
57
+ /**
58
+ * Code folded token by token: each run of tokens that differs is one value. Undefined when
59
+ * the targets' code does not have the same number of tokens, so differs in more than values.
60
+ */
61
+ export function foldCode(fold, values, place) {
62
+ const lists = fold.targets.map((target) => tokens(values[target], fold.models[target]));
63
+ if (new Set(lists.map((list) => list.length)).size > 1)
64
+ return undefined;
65
+ const parts = [];
66
+ let run = [];
67
+ let count = 0;
68
+ const flush = () => {
69
+ if (run.length === 0)
70
+ return;
71
+ const texts = Object.fromEntries(fold.targets.map((target, index) => [target, run.map((tokens) => tokens[index]).join("")]));
72
+ count += 1;
73
+ parts.push(foldText(fold, texts, place(count)));
74
+ run = [];
75
+ };
76
+ lists[0].forEach((token, position) => {
77
+ const column = lists.map((list) => list[position]);
78
+ if (new Set(column).size === 1) {
79
+ flush();
80
+ parts.push(token);
81
+ }
82
+ else
83
+ run.push(column);
84
+ });
85
+ flush();
86
+ return parts.join("");
87
+ }
@@ -0,0 +1,54 @@
1
+ import type { EnvironmentsProblem, ResolvedModel } from "@ingestron/definitions";
2
+ /** The targets' models, by target name, in project.yaml's order. */
3
+ export type Models = Readonly<Record<string, ResolvedModel>>;
4
+ /** One bundle variable. */
5
+ export interface Variable {
6
+ name: string;
7
+ description: string;
8
+ /** A list, declared `type: complex`. */
9
+ complex?: boolean;
10
+ /** Each target's value; a target not here leaves it to the deploying pipeline. */
11
+ values: Record<string, unknown>;
12
+ /** For a deploy-time value: its NAME, as in `${envvar:NAME}`. */
13
+ deploy?: string;
14
+ /** Targets in which the deploying pipeline must supply it. */
15
+ supplied: Set<string>;
16
+ }
17
+ /** Every variable of a fold, in the order made. */
18
+ export interface Variables {
19
+ list: Variable[];
20
+ /** Variables for values that differ, by the name their place gives and their values in target order. */
21
+ byValues: Map<string, Variable>;
22
+ }
23
+ /** The marker that stands for a variable while folding. */
24
+ export declare function marker(name: string): string;
25
+ /** Each marker in a text, with the variable's name. */
26
+ export declare const markers: RegExp;
27
+ /** True when a text holds a marker. */
28
+ export declare function hasMarker(text: string): boolean;
29
+ /** A variable's name from a deploy-time value's NAME: lower case, `_` for other characters. */
30
+ export declare function deployName(name: string): string;
31
+ /**
32
+ * The variable for a value that differs: places of the same kind (the same name) with the
33
+ * same value in every target share one, such as the catalog of every table.
34
+ */
35
+ export declare function placeVariable(variables: Variables, place: {
36
+ name: string;
37
+ description: string;
38
+ complex?: boolean;
39
+ }, values: Record<string, unknown>): Variable;
40
+ /** The variable of each deploy-time value in the models, in the order they first appear. */
41
+ export declare function deployVariables(models: Readonly<Record<string, ResolvedModel>>): Variables;
42
+ /** The variable of a deploy-time value. */
43
+ export declare function deployVariable(variables: Variables, name: string): Variable;
44
+ /** Where a target's deploy-time value is used, for a problem's location. */
45
+ export declare function useOf(model: ResolvedModel | undefined, name: string): {
46
+ file: string;
47
+ line?: number;
48
+ };
49
+ /** A `DEPLOY_VALUE` error at a deploy-time value's first use in a target. */
50
+ export declare function deployProblem(message: string, model: ResolvedModel | undefined, name: string): EnvironmentsProblem;
51
+ /** The deploying pipeline supplies the value in this target; a problem if the target sets it. */
52
+ export declare function supply(variable: Variable, target: string, model: ResolvedModel): EnvironmentsProblem[];
53
+ /** The target sets the deploy-time value's variable to this text; a problem if it cannot. */
54
+ export declare function setIn(variable: Variable, target: string, text: string, model: ResolvedModel): EnvironmentsProblem[];
@@ -0,0 +1,98 @@
1
+ const open = "⟦";
2
+ const close = "⟧";
3
+ /** The marker that stands for a variable while folding. */
4
+ export function marker(name) {
5
+ return `${open}${name}${close}`;
6
+ }
7
+ /** Each marker in a text, with the variable's name. */
8
+ export const markers = /⟦([^⟦⟧]+)⟧/g;
9
+ /** True when a text holds a marker. */
10
+ export function hasMarker(text) {
11
+ return text.includes(open);
12
+ }
13
+ /** A variable's name from a deploy-time value's NAME: lower case, `_` for other characters. */
14
+ export function deployName(name) {
15
+ return name.toLowerCase().replace(/[^a-z0-9_]+/g, "_");
16
+ }
17
+ function unique(variables, name) {
18
+ const taken = new Set(variables.list.map((variable) => variable.name));
19
+ let candidate = name;
20
+ for (let count = 2; taken.has(candidate); count += 1)
21
+ candidate = `${name}_${count}`;
22
+ return candidate;
23
+ }
24
+ /**
25
+ * The variable for a value that differs: places of the same kind (the same name) with the
26
+ * same value in every target share one, such as the catalog of every table.
27
+ */
28
+ export function placeVariable(variables, place, values) {
29
+ const key = JSON.stringify([place.name, Object.values(values)]);
30
+ const found = variables.byValues.get(key);
31
+ if (found)
32
+ return found;
33
+ const variable = {
34
+ ...place,
35
+ name: unique(variables, place.name),
36
+ values,
37
+ supplied: new Set(),
38
+ };
39
+ variables.list.push(variable);
40
+ variables.byValues.set(key, variable);
41
+ return variable;
42
+ }
43
+ /** The variable of each deploy-time value in the models, in the order they first appear. */
44
+ export function deployVariables(models) {
45
+ const variables = { list: [], byValues: new Map() };
46
+ for (const model of Object.values(models))
47
+ for (const value of model.deployValues ?? []) {
48
+ if (variables.list.some((variable) => variable.deploy === value.name))
49
+ continue;
50
+ const name = unique(variables, deployName(value.name));
51
+ variables.list.push({
52
+ name,
53
+ description: `Supplied when deploying, as BUNDLE_VAR_${name} (${value.name} in the definitions).`,
54
+ values: {},
55
+ deploy: value.name,
56
+ supplied: new Set(),
57
+ });
58
+ }
59
+ return variables;
60
+ }
61
+ /** The variable of a deploy-time value. */
62
+ export function deployVariable(variables, name) {
63
+ return variables.list.find((variable) => variable.deploy === name);
64
+ }
65
+ /** Where a target's deploy-time value is used, for a problem's location. */
66
+ export function useOf(model, name) {
67
+ const use = model?.deployValues?.find((value) => value.name === name)?.uses[0];
68
+ return use
69
+ ? { file: use.file, ...(use.line !== undefined && { line: use.line }) }
70
+ : { file: "project.yaml" };
71
+ }
72
+ /** A `DEPLOY_VALUE` error at a deploy-time value's first use in a target. */
73
+ export function deployProblem(message, model, name) {
74
+ return { code: "DEPLOY_VALUE", severity: "error", message, ...useOf(model, name) };
75
+ }
76
+ /** The deploying pipeline supplies the value in this target; a problem if the target sets it. */
77
+ export function supply(variable, target, model) {
78
+ variable.supplied.add(target);
79
+ if (variable.values[target] === undefined)
80
+ return [];
81
+ return [
82
+ conflict(variable, target, model, `writes "${String(variable.values[target])}" for it elsewhere`),
83
+ ];
84
+ }
85
+ /** The target sets the deploy-time value's variable to this text; a problem if it cannot. */
86
+ export function setIn(variable, target, text, model) {
87
+ const current = variable.values[target];
88
+ if (variable.supplied.has(target))
89
+ return [conflict(variable, target, model, `writes "${text}" where other environments use it`)];
90
+ if (current !== undefined && current !== text)
91
+ return [conflict(variable, target, model, `writes "${String(current)}" and "${text}" for it`)];
92
+ variable.values[target] = text;
93
+ return [];
94
+ }
95
+ function conflict(variable, target, model, what) {
96
+ const text = `\${envvar:${variable.deploy}}`;
97
+ return deployProblem(`${text} stands for one value in each environment, but ${target} ${what}.`, model, variable.deploy);
98
+ }
package/dist/fold.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import { type EnvironmentsBuild } from "@ingestron/definitions";
2
+ import type { Models } from "./fold-values.js";
3
+ /**
4
+ * One bundle for every target. With an error among the problems, the files are empty.
5
+ * @example
6
+ * const { files, deployValues, problems } = buildEnvironments({ dev: devModel, prod: prodModel });
7
+ */
8
+ export declare function buildEnvironments(models: Models): EnvironmentsBuild;
package/dist/fold.js ADDED
@@ -0,0 +1,69 @@
1
+ // Every environment in one bundle, with a target per environment: the values
2
+ // that differ become bundle variables, set in each target, and the result is
3
+ // checked against each environment's own bundle (spec/behaviour.md).
4
+ import { compareTargets, } from "@ingestron/definitions";
5
+ import { build } from "./build.js";
6
+ import { foldFiles } from "./fold-files.js";
7
+ import { pipelineCatalog, planFold } from "./fold-plan.js";
8
+ import { catalogProblems, continuousProblems, once, unlikeProblem } from "./fold-problems.js";
9
+ import { bundleView, sameFile, suppliedFor } from "./view.js";
10
+ /**
11
+ * One bundle for every target. With an error among the problems, the files are empty.
12
+ * @example
13
+ * const { files, deployValues, problems } = buildEnvironments({ dev: devModel, prod: prodModel });
14
+ */
15
+ export function buildEnvironments(models) {
16
+ const { structural, differing } = compareTargets(models);
17
+ if (structural.length > 0)
18
+ return failed(structural);
19
+ const early = [...catalogProblems(models, pipelineCatalog), ...continuousProblems(models)];
20
+ if (early.length > 0)
21
+ return failed(early);
22
+ const plan = planFold(models, differing);
23
+ const folded = foldFiles(plan);
24
+ const problems = once(plan.problems);
25
+ if (problems.some((problem) => problem.severity === "error"))
26
+ return failed(problems);
27
+ const deployValues = supplied(models, folded);
28
+ const unlike = viewProblems(models, folded.files, deployValues, plan.unfolded);
29
+ if (unlike.length > 0)
30
+ return failed(unlike);
31
+ return { files: folded.files, deployValues, problems };
32
+ }
33
+ function failed(problems) {
34
+ return { files: {}, deployValues: [], problems: once(problems) };
35
+ }
36
+ /** Each deploy-time value the files take, in the order the models first hold them. */
37
+ function supplied(models, folded) {
38
+ const names = [
39
+ ...new Set(Object.values(models).flatMap((model) => (model.deployValues ?? []).map((value) => value.name))),
40
+ ];
41
+ return names.flatMap((name) => {
42
+ const variable = folded.declared.find((item) => item.deploy === name);
43
+ const suppliedAs = [
44
+ ...(variable && variable.supplied.size > 0 ? [`BUNDLE_VAR_${variable.name}`] : []),
45
+ ...(Object.values(folded.hosts).includes(name) ? ["DATABRICKS_HOST"] : []),
46
+ ];
47
+ return suppliedAs.length > 0 ? [{ name, suppliedAs }] : [];
48
+ });
49
+ }
50
+ /**
51
+ * The fold property, checked when generating: each target's view of the folded files is
52
+ * what `build` gives for that target alone. What differs is ENVIRONMENTS_DIFFER.
53
+ */
54
+ function viewProblems(models, files, values, unfolded) {
55
+ const unlike = new Map();
56
+ for (const [target, model] of Object.entries(models)) {
57
+ const viewed = bundleView(files, target, suppliedFor(model, values));
58
+ const rendered = build(model);
59
+ const paths = [...new Set([...Object.keys(viewed), ...Object.keys(rendered)])];
60
+ for (const path of paths) {
61
+ const same = viewed[path] !== undefined &&
62
+ rendered[path] !== undefined &&
63
+ sameFile(path, viewed[path], rendered[path]);
64
+ if (!same)
65
+ unlike.set(path, [...(unlike.get(path) ?? []), target]);
66
+ }
67
+ }
68
+ return [...unlike].map(([path, targets]) => unlikeProblem(models, path, targets, unfolded));
69
+ }
package/dist/pack.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import type { Pack } from "@ingestron/definitions";
2
2
  /**
3
- * The Databricks pack, version 1. Name it in a standard as `pack: databricks@1`.
3
+ * The Databricks pack, version 2. Name it in a standard as `packs: [databricks@2]`. It
4
+ * builds every environment into one bundle, with a target per environment.
4
5
  * @example
5
- * const result = loadDefinitions("definitions", { pack: databricks, environment: "dev" });
6
+ * const result = loadDefinitions("definitions", { packs: [databricks], environment: "dev" });
6
7
  * if (result.model) console.log(Object.keys(databricks.build(result.model)));
7
8
  */
8
9
  export declare const databricks: Pack;
package/dist/pack.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { contract } from "./contract.js";
3
3
  import { connectors } from "./connectors.js";
4
4
  import { build } from "./build.js";
5
+ import { buildEnvironments } from "./fold.js";
5
6
  import { request } from "./request.js";
6
7
  import { capabilities, tagProblems } from "./capabilities.js";
7
8
  import { lakeflowConnect } from "./connect-method.js";
@@ -9,14 +10,15 @@ import { connectGroups, sharedProblems } from "./connect.js";
9
10
  import { files, materialisedView, sql, streamingTable } from "./methods.js";
10
11
  import { pyspark } from "./pyspark-method.js";
11
12
  /**
12
- * The Databricks pack, version 1. Name it in a standard as `pack: databricks@1`.
13
+ * The Databricks pack, version 2. Name it in a standard as `packs: [databricks@2]`. It
14
+ * builds every environment into one bundle, with a target per environment.
13
15
  * @example
14
- * const result = loadDefinitions("definitions", { pack: databricks, environment: "dev" });
16
+ * const result = loadDefinitions("definitions", { packs: [databricks], environment: "dev" });
15
17
  * if (result.model) console.log(Object.keys(databricks.build(result.model)));
16
18
  */
17
19
  export const databricks = {
18
20
  id: "databricks",
19
- version: 1,
21
+ version: 2,
20
22
  defaults: {
21
23
  naming: {
22
24
  catalog: "{{ environment }}",
@@ -58,6 +60,26 @@ export const databricks = {
58
60
  },
59
61
  connectors,
60
62
  capabilities,
63
+ platformFields: [
64
+ "mode",
65
+ "workspace.host",
66
+ "workspace.root_path",
67
+ "run_as",
68
+ "permissions",
69
+ "tags",
70
+ "compute.serverless",
71
+ "compute.budget_policy_id",
72
+ "compute.channel",
73
+ "schedule.cron",
74
+ "schedule.timezone",
75
+ "schedule.paused",
76
+ "notifications.on_failure",
77
+ "grants",
78
+ "manage.schemas",
79
+ "manage.tags",
80
+ "manage.comments",
81
+ "manage.checks",
82
+ ],
61
83
  contract,
62
84
  check(model) {
63
85
  const problems = [];
@@ -68,5 +90,6 @@ export const databricks = {
68
90
  return [...problems, ...tagProblems(model), ...sharedProblems(connectGroups(model))];
69
91
  },
70
92
  build,
93
+ buildEnvironments,
71
94
  request,
72
95
  };
@@ -1,6 +1,6 @@
1
- import { type ResolvedTable } from "@ingestron/definitions";
2
- /** Everything materialised-view.py.liquid needs. */
3
- export declare function pythonData(table: ResolvedTable, header: string): {
1
+ import type { ResolvedTable } from "@ingestron/definitions";
2
+ /** Everything materialised-view.py.liquid needs, in the pipeline whose catalog is `catalog`. */
3
+ export declare function pythonData(table: ResolvedTable, header: string, catalog: string): {
4
4
  header: string;
5
5
  code: string;
6
6
  name: string;