From 8a07caebfdc57b57e9147a27cb7df40348b60089 Mon Sep 17 00:00:00 2001 From: Kevin Longe <273747162+kvlonge@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:12:00 +0000 Subject: [PATCH] build: pin internal crate versions so releases stop recompiling every crate A crate's version is part of its Cargo build identity, so the shared release version in [workspace.package] recompiled all 21 crates after every release. Only sqlbuild-cli and sqlbuild-python now carry the release version; the others inherit a fixed 0.0.0. Lower crates that read CARGO_PKG_VERSION now take the release from the release crates: the built-in Rules fingerprint uses the native build identity, and the Recall schema version row records the producer version passed in through DdlOptions. The native layering check fails if a lower crate reads CARGO_PKG_VERSION again. --- .github/workflows/release-please.yml | 7 ++- .github/workflows/verify.yml | 2 +- Cargo.lock | 38 ++++++------ Cargo.toml | 5 +- crates/README.md | 13 ++-- crates/sqlbuild-cli/Cargo.toml | 2 +- crates/sqlbuild-python/Cargo.toml | 2 +- .../_helpers/recall/clickhouse_store.rs | 1 + .../src/bindings/_helpers/recall/command.rs | 1 + .../src/bindings/_helpers/recall/session.rs | 1 + .../src/clickhouse/main/store_state_sql.rs | 1 + .../src/clickhouse/tests/test_clickhouse.rs | 1 + .../src/schema/_helpers/version_row.rs | 8 +-- .../src/schema/main/view_columns.rs | 1 + crates/sqlbuild-recall/src/schema/models.rs | 4 +- .../src/schema/tests/helpers.rs | 1 + .../src/schema/tests/test_ddl.rs | 3 +- .../src/store/_helpers/schema.rs | 1 + .../src/writer/tests/helpers.rs | 1 + crates/sqlbuild-rules/src/constants.rs | 7 +-- .../src/engine/tests/test_evaluation.rs | 11 ++-- .../src/engine/tests/test_types.rs | 1 - .../src/rules/_helpers/catalogue.rs | 6 +- release-please-config.json | 9 ++- scripts/native_layering/_helpers/versions.py | 34 ++++++++++- scripts/native_layering/constants.py | 3 + .../cli/recall/test_recall_connections.py | 2 + .../native_layering/_helpers/_test_types.py | 2 + .../native_layering/_helpers/helpers.py | 28 +++++++-- .../native_layering/_helpers/test_versions.py | 61 +++++++++++++------ 30 files changed, 176 insertions(+), 81 deletions(-) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index b827895fc0..64cd17f919 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -92,8 +92,9 @@ jobs: if: steps.release.outputs.prs_created == 'true' env: RELEASE_BRANCH: ${{ steps.release_branch.outputs.name }} - # Release Please only bumps workspace versions, so updating the workspace entries of - # Cargo.lock is enough and needs no compile; metadata --locked proves the lock is complete. + # Release Please only bumps the sqlbuild-cli and sqlbuild-python crate versions, so updating + # the workspace entries of Cargo.lock is enough and needs no compile; metadata --locked + # proves the lock is complete. run: | cargo update --workspace cargo metadata --locked --format-version 1 > /dev/null @@ -198,7 +199,7 @@ jobs: # Keep this allowlist in sync with the release metadata check in verify.yml. unexpected_files="$(gh pr view "${RELEASE_PR}" --repo "${GITHUB_REPOSITORY}" \ - --json files --jq '[.files[].path | select(. != ".release-please-manifest.json" and . != "CHANGELOG.md" and . != "pyproject.toml" and . != "uv.lock" and . != "Cargo.lock" and . != "Cargo.toml")] | join("\n")')" + --json files --jq '[.files[].path | select(. != ".release-please-manifest.json" and . != "CHANGELOG.md" and . != "pyproject.toml" and . != "uv.lock" and . != "Cargo.lock" and . != "crates/sqlbuild-cli/Cargo.toml" and . != "crates/sqlbuild-python/Cargo.toml")] | join("\n")')" if [[ -n "${unexpected_files}" ]]; then echo "Release pull request contains non-metadata changes:" >&2 echo "${unexpected_files}" >&2 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 3f51d3fbb9..26a08418e8 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -87,7 +87,7 @@ jobs: changed_any=true case "${path}" in .release-please-manifest.json|CHANGELOG.md) ;; - Cargo.toml|Cargo.lock|pyproject.toml|uv.lock) + crates/sqlbuild-cli/Cargo.toml|crates/sqlbuild-python/Cargo.toml|Cargo.lock|pyproject.toml|uv.lock) diff="$(git diff -U0 --text --no-color --no-renames "${BASE_SHA}...${HEAD_SHA}" -- "${path}")" \ || return 1 offending="$(awk '/^@@/ { hunk = 1; next } hunk && /^[-+]/' <<< "${diff}" \ diff --git a/Cargo.lock b/Cargo.lock index 07324a98e1..e8bbd95b21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1209,7 +1209,7 @@ checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" [[package]] name = "sqlbuild-analysis" -version = "0.187.3" +version = "0.0.0" dependencies = [ "memchr", "polyglot-sql", @@ -1233,7 +1233,7 @@ dependencies = [ [[package]] name = "sqlbuild-attachments" -version = "0.187.3" +version = "0.0.0" dependencies = [ "sqlbuild-core", "sqlbuild-sqltext", @@ -1241,7 +1241,7 @@ dependencies = [ [[package]] name = "sqlbuild-build-features" -version = "0.187.3" +version = "0.0.0" dependencies = [ "arrayvec", "bstr", @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "sqlbuild-cache" -version = "0.187.3" +version = "0.0.0" dependencies = [ "blake3", "rayon", @@ -1291,7 +1291,7 @@ dependencies = [ [[package]] name = "sqlbuild-config" -version = "0.187.3" +version = "0.0.0" dependencies = [ "saphyr-parser", "sqlbuild-core", @@ -1302,7 +1302,7 @@ dependencies = [ [[package]] name = "sqlbuild-core" -version = "0.187.3" +version = "0.0.0" dependencies = [ "regex", "sha2", @@ -1310,14 +1310,14 @@ dependencies = [ [[package]] name = "sqlbuild-cursor" -version = "0.187.3" +version = "0.0.0" dependencies = [ "sqlbuild-core", ] [[package]] name = "sqlbuild-discovery" -version = "0.187.3" +version = "0.0.0" dependencies = [ "junction", "rayon", @@ -1331,14 +1331,14 @@ dependencies = [ [[package]] name = "sqlbuild-host" -version = "0.187.3" +version = "0.0.0" dependencies = [ "sqlbuild-core", ] [[package]] name = "sqlbuild-model-config" -version = "0.187.3" +version = "0.0.0" dependencies = [ "regex", "sqlbuild-core", @@ -1346,7 +1346,7 @@ dependencies = [ [[package]] name = "sqlbuild-project" -version = "0.187.3" +version = "0.0.0" dependencies = [ "sqlbuild-analysis", "sqlbuild-core", @@ -1355,7 +1355,7 @@ dependencies = [ [[package]] name = "sqlbuild-pysource" -version = "0.187.3" +version = "0.0.0" dependencies = [ "ruff_python_ast", "ruff_python_parser", @@ -1402,7 +1402,7 @@ dependencies = [ [[package]] name = "sqlbuild-recall" -version = "0.187.3" +version = "0.0.0" dependencies = [ "blake3", "rusqlite", @@ -1416,7 +1416,7 @@ dependencies = [ [[package]] name = "sqlbuild-refactor" -version = "0.187.3" +version = "0.0.0" dependencies = [ "serde", "serde_json", @@ -1431,7 +1431,7 @@ dependencies = [ [[package]] name = "sqlbuild-render" -version = "0.187.3" +version = "0.0.0" dependencies = [ "serde_json", "sqlbuild-cache", @@ -1442,7 +1442,7 @@ dependencies = [ [[package]] name = "sqlbuild-rules" -version = "0.187.3" +version = "0.0.0" dependencies = [ "blake3", "fensu-policy", @@ -1468,7 +1468,7 @@ dependencies = [ [[package]] name = "sqlbuild-scopes" -version = "0.187.3" +version = "0.0.0" dependencies = [ "serde_json", "sha2", @@ -1481,7 +1481,7 @@ dependencies = [ [[package]] name = "sqlbuild-sqltext" -version = "0.187.3" +version = "0.0.0" dependencies = [ "rayon", "serde", @@ -1490,7 +1490,7 @@ dependencies = [ [[package]] name = "sqlbuild-state" -version = "0.187.3" +version = "0.0.0" dependencies = [ "serde_json", "sha2", diff --git a/Cargo.toml b/Cargo.toml index 9bd219e053..c31227234f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,10 @@ python-boundary = "sqlbuild-python" polyglot-floor = "sqlbuild-analysis" [workspace.package] -version = "0.187.3" +# Fixed on purpose: a crate's version is part of its Cargo build identity, so a shared release +# version would recompile every crate after each release. The release version lives in the +# sqlbuild-cli and sqlbuild-python manifests; see crates/README.md. +version = "0.0.0" edition = "2024" license = "Apache-2.0" publish = false diff --git a/crates/README.md b/crates/README.md index 4aaac11815..c29eb79033 100644 --- a/crates/README.md +++ b/crates/README.md @@ -54,9 +54,9 @@ in `[workspace.metadata.native-layers]` in the workspace `Cargo.toml`, and - any crate other than `sqlbuild-python` reaches `pyo3`, directly or through another package; - a crate below `sqlbuild-analysis` reaches polyglot. -The same check fails when the workspace version, the `pyproject.toml` version and -`.release-please-manifest.json` disagree, or when a workspace crate's `Cargo.lock` entry carries a -version other than its manifest's, because the version is part of the build identity. +The same check fails when the `sqlbuild-cli` and `sqlbuild-python` versions, the `pyproject.toml` +version and `.release-please-manifest.json` disagree, or when a workspace crate's `Cargo.lock` +entry carries a version other than its manifest's. `fensu check` applies the Rust structure rules inside each crate: other crates use items through `main/` entry points, `models.rs`, `types.rs` and `constants.rs`, not through `_helpers/`. @@ -113,5 +113,8 @@ playground project on Windows. `cargo test -p ` or for all crates with `cargo test --workspace`. - The build identity (`sqlbuild._native.BUILD_IDENTITY`) hashes the sources of every crate in this directory, so editing any crate invalidates the native caches keyed on it. -- maturin builds `crates/sqlbuild-python`; the release version lives in - `[workspace.package]` and is shared by every crate. +- maturin builds `crates/sqlbuild-python`; the release version lives in the `sqlbuild-cli` and + `sqlbuild-python` manifests. Every other crate inherits the fixed `[workspace.package]` version, + because a crate's version is part of its Cargo build identity: a shared release version would + recompile every crate after each release. Read the release version from those two crates, not + from `CARGO_PKG_VERSION` in a lower crate. diff --git a/crates/sqlbuild-cli/Cargo.toml b/crates/sqlbuild-cli/Cargo.toml index 4a2253fb56..bb46f07f73 100644 --- a/crates/sqlbuild-cli/Cargo.toml +++ b/crates/sqlbuild-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlbuild-cli" -version.workspace = true +version = "0.187.3" description = "The native `sqb` executable: reused compiles and Python-free commands served without Python, every other command handed to the Python CLI." edition.workspace = true license-file = "LICENSE" diff --git a/crates/sqlbuild-python/Cargo.toml b/crates/sqlbuild-python/Cargo.toml index 2219287ac5..82967c115d 100644 --- a/crates/sqlbuild-python/Cargo.toml +++ b/crates/sqlbuild-python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sqlbuild-python" -version.workspace = true +version = "0.187.3" description = "The sqlbuild._native Python extension module." edition.workspace = true license.workspace = true diff --git a/crates/sqlbuild-python/src/bindings/_helpers/recall/clickhouse_store.rs b/crates/sqlbuild-python/src/bindings/_helpers/recall/clickhouse_store.rs index 45d6700ca0..5151a20798 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/recall/clickhouse_store.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/recall/clickhouse_store.rs @@ -74,6 +74,7 @@ impl ClickHouseLocation { database: Some(database.to_owned()), retention_days: retention, cluster: None, + producer_version: env!("CARGO_PKG_VERSION").to_owned(), }, } } diff --git a/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs b/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs index ad12342ac4..5ba2193fc8 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/recall/command.rs @@ -338,6 +338,7 @@ fn run_schema_ddl(py: Python<'_>, settings: &RecallSettings) -> PyResult { database: None, retention_days: settings.retention_days, cluster: None, + producer_version: env!("CARGO_PKG_VERSION").to_owned(), }; let statements = if path.exists() { open_sqlite_store(path, OpenMode::ReadOnly) diff --git a/crates/sqlbuild-python/src/bindings/_helpers/recall/session.rs b/crates/sqlbuild-python/src/bindings/_helpers/recall/session.rs index 85714984a5..5d625d3b49 100644 --- a/crates/sqlbuild-python/src/bindings/_helpers/recall/session.rs +++ b/crates/sqlbuild-python/src/bindings/_helpers/recall/session.rs @@ -348,6 +348,7 @@ fn permission_notice( database, retention_days: settings.retention_days, cluster: None, + producer_version: env!("CARGO_PKG_VERSION").to_owned(), }; let database_missing: bool = options .database diff --git a/crates/sqlbuild-recall/src/clickhouse/main/store_state_sql.rs b/crates/sqlbuild-recall/src/clickhouse/main/store_state_sql.rs index 4078f26baf..c1436c23dc 100644 --- a/crates/sqlbuild-recall/src/clickhouse/main/store_state_sql.rs +++ b/crates/sqlbuild-recall/src/clickhouse/main/store_state_sql.rs @@ -30,6 +30,7 @@ pub fn store_state_sql(database: &str) -> StoreStateSql { database: Some(database.to_owned()), retention_days: 0, cluster: None, + producer_version: String::new(), }; StoreStateSql { relations: format!( diff --git a/crates/sqlbuild-recall/src/clickhouse/tests/test_clickhouse.rs b/crates/sqlbuild-recall/src/clickhouse/tests/test_clickhouse.rs index e3f6d0f5a8..9d0afbaf71 100644 --- a/crates/sqlbuild-recall/src/clickhouse/tests/test_clickhouse.rs +++ b/crates/sqlbuild-recall/src/clickhouse/tests/test_clickhouse.rs @@ -16,6 +16,7 @@ const OPTIONS: fn() -> DdlOptions = || DdlOptions { database: Some("observability".to_owned()), retention_days: 30, cluster: None, + producer_version: "9.8.7".to_owned(), }; #[test] diff --git a/crates/sqlbuild-recall/src/schema/_helpers/version_row.rs b/crates/sqlbuild-recall/src/schema/_helpers/version_row.rs index 90b00ed303..658b51a58a 100644 --- a/crates/sqlbuild-recall/src/schema/_helpers/version_row.rs +++ b/crates/sqlbuild-recall/src/schema/_helpers/version_row.rs @@ -16,12 +16,12 @@ fn clickhouse_version_row(options: &DdlOptions) -> String { '{RECALL_SCHEMA_VERSION}', now64(6), '{DDL_SCRIPT_INVOCATION}', now64(6), \ {RECALL_SCHEMA_VERSION}, '{version}', now64(6), ''", meta = clickhouse_table(table_spec(Table::Meta).name, options), - version = env!("CARGO_PKG_VERSION"), + version = options.producer_version, ) } /// The SQLite `recall_meta` row recording the schema version. -fn sqlite_version_row() -> String { +fn sqlite_version_row(options: &DdlOptions) -> String { let now = "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')"; format!( "INSERT OR IGNORE INTO \"{meta}\" (component, key, value, recorded_at, invocation_id, \ @@ -30,7 +30,7 @@ fn sqlite_version_row() -> String { '{RECALL_SCHEMA_VERSION}', {now}, '{DDL_SCRIPT_INVOCATION}', {now}, \ {RECALL_SCHEMA_VERSION}, '{version}', {now}, '')", meta = table_spec(Table::Meta).name, - version = env!("CARGO_PKG_VERSION"), + version = options.producer_version, ) } @@ -38,7 +38,7 @@ fn sqlite_version_row() -> String { #[must_use] pub(crate) fn version_row_statement(dialect: Dialect, options: &DdlOptions) -> String { match dialect { - Dialect::Sqlite => sqlite_version_row(), + Dialect::Sqlite => sqlite_version_row(options), Dialect::ClickHouse => clickhouse_version_row(options), } } diff --git a/crates/sqlbuild-recall/src/schema/main/view_columns.rs b/crates/sqlbuild-recall/src/schema/main/view_columns.rs index ac8be7893f..dab72b9337 100644 --- a/crates/sqlbuild-recall/src/schema/main/view_columns.rs +++ b/crates/sqlbuild-recall/src/schema/main/view_columns.rs @@ -13,6 +13,7 @@ pub fn view_columns() -> Result>, String database: None, retention_days: 1, cluster: None, + producer_version: String::new(), }; connection .execute_batch(&ddl_script(Dialect::Sqlite, &options)) diff --git a/crates/sqlbuild-recall/src/schema/models.rs b/crates/sqlbuild-recall/src/schema/models.rs index 022e4f0b84..79386f14ce 100644 --- a/crates/sqlbuild-recall/src/schema/models.rs +++ b/crates/sqlbuild-recall/src/schema/models.rs @@ -89,12 +89,14 @@ pub enum Table { Conflicts, } -/// What DDL is rendered with: database (ClickHouse), retention and cluster. +/// What DDL is rendered with: database (ClickHouse), retention, cluster and producing release. #[derive(Clone, Debug, PartialEq, Eq)] pub struct DdlOptions { pub database: Option, pub retention_days: u32, pub cluster: Option, + /// The sqlbuild release the schema version row records; this crate's own version is fixed. + pub producer_version: String, } /// The statements SQLite maintenance runs, with their positional parameters. diff --git a/crates/sqlbuild-recall/src/schema/tests/helpers.rs b/crates/sqlbuild-recall/src/schema/tests/helpers.rs index d67b4d9c1d..b245808173 100644 --- a/crates/sqlbuild-recall/src/schema/tests/helpers.rs +++ b/crates/sqlbuild-recall/src/schema/tests/helpers.rs @@ -14,6 +14,7 @@ pub(crate) fn options(cluster: Option<&str>) -> DdlOptions { database: Some("observability".to_owned()), retention_days: 14, cluster: cluster.map(str::to_owned), + producer_version: "9.8.7".to_owned(), } } diff --git a/crates/sqlbuild-recall/src/schema/tests/test_ddl.rs b/crates/sqlbuild-recall/src/schema/tests/test_ddl.rs index 588e30a678..a3cf64d08e 100644 --- a/crates/sqlbuild-recall/src/schema/tests/test_ddl.rs +++ b/crates/sqlbuild-recall/src/schema/tests/test_ddl.rs @@ -146,7 +146,7 @@ fn given_clickhouse_statement_sets_when_rendered_then_every_statement_has_the_sh expected_count: VIEW_NAMES.len(), }, StatementSetTestCase { - description: "the administrator's script ends by recording the schema version", + description: "the administrator's script ends by recording the schema version and the producing release", statements: ddl_script(Dialect::ClickHouse, &options(None)) .rsplit(";\n\n") .take(1) @@ -155,6 +155,7 @@ fn given_clickhouse_statement_sets_when_rendered_then_every_statement_has_the_sh required_fragments: &[ "INSERT INTO `observability`.`recall_meta`", "SELECT 'schema', 'schema_version', '5'", + "5, '9.8.7', now64(6)", ], expected_count: 1, }, diff --git a/crates/sqlbuild-recall/src/store/_helpers/schema.rs b/crates/sqlbuild-recall/src/store/_helpers/schema.rs index 6b4c35bfd7..208a8144bf 100644 --- a/crates/sqlbuild-recall/src/store/_helpers/schema.rs +++ b/crates/sqlbuild-recall/src/store/_helpers/schema.rs @@ -17,6 +17,7 @@ pub(crate) fn sqlite_options() -> DdlOptions { database: None, retention_days: 0, cluster: None, + producer_version: String::new(), } } diff --git a/crates/sqlbuild-recall/src/writer/tests/helpers.rs b/crates/sqlbuild-recall/src/writer/tests/helpers.rs index 97a319ea81..98d4956e20 100644 --- a/crates/sqlbuild-recall/src/writer/tests/helpers.rs +++ b/crates/sqlbuild-recall/src/writer/tests/helpers.rs @@ -494,6 +494,7 @@ pub(crate) fn core_release_store(path: &std::path::Path) { database: None, retention_days: 30, cluster: None, + producer_version: "9.8.7".to_owned(), }; std::fs::create_dir_all(path.parent().expect("the store has a directory")) .expect("the store directory can be created"); diff --git a/crates/sqlbuild-rules/src/constants.rs b/crates/sqlbuild-rules/src/constants.rs index 250c806f9d..37b4e3f953 100644 --- a/crates/sqlbuild-rules/src/constants.rs +++ b/crates/sqlbuild-rules/src/constants.rs @@ -1,9 +1,6 @@ pub const API_VERSION: u32 = 1; -pub const NATIVE_BUILD_IDENTITY: &str = concat!( - env!("CARGO_PKG_VERSION"), - "+", - env!("SQLBUILD_NATIVE_SOURCE_HASH") -); +/// A content hash of every native crate, including the release version in their manifests. +pub const NATIVE_BUILD_IDENTITY: &str = env!("SQLBUILD_NATIVE_SOURCE_HASH"); pub(crate) const SCOPE_METADATA_SCHEMA_VERSION: u32 = 3; pub(crate) const BUILT_IN_RULE_NAMESPACE: &str = "SQBR"; pub(crate) const PIPELINE_SUBJECT: &str = "pipeline"; diff --git a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs index 819e600020..26cef0a1e1 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_evaluation.rs @@ -1445,18 +1445,17 @@ fn given_threshold_override_change_when_evaluating_then_ruleset_fingerprint_chan } #[test] -fn given_native_build_when_reading_cache_identity_then_version_carries_source_hash() { +fn given_native_build_when_reading_cache_identity_then_it_is_a_source_hash() { let test_cases = [test_types::BuildIdentityTestCase { - description: "cache identity is the package version plus a source content hash", - expected_prefix: concat!(env!("CARGO_PKG_VERSION"), "+"), + description: "cache identity is a source content hash", expected_hash_length: 16, }]; for test_case in &test_cases { - let hash = NATIVE_BUILD_IDENTITY.strip_prefix(test_case.expected_prefix); + let hash = NATIVE_BUILD_IDENTITY; assert!( - hash.is_some_and(|hash| hash.len() == test_case.expected_hash_length - && hash.bytes().all(|byte| byte.is_ascii_hexdigit())), + hash.len() == test_case.expected_hash_length + && hash.bytes().all(|byte| byte.is_ascii_hexdigit()), "{}", test_case.description ); diff --git a/crates/sqlbuild-rules/src/engine/tests/test_types.rs b/crates/sqlbuild-rules/src/engine/tests/test_types.rs index c18a15c5ad..fb3fd5bbea 100644 --- a/crates/sqlbuild-rules/src/engine/tests/test_types.rs +++ b/crates/sqlbuild-rules/src/engine/tests/test_types.rs @@ -105,7 +105,6 @@ pub(crate) struct ThresholdEvaluationTestCase { pub(crate) struct BuildIdentityTestCase { pub(crate) description: &'static str, - pub(crate) expected_prefix: &'static str, pub(crate) expected_hash_length: usize, } diff --git a/crates/sqlbuild-rules/src/rules/_helpers/catalogue.rs b/crates/sqlbuild-rules/src/rules/_helpers/catalogue.rs index 23d3f563f1..509a23fe01 100644 --- a/crates/sqlbuild-rules/src/rules/_helpers/catalogue.rs +++ b/crates/sqlbuild-rules/src/rules/_helpers/catalogue.rs @@ -18,7 +18,7 @@ macro_rules! rule { message: $message.into(), remediation: $remediation.into(), guidance: None, - implementation_fingerprint: env!("CARGO_PKG_VERSION").into(), + implementation_fingerprint: NATIVE_BUILD_IDENTITY.into(), enabled_by_default: true, project_wide: matches!( $code, @@ -384,7 +384,7 @@ pub(crate) fn catalogue() -> Vec { message: rule.message.into(), remediation: rule.remediation.into(), guidance: None, - implementation_fingerprint: env!("CARGO_PKG_VERSION").into(), + implementation_fingerprint: NATIVE_BUILD_IDENTITY.into(), enabled_by_default: defaults.contains(&rule.code), project_wide: false, custom: false, @@ -413,7 +413,7 @@ fn minimum_tests_rule() -> RuleMetadata { "Add a SQL unit test that mocks each real import and asserts concrete transformed rows. Empty-input-only tests flagged by SQBRTEST203 do not count toward this minimum unless they are listed in its allowed_tests option; do not write them to satisfy the minimum.", ), guidance: Some(guidance), - implementation_fingerprint: env!("CARGO_PKG_VERSION").into(), + implementation_fingerprint: NATIVE_BUILD_IDENTITY.into(), enabled_by_default: true, project_wide: false, custom: false, diff --git a/release-please-config.json b/release-please-config.json index 59d86efda8..7abc409aa0 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -67,8 +67,13 @@ "extra-files": [ { "type": "toml", - "path": "Cargo.toml", - "jsonpath": "$.workspace.package.version" + "path": "crates/sqlbuild-cli/Cargo.toml", + "jsonpath": "$.package.version" + }, + { + "type": "toml", + "path": "crates/sqlbuild-python/Cargo.toml", + "jsonpath": "$.package.version" }, { "type": "toml", diff --git a/scripts/native_layering/_helpers/versions.py b/scripts/native_layering/_helpers/versions.py index 8cbcbd57a8..ebbed277f8 100644 --- a/scripts/native_layering/_helpers/versions.py +++ b/scripts/native_layering/_helpers/versions.py @@ -11,10 +11,13 @@ CARGO_LOCKFILE, CARGO_MANIFEST, CLI_DISTRIBUTION_PIN_PREFIX, + CRATES_DIRECTORY, LOCK_SOURCE_KEY, + PACKAGE_VERSION_VARIABLE, PYPROJECT_LOCK_SOURCE, PYPROJECT_LOCKFILE, PYPROJECT_MANIFEST, + RELEASE_CRATES, RELEASE_MANIFEST, RELEASE_MANIFEST_PACKAGE, ) @@ -28,8 +31,12 @@ def get_native_version_errors(root: Path) -> tuple[str, ...]: pyproject: dict[str, Any] = tomllib.load(handle) release: dict[str, Any] = json.loads((root / RELEASE_MANIFEST).read_text(encoding="utf-8")) workspace_version: Any = cargo.get("workspace", {}).get("package", {}).get("version") - versions: dict[str, Any] = { - f"{CARGO_MANIFEST} [workspace.package] version": workspace_version, + versions: dict[str, Any] = {} + for name in RELEASE_CRATES: + manifest: str = f"{CRATES_DIRECTORY}/{name}/{CARGO_MANIFEST}" + with (root / manifest).open("rb") as handle: + versions[f"{manifest} version"] = tomllib.load(handle).get("package", {}).get("version") + versions |= { f"{PYPROJECT_MANIFEST} [project] version": pyproject.get("project", {}).get("version"), f"{RELEASE_MANIFEST} version": release.get(RELEASE_MANIFEST_PACKAGE), f"{PYPROJECT_MANIFEST} sqlbuild-cli dependency": next( @@ -49,9 +56,32 @@ def get_native_version_errors(root: Path) -> tuple[str, ...]: _cargo_lockfile_errors(root=root, cargo=cargo, workspace_version=workspace_version) ) errors.extend(_python_lockfile_errors(root=root, pyproject=pyproject)) + errors.extend(_fixed_version_reader_errors(root=root, cargo=cargo)) return tuple(errors) +def _fixed_version_reader_errors(*, root: Path, cargo: dict[str, Any]) -> tuple[str, ...]: + """Report Rust sources outside the release crates that read their fixed crate version.""" + readers: list[str] = [] + for pattern in cargo.get("workspace", {}).get("members", ()): + for directory in sorted(root.glob(pattern)): + with (directory / CARGO_MANIFEST).open("rb") as handle: + name: Any = tomllib.load(handle).get("package", {}).get("name") + if name in RELEASE_CRATES: + continue + readers.extend( + source.relative_to(root).as_posix() + for source in sorted(directory.rglob("*.rs")) + if PACKAGE_VERSION_VARIABLE in source.read_text(encoding="utf-8") + ) + release_crates: str = " and ".join(RELEASE_CRATES) + return tuple( + f"{reader} reads {PACKAGE_VERSION_VARIABLE}, which is the fixed workspace version " + f"outside {release_crates}; take the release version from those crates." + for reader in readers + ) + + def _python_lockfile_errors(*, root: Path, pyproject: dict[str, Any]) -> tuple[str, ...]: """Report a uv.lock entry for the project that carries a version other than pyproject's.""" project: dict[str, Any] = pyproject.get("project", {}) diff --git a/scripts/native_layering/constants.py b/scripts/native_layering/constants.py index cf19fd3f89..b073b83419 100644 --- a/scripts/native_layering/constants.py +++ b/scripts/native_layering/constants.py @@ -13,3 +13,6 @@ RELEASE_MANIFEST: str = ".release-please-manifest.json" RELEASE_MANIFEST_PACKAGE: str = "." CLI_DISTRIBUTION_PIN_PREFIX: str = "sqlbuild-cli==" +CRATES_DIRECTORY: str = "crates" +RELEASE_CRATES: tuple[str, ...] = ("sqlbuild-cli", "sqlbuild-python") +PACKAGE_VERSION_VARIABLE: str = "CARGO_PKG_VERSION" diff --git a/tests/integration/src/sqlbuild/cli/recall/test_recall_connections.py b/tests/integration/src/sqlbuild/cli/recall/test_recall_connections.py index 04fdac798d..8f44476ba9 100644 --- a/tests/integration/src/sqlbuild/cli/recall/test_recall_connections.py +++ b/tests/integration/src/sqlbuild/cli/recall/test_recall_connections.py @@ -1,6 +1,7 @@ import json import sqlite3 import subprocess +from importlib.metadata import version from pathlib import Path import pytest @@ -122,6 +123,7 @@ def test_given_only_a_read_connection_when_running_then_reads_are_shared_and_wri 'DROP VIEW IF EXISTS "invocations";', 'CREATE VIEW IF NOT EXISTS "invocations" AS', 'INSERT OR IGNORE INTO "recall_meta"', + f"'{version('sqlbuild')}', strftime(", ), expected_up_to_date_note="is up to date with this SQLBuild version", ), diff --git a/tests/unit/scripts/native_layering/_helpers/_test_types.py b/tests/unit/scripts/native_layering/_helpers/_test_types.py index 1737ba390f..6052ce3043 100644 --- a/tests/unit/scripts/native_layering/_helpers/_test_types.py +++ b/tests/unit/scripts/native_layering/_helpers/_test_types.py @@ -21,9 +21,11 @@ class RepositoryLayeringTestCase: class NativeVersionTestCase: description: str workspace_version: str + crate_version: str pyproject_version: str release_version: str cli_version: str expected_errors: tuple[str, ...] lock_versions: dict[str, str] = field(default_factory=dict) python_lock_version: str | None = None + version_readers: tuple[str, ...] = () diff --git a/tests/unit/scripts/native_layering/_helpers/helpers.py b/tests/unit/scripts/native_layering/_helpers/helpers.py index db682bd797..400a4d27f6 100644 --- a/tests/unit/scripts/native_layering/_helpers/helpers.py +++ b/tests/unit/scripts/native_layering/_helpers/helpers.py @@ -1,5 +1,6 @@ from pathlib import Path +from scripts.native_layering.constants import RELEASE_CRATES from tests.unit.scripts.native_layering._helpers._test_types import ( NativeLayeringTestCase, NativeVersionTestCase, @@ -45,11 +46,21 @@ def _lock_package(name: str, dependencies: tuple[str, ...]) -> str: def write_versions(root: Path, test_case: NativeVersionTestCase) -> None: """Write the release version sources and the lockfile entries the version check compares. - Every crate in `lock_versions` is a workspace member that inherits the workspace version, and - its Cargo.lock entry carries the mapped version; a registry crate rides along unchanged. The - uv.lock project entry carries `python_lock_version`, defaulting to the pyproject version. + The release crates set `crate_version` and every other crate in `lock_versions` inherits the + workspace version. Each Cargo.lock entry carries the mapped version, defaulting to the + crate's own; a registry crate rides along unchanged. The uv.lock project entry carries + `python_lock_version`, defaulting to the pyproject version. Each crate in `version_readers` + reads its package version in `src/lib.rs`. """ - members: str = ", ".join(f'"crates/{name}"' for name in test_case.lock_versions) + lock_versions: dict[str, str] = { + **dict.fromkeys(RELEASE_CRATES, test_case.crate_version), + **test_case.lock_versions, + } + manifest_versions: dict[str, str] = { + **dict.fromkeys(lock_versions, "version.workspace = true"), + **dict.fromkeys(RELEASE_CRATES, f'version = "{test_case.crate_version}"'), + } + members: str = ", ".join(f'"crates/{name}"' for name in lock_versions) (root / "Cargo.toml").write_text( f"[workspace]\nmembers = [{members}]\n\n" f'[workspace.package]\nversion = "{test_case.workspace_version}"\n', @@ -60,14 +71,19 @@ def write_versions(root: Path, test_case: NativeVersionTestCase) -> None: '[[package]]\nname = "serde"\nversion = "1.0.0"\n' 'source = "registry+https://github.com/rust-lang/crates.io-index"\n', ] - for name, version in test_case.lock_versions.items(): + for name, version in lock_versions.items(): crate: Path = root / "crates" / name crate.mkdir(parents=True) (crate / "Cargo.toml").write_text( - f'[package]\nname = "{name}"\nversion.workspace = true\n', encoding="utf-8" + f'[package]\nname = "{name}"\n{manifest_versions[name]}\n', encoding="utf-8" ) lock.append(f'[[package]]\nname = "{name}"\nversion = "{version}"\n') (root / "Cargo.lock").write_text("\n".join(lock), encoding="utf-8") + for name in test_case.version_readers: + (root / "crates" / name / "src").mkdir() + (root / "crates" / name / "src" / "lib.rs").write_text( + 'pub const VERSION: &str = env!("CARGO_PKG_VERSION");\n', encoding="utf-8" + ) python_lock_version: str = test_case.python_lock_version or test_case.pyproject_version (root / "uv.lock").write_text( "version = 1\n\n" diff --git a/tests/unit/scripts/native_layering/_helpers/test_versions.py b/tests/unit/scripts/native_layering/_helpers/test_versions.py index 8706b2d762..95b5034b02 100644 --- a/tests/unit/scripts/native_layering/_helpers/test_versions.py +++ b/tests/unit/scripts/native_layering/_helpers/test_versions.py @@ -14,13 +14,15 @@ "test_case", [ NativeVersionTestCase( - description="every release version source agrees", - workspace_version="1.2.0", + description="release crates agree with the release while the rest keep a fixed version", + workspace_version="0.0.0", + crate_version="1.2.0", pyproject_version="1.2.0", release_version="1.2.0", cli_version="1.2.0", expected_errors=(), - lock_versions={"example-core": "1.2.0", "example-cli": "1.2.0"}, + lock_versions={"example-core": "0.0.0"}, + version_readers=("sqlbuild-cli", "sqlbuild-python"), ), ], ids=lambda case: case.description, @@ -37,13 +39,15 @@ def test_given_matching_versions_when_checking_then_returns_no_errors( "test_case", [ NativeVersionTestCase( - description="workspace version lags a release", - workspace_version="1.1.9", + description="release crate versions lag a release", + workspace_version="0.0.0", + crate_version="1.1.9", pyproject_version="1.2.0", release_version="1.2.0", cli_version="1.2.0", expected_errors=( - "Release versions differ: Cargo.toml [workspace.package] version is 1.1.9, " + "Release versions differ: crates/sqlbuild-cli/Cargo.toml version is 1.1.9, " + "crates/sqlbuild-python/Cargo.toml version is 1.1.9, " "pyproject.toml [project] version is 1.2.0, " ".release-please-manifest.json version is 1.2.0, " "pyproject.toml sqlbuild-cli dependency is 1.2.0.", @@ -51,53 +55,72 @@ def test_given_matching_versions_when_checking_then_returns_no_errors( ), NativeVersionTestCase( description="native executable pin lags a release", - workspace_version="1.2.0", + workspace_version="0.0.0", + crate_version="1.2.0", pyproject_version="1.2.0", release_version="1.2.0", cli_version="1.1.9", expected_errors=( - "Release versions differ: Cargo.toml [workspace.package] version is 1.2.0, " + "Release versions differ: crates/sqlbuild-cli/Cargo.toml version is 1.2.0, " + "crates/sqlbuild-python/Cargo.toml version is 1.2.0, " "pyproject.toml [project] version is 1.2.0, " ".release-please-manifest.json version is 1.2.0, " "pyproject.toml sqlbuild-cli dependency is 1.1.9.", ), ), NativeVersionTestCase( - description="lockfile kept the previous workspace version after a release bump", - workspace_version="1.2.0", + description="lockfile kept the previous release crate versions after a release bump", + workspace_version="0.0.0", + crate_version="1.2.0", pyproject_version="1.2.0", release_version="1.2.0", cli_version="1.2.0", - lock_versions={"example-core": "1.1.9", "example-cli": "1.1.9"}, + lock_versions={"sqlbuild-cli": "1.1.9", "sqlbuild-python": "1.1.9"}, expected_errors=( - "Cargo.lock workspace crate versions are stale: example-cli is 1.1.9, not 1.2.0; " - "example-core is 1.1.9, not 1.2.0. Run `cargo update --workspace`.", + "Cargo.lock workspace crate versions are stale: sqlbuild-cli is 1.1.9, not 1.2.0; " + "sqlbuild-python is 1.1.9, not 1.2.0. Run `cargo update --workspace`.", ), ), NativeVersionTestCase( description="uv lockfile kept the previous project version after a release bump", - workspace_version="1.2.0", + workspace_version="0.0.0", + crate_version="1.2.0", pyproject_version="1.2.0", release_version="1.2.0", cli_version="1.2.0", - lock_versions={"example-cli": "1.2.0"}, python_lock_version="1.1.9", expected_errors=( "uv.lock locks example at 1.1.9, not pyproject.toml 1.2.0. Run `uv lock`.", ), ), NativeVersionTestCase( - description="one workspace crate lags in the lockfile", - workspace_version="1.2.0", + description="a fixed-version crate lags its workspace version in the lockfile", + workspace_version="0.0.0", + crate_version="1.2.0", pyproject_version="1.2.0", release_version="1.2.0", cli_version="1.2.0", - lock_versions={"example-core": "1.2.0", "example-cli": "1.1.9"}, + lock_versions={"example-core": "1.1.9"}, expected_errors=( - "Cargo.lock workspace crate versions are stale: example-cli is 1.1.9, not 1.2.0. " + "Cargo.lock workspace crate versions are stale: example-core is 1.1.9, not 0.0.0. " "Run `cargo update --workspace`.", ), ), + NativeVersionTestCase( + description="a fixed-version crate reads its package version", + workspace_version="0.0.0", + crate_version="1.2.0", + pyproject_version="1.2.0", + release_version="1.2.0", + cli_version="1.2.0", + lock_versions={"example-core": "0.0.0"}, + version_readers=("example-core", "sqlbuild-cli"), + expected_errors=( + "crates/example-core/src/lib.rs reads CARGO_PKG_VERSION, which is the fixed " + "workspace version outside sqlbuild-cli and sqlbuild-python; take the release " + "version from those crates.", + ), + ), ], ids=lambda case: case.description, )