sdmxlib 0.26.0__tar.gz → 0.27.0__tar.gz
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.
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/PKG-INFO +1 -1
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/pyproject.toml +1 -1
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/catalog.py +7 -6
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/data_store.py +40 -3
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/local/_data_store.py +224 -13
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/binding.py +93 -5
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/storage/readers.py +3 -1
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/storage/schema.py +1 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/storage/writers.py +16 -3
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/README.md +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/client.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/federated.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/filters.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/policy.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/providers.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/query.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/registry.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/api/session.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/local/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/local/_registry.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/annotations.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/base.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/category.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/code_query.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/codelist.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/collections.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/concept.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/constraint.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/convert.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/dataflow.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/dataset.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/datastructure.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/expr.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/hierarchy.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/hierarchy_query.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/in_memory_registry.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/istring.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/mapping.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/message.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/metadataflow.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/metadataset.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/metadatastructure.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/organisation.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/provision.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/ref.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/registry.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/representation.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/urn.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/model/validation.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/polars.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/py.typed +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/rest.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/sql.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/storage/__init__.py +0 -0
- {sdmxlib-0.26.0 → sdmxlib-0.27.0}/src/sdmxlib/storage/lazy.py +0 -0
|
@@ -21,9 +21,9 @@ Two construction paths:
|
|
|
21
21
|
Standard layout::
|
|
22
22
|
|
|
23
23
|
<bundle>/
|
|
24
|
-
|
|
24
|
+
meta.duckdb # sdmx.* + pinax.* schemas
|
|
25
25
|
lake.ducklake # DuckLake catalog
|
|
26
|
-
parquet/ # DuckLake-managed data
|
|
26
|
+
parquet/ # DuckLake-managed data (DATA_PATH)
|
|
27
27
|
|
|
28
28
|
2. ``FederatedCatalog.from_parts(structural=, bindings=, data=)`` —
|
|
29
29
|
composition. For federated topologies (remote structural registry +
|
|
@@ -136,21 +136,22 @@ class FederatedCatalog:
|
|
|
136
136
|
def open(self) -> Self:
|
|
137
137
|
"""Open the bundle's Registry + DuckDBDataStore against the standard layout.
|
|
138
138
|
|
|
139
|
-
Looks for ``<uri>/
|
|
139
|
+
Looks for ``<uri>/meta.duckdb`` (sdmx + pinax artefacts) and
|
|
140
140
|
``<uri>/lake.ducklake`` + ``<uri>/parquet/`` (DuckLake-managed
|
|
141
141
|
observations). The Registry serves both ``structural`` and
|
|
142
142
|
``bindings`` roles. Idempotent within a single open/close cycle.
|
|
143
143
|
"""
|
|
144
144
|
if self._structural is not None:
|
|
145
145
|
return self
|
|
146
|
-
registry_path = self._uri / "
|
|
146
|
+
registry_path = self._uri / "meta.duckdb"
|
|
147
147
|
lake_path = self._uri / "lake.ducklake"
|
|
148
148
|
parquet_path = self._uri / "parquet"
|
|
149
149
|
registry = Registry(registry_path).open()
|
|
150
|
-
|
|
150
|
+
attach_url = f"'ducklake:{lake_path}'"
|
|
151
|
+
attach_options = f"(DATA_PATH '{parquet_path}/')"
|
|
151
152
|
data = DuckDBDataStore.from_registry(
|
|
152
153
|
registry,
|
|
153
|
-
attach={"lake":
|
|
154
|
+
attach={"lake": (attach_url, attach_options)},
|
|
154
155
|
extensions=["ducklake"],
|
|
155
156
|
)
|
|
156
157
|
self._owned_registry = registry
|
|
@@ -19,20 +19,24 @@ structural side.
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
21
|
from collections.abc import Iterator
|
|
22
|
-
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
22
|
+
from typing import TYPE_CHECKING, Literal, Protocol, runtime_checkable
|
|
23
23
|
|
|
24
24
|
import polars as pl
|
|
25
25
|
|
|
26
26
|
from sdmxlib.model.binding import DataflowBinding
|
|
27
27
|
|
|
28
|
+
if TYPE_CHECKING:
|
|
29
|
+
import duckdb
|
|
30
|
+
import pyarrow as pa
|
|
31
|
+
|
|
32
|
+
WriteSource = pl.DataFrame | pl.LazyFrame | pa.Table | duckdb.DuckDBPyRelation
|
|
33
|
+
|
|
28
34
|
# SqlRelation lives in sdmxlib.sql now (suite-level public contract);
|
|
29
35
|
# re-exported here for backwards compatibility with code that imports
|
|
30
36
|
# ``from sdmxlib.data_store import SqlRelation``.
|
|
31
37
|
from sdmxlib.sql import SqlRelation as SqlRelation # noqa: PLC0414 — public re-export
|
|
32
38
|
|
|
33
39
|
if TYPE_CHECKING:
|
|
34
|
-
import pyarrow as pa
|
|
35
|
-
|
|
36
40
|
from sdmxlib.rest import ComponentFilter
|
|
37
41
|
|
|
38
42
|
|
|
@@ -115,3 +119,36 @@ class DataStore(Protocol):
|
|
|
115
119
|
One :class:`pyarrow.RecordBatch` at a time, in scan order.
|
|
116
120
|
"""
|
|
117
121
|
...
|
|
122
|
+
|
|
123
|
+
def write_observations(
|
|
124
|
+
self,
|
|
125
|
+
*,
|
|
126
|
+
schema: str,
|
|
127
|
+
table: str,
|
|
128
|
+
source: WriteSource,
|
|
129
|
+
partition_by: tuple[str, ...] = (),
|
|
130
|
+
mode: Literal["overwrite", "append"] = "overwrite",
|
|
131
|
+
lake_alias: str = "lake",
|
|
132
|
+
) -> None:
|
|
133
|
+
"""Materialise observation rows into a backend-managed table.
|
|
134
|
+
|
|
135
|
+
Implementations route the write to whatever physical storage they
|
|
136
|
+
manage (DuckLake, Iceberg, Postgres, raw Parquet). The caller
|
|
137
|
+
side is uniform: schema + table + a source that can be turned
|
|
138
|
+
into rows.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
schema: Target schema.
|
|
142
|
+
table: Target table name.
|
|
143
|
+
source: Rows to write. Accepted shapes vary by implementation
|
|
144
|
+
but Polars frames and Arrow tables are the portable
|
|
145
|
+
lowest common denominator.
|
|
146
|
+
partition_by: SDMX dimension ids the table is partitioned on.
|
|
147
|
+
Recorded on the matching :class:`DataflowBinding`.
|
|
148
|
+
mode: ``"overwrite"`` replaces atomically; ``"append"``
|
|
149
|
+
requires the table to already exist.
|
|
150
|
+
lake_alias: For ATTACH-based backends, the alias of the
|
|
151
|
+
target catalog. Implementations that don't use ATTACH
|
|
152
|
+
may ignore this argument.
|
|
153
|
+
"""
|
|
154
|
+
...
|
|
@@ -17,7 +17,7 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
from collections.abc import Iterator
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import TYPE_CHECKING, Self
|
|
20
|
+
from typing import TYPE_CHECKING, Literal, Self
|
|
21
21
|
|
|
22
22
|
import duckdb
|
|
23
23
|
import polars as pl
|
|
@@ -30,6 +30,11 @@ if TYPE_CHECKING:
|
|
|
30
30
|
from sdmxlib.local._registry import Registry
|
|
31
31
|
from sdmxlib.rest import ComponentFilter
|
|
32
32
|
|
|
33
|
+
# Sources that ``write_observations`` accepts. Type-checking only:
|
|
34
|
+
# the runtime path branches on isinstance(), so a runtime alias isn't
|
|
35
|
+
# needed (and pyarrow is an optional dependency).
|
|
36
|
+
WriteSource = pl.DataFrame | pl.LazyFrame | pa.Table | duckdb.DuckDBPyRelation
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
class DuckDBDataStore:
|
|
35
40
|
"""A :class:`~sdmxlib.data_store.DataStore` backed by DuckDB.
|
|
@@ -45,10 +50,27 @@ class DuckDBDataStore:
|
|
|
45
50
|
Lazy codelists keep working across queries, and queries can join
|
|
46
51
|
across the two without cross-connection plumbing.
|
|
47
52
|
|
|
48
|
-
The ``attach`` / ``extensions``
|
|
49
|
-
at open time. ``attach`` maps
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
The ``attach`` / ``extensions`` / ``azure_credentials`` knobs
|
|
54
|
+
configure the DuckDB session at open time. ``attach`` maps
|
|
55
|
+
``alias → ATTACH spec`` (each executed as
|
|
56
|
+
``ATTACH <spec> AS <alias>``); ``extensions`` INSTALL+LOAD the
|
|
57
|
+
listed extensions before any ATTACH. ``azure_credentials`` is a
|
|
58
|
+
dict that drives a DuckDB ``CREATE SECRET`` for ADLSv2 access — see
|
|
59
|
+
below.
|
|
60
|
+
|
|
61
|
+
Azure credential dicts accept one of three shapes:
|
|
62
|
+
|
|
63
|
+
* ``{"connection_string": "..."}`` → ``PROVIDER 'config'`` with
|
|
64
|
+
``CONNECTION_STRING``.
|
|
65
|
+
* ``{"account_name": ..., "tenant_id": ..., "client_id": ...,
|
|
66
|
+
"client_secret": ...}`` → ``PROVIDER 'service_principal'``.
|
|
67
|
+
* ``{"chain": "cli"}`` (or ``"env"``, ``"managed_identity"``) →
|
|
68
|
+
``PROVIDER 'credential_chain'`` with the named chain.
|
|
69
|
+
|
|
70
|
+
When ``azure_credentials`` is set, or any ATTACH spec contains
|
|
71
|
+
``abfss://``, the ``azure`` extension is auto-INSTALL+LOADed so
|
|
72
|
+
DATA_PATH URIs like ``abfss://acct.dfs.core.windows.net/c/p/`` work
|
|
73
|
+
out of the box.
|
|
52
74
|
|
|
53
75
|
Example::
|
|
54
76
|
|
|
@@ -65,12 +87,14 @@ class DuckDBDataStore:
|
|
|
65
87
|
self,
|
|
66
88
|
uri: str | Path,
|
|
67
89
|
*,
|
|
68
|
-
attach: dict[str, str] | None = None,
|
|
90
|
+
attach: dict[str, str | tuple[str, str]] | None = None,
|
|
69
91
|
extensions: list[str] | None = None,
|
|
92
|
+
azure_credentials: dict[str, str] | None = None,
|
|
70
93
|
) -> None:
|
|
71
94
|
self._uri: Path = Path(uri) if isinstance(uri, str) else uri
|
|
72
95
|
self._attach = attach
|
|
73
96
|
self._extensions = extensions
|
|
97
|
+
self._azure_credentials = azure_credentials
|
|
74
98
|
self._registry: Registry | None = None
|
|
75
99
|
self._conn: duckdb.DuckDBPyConnection | None = None
|
|
76
100
|
self._owns_connection = True
|
|
@@ -122,8 +146,9 @@ class DuckDBDataStore:
|
|
|
122
146
|
cls,
|
|
123
147
|
registry: Registry,
|
|
124
148
|
*,
|
|
125
|
-
attach: dict[str, str] | None = None,
|
|
149
|
+
attach: dict[str, str | tuple[str, str]] | None = None,
|
|
126
150
|
extensions: list[str] | None = None,
|
|
151
|
+
azure_credentials: dict[str, str] | None = None,
|
|
127
152
|
) -> Self:
|
|
128
153
|
"""Wrap a :class:`Registry`'s open connection.
|
|
129
154
|
|
|
@@ -135,11 +160,14 @@ class DuckDBDataStore:
|
|
|
135
160
|
registry: An open :class:`Registry`. Its connection is reused.
|
|
136
161
|
attach: ``alias → ATTACH spec`` mapping. Executed at open time.
|
|
137
162
|
extensions: DuckDB extensions to INSTALL+LOAD before any ATTACH.
|
|
163
|
+
azure_credentials: Optional Azure secret config. See
|
|
164
|
+
:meth:`__init__` for accepted keys.
|
|
138
165
|
"""
|
|
139
166
|
instance = cls.__new__(cls)
|
|
140
167
|
instance._uri = registry.uri # noqa: SLF001 — alternate constructor sets private state
|
|
141
168
|
instance._attach = attach # noqa: SLF001
|
|
142
169
|
instance._extensions = extensions # noqa: SLF001
|
|
170
|
+
instance._azure_credentials = azure_credentials # noqa: SLF001
|
|
143
171
|
instance._registry = registry # noqa: SLF001
|
|
144
172
|
instance._conn = registry.connection # noqa: SLF001
|
|
145
173
|
instance._owns_connection = False # noqa: SLF001
|
|
@@ -150,18 +178,42 @@ class DuckDBDataStore:
|
|
|
150
178
|
"""Apply the configured extensions + ATTACH specs to the live connection."""
|
|
151
179
|
if self._conn is None:
|
|
152
180
|
return
|
|
153
|
-
|
|
154
|
-
|
|
181
|
+
extensions = list(self._extensions or [])
|
|
182
|
+
# Auto-load the azure extension if we need to talk to abfss:// or
|
|
183
|
+
# have credentials configured. Callers can override by listing
|
|
184
|
+
# "azure" in extensions themselves — INSTALL/LOAD are idempotent.
|
|
185
|
+
if self._needs_azure() and "azure" not in extensions:
|
|
186
|
+
extensions.append("azure")
|
|
187
|
+
if extensions:
|
|
188
|
+
for ext in extensions:
|
|
155
189
|
self._conn.execute(f"INSTALL {_sql_identifier(ext)}")
|
|
156
190
|
self._conn.execute(f"LOAD {_sql_identifier(ext)}")
|
|
157
191
|
# Loading extensions can change the search_path; reset to
|
|
158
192
|
# ``sdmx`` so any colocated Registry's internal queries stay clean.
|
|
159
193
|
self._conn.execute("SET search_path = 'sdmx'")
|
|
194
|
+
if self._azure_credentials:
|
|
195
|
+
sql = _azure_create_secret_sql(self._azure_credentials)
|
|
196
|
+
self._conn.execute(sql)
|
|
160
197
|
if self._attach:
|
|
161
198
|
for alias, spec in self._attach.items():
|
|
162
|
-
|
|
199
|
+
url, options = _split_attach_spec(spec)
|
|
200
|
+
sql = f"ATTACH {url} AS {_sql_identifier(alias)}"
|
|
201
|
+
if options:
|
|
202
|
+
sql += f" {options}"
|
|
203
|
+
self._conn.execute(sql)
|
|
163
204
|
self._conn.execute("SET search_path = 'sdmx'")
|
|
164
205
|
|
|
206
|
+
def _needs_azure(self) -> bool:
|
|
207
|
+
"""True if Azure secret/extension setup is required for this session."""
|
|
208
|
+
if self._azure_credentials:
|
|
209
|
+
return True
|
|
210
|
+
if self._attach:
|
|
211
|
+
for spec in self._attach.values():
|
|
212
|
+
spec_text = spec if isinstance(spec, str) else " ".join(s for s in spec if s)
|
|
213
|
+
if "abfss://" in spec_text:
|
|
214
|
+
return True
|
|
215
|
+
return False
|
|
216
|
+
|
|
165
217
|
def __enter__(self) -> Self:
|
|
166
218
|
if self._conn is None:
|
|
167
219
|
self.open()
|
|
@@ -222,6 +274,69 @@ class DuckDBDataStore:
|
|
|
222
274
|
return pl.DataFrame(schema=col_names)
|
|
223
275
|
return pl.from_records(rows, schema=col_names, orient="row")
|
|
224
276
|
|
|
277
|
+
def write_observations(
|
|
278
|
+
self,
|
|
279
|
+
*,
|
|
280
|
+
schema: str,
|
|
281
|
+
table: str,
|
|
282
|
+
source: WriteSource,
|
|
283
|
+
partition_by: tuple[str, ...] = (),
|
|
284
|
+
mode: Literal["overwrite", "append"] = "overwrite",
|
|
285
|
+
lake_alias: str = "lake",
|
|
286
|
+
) -> None:
|
|
287
|
+
"""Materialise observation rows into a DuckLake-managed table.
|
|
288
|
+
|
|
289
|
+
Routes through the ATTACHed lake (``lake_alias``, default ``lake``).
|
|
290
|
+
Schema is created if missing. ``mode="overwrite"`` uses
|
|
291
|
+
``CREATE OR REPLACE TABLE``; ``mode="append"`` uses ``INSERT INTO``
|
|
292
|
+
and requires the target table to already exist with a compatible
|
|
293
|
+
schema. ``partition_by`` is currently informational — DuckLake
|
|
294
|
+
infers partition columns from table definition; we record the
|
|
295
|
+
layout on the matching :class:`DataflowBinding`.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
schema: Target schema inside the lake (created if absent).
|
|
299
|
+
table: Target table name. Together with ``schema`` and
|
|
300
|
+
``lake_alias`` this resolves to
|
|
301
|
+
``<lake_alias>.<schema>.<table>``.
|
|
302
|
+
source: Rows to write. Anything DuckDB can register as a
|
|
303
|
+
virtual relation — Polars frames, Arrow tables, or an
|
|
304
|
+
existing DuckDB relation.
|
|
305
|
+
partition_by: SDMX dimension ids the table is partitioned on.
|
|
306
|
+
mode: ``"overwrite"`` replaces the table atomically;
|
|
307
|
+
``"append"`` inserts into an existing table.
|
|
308
|
+
lake_alias: ATTACH alias of the target lake. Defaults to
|
|
309
|
+
``"lake"`` to match the conventional bundle layout.
|
|
310
|
+
"""
|
|
311
|
+
if not _is_valid_identifier(schema):
|
|
312
|
+
msg = f"Invalid schema identifier: {schema!r}"
|
|
313
|
+
raise ValueError(msg)
|
|
314
|
+
if not _is_valid_identifier(table):
|
|
315
|
+
msg = f"Invalid table identifier: {table!r}"
|
|
316
|
+
raise ValueError(msg)
|
|
317
|
+
if not _is_valid_identifier(lake_alias):
|
|
318
|
+
msg = f"Invalid lake alias: {lake_alias!r}"
|
|
319
|
+
raise ValueError(msg)
|
|
320
|
+
|
|
321
|
+
conn = self.connection
|
|
322
|
+
# Register the source as a temporary view the CTAS can scan.
|
|
323
|
+
view_name = f"__sdmxlib_write_obs_{id(source):x}"
|
|
324
|
+
rel = _as_relation(conn, source)
|
|
325
|
+
rel.create_view(view_name, replace=True)
|
|
326
|
+
try:
|
|
327
|
+
target = f"{lake_alias}.{schema}.{table}"
|
|
328
|
+
conn.execute(f"CREATE SCHEMA IF NOT EXISTS {lake_alias}.{schema}")
|
|
329
|
+
if mode == "overwrite":
|
|
330
|
+
conn.execute(f"CREATE OR REPLACE TABLE {target} AS SELECT * FROM {view_name}")
|
|
331
|
+
elif mode == "append":
|
|
332
|
+
conn.execute(f"INSERT INTO {target} SELECT * FROM {view_name}")
|
|
333
|
+
else:
|
|
334
|
+
msg = f"Unknown mode: {mode!r} (expected 'overwrite' or 'append')"
|
|
335
|
+
raise ValueError(msg)
|
|
336
|
+
finally:
|
|
337
|
+
conn.execute(f"DROP VIEW IF EXISTS {view_name}")
|
|
338
|
+
_ = partition_by # silence unused — see docstring
|
|
339
|
+
|
|
225
340
|
def query_stream(
|
|
226
341
|
self,
|
|
227
342
|
binding: DataflowBinding,
|
|
@@ -282,8 +397,9 @@ def _build_query(
|
|
|
282
397
|
attribute column maps and ANDed in.
|
|
283
398
|
"""
|
|
284
399
|
cols_sql = ", ".join(columns) if columns else "*"
|
|
285
|
-
main = binding.table_location
|
|
286
|
-
|
|
400
|
+
main = f"{binding.database_role}.{binding.table_location}"
|
|
401
|
+
sidecar_loc = binding.series_attributes_location
|
|
402
|
+
sidecar = f"{binding.database_role}.{sidecar_loc}" if sidecar_loc is not None else None
|
|
287
403
|
if sidecar is None:
|
|
288
404
|
sql = f"SELECT {cols_sql} FROM {main}"
|
|
289
405
|
else:
|
|
@@ -335,7 +451,102 @@ def _sql_identifier(name: str) -> str:
|
|
|
335
451
|
and underscores. Reject anything else to avoid SQL injection through
|
|
336
452
|
extension/alias names.
|
|
337
453
|
"""
|
|
338
|
-
if not
|
|
454
|
+
if not _is_valid_identifier(name):
|
|
339
455
|
msg = f"Invalid SQL identifier: {name!r}"
|
|
340
456
|
raise ValueError(msg)
|
|
341
457
|
return name
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _is_valid_identifier(name: str) -> bool:
|
|
461
|
+
"""True if ``name`` is a safe SQL identifier (alphanumeric + underscore)."""
|
|
462
|
+
return bool(name) and all(c.isalnum() or c == "_" for c in name)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _split_attach_spec(spec: object) -> tuple[str, str]:
|
|
466
|
+
"""Normalise an ATTACH spec into ``(url, options)``.
|
|
467
|
+
|
|
468
|
+
Two shapes accepted:
|
|
469
|
+
|
|
470
|
+
- ``"'ducklake:/path'"`` — connection URL only; no options.
|
|
471
|
+
- ``("'ducklake:/path'", "(DATA_PATH '/p/')")`` — URL + trailing
|
|
472
|
+
options clause that goes after ``AS alias``.
|
|
473
|
+
|
|
474
|
+
Returns ``(url, options)`` strings; ``options`` is empty when none.
|
|
475
|
+
"""
|
|
476
|
+
if isinstance(spec, str):
|
|
477
|
+
return spec, ""
|
|
478
|
+
if isinstance(spec, tuple) and len(spec) == 2:
|
|
479
|
+
url, options = spec
|
|
480
|
+
return str(url), str(options or "")
|
|
481
|
+
msg = f"Unsupported ATTACH spec shape: {type(spec).__name__}"
|
|
482
|
+
raise TypeError(msg)
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _azure_create_secret_sql(creds: dict[str, str]) -> str:
|
|
486
|
+
"""Render a DuckDB ``CREATE SECRET`` statement for an Azure credential dict.
|
|
487
|
+
|
|
488
|
+
Three accepted shapes:
|
|
489
|
+
|
|
490
|
+
* ``{"connection_string": "..."}`` → ``PROVIDER 'config'`` with
|
|
491
|
+
``CONNECTION_STRING``.
|
|
492
|
+
* ``{"account_name": ..., "tenant_id": ..., "client_id": ...,
|
|
493
|
+
"client_secret": ...}`` → ``PROVIDER 'service_principal'``.
|
|
494
|
+
* ``{"chain": "cli"|"env"|"managed_identity"}`` →
|
|
495
|
+
``PROVIDER 'credential_chain'``.
|
|
496
|
+
"""
|
|
497
|
+
name = "sdmxlib_azure"
|
|
498
|
+
if "connection_string" in creds:
|
|
499
|
+
cs = _sql_string_literal(creds["connection_string"])
|
|
500
|
+
return f"CREATE OR REPLACE SECRET {name} (TYPE AZURE, PROVIDER 'config', CONNECTION_STRING {cs})"
|
|
501
|
+
if "chain" in creds:
|
|
502
|
+
chain = _sql_string_literal(creds["chain"])
|
|
503
|
+
clauses = ["TYPE AZURE", "PROVIDER 'credential_chain'", f"CHAIN {chain}"]
|
|
504
|
+
if "account_name" in creds:
|
|
505
|
+
clauses.append(f"ACCOUNT_NAME {_sql_string_literal(creds['account_name'])}")
|
|
506
|
+
return f"CREATE OR REPLACE SECRET {name} ({', '.join(clauses)})"
|
|
507
|
+
required = {"account_name", "tenant_id", "client_id", "client_secret"}
|
|
508
|
+
if required.issubset(creds):
|
|
509
|
+
return (
|
|
510
|
+
f"CREATE OR REPLACE SECRET {name} ("
|
|
511
|
+
f"TYPE AZURE, PROVIDER 'service_principal', "
|
|
512
|
+
f"TENANT_ID {_sql_string_literal(creds['tenant_id'])}, "
|
|
513
|
+
f"CLIENT_ID {_sql_string_literal(creds['client_id'])}, "
|
|
514
|
+
f"CLIENT_SECRET {_sql_string_literal(creds['client_secret'])}, "
|
|
515
|
+
f"ACCOUNT_NAME {_sql_string_literal(creds['account_name'])}"
|
|
516
|
+
f")"
|
|
517
|
+
)
|
|
518
|
+
msg = (
|
|
519
|
+
"azure_credentials must be one of: "
|
|
520
|
+
"{'connection_string': ...}, "
|
|
521
|
+
"{'account_name': ..., 'tenant_id': ..., 'client_id': ..., 'client_secret': ...}, "
|
|
522
|
+
"or {'chain': 'cli'|'env'|'managed_identity'}. "
|
|
523
|
+
f"Got keys: {sorted(creds.keys())}"
|
|
524
|
+
)
|
|
525
|
+
raise ValueError(msg)
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _sql_string_literal(value: str) -> str:
|
|
529
|
+
"""Quote a value as a SQL string literal — single-quotes, doubled internals."""
|
|
530
|
+
escaped = value.replace("'", "''")
|
|
531
|
+
return f"'{escaped}'"
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _as_relation(
|
|
535
|
+
conn: duckdb.DuckDBPyConnection,
|
|
536
|
+
source: WriteSource,
|
|
537
|
+
) -> duckdb.DuckDBPyRelation:
|
|
538
|
+
"""Materialise ``source`` as a DuckDB relation the connection can scan.
|
|
539
|
+
|
|
540
|
+
Handles Polars frames (eager + lazy), Arrow tables, and existing
|
|
541
|
+
DuckDB relations uniformly. ``LazyFrame`` is collected to a DataFrame
|
|
542
|
+
first — callers that need streaming behaviour for very large frames
|
|
543
|
+
can pass an Arrow ``RecordBatchReader`` via :func:`pyarrow.Table.from_batches`.
|
|
544
|
+
"""
|
|
545
|
+
if isinstance(source, duckdb.DuckDBPyRelation):
|
|
546
|
+
return source
|
|
547
|
+
if isinstance(source, pl.LazyFrame):
|
|
548
|
+
source = source.collect()
|
|
549
|
+
if isinstance(source, pl.DataFrame):
|
|
550
|
+
return conn.from_arrow(source.to_arrow())
|
|
551
|
+
# Last branch: pyarrow.Table. Imported lazily to keep pyarrow optional.
|
|
552
|
+
return conn.from_arrow(source)
|
|
@@ -10,10 +10,18 @@ The binding artefact is local-store-owned: a Dataflow may live in a
|
|
|
10
10
|
remote registry (FMR, ESTAT, ECB), but the binding to a Parquet/DuckLake
|
|
11
11
|
table lives in the local registry that knows about the data.
|
|
12
12
|
|
|
13
|
+
The bundle architecture splits the physical address of a table into
|
|
14
|
+
``database_role`` (ATTACH alias of the catalog database) and
|
|
15
|
+
``table_location`` (``schema.table`` inside that database). The resolver
|
|
16
|
+
re-joins them at query time as ``<database_role>.<table_location>``;
|
|
17
|
+
this keeps bindings portable across bundles that may attach the lake
|
|
18
|
+
under different aliases.
|
|
19
|
+
|
|
13
20
|
Example:
|
|
14
21
|
binding = DataflowBinding(
|
|
15
22
|
dataflow=Ref[Dataflow].unresolved(dataflow_urn),
|
|
16
|
-
|
|
23
|
+
database_role="lake",
|
|
24
|
+
table_location="census_2021.observations",
|
|
17
25
|
partition_layout=("GEO_LEVEL",),
|
|
18
26
|
dimension_column_map={"REF_AREA": "REF_AREA", "TIME_PERIOD": "TIME_PERIOD"},
|
|
19
27
|
measure_column_map={"OBS_COUNT": "OBS_COUNT"},
|
|
@@ -26,7 +34,7 @@ Example:
|
|
|
26
34
|
local.add(binding)
|
|
27
35
|
"""
|
|
28
36
|
|
|
29
|
-
from datetime import datetime
|
|
37
|
+
from datetime import UTC, datetime
|
|
30
38
|
from typing import TYPE_CHECKING, ClassVar, Self
|
|
31
39
|
|
|
32
40
|
from attrs import define, field, frozen
|
|
@@ -36,6 +44,7 @@ from sdmxlib.model.ref import Ref
|
|
|
36
44
|
from sdmxlib.model.urn import SdmxUrn
|
|
37
45
|
|
|
38
46
|
if TYPE_CHECKING:
|
|
47
|
+
from sdmxlib.model.datastructure import DataStructure
|
|
39
48
|
from sdmxlib.model.registry import RegistryReader
|
|
40
49
|
|
|
41
50
|
|
|
@@ -45,9 +54,15 @@ class DataflowBinding:
|
|
|
45
54
|
|
|
46
55
|
Attributes:
|
|
47
56
|
dataflow: Ref to the bound dataflow (may be remote).
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
database_role: ATTACH alias of the catalog database that owns the
|
|
58
|
+
table. Defaults to ``"lake"`` to match the conventional
|
|
59
|
+
bundle layout (``meta.duckdb`` + ``lake.ducklake`` attached
|
|
60
|
+
as ``lake``). Combined with :attr:`table_location` at query
|
|
61
|
+
time as ``<database_role>.<table_location>``.
|
|
62
|
+
table_location: ``schema.table`` inside the database identified
|
|
63
|
+
by :attr:`database_role`. Does NOT include the database
|
|
64
|
+
alias; the resolver prepends it. For raw Parquet stores
|
|
65
|
+
without an ATTACH this can be a path or URI.
|
|
51
66
|
partition_layout: SDMX dimension ids the table is partitioned on,
|
|
52
67
|
in partition order. Drives partition pruning at query time.
|
|
53
68
|
dimension_column_map: SDMX dimension id → table column name.
|
|
@@ -73,6 +88,7 @@ class DataflowBinding:
|
|
|
73
88
|
|
|
74
89
|
dataflow: Ref[Dataflow]
|
|
75
90
|
table_location: str
|
|
91
|
+
database_role: str = "lake"
|
|
76
92
|
partition_layout: tuple[str, ...] = field(factory=tuple, converter=tuple)
|
|
77
93
|
dimension_column_map: dict[str, str] = field(factory=dict)
|
|
78
94
|
measure_column_map: dict[str, str] = field(factory=dict)
|
|
@@ -124,6 +140,78 @@ class DataflowBinding:
|
|
|
124
140
|
version=version,
|
|
125
141
|
)
|
|
126
142
|
|
|
143
|
+
# ── Convenience constructor ──────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
@classmethod
|
|
146
|
+
def from_dsd(
|
|
147
|
+
cls,
|
|
148
|
+
dsd: "DataStructure",
|
|
149
|
+
*,
|
|
150
|
+
dataflow: Ref[Dataflow],
|
|
151
|
+
schema: str,
|
|
152
|
+
table: str,
|
|
153
|
+
partition_layout: tuple[str, ...] = (),
|
|
154
|
+
source_registry: str = "local",
|
|
155
|
+
materialized_at: datetime | None = None,
|
|
156
|
+
bound_at: datetime | None = None,
|
|
157
|
+
database_role: str = "lake",
|
|
158
|
+
series_attributes_location: str | None = None,
|
|
159
|
+
) -> "DataflowBinding":
|
|
160
|
+
"""Build a DataflowBinding from a DSD with identity column mapping.
|
|
161
|
+
|
|
162
|
+
Derives ``dimension_column_map`` / ``measure_column_map`` /
|
|
163
|
+
``attribute_column_map`` from the DSD's components (SDMX id →
|
|
164
|
+
SDMX id), pins ``structural_version`` as
|
|
165
|
+
``f"{dsd.agency_id}:{dsd.id}({dsd.version})"``, and addresses the
|
|
166
|
+
target table via ``database_role`` + ``schema.table`` (no
|
|
167
|
+
database alias prefix is written into ``table_location``).
|
|
168
|
+
|
|
169
|
+
``materialized_at`` and ``bound_at`` default to ``datetime.now(UTC)``.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
dsd: The :class:`DataStructure` whose components define the
|
|
173
|
+
column maps.
|
|
174
|
+
dataflow: Ref to the bound dataflow.
|
|
175
|
+
schema: Schema portion of ``table_location``.
|
|
176
|
+
table: Table portion of ``table_location``.
|
|
177
|
+
partition_layout: SDMX dimension ids the table is partitioned
|
|
178
|
+
on, in partition order.
|
|
179
|
+
source_registry: Provenance label for the structural side.
|
|
180
|
+
materialized_at: When the data was last refreshed. Defaults
|
|
181
|
+
to ``datetime.now(UTC)``.
|
|
182
|
+
bound_at: When this binding record was created. Defaults to
|
|
183
|
+
``datetime.now(UTC)``.
|
|
184
|
+
database_role: ATTACH alias of the database that owns the
|
|
185
|
+
table. Defaults to ``"lake"``.
|
|
186
|
+
series_attributes_location: Optional sidecar table for
|
|
187
|
+
series-level attributes (same shape as
|
|
188
|
+
``schema.table`` — no database alias prefix).
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
A :class:`DataflowBinding` ready to be added to a registry.
|
|
192
|
+
"""
|
|
193
|
+
now = datetime.now(UTC)
|
|
194
|
+
dim_map = {d.id: d.id for d in dsd.dimensions}
|
|
195
|
+
if dsd.time_dimension is not None:
|
|
196
|
+
dim_map[dsd.time_dimension.id] = dsd.time_dimension.id
|
|
197
|
+
measure_map = {m.id: m.id for m in dsd.measures}
|
|
198
|
+
attr_map = {a.id: a.id for a in dsd.attributes}
|
|
199
|
+
structural_version = f"{dsd.agency_id}:{dsd.id}({dsd.version})"
|
|
200
|
+
return cls(
|
|
201
|
+
dataflow=dataflow,
|
|
202
|
+
database_role=database_role,
|
|
203
|
+
table_location=f"{schema}.{table}",
|
|
204
|
+
partition_layout=partition_layout,
|
|
205
|
+
dimension_column_map=dim_map,
|
|
206
|
+
measure_column_map=measure_map,
|
|
207
|
+
attribute_column_map=attr_map,
|
|
208
|
+
structural_version=structural_version,
|
|
209
|
+
source_registry=source_registry,
|
|
210
|
+
materialized_at=materialized_at if materialized_at is not None else now,
|
|
211
|
+
bound_at=bound_at if bound_at is not None else now,
|
|
212
|
+
series_attributes_location=series_attributes_location,
|
|
213
|
+
)
|
|
214
|
+
|
|
127
215
|
|
|
128
216
|
# ── Drift detection ─────────────────────────────────────────────────────
|
|
129
217
|
|
|
@@ -1176,7 +1176,7 @@ def _read_dataflow_binding(conn: duckdb.DuckDBPyConnection, urn: str) -> Dataflo
|
|
|
1176
1176
|
|
|
1177
1177
|
row = conn.execute(
|
|
1178
1178
|
"""
|
|
1179
|
-
SELECT dataflow_urn, table_location, partition_layout,
|
|
1179
|
+
SELECT dataflow_urn, table_location, database_role, partition_layout,
|
|
1180
1180
|
dimension_column_map, measure_column_map, attribute_column_map,
|
|
1181
1181
|
structural_version, binding_source_registry,
|
|
1182
1182
|
materialized_at, bound_at, series_attributes_location
|
|
@@ -1190,6 +1190,7 @@ def _read_dataflow_binding(conn: duckdb.DuckDBPyConnection, urn: str) -> Dataflo
|
|
|
1190
1190
|
(
|
|
1191
1191
|
dataflow_urn_str,
|
|
1192
1192
|
table_location,
|
|
1193
|
+
database_role,
|
|
1193
1194
|
partition_layout,
|
|
1194
1195
|
dim_map,
|
|
1195
1196
|
meas_map,
|
|
@@ -1204,6 +1205,7 @@ def _read_dataflow_binding(conn: duckdb.DuckDBPyConnection, urn: str) -> Dataflo
|
|
|
1204
1205
|
return DataflowBinding(
|
|
1205
1206
|
dataflow=Ref[Dataflow].unresolved(SdmxUrn.parse(dataflow_urn_str)),
|
|
1206
1207
|
table_location=table_location,
|
|
1208
|
+
database_role=database_role,
|
|
1207
1209
|
partition_layout=tuple(partition_layout or ()),
|
|
1208
1210
|
dimension_column_map=dict(dim_map or {}),
|
|
1209
1211
|
measure_column_map=dict(meas_map or {}),
|
|
@@ -361,6 +361,7 @@ CREATE TABLE IF NOT EXISTS dataflow_binding (
|
|
|
361
361
|
urn TEXT PRIMARY KEY,
|
|
362
362
|
dataflow_urn TEXT NOT NULL,
|
|
363
363
|
table_location TEXT NOT NULL,
|
|
364
|
+
database_role TEXT NOT NULL DEFAULT 'lake',
|
|
364
365
|
partition_layout TEXT[],
|
|
365
366
|
dimension_column_map MAP(VARCHAR, VARCHAR),
|
|
366
367
|
measure_column_map MAP(VARCHAR, VARCHAR),
|
|
@@ -296,7 +296,18 @@ def _put_refs(
|
|
|
296
296
|
conn: duckdb.DuckDBPyConnection,
|
|
297
297
|
refs: list[tuple[str, str, str, int | None]],
|
|
298
298
|
) -> None:
|
|
299
|
-
"""Insert artefact_ref rows.
|
|
299
|
+
"""Insert artefact_ref rows.
|
|
300
|
+
|
|
301
|
+
Treats ``(source_urn, target_urn, role)`` as set semantics: a DSD
|
|
302
|
+
that references the same codelist via multiple dimensions yields a
|
|
303
|
+
single dependency edge here. The per-dimension ordinal (position) is
|
|
304
|
+
recoverable from the matching component table
|
|
305
|
+
(``dsd_component.position`` / ``msd_component.position``), so we
|
|
306
|
+
don't lose information by collapsing duplicates. ``ON CONFLICT DO
|
|
307
|
+
NOTHING`` keeps the first ordinal we saw — arbitrary, but ordinal on
|
|
308
|
+
this table is only a hint for callers that want positional iteration
|
|
309
|
+
through the ref graph without a join.
|
|
310
|
+
"""
|
|
300
311
|
if not refs:
|
|
301
312
|
return
|
|
302
313
|
for source_urn, target_urn, role, ordinal in refs:
|
|
@@ -304,6 +315,7 @@ def _put_refs(
|
|
|
304
315
|
"""
|
|
305
316
|
INSERT INTO artefact_ref (source_urn, target_urn, role, ordinal)
|
|
306
317
|
VALUES (?, ?, ?, ?)
|
|
318
|
+
ON CONFLICT (source_urn, target_urn, role) DO NOTHING
|
|
307
319
|
""",
|
|
308
320
|
[source_urn, target_urn, role, ordinal],
|
|
309
321
|
)
|
|
@@ -1390,17 +1402,18 @@ def put_dataflow_binding(
|
|
|
1390
1402
|
conn.execute(
|
|
1391
1403
|
"""
|
|
1392
1404
|
INSERT INTO dataflow_binding (
|
|
1393
|
-
urn, dataflow_urn, table_location, partition_layout,
|
|
1405
|
+
urn, dataflow_urn, table_location, database_role, partition_layout,
|
|
1394
1406
|
dimension_column_map, measure_column_map, attribute_column_map,
|
|
1395
1407
|
structural_version, binding_source_registry,
|
|
1396
1408
|
materialized_at, bound_at, series_attributes_location
|
|
1397
1409
|
)
|
|
1398
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1410
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
1399
1411
|
""",
|
|
1400
1412
|
[
|
|
1401
1413
|
urn,
|
|
1402
1414
|
str(binding.dataflow.urn),
|
|
1403
1415
|
binding.table_location,
|
|
1416
|
+
binding.database_role,
|
|
1404
1417
|
list(binding.partition_layout),
|
|
1405
1418
|
dict(binding.dimension_column_map),
|
|
1406
1419
|
dict(binding.measure_column_map),
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|