From 086be741708f79cfbc6bcbfab7ffdb24806bbfc3 Mon Sep 17 00:00:00 2001 From: Doug Guthrie Date: Wed, 4 Feb 2026 08:20:09 -0700 Subject: [PATCH 1/2] no more sql :sad --- braintrust_migrate/btql.py | 15 ++- braintrust_migrate/resources/datasets.py | 4 +- braintrust_migrate/resources/experiments.py | 6 +- braintrust_migrate/resources/logs.py | 10 +- braintrust_migrate/streaming_utils.py | 21 ++-- tests/unit/test_btql_query_builder.py | 112 +++++++++--------- tests/unit/test_dataset_streaming_resume.py | 5 +- .../unit/test_experiment_streaming_resume.py | 5 +- tests/unit/test_logs_btql_sorted_fetch.py | 57 +++++---- tests/unit/test_logs_streaming_resume.py | 8 +- 10 files changed, 126 insertions(+), 117 deletions(-) diff --git a/braintrust_migrate/btql.py b/braintrust_migrate/btql.py index bcde4ad..ebaa006 100644 --- a/braintrust_migrate/btql.py +++ b/braintrust_migrate/btql.py @@ -1,8 +1,11 @@ """BTQL helpers (query execution + resilient paging). -Streaming migrators use BTQL (SQL) queries sorted by `_pagination_key` and need +Streaming migrators use native BTQL queries sorted by `_pagination_key` and need to be resilient to backend timeouts (504) and internal errors (500) that correlate with large LIMIT values. + +Native BTQL syntax is used instead of SQL for compatibility with data planes +that don't yet support SQL mode. """ from __future__ import annotations @@ -38,11 +41,11 @@ async def find_first_pagination_key_for_created_after( def _query_text_for_limit(n: int) -> str: return ( - "SELECT _pagination_key\n" - f"FROM {from_expr}\n" - f"WHERE created >= '{btql_quote(created_after)}'\n" - "ORDER BY _pagination_key ASC\n" - f"LIMIT {int(n)}" + "select: _pagination_key\n" + f"from: {from_expr}\n" + f"filter: created >= '{btql_quote(created_after)}'\n" + "sort: _pagination_key asc\n" + f"limit: {int(n)}" ) out = await fetch_btql_sorted_page_with_retries( diff --git a/braintrust_migrate/resources/datasets.py b/braintrust_migrate/resources/datasets.py index a9623e9..682ac45 100644 --- a/braintrust_migrate/resources/datasets.py +++ b/braintrust_migrate/resources/datasets.py @@ -478,12 +478,12 @@ async def _fetch_dataset_events_page_btql_sorted( limit: int, state: EventsStreamState, ) -> dict[str, Any]: - """Fetch one page via POST /btql using SQL syntax, sorted by _pagination_key.""" + """Fetch one page via POST /btql using native BTQL syntax, sorted by _pagination_key.""" last_pagination_key = state.btql_min_pagination_key def _query_text_for_limit(n: int) -> str: return build_btql_sorted_page_query( - from_expr=f"dataset('{btql_quote(dataset_id)}', shape => 'spans')", + from_expr=f"dataset('{btql_quote(dataset_id)}') spans", limit=n, last_pagination_key=last_pagination_key, select="*", diff --git a/braintrust_migrate/resources/experiments.py b/braintrust_migrate/resources/experiments.py index a86d4fc..c8de897 100644 --- a/braintrust_migrate/resources/experiments.py +++ b/braintrust_migrate/resources/experiments.py @@ -481,7 +481,7 @@ async def _migrate_experiment_events( ) -> None: """Migrate events from source experiment to destination experiment. - This uses BTQL (SQL) pagination ordered by `_pagination_key` and bounded inserts + This uses native BTQL pagination ordered by `_pagination_key` and bounded inserts so it can scale to very large experiments (potentially comparable to project logs). Deleted events (`_object_delete=true`) are skipped. @@ -555,12 +555,12 @@ async def _fetch_experiment_events_page_btql_sorted( limit: int, state: EventsStreamState, ) -> dict[str, Any]: - """Fetch one page via POST /btql using SQL syntax, sorted by _pagination_key.""" + """Fetch one page via POST /btql using native BTQL syntax, sorted by _pagination_key.""" last_pagination_key = state.btql_min_pagination_key def _query_text_for_limit(n: int) -> str: return build_btql_sorted_page_query( - from_expr=f"experiment('{btql_quote(experiment_id)}', shape => 'spans')", + from_expr=f"experiment('{btql_quote(experiment_id)}') spans", limit=n, last_pagination_key=last_pagination_key, select="*", diff --git a/braintrust_migrate/resources/logs.py b/braintrust_migrate/resources/logs.py index a2cbfe2..e65622f 100644 --- a/braintrust_migrate/resources/logs.py +++ b/braintrust_migrate/resources/logs.py @@ -268,7 +268,7 @@ async def _fetch_page_btql_sorted( project_id: str, limit: int, ) -> dict[str, Any]: - """Fetch one page via POST /btql using SQL syntax, sorted by _pagination_key. + """Fetch one page via POST /btql using native BTQL syntax, sorted by _pagination_key. This exists to allow inserting logs in created-ascending order, which makes destination `_xact_id` (and thus UI default ordering by `_pagination_key`) @@ -280,8 +280,8 @@ async def _fetch_page_btql_sorted( # For sorted pagination, we must do offset-based pagination by filtering on # the last sort key values from the previous page. - # Use SQL syntax for BTQL. This is simpler and tends to be more robust across - # deployments than multi-clause BTQL text generation. + # Use native BTQL syntax (select:/from:/filter:/sort:/limit:) for compatibility + # with data planes that don't yet support SQL mode. last_pagination_key = self._stream_state.btql_min_pagination_key last_pagination_key_inclusive = bool( self._stream_state.btql_min_pagination_key_inclusive @@ -289,7 +289,7 @@ async def _fetch_page_btql_sorted( created_after = self._stream_state.created_after created_before = self._stream_state.created_before - from_expr = f"project_logs('{btql_quote(project_id)}', shape => 'spans')" + from_expr = f"project_logs('{btql_quote(project_id)}') spans" def _query_text_for_limit(n: int) -> str: return build_btql_sorted_page_query( @@ -522,7 +522,7 @@ async def migrate_all(self, project_id: str | None = None) -> dict[str, Any]: start_pk = await find_first_pagination_key_for_created_after( client=self.source_client, from_expr=( - f"project_logs('{btql_quote(source_project_id)}', shape => 'spans')" + f"project_logs('{btql_quote(source_project_id)}') spans" ), created_after=self._stream_state.created_after, operation="btql_project_logs_created_after_start_pk", diff --git a/braintrust_migrate/streaming_utils.py b/braintrust_migrate/streaming_utils.py index e726791..5c81ff6 100644 --- a/braintrust_migrate/streaming_utils.py +++ b/braintrust_migrate/streaming_utils.py @@ -116,10 +116,13 @@ def build_btql_sorted_page_query( created_before: str | None = None, select: str = "*", ) -> str: - """Build a BTQL SQL query for stable sorted paging on `_pagination_key`. + """Build a native BTQL query for stable sorted paging on `_pagination_key`. + + Uses native BTQL syntax (select:/from:/filter:/sort:/limit:) instead of SQL + for compatibility with data planes that don't yet support SQL mode. Args: - from_expr: The FROM expression (e.g., "project_logs('...', shape => 'spans')") + from_expr: The FROM expression (e.g., "project_logs('...') spans") limit: Maximum number of rows to return last_pagination_key: Resume pagination from this key last_pagination_key_inclusive: If True, use >= instead of > for pagination key @@ -128,7 +131,7 @@ def build_btql_sorted_page_query( select: Fields to select (default "*") Returns: - BTQL SQL query string + Native BTQL query string """ conditions: list[str] = [] if isinstance(created_after, str) and created_after: @@ -138,14 +141,14 @@ def build_btql_sorted_page_query( if isinstance(last_pagination_key, str) and last_pagination_key: op = ">=" if last_pagination_key_inclusive else ">" conditions.append(f"_pagination_key {op} '{btql_quote(last_pagination_key)}'") - where = f"WHERE {' AND '.join(conditions)}\n" if conditions else "" + filter_clause = f"filter: {' and '.join(conditions)}\n" if conditions else "" return ( - f"SELECT {select}\n" - f"FROM {from_expr}\n" - f"{where}" - "ORDER BY _pagination_key ASC\n" - f"LIMIT {int(limit)}" + f"select: {select}\n" + f"from: {from_expr}\n" + f"{filter_clause}" + "sort: _pagination_key asc\n" + f"limit: {int(limit)}" ) diff --git a/tests/unit/test_btql_query_builder.py b/tests/unit/test_btql_query_builder.py index 7bfa6ad..a5d9ea2 100644 --- a/tests/unit/test_btql_query_builder.py +++ b/tests/unit/test_btql_query_builder.py @@ -13,115 +13,115 @@ class TestBuildBtqlSortedPageQuery: def test_basic_query_no_filters(self) -> None: """Test basic query without any filters.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, ) - assert "SELECT *" in query - assert "FROM project_logs('proj123', shape => 'spans')" in query - assert "ORDER BY _pagination_key ASC" in query - assert "LIMIT 100" in query - assert "WHERE" not in query + assert "select: *" in query + assert "from: project_logs('proj123') spans" in query + assert "sort: _pagination_key asc" in query + assert "limit: 100" in query + assert "filter:" not in query def test_query_with_pagination_key(self) -> None: """Test query with pagination key (resume).""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key="abc123", ) - assert "WHERE" in query + assert "filter:" in query assert "_pagination_key > 'abc123'" in query def test_query_with_pagination_key_inclusive(self) -> None: """Test query with inclusive pagination key.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key="abc123", last_pagination_key_inclusive=True, ) - assert "WHERE" in query + assert "filter:" in query assert "_pagination_key >= 'abc123'" in query def test_query_with_created_after_only(self) -> None: """Test query with created_after filter only.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, created_after="2026-01-15T00:00:00Z", ) - assert "WHERE" in query + assert "filter:" in query assert "created >= '2026-01-15T00:00:00Z'" in query assert "created <" not in query def test_query_with_created_before_only(self) -> None: """Test query with created_before filter only.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, created_before="2026-02-01T00:00:00Z", ) - assert "WHERE" in query + assert "filter:" in query assert "created < '2026-02-01T00:00:00Z'" in query assert "created >=" not in query def test_query_with_date_range(self) -> None: """Test query with both created_after and created_before (date range).""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, created_after="2026-01-01T00:00:00Z", created_before="2026-02-01T00:00:00Z", ) - assert "WHERE" in query + assert "filter:" in query assert "created >= '2026-01-01T00:00:00Z'" in query assert "created < '2026-02-01T00:00:00Z'" in query - # Both conditions should be ANDed together - assert " AND " in query + # Both conditions should be joined with 'and' + assert " and " in query def test_query_with_date_range_and_pagination(self) -> None: """Test query with date range and pagination key.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key="pk123", created_after="2026-01-01T00:00:00Z", created_before="2026-02-01T00:00:00Z", ) - assert "WHERE" in query + assert "filter:" in query assert "created >= '2026-01-01T00:00:00Z'" in query assert "created < '2026-02-01T00:00:00Z'" in query assert "_pagination_key > 'pk123'" in query - # All three conditions should be ANDed together - assert query.count(" AND ") == 2 + # All three conditions should be joined with 'and' + assert query.count(" and ") == 2 def test_query_with_custom_select(self) -> None: """Test query with custom select clause.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, select="_pagination_key", ) - assert "SELECT _pagination_key" in query - assert "SELECT *" not in query + assert "select: _pagination_key" in query + assert "select: *" not in query def test_query_escapes_special_characters_in_pagination_key(self) -> None: """Test that special characters in pagination key are escaped.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key="key'with'quotes", ) @@ -133,7 +133,7 @@ def test_query_escapes_special_characters_in_date_filters(self) -> None: """Test that special characters in date values are escaped.""" # This shouldn't happen with real dates, but the function should handle it query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, created_after="2026-01-01T00:00:00Z", @@ -145,85 +145,85 @@ def test_query_escapes_special_characters_in_date_filters(self) -> None: def test_query_for_experiment_events(self) -> None: """Test query for experiment events with date range.""" query = build_btql_sorted_page_query( - from_expr="experiment('exp123', shape => 'spans')", + from_expr="experiment('exp123') spans", limit=50, last_pagination_key=None, created_after="2026-01-15T00:00:00Z", created_before="2026-01-16T00:00:00Z", ) - assert "FROM experiment('exp123', shape => 'spans')" in query + assert "from: experiment('exp123') spans" in query assert "created >= '2026-01-15T00:00:00Z'" in query assert "created < '2026-01-16T00:00:00Z'" in query - assert "LIMIT 50" in query + assert "limit: 50" in query def test_query_for_dataset_events(self) -> None: """Test query for dataset events with date range.""" query = build_btql_sorted_page_query( - from_expr="dataset('ds123', shape => 'spans')", + from_expr="dataset('ds123') spans", limit=200, last_pagination_key="resume_pk", created_after="2026-01-01T00:00:00Z", ) - assert "FROM dataset('ds123', shape => 'spans')" in query + assert "from: dataset('ds123') spans" in query assert "created >= '2026-01-01T00:00:00Z'" in query assert "_pagination_key > 'resume_pk'" in query - assert "LIMIT 200" in query + assert "limit: 200" in query def test_empty_string_filters_are_ignored(self) -> None: """Test that empty string filters are treated as None.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key="", created_after="", created_before="", ) - # Empty strings should not create WHERE conditions - assert "WHERE" not in query + # Empty strings should not create filter conditions + assert "filter:" not in query def test_none_filters_are_ignored(self) -> None: """Test that None filters are properly ignored.""" query = build_btql_sorted_page_query( - from_expr="project_logs('proj123', shape => 'spans')", + from_expr="project_logs('proj123') spans", limit=100, last_pagination_key=None, created_after=None, created_before=None, ) - assert "WHERE" not in query + assert "filter:" not in query class TestQueryConditionOrder: """Tests to verify the order and structure of query conditions.""" def test_conditions_are_properly_joined_with_and(self) -> None: - """Test that multiple conditions are joined with AND.""" + """Test that multiple conditions are joined with 'and'.""" query = build_btql_sorted_page_query( - from_expr="project_logs('p', shape => 'spans')", + from_expr="project_logs('p') spans", limit=100, last_pagination_key="pk", created_after="2026-01-01T00:00:00Z", created_before="2026-02-01T00:00:00Z", ) - # Extract WHERE clause - where_start = query.find("WHERE") - order_start = query.find("ORDER BY") - where_clause = query[where_start:order_start].strip() + # Extract filter clause + filter_start = query.find("filter:") + sort_start = query.find("sort:") + filter_clause = query[filter_start:sort_start].strip() # Should have all three conditions - assert "created >=" in where_clause - assert "created <" in where_clause - assert "_pagination_key >" in where_clause + assert "created >=" in filter_clause + assert "created <" in filter_clause + assert "_pagination_key >" in filter_clause - def test_query_structure_is_valid_sql(self) -> None: - """Test that the generated query has valid SQL structure.""" + def test_query_structure_is_valid_btql(self) -> None: + """Test that the generated query has valid native BTQL structure.""" query = build_btql_sorted_page_query( - from_expr="project_logs('p', shape => 'spans')", + from_expr="project_logs('p') spans", limit=100, last_pagination_key="pk", created_after="2026-01-01T00:00:00Z", @@ -231,10 +231,10 @@ def test_query_structure_is_valid_sql(self) -> None: ) # Check proper clause ordering - select_pos = query.find("SELECT") - from_pos = query.find("FROM") - where_pos = query.find("WHERE") - order_pos = query.find("ORDER BY") - limit_pos = query.find("LIMIT") + select_pos = query.find("select:") + from_pos = query.find("from:") + filter_pos = query.find("filter:") + sort_pos = query.find("sort:") + limit_pos = query.find("limit:") - assert select_pos < from_pos < where_pos < order_pos < limit_pos + assert select_pos < from_pos < filter_pos < sort_pos < limit_pos diff --git a/tests/unit/test_dataset_streaming_resume.py b/tests/unit/test_dataset_streaming_resume.py index 0f61fbf..8fd4d6b 100644 --- a/tests/unit/test_dataset_streaming_resume.py +++ b/tests/unit/test_dataset_streaming_resume.py @@ -44,9 +44,10 @@ async def raw_request( assert json is not None and isinstance(json.get("query"), str) q = json.get("query") assert isinstance(q, str) - if "WHERE _pagination_key > 'p1'" in q: + # Native BTQL syntax uses `filter:` instead of SQL `WHERE`. + if "_pagination_key > 'p1'" in q: return {"data": self._page2} - if "WHERE _pagination_key > 'p2'" in q: + if "_pagination_key > 'p2'" in q: return {"data": []} return {"data": self._page1} diff --git a/tests/unit/test_experiment_streaming_resume.py b/tests/unit/test_experiment_streaming_resume.py index 452e254..dec3aad 100644 --- a/tests/unit/test_experiment_streaming_resume.py +++ b/tests/unit/test_experiment_streaming_resume.py @@ -44,9 +44,10 @@ async def raw_request( assert json is not None and isinstance(json.get("query"), str) q = json.get("query") assert isinstance(q, str) - if "WHERE _pagination_key > 'p1'" in q: + # Native BTQL syntax uses `filter:` instead of SQL `WHERE`. + if "_pagination_key > 'p1'" in q: return {"data": self._page2} - if "WHERE _pagination_key > 'p2'" in q: + if "_pagination_key > 'p2'" in q: return {"data": []} return {"data": self._page1} diff --git a/tests/unit/test_logs_btql_sorted_fetch.py b/tests/unit/test_logs_btql_sorted_fetch.py index 6a24a2b..dc70cfb 100644 --- a/tests/unit/test_logs_btql_sorted_fetch.py +++ b/tests/unit/test_logs_btql_sorted_fetch.py @@ -29,24 +29,24 @@ async def raw_request( _ = kwargs assert method.upper() == "POST" assert path == "/btql" - # We don't fully parse SQL/BTQL, but we do validate that the migrator is using + # We don't fully parse BTQL, but we do validate that the migrator is using # sorted + offset-based pagination (filter on the last _pagination_key), not cursor. q = (json or {}).get("query") assert isinstance(q, str) - assert "SELECT *" in q - assert "FROM project_logs('proj-source', shape => 'spans')" in q - assert "ORDER BY _pagination_key" in q - assert "LIMIT" in q + assert "select:" in q + assert "from: project_logs('proj-source') spans" in q + assert "sort: _pagination_key" in q + assert "limit:" in q assert "cursor:" not in q # cursor pagination doesn't work with sort self._calls += 1 PAGE_ONE = 1 PAGE_TWO = 2 # Page 1 should have no WHERE filter (start of stream). if self._calls == PAGE_ONE: - assert "WHERE _pagination_key >" not in q + assert "_pagination_key >" not in q # Page 2 should filter on the last row of page 1 (pk=2). if self._calls == PAGE_TWO: - assert "WHERE _pagination_key > '2'" in q + assert "_pagination_key > '2'" in q if self._pages: return {"data": self._pages.pop(0)} @@ -77,8 +77,8 @@ async def raw_request( assert isinstance(q, str) self.queries.append(q) - # Simulate the backend error you saw: LIMIT 1000 fails, smaller LIMIT succeeds. - if "LIMIT 1000" in q: + # Simulate the backend error you saw: limit: 1000 fails, smaller limit succeeds. + if "limit: 1000" in q: import httpx req = httpx.Request("POST", "https://api.braintrust.dev/btql") @@ -141,6 +141,7 @@ def __init__( self.queries: list[str] = [] self._fetch_calls = 0 self._preflight_calls = 0 + self._SECOND_CALL = 2 async def with_retry(self, _operation_name: str, coro_func): res = coro_func() @@ -159,31 +160,28 @@ async def raw_request( self.queries.append(q) # Preflight query selects only _pagination_key. - if "SELECT _pagination_key" in q: + if "select: _pagination_key" in q: self._preflight_calls += 1 - assert "FROM project_logs('proj-source', shape => 'spans')" in q - assert "WHERE created >= '2020-01-02T00:00:00Z'" in q - assert "ORDER BY _pagination_key ASC" in q - assert "LIMIT 1" in q + assert "from: project_logs('proj-source') spans" in q + assert "filter: created >= '2020-01-02T00:00:00Z'" in q + assert "sort: _pagination_key asc" in q + assert "limit: 1" in q # Start at pk=2 (row "b") return {"data": [{"_pagination_key": "2"}]} # Fetch queries select full rows. - assert "SELECT *" in q - assert "FROM project_logs('proj-source', shape => 'spans')" in q - assert "ORDER BY _pagination_key" in q + assert "select: *" in q + assert "from: project_logs('proj-source') spans" in q + assert "sort: _pagination_key asc" in q assert "cursor:" not in q self._fetch_calls += 1 if self._fetch_calls == 1: - assert ( - "WHERE created >= '2020-01-02T00:00:00Z' AND _pagination_key >= '2'" - in q - ) - if self._fetch_calls == 2: - assert ( - "WHERE created >= '2020-01-02T00:00:00Z' AND _pagination_key > '2'" in q - ) + assert "filter: created >= '2020-01-02T00:00:00Z'" in q + assert "_pagination_key >= '2'" in q + if self._fetch_calls == self._SECOND_CALL: + assert "filter: created >= '2020-01-02T00:00:00Z'" in q + assert "_pagination_key > '2'" in q if self._pages: return {"data": self._pages.pop(0)} @@ -254,10 +252,10 @@ async def test_logs_btql_fetch_retries_smaller_limit_on_500(tmp_path: Path) -> N assert res["migrated"] == 1 assert dest.inserted_ids == ["a"] - # Verify we tried LIMIT 1000, then retried with a smaller limit (500 is first). + # Verify we tried limit: 1000, then retried with a smaller limit (500 is first). combined = "\n".join(source.queries) - assert "LIMIT 1000" in combined - assert "LIMIT 500" in combined + assert "limit: 1000" in combined + assert "limit: 500" in combined @pytest.mark.asyncio @@ -293,7 +291,8 @@ async def test_logs_created_after_uses_preflight_and_inclusive_start_pk( migrator.set_destination_project_id("proj-dest") res = await migrator.migrate_all("proj-source") - assert res["migrated"] == 2 + EXPECTED_MIGRATED = 2 + assert res["migrated"] == EXPECTED_MIGRATED assert dest.inserted_ids == ["b", "c"] diff --git a/tests/unit/test_logs_streaming_resume.py b/tests/unit/test_logs_streaming_resume.py index 9f72406..6e6f7e4 100644 --- a/tests/unit/test_logs_streaming_resume.py +++ b/tests/unit/test_logs_streaming_resume.py @@ -38,12 +38,14 @@ async def raw_request( assert method.lower() == "post" if path == "/btql": - assert json is not None and isinstance(json.get("query"), str) + assert json is not None + assert isinstance(json.get("query"), str) q = json.get("query") assert isinstance(q, str) - if "WHERE _pagination_key > 'p1'" in q: + # Native BTQL syntax uses `filter:` instead of SQL `WHERE`. + if "_pagination_key > 'p1'" in q: return {"data": self._page2} - if "WHERE _pagination_key > 'p2'" in q: + if "_pagination_key > 'p2'" in q: return {"data": []} return {"data": self._page1} From 1e3d925b035db515b34618581d6ad7538ab7b151 Mon Sep 17 00:00:00 2001 From: Doug Guthrie Date: Wed, 4 Feb 2026 09:34:41 -0700 Subject: [PATCH 2/2] add gh action --- .github/workflows/test.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..108da3b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,24 @@ +name: Tests + +on: + pull_request: + branches: + - main + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + + - name: Install dependencies + run: uv sync --all-extras --dev + + - name: Run tests + run: uv run pytest