Skip to content

Commit 9496819

Browse files
committed
refactor: Convert legacy filter to be filter_query
1 parent af49aee commit 9496819

3 files changed

Lines changed: 70 additions & 92 deletions

File tree

cloud_pipelines_backend/filter_query_sql.py

Lines changed: 22 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -164,11 +164,12 @@ def build_list_filters(
164164
page_token.filter_query if page_token_value else filter_query_value
165165
)
166166

167-
where_clauses, resolved_filter = _build_filter_where_clauses(
168-
filter_value=filter_value,
169-
current_user=current_user,
170-
)
167+
if filter_value:
168+
filter_query_value = _convert_legacy_filter_to_filter_query(
169+
filter_value=filter_value,
170+
)
171171

172+
where_clauses: list[sql.ColumnElement] = []
172173
if filter_query_value:
173174
parsed = filter_query_models.FilterQuery.model_validate_json(filter_query_value)
174175
where_clauses.append(
@@ -180,7 +181,7 @@ def build_list_filters(
180181

181182
next_page_token = PageToken(
182183
offset=offset + page_size,
183-
filter=resolved_filter,
184+
filter=None,
184185
filter_query=filter_query_value,
185186
)
186187

@@ -219,39 +220,31 @@ def _parse_filter(filter: str) -> dict[str, str]:
219220
return parsed_filter
220221

221222

222-
def _build_filter_where_clauses(
223+
def _convert_legacy_filter_to_filter_query(
223224
*,
224-
filter_value: str | None,
225-
current_user: str | None,
226-
) -> tuple[list[sql.ColumnElement], str | None]:
227-
"""Parse a filter string into SQLAlchemy WHERE clauses.
225+
filter_value: str,
226+
) -> str:
227+
"""Convert a legacy ``filter`` string to an equivalent ``filter_query`` JSON string.
228228
229-
Returns (where_clauses, next_page_filter_value). The second value is the
230-
filter string with shorthand values resolved (e.g. "created_by:me" becomes
231-
"created_by:alice@example.com") so it can be embedded in the next page token.
229+
Only ``created_by`` is supported. ``"me"`` is NOT resolved here — the
230+
downstream ``_maybe_resolve_system_values`` handles that.
232231
"""
233-
where_clauses: list[sql.ColumnElement] = []
234-
parsed_filter = _parse_filter(filter_value) if filter_value else {}
235-
for key, value in parsed_filter.items():
232+
parsed = _parse_filter(filter_value)
233+
predicates: list[dict] = []
234+
for key, value in parsed.items():
236235
if key == "_text":
237236
raise NotImplementedError("Text search is not implemented yet.")
238237
elif key == "created_by":
239-
if value == "me":
240-
if current_user is None:
241-
current_user = ""
242-
value = current_user
243-
# TODO: Maybe make this a bit more robust.
244-
# We need to change the filter since it goes into the next_page_token.
245-
filter_value = filter_value.replace(
246-
"created_by:me", f"created_by:{current_user}"
238+
if not value:
239+
raise errors.ApiValidationError(
240+
"Legacy filter 'created_by' requires a non-empty value."
247241
)
248-
if value:
249-
where_clauses.append(bts.PipelineRun.created_by == value)
250-
else:
251-
where_clauses.append(bts.PipelineRun.created_by == None)
242+
predicates.append(
243+
{"value_equals": {"key": SystemKey.CREATED_BY, "value": value}}
244+
)
252245
else:
253246
raise NotImplementedError(f"Unsupported filter {filter_value}.")
254-
return where_clauses, filter_value
247+
return json.dumps({"and": predicates})
255248

256249

257250
# ---------------------------------------------------------------------------

tests/test_api_server_sql.py

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -155,27 +155,13 @@ def test_list_filter_created_by(self, session_factory, service):
155155
assert len(result.pipeline_runs) == 1
156156
assert result.pipeline_runs[0].created_by == "user1"
157157

158-
def test_list_filter_created_by_empty(self, session_factory, service):
159-
_create_run(
160-
session_factory,
161-
service,
162-
root_task=_make_task_spec(),
163-
created_by=None,
164-
)
165-
_create_run(
166-
session_factory,
167-
service,
168-
root_task=_make_task_spec(),
169-
created_by="user1",
170-
)
171-
158+
def test_list_filter_created_by_empty_raises(self, session_factory, service):
172159
with session_factory() as session:
173-
result = service.list(
174-
session=session,
175-
filter="created_by:",
176-
)
177-
assert len(result.pipeline_runs) == 1
178-
assert result.pipeline_runs[0].created_by is None
160+
with pytest.raises(errors.ApiValidationError, match="non-empty value"):
161+
service.list(
162+
session=session,
163+
filter="created_by:",
164+
)
179165

180166
def test_list_pagination(self, session_factory, service):
181167
for i in range(12):

tests/test_filter_query_sql.py

Lines changed: 42 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
import pytest
24
import sqlalchemy as sql
35
from sqlalchemy.dialects import sqlite as sqlite_dialect
@@ -161,59 +163,46 @@ def test_decode_empty_string(self):
161163
assert token.filter_query is None
162164

163165

164-
class TestBuildFilterWhereClauses:
165-
def test_no_filter(self):
166-
clauses, next_filter = filter_query_sql._build_filter_where_clauses(
167-
filter_value=None,
168-
current_user=None,
169-
)
170-
assert clauses == []
171-
assert next_filter is None
172-
166+
class TestConvertLegacyFilterToFilterQuery:
173167
def test_created_by_literal(self):
174-
clauses, next_filter = filter_query_sql._build_filter_where_clauses(
168+
result = filter_query_sql._convert_legacy_filter_to_filter_query(
175169
filter_value="created_by:alice",
176-
current_user=None,
177170
)
178-
assert len(clauses) == 1
179-
assert next_filter == "created_by:alice"
180-
181-
def test_created_by_me_resolves(self):
182-
clauses, next_filter = filter_query_sql._build_filter_where_clauses(
171+
parsed = json.loads(result)
172+
assert parsed == {
173+
"and": [
174+
{
175+
"value_equals": {
176+
"key": "system/pipeline_run.created_by",
177+
"value": "alice",
178+
}
179+
}
180+
]
181+
}
182+
183+
def test_created_by_me_not_resolved(self):
184+
result = filter_query_sql._convert_legacy_filter_to_filter_query(
183185
filter_value="created_by:me",
184-
current_user="alice@example.com",
185-
)
186-
assert len(clauses) == 1
187-
assert next_filter == "created_by:alice@example.com"
188-
189-
def test_created_by_me_no_current_user(self):
190-
clauses, next_filter = filter_query_sql._build_filter_where_clauses(
191-
filter_value="created_by:me",
192-
current_user=None,
193186
)
194-
assert len(clauses) == 1
195-
assert next_filter == "created_by:"
187+
parsed = json.loads(result)
188+
assert parsed["and"][0]["value_equals"]["value"] == "me"
196189

197-
def test_created_by_empty_value(self):
198-
clauses, next_filter = filter_query_sql._build_filter_where_clauses(
199-
filter_value="created_by:",
200-
current_user=None,
201-
)
202-
assert len(clauses) == 1
203-
assert next_filter == "created_by:"
190+
def test_created_by_empty_raises(self):
191+
with pytest.raises(errors.ApiValidationError, match="non-empty value"):
192+
filter_query_sql._convert_legacy_filter_to_filter_query(
193+
filter_value="created_by:",
194+
)
204195

205196
def test_unsupported_key_raises(self):
206197
with pytest.raises(NotImplementedError, match="Unsupported filter"):
207-
filter_query_sql._build_filter_where_clauses(
198+
filter_query_sql._convert_legacy_filter_to_filter_query(
208199
filter_value="unknown_key:value",
209-
current_user=None,
210200
)
211201

212202
def test_text_search_raises(self):
213203
with pytest.raises(NotImplementedError, match="Text search"):
214-
filter_query_sql._build_filter_where_clauses(
204+
filter_query_sql._convert_legacy_filter_to_filter_query(
215205
filter_value="some_text_without_colon",
216-
current_user=None,
217206
)
218207

219208

@@ -242,7 +231,7 @@ def test_mutual_exclusivity_raises(self):
242231
page_size=10,
243232
)
244233

245-
def test_legacy_filter_produces_clauses(self):
234+
def test_legacy_filter_produces_annotation_clause(self):
246235
clauses, offset, next_token = filter_query_sql.build_list_filters(
247236
filter_value="created_by:alice",
248237
filter_query_value=None,
@@ -251,8 +240,12 @@ def test_legacy_filter_produces_clauses(self):
251240
page_size=10,
252241
)
253242
assert len(clauses) == 1
243+
compiled = _compile(clauses[0])
244+
assert "EXISTS" in compiled.upper()
245+
assert "pipeline_run_annotation" in compiled
254246
assert offset == 0
255-
assert next_token.filter == "created_by:alice"
247+
assert next_token.filter is None
248+
assert next_token.filter_query is not None
256249

257250
def test_filter_query_produces_clauses(self):
258251
fq = '{"and": [{"key_exists": {"key": "team"}}]}'
@@ -268,7 +261,7 @@ def test_filter_query_produces_clauses(self):
268261
assert "EXISTS" in compiled.upper()
269262
assert next_token.filter_query == fq
270263

271-
def test_page_token_restores_offset_and_filters(self):
264+
def test_page_token_with_legacy_filter_converts(self):
272265
token = filter_query_sql.PageToken(
273266
offset=20,
274267
filter="created_by:alice",
@@ -282,8 +275,11 @@ def test_page_token_restores_offset_and_filters(self):
282275
)
283276
assert offset == 20
284277
assert len(clauses) == 1
278+
compiled = _compile(clauses[0])
279+
assert "EXISTS" in compiled.upper()
285280
assert next_token.offset == 30
286-
assert next_token.filter == "created_by:alice"
281+
assert next_token.filter is None
282+
assert next_token.filter_query is not None
287283

288284
def test_page_token_restores_filter_query(self):
289285
fq = '{"and": [{"key_exists": {"key": "env"}}]}'
@@ -319,7 +315,10 @@ def test_created_by_me_resolved_in_next_token(self):
319315
page_size=10,
320316
)
321317
assert len(clauses) == 1
322-
assert next_token.filter == "created_by:bob@example.com"
318+
assert next_token.filter is None
319+
assert next_token.filter_query is not None
320+
parsed_fq = json.loads(next_token.filter_query)
321+
assert parsed_fq["and"][0]["value_equals"]["value"] == "me"
323322

324323

325324
class TestSystemKeyValidation:

0 commit comments

Comments
 (0)