Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c09cd0c
fix(build): count Python checks in the build's completion line
kvlonge Oct 25, 2026
d88e7a8
fix(test): find mocked references on both sides of a parenthesised join
kvlonge Oct 25, 2026
449b962
fix(macros): locate a nested macro call that makes a dict key unhashable
kvlonge Oct 25, 2026
cd38939
fix(refactor): let sqb mv move a model that Python code names
kvlonge Oct 25, 2026
c7ef6bf
fix(sqlite): accept text columns as timestamp cursors
kvlonge Oct 25, 2026
d4a52e6
feat(sqlite): warn at compile about casts to date and time types
kvlonge Oct 25, 2026
a507c96
fix(dbt): reject --json on sqb dbt run and sqb dbt build
kvlonge Oct 25, 2026
4600aa4
fix(scenario): load replayed VARIANT values from their JSON text
kvlonge Oct 25, 2026
e47d174
fix(contracts): match Snowflake star outputs to lowercase contract co…
kvlonge Oct 25, 2026
d144678
feat(sqlite): load dlt and ingestr sources into SQLite projects
kvlonge Oct 25, 2026
6372efc
feat(postgres): add a connection option to turn off temporary test fu…
kvlonge Oct 25, 2026
6c4ff0f
fix(test): register DuckDB Python UDFs on every sqb test connection
kvlonge Oct 25, 2026
af906c9
fix(duckdb): attach a database once per project file and from the pro…
kvlonge Oct 25, 2026
fc9c777
fix(sqlserver): read timestamp cursor bounds as ISO 8601 text
kvlonge Oct 25, 2026
bb0df8c
fix(databricks): check schema existence in the current catalog when n…
kvlonge Oct 25, 2026
2d5fd90
fix(rules): report model rule columns from the file when the query sh…
kvlonge Oct 25, 2026
ed0c4db
fix(clickhouse): treat # and // line comments as comments
kvlonge Oct 25, 2026
91f5d6b
docs(contributing): explain why maturin develop needs --skip-install
kvlonge Oct 25, 2026
9e9e976
fix(make): stop make skills installing the development skill globally
kvlonge Oct 25, 2026
0c42246
fix(analysis): take Polyglot's parser, typing and star-expansion fixes
kvlonge Oct 25, 2026
829a701
test(scenario): describe leaked lifecycle events when the pipeline or…
kvlonge Oct 25, 2026
f9b23f8
test(recall): dump every thread's stack when a Recall test's CLI run …
kvlonge Oct 25, 2026
6f90c22
fix(compile): log why a warm compile could not keep cached SQL test a…
kvlonge Oct 25, 2026
7e5bb13
test(macros): expect a macro call in a star REPLACE to take its retur…
kvlonge Oct 25, 2026
3236459
fix(rules): map model rule positions back through macro and declarati…
kvlonge Oct 25, 2026
6371d5b
test(microbatch): report state rows and build output when a failure-w…
kvlonge Oct 25, 2026
6d6ee19
test: align unit guards with dbt --json rejection and Databricks sche…
kvlonge Oct 25, 2026
d6906ce
fix(scenario): compare replayed VARIANT columns as VARIANT values
kvlonge Oct 25, 2026
e1c2228
fix(scenario): compare VARIANT and JSON outputs by canonical JSON text
kvlonge Oct 25, 2026
368742e
fix(duckdb): resolve ~ attach and database paths from the home directory
kvlonge Oct 25, 2026
52c5cc5
fix(analysis): reject a bare VALUES CTE body on Snowflake
kvlonge Oct 25, 2026
2544c0a
fix(scenario): compare quoted VARIANT output columns by JSON value
kvlonge Oct 25, 2026
f4d60dd
Merge branch 'main' into fix/bug-batch-2
kvlonge Oct 25, 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
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,13 @@ Do not compare development-profile timings with release benchmarks. Dependency o
changes still require `uv sync --all-extras`; `--skip-install` does not refresh installed metadata,
dependencies or console scripts.

