-
-
Notifications
You must be signed in to change notification settings - Fork 45
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merge Tags #678
base: main
Are you sure you want to change the base?
Merge Tags #678
Conversation
71c33cd
to
0591aef
Compare
2b15c92
to
13c989c
Compare
let itemsWithOldTags = new Set() | ||
// find all item ids which have taggings of tags to be deleted | ||
for (const id of mergeIds) { | ||
itemsWithOldTags.add(...await this.items(db, id)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using this
here and below is potentially dangerous. It will commonly happen that these helpers are invoked directly and not via the common object used as the default export. We may also change the way we export these model functions.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there another way to access that function without using this
? I don't understand the JS export system well enough to properly understand this issue
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue itself is not so much about the module/export system (but it's likely to be exposed because of it).
The problem is that this
is only bound when the function is invoked (unless it's a bound or an arrow function!). If we reference this.items
here, this may or may not work depending on how the function is called. The way use these functions makes it very likely that we'll have to pass them around to be invoked later, which would likely break. For example:
let a = { f() { return this.g() }, g() { return '!' } }
a.f() //-> '!'
setTimeout(a.f, 25) //-> This will fail. It's similar to:
let f = a.f
f() //-> Which also fails.
It's very common that we have to pass these type of functions around (e.g., when we use them in Commands). The module/export dynamics also play a role, because named exports are the norm with ESM we may want to switch to named exports going forward so that we can import { merge } from './models/tags.js'
.
In summary, you can easily address this by hoisting the items
function similar to the load
function (which is referenced by create
). If we want to export all functions individually that's probably what we should do anyway, for all the model functions, but then we probably need address a few naming issues (e.g., the 'delete' function would clash with the keyword; 'items' in isolation is probably too cryptic etc..). Another alternative would be to still keep everything in a namespace object like we currently do, but also store it in a local variable for referencing purposes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In reference to the earlier example, of course setTimeout(a.f.bind(a), 25)
would work, but it's not good if we always need to remember when to bind functions and when it's safe to pass them around.
await this.delete(db, mergeIds) | ||
|
||
// recreate taggings for items which had deleted tags | ||
for (const item of itemsWithOldTags) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lets fold the loop into the DB command. This way we should be able to send a single insert command for all items to the DB instead of one command for each item.
...into('taggings') | ||
.insert({ tag_id: keepId, id: item })) | ||
} catch (e) { | ||
// TODO is there a nicer way to insert if not exists? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, there should be! We can probably use REPLACE INTO
which is just a SQLite shorthand for INSERT OR REPLACE INTO
. But maybe it would be better to use ON CONFLICT IGNORE
because REPLACE
will remove the old tagging first and we probably want to keep the original? (Our 'query generator' classes are far from complete; if we use this here we can add a way to set e.g. onConflict('ignore')
on the insert statement)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would INSERT OR IGNORE INTO
be appropriate here? I didn't know SQLite had all these extra options around insert
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think INSERT OR IGNORE
fits perfectly here. It applies to primary key constraints so we can use that instead of handling the error, or checking each tag/item combination first.
.insert({ tag_id: keepId, id: item })) | ||
} catch (e) { | ||
// TODO is there a nicer way to insert if not exists? | ||
console.error(e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We must be careful not to swallow real errors here (so make sure to throw). Because this function should be called with a transaction, we can be sure everything will be rolled back if we the function throws an error.
test/models/tag_test.js
Outdated
let db | ||
|
||
mkdbtmp(x => db = x, | ||
'db_test.sqlite', projectModel.create, { name: 'Test Project' }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here is a good illustration about the potential dangers of using this
in the model functions. If we use this
in create
here it will fail.
test/models/tag_test.js
Outdated
mkdbtmp(x => db = x, | ||
'db_test.sqlite', projectModel.create, { name: 'Test Project' }) | ||
// required to set up database schema | ||
// TODO is there a better way to do this? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is fine, but two things come to mind: I'd be interested to explore using an in-memory db for such kind of tests, because they're almost certainly going to be faster and we won't have to clean up the file afterwards. If I'm not mistaken then you should be able to call Database.create
with path ":memory:" to create an in-memory db. We could adjust the mkdbtmp helper to skip any file related actions if that's the path.
The second thought was that we could integrate this into the fixture helpers maybe.
let p = F.project.create()
And this works sort of like mkdbtmp
in that it adds before and after hooks which create the and close the db connection accessible under p.current
(sort of like React's refs). In fact we should change mkdbtmp
to also use that pattern then we can skip the callback and the tests are much easier to read. What do you say?
eda813e
to
fc053b3
Compare
I've explored re-writing the I've also changed some of the tests in the process where I thought it would be useful. Most importantly probably, when you test for exceptions or rejections always use the I may have made some other instructive changes -- just let me know if you anything catches your eye. The most confounding thing though, is that I broke your merge tests and I haven't figured out why yet. The failure is so bizarre that I wonder if it's due to some underlying bug so I committed this with the failure for now hoping that it will be instructive for us to figure out what's going on. Here is what I know so far: The before hook that inserts tags and items fails. The error is caused by the trigger that adds metadata values to the full-text index. Specifically, it looks like the |
OK I don't know yet why this fails, but I have isolated the issue now: it happens when you load the project schema and then keep using the db connection without closing it. Previously |
I think this is because virtual tables are registered per connection. When we restore the schema, the virtual table is added to the |
Seems like there's a simple solution! |
8726add
to
e734467
Compare
e734467
to
e755cf8
Compare
OK I rebased the branch again and added some minor changes to make the tests more concise and this should all be working again now. The jury is still out on the new It also looks like these DB tests fail consistently on the Windows CI again. Interestingly also the tests using the in-memory DB are failing so contrary to my priors, this might not be a file system issue after all! |
Sharp rebuilds recently started failing with this option.
Merge
command tocommands/tag
.actions/tag
that triggers the Merge command.merge
and forrename
tag(s) toTagList
TagList
context menu work with mutliple selected tagsCloses #144 #671