Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ jobs:
run:
cd ${{ github.workspace }}/app ;
cargo build --verbose
# Documents private items too: pgc ships as a binary, so most of the code
# a contributor reads (comparer::core is 4 pub fns out of 47) is private,
# and a broken intra-doc link there is just as wrong. -D warnings turns
# unresolved links into a build failure instead of silent plain text.
- name: Check docs
env:
RUSTDOCFLAGS: "-D warnings"
run:
cd ${{ github.workspace }}/app ;
cargo doc --no-deps --lib --document-private-items
- name: Run tests
run:
cd ${{ github.workspace }}/app ;
Expand Down
88 changes: 88 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,91 @@
2026-08-03 v1.0.27

No change to what pgc emits. This release reorganises
the test suite, adds a library target and integration
tests, and documents the crate. The dump file format,
the CLI and the generated SQL are all unchanged.

Internals:
- Added a library target (`app/src/lib.rs`)
exporting the four existing modules. `main.rs`
is now a thin binary over it and holds only CLI
parsing and command dispatch. A binary-only
crate cannot have integration tests or run
doctests, so this is what the two sections below
are built on. The binary's behaviour, its flags
and its output are unaffected.

Tests:
- Unit tests moved out of the source directories:
`src/<module>/<name>_tests.rs` is now
`src/<module>/tests/<name>.rs`. They remain
`#[cfg(test)] #[path = ...] mod tests;` children
of the module they cover, so they still reach
its private items; only the file layout changed.
`src/dump/` no longer interleaves 27 test files
with the 28 sources beside them.
- `comparer/core_tests.rs`, at 11,772 lines the
largest file in the project, was split by
concern into fifteen files under
`src/comparer/tests/core/` — grants, views,
persistence, routines, sequences, tables and so
on — with the fixtures used by more than one of
them in `helpers.rs`. The before and after test
name sets are identical. Note that these
submodules need explicit `#[path]` attributes:
their parent is itself loaded through `#[path]`,
so rustc resolves children against
`src/comparer/tests/` rather than the `core/`
subdirectory, and a bare `mod production;`
silently binds to the unrelated
`tests/production.rs`.
- New integration suite in `app/tests/`, thirty
tests over the public API only, covering ground
the in-memory unit tests cannot reach: the zip
dump file round-trip and the `#[serde(default)]`
contract that keeps dumps from older versions
readable; the full dump → file → dump → compare
→ script path the `compare` command takes,
including that a schema compared against itself
emits no DDL; `Config::load` against real files,
among them the shipped `data/pgc.conf`; and the
drop ordering of the `clear` command. Three
further tests exercise a live server and are
`#[ignore]`d by default — run them with
`cargo test -- --ignored` and the standard `PG*`
environment variables.
- `cargo test` now runs 1106 tests, up from 1062.

Documentation:
- Module-level documentation on all 36 modules.
`dump/mod.rs` describes the
hash / get_script / get_drop_script /
get_alter_script shape that nearly every object
module repeats, and what adding a new object
kind requires, so the individual module headers
only carry what is specific to that kind.
- 245 comments on public items that were written
with `//`, and therefore invisible to rustdoc,
promoted to `///`. Trailing comments moved above
the field they describe.
- Fourteen doctests on the primary public API.
They compile and run under `cargo test`, so the
examples cannot drift from the code.
- Fixed eight broken intra-doc links that had been
rendering as plain text. Four were only visible
with `--document-private-items`.

Tooling:
- CI gained a docs step running
`cargo doc --no-deps --lib
--document-private-items` under
`RUSTDOCFLAGS="-D warnings"`, so an unresolved
link fails the build instead of degrading
silently. Private items are included because pgc
ships as a binary: most of what a contributor
reads is private, and a broken link there is
just as wrong.

2026-07-28 v1.0.26

Bug fixes:
Expand Down
2 changes: 1 addition & 1 deletion app/Cargo.lock

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

6 changes: 5 additions & 1 deletion app/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[package]
name = "pgc"
version = "1.0.26"
version = "1.0.27"
edition = "2024"
license = "MIT"
authors = ["nettrash <nettrash@nettrash.me>"]

[lib]
name = "pgc"
path = "src/lib.rs"

[[bin]]
name = "pgc"
path = "src/main.rs"
Expand Down
57 changes: 48 additions & 9 deletions app/src/comparer/core.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
//! The [`Comparer`] — reads two [`Dump`]s and emits the
//! migration SQL that makes `FROM` equal to `TO`.
//!
//! Output is assembled from several ordered buffers rather than one string,
//! because PostgreSQL dependency rules do not match the order objects are
//! compared in. They are concatenated as:
//!
//! ```text
//! script → sequence_post → type_post → enum_post → trigger_post
//! ```
//!
//! The comparer also tracks cross-cutting state that individual passes need:
//! `dropped_views` and `recreated_tables` coordinate drop/recreate sequencing,
//! and `serial_columns` keeps owned sequences from being emitted independently
//! of their table.

