Skip to content

Commit 07198b0

Browse files
fix: Ensure SQL streams are sorted when a replication key is set (#1951)
* Add failing test * Treat as sorted if replication key is set
1 parent 10b61d2 commit 07198b0

File tree

5 files changed

+58
-10
lines changed

5 files changed

+58
-10
lines changed

samples/sample_tap_sqlite/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ class SQLiteStream(SQLStream):
3434

3535
connector_class = SQLiteConnector
3636

37+
# Use a smaller state message frequency to check intermediate state.
38+
STATE_MSG_FREQUENCY = 10
39+
3740

3841
class SQLiteTap(SQLTap):
3942
"""The Tap class for SQLite."""

singer_sdk/streams/sql.py

+12
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,17 @@ def get_records(self, context: dict | None) -> t.Iterable[dict[str, t.Any]]:
210210
continue
211211
yield transformed_record
212212

213+
@property
214+
def is_sorted(self) -> bool:
215+
"""Expect stream to be sorted.
216+
217+
When `True`, incremental streams will attempt to resume if unexpectedly
218+
interrupted.
219+
220+
Returns:
221+
`True` if stream is sorted. Defaults to `False`.
222+
"""
223+
return self.replication_key is not None
224+
213225

214226
__all__ = ["SQLStream", "SQLConnector"]

tests/samples/conftest.py

+13-7
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,7 @@ def _sqlite_sample_db(sqlite_connector):
3434

3535

3636
@pytest.fixture
37-
def sqlite_sample_tap(
38-
_sqlite_sample_db,
39-
sqlite_sample_db_config,
40-
sqlite_sample_db_state,
41-
) -> SQLiteTap:
42-
_ = _sqlite_sample_db
37+
def sqlite_sample_db_catalog(sqlite_sample_db_config) -> Catalog:
4338
catalog_obj = Catalog.from_dict(
4439
_get_tap_catalog(SQLiteTap, config=sqlite_sample_db_config, select_all=True),
4540
)
@@ -55,9 +50,20 @@ def sqlite_sample_tap(
5550
t2.key_properties = ["c1"]
5651
t2.replication_key = "c1"
5752
t2.replication_method = "INCREMENTAL"
53+
return catalog_obj
54+
55+
56+
@pytest.fixture
57+
def sqlite_sample_tap(
58+
_sqlite_sample_db,
59+
sqlite_sample_db_config,
60+
sqlite_sample_db_state,
61+
sqlite_sample_db_catalog,
62+
) -> SQLiteTap:
63+
_ = _sqlite_sample_db
5864
return SQLiteTap(
5965
config=sqlite_sample_db_config,
60-
catalog=catalog_obj.to_dict(),
66+
catalog=sqlite_sample_db_catalog.to_dict(),
6167
state=sqlite_sample_db_state,
6268
)
6369

tests/samples/test_tap_sqlite.py

+24
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33
import json
44
import typing as t
55

6+
import pytest
67
from click.testing import CliRunner
8+
from freezegun import freeze_time
79

810
from samples.sample_tap_sqlite import SQLiteTap
911
from samples.sample_target_csv.csv_target import SampleTargetCSV
1012
from singer_sdk import SQLStream
1113
from singer_sdk._singerlib import MetadataMapping, StreamMetadata
1214
from singer_sdk.testing import (
1315
get_standard_tap_tests,
16+
tap_sync_test,
1417
tap_to_target_sync_test,
1518
)
1619

@@ -116,3 +119,24 @@ def test_sync_sqlite_to_csv(sqlite_sample_tap: SQLTap, tmp_path: Path):
116119
sqlite_sample_tap,
117120
SampleTargetCSV(config={"target_folder": f"{tmp_path}/"}),
118121
)
122+
123+
124+
@pytest.fixture
125+
@freeze_time("2022-01-01T00:00:00Z")
126+
def sqlite_sample_tap_state_messages(sqlite_sample_tap: SQLTap) -> list[dict]:
127+
stdout, _ = tap_sync_test(sqlite_sample_tap)
128+
state_messages = []
129+
for line in stdout.readlines():
130+
message = json.loads(line)
131+
if message["type"] == "STATE":
132+
state_messages.append(message)
133+
134+
return state_messages
135+
136+
137+
def test_sqlite_state(sqlite_sample_tap_state_messages):
138+
assert all(
139+
"progress_markers" not in bookmark
140+
for message in sqlite_sample_tap_state_messages
141+
for bookmark in message["value"]["bookmarks"].values()
142+
)

tests/samples/test_target_sqlite.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@
1919
from samples.sample_target_sqlite import SQLiteSink, SQLiteTarget
2020
from singer_sdk import typing as th
2121
from singer_sdk.testing import (
22-
_get_tap_catalog,
2322
tap_sync_test,
2423
tap_to_target_sync_test,
2524
target_sync_test,
2625
)
2726

2827
if t.TYPE_CHECKING:
28+
from singer_sdk._singerlib import Catalog
2929
from singer_sdk.tap_base import SQLTap
3030
from singer_sdk.target_base import SQLTarget
3131

@@ -67,6 +67,7 @@ def sqlite_sample_target_batch(sqlite_target_test_config):
6767
def test_sync_sqlite_to_sqlite(
6868
sqlite_sample_tap: SQLTap,
6969
sqlite_sample_target: SQLTarget,
70+
sqlite_sample_db_catalog: Catalog,
7071
):
7172
"""End-to-end-to-end test for SQLite tap and target.
7273
@@ -84,8 +85,10 @@ def test_sync_sqlite_to_sqlite(
8485
)
8586
orig_stdout.seek(0)
8687
tapped_config = dict(sqlite_sample_target.config)
87-
catalog = _get_tap_catalog(SQLiteTap, config=tapped_config, select_all=True)
88-
tapped_target = SQLiteTap(config=tapped_config, catalog=catalog)
88+
tapped_target = SQLiteTap(
89+
config=tapped_config,
90+
catalog=sqlite_sample_db_catalog.to_dict(),
91+
)
8992
new_stdout, _ = tap_sync_test(tapped_target)
9093

9194
orig_stdout.seek(0)

0 commit comments

Comments
 (0)