Skip to content

Commit de62d7e

Browse files
authored
Merge pull request #2939 from mabel-dev/#2927
Time travel Iceberg data
2 parents e353fa6 + 93b1b33 commit de62d7e

15 files changed

Lines changed: 570 additions & 43 deletions

opteryx/__version__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
# THIS FILE IS AUTOMATICALLY UPDATED DURING THE BUILD PROCESS
22
# DO NOT EDIT THIS FILE DIRECTLY
33

4-
__build__ = 1889
4+
__build__ = 1893
55
__author__ = "@joocer"
6-
__version__ = "0.26.2-beta.1889"
6+
__version__ = "0.26.2-beta.1893"
77

88
# Store the version here so:
99
# 1) we don't load dependencies by storing it in __init__.py

opteryx/connectors/aws_s3_connector.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from opteryx.connectors.base.base_connector import BaseConnector
2121
from opteryx.connectors.capabilities import Asynchronous
2222
from opteryx.connectors.capabilities import Cacheable
23-
from opteryx.connectors.capabilities import Partitionable
23+
from opteryx.connectors.capabilities import Diachronic
2424
from opteryx.connectors.capabilities import PredicatePushable
2525
from opteryx.connectors.capabilities import Statistics
2626
from opteryx.exceptions import DataError
@@ -36,7 +36,7 @@
3636

3737

3838
class AwsS3Connector(
39-
BaseConnector, Cacheable, Partitionable, PredicatePushable, Asynchronous, Statistics
39+
BaseConnector, Cacheable, Diachronic, PredicatePushable, Asynchronous, Statistics
4040
):
4141
__mode__ = "Blob"
4242
__type__ = "S3"
@@ -67,7 +67,7 @@ def __init__(self, credentials=None, **kwargs):
6767
raise MissingDependencyError(err.name) from err
6868

6969
BaseConnector.__init__(self, **kwargs)
70-
Partitionable.__init__(self, **kwargs)
70+
Diachronic.__init__(self, **kwargs)
7171
Cacheable.__init__(self, **kwargs)
7272
PredicatePushable.__init__(self, **kwargs)
7373
Asynchronous.__init__(self, **kwargs)

opteryx/connectors/capabilities/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@
55

66
from opteryx.connectors.capabilities.asynchronous import Asynchronous
77
from opteryx.connectors.capabilities.cacheable import Cacheable
8+
from opteryx.connectors.capabilities.diachronic import Diachronic
89
from opteryx.connectors.capabilities.limit_pushable import LimitPushable
9-
from opteryx.connectors.capabilities.partitionable import Partitionable
1010
from opteryx.connectors.capabilities.predicate_pushable import PredicatePushable
1111
from opteryx.connectors.capabilities.statistics import Statistics
1212

1313
__all__ = (
1414
"Asynchronous",
1515
"Cacheable",
1616
"LimitPushable",
17-
"Partitionable",
17+
"Diachronic",
1818
"PredicatePushable",
1919
"Statistics",
2020
)

opteryx/connectors/capabilities/partitionable.py renamed to opteryx/connectors/capabilities/diachronic.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,14 @@
77
from opteryx.exceptions import InvalidConfigurationError
88

99

10-
class Partitionable:
10+
class Diachronic:
11+
"""Capability for connectors that support diachronic (time-travel) reads.
12+
13+
Historically this capability was named `Partitionable`; it stores a partition
14+
scheme and optional start/end date attributes used by connectors to support
15+
date-range reads and time-travel queries.
16+
"""
17+
1118
partitioned = True
1219

1320
def __init__(self, **kwargs):

opteryx/connectors/disk_connector.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
from orso.types import OrsoTypes
2424

2525
from opteryx.connectors.base.base_connector import BaseConnector
26+
from opteryx.connectors.capabilities import Diachronic
2627
from opteryx.connectors.capabilities import LimitPushable
27-
from opteryx.connectors.capabilities import Partitionable
2828
from opteryx.connectors.capabilities import PredicatePushable
2929
from opteryx.connectors.capabilities import Statistics
3030
from opteryx.exceptions import DataError
@@ -37,7 +37,7 @@
3737
OS_SEP = os.sep
3838

3939

40-
class DiskConnector(BaseConnector, Partitionable, PredicatePushable, LimitPushable, Statistics):
40+
class DiskConnector(BaseConnector, Diachronic, PredicatePushable, LimitPushable, Statistics):
4141
"""
4242
Connector for reading datasets from files on local storage.
4343
"""
@@ -75,7 +75,7 @@ def __init__(self, **kwargs):
7575
Arbitrary keyword arguments.
7676
"""
7777
BaseConnector.__init__(self, **kwargs)
78-
Partitionable.__init__(self, **kwargs)
78+
Diachronic.__init__(self, **kwargs)
7979
PredicatePushable.__init__(self, **kwargs)
8080
LimitPushable.__init__(self, **kwargs)
8181
Statistics.__init__(self, **kwargs)

opteryx/connectors/gcp_cloudstorage_connector.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from opteryx.connectors.base.base_connector import BaseConnector
1717
from opteryx.connectors.capabilities import Asynchronous
1818
from opteryx.connectors.capabilities import Cacheable
19-
from opteryx.connectors.capabilities import Partitionable
19+
from opteryx.connectors.capabilities import Diachronic
2020
from opteryx.connectors.capabilities import PredicatePushable
2121
from opteryx.connectors.capabilities import Statistics
2222
from opteryx.exceptions import DatasetNotFoundError
@@ -55,7 +55,7 @@ def get_storage_credentials():
5555

5656

5757
class GcpCloudStorageConnector(
58-
BaseConnector, Cacheable, Partitionable, PredicatePushable, Asynchronous, Statistics
58+
BaseConnector, Cacheable, Diachronic, PredicatePushable, Asynchronous, Statistics
5959
):
6060
__mode__ = "Blob"
6161
__type__ = "GCS"
@@ -89,7 +89,7 @@ def __init__(self, credentials=None, **kwargs):
8989
raise MissingDependencyError(name) from err
9090

9191
BaseConnector.__init__(self, **kwargs)
92-
Partitionable.__init__(self, **kwargs)
92+
Diachronic.__init__(self, **kwargs)
9393
Cacheable.__init__(self, **kwargs)
9494
PredicatePushable.__init__(self, **kwargs)
9595
Asynchronous.__init__(self, **kwargs)

opteryx/connectors/iceberg_connector.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,15 @@
2020
from orso.tools import single_item_cache
2121
from orso.types import OrsoTypes
2222

23-
from opteryx.connectors import DiskConnector
2423
from opteryx.connectors.base.base_connector import BaseConnector
24+
from opteryx.connectors.capabilities import Diachronic
2525
from opteryx.connectors.capabilities import LimitPushable
2626
from opteryx.connectors.capabilities import PredicatePushable
2727
from opteryx.connectors.capabilities import Statistics
2828
from opteryx.exceptions import DatasetNotFoundError
29+
from opteryx.exceptions import DatasetReadError
2930
from opteryx.exceptions import NotSupportedError
31+
from opteryx.exceptions import UnsupportedSyntaxError
3032
from opteryx.managers.expression import NodeType
3133
from opteryx.managers.expression import get_all_nodes_of_type
3234
from opteryx.models import RelationStatistics
@@ -117,7 +119,7 @@ def _predicate_to_iceberg_filter(root):
117119
return iceberg_filter if iceberg_filter else "True", unsupported
118120

119121

120-
class IcebergConnector(BaseConnector, LimitPushable, Statistics, PredicatePushable):
122+
class IcebergConnector(BaseConnector, Diachronic, LimitPushable, Statistics, PredicatePushable):
121123
__mode__ = "Blob"
122124
__type__ = "ICEBERG"
123125

@@ -140,22 +142,55 @@ class IcebergConnector(BaseConnector, LimitPushable, Statistics, PredicatePushab
140142
OrsoTypes.DATE,
141143
}
142144

143-
def __init__(self, *args, catalog=None, io=DiskConnector, **kwargs):
145+
def __init__(self, *args, catalog=None, **kwargs):
144146
BaseConnector.__init__(self, **kwargs)
145147
LimitPushable.__init__(self, **kwargs)
148+
Diachronic.__init__(self, **kwargs)
146149
Statistics.__init__(self, **kwargs)
147150
PredicatePushable.__init__(self, **kwargs)
148151

149152
import pyiceberg
150153

151154
try:
152155
self.table = catalog.load_table(self.dataset)
153-
self.io_connector = io(**kwargs)
156+
self.snapshot = self.table.current_snapshot()
157+
self.snapshot_id = self.snapshot.snapshot_id
154158
except pyiceberg.exceptions.NoSuchTableError:
155159
raise DatasetNotFoundError(dataset=self.dataset, connector=self.__type__) from None
156160

157161
def get_dataset_schema(self) -> RelationSchema:
158-
iceberg_schema = self.table.schema()
162+
if self.start_date != self.end_date:
163+
if self.start_date.date() != self.end_date.date():
164+
raise UnsupportedSyntaxError("This table only supports point in time reads.")
165+
raise UnsupportedSyntaxError(
166+
"This table only supports point in time reads. Are you missing the time component from your FOR clause?"
167+
)
168+
169+
if self.start_date is not None:
170+
snapshots = self.table.inspect.snapshots().sort_by("committed_at")
171+
snapshot_rows = snapshots.to_pylist()
172+
173+
if not snapshot_rows:
174+
raise DatasetReadError("No data available for the specified date.")
175+
176+
# Honor dates before the first snapshot, reject dates beyond the newest snapshot
177+
if self.start_date < snapshot_rows[0]["committed_at"]:
178+
selected = snapshot_rows[0]
179+
elif self.start_date > snapshot_rows[-1]["committed_at"]:
180+
raise DatasetReadError("No data available for the specified date.")
181+
else:
182+
selected = snapshot_rows[0]
183+
for candidate in snapshot_rows:
184+
if candidate["committed_at"] <= self.start_date:
185+
self.statistics.dataset_committed_at = candidate["committed_at"].isoformat()
186+
selected = candidate
187+
else:
188+
break
189+
190+
self.snapshot_id = selected["snapshot_id"]
191+
self.snapshot = self.table.snapshot_by_id(self.snapshot_id)
192+
193+
iceberg_schema = self.table.schemas()[self.snapshot.schema_id]
159194
arrow_schema = iceberg_schema.as_arrow()
160195

161196
self.schema = RelationSchema(
@@ -169,7 +204,7 @@ def get_dataset_schema(self) -> RelationSchema:
169204
column_names = {col.field_id: col.name for col in iceberg_schema.columns}
170205
column_types = {col.field_id: col.field_type for col in iceberg_schema.columns}
171206

172-
files = self.table.inspect.files()
207+
files = self.table.inspect.files(snapshot_id=self.snapshot_id)
173208

174209
# No files = empty table, no stats
175210
if len(files.column("file_path")) == 0:
@@ -225,7 +260,10 @@ def read_dataset(
225260
)
226261

227262
reader = self.table.scan(
228-
row_filter=pushed_filters, selected_fields=selected_columns, limit=limit
263+
row_filter=pushed_filters,
264+
selected_fields=selected_columns,
265+
limit=limit,
266+
snapshot_id=self.snapshot_id,
229267
).to_arrow_batch_reader()
230268

231269
batch = None

opteryx/connectors/virtual_data.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from opteryx.connectors.base.base_connector import BaseConnector
2222
from opteryx.connectors.base.base_connector import DatasetReader
23-
from opteryx.connectors.capabilities import Partitionable
23+
from opteryx.connectors.capabilities import Diachronic
2424
from opteryx.connectors.capabilities import Statistics
2525
from opteryx.exceptions import DatasetNotFoundError
2626
from opteryx.utils import arrow
@@ -66,13 +66,13 @@ def suggest(dataset):
6666
)
6767

6868

69-
class SampleDataConnector(BaseConnector, Partitionable, Statistics):
69+
class SampleDataConnector(BaseConnector, Diachronic, Statistics):
7070
__mode__ = "Internal"
7171
__type__ = "SAMPLE"
7272

7373
def __init__(self, *args, **kwargs):
7474
BaseConnector.__init__(self, **kwargs)
75-
Partitionable.__init__(self, **kwargs)
75+
Diachronic.__init__(self, **kwargs)
7676
Statistics.__init__(self, **kwargs)
7777
self.dataset = self.dataset.lower()
7878
self.variables = None

opteryx/planner/binder/binder_visitor.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -947,7 +947,7 @@ def visit_scan(self, node: Node, context: BindingContext) -> Tuple[Node, Binding
947947
from opteryx.connectors import connector_factory
948948
from opteryx.connectors.capabilities import Asynchronous
949949
from opteryx.connectors.capabilities import Cacheable
950-
from opteryx.connectors.capabilities import Partitionable
950+
from opteryx.connectors.capabilities import Diachronic
951951
from opteryx.connectors.capabilities import Statistics
952952
from opteryx.connectors.capabilities.cacheable import async_read_thru_cache
953953
from opteryx.connectors.capabilities.cacheable import read_thru_cache
@@ -969,7 +969,7 @@ def visit_scan(self, node: Node, context: BindingContext) -> Tuple[Node, Binding
969969

970970
if hasattr(node.connector, "variables"):
971971
node.connector.variables = context.connection.variables
972-
if Partitionable in connector_capabilities:
972+
if Diachronic in connector_capabilities:
973973
node.connector.start_date = node.start_date
974974
node.connector.end_date = node.end_date
975975
if Cacheable in connector_capabilities and "NO_CACHE" not in (node.hints or []):
@@ -1031,7 +1031,7 @@ def visit_subquery(self, node: Node, context: BindingContext) -> Tuple[Node, Bin
10311031
node, context = self.visit_exit(node, context)
10321032

10331033
# Extract the column names to check for duplicates
1034-
column_names = (n.schema_column.name for n in node.columns)
1034+
column_names = (n.current_name for n in node.columns)
10351035
seen = set()
10361036
duplicates = [name for name in column_names if name in seen or seen.add(name)] # type: ignore
10371037

opteryx/virtual_datasets/planet_data.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def read(end_date=None, *args) -> pyarrow.Table:
7777
# April 26, 1781 - Uranus discovered by Sir William Herschel
7878
return full_set.take([0, 1, 2, 3, 4, 5])
7979
if end_date < datetime.datetime(1846, 11, 13):
80-
# November 13, 1846 - Neptune
81-
return full_set.take([0, 1, 2, 3, 4, 5, 7])
80+
# November 13, 1846 - Neptune discovered, so only planets through Uranus exist
81+
return full_set.take([0, 1, 2, 3, 4, 5, 6])
8282
if end_date < datetime.datetime(1930, 3, 13):
8383
# March 13, 1930 - Pluto discovered by Clyde William Tombaugh
8484
return full_set.take([0, 1, 2, 3, 4, 5, 6, 7])

0 commit comments

Comments
 (0)