Skip to content

Commit aea4484

Browse files
authored
perf(compile): cut cache-write overhead and fresh compile preparation cost (#882)
* perf(analysis): scan only reference names for qualified relation references * perf(cache): serialise compact lineage rows directly when writing analyses * perf(tests): resolve SQL-test chains without sending model SQL * perf(validation): index function argument relations and column types * test(cache): guard cold compile cache-write overhead against --no-cache * perf(validation): precompute argument relations and folded column types per model * test: express cache encoding and qualifier scan cases as expected fields * test(cache): relax cold-relative warm and edit ratios and split cache-write overhead limits
1 parent 7400a48 commit aea4484

15 files changed

Lines changed: 541 additions & 64 deletions

File tree

‎crates/sqlbuild-rules-native/src/compiler/_helpers/sql_tests/planning.rs‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,19 @@ struct PlanBatchRequest {
7777
#[derive(Debug, Deserialize)]
7878
#[serde(rename_all = "camelCase")]
7979
struct ChainBatchRequest {
80-
models: Vec<ModelInput>,
80+
models: Vec<ChainModelInput>,
8181
tests: Vec<TestInput>,
8282
}
8383

84+
/// Chain ordering reads only declared dependencies, so chain requests omit model SQL.
85+
#[derive(Debug, Deserialize)]
86+
#[serde(rename_all = "camelCase")]
87+
struct ChainModelInput {
88+
name: String,
89+
#[serde(default)]
90+
model_dependencies: Vec<String>,
91+
}
92+
8493
#[derive(Debug, Serialize)]
8594
#[serde(rename_all = "camelCase")]
8695
struct ChainBatchResponse {
@@ -529,7 +538,7 @@ pub(crate) fn resolve_chains_json(request_json: &str) -> Result<String, String>
529538
(
530539
model.name,
531540
ModelInputOwned {
532-
query_sql: model.query_sql,
541+
query_sql: String::new(),
533542
model_dependencies: model.model_dependencies,
534543
},
535544
)

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

Lines changed: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@
5656
_COMPACT_CONFIDENCE_CODES: dict[str, int] = {
5757
CompactLineageFacts.confidence(code).value: code for code in range(3)
5858
}
59+
_COMPACT_TRANSFORM_VALUES: tuple[str, ...] = tuple(_COMPACT_TRANSFORM_CODES)
60+
_COMPACT_CONFIDENCE_VALUES: tuple[str, ...] = tuple(_COMPACT_CONFIDENCE_CODES)
5961
_RESOURCE_TYPE_VALUES: frozenset[str] = frozenset(item.value for item in CompiledResourceType)
6062
_NULLABILITY_BY_VALUE: dict[str, InferredNullability] = {
6163
item.value: item for item in InferredNullability
@@ -586,17 +588,31 @@ def write_model_analyses(
586588
) -> None:
587589
"""Transactionally persist deterministic analyses; cache failures never fail compilation."""
588590

591+
signed_analyses: dict[int, PolyglotAnalysisResult] = {
592+
id(analysis): analysis
593+
for analysis in (*analyses_by_key.values(), *(latest_analyses_by_model or {}).values())
594+
}
595+
output_signatures: dict[int, str] = {
596+
key: model_analysis_output_signature(analysis) for key, analysis in signed_analyses.items()
597+
}
589598
rows: list[tuple[str, str]] = []
590599
for cache_key, analysis in analyses_by_key.items():
591-
contents: str = _analysis_contents(cache_key=cache_key, analysis=analysis)
600+
try:
601+
contents: str = _analysis_contents(
602+
cache_key=cache_key,
603+
analysis=analysis,
604+
output_signature=output_signatures[id(analysis)],
605+
)
606+
except orjson.JSONEncodeError:
607+
continue
592608
if len(contents.encode()) <= _MAX_CACHE_ENTRY_BYTES:
593609
rows.append((cache_key, contents))
594610
signature_rows: list[tuple[str, str, str, str]] = [
595611
(
596612
context.shared_fingerprint,
597613
context.signature_namespace,
598614
model_name,
599-
model_analysis_output_signature(analysis),
615+
output_signatures[id(analysis)],
600616
)
601617
for model_name, analysis in (latest_analyses_by_model or {}).items()
602618
]
@@ -793,20 +809,26 @@ def _referenced_analysis_tables(
793809
return tuple(sorted(names))
794810

795811

796-
def _analysis_payload(*, cache_key: str, analysis: PolyglotAnalysisResult) -> dict[str, object]:
812+
def _analysis_payload(
813+
*, cache_key: str, analysis: PolyglotAnalysisResult, output_signature: str | None = None
814+
) -> dict[str, object]:
797815
return {
798816
"v": _ANALYSIS_CACHE_VERSION,
799817
"k": cache_key,
800818
"a": analysis.analysis_succeeded,
801-
"s": model_analysis_output_signature(analysis),
819+
"s": (
820+
model_analysis_output_signature(analysis)
821+
if output_signature is None
822+
else output_signature
823+
),
802824
"c": (
803825
None
804826
if analysis.columns is None
805827
else [
806828
[column.name, column.type, column.nullability.value] for column in analysis.columns
807829
]
808830
),
809-
"l": [_lineage_column_payload(column) for column in analysis.lineage_columns],
831+
"l": _lineage_payload(analysis.lineage_columns),
810832
"h": analysis.has_star,
811833
"sr": analysis.star_resolved,
812834
"b": [
@@ -825,6 +847,39 @@ def _analysis_payload(*, cache_key: str, analysis: PolyglotAnalysisResult) -> di
825847
}
826848

827849

850+
def _lineage_payload(lineage_columns: Sequence[CompiledLineageColumnFact]) -> list[list[object]]:
851+
if isinstance(lineage_columns, CompactLineageFacts):
852+
compact: list[list[object]] | None = _compact_lineage_payload(lineage_columns)
853+
if compact is not None:
854+
return compact
855+
return [_lineage_column_payload(column) for column in lineage_columns]
856+
857+
858+
def _compact_lineage_payload(lineage: CompactLineageFacts) -> list[list[object]] | None:
859+
"""Serialise indexed rows without projecting facts; None defers to the validating projection."""
860+
861+
pool: tuple[str, ...] = lineage.string_pool
862+
payload: list[list[object]] = []
863+
for name_index, transform_code, confidence_code, sources in lineage.rows:
864+
upstream_columns: list[list[str]] = []
865+
for resource_type_index, resource_name_index, column_index in sources:
866+
resource_type: str = pool[resource_type_index]
867+
if resource_type not in _RESOURCE_TYPE_VALUES:
868+
return None
869+
upstream_columns.append(
870+
[resource_type, lineage.resource_name(resource_name_index), pool[column_index]]
871+
)
872+
payload.append(
873+
[
874+
pool[name_index],
875+
_COMPACT_TRANSFORM_VALUES[transform_code],
876+
_COMPACT_CONFIDENCE_VALUES[confidence_code],
877+
upstream_columns,
878+
]
879+
)
880+
return payload
881+
882+
828883
def _lineage_column_payload(column: CompiledLineageColumnFact) -> list[object]:
829884
return [
830885
column.output_column,
@@ -1054,13 +1109,15 @@ def _cache_database_path(*, context: AnalysisCacheContext) -> Path:
10541109
return context.root / f"v{_ANALYSIS_CACHE_VERSION}" / _CACHE_DATABASE_NAME
10551110

10561111

1057-
def _analysis_contents(*, cache_key: str, analysis: PolyglotAnalysisResult) -> str:
1058-
serialized_payload: str = json.dumps(
1059-
_analysis_payload(cache_key=cache_key, analysis=analysis),
1060-
ensure_ascii=False,
1061-
separators=(",", ":"),
1062-
sort_keys=True,
1063-
)
1112+
def _analysis_contents(
1113+
*, cache_key: str, analysis: PolyglotAnalysisResult, output_signature: str | None = None
1114+
) -> str:
1115+
serialized_payload: str = orjson.dumps(
1116+
_analysis_payload(
1117+
cache_key=cache_key, analysis=analysis, output_signature=output_signature
1118+
),
1119+
option=orjson.OPT_SORT_KEYS,
1120+
).decode()
10641121
return _CACHE_ENTRY_SEPARATOR.join(
10651122
(
10661123
_cache_entry_digest(cache_key=cache_key, serialized_payload=serialized_payload),

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,11 @@
157157
r"(?:\s|--[^\r\n]*(?:\r?\n|$)|/\*.*?\*/)*\.",
158158
re.DOTALL,
159159
)
160+
_QUALIFIED_SCAN_CONSUMING_TOKENS: tuple[str, ...] = ('"', "`", "[", "--", "/*")
161+
_PLAIN_IDENTIFIER_PATTERN: re.Pattern[str] = re.compile(r"[a-z_$][a-z0-9_$]*")
162+
_IDENTIFIER_CHARACTERS: frozenset[str] = frozenset(
163+
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
164+
)
160165

161166

162167
def _infer_columns_with_polyglot(
@@ -944,6 +949,8 @@ def _replace_refs_with_stubs(
944949

945950

946951
def _qualified_reference_names(*, query_sql: str, reference_names: Iterable[str]) -> frozenset[str]:
952+
"""Return qualifier reference names, searching names directly when no token can hide one."""
953+
947954
if SQL_QUALIFIER_SEPARATOR_TOKEN not in query_sql:
948955
return frozenset()
949956
names_by_normalized: dict[str, list[str]] = {}
@@ -952,7 +959,42 @@ def _qualified_reference_names(*, query_sql: str, reference_names: Iterable[str]
952959
names_by_normalized.setdefault(name.casefold(), []).append(name)
953960
if not names_by_normalized:
954961
return frozenset()
962+
if not query_sql.isascii() or any(
963+
token in query_sql for token in _QUALIFIED_SCAN_CONSUMING_TOKENS
964+
):
965+
return _scanned_qualified_reference_names(
966+
query_sql=query_sql, names_by_normalized=names_by_normalized
967+
)
968+
969+
folded_sql: str = query_sql.lower()
970+
qualified: set[str] = set()
971+
for normalized, names in names_by_normalized.items():
972+
if _PLAIN_IDENTIFIER_PATTERN.fullmatch(normalized) is not None and _is_qualifier(
973+
folded_sql=folded_sql, name=normalized
974+
):
975+
qualified.update(names)
976+
return frozenset(qualified)
977+
978+
979+
def _is_qualifier(*, folded_sql: str, name: str) -> bool:
980+
length: int = len(folded_sql)
981+
start: int = folded_sql.find(name)
982+
while start != -1:
983+
end: int = start + len(name)
984+
if (start == 0 or folded_sql[start - 1] not in _IDENTIFIER_CHARACTERS) and (
985+
end == length or folded_sql[end] not in _IDENTIFIER_CHARACTERS
986+
):
987+
while end < length and folded_sql[end].isspace():
988+
end += 1
989+
if end < length and folded_sql[end] == SQL_QUALIFIER_SEPARATOR_TOKEN:
990+
return True
991+
start = folded_sql.find(name, start + 1)
992+
return False
993+
955994

995+
def _scanned_qualified_reference_names(
996+
*, query_sql: str, names_by_normalized: dict[str, list[str]]
997+
) -> frozenset[str]:
956998
qualified: set[str] = set()
957999
for match in _QUALIFIED_IDENTIFIER_PATTERN.finditer(query_sql):
9581000
identifier: str = next(value for value in match.groups() if value is not None)

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

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,20 @@ def get_semantic_metadata_diagnostics(
6666
if not project.settings.sql_analysis:
6767
return ()
6868
shapes: dict[str, dict[str, str]] = semantic_shapes(project=project, profile=profile)
69+
functions: dict[str, CompiledFunction] = {
70+
function.name.casefold(): function for function in project.functions
71+
}
6972
diagnostics: list[CompilerDiagnostic] = []
7073
for model in project.models:
7174
if model.config.values.get("sql_analysis") is False or not model.binding_validated:
7275
continue
7376
diagnostics.extend(
74-
_function_errors(model=model, project=project, shapes=shapes, profile=profile)
77+
_function_errors(
78+
model=model,
79+
functions=functions,
80+
shapes=shapes,
81+
profile=profile,
82+
)
7583
)
7684
shape: dict[str, str] | None = shapes.get(model.name)
7785
if shape is None:
@@ -431,13 +439,10 @@ def _names(value: object) -> tuple[str, ...]:
431439
def _function_errors(
432440
*,
433441
model: CompiledModel,
434-
project: CompiledProject,
442+
functions: dict[str, CompiledFunction],
435443
shapes: dict[str, dict[str, str]],
436444
profile: ExpressionInferenceProfile,
437445
) -> tuple[CompilerDiagnostic, ...]:
438-
functions: dict[str, CompiledFunction] = {
439-
function.name.casefold(): function for function in project.functions
440-
}
441446
if not functions or not any(
442447
reference.ref_kind in {SqlReferenceKind.UDF, SqlReferenceKind.TABLE_FUNCTION}
443448
for reference in model.references
@@ -454,13 +459,28 @@ def _function_errors(
454459
parsed: Any = module.parse_one(request.sql, dialect=profile.sql_analysis_dialect)
455460
except module.PolyglotError:
456461
return ()
462+
calls: list[tuple[Any, Any, dict[str, Any], CompiledFunction]] = list(
463+
_declared_function_calls(root=parsed, functions=functions)
464+
)
465+
column_selects: dict[int, Any] = {
466+
id(select): select
467+
for _, select, payload, function in calls
468+
if select is not None and _resolves_argument_columns(payload=payload, function=function)
469+
}
470+
relations_by_select: dict[int, list[dict[str, Any]]] = {
471+
key: _select_relations(select=select) for key, select in column_selects.items()
472+
}
473+
table_names: set[str] = set()
474+
for relations in relations_by_select.values():
475+
table_names.update(map(_relation_table_name, relations))
476+
folded_shapes: dict[str, dict[str, list[str]]] = {
477+
table_name: _folded_columns(shapes[table_name])
478+
for table_name in table_names
479+
if table_name in shapes
480+
}
457481
diagnostics: list[CompilerDiagnostic] = []
458-
for call, select in _function_scopes(parsed):
459-
payload: dict[str, Any] = call.to_dict().get("function", {})
482+
for call, select, payload, function in calls:
460483
name: str = str(payload.get("name", "")).removeprefix("__sqlbuild_udf_")
461-
function: CompiledFunction | None = functions.get(name.casefold())
462-
if function is None:
463-
continue
464484
arguments: list[dict[str, Any]] = payload.get("args", [])
465485
if len(arguments) != len(function.arguments):
466486
diagnostics.append(
@@ -490,7 +510,11 @@ def _function_errors(
490510
literal.get("literal_type"), "UNKNOWN"
491511
)
492512
elif column:
493-
actual = _argument_column_type(column=column, select=select, shapes=shapes)
513+
actual = _argument_column_type(
514+
column=column,
515+
relations=None if select is None else relations_by_select[id(select)],
516+
folded_shapes=folded_shapes,
517+
)
494518
if actual != _UNKNOWN_TYPE and _different_argument_families(
495519
left=actual, right=declaration.type, profile=profile
496520
):
@@ -510,6 +534,26 @@ def _function_errors(
510534
return tuple(diagnostics)
511535

512536

537+
def _declared_function_calls(
538+
*, root: Any, functions: dict[str, CompiledFunction]
539+
) -> Iterator[tuple[Any, Any, dict[str, Any], CompiledFunction]]:
540+
"""Yield calls of declared functions with their nearest select and serialised payload."""
541+
542+
for call, select in _function_scopes(root):
543+
payload: dict[str, Any] = call.to_dict().get("function", {})
544+
name: str = str(payload.get("name", "")).removeprefix("__sqlbuild_udf_")
545+
function: CompiledFunction | None = functions.get(name.casefold())
546+
if function is not None:
547+
yield call, select, payload, function
548+
549+
550+
def _resolves_argument_columns(*, payload: dict[str, Any], function: CompiledFunction) -> bool:
551+
arguments: list[dict[str, Any]] = payload.get("args", [])
552+
return len(arguments) == len(function.arguments) and any(
553+
not argument.get("literal") and argument.get("column") for argument in arguments
554+
)
555+
556+
513557
def _function_scopes(root: Any) -> Iterator[tuple[Any, Any]]:
514558
pending: list[tuple[Any, Any]] = [(root, None)]
515559
node: Any
@@ -523,29 +567,48 @@ def _function_scopes(root: Any) -> Iterator[tuple[Any, Any]]:
523567
pending.extend((child, select) for child in reversed(node.children()))
524568

525569

526-
def _argument_column_type(
527-
*, column: dict[str, Any], select: Any, shapes: dict[str, dict[str, str]]
528-
) -> str:
529-
if select is None:
530-
return _UNKNOWN_TYPE
570+
def _select_relations(*, select: Any) -> list[dict[str, Any]]:
531571
payload: dict[str, Any] = select.to_dict().get("select", {})
532572
relations: list[dict[str, Any]] = list((payload.get("from") or {}).get("expressions", []))
533573
relations.extend(join.get("this", {}) for join in payload.get("joins", []))
574+
return relations
575+
576+
577+
def _relation_table_name(relation: dict[str, Any]) -> str:
578+
return str(relation.get("table", {}).get("name", {}).get("name", ""))
579+
580+
581+
def _folded_columns(shape: dict[str, str]) -> dict[str, list[str]]:
582+
"""Index column types by case-folded name, preserving declaration order per name."""
583+
584+
folded: dict[str, list[str]] = {}
585+
for key, value in shape.items():
586+
folded.setdefault(key.casefold(), []).append(value)
587+
return folded
588+
589+
590+
def _argument_column_type(
591+
*,
592+
column: dict[str, Any],
593+
relations: list[dict[str, Any]] | None,
594+
folded_shapes: dict[str, dict[str, list[str]]],
595+
) -> str:
596+
if relations is None:
597+
return _UNKNOWN_TYPE
534598
qualifier: str | None = (column.get("table") or {}).get("name")
535599
name: str = str(column.get("name", {}).get("name", ""))
600+
folded_name: str = name.casefold()
536601
candidates: list[str] = []
537602
for relation in relations:
538603
table: dict[str, Any] = relation.get("table", {})
539-
table_name: str = str(table.get("name", {}).get("name", ""))
604+
table_name: str = _relation_table_name(relation)
540605
alias: str = str((table.get("alias") or {}).get("name", table_name))
541606
if qualifier is not None and qualifier.casefold() != alias.casefold():
542607
continue
543-
shape: dict[str, str] | None = shapes.get(table_name)
608+
shape: dict[str, list[str]] | None = folded_shapes.get(table_name)
544609
if shape is None:
545610
return _UNKNOWN_TYPE
546-
candidates.extend(
547-
value for key, value in shape.items() if key.casefold() == name.casefold()
548-
)
611+
candidates.extend(shape.get(folded_name, ()))
549612
return candidates[0] if len(candidates) == 1 else _UNKNOWN_TYPE
550613

551614

0 commit comments

Comments
 (0)