Conversation
|
|
||
| @staticmethod | ||
| def __get_engine(library_dir: Path, in_memory: bool, sql_filename: str): | ||
| def _get_engine(self, library_dir: Path, in_memory: bool, sql_filename: str): |
There was a problem hiding this comment.
Why make that non-static?
| class DBMigrations: | ||
| def __init__(self, library_dir: Path, engine: Engine) -> None: | ||
| from tagstudio.core.library.alchemy.library import Library | ||
| def __init__(self, library: "Library") -> None: |
There was a problem hiding this comment.
I had originally passed the library path and engine because the engine argument will be removed when the migrations are made sql alchemy independent (and thus fully independent of the model that the to-be-migrated lib doesn't yet follow). Once that is done the parameter would only be the library path which is a much looser coupling then passing the full library in1.
For that reason I feel it is undesirable to pass the entire library to the constructor here.
Also, afaict it isn't necessary.
Note: Afaict the statements setting library_dir in Library.create_sqlite_libraryLibrary.open_sqlite_library can also be removed once this is reverted.
Footnotes
-
We want the coupling to be as loose as possible here, because the logic in the Library class will be for the current schema while the migrations deal with old schemas and so we can't (and shouldn't where we still can) rely on that logic. ↩
| @override | ||
| @classmethod | ||
| def run(cls, session: Session, library_dir: Path, fmt_log): | ||
| def run(cls, session: Session, library: "Library", fmt_log: Callable[[str], str]): |
There was a problem hiding this comment.
I like annotating the methods with the type for fmt_log, but I think it would be better to define something like LoggingMethod = Callable[[str], str] and to then set fmt_log: LoggingMethod
| @staticmethod | ||
| def save_library_backup_to_disk(library_dir: Path) -> Path: |
There was a problem hiding this comment.
I had made this static since the idea is to call it when the library isn't loaded yet (and can't be loaded at all due to needing to be migrated), that's why it was static and why I still think it should be here.
We could maybe also just move this to the DBMigrations to be done as the first step of DBMigrations.run.
There was a problem hiding this comment.
The save_library_backup_to_disk() method doesn't require a library to be loaded, it just uses the Library instance's self.library_dir, which I tweaked to be set before the migrations are run in open_sqlite_library().
We could maybe also just move this to the DBMigrations to be done as the first step of DBMigrations.run.
This method is also used outside of the migrations, like in the UI - so there still needs to be a non-static version of it present in the Library class for instances to use without passing the argument of the instance's own library_dir.
There was a problem hiding this comment.
The
save_library_backup_to_disk()method doesn't require a library to be loaded, it just uses the Library instance'sself.library_dir,which I tweaked to be set before the migrations are run inopen_sqlite_library().
Yeah this is exactly what I meant; normally the library_dir means "the directory of the currently open library" but here it effectively is "the directory of the library that should be backed up" (unless a library is actually open, in which the original meaning is correct again).
We could maybe also just move this to the DBMigrations to be done as the first step of DBMigrations.run.
This method is also used outside of the migrations, like in the UI - so there still needs to be a non-static version of it present in the Library class for instances to use without passing the argument of the instance's own
library_dir.
I think having a static method and a non-static method that calls the static one (with self.library_dir as the param) would be best then.
| ) | ||
| ) | ||
| except Exception: | ||
| return 0 |
There was a problem hiding this comment.
This split existed, because it is being used in the migrations where the existing engine should be used to ensure consistency irrespective of future changes to the migrations.
However, I think this can just be moved to the migrations entirely as the only other use can be removed.
(It is in LibraryInfoWindow where get_version(DB_VERSION_CURRENT_KEY) is contrasted to DB_VERSION, which is useless because the migrations fail unless get_version(DB_VERSION_CURRENT_KEY) == DB_VERSION meaning that can be removed.)
There was a problem hiding this comment.
This split existed, because it is being used in the migrations where the existing engine should be used to ensure consistency irrespective of future changes to the migrations.
Where might an inconsistency arise that would be solved by passing a different engine to build a session from?
However, I think this can just be moved to the migrations entirely as the only other use can be removed.
(It is in LibraryInfoWindow where get_version(DB_VERSION_CURRENT_KEY) is contrasted to DB_VERSION, which is useless because the migrations fail unless get_version(DB_VERSION_CURRENT_KEY) == DB_VERSION meaning that can be removed.)
The current UI-facing use of this is working as intended, where it shows the current loaded DB version in comparison to the program's currently known DB_VERSION, which is useful for telling if you have a DB loaded with a minor version greater than the TagStudio version that's opening it. I'd also like to not gate the _get_version() method to the migrations, as that would limit future uses in the UI.
Perhaps the "static" version of this that includes the engine parameter and inspection conditional could be moved to the migrations class, while a simpler instance method that just uses the currently expected logic could remain in the library file? Though that's a tiny bit of code duplication
There was a problem hiding this comment.
This split existed, because it is being used in the migrations where the existing engine should be used to ensure consistency irrespective of future changes to the migrations.
Where might an inconsistency arise that would be solved by passing a different engine to build a session from?
The engine is specific to the DB that is currently open, so if we were to e.g. allow a user to bulk migrate libraries while a different library is open, then this get_version method would cause the migrations to see the incorrect version and not migrate anything because it would see the version of the currently open library and not the one of the DB being migrated.
However, I think this can just be moved to the migrations entirely as the only other use can be removed.
(It is in LibraryInfoWindow where get_version(DB_VERSION_CURRENT_KEY) is contrasted to DB_VERSION, which is useless because the migrations fail unless get_version(DB_VERSION_CURRENT_KEY) == DB_VERSION meaning that can be removed.)The current UI-facing use of this is working as intended, where it shows the current loaded DB version in comparison to the program's currently known
DB_VERSION, which is useful for telling if you have a DB loaded with a minor version greater than the TagStudio version that's opening it.
Good point, I missed that.
I'd also like to not gate the
_get_version()method to the migrations, as that would limit future uses in the UI.Perhaps the "static" version of this that includes the engine parameter and inspection conditional could be moved to the migrations class, while a simpler instance method that just uses the currently expected logic could remain in the library file? Though that's a tiny bit of code duplication
Yes, I think that would be best. That way get_version is basically a one-liner and the complicated DB introspection logic can be relegated to the migrations.
Also, uncoupling the migrations from sqlalchemy would probably require that anyway, so we might as well do it now.
RE the actual fix: I am not quite sure why this fixes anything, because the original setup was not missing any context managers at all. The |
For the non-static reversions, this was mostly motivated by the idea that the Library is not intended to be a singleton class - each instance of the Library class should be completely self-contained and methods for the class that affect instanced versions should only affect data for said instance. While making some methods static didn't necessarily turn the Library class into a singleton, it still separated the self-contained nature of the Library instance by requiring a session to be independently supplied (which raised a deeper issue with the
When turning the So a lot of the static reversions weren't 100% necessary to pull off this fix, but sort of went hand-in-hand with it and also reflect my own intentions for how the Library class should be used. |
This only applies to the Also, the semantics of
The session is in no way associated with the Library instance
If you compare how the session is built now and how it was built then, they are exactly identical, so this cannot be the reason afaict.
Not quite sure what you mean here |
The
I'm referring to how the session context managers in the Library class are constructed using
If you've got an alternate explanation for #1447 I'm all ears, but from all the investigating and testing I've done it really seems like the session currently is getting detached or desycned or something from the rest of the Library, causing a lock on the DB because there's now something outside the scope of the expected session that's using it (in conjunction with using Changes made from there were either due to other obscured issues like the DB9 migration flushing and/or due to wanting to enforce a particular pattern for accessing library methods. |
|
@Computerdores Unfortunately my hands are kind of tied with this, where the reversions of (at least some of) the static methods plus the reversion to use the session associated with a Library instance is fixing this critical issue that needs to ship as soon as it can, and I don't have evidence of an unrelated cause for all this. The closest I've been able to glean is a similar issue I fixed in dd00f4d where colors couldn't be added due to a dormant double session issue that started causing a DB lock, but I couldn't find evidence of a double session going on here. SQLAlchemy doesn't exactly make it straightforward to trace locks, unfortunately... We can revisit some of the static discussions and migration parameters in the future, but right now there needs to be a fix for #1432/#1456 and this is the only one, which was created on a best-effort basis for this nebulous ORM breakage. |
|
I believe This helps quell my concerns about passing sessions around, and with seeing the double session issue in dd00f4d plus a clear example of an internal need to shield library logic from double sessions in #1470, I've gained a greater context for what kinds of shifts are needed in the Library class design - keeping in mind that the session/cursor concept will likely stay after moving from SQLAlchemy to raw sqlite3. I think there's a growing split in what the Library class is thought to be used for now, when previously it was purely a "Class for the Library object, and all CRUD operations made upon it", and having static methods that are intended to be used publicly or psuedo-publicly that don't relate to a Library instance felt inherently against this - but more of these types of operations are becoming necessary, including by Library instances themselves. One direction things could take is splitting the static, non-context-managed logic out into a separate class ( So with that in mind, I think I'm going to do a quick commit to main with the |
So I wasn't going insane! Although I would have expected a warning from pyright for this kind of thing, but whatever
I would suggest calling it
Good decision, even before racerand's insight; much as I dislike pushing a fix I don't understand (must be the security part of my brain) I also think that it is more important to have it working than for the code to be beautiful. |
Summary
This PR fixes issues caused by a missing session context manager in the
all_entries()method in conjunction with the newautocommit=FalseDB parameter, and makes some tweaks to the migration refactors done in #1432 and #1456, most notably removing static methods in theLibraryclass and removing local imports because I'm an idiot and forgot that I didn't completely review #1456 before signing off and pulling itCloses #1447, Closes #1467
Tasks Completed