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
Crash when observing virtual table? #620
Comments
Had the time to dig into this and do some further debugging. It definitely has to do with my search index table. The The observation is created like this — using the request in the first post: let observation = RecipePreview
.searchRequest(filter: filter, query: query, tags: tags)
.observationForAll() I realize all of this could probably be worked around by creating a custom |
Hello @simme, Thanks for the investigation. It looks like observation and FTS4 virtual tables don't play well together. Is your FTS table an External Content Full-Text Table which mirrors and indexes the content of another non-FTS table? |
This is how I create the table: func createRecipeSearchIndex(in db: Database) throws {
try db.create(virtualTable: "recipeSearchIndex", using: FTS4()) { table in
table.tokenizer = .porter
table.column("stringIdentifier").notIndexed()
table.column("corpus")
}
} |
It is not automatically synchronized |
OK. So we can not observe the source regular table instead. To know if we can observe your full text table, could you run a raw TransactionObserver and log all calls to |
Alright, so with a custom observer looking like this: private class MyObserver: TransactionObserver {
func observes(eventsOfKind eventKind: DatabaseEventKind) -> Bool {
print("Observes event of kind: \(eventKind)")
return true
}
func databaseDidChange(with event: DatabaseEvent) {
print("Did change with event of kind \"\(event.kind)\" in table \"\(event.tableName)\"")
}
func databaseDidCommit(_ db: Database) { }
func databaseDidRollback(_ db: Database) { }
} I am getting this output:
When someone changes the language in the app for example I wipe the old index and insert a new one. (Looking at it like this I should probably do a delete all instead of individually when refreshing, but irrelevant to the issue at hand :) ) |
I've been snooping around, and if I break on line 172 in DatabaseRegion.swift, which is this: public func isModified(byEventsOfKind eventKind: DatabaseEventKind) -> Bool {
return intersection(eventKind.modifiedRegion).isEmpty == false
} That method in this case returns |
OK, those are all FTS4 "shadow tables". We have to read the raw doc in order to know if we can rely on them in order to observe the index.
For all events? DatabaseRegion has a description or debugDescription property which outputs it in a readable way. What does it give? |
@simme, I'm sorry to "give you work". I can investigate this myself: it's not hard to create an FTS4 index and reproduce your issue. I admit I appreciate that you are looking for the exact point of failure, because not everybody is interested in how observation works :-) |
No worries! I'm not expecting any full service support here. I'm using GRDB and deriving great benefit from it, so of course I want to help as much as possible! And it's a core part of my app so trying to understand how it works feels like a given :) Some help in the right direction would be much appreciated though ;)
In my case right now I don't even need to observe the index, really. But it wouldn't hurt. And might be what one expects.
Where should I print the |
This is the output of
And this is
|
I'm sorry, the above might've been misleading information. That method is called again later by another event in which Not sure yet how this connects :P But it seems as though an update to one of the shadow tables are reported as an update to the virtual table, which is observed. |
@simme, I'll have a look shortly. |
Could it be that all of the code is working as expected, but when one does an |
Don't mean to impose stress because of my spamming. Just posting my findings while debugging in hopes of rubber ducking to make myself understand what's going on and helping you help me :D |
It's exactly that. For simplicity, let's remove all the useless parameters that hide what's happening, and just reduce the problem to an SQL request which joins one regular table and an FTS4 table: try dbQueue.write { db in
try db.create(table: "document") { t in
t.autoIncrementedPrimaryKey("id")
}
try db.create(virtualTable: "ft_document", using: FTS4()) { t in
t.column("content")
}
}
let request = SQLRequest<Row>("""
SELECT document.* FROM document
JOIN ft_document ON ft_document.rowid = document.id
WHERE ft_document MATCH 'foo'
""") When asked what this request looks at, SQLite says: the // prints "document(*),ft_document(ROWID,ft_document)"
try dbQueue.read { db in
try print(request.databaseRegion(db))
} This region is observed: if it is impacted, the observation will refresh its value. Other database modifications that happen elsewhere will be ignored. Now let's run this statement: try dbQueue.write { db in
try db.execute(
sql: "INSERT INTO ft_document (rowid, content) VALUES (?, ?)",
arguments: [1, "foo"]) When asked what this statement is about to do, SQLite says that it will insert into the The observation becomes active, because this insertion can impact its observed region. Its transaction observer waits eagerly for actual changes that may impact its region. And there lies the crash: the modifications were advertised to happen on the We have a few options:
Options 1 is complex, and makes it less easy to maintain GRDB on the long run. What about FTS3, FTS5, other virtual tables? So option 2 is probably the correct one. |
It is not quite the end of the story 😁 Observing an External Content Full-Text Table fails to spot changes performed on the source regular table. |
Nice find! I've read your tests and looked at the fix. But I fail to come up with anything but naïve workarounds.. :P |
Fixed! I'm closing this issue in favor of #622. |
(As always, thanks @simme for your contributions) |
What did you do?
I have two tables, one that contains just the contents of a specific record type and another that is a FTS4 search index table of said the previously mentioned content.
Both tables have a corresponding type (
struct
conforming toTableRecord
andFetchableRecord
). The type that represents a row in the "regular" table defines ahasOne
association with the search index type.When performing a search in the app I create a
QueryInterfaceRequest
that among other things joins on thehasOne
association which allows me to filter the request on matches in the index table.It looks like this:
The query filter is just this extension on
QueryInterfaceRequest
:What did you expect to happen?
In most cases this works great and allows me to use a
UISearchController
to populate a collection view with the search results. However, I also observe this request because these items can be "favorited", among other things, which changes the display of the item in the list.What happened instead?
Whenever a search is performed while the search index is update I get a
fatalError
on line 201 ofDatabaseRegion.swift
stating:Environment
GRDB flavor(s): GRDB
GRDB version: 4.2.1
Installation method: Manual (git submodule)
Xcode version: 11
Swift version: 5.1
Platform(s) running GRDB: iOS
macOS version running Xcode: 10.15 beta.. 8?
I realise this probably has something to do with the virtual table. I guess my main questions is: is this a bug or intended behaviour? And can it be worked around somehow?
Thanks
The text was updated successfully, but these errors were encountered: