Page MenuHomePhabricator

[native/sqlite] set `journal_mode` on fresh install
ClosedPublic

Authored by kamil on Oct 4 2022, 6:11 AM.
Tags
None
Referenced Files
Unknown Object (File)
Fri, Nov 22, 2:33 AM
Unknown Object (File)
Fri, Nov 22, 1:39 AM
Unknown Object (File)
Thu, Nov 21, 7:25 PM
Unknown Object (File)
Thu, Nov 21, 12:43 AM
Unknown Object (File)
Sun, Nov 3, 12:36 AM
Unknown Object (File)
Sun, Nov 3, 12:36 AM
Unknown Object (File)
Sun, Nov 3, 12:36 AM
Unknown Object (File)
Sun, Nov 3, 12:36 AM

Details

Summary

Add missing pragma statement setting journal_mode on a fresh install.

More context here.

Test Plan
  1. Build app without db encryption code.
  2. Logout user.
  3. Connect with db using sqlite3 and run PRAGMA journal_mode; to check if mode was set properly.

Diff Detail

Repository
rCOMM Comm
Branch
fix-journal-mode
Lint
No Lint Coverage
Unit
No Test Coverage

Event Timeline

kamil held this revision as a draft.
kamil edited the test plan for this revision. (Show Details)
kamil published this revision for review.Oct 4 2022, 6:21 AM

Questions inline, leaving as a comment so that the diff stays on other reviewers' queues

native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
497 ↗(On Diff #17318)

I suspect there's a way to set up sqlite_orm to specify this... I can see journal_mode::WAL in sqlite_orm.h. That said, with the context in ENG-1595, it's probably not worth it. I guess if it's easy, wouldn't hurt

610 ↗(On Diff #17318)
  1. What happens when we throw std::runtime_error? I'm guessing the app crashes?
  2. Is this similar to how we handle errors that might arrise in other ways? I can see that errors in the PRAGMA user_version bump below are handled the same way, but I'm curious about how errors in sync_schema() are handled
  3. Should all of this be wrapped in a transaction? It seems like we're relying on an assumption that all of the migrations are idempotent as of D5290, but this assumption appears to be implicit and consequently there's a chance that it gets violated in the future

set journal_modeusing sqlite_orm

native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
497 ↗(On Diff #17318)

Initially, I used migration because there was nothing about this in sqlite_orm docs but quickly dive into the source code of this lib and there is an easy way to do it.

I saw ENG-1595 but maybe for now let's stick to one way of configuring things so I've added specifying this by sqlite_orm.

610 ↗(On Diff #17318)

According to the change it's not really related but let me respond for clarity.

What happens when we throw std::runtime_error? I'm guessing the app crashes?

Yes, because it's an error in the database code app will crash. The decision of using this is because any error tells us that there is something wrong with the database, suggested here.

Is this similar to how we handle errors that might arrise in other ways? I can see that errors in the PRAGMA user_version bump below are handled the same way,

It's a different approach, implemented like the suggestion linked previously but I write something more about this in the summary of https://phab.comm.dev/D5291

but I'm curious about how errors in sync_schema() are handled

It throws an error that is being handed further.

Should all of this be wrapped in a transaction? It seems like we're relying on an assumption that all of the migrations are idempotent as of D5290, but this assumption appears to be implicit and consequently there's a chance that it gets violated in the future

Do you mean only the pragma setting user version or the entire scope?

Do you mean only the pragma setting user version or the entire scope?

I meant everything that happens in the if (current_user_version == 0) { block.

It throws an error that is being handed further.

Can you link where it's handled?

Please answer all the questions before landing

This revision is now accepted and ready to land.Oct 5 2022, 2:23 AM
native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp
556 ↗(On Diff #17322)

If we set journal mode here do we also need to set it when running migrations? Isn't calling enable_write_ahead_logging_mode redundant now? I just want to make sure we do not perform redundant operations.

marcin requested changes to this revision.Oct 5 2022, 4:57 AM
This revision now requires changes to proceed.Oct 5 2022, 4:57 AM

set journal_mode using sqlite_orm only on fresh install

According to docs (https://stackoverflow.com/questions/63099657/sqlite-pragma-journal-mode-statement-persistence) this pragma with WAL value is persisted so it is safe to set it only when creating fresh database.

This revision is now accepted and ready to land.Oct 5 2022, 6:14 AM

If we set journal mode here do we also need to set it when running migrations? Isn't calling enable_write_ahead_logging_mode redundant now? I just want to make sure we do not perform redundant operations.

Moved setting journal_mode only to fresh install logic.

It throws an error that is being handed further.

Can you link where it's handled?

One note: I was meaning handed, like pass on/forwarded like any other error thrown in SQLiteQueryExecutor - not handled.

In general sqlite_orm performs sync_schema with simple CRUD operations which may throw std::system_error.

But to answer, the code we talking about happens in the migrate() function which is called in the constructor of SQLiteQueryExecutor or in the method responsible for deleting the database.
So it depends on usage of DatabaseManager::getQueryExecutor() - I saw places surrounded by try {} catch {} and without, so probably in some cases app is killed. I am not familiar enough to give a very descriptive answer and want to respond quickly so if I am wrong, @marcin could you correct me?

I meant everything that happens in the i`f (current_user_version == 0) {` block.

Honestly, I don't feel it's a good idea. ORM will create tables using standard SQLite operations and according to SQLite docs "Any command that accesses the database (basically, any SQL command, except a few PRAGMA statements) will automatically start a transaction if one is not already in effect.". By doing that we will change that default behavior and make everything that sqlite_orm doing into one huge transaction, which will need to be handled separately but thong have a strong opinion what is better.

I think we need to understand what's going on here better. It's clear from the discussion in ENG-1910 that our understanding is incomplete, and I'm concerned that we're simply not going deep enough here.

I have two questions below. We probably don't need to block landing this diff on them, especially because the original issue is blocking releases. @kamil, maybe you can create a task for each question, and follow-up in the task with the answer?

I saw places surrounded by try {} catch {} and without, so probably in some cases app is killed. I am not familiar enough to give a very descriptive answer and want to respond quickly so if I am wrong

Can you do a thorough analysis of all callsites please? I think you should be able to do this on your own, and @marcin can correct you if you're wrong :)

In D5292#156364, @kamil wrote:

Honestly, I don't feel it's a good idea. ORM will create tables using standard SQLite operations and according to SQLite docs "Any command that accesses the database (basically, any SQL command, except a few PRAGMA statements) will automatically start a transaction if one is not already in effect.". By doing that we will change that default behavior and make everything that sqlite_orm doing into one huge transaction, which will need to be handled separately but thong have a strong opinion what is better.

Can you do a thorough analysis of how sqlite_orm handles sync_schema and whether it creates a transaction for everything within that? The fact that SQLite starts a transaction per query if one isn't already in effect doesn't seem relevant here.

In D5292#156364, @kamil wrote:

Honestly, I don't feel it's a good idea. ORM will create tables using standard SQLite operations and according to SQLite docs "Any command that accesses the database (basically, any SQL command, except a few PRAGMA statements) will automatically start a transaction if one is not already in effect.". By doing that we will change that default behavior and make everything that sqlite_orm doing into one huge transaction, which will need to be handled separately but thong have a strong opinion what is better.

Can you do a thorough analysis of how sqlite_orm handles sync_schema and whether it creates a transaction for everything within that? The fact that SQLite starts a transaction per query if one isn't already in effect doesn't seem relevant here.

@kamil and I discussed this in our 1:1 today.

  1. We concluded that sqlite_orm does not wrap sync_schema in a transaction.
  2. We thought a bit about whether this could cause issues, but our conclusion was that an error in sync_schema would throw a system_error, which would prevent user_version from being incremented. Then we would expect sync_schema to run again on the next start, which would hopefully have the same effect as wrapping the whole thing in a transaction. (sync_schema is meant to be idempotent.)
  3. We also decided we should call sync_schema(true) instead of sync_schema(). By setting the preserve parameter to true, we make the call a little safer. (Even though we don't expect the call to ever happen with an existing database.)
  4. We're likely going to deprecate sqlite_orm soon anyways, so we didn't want to go too deep on this.

Only follow-up on this one is to update sync_schema() to sync_schema(true), which can be done in a separate diff. Meanwhile, the earlier discussion about errors and safety can be continued in ENG-1974.