Skip to content

feature(pedm): SQL initialization#1302

Merged
Allan Zhang (allan2) merged 28 commits intomasterfrom
pedm-sql-init3
Apr 10, 2025
Merged

feature(pedm): SQL initialization#1302
Allan Zhang (allan2) merged 28 commits intomasterfrom
pedm-sql-init3

Conversation

@allan2
Copy link
Copy Markdown
Contributor

  • adds version tracking in the pedm_schema_version table
  • adds table initialization
    • error checking is strict and initialization only happens if the version table is not found
  • adds SQLite error handling for microsecond timestamp conversion to/from
    Chrono
  • fixes a bug in the libSQL schema where datetimes were stored incorrectly
    • they were being stored fractionally, instead of the full microseconds since epoch
  • adds /about endpoint to get basic info about the running application
    • I used this to ensure that the libsql date handling is functional

A few other changes are included:

  • libsql feature set is changed (default is now false, "stream" removed)

This PR includes all commits from PR #1301, so an option is to merge this one only instead of #1301 first.

- `serve` arguments are changed
  - it now takes the `Config` struct instead of a path
  - pipe name is now part of `Config` rather than a direct argument
- `Config` struct is now exported as `devolutions_pedm::Config`
  - it can be used by server implementations directly
- `serve` is now exported as `devolutions_pedm::serve`
This commit makes it public again. It is used by the generate-openapi
tool in the `devolutions_pedm::api::openapi` call.
- adds version tracking in the `pedm_schema_version` table
- adds table initialization
  - error checking is strict and initialization only happens if the
    version table is not found
- adds SQLite error handling for microsecond timestamp conversion to/from
  Chrono
- fixes a bug in the libSQL schema where datetimes were stored
  incorrectly
  - they were being stored fractionally, instead of the full
    microseconds since epoch
- adds `/about` endpoint to get basic info about the running application
  - I used this to ensure that the libsql date handling is functional

A few other changes are included:
- libsql feature set is changed (default is now false, "stream" removed)
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 8, 2025

Let maintainers know that an action is required on their side

  • Add the label release-required Please cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module) when you request a maintainer to cut a new release (Devolutions Gateway, Devolutions Agent, Jetsocat, PowerShell module)

  • Add the label release-blocker Follow-up is required before cutting a new release if a follow-up is required before cutting a new release

  • Add the label publish-required Please publish libraries (`Devolutions.Gateway.Utils`, OpenAPI clients, etc) when you request a maintainer to publish libraries (Devolutions.Gateway.Utils, OpenAPI clients, etc.)

  • Add the label publish-blocker Follow-up is required before publishing libraries if a follow-up is required before publishing libraries

Desired changes were removed in the Cargo.toml reversion commit.
They are restored now.

A few `mod` statements are moved outside of the Windows-only block.
SQLite stores integers in 64 bit.

If we try to get i32, it is just [cast internally](https://docs.rs/libsql/0.9.2/src/libsql/rows.rs.html#169-177) as i32.

The logic for fallible int conversion is already written. This commit
makes use of it and makes it explicit that we are actually retrieving an i64 from the database before converting it to our model type.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: Is there some rationale for when the pedm prefix is used in table names?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was in case the database is also used by Gateway or other programs in the future.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I see what you mean. The table naming is inconsistent. And I can't see other programs using this specific http_request table.

Tables are renamed in 72bae7b. The database is meant for isolated PEDM usage. This is expressed in the example config, with a database name of pedm or file of pedm.sqlite.

Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) Apr 9, 2025

Choose a reason for hiding this comment

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

I think it’s better to use a different database completely! 🙂
I agree with the way you renamed the tables!

`pedm_` prefix is removed. The usage was inconsistent.
`pedm_schema_version` is now simply `version`.

The database is meant for isolated PEDM usage. This is expressed in the example config, with a database name of `pedm` or
file of `pedm.sqlite`.
Comment thread crates/devolutions-pedm/src/api/mod.rs Outdated
- use `DateTime` `Display` instead of `Debug` in `ParseTimestampError`
  `Display`
- comments should go above `#[cfg(feature)]`
- adds random comments
Postgres `log_http_layer` was calling `query_one` when nothing was
returned. The database was still written but the function returned an error.
This one was a bit funny.

