Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
469c990
feat(cache): keep a project image beside recent compile reuse slots
kvlonge Oct 18, 2026
887b711
feat(compile): let one compile's analysis and rules stand in for the …
kvlonge Oct 18, 2026
2fae7e5
perf(cache): read a delta compile's stored report only once it is needed
kvlonge Oct 18, 2026
1fb6f8a
feat(compile): compile only changed models and SQL tests against the …
kvlonge Oct 18, 2026
5324f54
perf(compile): decide delta fallbacks from a small image gate before …
kvlonge Oct 18, 2026
ced2ecf
perf(compile): stop copying the rules request and walking every lint …
kvlonge Oct 18, 2026
562cdce
chore(compile): log the files a delta compile considers changed
kvlonge Oct 18, 2026
874e793
feat(compile): compile edits that report or raise errors as deltas
kvlonge Oct 18, 2026
4718eb8
perf(compile): store the project image in the native record format
kvlonge Oct 18, 2026
767cd53
perf(compile): discover only the sources a delta compile's models read
kvlonge Oct 18, 2026
fa61615
perf(compile): cut whole-project work from delta compiles and SQL-tes…
kvlonge Oct 18, 2026
4cda946
perf(compile): keep loaded source YAML for later delta compiles
kvlonge Oct 19, 2026
c5bbfa4
perf(compile): overlap a delta compile's slow reads and writes
kvlonge Oct 19, 2026
08ea1a7
fix(compile): keep every source a delta compile's functions, audits a…
kvlonge Oct 19, 2026
ce2f8d6
feat(compile): add the delta_compile setting and fix recording multi-…
kvlonge Oct 19, 2026
a5fdc09
docs(compile): describe compiling only the changed files after an edit
kvlonge Oct 19, 2026
dda82c5
test(compile): verify seeded edit series of delta compiles end to end
kvlonge Oct 19, 2026
fbbf8f5
docs(compile): note that problems in untouched files keep compiles full
kvlonge Oct 19, 2026
fc2d29b
docs(compile): regenerate the skill reference pages for delta compiles
kvlonge Oct 19, 2026
2b6ea49
style(compile): satisfy clippy in the project image
kvlonge Oct 19, 2026
f80fbd3
refactor(compile): bring the cache, rules and analysis delta code in …
kvlonge Oct 19, 2026
9e71f5d
perf(compile): check a delta's references against the stored upstream…
kvlonge Oct 19, 2026
19ca61b
refactor(compile): bring the delta compile in line with workspace policy
kvlonge Oct 19, 2026
1615168
perf(compile): keep the retained rules rows in the native record format
kvlonge Oct 19, 2026
3ce44f4
fix(compile): make delta compiles print exactly what full compiles print
kvlonge Oct 19, 2026
d63907b
test(compile): guard the delta edit loop on the dense project
kvlonge Oct 19, 2026
ab68fd9
test(compile): tighten the fresh-process, cache and dense compile wal…
kvlonge Oct 19, 2026
023bc9d
test: list delta_compile among the allowed settings in the compiler g…
kvlonge Oct 19, 2026
4fb68ea
fix(compile): log the reuse check's digested file count with its argu…
kvlonge Oct 19, 2026
a4e7f64
fix(compile): keep delta compiles from replaying stale stored macro c…
kvlonge Oct 19, 2026
160031e
fix(compile): match path defaults against every project model in a de…
kvlonge Oct 19, 2026
cb2f077
test(compile): accept a delta compile's fresh analysis in the same-ru…
kvlonge Oct 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,8 @@ jobs:
needs: [changes, lockfile]
if: needs.changes.outputs.performance == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
# The 3k and 10k cells also run the delta edit-loop guard, about five minutes at 10k.
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
Expand All @@ -592,7 +593,7 @@ jobs:
run: uv python install 3.12
- name: Install dependencies
run: uv sync --locked --all-groups --all-extras
- name: Run dense all-rules guard under 4 GiB
- name: Run dense all-rules and delta edit-loop guards under 4 GiB
env:
BENCHMARK_MODELS: ${{ matrix.models }}
MEMORY_LIMIT_BYTES: "4294967296"
Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,17 @@ impl SessionCatalog {
self.schemas.get(name)
}

/// Every closed shape the catalog holds, sorted by name.
pub(crate) fn schema_rows(&self) -> Shapes {
let mut rows: Shapes = self
.schemas
.iter()
.map(|(name, shape)| (name.clone(), shape.clone()))
.collect();
rows.sort_by(|left, right| left.0.cmp(&right.0));
rows
}

/// `(shared members, shared members re-analysed alone)` so far.
pub(crate) fn sharing(&self) -> (usize, usize) {
(self.shared_members, self.reanalysed_members)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::assembly::analysis_session::constants::{UNKNOWN_NULLABILITY, UNKNOWN_
use crate::assembly::analysis_session::models::{
AnalysisSession, ColumnFact, ContractProof, DeferredCleaning, FinishedSession, LineageFacts,
LineageRow, ModelAnalysis, ModelOutcome, ModelRequest, Phase, PivotOutcome, PivotTables,
SessionModelFacts, SessionOutcome, SessionRequest, SessionStep,
RelationTables, SessionModelFacts, SessionOutcome, SessionRequest, SessionStep,
};
use crate::assembly::analysis_session::types::{Pairs, Shapes};
use crate::semantic_validation::models::ProjectCatalog;
Expand All @@ -49,13 +49,18 @@ impl AnalysisSession {
.ok_or_else(|| native_failure(SESSION_CONTEXT, "two analysed models share a name"))?;
let scheduled: Option<Vec<Vec<usize>>> = waves(&producers);
let complete_shapes: ShapeTable = ShapeTable::from_shapes(&request.complete_schemas);
let dependency_ordered: bool = scheduled.is_some()
&& request
.models
.iter()
.flat_map(|model| model.required_names.iter())
.any(|name| complete_shapes.get(name).is_none_or(Vec::is_empty));
let cyclic: bool = scheduled.is_none();
let (dependency_ordered, cyclic): (bool, bool) = match request.forced_order {
Some(order) => order,
None => (
scheduled.is_some()
&& request
.models
.iter()
.flat_map(|model| model.required_names.iter())
.any(|name| complete_shapes.get(name).is_none_or(Vec::is_empty)),
scheduled.is_none(),
),
};
let waves: Vec<Vec<usize>> = scheduled.unwrap_or_default();
Ok(Self {
catalog: SessionCatalog::new(catalog, &request.catalog_schemas),
Expand All @@ -76,6 +81,7 @@ impl AnalysisSession {
publications: Vec::new(),
failures: Vec::new(),
cache: None,
legacy_models: Vec::new(),
request,
})
}
Expand Down Expand Up @@ -118,6 +124,16 @@ impl AnalysisSession {
.into_iter()
.collect::<Option<_>>()
.ok_or("the session left a model unanalysed")?;
let relations: Option<RelationTables> =
self.request.retain_relations.then(|| RelationTables {
types: owned_rows(&self.available_types),
nullability: owned_rows(&self.available_nullability),
complete: owned_rows(&self.complete_shapes),
schemas: self.catalog.schema_rows(),
dependency_ordered: self.dependency_ordered,
cyclic: self.cyclic,
legacy_models: self.legacy_models.clone(),
});
let (schema_additions, analysis_names) = self.catalog.into_changes();
let mut request: SessionRequest = self.request;
for ((outcome, model), pending) in models
Expand Down Expand Up @@ -155,6 +171,7 @@ impl AnalysisSession {
pool,
models: model_facts,
dynamic_proofs,
relations,
};
let outcome = SessionOutcome {
models,
Expand Down Expand Up @@ -254,7 +271,9 @@ impl AnalysisSession {
let legacy: LegacyAnalysis =
self.native_legacy(request, &result.cleaned_sql)
.map_err(|reason| native_failure(&model_context(request), &reason))?;
let name: String = request.name.clone();
self.record_legacy_tables(*model, &mut tables);
self.legacy_models.push(name);
legacy_outcome(legacy, lineage, diagnostics)
}
};
Expand Down Expand Up @@ -669,6 +688,15 @@ fn batch_member<'a>(model: &'a ModelRequest, schema: &'a Shapes) -> BatchMember<
}
}

/// A table's shapes in insertion order, owned.
fn owned_rows(table: &ShapeTable) -> Shapes {
table
.ordered()
.into_iter()
.map(|(name, shape)| (name.to_owned(), shape.clone()))
.collect()
}

/// Python's `{column.name: column.type or "UNKNOWN"}`.
pub(crate) fn shape_columns(columns: &[ColumnFact]) -> Pairs {
dict_from_pairs(columns.iter().map(shape_column))
Expand Down
28 changes: 28 additions & 0 deletions crates/sqlbuild-analysis/src/assembly/analysis_session/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ pub struct FinishedSession {
pub(crate) models: HashMap<String, SessionModelFacts>,
/// Each model's dynamic pivot proof from the session's own relation facts, by model name.
pub(crate) dynamic_proofs: HashMap<String, ContractProof>,
/// The relation facts the session ended with, when the request asked to keep them.
pub(crate) relations: Option<RelationTables>,
}

/// The session's typed result for one compiled model, borrowed from the finished session.
Expand All @@ -121,6 +123,12 @@ pub struct SessionModelAnalysis<'a> {
}

impl FinishedSession {
/// The relation facts the session ended with, when its request asked to keep them.
#[must_use]
pub fn relation_tables(&self) -> Option<&RelationTables> {
self.relations.as_ref()
}

/// The session's typed columns and dynamic proof for model `name`, without re-analysis.
#[must_use]
pub fn model_analysis(&self, name: &str) -> SessionModelAnalysis<'_> {
Expand Down Expand Up @@ -173,6 +181,24 @@ pub struct SessionRequest {
/// Python's `dynamic_families_by_table`, in dict order.
pub dynamic_families_by_table: Vec<(String, Vec<DynamicFamily>)>,
pub models: Vec<ModelRequest>,
/// The whole project's `(dependency ordered, cyclic)`, for a session of only some models.
pub forced_order: Option<(bool, bool)>,
/// Keep the relation facts the session ends with, for the project image.
pub retain_relations: bool,
}

/// The relation facts a finished session ends with, and how it ordered its analysis.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct RelationTables {
pub types: Shapes,
pub nullability: Shapes,
pub complete: Shapes,
/// The binding catalog's closed shapes, sorted by name.
pub schemas: Shapes,
pub dependency_ordered: bool,
pub cyclic: bool,
/// Models whose analysis took the legacy path, which reads every relation's facts.
pub legacy_models: Vec<String>,
}

/// One inferred output column; nullability is Python's `InferredNullability` value.
Expand Down Expand Up @@ -323,6 +349,8 @@ pub struct AnalysisSession {
pub(crate) publications: Shapes,
pub(crate) failures: Vec<String>,
pub(crate) cache: Option<AnalysisCache>,
/// Models whose analysis took the legacy path.
pub(crate) legacy_models: Vec<String>,
}

/// Which of Python's column analyses one query outside the session takes.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ mod analysis_cache;
mod analysis_session;
#[path = "tests/test_contract_proof_json.rs"]
mod contract_proof_json;
#[path = "tests/test_delta_session.rs"]
mod delta_session;
#[path = "tests/test_expression_shapes.rs"]
mod expression_shapes;
#[path = "tests/helpers.rs"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::assembly::analysis_session::models::RelationTables;
use std::collections::HashMap;

use crate::assembly::analysis_session::_helpers::cte_facts::{
Expand Down Expand Up @@ -123,6 +124,8 @@ pub(crate) fn orders_request(models: Vec<ModelRequest>) -> SessionRequest {
nullability_rules: Some(Vec::new()),
nullability_callback: None,
rich_type_inference: true,
forced_order: None,
retain_relations: false,
column_types: raw_orders.clone(),
column_nullability: shapes(&[(
"raw_orders",
Expand Down Expand Up @@ -384,6 +387,7 @@ pub(crate) fn finished_session(tables: PivotTables) -> FinishedSession {
pool: catalog("duckdb", &Vec::new()).analysis_pool(),
models: HashMap::new(),
dynamic_proofs: HashMap::new(),
relations: None,
}
}

Expand Down Expand Up @@ -654,3 +658,30 @@ pub(crate) fn column_view(finished: &ModelColumns) -> ColumnView<'_> {
finished.proven_star,
)
}

/// `request` with `relations`' facts added for every relation it does not analyse.
pub(crate) fn injected_relations(
mut request: SessionRequest,
relations: &RelationTables,
) -> SessionRequest {
let analysed: Vec<String> = request
.models
.iter()
.map(|model| model.name.clone())
.collect();
for (table, facts) in [
(&mut request.column_types, &relations.types),
(&mut request.column_nullability, &relations.nullability),
(&mut request.complete_schemas, &relations.complete),
] {
let present: Vec<String> = table.iter().map(|(name, _)| name.clone()).collect();
table.extend(
facts
.iter()
.filter(|(name, _)| !analysed.contains(name) && !present.contains(name))
.cloned(),
);
}
request.forced_order = Some((relations.dependency_ordered, relations.cyclic));
request
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
use crate::assembly::analysis_session::models::{ModelRequest, RelationTables, SessionRequest};
use crate::assembly::analysis_session::tests::helpers::{
completed, injected_relations, model_requests, orders_request, started,
};
use crate::assembly::analysis_session::tests::test_types::{DeltaSessionTestCase, ModelSpec};

const ORDER_MODELS: [ModelSpec; 3] = [
(
"stg_orders",
"SELECT order_id, amount * 2 AS doubled FROM __source(\"raw_orders\")",
&["raw_orders"],
&[],
),
(
"orders_mart",
"SELECT doubled, order_id + 1 AS next_id FROM __ref(\"stg_orders\")",
&[],
&["stg_orders"],
),
(
"orders_star",
"SELECT * FROM __ref(\"stg_orders\")",
&[],
&["stg_orders"],
),
];

#[test]
fn given_whole_project_relations_when_analysing_one_model_then_it_matches_the_whole_session() {
let test_cases = [
DeltaSessionTestCase {
description: "a model reading another model's columns",
model_index: 1,
expected_whole_session_outcome: true,
},
DeltaSessionTestCase {
description: "a model selecting every column of another model",
model_index: 2,
expected_whole_session_outcome: true,
},
];
let mut whole: SessionRequest = orders_request(model_requests(&ORDER_MODELS));
whole.retain_relations = true;
let (_, whole_outcomes, finished) = completed(started(whole));
let relations: RelationTables = finished.relation_tables().cloned().expect("relations kept");
for test_case in test_cases {
let spec: &ModelSpec = &ORDER_MODELS[test_case.model_index];
let one: Vec<ModelRequest> = model_requests(std::slice::from_ref(spec));
let request: SessionRequest = injected_relations(orders_request(one), &relations);

let (_, outcomes, _) = completed(started(request));

assert_eq!(
outcomes == vec![whole_outcomes[test_case.model_index].clone()],
test_case.expected_whole_session_outcome,
"{}",
test_case.description
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,10 @@ pub(crate) struct ContractProofJsonTestCase {
pub(crate) proof: ContractProof,
pub(crate) expected_json: &'static str,
}

/// One model of a session analysed alone against the whole session's relation facts.
pub(crate) struct DeltaSessionTestCase {
pub(crate) description: &'static str,
pub(crate) model_index: usize,
pub(crate) expected_whole_session_outcome: bool,
}
Loading
Loading