Skip to content

Commit 107ab8f

Browse files
authored
perf: reduce semantic compile analysis cost (#746)
Co-authored-by: Kevin Longe <273747162+kvlonge@users.noreply.github.com>
1 parent 14738ff commit 107ab8f

8 files changed

Lines changed: 472 additions & 63 deletions

File tree

‎Cargo.lock‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ fensu-policy = "=0.13.0"
1515
globset = "0.4"
1616
proc-macro2 = { version = "1", features = ["span-locations"] }
1717
pyo3 = { version = "0.26", features = ["abi3-py312"] }
18-
polyglot-sql = { git = "https://github.com/chio-labs/polyglot", rev = "1307cb6a2ead0bd9bdfe1fb82887f37e42c291bb", default-features = false, features = [
18+
polyglot-sql = { git = "https://github.com/chio-labs/polyglot", rev = "afd2c73a2fe40f59e275aa918264c41ae83cbe94", default-features = false, features = [
1919
"ast-tools",
2020
"builder",
2121
"dialect-bigquery",

‎src/sqlbuild/compiler/compile/_helpers/analysis/cte_facts.py‎

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -456,11 +456,7 @@ def _polyglot_non_null_filter_context(
456456
select: Any,
457457
column_nullability_by_table: dict[str, dict[str, InferredNullability]],
458458
) -> NonNullFilterContext | None:
459-
select_args: object = getattr(select, "args", None)
460-
if not isinstance(select_args, dict):
461-
return None
462-
select_args_dict: dict[str, object] = cast(dict[str, object], select_args)
463-
where_payload: object = select_args_dict.get(_POLYGLOT_PAYLOAD_WHERE_CLAUSE)
459+
where_payload: object = select.arg(_POLYGLOT_PAYLOAD_WHERE_CLAUSE)
464460
if not isinstance(where_payload, dict):
465461
return None
466462
predicate: object = cast(dict[str, object], where_payload).get(_POLYGLOT_PAYLOAD_THIS)
@@ -616,7 +612,7 @@ def _polyglot_star_output_types(
616612

617613

618614
def _polyglot_top_level_ctes(root: Any) -> tuple[tuple[str, Any, bool], ...]:
619-
with_payload: object = getattr(root, "args", {}).get("with")
615+
with_payload: object = root.arg("with")
620616
raw_ctes: object = (
621617
cast(dict[str, object], with_payload).get("ctes")
622618
if isinstance(with_payload, dict)
@@ -678,10 +674,10 @@ def _polyglot_set_operation_output_types(
678674
inference_profile: ExpressionInferenceProfile,
679675
expression_type_resolver: _ExpressionTypeResolver,
680676
) -> dict[str, str]:
681-
if bool(getattr(operation, "args", {}).get("by_name")):
677+
if bool(operation.arg("by_name")):
682678
branch_types: list[dict[str, str]] = []
683679
for side in ("left", "right"):
684-
branch: Any = getattr(operation, "args", {}).get(side)
680+
branch: Any = operation.arg(side)
685681
if branch is None:
686682
return {}
687683
branch_types.append(
@@ -707,7 +703,7 @@ def _polyglot_set_operation_output_types(
707703
}
708704
branch_slots: list[tuple[tuple[str, str | None], ...]] = []
709705
for side in ("left", "right"):
710-
branch: Any = getattr(operation, "args", {}).get(side)
706+
branch: Any = operation.arg(side)
711707
if branch is None:
712708
return {}
713709
inferred: tuple[tuple[str, str | None], ...] = _polyglot_select_output_type_slots(
@@ -751,7 +747,7 @@ def _polyglot_select_output_type_slots(
751747
if str(getattr(select, "kind", "")) in _POLYGLOT_SET_OPERATION_KINDS:
752748
branch_slots: list[tuple[tuple[str, str | None], ...]] = []
753749
for side in ("left", "right"):
754-
branch: Any = getattr(select, "args", {}).get(side)
750+
branch: Any = select.arg(side)
755751
if branch is None:
756752
return ()
757753
slots: tuple[tuple[str, str | None], ...] = _polyglot_select_output_type_slots(
@@ -764,7 +760,7 @@ def _polyglot_select_output_type_slots(
764760
return ()
765761
branch_slots.append(slots)
766762
left_slots, right_slots = branch_slots
767-
if bool(getattr(select, "args", {}).get("by_name")):
763+
if bool(select.arg("by_name")):
768764
right_types: dict[str, str | None] = dict(right_slots)
769765
return tuple(
770766
(
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Compile performance benchmarks
2+
3+
SQLBuild maintains complementary generated projects because model count alone does not predict
4+
compile cost. Every fixture uses neutral orders-style names and synthetic SQL.
5+
6+
## Layered scale guard
7+
8+
The layered guard combines 976 models, 232 sources, 46 seeds, 23 SQL functions, 700 attached
9+
audits, 130 native tests, two hooks, and 54 execution layers. Its model files follow a bounded SQL
10+
size distribution up to roughly 520 KB. The graph contains a 54-layer spine and independent
11+
eight-model chains.
12+
13+
This guard measures cold, unchanged, leaf-model edit, central-model edit, test edit, macro edit,
14+
and project-config edit paths. It protects mixed-resource scaling and cache invalidation while the
15+
separate 3,000/10,000-model and test-heavy guards retain their narrower contracts.
16+
17+
Budgets:
18+
19+
- cold compile: under 7 seconds;
20+
- unchanged warm compile: under 3 seconds;
21+
- one-model, one-test, and one-macro edits: under 4 seconds;
22+
- project-config edit: under 8 seconds.
23+
24+
## Semantic density guard
25+
26+
The semantic guard exercises costs that SQL-size padding cannot represent:
27+
28+
| Characteristic | Generated guard |
29+
| --- | ---: |
30+
| Models | 976 |
31+
| Sources | 232 |
32+
| Seeds | 46 |
33+
| SQL functions | 23 |
34+
| Attached audits | 1,645 |
35+
| Native test cases | 958 |
36+
| Declared model columns | about 33,000 |
37+
| Model SQL | about 5.8 MB |
38+
| Compiled test SQL | about 13 MB |
39+
| Hooks | 2 |
40+
| Execution layers | 54 |
41+
42+
Declared column widths range from three to 538 columns. The SQL uses real expression trees rather
43+
than comment-only padding: casts, conditional expressions, CTEs, derived tables, macros, functions,
44+
seed joins, assertions, and a 267-branch set operation feeding a 270-column aggregate. The large set
45+
operation specifically protects schema validation and CTE type inference from nested-tree
46+
materialization regressions.
47+
48+
The guard records phase timings for cold and unchanged warm compiles. It enforces a 60-second cold
49+
budget and a 12-second warm budget. Resource counts, declared-column count, model SQL bytes, and
50+
compiled-test bytes are asserted so simplifying the fixture cannot make the performance check pass.
51+
52+
Both guards initialize the process-local SQL runtime with an unrelated 32-model project before
53+
measurement. “Cold” means the measured project has no compiler cache or target artifacts; it does
54+
not include one-time lazy initialization noise from the Python test worker. Phase timings are
55+
attribution spans rather than additive buckets.

‎tests/e2e/src/sqlbuild/cli/commands/main/compile/DAGSTER_SHAPED_BENCHMARK.md‎

Lines changed: 0 additions & 44 deletions
This file was deleted.

‎tests/e2e/src/sqlbuild/cli/commands/main/compile/_test_types.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,26 @@ class LayeredProductionCompilePerformanceGuardTestCase:
7070
expected_config_edit_max_seconds: float
7171

7272

73+
@dataclass(frozen=True)
74+
class SemanticCompilePerformanceGuardTestCase:
75+
description: str
76+
model_count: int
77+
source_count: int
78+
seed_count: int
79+
function_count: int
80+
macro_count: int
81+
test_count: int
82+
audit_count: int
83+
expected_min_declared_columns: int
84+
expected_max_declared_columns: int
85+
expected_min_model_sql_bytes: int
86+
expected_max_model_sql_bytes: int
87+
expected_min_compiled_test_bytes: int
88+
expected_max_compiled_test_bytes: int
89+
expected_cold_max_seconds: float
90+
expected_warm_max_seconds: float
91+
92+
7393
@dataclass(frozen=True)
7494
class NamespaceCompileTestCase:
7595
description: str

0 commit comments

Comments
 (0)