Skip to content

Conversation

tankyleo
Copy link

@tankyleo tankyleo commented Aug 15, 2025

    Create and initialize the database if it does not exist

    Also implement a versioning and migration scheme for future updates to
    the schema. It is an adaptation of the scheme used in CLN.

and

    Let the test suite setup the database

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Aug 15, 2025

👋 Thanks for assigning @tnull as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@tankyleo tankyleo requested a review from tnull August 15, 2025 07:42
Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took a first look and left some higher-level comments. Have yet to review the actual database statements and operations.

@tankyleo
Copy link
Author

A possible todo I thought of: right now our TABLE_CHECK_STMT SELECT 1 FROM vss_db WHERE false merely checks whether a vss_db table exists in the database. We could have a deeper check of all the columns in the table etc...

pub async fn new(postgres_endpoint: &str, db_name: &str, init_db: bool) -> Result<Self, Error> {
if init_db {
tokio::time::timeout(
tokio::time::Duration::from_secs(3),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract the timeout value(s) into appropriately named consts?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes thank you !

Ok(())
}

async fn check_health(&self) -> Result<(), Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? I think we should be able to trust that the CREATE TABLE statement does the right thing if it doesn't error out?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This mostly useful if the user does not set --init-db; in that case we want to make sure we can establish a connection to the database as part of startup checks before entering the main loop ?

If this passes, then we can log something like "Connected to PostgreSQL backend" in main as you suggested.

std::process::exit(1);
}
let init_db = args.iter().any(|arg| arg == "--init-db");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an optional flag to begin with?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • When the flag is not set, I want the startup sequence to abort if the database is not present.
  • When the flag is set, I want the startup sequence to create the database if it is not present.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, just wondering when we'd ever want the first case?

If the intention is that we can initiate a VSS server instance even if we don't have / can establish a connection to the Postgres backend (which I don't fully buy), then we'd need to take additional steps. AFAIU, Pool::build(), which we call ~just after the init_db block would just as well fail if we can't connect to the pool:

"The Pool will not be returned until it has established its configured minimum number of connections, or it times out." (https://docs.rs/bb8/0.9.0/bb8/struct.Builder.html#method.build)

Copy link
Author

@tankyleo tankyleo Aug 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, just wondering when we'd ever want the first case?

If the user does not intend to initialize a new database (which is most cases), I don't think we should silently create a new database if we don't find the one we are looking for. Instead, I think we should complain loudly, and make sure the user actually intends to create a new one.

If the intention is that we can initiate a VSS server instance even if we don't have / can establish a connection to the Postgres backend (which I don't fully buy), then we'd need to take additional steps. AFAIU, Pool::build(), which we call ~just after the init_db block would just as well fail if we can't connect to the pool:

Actually Pool::build returns Ok(()) even if it cannot establish a connection to the Postgres backend - see #52 (comment) . On the main branch, we then enter the main loop even though we have established no connections to the Postgres backend.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user does not intend to initialize a new database (which is most cases), I don't think we should silently create a new database if we don't find the one we are looking for. Instead, I think we should complain loudly, and make sure the user actually intends to create a new one.

Hmm, I don't know. A binary that needs a special flag to init just on first start is a bit weird tbh. I can't think of another daemon that works that way.

Actually Pool::build returns Ok(()) even if it cannot establish a connection to the Postgres backend - see #52 (comment) . On the main branch, we then enter the main loop even though we have established no connections to the Postgres backend.

Huh, this API is very unexpected then. You'd expect it to return a TimedOut error if the connection attempts failed, just as get does. Might even be good to add a comment there in our code.

Copy link
Author

@tankyleo tankyleo Aug 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, this API is very unexpected then. You'd expect it to return a TimedOut error if the connection attempts failed, just as get does. Might even be good to add a comment there in our code.

Sounds good will add the comment. Agreed this is unexpected.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't know. A binary that needs a special flag to init just on first start is a bit weird tbh. I can't think of another daemon that works that way.

I agree it is weird. FWIW, CLN, LDK-node automatically generates a new wallet, but LND requires you to explicitly run lncli createwallet instead of lncli unlock (lncli unlock aborts startup if the wallet does not exist).

If we always create a new database if it does not exist, when a user misconfigures the DB connection string to an existing database, we'll create a new database and complete startup, when instead we should(?) have aborted startup. See #52 (comment)

cc @TheBlueMatt

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a super strong feeling. I've seen it done both ways, though it is generally the case for binaries like this that they'll auto-create the table by default, I can see why that might be surprising.

.await
.unwrap(),
);
println!("Loaded postgres!");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I read that message I'd be a bit confused what's meant exactly. Maybe "Connected to PostgreSQL backend with address .."?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's clearer I agree thank you

let rest_svc_listener =
TcpListener::bind(&addr).await.expect("Failed to bind listening port");
println!("Bound to {}", addr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "Listening for incoming connections on .."

Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me know when this is ready for another round of review.

@tankyleo
Copy link
Author

Let me know when this is ready for another round of review.

Thanks for the review @tnull I've responded to your comments above would be good to see your responses before I write more code :)

const DB_INIT_CMD: &str = "CREATE DATABASE";
const TABLE_CHECK_STMT: &str = "SELECT 1 FROM vss_db WHERE false";
const TABLE_INIT_STMT: &str = "
CREATE TABLE IF NOT EXISTS vss_db (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition to the main table itself, we really need a "config" table that at least just stores the version of the schema we're currently using. That way we can easily check that the schema in use is compatible with this version and, in the future, we can upgrade the schema as needed.

Copy link
Contributor

@tnull tnull Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in Postgres migrations are usually done by sequentially applying the corresponding XXX_YYY.sql files. I agree it's probably good to track what version was last applied in a separate schema_version table or similar.

However, this probably also highlights that it would make sense to move the SQL statements to a dedicated migrations.rs, and rename TABLE_INIT_STMT in accordance to the .sql file to const V0_CREATE_VSS_DB, essentially to prepare for the eventual V1_.., V2_, .. migrations to follow.

FWIW, we could even consider to read in the sql files on startup. If we don't want to do that, we could consider dropping the sql files and just have the migrations.rs to avoid the risk of having them get out-of-sync at some point.

Also implement a versioning and migration scheme for future updates to
the schema. It is an adaptation of the scheme used in CLN.
@tankyleo tankyleo changed the title init db and table, check health Create and initialize the database if it does not exist Aug 27, 2025
@tankyleo tankyleo requested a review from tnull August 27, 2025 22:46
@tankyleo tankyleo moved this to Goal: Merge in Weekly Goals Aug 28, 2025
@tankyleo tankyleo self-assigned this Aug 28, 2025
@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @tnull! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@ldk-reviews-bot
Copy link

🔔 2nd Reminder

Hey @tnull! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

Copy link
Contributor

@tnull tnull left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments, but basically LGTM

const UPDATE_VERSION_STMT: &str = "UPDATE vss_db_version SET db_version=$1;";
const LOG_MIGRATION_STMT: &str = "INSERT INTO vss_db_upgrades VALUES($1);";

const MIGRATIONS: &[&str] = &[
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind adding a comment here that these are meant to be append-only (i.e., should never been changed in-place) and need to be run in order, and only the needed migration should be run?

Also, would it make sense to split out the migrations and potential test code into a migrations.rs?

const MIGRATIONS: &[&str] = &[
"CREATE TABLE vss_db_version (db_version INTEGER);",
"INSERT INTO vss_db_version VALUES(1);",
"CREATE TABLE vss_db_upgrades (upgrade_from INTEGER);",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you expand on what would be stored in the vss_db_upgrades table? Why do we need it in addition to vss_db_version?

Ok(postgres_backend)
}

async fn migrate_vss_database(&self) -> Result<(), Error> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be cool to get some test coverage for this migration logic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Goal: Merge
Development

Successfully merging this pull request may close these issues.

4 participants