Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
c9d31bd
fix: release the compute log lock before rejecting a write to a finis…
kvlonge Oct 19, 2026
c10b78c
test: fail a hung E2E command with every thread's stack instead of st…
kvlonge Oct 19, 2026
3deb6a2
refactor: share project scaffolding and rule listing text outside the…
kvlonge Oct 19, 2026
cf463ff
feat: run help, usage errors, init, skills, clean and rules list/show…
kvlonge Oct 19, 2026
986b63d
docs: note that Recall does not record commands sqb answers without P…
kvlonge Oct 19, 2026
2ca8a36
fix: resolve native project paths and the home directory as Python do…
kvlonge Oct 19, 2026
8ccb01f
fix: report lost stdout, Ctrl+C, init failures and the version as the…
kvlonge Oct 19, 2026
27fef08
Merge branch 'main' into feat/python-free-commands
kvlonge Oct 19, 2026
3e41427
Merge branch 'main' into fix/sqb-test-hang
kvlonge Oct 19, 2026
273df0c
chore(deps): bump fensu to 0.27
kvlonge Oct 19, 2026
62334d9
chore: move function name data under the crate source
kvlonge Oct 19, 2026
912815b
refactor: name large tuple types
kvlonge Oct 19, 2026
7c7bf79
chore: enable fensu dead-code enforcement and remove dead code
kvlonge Oct 19, 2026
738e9bf
refactor: name large tuple types introduced on main
kvlonge Oct 19, 2026
2655063
chore: satisfy fensu 0.27 in code added on main
kvlonge Oct 19, 2026
2dd4362
fix(clickhouse): accept ClickHouse Cloud SharedMergeTree engines
kvlonge Oct 19, 2026
aaa2ae6
Merge remote-tracking branch 'origin/fix/sqb-test-hang' into feat/com…
kvlonge Oct 19, 2026
396b190
Merge remote-tracking branch 'origin/fix/clickhouse-shared-merge-tree…
kvlonge Oct 19, 2026
410afe3
Merge remote-tracking branch 'origin/feat/python-free-commands' into …
kvlonge Oct 19, 2026
4984e9c
Merge remote-tracking branch 'origin/main' into feat/combined-batch
kvlonge Oct 19, 2026
c438f70
test: pin buffered stdout in broken-pipe parity tests and list the co…
kvlonge Oct 20, 2026
c4fbf35
perf: run sqb query from the project configuration without full disco…
kvlonge Oct 19, 2026
36b563d
test: guard the startup import footprint of scope, lineage, dag and q…
kvlonge Oct 19, 2026
9e15776
docs: regenerate the bundled query reference
kvlonge Oct 20, 2026
679c41a
Merge branch 'main' into feat/combined-batch
kvlonge Oct 20, 2026
1261a1c
fix(snowflake): collect run cost for large runs in bounded history sl…
kvlonge Oct 20, 2026
d752ab3
fix(dagster): report seed and end-of-run audits as asset checks
kvlonge Oct 20, 2026
600d40b
feat: allow concurrency per target
kvlonge Oct 20, 2026
6bfaff3
fix: use the effective concurrency for the microbatch warning and dbt…
kvlonge Oct 20, 2026
bf08fac
Merge branch 'main' into feat/combined-batch
kvlonge Oct 20, 2026
ea98d4c
feat(rules)!: make configuration the only Rule exception mechanism
kvlonge Oct 19, 2026
a04dd73
feat(rules): add published-layer domain isolation and name shape rules
kvlonge Oct 19, 2026
d6e61d7
docs(rules): document config-only exceptions and published-layer rules
kvlonge Oct 19, 2026
76d7083
Merge branch 'main' into feat/combined-batch
kvlonge Oct 20, 2026
2631894
fix: run provider teardown on the closing thread so exit cannot abort
kvlonge Oct 19, 2026
c379b16
feat: deliver lifecycle events on a long-lived thread per sink and al…
kvlonge Oct 20, 2026
a6144e9
fix: end a successful command on its own status after an incomplete-e…
kvlonge Oct 20, 2026
862cbb1
fix(snowflake): read run history in one query and give sqb cost a lon…
kvlonge Oct 20, 2026
ed20a8c
fix(rules): tighten cross join proofs, CTE labels and directive detec…
kvlonge Oct 20, 2026
fe1a67e
fix: count a cancelled lifecycle attempt as dropped only when it can …
kvlonge Oct 20, 2026
8642aaf
Merge branch 'perf/command-startup' into feat/combined-batch-2
kvlonge Oct 20, 2026
f4873bf
Merge branch 'fix/prod-batch' into feat/combined-batch-2
kvlonge Oct 20, 2026
962f07c
Merge branch 'feat/config-only-rule-exceptions' into feat/combined-ba…
kvlonge Oct 20, 2026
5135bf0
Merge branch 'fix/exit-abort' into feat/combined-batch-2
kvlonge Oct 20, 2026
8fff721
ci: give native-building verify jobs 45 minutes
kvlonge Oct 20, 2026
d03c5d0
fix(sqlite): switch a new database file to WAL one connection at a time
kvlonge Oct 20, 2026
d926c9d
ci: build the Intel macOS wheel on the Apple Silicon runner
kvlonge Oct 20, 2026
78eddf6
Merge origin/feat/combined-batch into feat/combined-batch-2
kvlonge Oct 20, 2026
6ec0076
fix(cli): answer rules list and show natively with config-only except…
kvlonge Oct 20, 2026
a2937ce
Merge origin/main into feat/combined-batch-2
kvlonge Oct 20, 2026
355db51
test: list concurrency among allowed target keys in the compiler golden
kvlonge Oct 20, 2026
fc76b92
Merge branch 'main' into feat/combined-batch-2
kvlonge Oct 20, 2026
b96b0f3
test: clear SQLBUILD_CONCURRENCY in the target concurrency E2E tests
kvlonge Oct 20, 2026
e1a78b6
ci: run the Intel macOS wheel smoke test under Rosetta
kvlonge Oct 20, 2026
bf931d9
Merge remote-tracking branch 'origin/feat/combined-batch-2' into feat…
kvlonge Oct 20, 2026
eaa0e0f
ci: run the native sqlbuild entry point under Rosetta like sqb
kvlonge Oct 20, 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
23 changes: 17 additions & 6 deletions .github/workflows/build-distributions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,10 @@ jobs:
runner: ubuntu-24.04-arm
target: aarch64
- name: macos-x86_64
runner: macos-15-intel
runner: macos-15
target: x86_64
python-architecture: x64
smoke-arch: x86_64
- name: macos-arm64
runner: macos-15
target: aarch64
Expand All @@ -116,6 +118,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: "3.12"
architecture: ${{ matrix.platform.python-architecture }}
- uses: dtolnay/rust-toolchain@master
if: inputs.prebuilt-run-id == ''
with:
Expand Down Expand Up @@ -144,20 +147,28 @@ jobs:
github-token: ${{ github.token }}
- name: Smoke test installed wheels and native console entry points
shell: bash
env:
SMOKE_ARCH: ${{ matrix.platform.smoke-arch }}
run: |
python -m venv "${RUNNER_TEMP}/wheel-smoke"
if [[ -x "${RUNNER_TEMP}/wheel-smoke/Scripts/python.exe" ]]; then
smoke_python="${RUNNER_TEMP}/wheel-smoke/Scripts/python.exe"
else
smoke_python="${RUNNER_TEMP}/wheel-smoke/bin/python"
fi
"${smoke_python}" -m pip install --find-links dist dist/*.whl
# A cross-compiled wheel runs under Rosetta; the cached Python is universal, so force
# the wheel's architecture or it starts as arm64 and rejects the wheel.
run_as=()
if [[ -n "${SMOKE_ARCH}" ]]; then
run_as=(arch "-${SMOKE_ARCH}")
fi
"${run_as[@]}" "${smoke_python}" -m pip install --find-links dist dist/*.whl
cd "${RUNNER_TEMP}"
"${smoke_python}" -c "import sqlbuild._native"
"${run_as[@]}" "${smoke_python}" -c "import sqlbuild._native"
smoke_scripts="$(dirname "${smoke_python}")"
"${smoke_scripts}/sqb" --version
"${smoke_scripts}/sqb" --help
"${smoke_scripts}/sqlbuild" --help
"${run_as[@]}" "${smoke_scripts}/sqb" --version
"${run_as[@]}" "${smoke_scripts}/sqb" --help
"${run_as[@]}" "${smoke_scripts}/sqlbuild" --help
- name: Check wheel contents and run a native command
if: matrix.platform.name == 'linux-x86_64'
run: |
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ E2E_DUCKDB_BUILD_CORE_PATHS := \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_scoped_declarations.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_selector_surface.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_table_function_dependency.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_target_concurrency.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_template_expressions.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_terminal_failure.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_validation_failures.py
Expand Down
13 changes: 13 additions & 0 deletions crates/sqlbuild-cli/src/dispatch/main/closing_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! The closing line after an incomplete-delivery notice, so the command's own outcome is the last word.

/// `sqb <command>`'s outcome by exit code, naming the only thing a preceding notice affects.
#[must_use]
pub fn closing_status(command: &str, exit_code: Option<i64>, affected: &str) -> String {
match exit_code {
Some(0) => {
format!("sqb {command} completed successfully; only its {affected} is affected.\n")
}
Some(code) => format!("sqb {command} failed with exit code {code}.\n"),
None => format!("sqb {command} failed.\n"),
}
}
1 change: 1 addition & 0 deletions crates/sqlbuild-cli/src/dispatch/main/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod closing_status;
pub mod command_dispatch;
pub mod event_export_warning;
pub mod failure_report;
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-cli/src/dispatch/tests.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#[path = "tests/test_closing_status.rs"]
mod closing_status;
#[path = "tests/test_command_dispatch.rs"]
mod command_dispatch;
#[path = "tests/test_event_export_warning.rs"]
Expand Down
32 changes: 32 additions & 0 deletions crates/sqlbuild-cli/src/dispatch/tests/test_closing_status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use crate::dispatch::main::closing_status::closing_status;
use crate::dispatch::tests::test_types::ClosingStatusTestCase;

#[test]
fn given_command_outcome_when_closing_a_notice_then_the_outcome_is_the_last_line() {
let test_cases = [
ClosingStatusTestCase {
description: "success names the only affected output",
exit_code: Some(0),
expected_status: "sqb audit completed successfully; only its lifecycle event export \
is affected.\n",
},
ClosingStatusTestCase {
description: "failure reports the exit code",
exit_code: Some(2),
expected_status: "sqb audit failed with exit code 2.\n",
},
ClosingStatusTestCase {
description: "unknown exit code reports failure",
exit_code: None,
expected_status: "sqb audit failed.\n",
},
];
for test_case in test_cases {
assert_eq!(
closing_status("audit", test_case.exit_code, "lifecycle event export"),
test_case.expected_status,
"{}",
test_case.description
);
}
}
7 changes: 7 additions & 0 deletions crates/sqlbuild-cli/src/dispatch/tests/test_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ pub(super) struct EventExportWarningTestCase {
pub(super) command_succeeded: bool,
pub(super) expected_warning: Option<&'static str>,
}

/// A command, its exit code and what a preceding notice affects, and the closing line.
pub(super) struct ClosingStatusTestCase {
pub(super) description: &'static str,
pub(super) exit_code: Option<i64>,
pub(super) expected_status: &'static str,
}
35 changes: 23 additions & 12 deletions crates/sqlbuild-cli/src/native_commands/_helpers/rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
use std::collections::BTreeMap;
use std::path::Path;

use sqlbuild_rules::configuration::main::forbidden_exceptions::forbidden_exceptions;
use sqlbuild_rules::configuration::main::load_rules_config::load_rules_config;
use sqlbuild_rules::models::{RuleMetadata, RulesConfig};
use sqlbuild_rules::rule_catalogue::main::format_rule_text::format_rule_text;
use sqlbuild_rules::rule_catalogue::main::render_rule_list::render_rule_list;
use sqlbuild_rules::rule_catalogue::main::sorted_catalogue_order::sorted_catalogue_order;
use sqlbuild_rules::rule_catalogue::main::string_tuple_repr::string_tuple_repr;
use sqlbuild_rules::rule_catalogue::models::{
CatalogueRule, OptionKind, OptionSpec, OptionText, OptionValue, RuleListing, RuleText,
};
Expand All @@ -18,21 +18,31 @@ use sqlbuild_rules::rules::models::{BuiltinOption, BuiltinOptionKind};

use crate::native_commands::_helpers::project_facts::has_custom_rules;
use crate::native_commands::constants::{
EMPTY_TUPLE_REPR, FALSE_REPR, FAMILY_SUFFIX_LENGTH, MODEL_SUBJECT, PROJECT_SUBJECT,
RULE_SELECTOR_REQUIRED, RULES_USAGE_EXIT_CODE, UNKNOWN_RULE_PREFIX,
DICT_TYPE_NAME, EMPTY_TUPLE_REPR, FALSE_REPR, FAMILY_SUFFIX_LENGTH, MODEL_SUBJECT,
PROJECT_SUBJECT, RULE_SELECTOR_REQUIRED, RULES_USAGE_EXIT_CODE, UNKNOWN_RULE_PREFIX,
};
use crate::native_commands::errors::PythonNeeded;
use crate::native_commands::models::{CommandOutput, RulesAction, RulesOptions};

/// `RuleOption.boolean(...)` or `RuleOption.string_list(...)` as the catalogue validates it.
/// `RuleOption.boolean(...)`, `.string_list(...)` or a table list, as the catalogue validates it.
fn option_spec(option: &BuiltinOption) -> OptionSpec {
let (value_type, default, item_type) = match option.kind {
BuiltinOptionKind::Boolean => (OptionKind::Bool, OptionValue::Bool(false), None),
BuiltinOptionKind::StringList => (
BuiltinOptionKind::StringList(defaults) => (
OptionKind::Tuple,
OptionValue::Tuple(Vec::new()),
OptionValue::Tuple(
defaults
.iter()
.map(|item| OptionValue::Str((*item).to_owned()))
.collect(),
),
Some(OptionKind::Str),
),
BuiltinOptionKind::TableList => (
OptionKind::Tuple,
OptionValue::Tuple(Vec::new()),
Some(OptionKind::Other(DICT_TYPE_NAME.to_owned())),
),
};
OptionSpec {
name: option.name.to_owned(),
Expand All @@ -49,15 +59,16 @@ fn option_spec(option: &BuiltinOption) -> OptionSpec {

/// The option as `rules show` prints it: the `repr` of its default, which is also effective.
fn option_text(option: &BuiltinOption) -> OptionText {
let default_repr: &str = match option.kind {
BuiltinOptionKind::Boolean => FALSE_REPR,
BuiltinOptionKind::StringList => EMPTY_TUPLE_REPR,
let default_repr: String = match option.kind {
BuiltinOptionKind::Boolean => FALSE_REPR.to_owned(),
BuiltinOptionKind::StringList(defaults) => string_tuple_repr(defaults),
BuiltinOptionKind::TableList => EMPTY_TUPLE_REPR.to_owned(),
};
OptionText {
name: option.name.to_owned(),
description: option.description.to_owned(),
default_repr: default_repr.to_owned(),
effective_repr: default_repr.to_owned(),
effective_repr: default_repr.clone(),
default_repr,
}
}

Expand Down Expand Up @@ -155,7 +166,7 @@ pub(crate) fn run_rules(
return Err(PythonNeeded::Project);
}
let config: RulesConfig = load_rules_config(project_dir).map_err(|_| PythonNeeded::Failure)?;
if forbidden_exceptions(&config).is_some() || !config.rule_options.is_empty() {
if !config.rule_options.is_empty() {
return Err(PythonNeeded::Project);
}
let rules: Vec<RuleMetadata> = sorted_catalogue()?;
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-cli/src/native_commands/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ pub(crate) const PROJECT_SUBJECT: &str = "project";
/// Python `repr` of the built-in option defaults: `False` and `()`.
pub(crate) const FALSE_REPR: &str = "False";
pub(crate) const EMPTY_TUPLE_REPR: &str = "()";
/// The Python type name of each `allowed_qualifiers`-style table option item.
pub(crate) const DICT_TYPE_NAME: &str = "dict";
pub(crate) const UNKNOWN_RULE_PREFIX: &str = "Unknown rule: ";
/// The whitespace `int()` strips once other spaces are ASCII spaces.
pub(crate) const INT_ASCII_SPACES: &str = " \t\n\x0b\x0c\r";
Expand Down
7 changes: 7 additions & 0 deletions crates/sqlbuild-config/src/effective/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,11 @@ pub(crate) const TARGET_FIELDS: &[TargetField] = &[
decided_by: &["query"],
},
},
TargetField {
group: None,
name: "concurrency",
rule: MergeRule::Overlay {
decided_by: &["concurrency"],
},
},
];
28 changes: 28 additions & 0 deletions crates/sqlbuild-config/src/effective/main/effective_concurrency.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//! `effective_concurrency`: the worker limit of one command and the layer it came from.

use crate::effective::models::{ConcurrencyLayers, ConcurrencySource, EffectiveConcurrency};

/// `--concurrency`, else the target's `concurrency`, else the local-then-project setting.
#[must_use]
pub fn effective_concurrency(layers: &ConcurrencyLayers<'_>) -> EffectiveConcurrency {
if let Some(limit) = layers.requested {
return EffectiveConcurrency {
limit,
source: ConcurrencySource::CommandLine,
};
}
if let Some((target, limit)) = layers.target {
return EffectiveConcurrency {
limit,
source: ConcurrencySource::Target(target.to_owned()),
};
}
EffectiveConcurrency {
limit: layers.settings,
source: if layers.local_override {
ConcurrencySource::LocalSettings
} else {
ConcurrencySource::ProjectSettings
},
}
}
1 change: 1 addition & 0 deletions crates/sqlbuild-config/src/effective/main/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod default_connection_name;
pub mod effective_concurrency;
pub mod effective_connection;
pub mod effective_settings;
pub mod effective_target_name;
Expand Down
40 changes: 40 additions & 0 deletions crates/sqlbuild-config/src/effective/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,43 @@ pub struct TargetChoice<K> {
pub default: Option<K>,
pub defined: Vec<K>,
}

/// The concurrency each configuration layer sets, highest precedence first.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ConcurrencyLayers<'a> {
/// `--concurrency` on the command line.
pub requested: Option<i64>,
/// The selected target's name and its `concurrency`, when it sets one.
pub target: Option<(&'a str, i64)>,
/// `[settings] concurrency` after any `sqlbuild_local.toml` override.
pub settings: i64,
/// Whether `sqlbuild_local.toml` `[settings]` overrides `concurrency`.
pub local_override: bool,
}

/// Where the effective concurrency came from.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ConcurrencySource {
CommandLine,
Target(String),
LocalSettings,
ProjectSettings,
}

impl std::fmt::Display for ConcurrencySource {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CommandLine => formatter.write_str("--concurrency"),
Self::Target(name) => write!(formatter, "targets.{name}.concurrency"),
Self::LocalSettings => formatter.write_str("sqlbuild_local.toml settings.concurrency"),
Self::ProjectSettings => formatter.write_str("settings.concurrency"),
}
}
}

/// One command's worker limit and the layer that set it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct EffectiveConcurrency {
pub limit: i64,
pub source: ConcurrencySource,
}
64 changes: 61 additions & 3 deletions crates/sqlbuild-config/src/effective/tests/test_effective.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
use crate::effective::errors::EffectiveConfigError;
use crate::effective::main::default_connection_name::default_connection_name;
use crate::effective::main::effective_concurrency::effective_concurrency;
use crate::effective::main::effective_connection::effective_connection;
use crate::effective::main::effective_settings::effective_settings;
use crate::effective::main::effective_target_name::effective_target_name;
use crate::effective::main::effective_vars::effective_vars;
use crate::effective::main::merged_target::merged_target;
use crate::effective::models::{
ConnectionChoice, ConnectionLayers, FieldValue, TargetChoice, TargetLayer,
ConcurrencyLayers, ConnectionChoice, ConnectionLayers, FieldValue, TargetChoice, TargetLayer,
};
use crate::effective::tests::helpers::{entries, field_value, local_layer, project_layer};
use crate::effective::tests::test_types::{
ConnectionTestCase, DefaultConnectionTestCase, MergeTestCase, MessageTestCase, Pairs,
TargetNameTestCase, UnionTestCase,
ConcurrencyTestCase, ConnectionTestCase, DefaultConnectionTestCase, MergeTestCase,
MessageTestCase, Pairs, TargetNameTestCase, UnionTestCase,
};
use crate::effective::types::Entries;

Expand Down Expand Up @@ -295,3 +296,60 @@ fn given_ambiguous_connection_when_rendering_then_names_join_into_the_python_mes
);
}
}

#[test]
fn given_concurrency_layers_when_resolving_then_cli_beats_target_beats_settings() {
let test_cases = [
ConcurrencyTestCase {
description: "the command line beats the target",
requested: Some(2),
target: Some(("prod", 8)),
settings: 16,
local_override: true,
expected_limit: 2,
expected_source: "--concurrency",
},
ConcurrencyTestCase {
description: "the target beats a local settings override",
requested: None,
target: Some(("prod", 8)),
settings: 4,
local_override: true,
expected_limit: 8,
expected_source: "targets.prod.concurrency",
},
ConcurrencyTestCase {
description: "a local settings override applies without a target value",
requested: None,
target: None,
settings: 4,
local_override: true,
expected_limit: 4,
expected_source: "sqlbuild_local.toml settings.concurrency",
},
ConcurrencyTestCase {
description: "the project setting is the default",
requested: None,
target: None,
settings: 16,
local_override: false,
expected_limit: 16,
expected_source: "settings.concurrency",
},
];

for test_case in test_cases {
let resolved = effective_concurrency(&ConcurrencyLayers {
requested: test_case.requested,
target: test_case.target,
settings: test_case.settings,
local_override: test_case.local_override,
});
assert_eq!(
(resolved.limit, resolved.source.to_string().as_str()),
(test_case.expected_limit, test_case.expected_source),
"{}",
test_case.description
);
}
}
Loading
Loading