Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e6ab072
Upgrade Squawk and guard GitHub environment setup
dsecurity49 Sep 5, 2026
730416f
Keep tests out of the interactive terminal
dsecurity49 Sep 5, 2026
6e05b79
Build the supported Rust API
dsecurity49 Sep 6, 2026
95165a8
feat: harden the v0.9 analysis model
dsecurity49 Sep 8, 2026
6587020
fix: preserve catalog state across table and partition changes
dsecurity49 Sep 9, 2026
3204e72
docs: describe migration coverage without roadmap labels
dsecurity49 Sep 9, 2026
d1844de
fix: preserve index metadata through catalog changes
dsecurity49 Sep 12, 2026
6c15fb2
fix: harden partition and inherited catalog transitions
dsecurity49 Sep 14, 2026
4daba63
docs: clarify contribution workflow and summarize v0.9 changes
dsecurity49 Sep 14, 2026
7d00bf0
Merge main into v0.9.0
dsecurity49 Sep 14, 2026
abf0ce1
fix: refresh V8 cache contracts
dsecurity49 Sep 14, 2026
886c4c6
fix: stabilize live catalog and fixture checks
dsecurity49 Sep 14, 2026
a4c51bf
fix: stabilize parser and catalog CI checks
dsecurity49 Sep 14, 2026
a744aba
fix: preserve partition predicates during attach
dsecurity49 Sep 14, 2026
6449bdc
fix: include legacy partition trigger clones
dsecurity49 Sep 14, 2026
ba6d52d
fix: align partition and catalog normalization
dsecurity49 Sep 14, 2026
26f6606
fix: hydrate created partition predicates
dsecurity49 Sep 14, 2026
70b412f
fix: refresh default partition predicates
dsecurity49 Sep 14, 2026
47feb83
fix: match default partition predicate deparse
dsecurity49 Sep 14, 2026
92270f0
fix: match default partition deparse grouping
dsecurity49 Sep 14, 2026
aa69072
fix: preserve uncertain partition attachments
dsecurity49 Sep 14, 2026
1337ead
fix: retain PostgreSQL 14 partition topology
dsecurity49 Sep 14, 2026
1835930
fix: keep PostgreSQL 14 attach fixtures versioned
dsecurity49 Sep 14, 2026
fe5bfbf
docs: date the v0.9.0 changelog
dsecurity49 Sep 15, 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
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/database-feedback.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ body:
label: Analysis baseline
options:
- Fresh `safe-migrate sync`
- Existing Cache V7
- Existing Cache V8
- '`auto_sync = true`'
- '`--no-cache`'
validations:
Expand Down
52 changes: 50 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
run: cargo build --locked

- name: Exercise CLI runtime
run: cargo test --locked --test cli_tests test_cli_help
run: cargo test --locked --lib internal_tests::cli_tests::test_cli_help -- --exact

live-differential:
name: PostgreSQL ${{ matrix.postgres }} differential harness
Expand Down Expand Up @@ -138,8 +138,23 @@ jobs:
--health-retries 5
ports:
- 5432:5432
publisher:
image: ${{ matrix.image }}
env:
POSTGRES_DB: safe_migrate
POSTGRES_USER: safe_migrate
POSTGRES_PASSWORD: safe_migrate
options: >-
--health-cmd "pg_isready -U safe_migrate -d safe_migrate"
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5433:5432
env:
DATABASE_URL: postgres://safe_migrate:safe_migrate@localhost:5432/safe_migrate
PUBLISHER_DATABASE_URL: postgres://safe_migrate:safe_migrate@localhost:5433/safe_migrate
SUBSCRIPTION_DATABASE_URL: postgres://safe_migrate:safe_migrate@publisher:5432/safe_migrate

steps:
- name: Checkout repository
Expand All @@ -153,6 +168,36 @@ jobs:
- name: Cache dependencies
uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2

- name: Enable logical replication on publisher
shell: bash
run: |
publisher_id='${{ job.services.publisher.id }}'
docker exec "$publisher_id" psql -U safe_migrate -d safe_migrate \
-v ON_ERROR_STOP=1 -c "ALTER SYSTEM SET wal_level = 'logical'"
docker restart "$publisher_id"
for attempt in {1..30}; do
if docker exec "$publisher_id" \
pg_isready -U safe_migrate -d safe_migrate; then
exit 0
fi
sleep 1
done
docker logs "$publisher_id"
exit 1

- name: Wait for PostgreSQL services
shell: bash
run: |
for port in 5432 5433; do
for attempt in {1..30}; do
if pg_isready -h 127.0.0.1 -p "$port" -U safe_migrate -d safe_migrate; then
break
fi
sleep 1
done
pg_isready -h 127.0.0.1 -p "$port" -U safe_migrate -d safe_migrate
done

