@ingestron/pack-databricks 0.0.0-stage → 1.0.1

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 (45) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +4 -0
  3. package/README.md +107 -2
  4. package/dist/apply-contract.d.ts +16 -0
  5. package/dist/apply-contract.js +99 -0
  6. package/dist/build.d.ts +7 -0
  7. package/dist/build.js +78 -0
  8. package/dist/bundle.d.ts +30 -0
  9. package/dist/bundle.js +136 -0
  10. package/dist/capabilities.d.ts +7 -0
  11. package/dist/capabilities.js +77 -0
  12. package/dist/connect-method.d.ts +7 -0
  13. package/dist/connect-method.js +88 -0
  14. package/dist/connect.d.ts +27 -0
  15. package/dist/connect.js +152 -0
  16. package/dist/connectors.d.ts +3 -0
  17. package/dist/connectors.js +55 -0
  18. package/dist/contract.d.ts +3 -0
  19. package/dist/contract.js +53 -0
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +2 -0
  22. package/dist/methods.d.ts +9 -0
  23. package/dist/methods.js +166 -0
  24. package/dist/pack.d.ts +8 -0
  25. package/dist/pack.js +72 -0
  26. package/dist/pyspark-method.d.ts +3 -0
  27. package/dist/pyspark-method.js +40 -0
  28. package/dist/python-data.d.ts +21 -0
  29. package/dist/python-data.js +49 -0
  30. package/dist/query.d.ts +8 -0
  31. package/dist/query.js +25 -0
  32. package/dist/request.d.ts +3 -0
  33. package/dist/request.js +46 -0
  34. package/dist/sql-data.d.ts +44 -0
  35. package/dist/sql-data.js +110 -0
  36. package/dist/sql.d.ts +21 -0
  37. package/dist/sql.js +80 -0
  38. package/package.json +68 -3
  39. package/templates/apply-contract.py.liquid +58 -0
  40. package/templates/change-data.sql.liquid +20 -0
  41. package/templates/materialised-view.py.liquid +28 -0
  42. package/templates/materialised-view.sql.liquid +4 -0
  43. package/templates/read-files.liquid +7 -0
  44. package/templates/streaming-table.sql.liquid +3 -0
  45. package/templates/table.liquid +17 -0
