@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.
- package/LICENSE +202 -0
- package/NOTICE +4 -0
- package/README.md +107 -2
- package/dist/apply-contract.d.ts +16 -0
- package/dist/apply-contract.js +99 -0
- package/dist/build.d.ts +7 -0
- package/dist/build.js +78 -0
- package/dist/bundle.d.ts +30 -0
- package/dist/bundle.js +136 -0
- package/dist/capabilities.d.ts +7 -0
- package/dist/capabilities.js +77 -0
- package/dist/connect-method.d.ts +7 -0
- package/dist/connect-method.js +88 -0
- package/dist/connect.d.ts +27 -0
- package/dist/connect.js +152 -0
- package/dist/connectors.d.ts +3 -0
- package/dist/connectors.js +55 -0
- package/dist/contract.d.ts +3 -0
- package/dist/contract.js +53 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/methods.d.ts +9 -0
- package/dist/methods.js +166 -0
- package/dist/pack.d.ts +8 -0
- package/dist/pack.js +72 -0
- package/dist/pyspark-method.d.ts +3 -0
- package/dist/pyspark-method.js +40 -0
- package/dist/python-data.d.ts +21 -0
- package/dist/python-data.js +49 -0
- package/dist/query.d.ts +8 -0
- package/dist/query.js +25 -0
- package/dist/request.d.ts +3 -0
- package/dist/request.js +46 -0
- package/dist/sql-data.d.ts +44 -0
- package/dist/sql-data.js +110 -0
- package/dist/sql.d.ts +21 -0
- package/dist/sql.js +80 -0
- package/package.json +68 -3
- package/templates/apply-contract.py.liquid +58 -0
- package/templates/change-data.sql.liquid +20 -0
- package/templates/materialised-view.py.liquid +28 -0
- package/templates/materialised-view.sql.liquid +4 -0
- package/templates/read-files.liquid +7 -0
- package/templates/streaming-table.sql.liquid +3 -0
- 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": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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,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,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 -%}
|