Skip to content

ENG-3288 Add onupdate to PrivacyPreferences.updated_at column#7851

Merged
erosselli merged 3 commits intomainfrom
erosselli/ENG-3288
Apr 8, 2026
Merged

ENG-3288 Add onupdate to PrivacyPreferences.updated_at column#7851
erosselli merged 3 commits intomainfrom
erosselli/ENG-3288

Conversation

@erosselli
Copy link
Copy Markdown
Contributor

@erosselli erosselli commented Apr 7, 2026

Ticket ENG-3288

Description Of Changes

The updated_at column on the PrivacyPreferences model was overridden from the Base class without onupdate=func.now(), so it remained NULL even when records were updated (e.g. when is_latest is flipped to False). This adds the onupdate trigger so updated_at is automatically set on any ORM-level UPDATE.

No migration needed — onupdate is a SQLAlchemy ORM-level behavior that adds SET updated_at = now() to UPDATE statements. It does not change the database schema.

Code Changes

  • Added onupdate=func.now() to the updated_at column on PrivacyPreferences
  • Added from sqlalchemy.sql import func import

Steps to Confirm

  1. Create a privacy preference via POST /api/v3/privacy-preferences
  2. Create a second preference for the same identity
  3. Fetch all records via GET /api/v3/privacy-preferences with include_historical=true
  4. Verify the demoted record (is_latest=False) has updated_at set
  5. Verify the new latest record has updated_at as null

Pre-Merge Checklist

  • Issue requirements met
  • All CI pipelines succeeded
  • CHANGELOG.md updated
    • Updates unreleased work already in Changelog, no new entry necessary
  • UX feedback:
    • No UX review needed
  • Followup issues:
    • No followup issues
  • Database migrations:
    • No migrations
  • Documentation:
    • No documentation updates required

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Actions Updated (UTC)
fides-plus-nightly Ignored Ignored Preview Apr 8, 2026 0:17am
fides-privacy-center Ignored Ignored Apr 8, 2026 0:17am

Request Review

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 7, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 85.06%. Comparing base (10d9fc7) to head (cf2c461).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7851   +/-   ##
=======================================
  Coverage   85.06%   85.06%           
=======================================
  Files         627      627           
  Lines       40765    40767    +2     
  Branches     4740     4740           
=======================================
+ Hits        34678    34680    +2     
  Misses       5018     5018           
  Partials     1069     1069           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@erosselli erosselli marked this pull request as ready for review April 7, 2026 20:12
@erosselli erosselli requested a review from a team as a code owner April 7, 2026 20:12
@erosselli erosselli requested review from adamsachs and removed request for a team April 7, 2026 20:12
Copy link
Copy Markdown

@claude claude bot left a comment

Choose a reason for hiding this comment

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

Code Review

The fix is correct and minimal. Adding onupdate=func.now() to PrivacyPreferences.updated_at is consistent with the pattern used across the codebase (including FidesBase.updated_at, privacy_preference.py, and many others), and no database migration is needed since this is a SQLAlchemy ORM-level hook, not a schema change.

Two observations worth noting:

  1. Raw SQL paths bypass onupdateonupdate=func.now() only triggers via the ORM's unit-of-work. consent_encryption_migration.py issues raw UPDATE SQL against this table, so those updates won't stamp updated_at. This is a pre-existing limitation, not a regression, but it's a gotcha if anyone relies on this column being current after running the encryption migration CLI command.

  2. NULL on INSERT is intentional but undocumented — unlike FidesBase, which sets server_default=func.now() on updated_at, this column stays NULL until the first ORM-level update. That's a defensible design choice (you can tell "never updated" from "updated at T"), but a short comment on the column definition would help future readers avoid confusion.

No blocking issues. The core fix is sound.

DateTime(timezone=True),
nullable=True,
onupdate=func.now(),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The onupdate hook fires only through SQLAlchemy's ORM unit-of-work pattern. It will not fire for bulk ORM updates (session.query(...).update(...)) or raw SQL. Looking at consent_encryption_migration.py, updates to this table are executed via raw SQL (UPDATE_QUERY = text(...)), which means updated_at will remain stale after an encryption migration run.

This isn't a regression introduced here — it's a pre-existing limitation — but it's worth being aware of. If accurate updated_at timestamps are important for those code paths too, those updates would need to explicitly set updated_at = now() in the raw SQL, or be routed through the ORM.

DateTime(timezone=True),
nullable=True,
onupdate=func.now(),
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nit / intentional difference to flag: the base class FidesBase.updated_at uses both server_default=func.now() and onupdate=func.now(), which means that column is populated on both INSERT and UPDATE. This model keeps nullable=True with no server_default, so updated_at starts as NULL and only gets set on the first ORM UPDATE. That appears intentional (you can distinguish "never updated" from "updated at time T"), but worth a brief comment in the code to make the design choice explicit for future readers.

@erosselli erosselli changed the title Add onupdate to PrivacyPreferences.updated_at column ENg-3288 Add onupdate to PrivacyPreferences.updated_at column Apr 7, 2026
Copy link
Copy Markdown
Contributor

@adamsachs adamsachs left a comment

Choose a reason for hiding this comment

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

generally looks good!

only comment is similar to what claude called out - is the deviation from the base class (no server default, nullable=true) intentional? if not, then we should just rely on the base class definition. if so (which i imagine it is), then i do think a quick comment could be helpful explaining what deviates and how. but that's a nit.

@erosselli erosselli changed the title ENg-3288 Add onupdate to PrivacyPreferences.updated_at column ENG-3288 Add onupdate to PrivacyPreferences.updated_at column Apr 7, 2026
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@erosselli erosselli added this pull request to the merge queue Apr 8, 2026
Merged via the queue into main with commit 9eda5a0 Apr 8, 2026
69 checks passed
@erosselli erosselli deleted the erosselli/ENG-3288 branch April 8, 2026 12:49
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