package/dist/bundle.js ADDED
@@ -0,0 +1,136 @@
1
+ import { stringify } from "yaml";
2
+ /** `databricks.yml`: one target, named after the environment. */
3
+ export function bundleFile(model) {
4
+ 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 };
8
+ if (platform.run_as)
9
+ target.run_as = platform.run_as;
10
+ if (platform.permissions.length > 0)
11
+ target.permissions = platform.permissions;
12
+ return {
13
+ bundle: { name: model.project.id },
14
+ include: ["resources/*.yml"],
15
+ targets: { [model.project.environment]: target },
16
+ };
17
+ }
18
+ /** The pipeline that builds the tables with SQL files under `src/`. */
19
+ export function pipelineFile(model, tables, schemaKeys) {
20
+ const { platform, names } = model;
21
+ const first = tables[0].fullName;
22
+ const pipeline = {
23
+ name: names.pipeline,
24
+ catalog: first.catalog,
25
+ schema: schemaRef(schemaKeys, first.catalog, first.schema),
26
+ serverless: platform.compute.serverless,
27
+ ...(platform.compute.budget_policy_id && {
28
+ budget_policy_id: platform.compute.budget_policy_id,
29
+ }),
30
+ ...(platform.compute.channel && { channel: platform.compute.channel }),
31
+ root_path: `../src/${names.pipeline}`,
32
+ libraries: [{ glob: { include: `../src/${names.pipeline}/**` } }],
33
+ };
34
+ return {
35
+ resources: { pipelines: { [names.pipeline]: { ...pipeline, ...pipelineExtras(model) } } },
36
+ };
37
+ }
38
+ /** The serverless environment Python tasks run in. */
39
+ const environment = { environment_key: "ingestron", spec: { environment_version: "6" } };
40
+ /**
41
+ * A job that refreshes the pipelines in order, on the platform's schedule.
42
+ * With a Python task it is built without a schedule too, to run on demand.
43
+ */
44
+ export function jobFile(model, tasks) {
45
+ const { platform, names } = model;
46
+ const python = tasks.some((task) => task.python);
47
+ if ((!platform.schedule && !python) || tasks.length === 0)
48
+ return undefined;
49
+ const job = {
50
+ name: names.job,
51
+ ...(platform.schedule && {
52
+ schedule: {
53
+ quartz_cron_expression: platform.schedule.cron,
54
+ timezone_id: platform.schedule.timezone,
55
+ pause_status: platform.schedule.paused ? "PAUSED" : "UNPAUSED",
56
+ },
57
+ }),
58
+ tasks: tasks.map(jobTask),
59
+ ...(python && { environments: [environment] }),
60
+ };
61
+ if (Object.keys(platform.tags).length > 0)
62
+ job.tags = platform.tags;
63
+ if (platform.notifications.on_failure.length > 0)
64
+ job.email_notifications = { on_failure: platform.notifications.on_failure };
65
+ return { resources: { jobs: { [names.job]: job } } };
66
+ }
67
+ function jobTask(task) {
68
+ return {
69
+ task_key: task.key,
70
+ ...(task.after.length > 0 && { depends_on: task.after.map((key) => ({ task_key: key })) }),
71
+ ...(task.pipeline && {
72
+ pipeline_task: { pipeline_id: `\${resources.pipelines.${task.pipeline}.id}` },
73
+ }),
74
+ ...(task.python && {
75
+ environment_key: environment.environment_key,
76
+ spark_python_task: { python_file: task.python },
77
+ }),
78
+ };
79
+ }
80
+ /** Bundle keys of the schemas this bundle manages, by `catalog.schema`. Empty unless `manage.schemas`. */
81
+ export function schemaKeys(model) {
82
+ if (!model.platform.manage.schemas)
83
+ return new Map();
84
+ const keys = model.tables.map(({ fullName }) => [`${fullName.catalog}.${fullName.schema}`, `${fullName.catalog}_${fullName.schema}`]);
85
+ return new Map(keys);
86
+ }
87
+ /** A schema's name, or a reference to it when the bundle manages it, so the bundle creates it first. */
88
+ export function schemaRef(keys, catalog, schema) {
89
+ const key = keys.get(`${catalog}.${schema}`);
90
+ return key ? `\${resources.schemas.${key}.name}` : schema;
91
+ }
92
+ /** Managed schemas, with the platform's grants for their layer. */
93
+ export function schemasFile(model, keys) {
94
+ if (keys.size === 0)
95
+ return undefined;
96
+ const schemas = {};
97
+ for (const table of uniqueSchemas(model.tables)) {
98
+ const key = keys.get(`${table.fullName.catalog}.${table.fullName.schema}`);
99
+ const grants = model.platform.grants[table.layer] ?? [];
100
+ schemas[key] = {
101
+ catalog_name: table.fullName.catalog,
102
+ name: table.fullName.schema,
103
+ comment: `Layer ${table.layer} of ${model.project.id}.`,
104
+ ...(grants.length > 0 && { grants }),
105
+ };
106
+ }
107
+ return { resources: { schemas } };
108
+ }
109
+ function uniqueSchemas(tables) {
110
+ const seen = new Set();
111
+ return tables.filter(({ fullName }) => {
112
+ const key = `${fullName.catalog}.${fullName.schema}`;
113
+ if (seen.has(key))
114
+ return false;
115
+ seen.add(key);
116
+ return true;
117
+ });
118
+ }
119
+ /** Tags and failure notifications, the same on every pipeline. */
120
+ export function pipelineExtras(model) {
121
+ const { platform } = model;
122
+ const alerts = ["on-update-failure", "on-update-fatal-failure", "on-flow-failure"];
123
+ return {
124
+ ...(Object.keys(platform.tags).length > 0 && { tags: platform.tags }),
125
+ ...(platform.notifications.on_failure.length > 0 && {
126
+ notifications: [{ email_recipients: platform.notifications.on_failure, alerts }],
127
+ }),
128
+ };
129
+ }
130
+ /** YAML text with header comments, one per line given. */
131
+ export function yamlText(headers, data) {
132
+ const comments = (Array.isArray(headers) ? headers : [headers])
133
+ .map((line) => `# ${line}\n`)
134
+ .join("");
135
+ return `${comments}${stringify(data, { lineWidth: 0 })}`;
136
+ }
@@ -0,0 +1,7 @@
1
+ import type { Capability, ResolvedModel } from "@ingestron/definitions";
2
+ /** The Databricks pack's capabilities. */
3
+ export declare const capabilities: Capability[];
4
+ /** Tag keys Unity Catalog refuses: `. , - = / :` or longer than 255 characters. */
5
+ export declare function tagProblems(model: ResolvedModel): string[];
6
+ /** SQL an administrator runs once, so the run_as identity may do what is turned on. */
7
+ export declare function prerequisites(model: ResolvedModel, header: string): string | undefined;
@@ -0,0 +1,77 @@
1
+ import { tagParts } from "./apply-contract.js";
2
+ import { quoteName } from "./query.js";
3
+ const owner = "The run_as identity owns the tables the pipelines create, so it may change them; any other identity needs MODIFY on each table";
4
+ /** The Databricks pack's capabilities. */
5
+ export const capabilities = [
6
+ {
7
+ id: "schemas",
8
+ title: "Managed schemas",
9
+ does: "Creates each layer's schema in the bundle and applies the platform's grants to it.",
10
+ needs: [
11
+ "USE CATALOG and CREATE SCHEMA on each catalog for the identity that deploys",
12
+ "MANAGE on the schemas, or ownership, to apply grants",
13
+ ],
14
+ },
15
+ {
16
+ id: "tags",
17
+ title: "Tags",
18
+ does: "Sets the contract's tags and classifications on tables and columns after each refresh.",
19
+ needs: [
20
+ "Databricks Runtime 16.3 or later (serverless jobs have it)",
21
+ `${owner}, or APPLY TAG with USE SCHEMA and USE CATALOG`,
22
+ "ASSIGN on each governed tag the contract uses",
23
+ ],
24
+ },
25
+ {
26
+ id: "comments",
27
+ title: "Column comments",
28
+ does: "Sets each column's description as its comment after each refresh.",
29
+ needs: ["Databricks Runtime 16.3 or later (serverless jobs have it)", owner],
30
+ },
31
+ {
32
+ id: "checks",
33
+ title: "Table checks",
34
+ does: "Counts the contract's rules over all rows after each refresh, then warns or fails the run.",
35
+ needs: ["SELECT on each table for the run_as identity (an owner has it)", "Serverless jobs"],
36
+ },
37
+ ];
38
+ /** Tag keys Unity Catalog refuses: `. , - = / :` or longer than 255 characters. */
39
+ export function tagProblems(model) {
40
+ if (!model.platform.manage.tags)
41
+ return [];
42
+ const tags = model.tables.flatMap((table) => [
43
+ ...table.contract.tags,
44
+ ...table.contract.columns.flatMap((column) => column.tags ?? []),
45
+ ]);
46
+ return [...new Set(tags)]
47
+ .filter((tag) => /[.,\-=/:]/.test(tagParts(tag)[0]) || tagParts(tag)[0].length > 255)
48
+ .map((tag) => `the tag "${tag}" has a key Unity Catalog does not allow: keys have no . , - = / : and at most 255 characters; write key:value for a value`);
49
+ }
50
+ /** SQL an administrator runs once, so the run_as identity may do what is turned on. */
51
+ export function prerequisites(model, header) {
52
+ const { manage, run_as: runAs } = model.platform;
53
+ const on = capabilities.filter((capability) => manage[capability.id]);
54
+ if (on.length === 0)
55
+ return undefined;
56
+ const principal = runAs
57
+ ? quoteName(Object.values(runAs)[0] ?? "")
58
+ : "`<the identity that deploys>`";
59
+ const catalogs = [...new Set(model.tables.map((table) => table.fullName.catalog))];
60
+ const lines = [
61
+ `-- ${header}`,
62
+ "-- Run once as an administrator; review before running.",
63
+ runAs
64
+ ? "-- The grants go to run_as; give the same to the identity that deploys when it differs."
65
+ : "-- Set run_as in the platform, or replace the principal below with the identity that deploys.",
66
+ "",
67
+ ];
68
+ for (const capability of on) {
69
+ lines.push(`-- ${capability.title} needs:`, ...capability.needs.map((need) => `-- - ${need}.`));
70
+ if (capability.id === "schemas")
71
+ lines.push(...catalogs.map((catalog) => `GRANT USE CATALOG, CREATE SCHEMA ON CATALOG ${quoteName(catalog)} TO ${principal};`));
72
+ if (capability.id === "tags")
73
+ lines.push("-- Governed tags: an account administrator grants ASSIGN on each governed tag in the tag policy.");
74
+ lines.push("");
75
+ }
76
+ return `${lines.join("\n").trimEnd()}\n`;
77
+ }
@@ -0,0 +1,7 @@
1
+ import type { Method } from "@ingestron/definitions";
2
+ /** Connectors that read through an ingestion gateway (database change capture). */
3
+ export declare const gatewayConnectors: Set<string>;
4
+ /** Every connector this method supports, verified against the Databricks docs. */
5
+ export declare const managedConnectors: string[];
6
+ /** Lakeflow Connect managed ingestion. */
7
+ export declare const lakeflowConnect: Method;
@@ -0,0 +1,88 @@
1
+ /** Connectors that read through an ingestion gateway (database change capture). */
2
+ export const gatewayConnectors = new Set(["sqlserver"]);
3
+ /** Every connector this method supports, verified against the Databricks docs. */
4
+ export const managedConnectors = ["sqlserver", "salesforce", "kafka"];
5
+ /** Lakeflow Connect managed ingestion. */
6
+ export const lakeflowConnect = {
7
+ action: "ingest",
8
+ description: "Databricks Lakeflow Connect: managed ingestion from databases (with a gateway), SaaS applications and Kafka.",
9
+ connectors: managedConnectors,
10
+ connectivity: ["public", "workspace"],
11
+ produces: "table",
12
+ defaults: { mode: "upsert", columns: "contract" },
13
+ settings: {
14
+ type: "object",
15
+ additionalProperties: false,
16
+ properties: {
17
+ mode: {
18
+ enum: ["append", "upsert", "history"],
19
+ enumDescriptions: [
20
+ "Add every change as a row.",
21
+ "Keep the latest row per key (SCD type 1).",
22
+ "Keep every version of a row with when it was valid (SCD type 2).",
23
+ ],
24
+ description: "How rows are kept.",
25
+ },
26
+ columns: {
27
+ enum: ["contract", "all"],
28
+ enumDescriptions: ["Only the contract's columns.", "Every source column."],
29
+ description: "Which source columns are ingested.",
30
+ },
31
+ gateway_policy_id: {
32
+ type: "string",
33
+ description: "Cluster policy for the gateway, which runs on classic compute.",
34
+ examples: ["0012AB34CD56EF78"],
35
+ },
36
+ staging_schema: {
37
+ type: "string",
38
+ description: "Schema for the gateway's staging volume. Default: the table's schema.",
39
+ examples: ["staging"],
40
+ },
41
+ value_format: {
42
+ enum: ["json", "avro", "protobuf", "string"],
43
+ enumDescriptions: ["JSON text.", "Avro.", "Protocol Buffers.", "Plain text, not parsed."],
44
+ description: "Kafka: how message values are parsed.",
45
+ },
46
+ continuous: {
47
+ type: "boolean",
48
+ description: "Kafka: run the ingestion pipeline continuously. Default: true in production; elsewhere the refresh job runs it, which costs less.",
49
+ },
50
+ starting_offset: {
51
+ enum: ["earliest", "latest"],
52
+ enumDescriptions: [
53
+ "From the oldest message the topic keeps.",
54
+ "From the messages that arrive after the pipeline starts.",
55
+ ],
56
+ description: "Kafka: where a new pipeline starts reading.",
57
+ },
58
+ },
59
+ },
60
+ check: checkConnect,
61
+ };
62
+ /** Lakeflow Connect writes the table with the source's names, so it cannot rename columns. */
63
+ function renamesOf(table) {
64
+ return table.contract.columns
65
+ .filter((column) => column.physicalName && column.physicalName !== column.name)
66
+ .map((column) => `Lakeflow Connect keeps the source's column names, so it cannot read "${column.physicalName}" as "${column.name}"; remove physicalName, or rename the column in a transform`);
67
+ }
68
+ function checkConnect(table) {
69
+ const ingest = table.ingest;
70
+ if (!ingest)
71
+ return [];
72
+ const problems = renamesOf(table);
73
+ if (typeof ingest.connection.settings.uc_connection !== "string") {
74
+ problems.push('the connection needs "settings.uc_connection", the name of a Unity Catalog connection');
75
+ }
76
+ if (gatewayConnectors.has(ingest.connection.connector) && ingest.object.split(".").length !== 3) {
77
+ problems.push(`a ${ingest.connection.connector} object is written database.schema.table, not "${ingest.object}"`);
78
+ }
79
+ if (ingest.connection.connector === "kafka" && ingest.settings.mode !== "append") {
80
+ problems.push("kafka topics can only be appended (mode: append)");
81
+ }
82
+ const own = table.expectations
83
+ .filter((item) => item.from === "definition")
84
+ .map((item) => item.name);
85
+ if (own.length > 0)
86
+ problems.push(`managed ingestion cannot run expectations (${own.join(", ")}); check the data in a transform`);
87
+ return problems;
88
+ }
@@ -0,0 +1,27 @@
1
+ import type { ResolvedModel, ResolvedTable } from "@ingestron/definitions";
2
+ type Data = Record<string, unknown>;
3
+ /** The tables one connection ingests, and the pipelines it needs. */
4
+ export interface ConnectGroup {
5
+ connection: NonNullable<ResolvedTable["ingest"]>["connection"];
6
+ tables: ResolvedTable[];
7
+ ingestion: string;
8
+ gateway?: string;
9
+ /** Kafka runs continuously (by default in production only); the others are refreshed by the job. */
10
+ continuous: boolean;
11
+ }
12
+ /** Bundle keys of managed schemas, by `catalog.schema` (see bundle.ts). */
13
+ type Keys = Map<string, string>;
14
+ /** Tables ingested by lakeflow-connect, grouped by connection in path order. */
15
+ export declare function connectGroups(model: ResolvedModel): ConnectGroup[];
16
+ /**
17
+ * Problems with settings that one connection's tables share: its gateway, or
18
+ * whether its ingestion runs continuously.
19
+ */
20
+ export declare function sharedProblems(groups: ConnectGroup[]): string[];
21
+ /** The gateway pipeline: captures changes continuously on classic compute. */
22
+ export declare function gatewayPipeline(group: ConnectGroup, keys: Keys): Data;
23
+ /** The serverless ingestion pipeline that writes the tables. */
24
+ export declare function ingestionPipeline(group: ConnectGroup, keys: Keys): Data;
25
+ /** A note for generated files that use features Databricks has not made generally available. */
26
+ export declare function previewNote(group: ConnectGroup): string;
27
+ export {};
@@ -0,0 +1,152 @@
1
+ import { schemaRef } from "./bundle.js";
2
+ import { gatewayConnectors } from "./connect-method.js";
3
+ import { quoteName } from "./query.js";
4
+ /** Tables ingested by lakeflow-connect, grouped by connection in path order. */
5
+ export function connectGroups(model) {
6
+ const groups = new Map();
7
+ for (const table of model.tables.filter((candidate) => candidate.ingest?.method === "lakeflow-connect")) {
8
+ const connection = table.ingest.connection;
9
+ const base = `${model.names.pipeline}_${connection.id}`;
10
+ const continuous = table.ingest.settings.continuous ?? model.platform.mode === "production";
11
+ const group = groups.get(connection.id) ?? {
12
+ connection,
13
+ tables: [],
14
+ ingestion: `${base}_ingestion`,
15
+ ...(gatewayConnectors.has(connection.connector) && { gateway: `${base}_gateway` }),
16
+ continuous: connection.connector === "kafka" && continuous === true,
17
+ };
18
+ group.tables.push(table);
19
+ groups.set(connection.id, group);
20
+ }
21
+ return [...groups.values()];
22
+ }
23
+ /**
24
+ * Problems with settings that one connection's tables share: its gateway, or
25
+ * whether its ingestion runs continuously.
26
+ */
27
+ export function sharedProblems(groups) {
28
+ return groups.flatMap((group) => {
29
+ const keys = group.gateway ? ["gateway_policy_id", "staging_schema"] : ["continuous"];
30
+ const shared = (table) => JSON.stringify([
31
+ group.gateway ? gatewayStorage(table) : undefined,
32
+ ...keys.map((key) => table.ingest?.settings[key]),
33
+ ]);
34
+ const [first] = group.tables;
35
+ const different = group.tables.find((table) => shared(table) !== shared(first));
36
+ if (!first || !different)
37
+ return [];
38
+ const what = group.gateway ? "one gateway" : "one ingestion pipeline";
39
+ return [
40
+ `the tables of connection ${group.connection.id} share ${what}, so ${keys.join(" and ")} must be the same; ${different.name} differs from ${first.name}`,
41
+ ];
42
+ });
43
+ }
44
+ /** Where the gateway stages changes: the table's catalog, and its schema unless set. */
45
+ function gatewayStorage(table) {
46
+ const staging = table.ingest?.settings.staging_schema;
47
+ return {
48
+ catalog: table.fullName.catalog,
49
+ schema: typeof staging === "string" ? staging : table.fullName.schema,
50
+ };
51
+ }
52
+ /** The gateway pipeline: captures changes continuously on classic compute. */
53
+ export function gatewayPipeline(group, keys) {
54
+ const first = group.tables[0];
55
+ const { catalog, schema } = gatewayStorage(first);
56
+ const staging = schemaRef(keys, catalog, schema);
57
+ const policy = first.ingest?.settings.gateway_policy_id;
58
+ return {
59
+ name: group.gateway,
60
+ catalog,
61
+ schema: staging,
62
+ continuous: true,
63
+ gateway_definition: {
64
+ connection_name: group.connection.settings.uc_connection,
65
+ gateway_storage_catalog: catalog,
66
+ gateway_storage_schema: staging,
67
+ },
68
+ ...(typeof policy === "string" && {
69
+ clusters: [{ label: "default", policy_id: policy, apply_policy_default_values: true }],
70
+ }),
71
+ };
72
+ }
73
+ /** The serverless ingestion pipeline that writes the tables. */
74
+ export function ingestionPipeline(group, keys) {
75
+ const first = group.tables[0];
76
+ const kafka = group.connection.connector === "kafka";
77
+ const source = group.gateway
78
+ ? { ingestion_gateway_id: `\${resources.pipelines.${group.gateway}.id}` }
79
+ : { connection_name: group.connection.settings.uc_connection };
80
+ return {
81
+ name: group.ingestion,
82
+ catalog: first.fullName.catalog,
83
+ schema: schemaRef(keys, first.fullName.catalog, first.fullName.schema),
84
+ serverless: true,
85
+ ...(group.continuous && { continuous: true }),
86
+ ...(kafka && { channel: "PREVIEW" }),
87
+ ingestion_definition: {
88
+ ...source,
89
+ objects: group.tables.map((table) => ({ table: tableObject(group, table, keys) })),
90
+ },
91
+ };
92
+ }
93
+ function tableObject(group, table, keys) {
94
+ const ingest = table.ingest;
95
+ const destination = {
96
+ destination_catalog: table.fullName.catalog,
97
+ destination_schema: schemaRef(keys, table.fullName.catalog, table.fullName.schema),
98
+ destination_table: table.fullName.table,
99
+ table_configuration: tableConfiguration(table),
100
+ };
101
+ if (group.connection.connector === "kafka")
102
+ return {
103
+ source_table: "N/A",
104
+ ...destination,
105
+ connector_options: { kafka_options: kafkaOptions(table) },
106
+ };
107
+ if (group.gateway) {
108
+ const [catalog, schema, name] = ingest.object.split(".");
109
+ return { source_catalog: catalog, source_schema: schema, source_table: name, ...destination };
110
+ }
111
+ return { source_schema: "objects", source_table: ingest.object, ...destination };
112
+ }
113
+ const scdTypes = {
114
+ append: "APPEND_ONLY",
115
+ upsert: "SCD_TYPE_1",
116
+ history: "SCD_TYPE_2",
117
+ };
118
+ function tableConfiguration(table) {
119
+ const settings = table.ingest.settings;
120
+ const keyed = settings.mode !== "append" && table.contract.primaryKey.length > 0;
121
+ // Kafka values are parsed with the contract's schema hints instead.
122
+ const kafka = table.ingest.connection.connector === "kafka";
123
+ return {
124
+ scd_type: scdTypes[String(settings.mode)],
125
+ ...(settings.columns === "contract" &&
126
+ !kafka && { include_columns: table.contract.columns.map((column) => column.name) }),
127
+ ...(keyed && { primary_keys: table.contract.primaryKey }),
128
+ };
129
+ }
130
+ function kafkaOptions(table) {
131
+ const settings = table.ingest.settings;
132
+ const format = String(settings.value_format ?? "json");
133
+ const hints = table.contract.columns
134
+ .filter((column) => column.type)
135
+ .map((column) => `${quoteName(column.name)} ${column.type}`);
136
+ const json = format === "json" && hints.length > 0
137
+ ? { json_options: { schema_hints: hints.join(", ") } }
138
+ : {};
139
+ return {
140
+ topics: [table.ingest.object],
141
+ ...(typeof settings.starting_offset === "string" && {
142
+ starting_offset: settings.starting_offset,
143
+ }),
144
+ value_transformer: { format: format.toUpperCase(), ...json },
145
+ };
146
+ }
147
+ /** A note for generated files that use features Databricks has not made generally available. */
148
+ export function previewNote(group) {
149
+ if (group.connection.connector === "kafka")
150
+ return "Lakeflow Connect for Kafka is in Beta on Databricks (checked 2026-10-25).";
151
+ return "Lakeflow Connect fields used here are marked Preview in the Databricks CLI v1.22.1 bundle schema (checked 2026-10-25).";
152
+ }
@@ -0,0 +1,3 @@
1
+ import type { Connector } from "@ingestron/definitions";
2
+ /** One entry per connector an ingest method reads. */
3
+ export declare const connectors: Connector[];
@@ -0,0 +1,55 @@
1
+ const lakeflow = "https://docs.databricks.com/aws/en/ingestion/lakeflow-connect";
2
+ /** One entry per connector an ingest method reads. */
3
+ export const connectors = [
4
+ {
5
+ id: "files",
6
+ category: "Files and storage",
7
+ title: "Files",
8
+ method: "files",
9
+ object: "a folder under the connection's path",
10
+ settings: [
11
+ { name: "path", description: "Where the files land, such as /Volumes/main/landing/shop." },
12
+ ],
13
+ generates: "a streaming table that reads new files with Auto Loader",
14
+ docs: "https://docs.databricks.com/sql/language-manual/functions/read_files.html",
15
+ },
16
+ {
17
+ id: "sqlserver",
18
+ category: "Databases and analytics",
19
+ title: "SQL Server",
20
+ method: "lakeflow-connect",
21
+ object: "database.schema.table",
22
+ settings: [
23
+ { name: "uc_connection", description: "The Unity Catalog connection to the source." },
24
+ ],
25
+ generates: "an ingestion gateway and an ingestion pipeline (Lakeflow Connect)",
26
+ maturity: { status: "Public Preview", checked: "2026-10-25" },
27
+ docs: `${lakeflow}/sql-server-pipeline`,
28
+ },
29
+ {
30
+ id: "salesforce",
31
+ category: "Business applications",
32
+ title: "Salesforce",
33
+ method: "lakeflow-connect",
34
+ object: "the object name, such as Account",
35
+ settings: [
36
+ { name: "uc_connection", description: "The Unity Catalog connection to the source." },
37
+ ],
38
+ generates: "an ingestion pipeline (Lakeflow Connect)",
39
+ maturity: { status: "Public Preview", checked: "2026-10-25" },
40
+ docs: `${lakeflow}/salesforce-pipeline`,
41
+ },
42
+ {
43
+ id: "kafka",
44
+ category: "Streaming",
45
+ title: "Kafka",
46
+ method: "lakeflow-connect",
47
+ object: "the topic",
48
+ settings: [
49
+ { name: "uc_connection", description: "The Unity Catalog connection to the source." },
50
+ ],
51
+ generates: "an ingestion pipeline on the PREVIEW channel (Lakeflow Connect)",
52
+ maturity: { status: "Beta", checked: "2026-10-25" },
53
+ docs: `${lakeflow}/kafka-pipeline`,
54
+ },
55
+ ];
@@ -0,0 +1,3 @@
1
+ import { type ContractUse } from "@ingestron/definitions";
2
+ /** The Databricks pack's use of every contract field, by path. */
3
+ export declare const contract: Record<string, ContractUse>;
@@ -0,0 +1,53 @@
1
+ // What the Databricks pack does with each ODCS contract field: used, not yet
2
+ // (with why), or handled elsewhere. The documentation and the editor show
3
+ // these, and a contract field no pack uses yet is a warning.
4
+ import { contractCommon } from "@ingestron/definitions";
5
+ const used = (does) => ({ use: "used", does });
6
+ /** Used when the platform turns the capability on, after each refresh. */
7
+ const needs = (id, does) => ({ use: "used", does, needs: id });
8
+ /** Used, with the fields under it read as part of it. */
9
+ const whole = (does) => ({ use: "used", does, whole: true });
10
+ const notYet = (reason) => ({ use: "not-yet", reason });
11
+ const elsewhere = (reason) => ({ use: "elsewhere", reason });
12
+ const object = "schema.*";
13
+ const column = "schema.*.properties.*";
14
+ const tags = needs("tags", "Set as Unity Catalog tags after each refresh; key:value gives a value.");
15
+ const relationships = notYet("Relationships between tables are not declared on the tables yet.");
16
+ /** The Databricks pack's use of every contract field, by path. */
17
+ export const contract = {
18
+ ...contractCommon,
19
+ schema: used("One object per table; each table uses the object named like it."),
20
+ [`${object}.name`]: used("Matched to the table by its name."),
21
+ [`${object}.logicalType`]: { use: "documentation" },
22
+ [`${object}.physicalName`]: elsewhere("The table's name comes from the standard's naming."),
23
+ [`${object}.physicalType`]: elsewhere("The store method decides: a streaming table or a materialised view."),
24
+ [`${object}.description`]: used("The table's comment."),
25
+ [`${object}.properties`]: used("The table's columns."),
26
+ [`${object}.relationships`]: relationships,
27
+ [`${object}.quality`]: {
28
+ use: "used",
29
+ whole: true,
30
+ needs: "checks",
31
+ does: "Rules with a comparison are counted over all rows after each refresh, then warn or fail the run as quality.default says.",
32
+ },
33
+ [`${object}.tags`]: tags,
34
+ [`${column}.name`]: used("The column's name in the table."),
35
+ [`${column}.logicalType`]: used("The column's type, through the standard's types; a schema hint when reading files."),
36
+ [`${column}.physicalType`]: used("The column's type, as written."),
37
+ [`${column}.required`]: used("A not-null expectation with the standard's quality.required action."),
38
+ [`${column}.primaryKey`]: used("The keys that upsert and history keep the current row by."),
39
+ [`${column}.primaryKeyPosition`]: used("The order of the keys."),
40
+ [`${column}.physicalName`]: used("The column's name at the source: files are read with it renamed to the contract's name. Lakeflow Connect keeps source names, so it refuses physicalName."),
41
+ [`${column}.logicalTypeOptions`]: whole("Limits become expectations with quality.default's action: length, pattern, minimum and maximum, multipleOf and array items. format is documentation."),
42
+ [`${column}.unique`]: needs("checks", "A table check after each refresh that no value repeats."),
43
+ [`${column}.partitioned`]: elsewhere("Tables use liquid clustering; set the store method's cluster_by."),
44
+ [`${column}.partitionKeyPosition`]: elsewhere("Tables use liquid clustering; set the store method's cluster_by."),
45
+ [`${column}.classification`]: needs("tags", "Set as the column's tag classification after each refresh."),
46
+ [`${column}.encryptedName`]: elsewhere("Encryption is managed outside Ingestron."),
47
+ [`${column}.relationships`]: relationships,
48
+ [`${column}.quality`]: whole("Rules that allow no failing row (nullValues, missingValues, invalidValues with mustBe: 0) become expectations; the others are a warning."),
49
+ [`${column}.description`]: needs("comments", "Set as the column's comment after each refresh: a pipeline takes column comments only with every column's type."),
50
+ [`${column}.tags`]: tags,
51
+ [`${column}.properties`]: notYet("The fields of an object column are not read yet; give the column's physicalType, such as STRUCT<street: STRING>."),
52
+ [`${column}.items`]: notYet("The element of an array column is not read yet; give the column's physicalType, such as ARRAY<STRING>."),
53
+ };
@@ -0,0 +1 @@
1
+ export { databricks } from "./pack.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // Public API of @ingestron/pack-databricks.
2
+ export { databricks } from "./pack.js";
@@ -0,0 +1,9 @@
1
+ import type { Method } from "@ingestron/definitions";
2
+ /** Reads files where they already are, with Auto Loader (`read_files`). */
3
+ export declare const files: Method;
4
+ /** A streaming table: each file is read once; optionally kept current with AUTO CDC. */
5
+ export declare const streamingTable: Method;
6
+ /** A materialised view, recomputed (incrementally where possible) on each update. */
7
+ export declare const materialisedView: Method;
8
+ /** The transform's own SQL, with `{{ tables.<input> }}` replaced by full names. */
9
+ export declare const sql: Method;