interlaced 2.0.0a2__py3-none-any.whl
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.
- interlace/__init__.py +26 -0
- interlace/checks/__init__.py +10 -0
- interlace/checks/builtin.py +152 -0
- interlace/checks/runner.py +103 -0
- interlace/checks/spec.py +113 -0
- interlace/cli/__init__.py +7 -0
- interlace/cli/main.py +1072 -0
- interlace/config/__init__.py +7 -0
- interlace/config/config.py +172 -0
- interlace/contracts.py +35 -0
- interlace/dsl/__init__.py +16 -0
- interlace/dsl/decorators.py +222 -0
- interlace/dsl/discovery.py +75 -0
- interlace/dsl/sql_config.py +47 -0
- interlace/engines/__init__.py +16 -0
- interlace/engines/base.py +86 -0
- interlace/engines/duckdb.py +249 -0
- interlace/engines/postgres.py +157 -0
- interlace/engines/quack.py +113 -0
- interlace/engines/registry.py +125 -0
- interlace/exceptions.py +66 -0
- interlace/exports.py +145 -0
- interlace/graph/__init__.py +17 -0
- interlace/graph/column_lineage.py +69 -0
- interlace/graph/dag.py +81 -0
- interlace/graph/project.py +242 -0
- interlace/graph/selectors.py +45 -0
- interlace/ir/__init__.py +24 -0
- interlace/ir/canonicalize.py +71 -0
- interlace/ir/fingerprint.py +55 -0
- interlace/ir/relation.py +71 -0
- interlace/ir/schema.py +23 -0
- interlace/plan/__init__.py +23 -0
- interlace/plan/apply.py +527 -0
- interlace/plan/differ.py +211 -0
- interlace/plan/plan.py +177 -0
- interlace/plan/resolve.py +74 -0
- interlace/plan/run.py +79 -0
- interlace/project.py +278 -0
- interlace/py.typed +0 -0
- interlace/runtime/__init__.py +6 -0
- interlace/runtime/handles.py +48 -0
- interlace/runtime/python_model.py +175 -0
- interlace/scaffold.py +83 -0
- interlace/scheduler/__init__.py +17 -0
- interlace/scheduler/engine.py +66 -0
- interlace/scheduler/triggers.py +84 -0
- interlace/scheduler/worker.py +154 -0
- interlace/service/__init__.py +7 -0
- interlace/service/app.py +836 -0
- interlace/service/auth.py +41 -0
- interlace/state/__init__.py +18 -0
- interlace/state/interval.py +134 -0
- interlace/state/janitor.py +142 -0
- interlace/state/snapshot.py +47 -0
- interlace/state/store.py +906 -0
- interlace/strategies/__init__.py +64 -0
- interlace/strategies/base.py +71 -0
- interlace/strategies/full.py +38 -0
- interlace/strategies/full_merge.py +98 -0
- interlace/strategies/incremental_by_time.py +65 -0
- interlace/strategies/merge_by_key.py +69 -0
- interlace/strategies/scd_type_2.py +122 -0
- interlace/strategies/view.py +33 -0
- interlace/streaming/__init__.py +18 -0
- interlace/streaming/log.py +298 -0
- interlace/streaming/materializer.py +166 -0
- interlace/streaming/schema.py +218 -0
- interlaced-2.0.0a2.dist-info/METADATA +216 -0
- interlaced-2.0.0a2.dist-info/RECORD +74 -0
- interlaced-2.0.0a2.dist-info/WHEEL +5 -0
- interlaced-2.0.0a2.dist-info/entry_points.txt +2 -0
- interlaced-2.0.0a2.dist-info/licenses/LICENSE +21 -0
- interlaced-2.0.0a2.dist-info/top_level.txt +1 -0
interlace/ir/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Intermediate representation: the canonical sqlglot AST + Arrow contract."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from interlace.ir.canonicalize import parse, resolve_references, table_references
|
|
6
|
+
from interlace.ir.fingerprint import canonical_sql, data_fingerprint, metadata_fingerprint
|
|
7
|
+
from interlace.ir.relation import EngineRef, Relation, SqlRelation, TableRef
|
|
8
|
+
from interlace.ir.schema import ArrowSchema, empty_schema, schema_from_fields
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ArrowSchema",
|
|
12
|
+
"EngineRef",
|
|
13
|
+
"Relation",
|
|
14
|
+
"SqlRelation",
|
|
15
|
+
"TableRef",
|
|
16
|
+
"canonical_sql",
|
|
17
|
+
"data_fingerprint",
|
|
18
|
+
"empty_schema",
|
|
19
|
+
"metadata_fingerprint",
|
|
20
|
+
"parse",
|
|
21
|
+
"resolve_references",
|
|
22
|
+
"schema_from_fields",
|
|
23
|
+
"table_references",
|
|
24
|
+
]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Parsing and static analysis of SQL into the canonical IR.
|
|
2
|
+
|
|
3
|
+
Phase 1 covers parsing (dialect-aware) and table-reference extraction for
|
|
4
|
+
implicit dependency discovery. Qualification and type annotation
|
|
5
|
+
(``sqlglot.optimizer.qualify`` against the project schema graph) land alongside
|
|
6
|
+
column lineage in a later phase.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sqlglot
|
|
12
|
+
from sqlglot import exp
|
|
13
|
+
|
|
14
|
+
from interlace.exceptions import CompilationError
|
|
15
|
+
from interlace.ir.relation import TableRef
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def parse(sql: str, dialect: str = "duckdb") -> exp.Expression:
|
|
19
|
+
"""Parse a single SQL statement in the given dialect into a sqlglot AST."""
|
|
20
|
+
try:
|
|
21
|
+
parsed = sqlglot.parse_one(sql, dialect=dialect)
|
|
22
|
+
except Exception as exc: # sqlglot raises ParseError and friends
|
|
23
|
+
raise CompilationError(f"failed to parse SQL ({dialect})", details={"sql": sql, "error": str(exc)}) from exc
|
|
24
|
+
if parsed is None:
|
|
25
|
+
raise CompilationError("empty SQL statement", details={"sql": sql})
|
|
26
|
+
return parsed
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def table_references(ast: exp.Expression) -> list[str]:
|
|
30
|
+
"""Return the distinct real table references in an AST, excluding CTE names.
|
|
31
|
+
|
|
32
|
+
Each reference is keyed as ``db.name`` (or ``name`` when unqualified). CTE
|
|
33
|
+
aliases are filtered out — they are local names, not dependencies.
|
|
34
|
+
"""
|
|
35
|
+
cte_names = {cte.alias_or_name for cte in ast.find_all(exp.CTE)}
|
|
36
|
+
refs: list[str] = []
|
|
37
|
+
seen: set[str] = set()
|
|
38
|
+
for table in ast.find_all(exp.Table):
|
|
39
|
+
name = table.name
|
|
40
|
+
if not name or name in cte_names:
|
|
41
|
+
continue
|
|
42
|
+
key = f"{table.db}.{name}" if table.db else name
|
|
43
|
+
if key not in seen:
|
|
44
|
+
seen.add(key)
|
|
45
|
+
refs.append(key)
|
|
46
|
+
return refs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def resolve_references(ast: exp.Expression, mapping: dict[str, TableRef]) -> exp.Expression:
|
|
50
|
+
"""Rewrite model-name table references to their physical tables (returns a new AST).
|
|
51
|
+
|
|
52
|
+
``mapping`` is keyed by dependency model name; a reference matches by its
|
|
53
|
+
``db.name`` key or, failing that, its bare name (so ``main.orders`` resolves
|
|
54
|
+
to model ``orders``). Aliases are preserved; the input AST is not mutated.
|
|
55
|
+
"""
|
|
56
|
+
if not mapping:
|
|
57
|
+
return ast
|
|
58
|
+
|
|
59
|
+
def rewrite(node: exp.Expression) -> exp.Expression:
|
|
60
|
+
if isinstance(node, exp.Table):
|
|
61
|
+
key = f"{node.db}.{node.name}" if node.db else node.name
|
|
62
|
+
target = mapping.get(key) or mapping.get(node.name)
|
|
63
|
+
if target is not None:
|
|
64
|
+
if not node.alias: # keep qualified column refs (b.x) resolving after the rename
|
|
65
|
+
node.set("alias", exp.TableAlias(this=exp.to_identifier(node.name)))
|
|
66
|
+
node.set("this", exp.to_identifier(target.name))
|
|
67
|
+
node.set("db", exp.to_identifier(target.schema) if target.schema else None)
|
|
68
|
+
node.set("catalog", exp.to_identifier(target.catalog) if target.catalog else None)
|
|
69
|
+
return node
|
|
70
|
+
|
|
71
|
+
return ast.transform(rewrite)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Snapshot fingerprinting.
|
|
2
|
+
|
|
3
|
+
A model's ``data`` fingerprint is a hash of its normalised SQL AST, its
|
|
4
|
+
materialisation/strategy config, and the sorted fingerprints of its upstreams —
|
|
5
|
+
so any change that affects results (here or upstream) yields a new fingerprint
|
|
6
|
+
and triggers a rebuild. A separate ``metadata`` fingerprint covers comments,
|
|
7
|
+
owner, and tags, which must never trigger a rebuild.
|
|
8
|
+
|
|
9
|
+
This mirrors sqlmesh's snapshot model. The hash is deliberately short (16 hex
|
|
10
|
+
chars) because it becomes part of physical table names.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import json
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from sqlglot import exp
|
|
20
|
+
|
|
21
|
+
_FP_LEN = 16
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def canonical_sql(ast: exp.Expression) -> str:
|
|
25
|
+
"""Render an AST to a stable, comment-free, normalised string for hashing."""
|
|
26
|
+
return ast.sql(comments=False, normalize=True, pretty=False)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _stable_json(value: Any) -> str:
|
|
30
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), default=str)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _digest(*parts: str) -> str:
|
|
34
|
+
h = hashlib.sha256("\x00".join(parts).encode("utf-8"))
|
|
35
|
+
return h.hexdigest()[:_FP_LEN]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def data_fingerprint(
|
|
39
|
+
*,
|
|
40
|
+
query: str | exp.Expression,
|
|
41
|
+
strategy_config: dict[str, Any],
|
|
42
|
+
upstream_fingerprints: list[str],
|
|
43
|
+
) -> str:
|
|
44
|
+
"""Fingerprint that changes whenever the model's output could change.
|
|
45
|
+
|
|
46
|
+
``query`` is the canonical SQL for SQL models, or for Python models the
|
|
47
|
+
dedented source plus closure bytecode digest produced by the caller.
|
|
48
|
+
"""
|
|
49
|
+
sql = canonical_sql(query) if isinstance(query, exp.Expression) else query
|
|
50
|
+
return _digest(sql, _stable_json(strategy_config), *sorted(upstream_fingerprints))
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def metadata_fingerprint(metadata: dict[str, Any]) -> str:
|
|
54
|
+
"""Fingerprint over non-semantic metadata (comments, owner, tags)."""
|
|
55
|
+
return _digest(_stable_json(metadata))
|
interlace/ir/relation.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""The universal model-output contract.
|
|
2
|
+
|
|
3
|
+
Every model produces a :class:`Relation`. A relation is *logical* until a sink
|
|
4
|
+
forces it: a :class:`SqlRelation` carries a sqlglot AST that the owning engine
|
|
5
|
+
evaluates natively (zero rows enter Python). Python models move data as plain
|
|
6
|
+
Arrow ``RecordBatchReader`` handles (see ``runtime/``), not as relations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import ClassVar, Protocol, runtime_checkable
|
|
13
|
+
|
|
14
|
+
from sqlglot import exp
|
|
15
|
+
|
|
16
|
+
from interlace.ir.schema import ArrowSchema
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class TableRef:
|
|
21
|
+
"""A fully-qualified table identifier, dialect-agnostic until transpiled."""
|
|
22
|
+
|
|
23
|
+
schema: str
|
|
24
|
+
name: str
|
|
25
|
+
catalog: str | None = None
|
|
26
|
+
|
|
27
|
+
def to_expr(self) -> exp.Table:
|
|
28
|
+
"""This table as a sqlglot Table node (identifier-safe, dialect-agnostic)."""
|
|
29
|
+
return exp.table_(self.name, db=self.schema, catalog=self.catalog)
|
|
30
|
+
|
|
31
|
+
def qualified(self) -> str:
|
|
32
|
+
parts = [p for p in (self.catalog, self.schema, self.name) if p]
|
|
33
|
+
return ".".join(parts)
|
|
34
|
+
|
|
35
|
+
def __str__(self) -> str:
|
|
36
|
+
return self.qualified()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class EngineRef:
|
|
41
|
+
"""Identifies which engine (gateway) can evaluate a relation, and its dialect."""
|
|
42
|
+
|
|
43
|
+
name: str # connection/gateway name from config
|
|
44
|
+
dialect: str # sqlglot dialect name
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@runtime_checkable
|
|
48
|
+
class Relation(Protocol):
|
|
49
|
+
"""What every model produces. Logical until a sink forces it."""
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def schema(self) -> ArrowSchema: ...
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def plane(self) -> str: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class SqlRelation:
|
|
60
|
+
"""Logical plane: a sqlglot AST bound to the engine that can evaluate it.
|
|
61
|
+
|
|
62
|
+
The AST is canonical (qualified, type-annotated, dialect-neutral); the engine
|
|
63
|
+
adapter transpiles it at execution time. Composing SQL models never leaves
|
|
64
|
+
this plane, so no data is materialised until a sink runs a single statement.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
ast: exp.Expression
|
|
68
|
+
engine: EngineRef
|
|
69
|
+
schema: ArrowSchema
|
|
70
|
+
|
|
71
|
+
plane: ClassVar[str] = "logical"
|
interlace/ir/schema.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Canonical schema representation.
|
|
2
|
+
|
|
3
|
+
Apache Arrow is the single source of truth for column names and types across the
|
|
4
|
+
IR. Engine adapters map this to and from their native catalogs; nothing else in
|
|
5
|
+
the framework invents its own type system.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import pyarrow as pa
|
|
11
|
+
|
|
12
|
+
# The canonical schema type used throughout the IR.
|
|
13
|
+
type ArrowSchema = pa.Schema
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def schema_from_fields(fields: dict[str, pa.DataType]) -> ArrowSchema:
|
|
17
|
+
"""Build an Arrow schema from an ordered mapping of name to Arrow type."""
|
|
18
|
+
return pa.schema([pa.field(name, dtype) for name, dtype in fields.items()])
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def empty_schema() -> ArrowSchema:
|
|
22
|
+
"""An empty schema (no columns) — used when a relation's shape is not yet known."""
|
|
23
|
+
return pa.schema([])
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Plan/apply: diff models, classify changes, compute backfills, swap views."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from interlace.plan.apply import ApplyResult, apply
|
|
6
|
+
from interlace.plan.differ import diff, snapshot_of
|
|
7
|
+
from interlace.plan.plan import BackfillTask, ChangeType, ModelChange, Plan, TransferEdge, ViewSwap, env_view
|
|
8
|
+
from interlace.plan.run import run_plan
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"ApplyResult",
|
|
12
|
+
"BackfillTask",
|
|
13
|
+
"ChangeType",
|
|
14
|
+
"ModelChange",
|
|
15
|
+
"Plan",
|
|
16
|
+
"TransferEdge",
|
|
17
|
+
"ViewSwap",
|
|
18
|
+
"apply",
|
|
19
|
+
"diff",
|
|
20
|
+
"env_view",
|
|
21
|
+
"run_plan",
|
|
22
|
+
"snapshot_of",
|
|
23
|
+
]
|