Skip to content

Add observe(statement:) to TransactionObserver#1864

Open
simolus3 wants to merge 2 commits into
groue:developmentfrom
simolus3:development
Open

Add observe(statement:) to TransactionObserver#1864
simolus3 wants to merge 2 commits into
groue:developmentfrom
simolus3:development

Conversation

@simolus3
Copy link
Copy Markdown

@simolus3 simolus3 commented May 5, 2026

Some statements might issue writes to the databases through a layer of indirection (e.g. via a user-defined function). For these statements, GRDB is unable to detect the writes made and doesn't call TransactionObserver.observes(eventKind:), doesn't install an update listener and has no way of observing these writes.

This adds TransactionObserver.observesAllDatabaseChanges, which can be used to make transaction observers listen for all database changes. It returns false by default, but can be added to implementations to observe indirect statements as well.

Closes #1863.

Pull Request Checklist

  • CONTRIBUTING: You have read https://github.com/groue/GRDB.swift/blob/master/CONTRIBUTING.md
  • BRANCH: This pull request is submitted against the development branch.
  • DOCUMENTATION: Inline documentation has been updated.
  • DOCUMENTATION: README.md or another dedicated guide has been updated.
  • TESTS: Changes are tested.
  • TESTS: The make smokeTest terminal command runs without failure.

@groue
Copy link
Copy Markdown
Owner

groue commented May 6, 2026

Thank you @simolus3!

I'll review this pull request in the next few days. At first glance, it does exactly what we talked about, and it does it well 👍

Please tell me if it allows PowerSync to achieve the goals you described in #1863, and in particular if later on you can call notifyChanges(in:) with the desired effects 🙏

@simolus3
Copy link
Copy Markdown
Author

simolus3 commented May 6, 2026

Please tell me if it allows PowerSync to achieve the goals you described in #1863, and in particular if later on you can call notifyChanges(in:) with the desired effects 🙏

Yes, I've tested these changes with PowerSync and they seem to do exactly what we need :) My approach right now is to add a catchall observer in a writeWithoutTransaction block running statements for PowerSync, once that is complete I forward collected writes with notifyChanges(in:) in an empty transaction. This updates GRDB value observations reliably 👍

@groue
Copy link
Copy Markdown
Owner

groue commented May 11, 2026

I had a closer look, and I could spot a design flaw 🙂

Your changes closely reflect our initial intent, but I gave you a misleading direction. It won't be difficult to fix, fortunately!


The symptom of the design flaw is that the new test fails if the observer returns false from observes(eventsOfKind:):

 private class EverythingObserver: TransactionObserver {
     func observes(statement: Statement) -> Bool {
         return true
     }
 
     func observes(eventsOfKind eventKind: GRDB.DatabaseEventKind) -> Bool {
-        return true  // test passes
+        return false // test fails
     }
 }

This is definitely a bug. With this modification of the test, the observes(statement:) method still returns true for both DELETE FROM foo and SELECT custom_clear_function(), so we ought to be notified of deletions.

The cause is that SQLite compiles the DELETE FROM foo statement with the truncate optimization enabled. When run, the statement truncates the table instead of deleting rows one by one. Individual deletions are not notified to the update hook, and the transaction observer is blind.

And there lies the design flaw: the truncate optimization is enabled or disabled from the compile-time authorization callbacks, before we can call observes(statement:), which means before the observer can instruct GRDB to disable the truncate optimization.


The intention described at #1863 (comment) still holds:

[...] because we can't build an argument for observes(eventsOfKind:) before executing SELECT powersync_clear(), it is necessary to come up with a new TransactionObserver method that opts in for the events created by this statement.

It turns wrong in the next paragraph:

For example: func observes(statement: DatabaseStatement) -> Bool that returns false by default (preserving the API contract with other transaction observers that do not implement this method).

I think our only solution is to let observers opt in for all update events, without any filtering:

 protocol TransactionObserver {
-    func observes(statement: Statement) -> Bool
+    var observesAllDatabaseChanges: Bool { get }
 }

If you lack time, I'll update this pull request accordingly. The TransactionObserver high-level documentation needs an update, too. Probably we should start mentioning the "update hook" aka the Data Change Notification Callbacks very explicitly, without beating around the bush. We also have various paragraphs titled "Dealing with undetected changes" (1, 2, 3) that need to include "statements ran from a custom SQL function" in the possible causes for undetected changes.

@simolus3
Copy link
Copy Markdown
Author

I think our only solution is to let observers opt in for all update events, without any filtering:

That will definitely work for PowerSync too, I've changed the PR to be based on that 👍

The TransactionObserver high-level documentation needs an update, too

I've added a small note on observesAllDatabaseChanges.

We also have various paragraphs titled "Dealing with undetected changes" (1, 2, 3) that need to include "statements ran from a custom SQL function" in the possible causes for undetected changes.

I suppose "Changes performed by SQLite statements that are not both compiled and executed through GRDB APIs" implicitly covers that (since the function executes the statement with direct SQLite APIs internally, not with GRDB APIs). But it's good to be explicit about this, and I've expanded that bullet point.

Probably we should start mentioning the "update hook" aka the Data Change Notification Callbacks very explicitly, without beating around the bush.

I'm not really sure where we'd mention that, in the section about transaction observers controlling for which events they'd be invoked?

I would be happy to expand the documentation with a bit more help from your side, but if you think it's quicker if you update this PR, then that's definitely fine with me as well :)

@groue
Copy link
Copy Markdown
Owner

groue commented May 12, 2026

Sounds great! OK I take charge of the remaining tidbits 👍 Thank you very much for your help 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants