Skip to content

Commit c895fcf

Browse files
authored
Merge pull request #2941 from mabel-dev/#2940
Iceberg tables with no snapshots give unhelpful error
2 parents de62d7e + f60c826 commit c895fcf

6 files changed

Lines changed: 460 additions & 11 deletions

File tree

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__ = 1893
4+
__build__ = 1894
55
__author__ = "@joocer"
6-
__version__ = "0.26.2-beta.1893"
6+
__version__ = "0.26.2-beta.1894"
77

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

opteryx/connectors/iceberg_connector.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,11 @@ def __init__(self, *args, catalog=None, **kwargs):
154154
try:
155155
self.table = catalog.load_table(self.dataset)
156156
self.snapshot = self.table.current_snapshot()
157-
self.snapshot_id = self.snapshot.snapshot_id
157+
# If the table exists but has no snapshots, we don't raise here —
158+
# we allow non-time-travel reads to return an empty result set with
159+
# a valid schema. For time-travel (start_date specified), we will
160+
# still raise DatasetReadError in get_dataset_schema.
161+
self.snapshot_id = None if self.snapshot is None else self.snapshot.snapshot_id
158162
except pyiceberg.exceptions.NoSuchTableError:
159163
raise DatasetNotFoundError(dataset=self.dataset, connector=self.__type__) from None
160164

@@ -173,11 +177,19 @@ def get_dataset_schema(self) -> RelationSchema:
173177
if not snapshot_rows:
174178
raise DatasetReadError("No data available for the specified date.")
175179

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+
# Honor dates before the first snapshot by rejecting them, but treat
181+
# dates after the latest snapshot as selecting the latest snapshot
182+
first_committed = snapshot_rows[0]["committed_at"]
183+
last_committed = snapshot_rows[-1]["committed_at"]
184+
185+
if self.start_date < first_committed:
186+
# Point-in-time read is before our first snapshot — no data available then
180187
raise DatasetReadError("No data available for the specified date.")
188+
elif self.start_date > last_committed:
189+
# Point-in-time read after the latest snapshot — return current data
190+
selected = snapshot_rows[-1]
191+
# ensure we store the commit time for statistics/context
192+
self.statistics.dataset_committed_at = selected["committed_at"].isoformat()
181193
else:
182194
selected = snapshot_rows[0]
183195
for candidate in snapshot_rows:
@@ -190,7 +202,12 @@ def get_dataset_schema(self) -> RelationSchema:
190202
self.snapshot_id = selected["snapshot_id"]
191203
self.snapshot = self.table.snapshot_by_id(self.snapshot_id)
192204

193-
iceberg_schema = self.table.schemas()[self.snapshot.schema_id]
205+
# If the table has no snapshot and the read is not time-travel, use
206+
# the table's declared schema (from metadata) and return an empty result set.
207+
if self.snapshot is None:
208+
iceberg_schema = self.table.schema()
209+
else:
210+
iceberg_schema = self.table.schemas()[self.snapshot.schema_id]
194211
arrow_schema = iceberg_schema.as_arrow()
195212

196213
self.schema = RelationSchema(
@@ -266,6 +283,23 @@ def read_dataset(
266283
snapshot_id=self.snapshot_id,
267284
).to_arrow_batch_reader()
268285

286+
# If there are no snapshots (snapshot_id is None), return an empty morsel
287+
if self.snapshot_id is None:
288+
from orso.schema import RelationSchema
289+
from orso.schema import convert_orso_schema_to_arrow_schema
290+
291+
orso_schema = RelationSchema(
292+
name="Relation", columns=[c.schema_column for c in columns]
293+
)
294+
arrow_shema = convert_orso_schema_to_arrow_schema(orso_schema, use_identities=True)
295+
296+
morsel = pyarrow.Table.from_arrays(
297+
[pyarrow.array([]) for _ in columns],
298+
schema=arrow_shema,
299+
)
300+
yield morsel
301+
return
302+
269303
batch = None
270304
for batch in reader:
271305
# Check for decimal columns in the batch schema

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "opteryx"
3-
version = "0.26.2-beta.1893"
3+
version = "0.26.2-beta.1894"
44
description = "Query your data, where it lives"
55
requires-python = '>=3.11'
66
readme = {file = "README.md", content-type = "text/markdown"}

tests/__init__.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,48 @@ def cast_dataset(dataset):
747747
needs_setup = True
748748

749749
if not needs_setup:
750+
# Ensure a pair of convenience test tables exist even if catalog is already
751+
# initialized by a prior run. These are used in battery tests.
752+
from pyiceberg.exceptions import NoSuchTableError as _NoSuchTableError
753+
import pyarrow as _pa
754+
from freezegun import freeze_time as _freeze_time
755+
756+
def _ensure_table_empty(identifier):
757+
try:
758+
catalog.load_table(identifier)
759+
except _NoSuchTableError:
760+
schema = _pa.schema([_pa.field("id", _pa.int64()), _pa.field("name", _pa.string())])
761+
catalog.create_table(identifier, schema=schema)
762+
763+
def _ensure_single_snapshot(identifier):
764+
try:
765+
catalog.load_table(identifier)
766+
except _NoSuchTableError:
767+
schema = _pa.schema([_pa.field("id", _pa.int64()), _pa.field("name", _pa.string())])
768+
table = catalog.create_table(identifier, schema=schema)
769+
data = _pa.Table.from_arrays([_pa.array([1, 2, 3]), _pa.array(["a", "b", "c"])], schema=schema)
770+
commit_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
771+
with _freeze_time(commit_time):
772+
table.append(data)
773+
774+
def _ensure_two_snapshots(identifier):
775+
try:
776+
catalog.load_table(identifier)
777+
except _NoSuchTableError:
778+
schema = _pa.schema([_pa.field("id", _pa.int64()), _pa.field("name", _pa.string())])
779+
table = catalog.create_table(identifier, schema=schema)
780+
data1 = _pa.Table.from_arrays([_pa.array([1, 2]), _pa.array(["a", "b"])], schema=schema)
781+
commit_time1 = datetime.datetime(2021, 1, 1, 12, 0, 0)
782+
with _freeze_time(commit_time1):
783+
table.append(data1)
784+
data2 = _pa.Table.from_arrays([_pa.array([1, 2, 3]), _pa.array(["a", "b", "c"])], schema=schema)
785+
commit_time2 = datetime.datetime(2022, 1, 1, 12, 0, 0)
786+
with _freeze_time(commit_time2):
787+
table.overwrite(data2)
788+
789+
_ensure_table_empty("opteryx.empty_battery")
790+
_ensure_single_snapshot("opteryx.single_snap_battery")
791+
_ensure_two_snapshots("opteryx.two_snap_battery")
750792
return catalog
751793

752794
with contextlib.suppress(NamespaceAlreadyExistsError):
@@ -830,4 +872,46 @@ def load_planet_snapshot(cutoff):
830872
del iceberged # Free memory immediately
831873

832874

875+
# Create additional tables that tests rely on (empty and single snapshot)
876+
from pyiceberg.exceptions import NoSuchTableError as _NoSuchTableError
877+
import pyarrow as _pa
878+
from freezegun import freeze_time as _freeze_time
879+
880+
def _ensure_table_empty(identifier):
881+
try:
882+
catalog.load_table(identifier)
883+
except _NoSuchTableError:
884+
schema = _pa.schema([_pa.field("id", _pa.int64()), _pa.field("name", _pa.string())])
885+
catalog.create_table(identifier, schema=schema)
886+
887+
def _ensure_single_snapshot(identifier):
888+
try:
889+
catalog.load_table(identifier)
890+
except _NoSuchTableError:
891+
schema = _pa.schema([_pa.field("id", _pa.int64()), _pa.field("name", _pa.string())])
892+
table = catalog.create_table(identifier, schema=schema)
893+
data = _pa.Table.from_arrays([_pa.array([1, 2, 3]), _pa.array(["a", "b", "c"])], schema=schema)
894+
commit_time = datetime.datetime(2023, 1, 1, 12, 0, 0)
895+
with _freeze_time(commit_time):
896+
table.append(data)
897+
898+
def _ensure_two_snapshots(identifier):
899+
try:
900+
catalog.load_table(identifier)
901+
except _NoSuchTableError:
902+
schema = _pa.schema([_pa.field("id", _pa.int64()), _pa.field("name", _pa.string())])
903+
table = catalog.create_table(identifier, schema=schema)
904+
data1 = _pa.Table.from_arrays([_pa.array([1, 2]), _pa.array(["a", "b"])], schema=schema)
905+
commit_time1 = datetime.datetime(2021, 1, 1, 12, 0, 0)
906+
with _freeze_time(commit_time1):
907+
table.append(data1)
908+
data2 = _pa.Table.from_arrays([_pa.array([1, 2, 3]), _pa.array(["a", "b", "c"])], schema=schema)
909+
commit_time2 = datetime.datetime(2022, 1, 1, 12, 0, 0)
910+
with _freeze_time(commit_time2):
911+
table.overwrite(data2)
912+
913+
_ensure_table_empty("opteryx.empty_battery")
914+
_ensure_single_snapshot("opteryx.single_snap_battery")
915+
_ensure_two_snapshots("opteryx.two_snap_battery")
916+
833917
return catalog

0 commit comments

Comments
 (0)