@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.
package/README.md CHANGED
@@ -4,7 +4,7 @@ The Databricks pack: from resolved Ingestron definitions it generates a
4
4
  [Declarative Automation Bundle](https://docs.databricks.com/aws/en/dev-tools/bundles/)
5
5
  (formerly Databricks Asset Bundle) with one
6
6
  [Lakeflow Spark Declarative Pipeline](https://docs.databricks.com/aws/en/ldp/concepts/)
7
- per project. Name it in your standard: `packs: [databricks@1]`.
7
+ per project. Name it in your standard: `packs: [databricks@2]`.
8
8
 
9
9
  What it uses on Databricks:
10
10
 
@@ -23,24 +23,33 @@ What it uses on Databricks:
23
23
  - **Serverless pipelines**, a scheduled refresh job, Unity Catalog schemas
24
24
  and grants when you ask for them, permissions, tags and notifications.
25
25
 
26
- Every value is resolved when generating, so the bundle has no variables.
27
- Each environment gets its own bundle folder.
26
+ One bundle covers every environment, with a target per environment, as
27
+ Databricks teams lay out bundles by hand. A value is a bundle variable only
28
+ where environments differ; everything the same in every environment is written
29
+ as it is. Each target sets its own variables, so the `targets` block shows
30
+ exactly what changes between dev and prod.
28
31
 
29
32
  ## Example
30
33
 
31
- Most people use this through the [`ingestron`](https://www.npmjs.com/package/ingestron) command. In code:
34
+ Most people use this through the [`ingestron`](https://www.npmjs.com/package/ingestron) command, which writes the bundle to `build/databricks/`. In code:
32
35
 
33
36
  ```ts
34
- import { loadDefinitions } from "@ingestron/definitions";
37
+ import { loadDefinitions, type ResolvedModel } from "@ingestron/definitions";
35
38
  import { databricks } from "@ingestron/pack-databricks";
36
39
 
37
- const result = loadDefinitions("definitions", {
38
- packs: [databricks],
39
- environment: "dev",
40
- });
41
- if (result.model) {
42
- for (const path of Object.keys(databricks.build(result.model))) console.log(path);
40
+ const models: Record<string, ResolvedModel> = {};
41
+ for (const environment of ["dev", "prod"]) {
42
+ const result = loadDefinitions("definitions", {
43
+ packs: [databricks],
44
+ environment,
45
+ deployTime: true,
46
+ environmentsTogether: true,
47
+ });
48
+ if (result.model) models[environment] = result.model;
43
49
  }
50
+ const { files, problems } = databricks.buildEnvironments!(models);
51
+ for (const problem of problems) console.log(problem.code, problem.message);
52
+ for (const path of Object.keys(files)) console.log(path);
44
53
  ```
45
54
 
46
55
  For the retail example this prints:
@@ -54,12 +63,119 @@ src/retail/silver/daily_sales.sql
54
63
  resources/retail_refresh.job.yml
55
64
  ```
56
65
 
66
+ The values that differ between dev and prod are in `databricks.yml`:
67
+
68
+ ```yaml
69
+ variables:
70
+ catalog:
71
+ description: Catalog of the retail tables.
72
+ shop_path:
73
+ description: Folder the files of connection "shop" are read from.
74
+ targets:
75
+ dev:
76
+ mode: development
77
+ default: true
78
+ workspace:
79
+ host: https://adb-1111111111111111.1.azuredatabricks.net
80
+ # run_as and permissions, as the platform sets them
81
+ presets:
82
+ tags:
83
+ environment: dev
84
+ variables:
85
+ catalog: dev_sales
86
+ shop_path: abfss://landing@retaildev.dfs.core.windows.net/shop
87
+ prod:
88
+ mode: production
89
+ # prod's workspace, run_as, permissions, presets and variables
90
+ ```
91
+
92
+ The pipeline reads them as `catalog: ${var.catalog}` and as the configuration
93
+ value `ingestron.shop_path`, which its SQL uses as `${ingestron.shop_path}`.
94
+ Tables in the pipeline's catalog are named with two parts, such as
95
+ `bronze.orders`.
96
+
97
+ ## Deploy
98
+
99
+ From the bundle folder, deploy one environment at a time, the same files each
100
+ time:
101
+
102
+ ```sh
103
+ cd build/databricks
104
+ databricks bundle validate -t prod
105
+ databricks bundle deploy -t prod
106
+ ```
107
+
108
+ A value written as `${envvar:NAME}` in the definitions is supplied when
109
+ deploying: the bundle declares it as a variable that the targets leave unset,
110
+ and the deploying CI pipeline sets `BUNDLE_VAR_<name>` (`name` is NAME in lower
111
+ case). A workspace host supplied this way is `DATABRICKS_HOST`.
112
+
113
+ `prerequisites/<environment>.sql` holds the grants an administrator runs once
114
+ in that environment. It is not deployed, and neither are `.generated.json` and
115
+ `.generated.lock`, which `ingestron` keeps in the folder.
116
+
117
+ The after-refresh task (tags, column comments and table checks) is
118
+ `tasks/<pipeline>/apply_contract.py`, outside the pipeline's `src/<pipeline>/`
119
+ sources, so that only the refresh job runs it.
120
+
121
+ ## What changed in 2.0
122
+
123
+ - One bundle in `build/databricks/` for every environment, in place of a
124
+ bundle per environment in `build/<environment>/`. Change the standard to
125
+ `databricks@2`.
126
+ - The bundle name, pipeline and job keys, target names and `root_path` are the
127
+ same as in 1.x, so the first `bundle deploy -t dev` or `-t prod` updates
128
+ that environment's resources in place. Run `bundle validate` and
129
+ `bundle summary` first.
130
+ - The mode is `production` for every environment except `dev`, unless the
131
+ platform sets it. In 1.x, environments such as `test`, `uat` and `staging`
132
+ were in `development` mode. On their first 2.0 deploy, jobs and pipelines
133
+ lose the `[dev <identity>]` prefix in their names, pipelines are no longer in
134
+ development, managed schemas lose their `dev_<identity>_` prefix (the old
135
+ schema is deleted after approval, and the pipelines write to a new one), and
136
+ production mode wants `run_as` and `permissions` unless a service principal
137
+ deploys. To keep `development` mode for such an environment, write
138
+ `mode: ${values.mode}` in the platform, `mode: production` in project.yaml's
139
+ `values`, and `mode: development` in that environment's `values`.
140
+ - Managed schemas are keyed by layer (`bronze`), or by schema name when a
141
+ layer has several (`bronze_shop`), not `<catalog>_<schema>`. Each
142
+ environment's deployment state still holds the old key, so with
143
+ `manage.schemas`, before the first 2.0 deploy, for each environment and each
144
+ managed schema:
145
+ 1. in the 1.x folder `build/<environment>/`, run
146
+ `databricks bundle deployment unbind <catalog>_<schema> -t <environment>`;
147
+ 2. in `build/databricks/`, run
148
+ `databricks bundle deployment bind <key> <catalog>.<schema> -t <environment>`,
149
+ with the key from `resources/schemas.yml`;
150
+ 3. run `bundle validate` and `bundle summary`, and check that the deploy
151
+ deletes no schema.
152
+
153
+ Without this, the deploy plans to delete each schema under its old key.
154
+
155
+ - The after-refresh script moved from `src/<pipeline>/apply_contract.py`,
156
+ where the pipeline also loaded it as its own code, to
157
+ `tasks/<pipeline>/apply_contract.py`. The first deploy removes the old file.
158
+ - Pipeline code names tables in the pipeline's catalog with two parts.
159
+ - `prerequisites.sql` is `prerequisites/<environment>.sql`.
160
+ - Environments may differ only in values. A table built in some environments
161
+ only, or code that differs in more than a value, is `ENVIRONMENTS_DIFFER`.
162
+ A PySpark transform reads names that differ from the pipeline's
163
+ configuration with `spark.conf.get("ingestron.<name>")`, into a module-level
164
+ constant for its decorator; values in its own code cannot differ yet.
165
+ - Needs `@ingestron/definitions` 1.9 or later, and Databricks CLI v0.264.0 or
166
+ later, which adds preset tags to pipelines.
167
+
168
+ The rules are in [spec/behaviour.md](spec/behaviour.md).
169
+
57
170
  ## With the ADF pack
58
171
 
59
172
  Used with the [ADF pack](https://github.com/ingestron/pack-adf)
60
- (`packs: [databricks@1, adf@1]`), streaming tables read the files ADF lands,
173
+ (`packs: [databricks@2, adf@2]`), streaming tables read the files ADF lands,
61
174
  from its `landing` folder.
62
175
 
176
+ Columns ADF adds to the landed rows (`audit_columns` in ADF 1.4) are read
177
+ with the files; timestamps, which ADF writes as text, are cast to `TIMESTAMP`.
178
+
63
179
  ## Dependencies
64
180
 
65
181
  - [`@ingestron/definitions`](https://www.npmjs.com/package/@ingestron/definitions), a peer dependency: the resolved model.
@@ -67,10 +183,14 @@ from its `landing` folder.
67
183
  `templates/` and the transforms' SQL.
68
184
  - [`yaml`](https://eemeli.org/yaml/): writes the bundle's YAML files.
69
185
 
70
- ## Sources (accessed 2026-10-24)
186
+ ## Sources
187
+
188
+ Bundle variables, targets, presets and pipeline configuration:
189
+ [spec/references.md](spec/references.md) (accessed 2026-11-04). Bundle fields
190
+ from Databricks CLI v1.23.0 `databricks bundle schema` (2026-11-04).
71
191
 
72
- Bundle fields from Databricks CLI v1.22.1 `databricks bundle schema` and
73
- `databricks/bundle-examples` (`lakeflow_pipelines_sql`). SQL from
192
+ Accessed 2026-10-24: `databricks/bundle-examples` (`lakeflow_pipelines_sql`).
193
+ SQL from
74
194
  [CREATE STREAMING TABLE](https://docs.databricks.com/aws/en/ldp/developer/ldp-sql-ref-create-streaming-table.html),
75
195
  [CREATE MATERIALIZED VIEW](https://docs.databricks.com/aws/en/ldp/developer/ldp-sql-ref-create-materialized-view),
76
196
  [AUTO CDC INTO](https://docs.databricks.com/aws/en/dlt-ref/dlt-sql-ref-apply-changes-into),
package/dist/build.d.ts CHANGED
@@ -1,7 +1,31 @@
1
1
  import type { ResolvedModel } from "@ingestron/definitions";
2
+ import { type Data } from "./bundle.js";
3
+ /** A YAML file as data, with its header comments. */
4
+ export interface YamlFile {
5
+ headers: string[];
6
+ data: Data;
7
+ }
8
+ /** The bundle's files before they are written: YAML as data, code as text. */
9
+ export interface BundleData {
10
+ /** By path, in the order they are built. */
11
+ files: Record<string, YamlFile | string>;
12
+ /** The definition file each table's code file comes from, by path. */
13
+ origins: Record<string, string>;
14
+ }
2
15
  /**
3
- * Every file of the bundle for one environment.
16
+ * Every file of the bundle for one environment: one target, every value as resolved.
4
17
  * @example
5
18
  * const files = build(model); // { "databricks.yml": "...", "src/retail/bronze/orders.sql": "..." }
6
19
  */
7
20
  export declare function build(model: ResolvedModel): Record<string, string>;
21
+ /** The files as text. */
22
+ export declare function written(files: BundleData["files"]): Record<string, string>;
23
+ /** The header of the bundle's own files. */
24
+ export declare function headerOf(model: ResolvedModel): string;
25
+ /** Every file of the bundle for one environment, as data. */
26
+ export declare function bundleData(model: ResolvedModel): BundleData;
27
+ /**
28
+ * The after-refresh script's path: in `tasks/`, outside every pipeline's `src/<pipeline>/**`
29
+ * sources whatever the naming standard, so that no pipeline loads it as its own code.
30
+ */
31
+ export declare function scriptPath(pipeline: string): string;
package/dist/build.js CHANGED
@@ -1,32 +1,49 @@
1
- import { bundleFile, jobFile, pipelineExtras, pipelineFile, schemaKeys, schemasFile, yamlText, } from "./bundle.js";
1
+ import { bundleFile, jobFile, pipelineExtras, pipelineFile, pipelineTags, schemaKeys, schemasFile, yamlText, } from "./bundle.js";
2
2
  import { connectGroups, gatewayPipeline, ingestionPipeline, previewNote, } from "./connect.js";
3
3
  import { applyContract } from "./apply-contract.js";
4
4
  import { prerequisites } from "./capabilities.js";
5
5
  import { renderApply, renderSql, sqlFile } from "./sql.js";
6
6
  const doNotEdit = "Do not edit: change the definitions and generate again.";
7
7
  /**
8
- * Every file of the bundle for one environment.
8
+ * Every file of the bundle for one environment: one target, every value as resolved.
9
9
  * @example
10
10
  * const files = build(model); // { "databricks.yml": "...", "src/retail/bronze/orders.sql": "..." }
11
11
  */
12
12
  export function build(model) {
13
- const header = `Generated by Ingestron from the ${model.project.id} definitions (${model.project.environment}). ${doNotEdit}`;
13
+ return written(bundleData(model).files);
14
+ }
15
+ /** The files as text. */
16
+ export function written(files) {
17
+ return Object.fromEntries(Object.entries(files).map(([path, file]) => [
18
+ path,
19
+ typeof file === "string" ? file : yamlText(file.headers, file.data),
20
+ ]));
21
+ }
22
+ /** The header of the bundle's own files. */
23
+ export function headerOf(model) {
24
+ return `Generated by Ingestron from the ${model.project.id} definitions. ${doNotEdit}`;
25
+ }
26
+ /** Every file of the bundle for one environment, as data. */
27
+ export function bundleData(model) {
28
+ const header = headerOf(model);
14
29
  const keys = schemaKeys(model);
15
30
  const groups = connectGroups(model);
16
- const files = { "databricks.yml": yamlText(header, bundleFile(model)) };
17
- const output = { header, keys, files };
31
+ const files = {
32
+ "databricks.yml": { headers: [header], data: bundleFile(model) },
33
+ };
34
+ const output = { header, keys, files, origins: {} };
18
35
  const refreshes = [...connectFiles(model, groups, output), ...sqlFiles(model, groups, output)];
19
36
  const tasks = [...refreshes, ...applyFiles(model, refreshes, output)];
20
37
  const job = jobFile(model, tasks);
21
38
  if (job)
22
- files[`resources/${model.names.job}.job.yml`] = yamlText(header, job);
39
+ files[`resources/${model.names.job}.job.yml`] = { headers: [header], data: job };
23
40
  const schemas = schemasFile(model, keys);
24
41
  if (schemas)
25
- files["resources/schemas.yml"] = yamlText(header, schemas);
42
+ files["resources/schemas.yml"] = { headers: [header], data: schemas };
26
43
  const needs = prerequisites(model, header);
27
44
  if (needs)
28
- files["prerequisites.sql"] = needs;
29
- return files;
45
+ files[`prerequisites/${model.project.environment}.sql`] = needs;
46
+ return { files, origins: output.origins };
30
47
  }
31
48
  /** Lakeflow Connect pipelines; returns the job tasks for those the job refreshes. */
32
49
  function connectFiles(model, groups, output) {
@@ -34,14 +51,13 @@ function connectFiles(model, groups, output) {
34
51
  return groups.flatMap((group) => {
35
52
  const headers = [header, previewNote(group)];
36
53
  if (group.gateway) {
37
- const gateway = {
38
- resources: { pipelines: { [group.gateway]: gatewayPipeline(group, keys) } },
39
- };
40
- files[`resources/${group.gateway}.pipeline.yml`] = yamlText(headers, gateway);
54
+ const gateway = { ...gatewayPipeline(group, keys), ...pipelineTags(model) };
55
+ const data = { resources: { pipelines: { [group.gateway]: gateway } } };
56
+ files[`resources/${group.gateway}.pipeline.yml`] = { headers, data };
41
57
  }
42
58
  const pipeline = { ...ingestionPipeline(group, keys), ...pipelineExtras(model) };
43
- const ingestion = { resources: { pipelines: { [group.ingestion]: pipeline } } };
44
- files[`resources/${group.ingestion}.pipeline.yml`] = yamlText(headers, ingestion);
59
+ const data = { resources: { pipelines: { [group.ingestion]: pipeline } } };
60
+ files[`resources/${group.ingestion}.pipeline.yml`] = { headers, data };
45
61
  return group.continuous
46
62
  ? []
47
63
  : [{ key: `ingest_${group.connection.id}`, pipeline: group.ingestion, after: [] }];
@@ -54,23 +70,34 @@ function sqlFiles(model, groups, output) {
54
70
  if (tables.length === 0)
55
71
  return [];
56
72
  const { pipeline } = model.names;
57
- files[`resources/${pipeline}.pipeline.yml`] = yamlText(output.header, pipelineFile(model, tables, keys));
73
+ const data = pipelineFile(model, tables, keys);
74
+ files[`resources/${pipeline}.pipeline.yml`] = { headers: [output.header], data };
75
+ const catalog = tables[0].fullName.catalog;
58
76
  for (const table of tables) {
59
77
  const tableHeader = `Generated by Ingestron from ${table.origin.file} (table ${table.name}). ${doNotEdit}`;
60
- const file = sqlFile(table, tableHeader);
61
- files[`src/${pipeline}/${table.layer}/${table.name}.${file.extension}`] = renderSql(file);
78
+ const file = sqlFile(table, tableHeader, catalog);
79
+ const path = `src/${pipeline}/${table.layer}/${table.name}.${file.extension}`;
80
+ files[path] = renderSql(file);
81
+ output.origins[path] = table.origin.file;
62
82
  }
63
83
  const ingestion = groups
64
84
  .filter((group) => !group.continuous)
65
85
  .map((group) => `ingest_${group.connection.id}`);
66
86
  return [{ key: "refresh", pipeline, after: ingestion }];
67
87
  }
88
+ /**
89
+ * The after-refresh script's path: in `tasks/`, outside every pipeline's `src/<pipeline>/**`
90
+ * sources whatever the naming standard, so that no pipeline loads it as its own code.
91
+ */
92
+ export function scriptPath(pipeline) {
93
+ return `tasks/${pipeline}/apply_contract.py`;
94
+ }
68
95
  /** The Python file that applies tags, comments and table checks after every refresh. */
69
96
  function applyFiles(model, refreshes, output) {
70
97
  const { statements, checks } = applyContract(model);
71
98
  if (statements.length === 0 && checks.length === 0)
72
99
  return [];
73
- const path = `src/${model.names.pipeline}/apply_contract.py`;
100
+ const path = scriptPath(model.names.pipeline);
74
101
  output.files[path] = renderApply({ header: output.header, statements, checks });
75
102
  return [
76
103
  { key: "apply_contract", python: `../${path}`, after: refreshes.map((task) => task.key) },
package/dist/bundle.d.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import type { ResolvedModel, ResolvedTable } from "@ingestron/definitions";
2
- type Data = Record<string, unknown>;
2
+ /** A YAML document as data. */
3
+ export type Data = Record<string, unknown>;
4
+ /**
5
+ * Files in the bundle folder that are not deployed: the generation manifest, its lock
6
+ * (left behind if writing stops part way), and the grants an administrator runs by hand.
7
+ */
8
+ export declare const notDeployed: string[];
3
9
  /** `databricks.yml`: one target, named after the environment. */
4
10
  export declare function bundleFile(model: ResolvedModel): Data;
11
+ /** The settings Databricks takes per target: mode, workspace, run_as and permissions. */
12
+ export declare function targetOf(model: ResolvedModel): Data;
5
13
  /** The pipeline that builds the tables with SQL files under `src/`. */
6
14
  export declare function pipelineFile(model: ResolvedModel, tables: ResolvedTable[], schemaKeys: Map<string, string>): Data;
7
15
  /** A task of the refresh job: a pipeline to refresh, or a Python file to run, after others. */
@@ -17,14 +25,20 @@ export interface Task {
17
25
  * With a Python task it is built without a schedule too, to run on demand.
18
26
  */
19
27
  export declare function jobFile(model: ResolvedModel, tasks: Task[]): Data | undefined;
20
- /** Bundle keys of the schemas this bundle manages, by `catalog.schema`. Empty unless `manage.schemas`. */
28
+ /**
29
+ * Bundle keys of the schemas this bundle manages, by `catalog.schema`, the same in every
30
+ * environment where only the catalog differs: the layer when the layer has one schema,
31
+ * else the schema's name. When two catalogs hold a schema of that name, or the key is
32
+ * taken, it is `<catalog>_<schema>`. Empty unless `manage.schemas`.
33
+ */
21
34
  export declare function schemaKeys(model: ResolvedModel): Map<string, string>;
22
35
  /** A schema's name, or a reference to it when the bundle manages it, so the bundle creates it first. */
23
36
  export declare function schemaRef(keys: Map<string, string>, catalog: string, schema: string): string;
24
37
  /** Managed schemas, with the platform's grants for their layer. */
25
38
  export declare function schemasFile(model: ResolvedModel, keys: Map<string, string>): Data | undefined;
26
- /** Tags and failure notifications, the same on every pipeline. */
39
+ /** The platform's tags, on every pipeline and job. */
40
+ export declare function pipelineTags(model: ResolvedModel): Data;
41
+ /** Tags and failure notifications, the same on every pipeline that builds tables. */
27
42
  export declare function pipelineExtras(model: ResolvedModel): Data;
28
43
  /** YAML text with header comments, one per line given. */
29
44
  export declare function yamlText(headers: string | string[], data: Data): string;
30
- export {};
package/dist/bundle.js CHANGED
@@ -1,19 +1,30 @@
1
1
  import { stringify } from "yaml";
2
+ /**
3
+ * Files in the bundle folder that are not deployed: the generation manifest, its lock
4
+ * (left behind if writing stops part way), and the grants an administrator runs by hand.
5
+ */
6
+ export const notDeployed = [".generated.json", ".generated.lock", "prerequisites/**"];
2
7
  /** `databricks.yml`: one target, named after the environment. */
3
8
  export function bundleFile(model) {
9
+ return {
10
+ bundle: { name: model.project.id },
11
+ include: ["resources/*.yml"],
12
+ sync: { exclude: notDeployed },
13
+ targets: { [model.project.environment]: { ...targetOf(model), default: true } },
14
+ };
15
+ }
16
+ /** The settings Databricks takes per target: mode, workspace, run_as and permissions. */
17
+ export function targetOf(model) {
4
18
  const { platform } = model;
5
- const target = { mode: platform.mode, default: true };
6
- if (platform.workspace.host || platform.workspace.root_path)
7
- target.workspace = { ...platform.workspace };
19
+ const target = { mode: platform.mode };
20
+ const workspace = Object.fromEntries(Object.entries(platform.workspace).filter(([, value]) => value));
21
+ if (Object.keys(workspace).length > 0)
22
+ target.workspace = workspace;
8
23
  if (platform.run_as)
9
24
  target.run_as = platform.run_as;
10
25
  if (platform.permissions.length > 0)
11
26
  target.permissions = platform.permissions;
12
- return {
13
- bundle: { name: model.project.id },
14
- include: ["resources/*.yml"],
15
- targets: { [model.project.environment]: target },
16
- };
27
+ return target;
17
28
  }
18
29
  /** The pipeline that builds the tables with SQL files under `src/`. */
19
30
  export function pipelineFile(model, tables, schemaKeys) {
@@ -77,12 +88,31 @@ function jobTask(task) {
77
88
  }),
78
89
  };
79
90
  }
80
- /** Bundle keys of the schemas this bundle manages, by `catalog.schema`. Empty unless `manage.schemas`. */
91
+ /**
92
+ * Bundle keys of the schemas this bundle manages, by `catalog.schema`, the same in every
93
+ * environment where only the catalog differs: the layer when the layer has one schema,
94
+ * else the schema's name. When two catalogs hold a schema of that name, or the key is
95
+ * taken, it is `<catalog>_<schema>`. Empty unless `manage.schemas`.
96
+ */
81
97
  export function schemaKeys(model) {
82
98
  if (!model.platform.manage.schemas)
83
99
  return new Map();
84
- const keys = model.tables.map(({ fullName }) => [`${fullName.catalog}.${fullName.schema}`, `${fullName.catalog}_${fullName.schema}`]);
85
- return new Map(keys);
100
+ const schemas = new Map();
101
+ for (const { fullName, layer } of model.tables)
102
+ schemas.set(`${fullName.catalog}.${fullName.schema}`, { ...fullName, layer });
103
+ const list = [...schemas.values()];
104
+ const keys = new Map();
105
+ const used = new Set();
106
+ for (const { catalog, schema, layer } of list) {
107
+ const alone = list.filter((other) => other.layer === layer).length === 1;
108
+ const shared = list.filter((other) => other.schema === schema).length > 1;
109
+ const both = `${catalog}_${schema}`;
110
+ const key = alone ? layer : shared ? both : schema;
111
+ const unique = used.has(key) ? both : key;
112
+ used.add(unique);
113
+ keys.set(`${catalog}.${schema}`, unique);
114
+ }
115
+ return keys;
86
116
  }
87
117
  /** A schema's name, or a reference to it when the bundle manages it, so the bundle creates it first. */
88
118
  export function schemaRef(keys, catalog, schema) {
@@ -116,12 +146,17 @@ function uniqueSchemas(tables) {
116
146
  return true;
117
147
  });
118
148
  }
119
- /** Tags and failure notifications, the same on every pipeline. */
149
+ /** The platform's tags, on every pipeline and job. */
150
+ export function pipelineTags(model) {
151
+ const { tags } = model.platform;
152
+ return Object.keys(tags).length > 0 ? { tags } : {};
153
+ }
154
+ /** Tags and failure notifications, the same on every pipeline that builds tables. */
120
155
  export function pipelineExtras(model) {
121
156
  const { platform } = model;
122
157
  const alerts = ["on-update-failure", "on-update-fatal-failure", "on-flow-failure"];
123
158
  return {
124
- ...(Object.keys(platform.tags).length > 0 && { tags: platform.tags }),
159
+ ...pipelineTags(model),
125
160
  ...(platform.notifications.on_failure.length > 0 && {
126
161
  notifications: [{ email_recipients: platform.notifications.on_failure, alerts }],
127
162
  }),
package/dist/connect.d.ts CHANGED
@@ -13,6 +13,8 @@ export interface ConnectGroup {
13
13
  type Keys = Map<string, string>;
14
14
  /** Tables ingested by lakeflow-connect, grouped by connection in path order. */
15
15
  export declare function connectGroups(model: ResolvedModel): ConnectGroup[];
16
+ /** True when the bundle has a pipeline that runs continuously: a gateway, or Kafka read continuously. */
17
+ export declare function hasContinuous(model: ResolvedModel): boolean;
16
18
  /**
17
19
  * Problems with settings that one connection's tables share: its gateway, or
18
20
  * whether its ingestion runs continuously.
package/dist/connect.js CHANGED
@@ -20,6 +20,10 @@ export function connectGroups(model) {
20
20
  }
21
21
  return [...groups.values()];
22
22
  }
23
+ /** True when the bundle has a pipeline that runs continuously: a gateway, or Kafka read continuously. */
24
+ export function hasContinuous(model) {
25
+ return connectGroups(model).some((group) => group.gateway !== undefined || group.continuous);
26
+ }
23
27
  /**
24
28
  * Problems with settings that one connection's tables share: its gateway, or
25
29
  * whether its ingestion runs continuously.
@@ -0,0 +1,12 @@
1
+ import type { Plan } from "./fold-plan.js";
2
+ import { type Targets } from "./fold-target.js";
3
+ import { type Variable } from "./fold-values.js";
4
+ /** The folded files, and each target's host when it is supplied when deploying. */
5
+ export interface Folded {
6
+ files: Record<string, string>;
7
+ hosts: Targets["hosts"];
8
+ /** The variables the files declare. */
9
+ declared: Variable[];
10
+ }
11
+ /** The files for every target, folded from the plan's template. */
12
+ export declare function foldFiles(plan: Plan): Folded;