`query_opt` returns `Option<Row>`. It was doing `opt_row.map(|r| r.get::<_, Option<D>(0))`, which would be `Some(Some(D))` or `None` if the row was not found. With `unwrap_or_default`, this was returning `Some(D)` or `None`, which is perfectly well.

However, the actual column is not nullable so `opt_row.map(|r|
r.get::<_, D>(0))` is simpler.
This disambiguates `requests_received` and `request_count`.
Comment thread crates/devolutions-pedm/src/model.rs Outdated
Comment on lines +25 to +26
pub(crate) request_count: i32,
/// The request count at the time of the server startup.
pub(crate) startup_request_count: i32,
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) Apr 10, 2025

Choose a reason for hiding this comment

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

naming: I feel it’s not necessary to add the startup prefix, since this is a field of a struct named StartupInfo. Or maybe I don’t really understand what this is? Is the intention to count the total of requests ever made? "All time request count"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It was because I was using the field in #[serde(flatten)]. But evidently the naming still wasn't clear.

My proposal is c17bdc5.

Comment thread crates/devolutions-pedm/src/db/mod.rs Outdated

mod err;

use chrono::{DateTime, Utc};
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

style: Inconsistent import order.

Copy link
Copy Markdown
Contributor Author

@allan2 Allan Zhang (allan2) Apr 10, 2025

Choose a reason for hiding this comment

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

Comment on lines +164 to +170
/// Returns the schema version from the `version` table.
async fn get_schema_version(&self) -> Result<i16, DbError>;

/// Initializes the database schema.
///
/// This creates tables.
async fn init_schema(&self) -> Result<(), DbError>;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

question: Should we really expose the schema version to the user of the Database trait?

I think it’s more appropriate to define a single method called setup that is performing any backend-dependent setup sequence, including initializing the schema and/or performing migrations if necessary.

See how it’s done in job-queue/job-queue-libsql crates:

async fn setup(&self) -> anyhow::Result<()> {
self.apply_pragmas().await?;
self.migrate().await?;
Ok(())
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

My proposal is 4eed892.

I put shared logic in the Db wrapper setup function, formerly called init_schema.

I kept get_schema_version and init_schema on the Database trait because they are just DB operations.

I didn't do Database::setup because I didn't want to duplicate the code in Db::setup. But I can make Database::setup with the duplication if you prefer to keep the logic in the trait implementation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fair enough. My thinking was to keep the exact setup logic as an implementation detail to leave more leeway, but at this point it does not really matter so we may as well avoid the logic duplication as you suggest. Certainly a better abstraction will emerge when it becomes necessary.

Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

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

thought: It seems the /about route is close to something that is typically called /health. I’m curious if you have further motivations for the /about route?

seconds integer NOT NULL
);

INSERT INTO version (version) VALUES (1) ON CONFLICT DO NOTHING; No newline at end of file
Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) Apr 10, 2025

Choose a reason for hiding this comment

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

question: Any objection to starting with 0? I think it can be useful in the future, so we can index an array of migrations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just chose 1 because SQL sequences start at 1.

Let me know what you prefer and I'll change it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think 0 would be better because we wouldn’t index into a SQL sequence. But, it’s not a hill I’m willing to die on either as it’s not too hard to throw a - 1 in there.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Changed to 0 in 2203fb9.

This further disambiguates `startup_request_count` and
`current_request_count` in `AboutData`.
I went through every file in the crate.
This adds a placeholder for backend-specific setup code, such as pragma
tuning on SQLite.
@allan2
Copy link
Copy Markdown
Contributor Author

thought: It seems the /about route is close to something that is typically called /health. I’m curious if you have further motivations for the /about route?

/health is an infallible endpoint to check if the server is running.
/about could fail because of a DB error.

As for the motivation, I included some info that was useful for debugging.

Copy link
Copy Markdown
Member

@CBenoit Benoît Cortier (CBenoit) left a comment

Choose a reason for hiding this comment

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

LGTM! Feel free to merge!

@allan2 Allan Zhang (allan2) enabled auto-merge (squash) April 10, 2025 16:26
@allan2 Allan Zhang (allan2) merged commit 76138e9 into master Apr 10, 2025
38 checks passed
@allan2 Allan Zhang (allan2) deleted the pedm-sql-init3 branch April 10, 2025 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants