Skip to content

Commit d692bee

Browse files
kurodo3[bot]claude
authored andcommitted
feat(sources): implement SpiralDBTableSource with PK as default tag columns
Add SpiralDBTableSource — a read-only RootSource backed by a SpiralDB table. PK (key-schema) columns are used as tag columns by default; explicit tag_columns override this at construction time. Tables with no key schema and no explicit tag_columns raise ValueError. The class follows the same pattern as SQLiteTableSource: it opens a SpiralDBConnector, delegates to DBTableSource for fetching and stream building, then closes the connector immediately (eager load). Config round-trip (to_config / from_config) and serialization registration under the "spiraldb_table" key are included. 50 unit and integration tests added, all passing. Closes PLT-1073 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7f9cd8a commit d692bee

4 files changed

Lines changed: 896 additions & 0 deletions

File tree

src/orcapod/core/sources/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from .list_source import ListSource
1111
from .source_registry import GLOBAL_SOURCE_REGISTRY, SourceRegistry
1212
from .source_proxy import SourceProxy
13+
from .spiraldb_table_source import SpiralDBTableSource
1314
from .sqlite_table_source import SQLiteTableSource
1415

1516
__all__ = [
@@ -25,6 +26,7 @@
2526
"ListSource",
2627
"SourceRegistry",
2728
"SourceProxy",
29+
"SpiralDBTableSource",
2830
"SQLiteTableSource",
2931
"GLOBAL_SOURCE_REGISTRY",
3032
]
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""SpiralDBTableSource — a read-only RootSource backed by a SpiralDB table.
2+
3+
Wraps a SpiralDB table as an OrcaPod Source. The table's primary-key
4+
(key-schema) columns are used as tag columns by default.
5+
6+
Requires the ``spiraldb`` optional extra: ``pip install orcapod[spiraldb]``.
7+
Authentication is handled externally — run ``spiral login`` once to store
8+
credentials in ``~/.config/pyspiral/auth.json``.
9+
10+
Example::
11+
12+
# Default dataset, PK columns become tag columns automatically
13+
source = SpiralDBTableSource("my-project-123456", "spike_data")
14+
15+
# Explicit dataset
16+
source = SpiralDBTableSource(
17+
"my-project-123456", "spike_data", dataset="prod"
18+
)
19+
20+
# Override tag columns
21+
source = SpiralDBTableSource(
22+
"my-project-123456", "spike_data", tag_columns=["session_id"]
23+
)
24+
25+
Note:
26+
SpiralDB enforces non-null values in key-schema columns at storage time,
27+
so NULL values in PK (tag) columns are not expected in practice. If a
28+
table is written with NULL key columns via an external tool, those NULLs
29+
will be propagated into the tag columns as-is.
30+
31+
Tables with no key schema and no explicit ``tag_columns`` will raise
32+
``ValueError`` at construction time. Either define a key schema on the
33+
SpiralDB table or supply explicit ``tag_columns``.
34+
"""
35+
from __future__ import annotations
36+
37+
from collections.abc import Collection
38+
from typing import TYPE_CHECKING, Any
39+
40+
from orcapod.core.sources.db_table_source import DBTableSource
41+
from orcapod.databases.spiraldb_connector import SpiralDBConnector
42+
43+
if TYPE_CHECKING:
44+
from orcapod import contexts
45+
from orcapod.config import Config
46+
47+
48+
class SpiralDBTableSource(DBTableSource):
49+
"""A read-only Source backed by a table in a SpiralDB dataset.
50+
51+
At construction time the source:
52+
53+
1. Opens a ``SpiralDBConnector`` for *project_id* and *dataset*.
54+
2. Validates the table exists.
55+
3. Resolves tag columns:
56+
57+
- If *tag_columns* is provided, uses them as-is.
58+
- Otherwise uses the table's primary-key (key-schema) columns.
59+
- Raises ``ValueError`` if the table has no key schema and no
60+
explicit *tag_columns* are given.
61+
62+
4. Delegates to ``DBTableSource.__init__`` for fetching and stream
63+
building (source-info provenance, schema hash, system tags).
64+
5. Closes the connector — all data is eagerly loaded into memory, so
65+
the connection is released immediately.
66+
67+
Args:
68+
project_id: SpiralDB project identifier (e.g. ``"my-project-123456"``).
69+
table_name: Name of the SpiralDB table to expose as a source.
70+
dataset: Dataset within the project. Defaults to ``"default"``.
71+
tag_columns: Columns to use as tag columns. If ``None`` (default),
72+
the table's primary-key (key-schema) columns are used. Raises
73+
``ValueError`` if the table has no key schema.
74+
system_tag_columns: Additional system-level tag columns.
75+
record_id_column: Column for stable per-row record IDs in provenance.
76+
source_id: Canonical source name for the registry and provenance
77+
tokens. Defaults to *table_name*.
78+
label: Human-readable label for this source node.
79+
data_context: Data context governing type conversion and hashing.
80+
config: OrcaPod configuration.
81+
overrides: Optional pyspiral client config overrides passed through
82+
to ``SpiralDBConnector``, e.g.
83+
``{"server.url": "http://api.spiraldb.dev"}``.
84+
85+
Raises:
86+
ValueError: If the table is not found, has no primary-key columns
87+
and no explicit *tag_columns* are given, or the table is empty.
88+
"""
89+
90+
def __init__(
91+
self,
92+
project_id: str,
93+
table_name: str,
94+
dataset: str = "default",
95+
tag_columns: Collection[str] | None = None,
96+
system_tag_columns: Collection[str] = (),
97+
record_id_column: str | None = None,
98+
source_id: str | None = None,
99+
label: str | None = None,
100+
data_context: "str | contexts.DataContext | None" = None,
101+
config: "Config | None" = None,
102+
overrides: dict[str, str] | None = None,
103+
) -> None:
104+
self._project_id = project_id
105+
self._dataset = dataset
106+
self._overrides = overrides
107+
108+
connector = SpiralDBConnector(
109+
project_id=project_id,
110+
dataset=dataset,
111+
overrides=overrides,
112+
)
113+
114+
try:
115+
resolved_tags: list[str] | None = (
116+
list(tag_columns) if tag_columns is not None else None
117+
)
118+
119+
# DBTableSource handles PK resolution and raises ValueError when
120+
# the table has no key schema and no explicit tag_columns are given.
121+
super().__init__(
122+
connector,
123+
table_name,
124+
tag_columns=resolved_tags,
125+
system_tag_columns=system_tag_columns,
126+
record_id_column=record_id_column,
127+
source_id=source_id,
128+
label=label,
129+
data_context=data_context,
130+
config=config,
131+
)
132+
finally:
133+
try:
134+
connector.close()
135+
except Exception:
136+
# Suppress connector close errors to avoid masking __init__
137+
# failures.
138+
pass
139+
140+
def to_config(self) -> dict[str, Any]:
141+
"""Serialize source configuration to a JSON-compatible dict."""
142+
base = super().to_config()
143+
base.pop("connector", None)
144+
return {
145+
**base,
146+
"source_type": "spiraldb_table",
147+
"project_id": self._project_id,
148+
"dataset": self._dataset,
149+
"overrides": self._overrides,
150+
}
151+
152+
@classmethod
153+
def from_config(cls, config: dict[str, Any]) -> "SpiralDBTableSource":
154+
"""Reconstruct a SpiralDBTableSource from a config dict.
155+
156+
Args:
157+
config: Dict as produced by ``to_config()``.
158+
159+
Returns:
160+
A new ``SpiralDBTableSource`` instance.
161+
"""
162+
return cls(
163+
project_id=config["project_id"],
164+
table_name=config["table_name"],
165+
dataset=config.get("dataset", "default"),
166+
tag_columns=config.get("tag_columns"),
167+
system_tag_columns=config.get("system_tag_columns", ()),
168+
record_id_column=config.get("record_id_column"),
169+
source_id=config.get("source_id"),
170+
label=config.get("label"),
171+
data_context=config.get("data_context"),
172+
overrides=config.get("overrides"),
173+
)

src/orcapod/pipeline/serialization.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ def _build_source_registry() -> dict[str, type]:
6464
from orcapod.core.sources.delta_table_source import DeltaTableSource
6565
from orcapod.core.sources.dict_source import DictSource
6666
from orcapod.core.sources.list_source import ListSource
67+
from orcapod.core.sources.spiraldb_table_source import SpiralDBTableSource
6768
from orcapod.core.sources.sqlite_table_source import SQLiteTableSource
6869

6970
return {
@@ -74,6 +75,7 @@ def _build_source_registry() -> dict[str, type]:
7475
"data_frame": DataFrameSource,
7576
"arrow_table": ArrowTableSource,
7677
"cached": CachedSource,
78+
"spiraldb_table": SpiralDBTableSource,
7779
"sqlite_table": SQLiteTableSource,
7880
}
7981

0 commit comments

Comments
 (0)