dumpling-cli 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/CHANGELOG.md +7 -0
  2. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/Cargo.lock +1 -1
  3. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/Cargo.toml +1 -1
  4. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/PKG-INFO +1 -1
  5. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/pyproject.toml +1 -1
  6. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/sql.rs +37 -1
  7. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.dumplingconf.example +0 -0
  8. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/ci.yml +0 -0
  9. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/docs-pr.yml +0 -0
  10. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/docs.yml +0 -0
  11. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/platform-compat-latest.yml +0 -0
  12. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/platform-compat-matrix.yml +0 -0
  13. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/policy-lint.yml +0 -0
  14. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/publish.yml +0 -0
  15. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/release.yml +0 -0
  16. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.github/workflows/tests.yml +0 -0
  17. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/.gitignore +0 -0
  18. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/AGENTS.md +0 -0
  19. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/CONTRIBUTING.md +0 -0
  20. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/MAINTENANCE.md +0 -0
  21. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/README.md +0 -0
  22. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/assets/logo.svg +0 -0
  23. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/book.toml +0 -0
  24. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/datetime_out.sql +0 -0
  25. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/datetime_sample.sql +0 -0
  26. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/docs/src/SUMMARY.md +0 -0
  27. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/docs/src/ci-guardrails.md +0 -0
  28. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/docs/src/configuration.md +0 -0
  29. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/docs/src/getting-started.md +0 -0
  30. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/docs/src/index.md +0 -0
  31. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/docs/src/releasing.md +0 -0
  32. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/rust-toolchain.toml +0 -0
  33. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/scripts/setup-dev.sh +0 -0
  34. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/faker_dispatch.rs +0 -0
  35. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/filter.rs +0 -0
  36. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/lint.rs +0 -0
  37. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/main.rs +0 -0
  38. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/report.rs +0 -0
  39. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/scan.rs +0 -0
  40. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/settings.rs +0 -0
  41. {dumpling_cli-0.4.2 → dumpling_cli-0.4.3}/src/transform.rs +0 -0
@@ -7,6 +7,12 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.3] - 2026-05-03
11
+
12
+ ### Fixed
13
+
14
+ - **COPY row integrity after anonymization**: Control characters in anonymized COPY text fields are escaped so tab/newline/etc. cannot break column alignment or row boundaries ([#53](https://github.com/ababic/dumpling/pull/53)).
15
+
10
16
  ## [0.4.2] - 2026-05-03
11
17
 
12
18
  ### Fixed
@@ -68,6 +74,7 @@ and this project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.ht
68
74
  - Configurable output scan severities and per-category thresholds via `[output_scan]`.
69
75
  - JSON report section for output scan findings including category, count, threshold, severity, and sample locations.
70
76
 
77
+ [0.4.3]: https://github.com/ababic/dumpling/compare/v0.4.2...v0.4.3
71
78
  [0.4.2]: https://github.com/ababic/dumpling/compare/v0.4.1...v0.4.2
72
79
  [0.4.1]: https://github.com/ababic/dumpling/compare/v0.4.0...v0.4.1
73
80
  [0.4.0]: https://github.com/ababic/dumpling/compare/v0.3.0...v0.4.0
@@ -262,7 +262,7 @@ dependencies = [
262
262
 
263
263
  [[package]]
264
264
  name = "dumpling"
265
- version = "0.4.2"
265
+ version = "0.4.3"
266
266
  dependencies = [
267
267
  "anyhow",
268
268
  "chrono",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "dumpling"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dumpling-cli
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "dumpling-cli"
7
- version = "0.4.2"
7
+ version = "0.4.3"
8
8
  description = "Static anonymizer for plain SQL dumps (PostgreSQL, SQLite, SQL Server)."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -299,7 +299,8 @@ impl SqlStreamProcessor {
299
299
  if repl.is_null {
300
300
  new_fields.push(r"\N".to_string());
301
301
  } else {
302
- new_fields.push(repl.value);
302
+ new_fields
303
+ .push(escape_postgres_copy_text_field(&repl.value));
303
304
  }
304
305
  }
305
306
  Err(e) => return Err(e),
@@ -1181,6 +1182,32 @@ impl Cell {
1181
1182
  }
1182
1183
  }
1183
1184
 
1185
+ /// Escapes a field value for PostgreSQL `COPY ... FROM stdin` **text** format so the output
1186
+ /// line still has one physical TAB-separated field per logical column. Without this, a
1187
+ /// replacement containing a literal TAB or newline would split the row on restore and surface
1188
+ /// as PostgreSQL errors like `missing data for column "..."`.
1189
+ fn escape_postgres_copy_text_field(s: &str) -> String {
1190
+ let mut out = String::with_capacity(s.len());
1191
+ for c in s.chars() {
1192
+ match c {
1193
+ '\\' => out.push_str("\\\\"),
1194
+ '\t' => out.push_str("\\t"),
1195
+ '\n' => out.push_str("\\n"),
1196
+ '\r' => out.push_str("\\r"),
1197
+ '\u{0008}' => out.push_str("\\b"),
1198
+ '\u{000c}' => out.push_str("\\f"),
1199
+ '\u{000b}' => out.push_str("\\v"),
1200
+ '\0' => out.push_str("\\0"),
1201
+ c if (c as u32) < 0x20 => {
1202
+ use std::fmt::Write;
1203
+ let _ = write!(out, "\\x{:02x}", c as u32);
1204
+ }
1205
+ c => out.push(c),
1206
+ }
1207
+ }
1208
+ out
1209
+ }
1210
+
1184
1211
  fn render_cell(repl: &Replacement, original: &Cell) -> String {
1185
1212
  let trailing = original.trailing_expr.as_deref().unwrap_or("");
1186
1213
  if repl.is_null {
@@ -2119,6 +2146,15 @@ INSERT INTO public.users (id, email) VALUES
2119
2146
  );
2120
2147
  }
2121
2148
 
2149
+ #[test]
2150
+ fn escape_postgres_copy_text_field_escapes_control_chars() {
2151
+ assert_eq!(
2152
+ escape_postgres_copy_text_field("a\tb\nc\\"),
2153
+ "a\\tb\\nc\\\\"
2154
+ );
2155
+ assert_eq!(escape_postgres_copy_text_field("\0\u{01}"), "\\0\\x01");
2156
+ }
2157
+
2122
2158
  #[test]
2123
2159
  fn domain_mapping_null_and_non_null_cross_table_consistency() {
2124
2160
  // When the same domain spans two tables, NULL stays NULL in both, and
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes