@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/sql.js ADDED
@@ -0,0 +1,80 @@
1
+ // Renders each table's SQL file from the templates in ../templates.
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Liquid } from "liquidjs";
5
+ import { renderQuery } from "./query.js";
6
+ import { pythonData } from "./python-data.js";
7
+ import { changeData, sourceData, tableData } from "./sql-data.js";
8
+ /** The templates folder, also used by the template parity check. */
9
+ export const templatesFolder = join(dirname(fileURLToPath(import.meta.url)), "../templates/");
10
+ const engine = new Liquid({
11
+ root: templatesFolder,
12
+ extname: ".liquid",
13
+ strictVariables: true,
14
+ strictFilters: true,
15
+ });
16
+ /** The SQL or Python file for one table; `header` is its first comment line. */
17
+ export function tableSql(table, header) {
18
+ return renderSql(sqlFile(table, header));
19
+ }
20
+ /** Renders a file chosen by `sqlFile`. */
21
+ export function renderSql({ template, data }) {
22
+ return `${engine.renderFileSync(template, data).trimEnd()}\n`;
23
+ }
24
+ /** The after-refresh Python file, with its statements and checks as JSON. */
25
+ export function renderApply(data) {
26
+ return renderSql({
27
+ template: "apply-contract.py.liquid",
28
+ data: {
29
+ header: data.header,
30
+ statements: JSON.stringify(data.statements, null, 2),
31
+ checks: JSON.stringify(data.checks, null, 2),
32
+ },
33
+ extension: "py",
34
+ });
35
+ }
36
+ /** Chooses the template by transform and store method, and builds its data. */
37
+ export function sqlFile(table, header) {
38
+ if (table.transform?.method === "pyspark") {
39
+ return {
40
+ template: "materialised-view.py.liquid",
41
+ data: pythonData(table, header),
42
+ extension: "py",
43
+ };
44
+ }
45
+ const store = table.store?.method;
46
+ if (store === "streaming-table")
47
+ return streamingTable(table, header);
48
+ if (store === "materialised-view")
49
+ return materialisedView(table, header);
50
+ throw new Error(`No SQL for store method "${String(store)}" (table ${table.name}).`);
51
+ }
52
+ function streamingTable(table, header) {
53
+ const data = {
54
+ header,
55
+ table: tableData(table, "STREAMING TABLE"),
56
+ source: sourceData(table, true),
57
+ };
58
+ if (table.store?.settings.mode === "append")
59
+ return { template: "streaming-table.sql.liquid", data, extension: "sql" };
60
+ return {
61
+ template: "change-data.sql.liquid",
62
+ data: { ...data, cdc: changeData(table) },
63
+ extension: "sql",
64
+ };
65
+ }
66
+ function materialisedView(table, header) {
67
+ const rendered = table.transform
68
+ ? renderQuery(table.transform.code, table.transform.inputs)
69
+ : { sql: "" };
70
+ // The sql method's check has already reported SQL that cannot be rendered.
71
+ const query = "sql" in rendered ? rendered.sql : "";
72
+ const source = table.ingest
73
+ ? sourceData(table, false)
74
+ : { stream: false, select: "*", path: "", options: [] };
75
+ return {
76
+ template: "materialised-view.sql.liquid",
77
+ data: { header, table: tableData(table, "MATERIALIZED VIEW"), source, query },
78
+ extension: "sql",
79
+ };
80
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@ingestron/pack-databricks",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.0.1",
4
+ "description": "Databricks methods for Ingestron: generates a bundle with a Lakeflow Spark Declarative Pipeline.",
5
+ "license": "Apache-2.0",
6
+ "author": "IntentLabs <hello@ingestron.io>",
7
+ "homepage": "https://docs.ingestron.io",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ingestron/pack-databricks.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ingestron/pack-databricks/issues",
14
+ "email": "hello@ingestron.io"
15
+ },
16
+ "type": "module",
17
+ "engines": {
18
+ "node": ">=22.13.0"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js"
24
+ }
25
+ },
26
+ "files": [
27
+ "dist",
28
+ "templates",
29
+ "README.md",
30
+ "NOTICE"
31
+ ],
32
+ "dependencies": {
33
+ "liquidjs": "10.30.0",
34
+ "yaml": "2.9.1"
35
+ },
36
+ "peerDependencies": {
37
+ "@ingestron/definitions": "^1.0.0"
38
+ },
39
+ "devDependencies": {
40
+ "@ingestron/config": "^1.0.0",
41
+ "@ingestron/definitions": "^1.0.0",
42
+ "@microsoft/api-extractor": "7.59.4",
43
+ "@types/node": "22.20.5",
44
+ "ajv": "8.20.0",
45
+ "eslint": "10.13.0",
46
+ "eslint-plugin-jsdoc": "65.4.2",
47
+ "prettier": "3.9.10",
48
+ "tsx": "4.23.15",
49
+ "typescript": "6.0.3",
50
+ "typescript-eslint": "8.72.0",
51
+ "vitest": "5.0.5"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -p tsconfig.build.json",
58
+ "typecheck": "pnpm build && tsc -p tsconfig.json --noEmit",
59
+ "lint": "eslint .",
60
+ "test": "vitest run",
61
+ "check:docs": "tsx scripts/check-docs.ts",
62
+ "validate": "pnpm format:check && pnpm secrets:check && pnpm licences:check && node --test scripts/*.test.mjs && pnpm lint && pnpm typecheck && pnpm test && pnpm check:docs && pnpm check:api",
63
+ "api": "api-extractor run --local",
64
+ "check:api": "api-extractor run",
65
+ "format": "prettier --write .",
66
+ "format:check": "prettier --check .",
67
+ "secrets:check": "node scripts/scan-secrets.mjs",
68
+ "licences:check": "node scripts/check-licences.mjs",
69
+ "dco:check": "node scripts/check-dco.mjs"
70
+ }
6
71
  }