use crate::comparer::production::{self, ChildRef, PartitionContext};
use crate::config::grants_mode::GrantsMode;
use crate::dump::acl;
Expand All @@ -15,8 +31,8 @@ use std::{
io::{Error, Write},
};

// This is a Dump comparer that generates a script comparing two PostgreSQL dumps.
// The result script, if it will be applied on "from" dump database, will make it equal to "to" dump database.
/// This is a Dump comparer that generates a script comparing two PostgreSQL dumps.
/// The result script, if it will be applied on "from" dump database, will make it equal to "to" dump database.
pub struct Comparer {
// The dump to compare from
from: Dump,
Expand Down Expand Up @@ -58,7 +74,7 @@ pub struct Comparer {
}

impl Comparer {
// Creates a new Comparer with the given dumps
/// Creates a new Comparer with the given dumps
pub fn new(
from: Dump,
to: Dump,
Expand Down Expand Up @@ -108,12 +124,35 @@ impl Comparer {
/// built concurrently (partition-aware), foreign keys are added `NOT VALID`
/// then validated after commit, and indexes are dropped concurrently — all
/// post-commit statements are emitted after the main transaction.
/// Returns `&mut Self` so it can be chained after construction.
///
/// ```
/// # use pgc::comparer::core::Comparer;
/// # use pgc::config::dump_config::DumpConfig;
/// # use pgc::config::grants_mode::GrantsMode;
/// # use pgc::dump::core::Dump;
/// # let config = || DumpConfig {
/// # host: "localhost".to_string(), port: "5432".to_string(),
/// # user: "postgres".to_string(), password: String::new(),
/// # database: "shop".to_string(), scheme: "public".to_string(),
/// # ssl: false, file: String::new(),
/// # };
/// let mut comparer = Comparer::new(
/// Dump::new(config()),
/// Dump::new(config()),
/// true,
/// true,
/// true,
/// GrantsMode::Ignore,
/// );
/// comparer.set_output_for_production(true);
/// ```
pub fn set_output_for_production(&mut self, value: bool) -> &mut Self {
self.output_for_production = value;
self
}

// Compare dumps and generate the script
/// Compare dumps and generate the script
pub async fn compare(&mut self) -> Result<(), Error> {
if self.output_for_production {
// The statements that cannot run inside a transaction block are
Expand Down Expand Up @@ -657,7 +696,7 @@ impl Comparer {
Self::kahn_toposort_detect_cycle(n, depends_on, sort_key).0
}

/// Like [`kahn_toposort`], but also returns the set of nodes that
/// Like [`Comparer::kahn_toposort`], but also returns the set of nodes that
/// could not be ordered acyclically — i.e. nodes whose in-degree
/// never reached zero during the BFS. Those nodes still appear in
/// the returned `Vec` (appended in `sort_key` order so the result
Expand All @@ -671,7 +710,7 @@ impl Comparer {
/// Kahn was unable to remove — that includes nodes *blocked by*
/// a cycle (e.g. `C` in `A↔B + A→C`), not only nodes *in* a
/// cycle. Callers that need to act on *true* cycle members
/// should pair this with [`strongly_connected_components`] —
/// should pair this with [`Comparer::strongly_connected_components`] —
/// `topo_order_within_subset_detect_cycle` does exactly that
/// (PR #198 review).
fn kahn_toposort_detect_cycle<K: Ord>(
Expand Down Expand Up @@ -980,7 +1019,7 @@ impl Comparer {
dependent_views
}

// Saves the generated script to a file
/// Saves the generated script to a file
pub async fn save_script(&self, output: &str) -> Result<(), Error> {
let mut file = File::create(output)?;
file.write_all(self.get_script().as_bytes())?;
Expand Down Expand Up @@ -2636,7 +2675,7 @@ impl Comparer {
/// restricted to `subset` is acyclic the cyclic set is empty.
///
/// PR #198 review: the cyclic set is computed via
/// [`strongly_connected_components`] (Tarjan), not the raw Kahn
/// [`Comparer::strongly_connected_components`] (Tarjan), not the raw Kahn
/// remainder. The remainder would include nodes merely *blocked
/// by* a cycle (e.g. `C` in `A↔B + A→C`), and treating them as
/// cycle participants would drop FKs that are not actually in
Expand Down Expand Up @@ -5986,5 +6025,5 @@ fn policy_recreate_block(policy: &TablePolicy) -> String {
}

#[cfg(test)]
#[path = "core_tests.rs"]
#[path = "tests/core.rs"]
mod tests;
Loading
Loading