- name: Reject an unreachable live database
shell: bash
env:
Expand All @@ -178,6 +223,9 @@ jobs:
- name: Compare routine and replication state with PostgreSQL
run: scripts/live-catalog-differential

- name: Validate a connected logical subscription
run: scripts/live-connected-subscription

- name: Compare simulator state with PostgreSQL
shell: bash
run: |
Expand Down Expand Up @@ -260,7 +308,7 @@ jobs:
run: |
test "$SYNC_STATUS" = "refreshed"
test "$BASELINE_SOURCE" = "synced"
test "$CACHE_PATH" = "$HOME/.cache/safe-migrate-action/baselines/action-smoke/baseline-v7.cache"
test "$CACHE_PATH" = "$HOME/.cache/safe-migrate-action/baselines/action-smoke/baseline-v8.cache"
test -z "$JSON_REPORT"
rm -f -- "$CACHE_PATH"

Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,30 @@ commits and pull requests. Published binaries, checksums, and generated release
notes are available on the
[GitHub Releases page](https://github.com/dsecurity49/safe-migrate/releases).

## v0.9.0 — 2026-09-15

- Established `safe_migrate::api` as the supported Rust interface for analysis,
configuration, synchronization, and reporting; `_internal` is now private.
- Unified API and CLI reports and evidence, with typed results, categorized
errors, and redacted database/cache-key inputs for embedded callers.
- Added `table-lock` to flag explicit blocking locks, bringing the rule count
to 29.
- Expanded SQL modeling for `TRUNCATE`, storage and column settings, inheritance,
partition detach, `SELECT INTO`, sequence options, and temporary-table commits.
- Added Cache V8 metadata for CHECK definitions, extended statistics, column
inheritance, generated/identity columns, and partition-trigger parentage.
- Improved constraint/index rename and drop propagation, generated CHECK names,
expression preservation, `LIKE` copies, and replica-identity eligibility checks.
- Fixed concurrent-detach state and recursive rename collisions; invalidate
descendant predicates after ancestry changes. Unsupported predicate forms
remain conservative.
- Track PostgreSQL 16+ role-membership grantors and per-grant options for more
accurate `REVOKE`/`CASCADE` analysis.
- Hardened rollback, baseline/version validation, secret cleanup, and report
rendering; reject unsupported PostgreSQL-version assumptions.
- Expanded live catalog comparisons and interrupted-detach coverage, repaired
the CI smoke-test target, and improved live scripts and crate packaging.

## v0.8.1 — 2026-09-06

- Upgraded Squawk's parser, lexer, syntax tree, and linter to 2.64.0, including
Expand Down
39 changes: 25 additions & 14 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
# Contributing to safe-migrate

## Start with an issue

Before submitting a new issue, [search existing issues](https://github.com/dsecurity49/safe-migrate/issues),
including closed ones, for similar reports or proposals. If one already covers
your topic, add any new information there rather than opening a duplicate.

Otherwise, [open an issue](https://github.com/dsecurity49/safe-migrate/issues/new/choose).
For substantial changes, discuss the approach before starting implementation.
For bug reports, include:

- minimal SQL;
- expected and actual output;
- safe-migrate version;
- PostgreSQL version or assumed version;
- whether a cache was used;
- relevant configuration, with credentials and other secrets removed.

## How analysis works

Thanks for contributing. safe-migrate is a Rust PostgreSQL migration analyzer
with typed AST extraction, stateful schema simulation, and safety rules.

Expand Down Expand Up @@ -29,7 +48,7 @@ src/_internal/engine/ configuration, orchestration, and rule dispatch
src/_internal/model/ modeled PostgreSQL objects
src/_internal/report/ human, JSON, and interactive reporting
src/_internal/rules/ safety rule implementations
src/api.rs supported Rust integration façade
src/api.rs, src/api/ supported Rust integration API
tests/ integration, state-machine, rule, CLI, and regression tests
live_tests/ end-to-end SQL fixtures and frozen database cache
docs/ Action guide and CLI/report contract
Expand All @@ -56,6 +75,10 @@ cargo test architectural_gap
cargo test expression_parsing
```

Implementation tests are registered under the library target (`--lib`), not
individual `--test` targets. The independent public API suite uses
`cargo test --locked --test api_facade`.

End-to-end fixtures:

```bash
Expand Down Expand Up @@ -182,7 +205,7 @@ The frozen cache under `live_tests/` belongs to the test corpus. Update it only
when a fixture requires a changed baseline, and explain the assumption in the
pull request.

Cache V7 synchronizes every PostgreSQL routine kind, publications, redacted
Cache V8 synchronizes every PostgreSQL routine kind, publications, redacted
subscription metadata, and explicit catalog coverage. Never query or store
`pg_subscription.subconninfo`.
Changes to the cache model require serialization and inspection regressions,
Expand All @@ -196,15 +219,3 @@ versions.
- Use idiomatic Rust naming and four-space indentation.
- Keep one rule concept per file or focused module.
- Document non-obvious undo-log and dependency-graph behavior inline.

## Reporting bugs

[Open an issue](https://github.com/dsecurity49/safe-migrate/issues/new/choose)
with:

- minimal SQL;
- expected and actual output;
- safe-migrate version;
- PostgreSQL version or assumed version;
- whether a cache was used;
- relevant configuration.
11 changes: 10 additions & 1 deletion Cargo.lock

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

18 changes: 15 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
[package]
name = "safe-migrate"
version = "0.8.1"
version = "0.9.0"
edition = "2024"
autotests = false
rust-version = "1.94"
description = "Check PostgreSQL migrations against a synchronized database baseline"
license = "MIT OR Apache-2.0"
repository = "https://github.com/dsecurity49/safe-migrate"
homepage = "https://github.com/dsecurity49/safe-migrate"
documentation = "https://docs.rs/safe-migrate"
readme = "README.md"
exclude = ["live_tests/.safe-migrate.cache"]
exclude = [
"live_tests/.safe-migrate.cache",
"/ROADMAP.md",
"/log.txt",
"/heap.c.*",
"/pg_*.h.*",
]
keywords = ["postgres", "migration", "linter", "ast", "database"]
categories = ["command-line-utilities", "database"]

Expand All @@ -33,10 +40,15 @@ bincode = { package = "bincode-next", version = "2.1", features = ["serde"] }
zstd = "0.13"
crossterm = "0.27.0"
tempfile = "3.10"
chacha20poly1305 = { version = "0.11", features = ["getrandom"] }
chacha20poly1305 = { version = "0.11", features = ["getrandom", "zeroize"] }
zeroize = "1.8"

[dev-dependencies]
assert_cmd = "2.0"
postgres = "0.19"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

[[test]]
name = "api_facade"
path = "tests/api_facade.rs"
44 changes: 39 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Prebuilt binaries are available from
installer verifies release checksums:

```bash
VERSION='v0.8.1'
VERSION='v0.9.0'
curl -fsSL "https://raw.githubusercontent.com/dsecurity49/safe-migrate/${VERSION}/install.sh" |
bash -s -- --version "${VERSION}"
```
Expand All @@ -50,7 +50,7 @@ Run `safe-migrate cache inspect` to view its provenance and redacted contents.

## What it checks

The 28 built-in rules cover:
The 29 built-in rules cover:

- blocking locks, table rewrites, constraints, indexes, partitions, and
materialized-view refreshes;
Expand All @@ -68,7 +68,7 @@ safe-migrate rules --rule require-concurrent-index

## GitHub Actions

Create the `safe-migrate-baseline` GitHub environment, then run:
Create and protect the `safe-migrate-baseline` GitHub environment, then run:

```bash
safe-migrate init github-actions --path migrations --configure-secrets
Expand Down Expand Up @@ -155,6 +155,10 @@ disabled = true
Unknown settings and rule IDs are rejected. `safe-migrate rules --json` lists
the configuration supported by each rule.

Without a synchronized baseline, the built-in version fallback is deliberately
conservative. Set `assume_pg_version` only when the target is known to be
PostgreSQL 14–18; for example, `assume_pg_version = 170000`.

Suppress a reviewed finding with its primary rule ID:

```sql
Expand All @@ -178,8 +182,38 @@ Keep a positive `lock_timeout` shorter than a positive `statement_timeout`.

## Rust library

Rust integrations should use the supported `safe_migrate::api` façade.
Documentation is published on [docs.rs](https://docs.rs/safe-migrate).
Rust integrations use `safe_migrate::api`. Load a synchronized baseline when
one is available; otherwise choose explicit conservative analysis.

```rust,no_run
use safe_migrate::api::{self, Baseline, Config};
use std::path::Path;

let config = Config::load_from_file(Path::new("safe-migrate.toml"))?;
let baseline = Baseline::load_optional(Path::new(".safe-migrate.cache"), &config)?;
let outcome = api::analyze(
&config,
"2026-09-05_add_index.sql",
"CREATE INDEX ...",
&baseline,
)?;

if outcome.should_halt() {
eprintln!("{}", outcome.markdown());
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

`load_optional` treats only a missing cache as unavailable; corrupt,
incompatible, or incorrectly encrypted caches remain errors. The API exposes
typed immutable findings, verdicts, evidence, baseline inspection, rule
metadata, and synchronization. Mutable parser, cache, and state-machine
internals are not public. Full API documentation is on
[docs.rs](https://docs.rs/safe-migrate).

Embedded applications can call `sync_with_secrets` with a validated
`DatabaseUrl` and optional `CacheKey`. This avoids changing process-wide
environment variables; the CLI continues to read secrets from its environment.

## Contributing

Expand Down
Loading
Loading