@@ -0,0 +1,58 @@
1
+ # {{ header }}
2
+ # Runs after each refresh: Lakeflow pipelines cannot set tags or column
3
+ # comments, nor compare counts over all rows, so this task does.
4
+ import json
5
+
6
+ from pyspark.sql import SparkSession
7
+
8
+ STATEMENTS = json.loads(r"""{{ statements }}""")
9
+ CHECKS = json.loads(r"""{{ checks }}""")
10
+
11
+
12
+ def passes(operator, expected, measured):
13
+ """True when the measured count meets the rule's comparison."""
14
+ if operator == "mustBe":
15
+ return measured == expected
16
+ if operator == "mustNotBe":
17
+ return measured != expected
18
+ if operator == "mustBeGreaterThan":
19
+ return measured > expected
20
+ if operator == "mustBeGreaterOrEqualTo":
21
+ return measured >= expected
22
+ if operator == "mustBeLessThan":
23
+ return measured < expected
24
+ if operator == "mustBeLessOrEqualTo":
25
+ return measured <= expected
26
+ low, high = expected
27
+ inside = low <= measured <= high
28
+ return inside if operator == "mustBeBetween" else not inside
29
+
30
+
31
+ def measure(spark, check):
32
+ """The rule's count, in rows or as a percentage of all rows."""
33
+ row = spark.sql(check["query"]).first()
34
+ counted, total = row["counted"] or 0, row["total"] or 0
35
+ if check["unit"] == "percent":
36
+ return 0 if total == 0 else counted * 100 / total
37
+ return counted
38
+
39
+
40
+ def main():
41
+ spark = SparkSession.builder.getOrCreate()
42
+ for statement in STATEMENTS:
43
+ spark.sql(statement)
44
+ failures = []
45
+ for check in CHECKS:
46
+ measured = measure(spark, check)
47
+ if passes(check["operator"], check["value"], measured):
48
+ continue
49
+ message = f'{check["table"]}: {check["name"]} measured {measured}, expected {check["operator"]} {check["value"]}'
50
+ if check["action"] == "fail":
51
+ failures.append(message)
52
+ else:
53
+ print(f"Warning: {message}")
54
+ if failures:
55
+ raise RuntimeError("Table checks failed:\n" + "\n".join(failures))
56
+
57
+
58
+ main()
@@ -0,0 +1,20 @@
1
+ -- {{ header }}
2
+ -- Changes as received, each read once.
3
+ CREATE OR REFRESH STREAMING TABLE {{ cdc.changes }}
4
+ AS {% render "read-files", source: source %};
5
+
6
+ -- The current rows{% if cdc.scd == 2 %}, with history{% endif %}, kept by key.
7
+ {% render "table", table: table %};
8
+
9
+ CREATE FLOW {{ cdc.flow }}
10
+ AS AUTO CDC INTO {{ table.target }}
11
+ FROM STREAM({{ cdc.changes }})
12
+ KEYS ({{ cdc.keys }})
13
+ {%- if cdc.delete_when != "" %}
14
+ APPLY AS DELETE WHEN {{ cdc.delete_when }}
15
+ {%- endif %}
16
+ SEQUENCE BY {{ cdc.sequence }}
17
+ {%- if cdc.except_columns != "" %}
18
+ COLUMNS * EXCEPT ({{ cdc.except_columns }})
19
+ {%- endif %}
20
+ STORED AS SCD TYPE {{ cdc.scd }};
@@ -0,0 +1,28 @@
1
+ # {{ header }}
2
+ from pyspark import pipelines as dp
3
+
4
+ # The transform's own code, unchanged:
5
+ {{ code }}
6
+
7
+
8
+ @dp.materialized_view(
9
+ name={{ name }},
10
+ {%- if comment != "" %}
11
+ comment={{ comment }},
12
+ {%- endif %}
13
+ {%- if cluster != "" %}
14
+ {{ cluster }},
15
+ {%- endif %}
16
+ {%- if properties != "" %}
17
+ table_properties={{ properties }},
18
+ {%- endif %}
19
+ )
20
+ {%- for group in expectations %}
21
+ @dp.{{ group.decorator }}({{ group.checks }})
22
+ {%- endfor %}
23
+ def {{ wrapper }}():
24
+ return {{ function }}({
25
+ {%- for input in inputs %}
26
+ {{ input.name }}: spark.read.table({{ input.table }}),
27
+ {%- endfor %}
28
+ })
@@ -0,0 +1,4 @@
1
+ -- {{ header }}
2
+ {% render "table", table: table %}
3
+ AS
4
+ {% if query != "" %}{{ query }}{% else %}{% render "read-files", source: source %}{% endif %};
@@ -0,0 +1,7 @@
1
+ {%- comment %} Ends without a newline: the caller continues after it. {% endcomment -%}
2
+ SELECT {{ source.select }} FROM {% if source.stream %}STREAM {% endif %}read_files(
3
+ {{ source.path }}
4
+ {%- for option in source.options %},
5
+ {{ option.key }} => {{ option.value }}
6
+ {%- endfor %}
7
+ ){%- comment %}{% endcomment -%}
@@ -0,0 +1,3 @@
1
+ -- {{ header }}
2
+ {% render "table", table: table %}
3
+ AS {% render "read-files", source: source %};
@@ -0,0 +1,17 @@
1
+ {%- comment %} Ends without a newline: the caller continues after it. {% endcomment -%}
2
+ CREATE OR REFRESH {{ table.kind }} {{ table.target }}
3
+ {%- if table.constraints.size > 0 %} (
4
+ {%- for constraint in table.constraints %}
5
+ CONSTRAINT {{ constraint.name }} EXPECT ({{ constraint.check }}){{ constraint.on_violation }}{% unless forloop.last %},{% endunless %}
6
+ {%- endfor %}
7
+ )
8
+ {%- endif %}
9
+ {%- if table.comment != "" %}
10
+ COMMENT {{ table.comment }}
11
+ {%- endif %}
12
+ {%- if table.cluster != "" %}
13
+ CLUSTER BY {{ table.cluster }}
14
+ {%- endif %}
15
+ {%- if table.properties.size > 0 %}
16
+ TBLPROPERTIES ({% for property in table.properties %}{{ property.key }} = {{ property.value }}{% unless forloop.last %}, {% endunless %}{% endfor %})
17
+ {%- endif -%}