Skip to content

Commit 5eaebbd

Browse files
committed
release: 7.0.13
1 parent 4f9b252 commit 5eaebbd

File tree

6 files changed

+74
-11
lines changed

6 files changed

+74
-11
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
44

55
## [7.0.12] - 2025-12-11
66

7+
## [7.0.13] - 2025-12-11
8+
9+
### Fixed
10+
11+
- **SQLite re-runs are now deterministic**: `truncate_jsonb_table()` now logs at INFO level, verifies the target table is empty, and errors early if rows remain instead of failing later with duplicate primary keys.
12+
- **`--drop-existing` works for SQLite**: When the flag is supplied, each JSONB table is dropped before recreation, ensuring a pristine schema even if manual changes were made to the target database.
13+
14+
### Added
15+
16+
- **`drop_jsonb_table()` helper**: Shared helper allows future workflows (and the SQLite path today) to explicitly remove JSONB tables when a clean rebuild is required.
17+
718
### Added
819

920
- **Memory-efficient SQLite migration**: SQLite to PostgreSQL migration now processes rows in batches instead of loading entire tables into memory. This enables migration of large SQLite databases (7M+ rows, multi-GB files) without OOM errors. Batch size automatically adjusts based on available system memory.

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "database-replicator"
3-
version = "7.0.12"
3+
version = "7.0.13"
44
edition = "2021"
55
license = "Apache-2.0"
66
description = "Universal database-to-PostgreSQL replication CLI. Supports PostgreSQL, SQLite, MongoDB, and MySQL."

README-SQLite.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,7 +560,7 @@ No. The tool opens SQLite databases in **read-only mode**. It's impossible to mo
560560

561561
### Can I replicate the same database twice?
562562

563-
Re-running `init` will **drop and recreate** tables. All data will be replaced with fresh data from SQLite. This is useful for:
563+
Re-running `init` will **clear** previously migrated tables (via truncate by default or a full drop when `--drop-existing` is supplied). All data will be replaced with fresh data from SQLite. This is useful for:
564564
- Correcting errors in the first migration
565565
- Refreshing data from an updated SQLite snapshot
566566

@@ -570,7 +570,7 @@ SQLite replications are snapshot-only. For incremental updates:
570570

571571
1. **Option 1**: Periodic full re-migration
572572
- Create SQLite backup/snapshot
573-
- Re-run `init` with `--drop-existing` flag (implied in init)
573+
- Re-run `init` (add `--drop-existing` to drop/recreate tables instead of truncate)
574574

575575
2. **Option 2**: Track changes in application
576576
- Maintain updated_at timestamps

src/commands/init.rs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -153,15 +153,17 @@ pub async fn init(
153153
);
154154
}
155155
if drop_existing {
156-
tracing::warn!("⚠ --drop-existing flag is not applicable for SQLite sources");
156+
tracing::info!(
157+
"--drop-existing: existing JSONB tables on the target will be dropped"
158+
);
157159
}
158160
if !enable_sync {
159161
tracing::warn!(
160162
"⚠ SQLite sources don't support continuous replication (one-time migration only)"
161163
);
162164
}
163165

164-
return init_sqlite_to_postgres(source_url, target_url).await;
166+
return init_sqlite_to_postgres(source_url, target_url, drop_existing).await;
165167
}
166168
crate::SourceType::MongoDB => {
167169
// MongoDB to PostgreSQL migration (simpler path)
@@ -853,6 +855,7 @@ async fn drop_database_if_exists(target_conn: &Client, db_name: &str) -> Result<
853855
///
854856
/// * `sqlite_path` - Path to SQLite database file (.db, .sqlite, or .sqlite3)
855857
/// * `target_url` - PostgreSQL connection string for target (Seren) database
858+
/// * `drop_existing` - Drop any existing JSONB tables on the target before migrating
856859
///
857860
/// # Returns
858861
///
@@ -874,12 +877,17 @@ async fn drop_database_if_exists(target_conn: &Client, db_name: &str) -> Result<
874877
/// # async fn example() -> Result<()> {
875878
/// init_sqlite_to_postgres(
876879
/// "database.db",
877-
/// "postgresql://user:[email protected]/targetdb"
880+
/// "postgresql://user:[email protected]/targetdb",
881+
/// false,
878882
/// ).await?;
879883
/// # Ok(())
880884
/// # }
881885
/// ```
882-
pub async fn init_sqlite_to_postgres(sqlite_path: &str, target_url: &str) -> Result<()> {
886+
pub async fn init_sqlite_to_postgres(
887+
sqlite_path: &str,
888+
target_url: &str,
889+
drop_existing: bool,
890+
) -> Result<()> {
883891
tracing::info!("Starting SQLite to PostgreSQL migration...");
884892

885893
// Step 1: Validate SQLite file
@@ -940,6 +948,12 @@ pub async fn init_sqlite_to_postgres(sqlite_path: &str, target_url: &str) -> Res
940948
row_count
941949
);
942950

951+
if drop_existing {
952+
crate::jsonb::writer::drop_jsonb_table(&target_client, table_name)
953+
.await
954+
.with_context(|| format!("Failed to drop existing JSONB table '{}'", table_name))?;
955+
}
956+
943957
// Create JSONB table in PostgreSQL
944958
crate::jsonb::writer::create_jsonb_table(&target_client, table_name, "sqlite")
945959
.await

src/jsonb/writer.rs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ABOUTME: Write JSONB data to PostgreSQL with metadata
22
// ABOUTME: Handles table creation, single row inserts, and batch inserts
33

4-
use anyhow::{Context, Result};
4+
use anyhow::{bail, Context, Result};
55
use tokio_postgres::Client;
66

77
/// Create a table with JSONB schema for storing non-PostgreSQL data
@@ -126,7 +126,7 @@ pub async fn truncate_jsonb_table(client: &Client, table_name: &str) -> Result<(
126126
crate::jsonb::validate_table_name(table_name)
127127
.context("Invalid table name for JSONB table truncation")?;
128128

129-
tracing::debug!("Truncating JSONB table '{}'", table_name);
129+
tracing::info!("Truncating JSONB table '{}'", table_name);
130130

131131
let truncate_sql = format!(
132132
r#"TRUNCATE TABLE "{}" RESTART IDENTITY CASCADE"#,
@@ -138,7 +138,45 @@ pub async fn truncate_jsonb_table(client: &Client, table_name: &str) -> Result<(
138138
.await
139139
.with_context(|| format!("Failed to truncate JSONB table '{}'", table_name))?;
140140

141-
tracing::debug!("Truncated JSONB table '{}'", table_name);
141+
let verify_sql = format!(r#"SELECT COUNT(*) FROM "{}""#, table_name);
142+
let remaining_rows: i64 = client
143+
.query_one(&verify_sql, &[])
144+
.await
145+
.with_context(|| format!("Failed to verify truncate of '{}'", table_name))?
146+
.get(0);
147+
148+
if remaining_rows > 0 {
149+
bail!(
150+
"Truncate verification failed: table '{}' still has {} rows after truncate",
151+
table_name,
152+
remaining_rows
153+
);
154+
}
155+
156+
tracing::info!(
157+
"Truncated JSONB table '{}' successfully ({} rows remaining)",
158+
table_name,
159+
remaining_rows
160+
);
161+
162+
Ok(())
163+
}
164+
165+
/// Drop a JSONB table if it exists.
166+
pub async fn drop_jsonb_table(client: &Client, table_name: &str) -> Result<()> {
167+
crate::jsonb::validate_table_name(table_name)
168+
.context("Invalid table name for JSONB table drop")?;
169+
170+
tracing::info!("Dropping JSONB table '{}'", table_name);
171+
172+
let drop_sql = format!(r#"DROP TABLE IF EXISTS "{}" CASCADE"#, table_name);
173+
174+
client
175+
.execute(&drop_sql, &[])
176+
.await
177+
.with_context(|| format!("Failed to drop JSONB table '{}'", table_name))?;
178+
179+
tracing::info!("Dropped JSONB table '{}' (if it existed)", table_name);
142180

143181
Ok(())
144182
}

0 commit comments

Comments
 (0)