Remove ipx copy-db (#85)
It was the one-off copy from SQLite to Postgres (#18), run once on 2026-09-18. Production has run on Postgres since; rolling back needs only the old state.db, which is kept, not this command. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -37,6 +37,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
|
- `ipx copy-db`, the one-off move from SQLite to Postgres. Going back needs only the old `state.db`.
|
||||||
- Starting up no longer looks for files downloaded twice before 0.6.0 read WordPress's player
|
- Starting up no longer looks for files downloaded twice before 0.6.0 read WordPress's player
|
||||||
links correctly; that clean-up has run.
|
links correctly; that clean-up has run.
|
||||||
|
|
||||||
|
|||||||
@@ -25,8 +25,7 @@ config.toml's default location is `$XDG_CONFIG_HOME/ipx/config.toml`
|
|||||||
|
|
||||||
`~` is expanded in paths. The database is SQLite in WAL mode unless `IPX_DATABASE_URL` names a
|
`~` is expanded in paths. The database is SQLite in WAL mode unless `IPX_DATABASE_URL` names a
|
||||||
Postgres database instead. Back SQLite up by copying `state.db` while the daemon is stopped, or
|
Postgres database instead. Back SQLite up by copying `state.db` while the daemon is stopped, or
|
||||||
with `sqlite3 state.db .backup`; back Postgres up with `pg_dump`. `ipx copy-db <state.db>` copies a
|
with `sqlite3 state.db .backup`; back Postgres up with `pg_dump`.
|
||||||
SQLite database into the empty Postgres one `IPX_DATABASE_URL` names.
|
|
||||||
|
|
||||||
## `[general]`
|
## `[general]`
|
||||||
|
|
||||||
|
|||||||
66
src/db.rs
66
src/db.rs
@@ -71,30 +71,6 @@ async fn create_missing(orm: &sea_orm::DatabaseConnection) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One table's rows, a page at a time in primary-key order, from one database to another.
|
|
||||||
async fn copy_table<E>(from: &sea_orm::DatabaseConnection, to: &impl ConnectionTrait) -> Result<u64>
|
|
||||||
where
|
|
||||||
E: EntityTrait,
|
|
||||||
E::Model: sea_orm::IntoActiveModel<E::ActiveModel> + Send + Sync,
|
|
||||||
E::ActiveModel: ActiveModelTrait<Entity = E> + Send,
|
|
||||||
{
|
|
||||||
use sea_orm::{IntoActiveModel, Iterable, PrimaryKeyToColumn};
|
|
||||||
let mut query = E::find();
|
|
||||||
for key in E::PrimaryKey::iter() {
|
|
||||||
query = query.order_by_asc(key.into_column());
|
|
||||||
}
|
|
||||||
let mut pages = query.paginate(from, 1000);
|
|
||||||
let mut n = 0;
|
|
||||||
while let Some(rows) = pages.fetch_and_next().await? {
|
|
||||||
n += rows.len() as u64;
|
|
||||||
// reset_all: every column written, the primary key included, not just the changed ones.
|
|
||||||
E::insert_many(rows.into_iter().map(|m| m.into_active_model().reset_all()))
|
|
||||||
.exec_without_returning(to)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
Ok(n)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Where the database is: IPX_DATABASE_URL, a postgres:// URL, when it is set; otherwise the
|
/// Where the database is: IPX_DATABASE_URL, a postgres:// URL, when it is set; otherwise the
|
||||||
/// SQLite file in the data directory, as it has always been.
|
/// SQLite file in the data directory, as it has always been.
|
||||||
pub fn location() -> String {
|
pub fn location() -> String {
|
||||||
@@ -1079,48 +1055,6 @@ impl Db {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- moving to another database ----
|
|
||||||
|
|
||||||
/// Copies every row of `from` into this database, which must be empty: the move from the
|
|
||||||
/// SQLite file to Postgres (issue #18). One transaction, so a copy that fails part-way leaves
|
|
||||||
/// nothing behind and can simply be run again. Returns each table's row count.
|
|
||||||
pub async fn copy_from(&self, from: &Db) -> Result<Vec<(&'static str, u64)>> {
|
|
||||||
use crate::entity::*;
|
|
||||||
use sea_orm::TransactionTrait;
|
|
||||||
let held = users::Entity::find().count(&self.orm).await?
|
|
||||||
+ feeds::Entity::find().count(&self.orm).await?
|
|
||||||
+ entries::Entity::find().count(&self.orm).await?;
|
|
||||||
anyhow::ensure!(held == 0, "the database being copied into already holds rows; it has to be empty");
|
|
||||||
let tx = self.orm.begin().await?;
|
|
||||||
// Users first: the tables that belong to a person refer to them.
|
|
||||||
let counts = vec![
|
|
||||||
("users", copy_table::<users::Entity>(&from.orm, &tx).await?),
|
|
||||||
("feeds", copy_table::<feeds::Entity>(&from.orm, &tx).await?),
|
|
||||||
("entries", copy_table::<entries::Entity>(&from.orm, &tx).await?),
|
|
||||||
("enclosures", copy_table::<enclosures::Entity>(&from.orm, &tx).await?),
|
|
||||||
("subscriptions", copy_table::<subscriptions::Entity>(&from.orm, &tx).await?),
|
|
||||||
("entry_state", copy_table::<entry_state::Entity>(&from.orm, &tx).await?),
|
|
||||||
("sessions", copy_table::<sessions::Entity>(&from.orm, &tx).await?),
|
|
||||||
("catalogue", copy_table::<catalogue::Entity>(&from.orm, &tx).await?),
|
|
||||||
("settings", copy_table::<settings::Entity>(&from.orm, &tx).await?),
|
|
||||||
("blocklists", copy_table::<blocklists::Entity>(&from.orm, &tx).await?),
|
|
||||||
("hidden", copy_table::<hidden::Entity>(&from.orm, &tx).await?),
|
|
||||||
];
|
|
||||||
if self.orm.get_database_backend() == sea_orm::DbBackend::Postgres {
|
|
||||||
// The copied ids came with the rows; the counters that hand out new ones start past
|
|
||||||
// them, or the next account or file would collide with one copied.
|
|
||||||
for table in ["users", "enclosures"] {
|
|
||||||
tx.execute_unprepared(&format!(
|
|
||||||
"SELECT setval(pg_get_serial_sequence('{table}', 'id'), \
|
|
||||||
coalesce((SELECT max(id) FROM {table}), 0) + 1, false)"
|
|
||||||
))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tx.commit().await?;
|
|
||||||
Ok(counts)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- hand-written SQL ----
|
// ---- hand-written SQL ----
|
||||||
//
|
//
|
||||||
// For what reads better as SQL than as a query builder: joins, sums, upserts. Written to
|
// For what reads better as SQL than as a query builder: joins, sums, upserts. Written to
|
||||||
|
|||||||
27
src/main.rs
27
src/main.rs
@@ -38,12 +38,6 @@ struct Cli {
|
|||||||
enum Command {
|
enum Command {
|
||||||
/// Show configured feeds and their state
|
/// Show configured feeds and their state
|
||||||
List,
|
List,
|
||||||
/// Copy everything from a SQLite state.db into the database IPX_DATABASE_URL names, which
|
|
||||||
/// must be empty: the one-off move to Postgres
|
|
||||||
CopyDb {
|
|
||||||
/// The SQLite file to copy from
|
|
||||||
from: PathBuf,
|
|
||||||
},
|
|
||||||
/// Scan feeds for new entries
|
/// Scan feeds for new entries
|
||||||
Fetch {
|
Fetch {
|
||||||
/// Only this feed id
|
/// Only this feed id
|
||||||
@@ -236,8 +230,7 @@ async fn main() -> Result<()> {
|
|||||||
| Command::Add { .. }
|
| Command::Add { .. }
|
||||||
| Command::Rm { .. }
|
| Command::Rm { .. }
|
||||||
| Command::Import { .. }
|
| Command::Import { .. }
|
||||||
| Command::Export { .. }
|
| Command::Export { .. } => None,
|
||||||
| Command::CopyDb { .. } => None,
|
|
||||||
};
|
};
|
||||||
if let Some(cmd) = &wire_cmd
|
if let Some(cmd) = &wire_cmd
|
||||||
&& !cli.local
|
&& !cli.local
|
||||||
@@ -246,13 +239,7 @@ async fn main() -> Result<()> {
|
|||||||
return ipc::proxy(&cfg.general.socket, cmd).await;
|
return ipc::proxy(&cfg.general.socket, cmd).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy-db fills an empty database from another, configuration included; taking config.toml
|
let cfg = assemble_config(&db, cfg, &config_path).await?;
|
||||||
// into it first would have the copy collide with it.
|
|
||||||
let cfg = if matches!(cli.command, Command::CopyDb { .. }) {
|
|
||||||
cfg
|
|
||||||
} else {
|
|
||||||
assemble_config(&db, cfg, &config_path).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
let is_daemon = matches!(cli.command, Command::Daemon { .. });
|
let is_daemon = matches!(cli.command, Command::Daemon { .. });
|
||||||
let (events, _) = broadcast::channel(1024);
|
let (events, _) = broadcast::channel(1024);
|
||||||
@@ -279,7 +266,6 @@ async fn main() -> Result<()> {
|
|||||||
Command::User { cmd } => user_cmd(&ctx, cmd).await,
|
Command::User { cmd } => user_cmd(&ctx, cmd).await,
|
||||||
Command::Import { file } => import(&ctx, &file).await,
|
Command::Import { file } => import(&ctx, &file).await,
|
||||||
Command::Export { file } => export(&ctx, &file).await,
|
Command::Export { file } => export(&ctx, &file).await,
|
||||||
Command::CopyDb { from } => copy_db(&ctx, &from).await,
|
|
||||||
_ => run(&ctx, wire_cmd.expect("only List and Daemon have no wire form")).await,
|
_ => run(&ctx, wire_cmd.expect("only List and Daemon have no wire form")).await,
|
||||||
};
|
};
|
||||||
// The batch exporter holds the last few seconds of spans; without this they are lost.
|
// The batch exporter holds the last few seconds of spans; without this they are lost.
|
||||||
@@ -871,15 +857,6 @@ async fn assemble_config(db: &db::Db, mut cfg: config::Config, path: &std::path:
|
|||||||
anyhow::bail!("the database says it holds the configuration and then that it does not")
|
anyhow::bail!("the database says it holds the configuration and then that it does not")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn copy_db(ctx: &Ctx, from: &std::path::Path) -> Result<()> {
|
|
||||||
anyhow::ensure!(from.exists(), "{} does not exist", from.display());
|
|
||||||
let source = db::Db::open(&from.display().to_string()).await?;
|
|
||||||
for (table, n) in ctx.db.copy_from(&source).await? {
|
|
||||||
println!("{table:14} {n}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn export(ctx: &Ctx, file: &std::path::Path) -> Result<()> {
|
async fn export(ctx: &Ctx, file: &std::path::Path) -> Result<()> {
|
||||||
let mut doc = opml::OPML::default();
|
let mut doc = opml::OPML::default();
|
||||||
doc.head = Some(opml::Head {
|
doc.head = Some(opml::Head {
|
||||||
|
|||||||
Reference in New Issue
Block a user