Skip to content

Bug 2084607 - reinstate recursive_triggers in Places - #7685

Open
ddurst wants to merge 2 commits into
mozilla:mainfrom
ddurst:main
Open

ddurst wants to merge 2 commits into
mozilla:mainfrom
ddurst:main

Conversation

@ddurst

@ddurst ddurst commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

For bug 2084607, this adds an explicit PRAGMA recursive_triggers = ON. Accordingly, also it adds the following:

  • a clarifying comment about moz_bookmarks_parent_afterdelete_trigger in sql/create_shared_triggers.sql
  • a test for the foreign_count decrement/increment in src/db/schema.rs
  • a test for the delete triggering in src/storage/bookmarks.rs

This restores the behavior for cascading deletions that were present with the previous FK, which were assumed to still be in effect.

Two more changes will be needed, addressed separately (handling existing erroneous orphans, and recalculating the moz_places.foreign_count).

Pull Request checklist

  • Breaking changes: This PR follows our breaking change policy
    • This PR follows the breaking change policy:
      • This PR has no breaking API changes, or
      • There are corresponding PRs for our consumer applications that resolve the breaking changes and have been approved
  • Quality: This PR builds and tests run cleanly
    • Note:
      • For changes that need extra cross-platform testing, consider adding [ci full] to the PR title.
      • If this pull request includes a breaking change, consider cutting a new release after merging.
  • Tests: This PR includes thorough tests or an explanation of why it does not
  • Changelog: This PR includes a changelog entry in CHANGELOG.md or an explanation of why it does not need one
    • Any breaking changes to Swift or Kotlin binding APIs are noted explicitly
  • n/a Dependencies: This PR follows our dependency management guidelines
    • Any new dependencies are accompanied by a summary of the due diligence applied in selecting them.

@ddurst
ddurst requested review from a team, bendk and mhammond and removed request for a team October 23, 2026 18:43
@ddurst

ddurst commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

There's a followup patch that should land with this, I'll update momentarily.

@ddurst

ddurst commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

Added the migration from 20 => 21 to clean up any existing orphans that should be deleted, and recalculates foreign_count on what's left.

@ddurst
ddurst marked this pull request as ready for review October 26, 2026 16:33

@bendk bendk left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks right to me, with the possible exception of the comment I left about SyncStatus::normal. Mark should also take a look though, since he knows this code better than me.

UPDATE moz_places SET foreign_count =
IFNULL((SELECT n FROM placeForeignCounts WHERE place_id = moz_places.id), 0)
WHERE foreign_count IS NOT
IFNULL((SELECT n FROM placeForeignCounts WHERE place_id = moz_places.id), 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the WHERE needed? It seems like setting something to it's current value should be okay and just a no-op. Is there a trigger or something that runs when this is updated?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a trigger that would normally run, but it's not defined yet, so that WHERE is just updating only what rows would change based on this recalculation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have strong feelings either way, but it seems simpler and maybe faster to just update all rows. I'd expect that the WHERE check is slower than just setting the value regardless.

DELETE FROM moz_places
WHERE foreign_count = 0 AND last_visit_date_local = 0 AND last_visit_date_remote = 0;",
normal = SyncStatus::Normal as u8,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only SyncStatus::Normal? I think this bug could also happen for SyncStatus::new

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you have a new one that's an existing orphan due to its grandparent being deleted?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where we need Mark, but I thought SyncStatus::New just meant that it hasn't been synced yet.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. If that's what it means, it's probably ok since it should've been deleted?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't it have hit the same bug you're fixing now? I didn't think it was sync only, just that we only observed it during sync.

// places could remain with zero foreign_counts, instead of
// leaving them to be cleaned up by history, they are explicitly
// deleted at the end of this migration.)
db.execute_batch(&format!(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be split up into a series of db.execute() calls? That way SQLite will handle interpolating values like normal and now rather than Rust's format! method.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought now() isn't available because the connection doesn't run prepare (where that custom function is defined). But it's still a good question given the number of statements here.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with generating now in Rust, but my main concern is that SQLite should handle interpolation, rather than format!. The code would be something like:

db.execute("
     INSERT OR IGNORE INTO moz_bookmarks_deleted(guid, dateRemoved)
     SELECT guid, ? FROM moz_bookmarks
     WHERE id IN (SELECT id FROM orphanedBookmarks)
     AND syncStatus = ?", now, normal)?;

This branch has not been deployed

No deployments
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