Skip to content

Commit 7cbc6bf

Browse files
committed
wip: changed builds through CLI and scheduler with adapter clocks and docs
Consumption I/O moves behind a native-host edge, scheduled jobs accept the changed setting and report skipped models as successful, the janitor prunes the table, and the build docs describe --changed. Validated locally: make check-ci, cargo tests for sqlbuild-state, DuckDB and SQLite end-to-end CLI, crash and scheduler tests, and manual PostgreSQL, ClickHouse and SQL Server recorded_at checks. Not yet reviewed; timings pending.
1 parent 601b8cf commit 7cbc6bf

41 files changed

Lines changed: 870 additions & 121 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Makefile‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ E2E_DUCKDB_BUILD_INCREMENTAL_PATHS := \
145145
tests/e2e/src/sqlbuild/cli/commands/main/build/own_change_replay \
146146
tests/e2e/src/sqlbuild/cli/commands/main/build/test_append_cursor_build.py \
147147
tests/e2e/src/sqlbuild/cli/commands/main/build/test_capped_microbatch_build.py \
148+
tests/e2e/src/sqlbuild/cli/commands/main/build/test_changed_builds.py \
148149
tests/e2e/src/sqlbuild/cli/commands/main/build/test_concurrent_microbatch_build.py \
149150
tests/e2e/src/sqlbuild/cli/commands/main/build/test_cursor_runtime_failures.py \
150151
tests/e2e/src/sqlbuild/cli/commands/main/build/test_dropped_relation_rebuild.py \
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
MODEL (description "Orders per customer, two models downstream of both sources.", materialized table);
2+
SELECT c.customer_id, count(o.order_id) AS orders
3+
FROM __ref("stg_customers") AS c
4+
LEFT JOIN __ref("stg_orders") AS o USING (customer_id)
5+
GROUP BY c.customer_id
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
MODEL (description "Staged customers.", materialized table);
2+
SELECT customer_id FROM __source("raw_customers")
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
MODEL (description "Staged orders.", materialized table);
2+
SELECT order_id, customer_id FROM __source("raw_orders")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
sources:
2+
- name: raw_orders
3+
description: Synthetic orders, numbered by load batch.
4+
schema: raw
5+
table: orders
6+
freshness:
7+
strategy: column
8+
column: load_id
9+
type: integer
10+
- name: raw_customers
11+
description: Synthetic customers, numbered by load batch.
12+
schema: raw
13+
table: customers
14+
freshness:
15+
strategy: column
16+
column: load_id
17+
type: integer

‎crates/sqlbuild-python/src/bindings/_helpers/build_command/execution.rs‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ use crate::bindings::_helpers::cost_telemetry::context::CostContext;
3838
use crate::bindings::_helpers::cursor::overrides::{cursor_integer, cursor_timestamp};
3939
use crate::bindings::_helpers::freshness::publication::append_freshness_records;
4040
use crate::bindings::_helpers::graph::python_node_graph::python_node_graph;
41+
use crate::bindings::_helpers::native_host::consumption_state::recorder_callbacks;
4142
use crate::bindings::_helpers::native_host::host;
4243
use crate::bindings::_helpers::native_host::results::ReadResult;
4344
use crate::bindings::_helpers::planner_resolution::cursor_overrides::type_cursor_overrides;
@@ -387,6 +388,7 @@ pub(crate) fn execute_build_plan<'py>(
387388
)?;
388389
report_changed_decision(context, &prepared)?;
389390
let recorder: &Bound<'py, ConsumptionRecorder> = &prepared.recorder;
391+
let (on_node_complete, before_node_write) = recorder_callbacks(recorder.as_any())?;
390392
let connection: &Bound<'py, PyAny> = &preparation.connection_progress;
391393
let build_callbacks: Bound<'py, PyAny> = call_with(
392394
&host::entry(py, ExecutorHost::BuildCallbacks)?,
@@ -396,8 +398,8 @@ pub(crate) fn execute_build_plan<'py>(
396398
"on_node_start",
397399
keyword_relay(native_field(callbacks, "on_node_start")?, &NODE_START)?,
398400
),
399-
("on_node_complete", recorder.getattr("on_node_complete")?),
400-
("before_node_write", recorder.getattr("before_node_write")?),
401+
("on_node_complete", on_node_complete),
402+
("before_node_write", before_node_write),
401403
(
402404
"on_sub_progress",
403405
native_field(callbacks, "on_sub_progress")?,
@@ -497,6 +499,14 @@ fn report_changed_decision(context: &BuildContext<'_>, prepared: &Prepared<'_>)
497499
let Some(decision) = &prepared.decision else {
498500
return Ok(());
499501
};
502+
if let Some(worker) = &context.worker {
503+
let skipped: Vec<String> = decision
504+
.skipped
505+
.iter()
506+
.map(|key| format!("{}:{}", key.kind, key.name))
507+
.collect();
508+
worker.unchanged(context.py(), &skipped)?;
509+
}
500510
emit(
501511
&context.stream,
502512
&format!(

‎crates/sqlbuild-python/src/bindings/_helpers/cli/registration.rs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ use crate::bindings::_helpers::cli::{
88
};
99
use crate::bindings::_helpers::{
1010
audit_command, build_command, build_progress, change_selection, check, clone, command_runtime,
11-
compute_logs, consumption, contract_command, cost_command, cost_report, cost_telemetry,
12-
dbt_command, dbt_events, dbt_interop, debug_command, diff_report, event_exporting,
13-
execution_history, format_command, freshness, init_command, janitor, load, output_capture,
14-
plan_command, presentation_text, query_command, recall, result_output, review_command,
15-
scenario_command, seed_command, sql_test_command, workspace_commands,
11+
compute_logs, contract_command, cost_command, cost_report, cost_telemetry, dbt_command,
12+
dbt_events, dbt_interop, debug_command, diff_report, event_exporting, execution_history,
13+
format_command, freshness, init_command, janitor, load, output_capture, plan_command,
14+
presentation_text, query_command, recall, result_output, review_command, scenario_command,
15+
seed_command, sql_test_command, workspace_commands,
1616
};
1717

1818
pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
@@ -28,7 +28,7 @@ pub(crate) fn register(module: &Bound<'_, PyModule>) -> PyResult<()> {
2828
janitor::registration::register(module)?;
2929
plan_command::registration::register(module)?;
3030
freshness::registration::register(module)?;
31-
consumption::state_io::register(module)?;
31+
crate::bindings::_helpers::native_host::consumption_state::register(module)?;
3232
clone::registration::register(module)?;
3333
compute_logs::registration::register(module)?;
3434
execution_history::registration::register(module)?;

‎crates/sqlbuild-python/src/bindings/_helpers/compiled_project/snapshot_execution.rs‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ impl WorkerBuild {
9393
Ok(())
9494
}
9595

96+
/// Nodes `build --changed` leaves unbuilt are up to date, as a planner `no_change` skip is.
97+
pub(crate) fn unchanged(&self, py: Python<'_>, keys: &[String]) -> PyResult<()> {
98+
for key in keys {
99+
self.complete(py, key, (NodeOutcome::Success, None, None))?;
100+
}
101+
Ok(())
102+
}
103+
96104
fn complete(
97105
&self,
98106
py: Python<'_>,
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
pub(crate) mod facts;
22
pub(crate) mod recorder;
33
pub(crate) mod relation_reads;
4-
pub(crate) mod state_io;

‎crates/sqlbuild-python/src/bindings/_helpers/consumption/recorder.rs‎

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::sync::Mutex;
55
use std::time::{Duration, Instant};
66

77
use pyo3::exceptions::PyRuntimeError;
8-
use pyo3::prelude::{Bound, Py, PyAny, PyAnyMethods, PyResult, Python};
8+
use pyo3::prelude::{Bound, PyAny, PyAnyMethods, PyResult, Python};
99
use pyo3::{pyclass, pymethods};
1010
use sqlbuild_state::consumption::constants::RECENT_EXECUTIONS;
1111
use sqlbuild_state::consumption::main::changed_run_set::changed_run_set;
@@ -20,8 +20,8 @@ use sqlbuild_state::consumption::models::{
2020
use uuid::Uuid;
2121

2222
use crate::bindings::_helpers::consumption::facts::{BuildFacts, build_facts};
23-
use crate::bindings::_helpers::consumption::state_io::{
24-
Location, append, ensure_table, read_recent,
23+
use crate::bindings::_helpers::native_host::consumption_state::{
24+
ConsumptionConnection, Location, NodeCompleteHook,
2525
};
2626
use crate::bindings::_helpers::records::fields::field as native_field;
2727

@@ -33,6 +33,7 @@ const MODEL_RESULT: &str = "model_name";
3333
const SEED_RESULT: &str = "seed_name";
3434
const SUCCESS: &str = "success";
3535
const ERROR_OUTCOME: &str = "error";
36+
const SEED_KIND: &str = "seed";
3637

3738
/// The mutable side of one build's recording.
3839
struct Recording {
@@ -44,17 +45,29 @@ struct Recording {
4445
warnings: Vec<String>,
4546
}
4647

48+
impl Default for Recording {
49+
fn default() -> Self {
50+
Self {
51+
pending: BTreeMap::new(),
52+
pending_executions: 0,
53+
pending_seeds: Vec::new(),
54+
tables: BTreeSet::new(),
55+
last_flush: Instant::now(),
56+
warnings: Vec::new(),
57+
}
58+
}
59+
}
60+
4761
/// Appends `started` rows before a build and `succeeded` rows as its models and seeds complete.
4862
#[pyclass(module = "sqlbuild._native", frozen)]
4963
pub(crate) struct ConsumptionRecorder {
50-
adapter: Py<PyAny>,
51-
connection: Py<PyAny>,
64+
connection: ConsumptionConnection,
5265
run_id: String,
5366
nodes: HashMap<NodeKey, NodeFacts>,
5467
history: ConsumptionHistory,
5568
executions: HashMap<NodeKey, String>,
5669
failed_start: HashMap<NodeKey, String>,
57-
base_on_node_complete: Py<PyAny>,
70+
base_on_node_complete: NodeCompleteHook,
5871
recording: Mutex<Recording>,
5972
}
6073

@@ -86,36 +99,38 @@ impl ConsumptionRecorder {
8699
.map_err(|_| PyRuntimeError::new_err("consumption recorder state is poisoned"))
87100
}
88101

89-
/// Queue the succeeded rows of one execution that wrote its relation.
90-
fn queue(&self, recording: &mut Recording, key: &NodeKey) {
102+
/// The succeeded rows of one execution that wrote its relation, with their schema.
103+
fn succeeded(&self, key: &NodeKey) -> Option<(Location, Vec<ConsumptionAppend>)> {
91104
let (Some(node), Some(execution_id)) = (self.nodes.get(key), self.executions.get(key))
92105
else {
93-
return;
106+
return None;
94107
};
95108
let inputs = consumed_inputs(node, (&self.nodes, &self.history), &self.executions);
109+
Some((
110+
Location::of(&node.relation),
111+
succeeded_appends(node, (execution_id, &self.run_id), &inputs),
112+
))
113+
}
114+
115+
/// The recording with `key`'s succeeded rows queued.
116+
fn queued(&self, mut recording: Recording, key: &NodeKey) -> Recording {
117+
if let Some((location, rows)) = self.succeeded(key) {
118+
recording.pending.entry(location).or_default().extend(rows);
119+
recording.pending_executions += 1;
120+
}
96121
recording
97-
.pending
98-
.entry(Location::of(&node.relation))
99-
.or_default()
100-
.extend(succeeded_appends(
101-
node,
102-
(execution_id, &self.run_id),
103-
&inputs,
104-
));
105-
recording.pending_executions += 1;
106122
}
107123

108-
/// Append every pending row; a failure is reported once, and its executions simply run again.
109-
fn flush(&self, py: Python<'_>, recording: &mut Recording) {
110-
let adapter: &Bound<'_, PyAny> = self.adapter.bind(py);
111-
let connection: &Bound<'_, PyAny> = self.connection.bind(py);
124+
/// The recording after appending every pending row; a failure is reported, and its executions run again.
125+
fn flushed(&self, py: Python<'_>, mut recording: Recording) -> Recording {
112126
for (location, rows) in std::mem::take(&mut recording.pending) {
113-
let written: PyResult<()> = (|| {
114-
if recording.tables.insert(location.clone()) {
115-
ensure_table(adapter, connection, &location)?;
116-
}
117-
append(adapter, connection, &location, &rows)
118-
})();
127+
let created: bool = !recording.tables.insert(location.clone());
128+
let written: PyResult<()> = (if created {
129+
Ok(())
130+
} else {
131+
self.connection.ensure_table(py, &location)
132+
})
133+
.and_then(|()| self.connection.append(py, &location, &rows));
119134
if let Err(error) = written {
120135
recording.warnings.push(format!(
121136
"could not record consumed inputs in schema {}: {error}; those models and seeds run again under --changed",
@@ -125,6 +140,7 @@ impl ConsumptionRecorder {
125140
}
126141
recording.pending_executions = 0;
127142
recording.last_flush = Instant::now();
143+
recording
128144
}
129145

130146
/// Queue the seeds whose error audits passed, flush everything and close the connection.
@@ -145,12 +161,11 @@ impl ConsumptionRecorder {
145161
let mut recording = self.lock()?;
146162
for seed in std::mem::take(&mut recording.pending_seeds) {
147163
if !failed_audits.contains(&seed.name) {
148-
self.queue(&mut recording, &seed);
164+
*recording = self.queued(std::mem::take(&mut *recording), &seed);
149165
}
150166
}
151-
self.flush(py, &mut recording);
152-
let adapter: &Bound<'_, PyAny> = self.adapter.bind(py);
153-
let closed = adapter.call_method1("close", (self.connection.bind(py),));
167+
*recording = self.flushed(py, std::mem::take(&mut *recording));
168+
let closed = self.connection.close(py);
154169
let warnings: Vec<String> = std::mem::take(&mut recording.warnings);
155170
closed?;
156171
Ok(warnings)
@@ -161,7 +176,7 @@ impl ConsumptionRecorder {
161176
impl ConsumptionRecorder {
162177
/// Forward a finished node, then queue a successful model, or a seed until its audits are known.
163178
fn on_node_complete(&self, py: Python<'_>, node_result: Bound<'_, PyAny>) -> PyResult<()> {
164-
self.base_on_node_complete.bind(py).call1((&node_result,))?;
179+
self.base_on_node_complete.forward(&node_result)?;
165180
let Some((kind, name)) = kind_of(&node_result)? else {
166181
return Ok(());
167182
};
@@ -173,15 +188,15 @@ impl ConsumptionRecorder {
173188
name,
174189
};
175190
let mut recording = self.lock()?;
176-
if kind == "seed" {
191+
if kind == SEED_KIND {
177192
recording.pending_seeds.push(key);
178193
return Ok(());
179194
}
180-
self.queue(&mut recording, &key);
195+
*recording = self.queued(std::mem::take(&mut *recording), &key);
181196
if recording.pending_executions >= FLUSH_EXECUTIONS
182197
|| recording.last_flush.elapsed() >= FLUSH_INTERVAL
183198
{
184-
self.flush(py, &mut recording);
199+
*recording = self.flushed(py, std::mem::take(&mut *recording));
185200
}
186201
Ok(())
187202
}
@@ -213,8 +228,7 @@ impl ConsumptionRecorder {
213228
}
214229
}
215230

216-
/// The locations whose records this build reads: all of them under `--changed`, else the
217-
/// relations of unselected upstreams whose last execution the selected nodes record.
231+
/// Schemas to read records from: all under `--changed`, else those of unselected upstreams.
218232
fn read_locations(facts: &BuildFacts, changed: bool) -> BTreeSet<Location> {
219233
let by_key: HashMap<&NodeKey, &NodeFacts> =
220234
facts.nodes.iter().map(|node| (&node.key, node)).collect();
@@ -254,11 +268,11 @@ pub(crate) fn prepare<'py>(
254268
) -> PyResult<Prepared<'py>> {
255269
let py: Python<'py> = plan.py();
256270
let facts: BuildFacts = build_facts((plan, project), (changed, full_refresh))?;
257-
let connection: Bound<'py, PyAny> = adapter.call_method1("connect", (connection_config,))?;
271+
let connection = ConsumptionConnection::open(adapter, connection_config)?;
258272
let mut warnings: Vec<String> = Vec::new();
259273
let mut rows: Vec<ConsumptionRow> = Vec::new();
260274
for location in read_locations(&facts, changed) {
261-
match read_recent(adapter, &connection, &location) {
275+
match connection.read_recent(py, &location) {
262276
Ok(read) => rows.extend(read),
263277
Err(error) => warnings.push(format!(
264278
"could not read consumed inputs in schema {}: {error}; its models and seeds count as changed",
@@ -312,8 +326,9 @@ pub(crate) fn prepare<'py>(
312326
let mut failed_start: HashMap<NodeKey, String> = HashMap::new();
313327
let mut tables: BTreeSet<Location> = BTreeSet::new();
314328
for (location, rows) in started {
315-
let written: PyResult<()> = ensure_table(adapter, &connection, &location)
316-
.and_then(|()| append(adapter, &connection, &location, &rows));
329+
let written: PyResult<()> = connection
330+
.ensure_table(py, &location)
331+
.and_then(|()| connection.append(py, &location, &rows));
317332
match written {
318333
Ok(()) => {
319334
tables.insert(location);
@@ -327,21 +342,16 @@ pub(crate) fn prepare<'py>(
327342
}
328343
}
329344
let recorder = ConsumptionRecorder {
330-
adapter: adapter.clone().unbind(),
331-
connection: connection.unbind(),
345+
connection,
332346
run_id,
333347
nodes,
334348
history,
335349
executions,
336350
failed_start,
337-
base_on_node_complete: base_on_node_complete.unbind(),
351+
base_on_node_complete: NodeCompleteHook::new(&base_on_node_complete),
338352
recording: Mutex::new(Recording {
339-
pending: BTreeMap::new(),
340-
pending_executions: 0,
341-
pending_seeds: Vec::new(),
342353
tables,
343-
last_flush: Instant::now(),
344-
warnings: Vec::new(),
354+
..Recording::default()
345355
}),
346356
};
347357
Ok(Prepared {

0 commit comments

Comments
 (0)