Skip to content

Commit d09b53a

Browse files
authored
feat: support table function fixtures in model tests (#740)
Co-authored-by: Kevin Longe <273747162+kvlonge@users.noreply.github.com>
1 parent db3d670 commit d09b53a

19 files changed

Lines changed: 546 additions & 59 deletions

File tree

‎src/sqlbuild/cli/commands/_helpers/test/inspection.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def write_test_plan_inspection(*, stream: TextIO, plan_output: PlanOutput) -> in
1919
_write_names(stream=stream, label="mocked refs", names=entry.mock_ref_names)
2020
_write_names(stream=stream, label="mocked seeds", names=entry.mock_seed_names)
2121
_write_names(stream=stream, label="mocked dbt refs", names=entry.mock_dbt_ref_names)
22+
_write_names(
23+
stream=stream,
24+
label="mocked table functions",
25+
names=entry.mock_table_function_names,
26+
)
2227
_write_names(
2328
stream=stream,
2429
label="real models",

‎src/sqlbuild/compiler/compile/_helpers/assembly/project.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,6 +1439,7 @@ def _assemble_compiled_sql_test(
14391439
mock_source_names=model_payload.mock_source_names,
14401440
mock_seed_names=model_payload.mock_seed_names,
14411441
mock_dbt_ref_names=model_payload.mock_dbt_ref_names,
1442+
mock_table_function_names=model_payload.mock_table_function_names,
14421443
expected_ctes=model_payload.expected_ctes,
14431444
expected_model_names=model_payload.expected_model_names,
14441445
assertion_ctes=model_payload.assertion_ctes,

‎src/sqlbuild/compiler/compile/_helpers/attachment/sql_tests.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ def build_test_inputs(
273273
known_model_names=known_model_names,
274274
known_seed_names=known_seed_names,
275275
known_source_names=known_source_names,
276+
known_table_function_names=known_table_function_names,
276277
loaded_macros=loaded_macros,
277278
)
278279
test_payload: (
@@ -387,6 +388,7 @@ def _build_test_input_payload(
387388
mock_source_names=model_payload.mock_source_names,
388389
mock_seed_names=model_payload.mock_seed_names,
389390
mock_dbt_ref_names=model_payload.mock_dbt_ref_names,
391+
mock_table_function_names=model_payload.mock_table_function_names,
390392
expected_ctes=model_payload.expected_ctes,
391393
expected_model_names=model_payload.expected_model_names,
392394
assertion_ctes=model_payload.assertion_ctes,
@@ -678,6 +680,7 @@ def validate_test_ctes(
678680
known_model_names: set[str],
679681
known_seed_names: set[str],
680682
known_source_names: set[str],
683+
known_table_function_names: set[str],
681684
loaded_macros: dict[str, LoadedMacro],
682685
) -> None:
683686
"""Validate SQL-native test CTE targets against discovered inputs."""
@@ -705,6 +708,13 @@ def validate_test_ctes(
705708
raise CompileInputError(
706709
f"SQL test file {test_file.relative_path} mocks unknown seed '{mock_seed_name}'"
707710
)
711+
mock_table_function_name: str
712+
for mock_table_function_name in model_payload.mock_table_function_names:
713+
if mock_table_function_name not in known_table_function_names:
714+
raise CompileInputError(
715+
f"SQL test file {test_file.relative_path} mocks unknown table function "
716+
f"'{mock_table_function_name}'"
717+
)
708718
macro_mock_name: str
709719
for macro_mock_name in model_payload.macro_mocks:
710720
if macro_mock_name not in loaded_macros:

‎src/sqlbuild/compiler/compile/_helpers/sql_tests/core.py‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
SQL_WITH_KEYWORD,
3737
TABLE_FN_ACTUAL_TEST_CTE_NAME,
3838
TABLE_FN_EXPECTED_TEST_CTE_NAME,
39+
TABLE_FN_TEST_CTE_PREFIX,
3940
UDF_ACTUAL_TEST_CTE_NAME,
4041
UDF_EXPECTED_TEST_CTE_NAME,
4142
)
@@ -392,6 +393,7 @@ def _classify_model_sql_test_ctes(
392393
mock_source_names: list[str] = []
393394
mock_seed_names: list[str] = []
394395
mock_dbt_ref_names: list[str] = []
396+
mock_table_function_names: list[str] = []
395397
expected_ctes: list[CompileSqlTestCte] = []
396398
expected_model_names: list[str] = []
397399
assertion_ctes: list[CompileSqlTestCte] = []
@@ -467,6 +469,17 @@ def _classify_model_sql_test_ctes(
467469
)
468470
authored_ctes.append(cte)
469471
continue
472+
if cte.name.startswith(TABLE_FN_TEST_CTE_PREFIX):
473+
mock_table_function_names.append(
474+
_require_prefixed_name(
475+
cte_name=cte.name,
476+
prefix=TABLE_FN_TEST_CTE_PREFIX,
477+
label="__table_fn__<function>",
478+
file_label=file_label,
479+
)
480+
)
481+
authored_ctes.append(cte)
482+
continue
470483
if cte.name.startswith(EXPECTED_TEST_CTE_PREFIX):
471484
expected_model_names.append(
472485
_require_prefixed_name(
@@ -501,11 +514,12 @@ def _classify_model_sql_test_ctes(
501514
*mock_source_names,
502515
*mock_seed_names,
503516
*mock_dbt_ref_names,
517+
*mock_table_function_names,
504518
)
505519
if not mock_target_names:
506520
raise CompileInputError(
507521
f"SQL test '{file_label}' must define at least one __ref__*, __source__*, "
508-
"__seed__*, or __dbt_ref__* mock CTE"
522+
"__seed__*, __dbt_ref__*, or __table_fn__* mock CTE"
509523
)
510524
check_names: tuple[str, ...] = (*expected_model_names, *assertion_names)
511525
if not check_names:
@@ -520,6 +534,7 @@ def _classify_model_sql_test_ctes(
520534
mock_source_names=tuple(mock_source_names),
521535
mock_seed_names=tuple(mock_seed_names),
522536
mock_dbt_ref_names=tuple(mock_dbt_ref_names),
537+
mock_table_function_names=tuple(mock_table_function_names),
523538
expected_ctes=tuple(expected_ctes),
524539
expected_model_names=tuple(expected_model_names),
525540
assertion_ctes=tuple(assertion_ctes),
@@ -536,6 +551,7 @@ def _is_model_mode_cte(cte_name: str) -> bool:
536551
SOURCE_TEST_CTE_PREFIX,
537552
SEED_TEST_CTE_PREFIX,
538553
DBT_REF_TEST_CTE_PREFIX,
554+
TABLE_FN_TEST_CTE_PREFIX,
539555
EXPECTED_TEST_CTE_PREFIX,
540556
ASSERT_TEST_CTE_PREFIX,
541557
)

‎src/sqlbuild/compiler/compile/constants.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@
111111
SOURCE_TEST_CTE_PREFIX: str = SqlReferenceKind.SOURCE.fixture_cte_prefix
112112
SEED_TEST_CTE_PREFIX: str = SqlReferenceKind.SEED.fixture_cte_prefix
113113
DBT_REF_TEST_CTE_PREFIX: str = SqlReferenceKind.DBT_REF.fixture_cte_prefix
114+
TABLE_FN_TEST_CTE_PREFIX: str = SqlReferenceKind.TABLE_FUNCTION.fixture_cte_prefix
114115
MACRO_TEST_CTE_PREFIX: str = "__macro__"
115116
MACRO_ACTUAL_TEST_CTE_NAME: str = "__macro_actual__"
116117
MACRO_EXPECTED_TEST_CTE_NAME: str = "__macro_expected__"

‎src/sqlbuild/compiler/compile/models.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,7 @@ class CompileModelSqlTestCtes:
926926
mock_source_names: tuple[str, ...] = field(default_factory=tuple)
927927
mock_seed_names: tuple[str, ...] = field(default_factory=tuple)
928928
mock_dbt_ref_names: tuple[str, ...] = field(default_factory=tuple)
929+
mock_table_function_names: tuple[str, ...] = field(default_factory=tuple)
929930
expected_ctes: tuple[CompileSqlTestCte, ...] = field(default_factory=tuple)
930931
expected_model_names: tuple[str, ...] = field(default_factory=tuple)
931932
assertion_ctes: tuple[CompileSqlTestCte, ...] = field(default_factory=tuple)
@@ -960,6 +961,7 @@ class CompileModelSqlTestInputPayload:
960961
mock_source_names: tuple[str, ...] = field(default_factory=tuple)
961962
mock_seed_names: tuple[str, ...] = field(default_factory=tuple)
962963
mock_dbt_ref_names: tuple[str, ...] = field(default_factory=tuple)
964+
mock_table_function_names: tuple[str, ...] = field(default_factory=tuple)
963965
expected_ctes: tuple[CompileSqlTestCte, ...] = field(default_factory=tuple)
964966
expected_model_names: tuple[str, ...] = field(default_factory=tuple)
965967
assertion_ctes: tuple[CompileSqlTestCte, ...] = field(default_factory=tuple)
@@ -1007,6 +1009,7 @@ class CompiledModelSqlTestPayload:
10071009
mock_source_names: tuple[str, ...] = field(default_factory=tuple)
10081010
mock_seed_names: tuple[str, ...] = field(default_factory=tuple)
10091011
mock_dbt_ref_names: tuple[str, ...] = field(default_factory=tuple)
1012+
mock_table_function_names: tuple[str, ...] = field(default_factory=tuple)
10101013
expected_ctes: tuple[CompileSqlTestCte, ...] = field(default_factory=tuple)
10111014
expected_model_names: tuple[str, ...] = field(default_factory=tuple)
10121015
assertion_ctes: tuple[CompileSqlTestCte, ...] = field(default_factory=tuple)

‎src/sqlbuild/compiler/planner/_helpers/resolve/refs.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,42 @@ def resolve_table_function_references(
253253
return "".join(parts)
254254

255255

256+
def resolve_table_function_fixture_references(
257+
*, query_sql: str, fixtures: dict[str, str]
258+
) -> tuple[str, frozenset[str]]:
259+
"""Replace complete table-function invocations with model-test fixture relations."""
260+
261+
parts: list[str] = []
262+
last_index: int = 0
263+
reached: set[str] = set()
264+
match: re.Match[str]
265+
for match in _iter_executable_matches(sql=query_sql, pattern=_TABLE_FUNCTION_PATTERN):
266+
if match.start() < last_index:
267+
continue
268+
function_name: str = match.group(1)
269+
fixture_sql: str | None = fixtures.get(function_name)
270+
if fixture_sql is None:
271+
continue
272+
call_suffix_start: int = _skip_whitespace(sql=query_sql, start=match.end())
273+
if (
274+
call_suffix_start >= len(query_sql)
275+
or query_sql[call_suffix_start] != SQL_FUNCTION_CALL_OPEN_PAREN
276+
):
277+
continue
278+
call_suffix_end: int = find_matching_paren(
279+
sql=query_sql,
280+
open_paren_index=call_suffix_start,
281+
context="SQL table function fixture call",
282+
)
283+
parts.append(query_sql[last_index : match.start()])
284+
parts.append(fixture_sql)
285+
last_index = call_suffix_end + 1
286+
reached.add(function_name)
287+
288+
parts.append(query_sql[last_index:])
289+
return "".join(parts), frozenset(reached)
290+
291+
256292
def _iter_executable_matches(*, sql: str, pattern: re.Pattern[str]) -> Iterator[re.Match[str]]:
257293
index: int = 0
258294
while index < len(sql):

0 commit comments

Comments
 (0)