Always pass `--skip-install` to `maturin develop`, or use `make dev-native` or `make fast-native`.
Without it, maturin installs the `sqlbuild-cli` version that `pyproject.toml` pins from the package
index, not this checkout's `crates/sqlbuild-cli`: the `sqb` executable is then a released build,
and between a release commit and its publication the install fails because that version does not
exist yet. `uv sync` and `uv run` install the workspace `sqlbuild-cli` and rebuild the extension
when Rust sources change.

Before opening a pull request, run:

```bash
Expand Down
6 changes: 3 additions & 3 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,14 @@ junction = { version = "2.1.0", default-features = false }
mimalloc = { version = "0.1", default-features = false, features = ["local_dynamic_tls"] }
proc-macro2 = { version = "1", features = ["span-locations"] }
pyo3 = { version = "0.26", features = ["abi3-py312"] }
polyglot-sql = { git = "https://github.com/chio-labs/polyglot.git", rev = "1ef5056c2b315e69bd13023bd90ffe60d94880a3", default-features = false, features = [
polyglot-sql = { git = "https://github.com/chio-labs/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [
"ast-tools",
"builder",
"all-dialects",
"semantic",
"stacker",
] }
polyglot-sql-function-catalogs = { git = "https://github.com/chio-labs/polyglot.git", rev = "1ef5056c2b315e69bd13023bd90ffe60d94880a3", default-features = false, features = [
polyglot-sql-function-catalogs = { git = "https://github.com/chio-labs/polyglot.git", rev = "8767d688c5b504ad609645e27ab77a260d91c5a1", default-features = false, features = [
"dialect-duckdb",
] }
blake3 = { version = "1", features = ["rayon"] }
Expand Down
8 changes: 7 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,8 @@ E2E_DUCKDB_BUILD_CORE_PATHS := \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_no_load_sources.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_duckdb_storage_key.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_source_end_audits.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_python_check_outcomes.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_duckdb_attach.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_unbuilt_dependencies.py \
tests/e2e/src/sqlbuild/cli/commands/main/build/test_validation_failures.py

Expand Down Expand Up @@ -370,9 +372,13 @@ planner-differential:

skills:
uv run python -m scripts.generate_docs_skill
uv run sqb skills --global --target opencode
uv run fensu skills

# Installs this checkout's development skill into your user-level OpenCode skills; never run by CI.
.PHONY: install-global-skill
install-global-skill:
uv run sqb skills --global --target opencode


DBT_EXECUTABLE ?= dbt
export DBT_EXECUTABLE
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-analysis/src/binding_locations/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@
pub(crate) const UNKNOWN_TABLE_INTERNAL_CODE: &str = "B000";
/// The severity whose repeats at a known span compile drops.
pub(crate) const ERROR_SEVERITY: &str = "error";
/// SQLite temporal-cast warnings, which every binding pass reports, so they are kept once.
pub(crate) const SQLITE_TEMPORAL_CAST_CODE: &str = "B104";
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

use std::collections::HashSet;

use crate::binding_locations::constants::{ERROR_SEVERITY, UNKNOWN_TABLE_INTERNAL_CODE};
use crate::binding_locations::constants::{
ERROR_SEVERITY, SQLITE_TEMPORAL_CAST_CODE, UNKNOWN_TABLE_INTERNAL_CODE,
};
use crate::binding_locations::models::{
AuthoredSpan, BindingSource, LocatedDiagnostic, SeenDiagnostic,
};
Expand Down Expand Up @@ -56,7 +58,10 @@ pub fn locate_binding_diagnostics(
line,
column,
};
if seen.contains(&key) && start.is_some() && severity == ERROR_SEVERITY {
if seen.contains(&key)
&& start.is_some()
&& (severity == ERROR_SEVERITY || code == SQLITE_TEMPORAL_CAST_CODE)
{
continue;
}
seen.insert(key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ pub(crate) fn plain_model_facts(model: PlainModel) -> CompiledModelFacts {
constant_declarations: model.constant_declarations,
analysis: None,
typed_calls: model.typed_calls,
expansion_passes: model.expansion_passes,
}
}
5 changes: 5 additions & 0 deletions crates/sqlbuild-analysis/src/compiled_project/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::collections::{BTreeMap, HashMap};
use std::sync::Arc;

use serde_json::Value;
use sqlbuild_sqltext::compiler::models::CharSpan;

/// One compile's project facts; models keep the order the compile-input stage produced them.
#[derive(Clone, Debug, Default, PartialEq)]
Expand Down Expand Up @@ -34,6 +35,8 @@ pub struct CompiledModelFacts {
pub analysis: Option<ModelAnalysisFact>,
/// Each expression SQL macro call in `query_sql`, by byte range.
pub typed_calls: Vec<TypedCall>,
/// The expansion passes from `authored_query_sql` to `query_sql`; empty when unknown.
pub expansion_passes: Vec<Vec<CharSpan>>,
}

/// A model's effective config values with sorted keys, as the rules request has always read them.
Expand Down Expand Up @@ -217,6 +220,8 @@ pub struct PlainModel {
pub constant_declarations: Vec<DeclarationFact>,
/// Each expression SQL macro call in `query_sql`, by byte range.
pub typed_calls: Vec<TypedCall>,
/// The expansion passes from `authored_query_sql` to `query_sql`; empty when unknown.
pub expansion_passes: Vec<Vec<CharSpan>>,
}

/// A one-based line and column, counted in characters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,20 @@ fn node_marker_calls(mut calls: Vec<MarkerCall>, node: &Expression) -> Vec<Marke
),
Expression::From(from) => calls.extend(from_markers(from)),
Expression::Delete(delete) => calls.extend(join_markers(&delete.joins)),
Expression::JoinedTable(table) => calls.extend(join_markers(&table.joins)),
Expression::JoinedTable(table) => {
calls.extend(
marker(&table.left)
.into_iter()
.chain(join_markers(&table.joins)),
);
}
Expression::Update(update) => calls.extend(
update
.from_clause
.iter()
.flat_map(from_markers)
.chain(join_markers(&update.from_joins)),
),
Expression::DataDeletionProperty(property) => {
return property.on.dfs().fold(calls, node_marker_calls);
}
Expand Down
15 changes: 8 additions & 7 deletions crates/sqlbuild-analysis/src/compiler/tests/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1574,17 +1574,18 @@ type MarkerCalls = Option<Vec<(String, String)>>;

/// The original JSON-tree walk, kept as the oracle the direct relation walk must match.
fn json_relation_marker_calls(value: &Value) -> Vec<(String, String)> {
let from_markers = value
.get("from")
.and_then(|from| from.get("expressions"))
.and_then(Value::as_array)
let from_markers = ["from", "from_clause"]
.into_iter()
.filter_map(|key| value.get(key))
.filter_map(|from| from.get("expressions"))
.filter_map(Value::as_array)
.flatten()
.chain(value.get("left").filter(|_| value.get("joins").is_some()))
.filter_map(json_marker);
let join_markers = value
.get("joins")
.and_then(Value::as_array)
let join_markers = ["joins", "from_joins"]
.into_iter()
.filter_map(|key| value.get(key))
.filter_map(Value::as_array)
.flatten()
.filter_map(|join| join.get("this"))
.filter_map(json_marker);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ fn given_parsed_statements_when_collecting_relation_markers_then_json_walk_order
sql: "SELECT __ref(\"orders\") AS value FROM __table_fn(\"orders\", 2) WHERE id IN (SELECT id FROM t)",
expected_calls: &[],
},
RelationMarkersTestCase {
description: "a parenthesised join keeps its left side",
dialect: "duckdb",
sql: "SELECT * FROM (__ref(\"orders\") JOIN __ref(\"customers\") ON TRUE)",
expected_calls: &[("__ref", "orders"), ("__ref", "customers")],
},
RelationMarkersTestCase {
description: "update from relations are collected",
dialect: "postgres",
sql: "UPDATE t SET x = 1 FROM __ref(\"orders\") JOIN __ref(\"customers\") ON TRUE",
expected_calls: &[("__ref", "orders"), ("__ref", "customers")],
},
RelationMarkersTestCase {
description: "set operations walk each branch",
dialect: "postgres",
Expand Down
23 changes: 19 additions & 4 deletions crates/sqlbuild-analysis/src/contracts/_helpers/columns.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,25 @@ pub(crate) fn model_contract_diagnostics(
.map(|column| (column.name.as_str(), column))
.collect();
if contract_enforced {
diagnostics.extend(extra_columns(model, schema, inferred_columns));
diagnostics.extend(extra_columns(
model,
schema,
inferred_columns,
types.folds_upper(),
));
}
let unchecked: HashSet<&str> = model
.unchecked_output_columns
.iter()
.map(String::as_str)
.collect();
for (index, declared) in schema.columns.iter().enumerate() {
let Some(inferred) = inferred_by_name.get(declared.name.as_str()) else {
let Some(inferred) = inferred_by_name.get(declared.name.as_str()).or_else(|| {
types
.folds_upper()
.then(|| inferred_by_name.get(declared.name.to_uppercase().as_str()))
.flatten()
}) else {
if validate_declared_shape && !model.fast_lineage_has_star {
diagnostics.push(missing_column(model, index, declared, contract_enforced));
}
Expand Down Expand Up @@ -147,15 +157,20 @@ fn missing_declarations(model: &ContractModel) -> ContractDiagnostic {
}
}

/// Inferred columns no declared column names; a folded `order_id` also names a star's `ORDER_ID`.
fn extra_columns(
model: &ContractModel,
schema: &ContractSchema,
inferred_columns: &[InferredOutputColumn],
folds_upper: bool,
) -> Vec<ContractDiagnostic> {
let declared_names: HashSet<&str> = schema
let declared_names: HashSet<String> = schema
.columns
.iter()
.map(|column| column.name.as_str())
.flat_map(|column| {
let folded: Option<String> = folds_upper.then(|| column.name.to_uppercase());
std::iter::once(column.name.clone()).chain(folded)
})
.collect();
let help: &str = if schema.named_schema {
EXTRA_COLUMN_NAMED_SCHEMA_HELP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
use std::cell::RefCell;
use std::collections::HashMap;

use crate::contracts::constants::UPPER_FOLDING_DIALECT;
use crate::type_system::errors::TypeNormalizationError;
use crate::type_system::main::normalize_type::normalize_type;
use crate::type_system::models::{NormalizedType, TypeNormalization};
Expand All @@ -21,6 +22,11 @@ pub(crate) struct TypeComparer<'a> {
}

impl<'a> TypeComparer<'a> {
/// Whether the dialect folds unquoted identifiers to upper case, as star expansion names them.
pub(crate) fn folds_upper(&self) -> bool {
self.dialect == UPPER_FOLDING_DIALECT
}

pub(crate) fn new(dialect: &'a str) -> Self {
Self {
dialect,
Expand Down
2 changes: 2 additions & 0 deletions crates/sqlbuild-analysis/src/contracts/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ pub(crate) const PROMOTION_REMOVE_CONTRACT_HELP: &str = "or remove the model's e

pub(crate) const SETTING_SNIPPET_INDENT: &str = " ";
pub(crate) const ADDITIONAL_HELP_SEPARATOR: &str = "\n = help: ";
/// The dialect that folds unquoted identifiers to upper case.
pub(crate) const UPPER_FOLDING_DIALECT: &str = "snowflake";
Original file line number Diff line number Diff line change
Expand Up @@ -196,16 +196,15 @@ fn given_models_without_compact_facts_when_building_then_matches_python_lineage(
expected_detail: Some("Expected 1 statement, found 2"),
},
ParsedLineageTestCase {
description: "a parser panic on truncated T-SQL fails as an internal error",
description: "truncated T-SQL reports its parse error",
sql: "SELECT IF(region > 1, re",
dialect: Some("tsql"),
inferred_columns: &[],
expected_status: "error",
expected_status: "unparsed",
expected_lines: &[],
expected_has_star: false,
expected_detail: Some(
"NativeCompilerError: native SQL compilation panicked \
(native fast lineage of request model 0)",
"Parse error at line 1, column 25: Expected ')' to close the IF condition",
),
},
ParsedLineageTestCase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@

use crate::planner_warehouse::models::PhysicalCursorQuery;

/// Select the requested bounds of the cursor column as text.
pub fn cursor_query_sql(query: &PhysicalCursorQuery) -> String {
/// Select the requested bounds of the cursor column as text, each through `as_text`.
pub fn cursor_query_sql(query: &PhysicalCursorQuery, as_text: &dyn Fn(&str) -> String) -> String {
let mut parts: Vec<String> = Vec::new();
if !query.min_tags.is_empty() {
parts.push(format!(
"CAST(MIN({}) AS VARCHAR) AS _min",
query.cursor_column
"{} AS _min",
as_text(&format!("MIN({})", query.cursor_column))
));
}
if !query.max_tags.is_empty() {
parts.push(format!(
"CAST(MAX({}) AS VARCHAR) AS _max",
query.cursor_column
"{} AS _max",
as_text(&format!("MAX({})", query.cursor_column))
));
}
format!("SELECT {} FROM {}", parts.join(", "), query.relation)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ fn given_logical_bounds_when_grouping_then_reads_each_relation_once_in_first_ord
];
for test_case in test_cases {
let groups = cursor_query_groups(&test_case.requests);
let sql: Vec<String> = groups.iter().map(cursor_query_sql).collect();
let sql: Vec<String> = groups
.iter()
.map(|group| cursor_query_sql(group, &|bound| format!("CAST({bound} AS VARCHAR)")))
.collect();
let identities: Vec<String> = groups
.iter()
.enumerate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ static SQL_TEST_CTE: LazyLock<Result<Regex, String>> =
LazyLock::new(|| compiled(SQL_TEST_CTE_PATTERN));

const INTEGER_CURSOR_FAMILIES: &[TypeFamily] = &[TypeFamily::Integer, TypeFamily::Decimal];
/// The dialect that stores timestamps as ISO-8601 text, so a text column is a timestamp cursor.
const TEXT_TIMESTAMP_DIALECT: &str = "sqlite";
const TEMPORAL_CURSOR_FAMILIES: &[TypeFamily] = &[
TypeFamily::Timestamp,
TypeFamily::Datetime,
Expand Down Expand Up @@ -170,7 +172,7 @@ fn reference_errors(
)?);
}
}
if let Some(error) = cursor_error(model, shape, context.families)? {
if let Some(error) = cursor_error(model, shape, (context.families, context.dialect))? {
errors.push(error);
}
Ok(errors)
Expand All @@ -180,7 +182,7 @@ fn reference_errors(
fn cursor_error(
model: &MetadataModel,
shape: &[(String, String)],
families: &Families,
(families, dialect): (&Families, Option<&str>),
) -> Result<Option<MetadataFinding>, SemanticFailure> {
let (Some(cursor), Some(cursor_type)) = (model.cursor.as_deref(), model.cursor_type.as_deref())
else {
Expand All @@ -203,6 +205,12 @@ fn cursor_error(
if family == TypeFamily::Other || allowed.contains(&family) {
return Ok(None);
}
if family == TypeFamily::String
&& allowed == TEMPORAL_CURSOR_FAMILIES
&& dialect == Some(TEXT_TIMESTAMP_DIALECT)
{
return Ok(None);
}
let message =
format!("cursor_type {cursor_type} does not match column '{cursor}' type {actual}");
Ok(Some(model_error(
Expand Down
5 changes: 5 additions & 0 deletions crates/sqlbuild-analysis/src/semantic_checks/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ pub(crate) const SEMANTIC_HELP: &[(&str, &str)] = &[
"rewrite the truth-value test null-safely, for example COALESCE(x, FALSE) for x IS TRUE, \
and NOT COALESCE(x, FALSE) or x IS DISTINCT FROM TRUE for x IS NOT TRUE",
),
(
"B104",
"keep the ISO-8601 text without a cast, or convert it with datetime(), date() or \
strftime()",
),
(
"B210",
"use an expression whose inferred type satisfies this SQL construct",
Expand Down
Loading
Loading