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
12 changes: 8 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,14 @@ left over from a previous run.

Before computing deltas, the library detects field layout changes by comparing
each table's stored fields in the STATE file against the current config's
`ordered_field_names()`. Tables whose layout changed are recorded in the block
as a `TableChange` with no delta (`delta: None`), signaling that patch
consolidation should use a full state snapshot for that table instead of
attempting to merge incompatible deltas.
canonical field list (primary keys first, then subsidiaries; each group sorted
lexicographically by name).

Because tuple identity is canonical, reordering fields in `tables.toml` does not
register as a layout change. Adding, removing, or renaming a field does. Tables
whose layout changed are recorded in the block as a `TableChange` with no delta
(`delta: None`), signaling that patch consolidation should use a full state
snapshot for that table instead of attempting to merge incompatible deltas.

All table changes are bundled into a block together with a parent hash and a
timestamp, SHA-1 hashed, and stored as a file named by its hash. The `HEAD`
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ Config can be `config.toml` or `config.json`.

- Each table must have at least one field marked `primary-key = true`
- Field names within a table must be unique
- When `header = false` (the default), CSV columns are mapped to config fields by
position — the first column maps to the first field, etc.
- When `header = true`, the first row of the CSV is treated as a header. Each
config field is matched to a CSV column by name, so columns may appear in any
order. Every config field name must be present in the header; extra CSV columns
are ignored. When `header = false` (the default), CSV columns are mapped to
config fields by position — the first column maps to the first field, etc.
are ignored. In this mode, the order in which fields are declared under the
table is cosmetic — reordering them does not invalidate existing state.
- The type field controls how values are quoted in generated SQL. These are not
database column types — your database may use any compatible type (e.g.
`INTEGER`, `FLOAT`, `TIMESTAMP`). It is your responsibility to ensure the
Expand Down Expand Up @@ -114,9 +116,9 @@ fields = [
]
```

`BOOLEAN` fields can override the strings recognised as true and false. When
`BOOLEAN` fields can override the strings recognized as true and false. When
either override is set, only the configured strings are accepted — the
defaults are not honoured alongside them. The `true`, `false`, and `null`
defaults are not honored alongside them. The `true`, `false`, and `null`
sentinels on a single field must all differ.

```toml
Expand Down
67 changes: 67 additions & 0 deletions tests/accept_field_reorder.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
mod common;

use leech2::block::Block;
use leech2::config::Config;
use leech2::patch::Patch;
use leech2::sql;

/// Reordering fields in `tables.toml` between two blocks must not produce a
/// spurious delta when the underlying CSV data is unchanged. Tuple identity
/// is canonical (lexicographic by field name) so that the field declaration
/// order in the config is cosmetic.
#[test]
fn test_field_reorder_in_config_produces_no_delta() {
common::init_logging();
let tmp = tempfile::tempdir().unwrap();
let work_dir = tmp.path();

common::write_csv(
work_dir,
"users.csv",
"id,name,email\n1,Alice,a@example.com\n2,Bob,b@example.com\n",
);

common::write_config(
work_dir,
"config.toml",
r#"
[tables.users]
source = "users.csv"
header = true
fields = [
{ name = "id", type = "NUMBER", primary-key = true },
{ name = "name", type = "TEXT" },
{ name = "email", type = "TEXT" },
]
"#,
);
let hash1 = Block::create(&Config::load(work_dir).unwrap()).unwrap();

// Reorder fields (id is still the only primary key, but its position in
// the declared field list moves; subsidiaries are also reordered).
common::write_config(
work_dir,
"config.toml",
r#"
[tables.users]
source = "users.csv"
header = true
fields = [
{ name = "email", type = "TEXT" },
{ name = "id", type = "NUMBER", primary-key = true },
{ name = "name", type = "TEXT" },
]
"#,
);
let config = Config::load(work_dir).unwrap();
let _hash2 = Block::create(&config).unwrap();

// Patch from hash1 should be empty: same data, just a different
// declaration order.
let patch = Patch::create(&config, &hash1).unwrap();
if let Some(s) = sql::patch_to_sql(&config, &patch).unwrap() {
assert_eq!(s.trim(), "BEGIN;\nCOMMIT;");
}

common::assert_wire_roundtrip(&config, &patch);
}
Loading