Skip to content

feat(pg): PostgreSQL datastore compatibility layer - #6

Open
dnplkndll wants to merge 83 commits into
mainfrom
feat/pg-compat-clean
Open

feat(pg): PostgreSQL datastore compatibility layer#6
dnplkndll wants to merge 83 commits into
mainfrom
feat/pg-compat-clean

Conversation

@dnplkndll

@dnplkndll dnplkndll commented Apr 30, 2026

Copy link
Copy Markdown

Related issue: internal PG-compat fork work (no upstream issue)

Checklist for submitter

  • Input data is properly validated, SELECT * is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters.
  • Timeouts are implemented and retries are limited to avoid infinite loops

Testing

Database migrations

  • Checked schema for all modified table for columns that will auto-update timestamps during migration.
  • Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects.
  • Ensured the correct collation is explicitly set for character columns (COLLATE utf8mb4_unicode_ci). (N/A on PG; MySQL paths unchanged.)

What changed

Full PostgreSQL compatibility for Fleet's datastore layer, enabling the production deployment at fleet.hz.ledoweb.com to run against PostgreSQL 16 instead of MySQL.

Current state (2026-07-28): the live branch is feat/pg-compat-rebased, rebased onto upstream fleetdm/fleet@5cd8edbfb5. A full adversarial review (2026-07-27) was remediated across three phases — honest test gate, schema parity repair, driver/runner hardening — and a follow-up adversarial re-review found 3 more must-fixes, all closed the same day: the swap-caller bare DROP TABLE (broke the vulnerability host-counts cron hourly), the Windows ESP tri-state collapse, and updated_at frozen on ~125 tables (now a generated 138-trigger set applied on every prepare db with exact MySQL semantics). See docs/Deploy/pg-review-remediation.md for the complete findings reconciliation. Five validators + three staleness-checked generators gate every push. A branch quality pass (DRY/coverage/docs) followed. Phase 4 (the declared merge gate) is COMPLETE (2026-07-29): the PG CI job runs the datastore package with no -run filter — 121 dual-dialect tests pass against PG, 59 MySQL-only tests skip visibly, zero failures. Mid-phase the branch was re-rebased onto upstream fleetdm/fleet@54b32ddf88 (~84 more commits, conflict-free); two upstream migrations timestamped below our baseline marker were caught by the below-marker drift backstop and ported via Migration 20260729190000 (with a post-condition assert; one insert inlined because MySQL's integer literal for a boolean column is a 42804 on PG). Baseline marker: 20260729190000.

July 2026 rebase onto upstream 5cd8edb (1,114 upstream commits)

  • Re-resolved the PG layer across upstream's June–July refactors (Windows MDM reconciler rewrite, policy exclude-all scoping, FMA canonical naming — which upstreamed this fork's inline rename feature, software-title singleflight inserts, DDM scoped declarations).
  • 14 new upstream migrations ported to PG with isPostgres() branches: ALTER … CHANGE/MODIFY, DROP CHECK, inline ADD [UNIQUE] KEY, combined DROP/ADD INDEX, VIRTUAL generated ENUM columns (plain text on PG), IF() in CHECK expressions, REGEXP~, MySQL information_schema FK/index lookups → pg_constraint/pg_indexes. Idempotency guards added for schema this fork shipped ahead of upstream (pending-delete table, policy_gated, BYOD/ADUE).
  • PG tests now replay post-baseline migrations exactly as fleet prepare db does (seed history ≤ marker, goose Up through the rebind driver), so migration incompatibilities surface in TestPostgres* instead of at deploy.
  • Baseline regenerated from a scratch PG migrated via fleet prepare db --mysql_driver=postgres; marker bumped to 20260724134801. schema_identity_cols_gen.go regenerated; schema/column drift validators clean.
  • Rebind driver: strips ON UPDATE NOW(n) like ON UPDATE CURRENT_TIMESTAMP (emitting the updated_at trigger); ten new knownPrimaryKeys entries for new upstream upsert sites; the two mdm_configuration_profile_variables upserts (per-kind conflict targets) converted to the dialect helper.
  • GetHostMDM fix: upstream's new connected_to_fleet CASE mixed EXISTS(...) with 1/0 integer literals — PG rejects mixed CASE branch types (SQLSTATE 42804), which broke /api/fleet/orbit/config on first deploy. Converted to true/false literals (valid in both dialects); regression-covered by TestPostgresGetHostMDM.
  • Vulnerabilities cron fix: InsertCVEMeta on PG adds a WHERE … IS DISTINCT FROM guard so re-upserting the ~300k-row NVD set doesn't rewrite unchanged rows (dead tuples + index churn), and LoadCVEMeta's bulk-load deadline is raised 1m → 10m. This unblocks the hourly insert cve scores: context deadline exceeded failure observed in prod since at least Jul 12.

Core dialect abstraction

  • DialectHelper interface (dialect.go) abstracting all MySQL vs PostgreSQL SQL differences
  • mysqlDialect and postgresDialect implementations covering all methods: InsertIgnoreInto, ReplaceInto, FromDual, OnDuplicateKey, OnConflictDoNothing, GroupConcat, JsonQuote, JSONAgg, JSONExtract, JSONUnquoteExtract, JSONBuildObject, JSONObjectFunc, FindInSet, FullTextMatch, RegexpMatch, GoquDialect, ReturningID, IsPostgres, CreateTableLike, AtomicTableSwap

Runtime SQL translation (server/platform/postgres/rebind_driver.go)

The pgx-rebind driver wraps pgx/stdlib and rewrites MySQL syntax to PG at the driver layer:

  • Placeholders: ?$N
  • Boolean columns: col = 1/col = 0col = true/col = false (for the ~60 boolean cols listed in schema_bool_cols_gen.go)
  • JOIN syntax: UPDATE … JOINUPDATE … FROM; multi-table DELETE → DELETE … USING
  • JSON functions: JSON_EXTRACT(col, '$.path')col->'path'; JSON_OBJECT(…)jsonb_build_object(…); JSON_QUOTE(…), JSON_UNQUOTE(…), JSON_ARRAYAGG(…) → PG equivalents
  • MySQL functions: IF()CASE WHEN, IFNULLCOALESCE, MD5()md5(), UUID()gen_random_uuid()::text, UTC_TIMESTAMP()TO_CHAR(NOW() AT TIME ZONE 'UTC', …), CURDATE()CURRENT_DATE, DATABASE()current_schema(), HEX()/UNHEX()encode/decode, FIND_IN_SETarray_position(string_to_array), …
  • Type casts: CAST AS UNSIGNEDCAST AS integer, CAST AS SIGNED INTCAST AS integer, TIMESTAMP(?)(?)::timestamp
  • Upsert: INSERT IGNORE INTOINSERT INTO … ON CONFLICT DO NOTHING; REPLACE INTOINSERT … ON CONFLICT DO UPDATE; ON DUPLICATE KEY UPDATE VALUES(col)ON CONFLICT (pk) DO UPDATE SET col = EXCLUDED.col
  • DDL column-type translation: BLOBbytea, MEDIUMTEXT/LONGTEXTTEXT, TINYINT(1)smallint, DATETIME[(N)]timestamp[(N)], INT UNSIGNED NOT NULL AUTO_INCREMENTINTEGER NOT NULL GENERATED BY DEFAULT AS IDENTITY, INT UNSIGNEDINTEGER, inline UNIQUE KEY name (cols)CONSTRAINT name UNIQUE (cols), enum('a','b','c')VARCHAR(255) CHECK (col IN ('a','b','c')), ENGINE=InnoDB/DEFAULT CHARSET=…/ALGORITHM=INSTANT stripped
  • DDL multi-statement output: ALTER TABLE … ADD COLUMN …, ADD KEY <name> (<cols>) produces an ALTER followed by a separate CREATE INDEX. ADD UNIQUE KEYCREATE UNIQUE INDEX. Multiple ADD KEY clauses each become their own CREATE INDEX.
  • ON UPDATE CURRENT_TIMESTAMP / NOW(n): the MySQL column attribute is stripped from CREATE TABLE and a per-table BEFORE UPDATE trigger calling fleet_set_updated_at() is appended. The trigger function is installed by pg_baseline_post.sql.

Migration runner

  • MigrateTables on PG: applies the embedded baseline on fresh DBs, seeds migration_status_tables/migration_status_data ≤ the baseline marker, then runs goose Up so newer upstream migrations execute through the rebind driver.
  • pg_baseline_post.sql: idempotent ownership fixups + fleet_set_updated_at() trigger function; re-applied on every prepare db.
  • Migrations that MySQL expresses as generated-column redefinitions are modeled on PG as plain columns with one-time stored-value recomputes.

pgcompat tooling (tools/pgcompat/)

Validators that gate every push:

  • check_primary_keys — every raw ON DUPLICATE KEY UPDATE site is covered by knownPrimaryKeys, and every entry's columns match a real PK/UNIQUE constraint in the baseline (also run with --include-migrations)
  • check_schema_drift / check_column_driftschema.sql vs pg_baseline_schema.sql table and column sets match (allowlists for intentional drift)
  • check_constraint_drift — PK/UNIQUE/index/FK parity by (table, kind, column-set); the deferred ~160-FK set is allowlisted with rationale
  • check_bool_col_split — no column name typed boolean and smallint in different tables (split names are excluded from all name-keyed rewrites)
  • gen_updated_at_triggers — generates the ON UPDATE CURRENT_TIMESTAMP trigger mirror (138 triggers) applied on every prepare db; CI-staleness-checked like gen_bool_cols / gen_identity_cols
  • gen_bool_cols / gen_identity_cols — regenerate the schema-derived driver tables; CI fails if stale
  • Fresh-PG-install smoke test: empty PG + prepare db (baseline + post-marker migrations), asserted idempotent on second run

Test plan

  • Validate PG Compatibility CI — green on feat/pg-compat-rebased @ dd8ae2b and on aggregated
  • Go Tests (PostgreSQL) CI — green on aggregated
  • Build & Push Ledo Fleet Image CI — green; image sha256:6680b5b9eefd… in registry.hz.ledoweb.com
  • Local: full TestPostgres* suite against real PG 16, including post-baseline migration replay and the new TestPostgresInsertCVEMeta
  • Local: MySQL TestMigrations passes (MySQL paths of ported migrations unchanged)
  • fleet prepare db --mysql_driver=postgres migrates a scratch PG cleanly through all July migrations (used to regenerate the baseline)
  • Prod DB migrated (all 27 post-baseline migrations applied cleanly via fleet prepare db job, 2026-07-27) and rebased image rolled out
  • Post-deploy verification: orbit config errors stopped, hourly cve_meta timeout gone, CVE/EPSS data refreshing (verified 2026-07-27)
  • Review-remediation phases 1–3 deployed to prod with per-phase verification (migrations, backfills, index parity, swap-name canonicalization, device-endpoint walks, zero error logs)
  • Phase 4: Go Tests (PostgreSQL) CI runs the datastore package unfiltered — 121 pass / 59 MySQL-only skips / 0 fail; driver + goose + pgcompat suites green; fresh + idempotent prepare db verified post-rebase onto 54b32ddf88
  • Post-deploy adversarial re-review of the Phase-4 delta (2026-07-29, three parallel reviewers): all findings fixed in-branch — 084359's integer boolean literal (a deploy wedge for PG DBs still below that version), the dedup migration's missed policies.patch_software_title_id and unguarded software_title_team_pins repoints, intra-loser slot collisions in the guarded repoint pattern, a pre-flight guard for the bundle-identifier unique index the backfill newly activates, both-table guards + per-row idempotent variable inserts in the porting wrapper, TranslateError unit coverage, and trigger-formula parity tests for all four generated columns. Prod audited for the missed-table gap: zero orphans (no patch policies exist there)
  • Phase 4 deployed to prod (2026-07-29): the two failed migration attempts each hardened the branch — the below-marker backstop now admits declared porting wrappers (PortedBelowMarker), and the generated-column backfill dedups the 95 duplicate software_titles rows that stale unique_identifier values had let accumulate (rehearsed against prod data in a rolled-back transaction first). Post-deploy: max version 20260729190000, 0 dup groups, 0 orphans, 138 updated_at-trigger tables, crons completing, hosts checking in

@dnplkndll dnplkndll changed the title fix(pg): Windows MDM + remaining MySQL-only SQL → PostgreSQL dialect helpers feat(pg): PostgreSQL datastore compatibility layer May 5, 2026
@dnplkndll
dnplkndll force-pushed the feat/pg-compat-clean branch 3 times, most recently from dbcec59 to 6062962 Compare May 14, 2026 12:57
@dnplkndll
dnplkndll changed the base branch from ledoent to main May 14, 2026 13:20
@dnplkndll
dnplkndll force-pushed the feat/pg-compat-clean branch 2 times, most recently from a3b9ccf to f9dab02 Compare May 23, 2026 15:53
@dnplkndll
dnplkndll force-pushed the feat/pg-compat-clean branch from 10cd4d0 to 1e6ac95 Compare July 27, 2026 12:24
nulmete and others added 5 commits July 29, 2026 08:22
<!-- Add the related story/sub-task/bug number, like Resolves fleetdm#123, or
remove if NA -->
**Related issue:** Resolves fleetdm#49421

Custom host vitals (`$FLEET_HOST_VITAL_<id>`) already worked in scripts
and Apple/Windows configuration profiles, but Android configuration
profiles and managed app configuration explicitly rejected them at
upload to keep parity with `$FLEET_SECRET_*`. This left admins unable to
inject per-host vitals (e.g. an asset tag) into Android MDM
configuration the same way they can for every other platform.

For more context, prior PRs:
- fleetdm#49334
- fleetdm#49586

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [x] Added/updated automated tests

- [x] QA'd all new/changed functionality manually

- Created an "Asset tag" host vital.
- Enrolled an Android device.
- Initially the test profile showed as "Failed" because no value was set
for the vital.
- Set a value for the vital, saw that it went from Enforcing to
Verified.

<img width="1446" height="510" alt="Screenshot 2026-07-24 at 8 57 46 AM"
src="https://github.com/user-attachments/assets/c0e2348c-e521-48f3-85cd-6f884689b2cd"
/>
<img width="1520" height="936" alt="Screenshot 2026-07-24 at 8 56 56 AM"
src="https://github.com/user-attachments/assets/169b9545-ec7a-429b-8f45-0e2740f61c77"
/>
<img width="1607" height="1136" alt="Screenshot 2026-07-24 at 8 57
30 AM"
src="https://github.com/user-attachments/assets/a8213745-b224-4a36-a54d-32152a15c377"
/>

Also tested the rejection cases:
- trying to upload a profile with an invalid custom host vital id
(either a non-numeric value, a numeric but non-existent ID, and
referencing a vital as a JSON key instead of a value)
- deleting a vital referenced in a profile



https://github.com/user-attachments/assets/e8b4acde-ddf4-41c0-b00a-5ab4945d0bc2



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Summary by CodeRabbit

* **New Features**
* Android app configurations and profiles now support custom host vital
placeholders (`$FLEET_HOST_VITAL_<id>`).
* Custom host vital values are expanded per device during Android
delivery.
* Managed Android profiles/configurations are automatically resent when
a referenced vital value changes.

* **Bug Fixes**
* Added validation for malformed, missing, or undefined vital references
during Android app association and profile/config uploads.
  * Prevented deletion of vitals referenced by Android profiles.
* Improved error handling and delivery failure details when a device
lacks a required vital value.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
<!-- Add the related story/sub-task/bug number, like Resolves fleetdm#123, or
remove if NA -->
**Related issue:** Resolves fleetdm#50122

<img width="730" height="148" alt="image"
src="https://github.com/user-attachments/assets/e7a803bd-b50d-404f-8af2-9fa32cc34772"
/>


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information. (Unreleased bug)

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [x] Confirmed that the fix is not expected to adversely impact load
test results


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Account provisioning settings now indicate when the feature is
included with Fleet Premium.
- Premium-tier accounts continue to see the full provisioning
configuration and save controls.

- **Bug Fixes**
- Improved license-tier handling so account provisioning displays the
appropriate experience for free and premium plans.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually

## fleetd/orbit/Fleet Desktop

- [X] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [X] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Security Improvements**
* Updated device-authenticated policy and host-detail responses to omit
policy author identity fields and any raw SQL/query data.
* Device policy endpoints now return a device-safe policy representation
consistently.

* **Bug Fixes**
* Prevented administrative policy information from appearing in
device-authenticated host details and policy listings.

* **Tests**
* Strengthened integration coverage to verify device-safe responses
(required user-facing fields present; sensitive fields absent).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…etdm#50136)

<!-- Add the related story/sub-task/bug number, like Resolves fleetdm#123, or
remove if NA -->
**Related issue:** Resolves fleetdm#40074 unreleased bug

<img width="539" height="141" alt="image"
src="https://github.com/user-attachments/assets/1ac8e2c2-236d-4567-a200-0eb35cce46e7"
/>


# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved configuration profile validation for unescaped special
characters in Apple payloads.
* Error messages now consistently indicate when characters like `&` and
`<` must be XML-escaped.
  * Updated error examples to show properly escaped guidance.
* Expanded test coverage to verify the standardized XML-escaping error
for additional failing scenarios.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
dnplkndll added a commit that referenced this pull request Jul 29, 2026
The PR #6 merge gate: the PG CI job now runs the entire datastore package
unfiltered — 121 top-level tests pass on PG, 59 MySQL-only suites skip
visibly (CreateMySQLDS now gates on MYSQL_TEST like CreateDS), zero failures.

Test-infra parity:
- No builtin-label pre-seeding on PG (MySQL test DBs are schema-only; tests
  create their own labels — the seeds collided on idx_label_unique_name).
- PG TruncateTables clears the in-process software-title cache like the
  MySQL path (stale cache made UpdateHostSoftware skip recreating titles).
- users_test/jobs_test converted to CreateDS; the users timestamp assertion
  gets an explicit 1s ceiling on PG (MySQL's whole-second TIMESTAMP gave the
  same slack implicitly).

Cross-dialect datastore fixes:
- MySQL-only 'DELETE alias FROM ... JOIN' → keyed subqueries (apple device
  names ×2, software title display names).
- Raw REGEXP → dialect.RegexpMatch with dialect-correct word boundary
  (MySQL \b, PG \y) in the device-name secret scan.
- Windows profile upsert bound command_uuid as an arg instead of a MySQL-only
  bare column reference in VALUES.
- Jobs: not_before defaults to an app-clock whole second and unpinned queue
  reads compare against DB NOW() — immune to container clock skew and
  NOW()-precision differences (MySQL rounds, PG keeps micros).
- query_results cleanup used LIMIT inside IN (MySQL error 1235 — a fork
  conversion bug on the MySQL path); wrapped in a derived table.

Driver:
- SUBSTRING_INDEX(x,d,1) → split_part; JSON_TYPE(x) → upper(jsonb_typeof(x))
  with balanced-paren scan; unique violations gain MySQL-style
  'table.constraint' phrasing via TranslateError (message-assertion parity).
- default_query_exec_mode=describe_exec: pgx's statement cache breaks under
  live DDL ('cached plan must not change result type') — our deploy shape.

Migration 20260729120000 ports the four remaining orphaned generated columns
(windows profile checksum, apple declaration token, software_titles
additional_identifier, calendar_events uuid) as triggers + backfill; baseline
regenerated, marker 20260729120000. operating_systems_test's raw ODKU became
dialect-neutral insert-if-missing.

MySQL parity verified on every touched suite; both dialects green.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
jkatz01 and others added 14 commits July 29, 2026 10:00
<!-- Add the related story/sub-task/bug number, like Resolves fleetdm#123, or
remove if NA -->
**Related issue:** Resolves fleetdm#49811 

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed Fleet-maintained app updates when a rebuilt installer keeps the
same version.
* Rebuilt installers now update their files, hashes, filenames, and
install scripts correctly.
* Prevented installers from being incorrectly skipped when their
contents differ despite matching versions.
* **Tests**
* Added coverage for same-version installer rebuilds and team-specific
caching behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
The gitops-auto-complete uses `replace github.com/fleetdm/fleet/v4 =>
../..` which means it is frequently broken whenever fleet updates shared
libraries.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Added automated build and dependency verification for the GitOps
auto-complete tool.
* Updated workflow triggers so changes to the tool are checked
automatically.
  * Refreshed supporting service dependencies used by the tool.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Automated ingestion of latest Fleet-maintained app data.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added support for the latest versions of AnyBurn, BetterZip, Canva,
Eclipse Temurin, ExifCleaner, Gemini, LibreOffice, NordVPN, Postman,
TablePlus, Tailscale, and other maintained applications across Windows
and macOS.
* **Bug Fixes**
* Improved macOS uninstall cleanup for numerous applications by removing
additional caches, preferences, recent-document entries, containers,
support files, and related data.
* Updated installer downloads and verification checks to match the
latest releases.


<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: allenhouchins <32207388+allenhouchins@users.noreply.github.com>
Co-authored-by: Allen Houchins <allenhouchins@mac.com>
Fixed spelling on LinkedIn posting.

<!-- Add the related story/sub-task/bug number, like Resolves fleetdm#123, or
remove if NA -->
**Related issue:** Resolves #

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [ ] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

- [ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [ ] Timeouts are implemented and retries are limited to avoid infinite
loops
- [ ] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [ ] Added/updated automated tests
- [ ] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [ ] QA'd all new/changed functionality manually

For unreleased bug fixes in a release candidate, one of:

- [ ] Confirmed that the fix is not expected to adversely impact load
test results
- [ ] Alerted the release DRI if additional load testing is needed

## Database migrations

- [ ] Checked schema for all modified table for columns that will
auto-update timestamps during migration.
- [ ] Confirmed that updating the timestamps is acceptable, and will not
cause unwanted side effects.
- [ ] Ensured the correct collation is explicitly set for character
columns (`COLLATE utf8mb4_unicode_ci`).

## New Fleet configuration settings

- [ ] Setting(s) is/are explicitly excluded from GitOps

If you didn't check the box above, follow this checklist for
GitOps-enabled settings:

- [ ] Verified that the setting is exported via `fleetctl
generate-gitops`
- [ ] Verified the setting is documented in a separate PR to [the GitOps
documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485)
- [ ] Verified that the setting is cleared on the server if it is not
supplied in a YAML file (or that it is documented as being optional)
- [ ] Verified that any relevant UI is disabled when GitOps mode is
enabled

## fleetd/orbit/Fleet Desktop

- [ ] Verified compatibility with the latest released version of Fleet
(see [Must
rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md))
- [ ] If the change applies to only one platform, confirmed that
`runtime.GOOS` is used as needed to isolate changes
- [ ] Verified that fleetd runs on macOS, Linux and Windows
- [ ] Verified auto-update works from the released version of component
to the new version (see [tools/tuf/test](../tools/tuf/test/README.md))
Adds a Postgres backend to Fleet's datastore alongside the existing
MySQL. Non-breaking: MySQL remains the default and is unaffected.

Core pieces:
  - DialectHelper interface (server/datastore/mysql/dialect.go) abstracts
    SQL dialect differences for upserts, aggregates, JSON ops, error
    classification, and atomic swap-table DDL. mysqlDialect + postgresDialect
    implementations, dialect.IsPostgres() routes runtime branches.
  - pgx-rebind driver (server/platform/postgres/rebind_driver.go)
    transparently translates MySQL SQL to Postgres at query execution time
    via 50+ regex-based rewrites compiled once at startup. Per-table-name
    regexes cached in sync.Map. knownPrimaryKeys map drives ON DUPLICATE
    KEY → ON CONFLICT (<pk>) DO UPDATE rewriting.
  - Embedded PG baseline (server/datastore/mysql/pg_baseline_schema.sql,
    pg_baseline_post.sql) seeded from production pg_dump. Carries a
    pg-baseline-up-to-migration: <ts> marker; fresh-apply seeds
    migration_status_tables from code and logs a loud warning whenever
    code carries migrations newer than the baseline. Object-ownership is
    reasserted on every startup so atomic table swaps work even when the
    baseline was loaded as the postgres superuser.
  - server/goose/migration.go gains UpFnPG / DownFnPG / UpFnMySQL /
    DownFnMySQL fields so individual migrations can target one dialect.
    First user: 20260513210000_AddMissingPGIndexes (this commit).
  - 349 missing PG indexes added via the AddMissingPGIndexes migration
    (UpFnPG-only), bringing PG to index parity with MySQL on hot paths
    like host_software_installed_paths (host_id, software_id).

Wiring:
  - FLEET_MYSQL_DRIVER=postgres selects the new driver; standard
    FLEET_MYSQL_ADDRESS / USERNAME / PASSWORD / DATABASE env vars route to
    the PG cluster unchanged.
  - server/config/config.go validates the new driver value.
  - cmd/fleet/prepare.go threads dialect into the migration apply path.
  - docker-compose.yml gains a postgres service for local dev.

Tests:
  - 39 PG smoke tests (hosts, software, vulnerabilities, policies,
    host-counts) and B1/B2/B3 tiers running on both backends via the new
    CreateDS(t) helper.
  - Driver-rewrite unit tests cover every regex (UPDATE...JOIN,
    DELETE USING, GROUP_CONCAT, ON CONFLICT ambiguity resolution,
    smallint-bool encoding, MAX(bool), INTERVAL placeholder, CAST NULL
    AS SIGNED, FIND_IN_SET, COALESCE token, null-byte stripping, ...).
  - Dialect unit tests for both dialects (LAST_INSERT_ID stripping,
    ReturningID, AtomicTableSwap, CreateTableLike).
  - List-options helper has new coverage for single-aggregate ORDER BY
    skip and text-column cursor binding.
  - Benchmarks for UpdateHostSoftware / ListSoftware / ListHosts in
    server/datastore/mysql/benchmarks_test.go.

Squashed from 70+ incremental commits on feat/pg-compat-clean; full
provenance preserved on feat/pg-compat-clean-backup-2026-05-13.
…p on dep-review

CI infrastructure that gates the PG backend:
  - test-go-postgres.yaml: spins up Postgres in a service container, runs
    the full datastore + service test suites against the PG driver. Mirrors
    the existing MySQL test workflow.
  - validate-pg-compat.yml: invokes the tools/pgcompat validators on every
    PR/push — check_primary_keys, check_schema_drift, check_column_drift.
    Empty-allowlist gate-of-the-gate test ensures the validators themselves
    can never become a no-op.
  - build-ledo.yml: ledoent-specific image build that refuses to publish to
    ghcr.io unless both test-go-postgres and validate-pg-compat succeeded
    on the build SHA.
  - sync-upstream.yml: paranoia check that refuses to force-push ledoent/main
    if any non-bot commits exist outside upstream/main.
  - weekly-aggregate.yml: gitaggregate cron + workflow_dispatch, pinned to
    git-aggregator==4.1.
  - dependency-review.yml: skip on private repos (the action requires
    GitHub Advanced Security which isn't available on free private mirrors).
    Upstream public fleetdm/fleet still runs it.
  - test-website.yml: npm audit step added so frontend dep regressions
    block PRs.
  - tools/ci/apiparamcheck: custom golangci-lint plugin that flags REST
    handler params not registered in the request struct, catching the
    'missing query param decode' class of bug.
…rift

Three small static-analysis tools that prevent silent PG-compat regressions.
None require a running Postgres; they read Go source and SQL schema files.

  - check_primary_keys: scans non-test Go for raw 'ON DUPLICATE KEY UPDATE'
    SQL and verifies every targeted table has an entry in knownPrimaryKeys
    (the map in server/platform/postgres/rebind_driver.go that drives the
    ON CONFLICT (<pk>) DO UPDATE rewrite). Missing entries produce invalid
    PG SQL at runtime.
  - check_schema_drift: diffs CREATE TABLE identifier sets between
    server/datastore/mysql/schema.sql (MySQL canonical) and
    pg_baseline_schema.sql (PG baseline). known_schema_diff.txt records
    intentional divergence and is itself validated — stale entries fail.
  - check_column_drift: diffs column lists per shared table. Optional
    allowlist via known_column_drift.txt.
  - gen_identity_cols / gen_bool_cols: code generators that produce the
    Postgres dialect's static knowledge of IDENTITY columns and bool
    columns so the rebind driver can rewrite INSERTs correctly.
  - validators_test.go is a gate-of-the-gate: an empty schema-diff
    allowlist must produce a non-zero exit.

Designed to be extractable as a standalone PR to fleetdm/fleet — they're
useful to any Fleet operator building PG support, with or without the
larger driver/baseline layer.
Playwright API-mode test matrix that exercises every URL filter Fleet's
frontend can construct against a live server, asserting each response is
not a Postgres-driver or Postgres-syntax failure (SQLSTATE, 'must appear
in the GROUP BY', 'operator does not exist', 'cannot find encode plan',
'syntax error', etc.).

Read-only (HTTP GET only). ~220 probes in ~15s with 8 workers.

Coverage:
  - /hosts + /hosts/count: status, low_disk_space, mdm_enrollment_status,
    os_settings/apple_settings/disk_encryption/bootstrap_package, populate_*,
    every ORDER BY allowlist key × direction, cursor pagination (after=),
    vulnerability filter, search.
  - /software/versions, /software/titles, /software (deprecated):
    vulnerable, exploit, cvss range, self_service, available_for_install,
    packages_only, team filtering, ordering.
  - /vulnerabilities, /host_summary, /labels/:id/hosts, /hosts/:id/*,
    sanity endpoints (/config, /version, /me, /labels, /teams, ...).

Run:
  cd tools/pg-compat-harness
  yarn install
  export FLEET_URL=https://your-fleet
  export FLEET_TOKEN=$(awk '/token:/ {print $2}' ~/.fleet/config)
  yarn test

This harness found and gated the GROUP BY and cursor-encoding regressions
fixed elsewhere in this branch (selectSoftwareSQL GroupByAppend,
AppendListOptionsWithParamsSecure textOrderKeys hint).
Small Go program that parses server/datastore/mysql/schema.sql and emits
one CREATE INDEX IF NOT EXISTS statement per MySQL KEY / UNIQUE KEY clause,
suitable for embedding into a PG-only migration.

Handles:
  - balanced parens in column lists (expression bodies)
  - USING BTREE / USING HASH suffix (MySQL hint, PG ignores)
  - DESC column ordering (PG supports natively)
  - identifier quoting where required
  - stable per-table grouping for reviewable diffs

Deliberately skips with explicit reasons:
  - PRIMARY KEY (the CREATE TABLE handles it)
  - FULLTEXT KEY, SPATIAL KEY (need pg_trgm / GiST equivalents)
  - prefix-length indexes col(N) (need PG expression indexes)
  - expression indexes using MySQL-specific functions (ifnull, cast as
    signed) that need PG translation (COALESCE, CAST AS integer)

main_test.go drives translate() from inline schema fixtures — no file I/O
required. Covers plain/unique keys, DESC, USING BTREE, every skip reason,
balanced-paren edge cases, multi-table, PRIMARY ignored, plus unit tests
for extractParenBody and quoteIdent helpers.

Usage:
  go run ./tools/pg-index-translate \
    -in  server/datastore/mysql/schema.sql \
    -out server/datastore/mysql/migrations/tables/{ts}_AddMissingPGIndexes.sql
  - docs/Deploy/postgresql.md: end-to-end guide for running Fleet against
    Postgres — connection env vars, baseline schema apply, migration
    apply, ownership reassertion, troubleshooting (drift warning, must
    be owner of table, schema/column drift validator output).
  - docs/Deploy/README.md: links the new guide from the deployment index
    alongside the MySQL guide.
GetDBVersion returned a too-old current version on production PG because
the baseline-seed path (and goose's own run-and-record loop for newly
introduced migrations) inserted rows into migration_status_tables out of
version_id order. Concretely, id 523 carried version 20260422181702 while
id 521 carried 20260506171058. Plain 'ORDER BY id DESC' picked the
older version, so 'fleet prepare db' tried to re-run every migration
from 20260423161823 onward and failed on json_merge_patch — a MySQL-only
function that PG never had, with the migration body long since folded
into the embedded baseline.

Switching to 'ORDER BY version_id DESC, id DESC' makes the query
immune to insertion order while preserving up/down semantics: the
tie-break by id DESC keeps the most recent applied/rolled-back state
for the same version. MySQL is unaffected — its migration runner
always applies in monotonic version order so id and version_id stay
aligned. We do not change the MySQL dialect to keep blast radius
minimal; that path has years of behavior to preserve.

Test pins the exact ORDER BY clause via sqlmock so any future change
back to the buggy form fails CI loudly.
…ces/views

pg_baseline_post.sql already loops over public tables, sequences, and
views and reasserts ownership to current_user, but it skipped functions.
On baselines that were loaded by the postgres superuser (typical on
self-hosted PG), CREATE OR REPLACE FUNCTION later in the same file
errored with 'must be owner of function fleet_set_updated_at' — the
application user can't replace something it doesn't own.

Add a fourth loop using pg_proc / pg_namespace to enumerate public
functions whose owner is not current_user, and ALTER FUNCTION ... OWNER
TO current_user with the standard insufficient_privilege fallback.
pg_get_function_identity_arguments() disambiguates overloaded
signatures.

Hit in production tonight on the AddMissingPGIndexes deploy. With this
fix every future fleet prepare db on a postgres-superuser-loaded
baseline succeeds without manual ALTER FUNCTION.
The existing implementation already sorts the seeded versions ascending
(via versionsAtOrBelow → partitionMigrationVersions → slices.Sort), so
PG assigns auto-increment ids in the same order as version_id. That
property is load-bearing for any downstream consumer that infers
'current version' from MAX(id), even with the dialect query now
correctly ordered by version_id DESC.

No functional change — just document the invariant so a future refactor
doesn't quietly drop the sort.
dnplkndll added 20 commits July 29, 2026 11:21
Review findings 17, 18, 22 + scanner nits:

- UPDATE/DELETE ... LIMIT is no longer silently stripped: the driver rejects
  it at Exec/Query/Prepare with a descriptive error. The three runtime sites
  are rewritten cross-dialect (in_house_apps keyed-subquery batch; statistics
  and nanomdm bstoken drop redundant LIMIT 1).
- The innodb/sql_mode SELECT-1 replacement is anchored on leading SET/SHOW;
  DBLocks (pg_blocking_pids) and InnoDBStatus (n/a placeholder) get explicit
  PG implementations instead of relying on query swallowing.
- Bool-literal rewrite is single-pass and identifier-anchored: suffix
  collisions (num_canceled vs canceled, *_encrypted vs encrypted) can no
  longer mis-rewrite. First tests for it and rewriteOnDuplicateKey.
- rewriteOnDuplicateKey's unknown-table fallback emits a self-describing
  conflict target naming the missing knownPrimaryKeys entry instead of
  invalid SQL with an opaque parse error.
- The ?→$N scanner skips single-quoted literals ('' escapes) and /* */
  block comments, so a literal question mark can't shift later ordinals.
- PrepareContext applies the RETURNING rewrite via a wrapping statement whose
  Exec routes through Query — prepared identity-table inserts no longer
  silently return LastInsertId()==0.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Review findings 15, 16:

- seedPGMigrationHistory runs unconditionally (its empty-table guard keeps it
  idempotent): a psql-loaded baseline no longer causes goose to replay every
  migration against a populated schema.
- New checkPGBelowMarkerDrift rebase backstop: a table migration numbered
  below the baseline marker with no applied record (upstream authored-early /
  merged-late) fails prepare with remediation guidance instead of being
  silently skipped forever. Covered by TestPostgresBelowMarkerDriftCheck.
- MigrateData is no longer an unconditional no-op on PG: seeded history means
  goose only runs post-marker data migrations, and MigrationStatus can no
  longer wedge serve on the first new data migration.
- pg_baseline_post.sql: the nano_view_queue drop+recreate runs inside one DO
  block and the software_titles trigger uses CREATE OR REPLACE TRIGGER — no
  observable window where either object is missing while old pods serve.
  Header no longer claims it runs on startup.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Review findings 12b/c, 19, 20:

- check_primary_keys now verifies every knownPrimaryKeys entry's columns
  against a real PK/UNIQUE constraint in the baseline (catches wrong-column
  entries as CI failures naming SQLSTATE 42P10, the class Phase 2 fixed by
  hand); attribution of ODKU→INSERT is bounded to ~120 source lines instead
  of an 8KB byte window across function boundaries; ee/ and cmd/ are scanned.
- gen_identity_cols reads schema.sql (regenerated by upstream on every
  migration) instead of the PG baseline. The switch immediately surfaced a
  real gap: host_scd_data (sequence-default id, not IDENTITY syntax) was
  missing from the map, so its PG inserts returned LastInsertId()==0.
- New check_bool_col_split validator fails when a column name is boolean in
  one table and smallint in another (the name-keyed-rewrite hazard class;
  acme_*.revoked was the live instance). awaiting_configuration's
  intentional tri-state split is allowlisted with rationale.
- All three wired into CI and make check-pg-compat with negative tests.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
… leftovers

Review finding 21 + activities audit:

- common_mysql.IsReadOnlyError recognizes PostgreSQL's SQLSTATE 25006
  (matched structurally via SQLState(), no pgx dependency), so every
  fail-fast site — sessions, withRetryTxx, common tx helpers — now trips on
  a PG primary→replica failover exactly as on Aurora demotion.
- Prod audit: activities/host_activities (pre-rename generation the
  MySQL-only RENAME TABLE never converted on PG) are empty; activity_past
  holds all history. Migration 20260727210000 drops them with an
  emptiness guard that fails loudly if any deployment has stranded rows.
  known_schema_diff.txt comments corrected (they were mislabeled
  'no MySQL equivalent').

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Review nits:

- FullTextMatch concatenates multi-column input into one tsvector instead of
  silently dropping cols[1:].
- docs/Deploy/postgresql.md: knownBooleanColumns → generated
  schema_bool_cols_gen.go reality; prepared-statement LastInsertId rows
  marked resolved (fixed in 3.1); stale claims corrected.
- Dead code removed: goose AddDualDialectMigration (20260513210000 sets
  UpFnPG directly), unreachable pgx branch in tables.SetDialect;
  indexExists documented as MySQL-test-only (migration code must use the
  dialect-aware indexExistsTx).
- validate-pg-compat skip ledger counts isPG(ds) sites — the TODO-string grep
  matched nothing and reported Total: 0 forever.
- pg-compat-harness playwright config requires FLEET_URL (bare yarn test no
  longer targets prod).
- docker-compose dev postgres uses the same glibc postgres:16 image as
  postgres_test (musl collation differs; collation_test.go exists) and binds
  to 127.0.0.1 like every other service.
- CLAUDE.md: migration batching convention (finding 25's residue).

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Baseline drops the pre-rename activities/host_activities tables; schema-diff
allowlist pruned accordingly. All six validators green; fresh + idempotent
prepare db verified on scratch.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…marker

The first Phase-3 prod deploy aborted on the backstop's own false positive:
right after a baseline regen the marker IS the newest (not-yet-applied)
migration, which goose is about to run — that is the normal pending state,
not drift. Unreachable migrations are exactly those below the database's max
applied version (goose only runs versions above it). Regression-covered with
the exact prod scenario in TestPostgresBelowMarkerDriftCheck.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…ed_at triggers

Adversarial re-review (2026-07-28) found 3 must-fix issues; all fixed:

M1 — UpdateVulnerabilityHostCounts failed every hourly run since the swap
change: its old-table cleanup was the one bare DROP TABLE among the four
swap callers (the PG swap now drops the table itself). IF EXISTS like its
siblings; confirmed live in prod cron_stats before fixing. New
TestPostgresHostCountCrons runs all four swap crons end-to-end, twice.

M2 — Windows ESP state machine: mdm_windows_enrollments.awaiting_configuration
is a tri-state uint but sat in smallintBoolColumns, whose bool-CASE rewrite
collapsed Active=2 to 0. Split-typed names are now excluded from ALL generic
bool machinery: removed from smallintBoolColumns, gen_bool_cols skips names
in known_bool_col_splits.txt (regenerated), both sides map natively. Driver
unit test asserts pure passthrough; TestPostgresWindowsESPStateMachine walks
the real 0→1→2→0 transitions.

M3 — updated_at was frozen at insert time on ~125 tables: MySQL's ON UPDATE
CURRENT_TIMESTAMP had a PG trigger on only the 12 driver-created tables.
New gen_updated_at_triggers generates the full trigger set from schema.sql
(138 triggers/137 tables incl. the three non-updated_at auto-touch columns
via a generic fleet_touch_column), embedded and applied on every prepare db
(CREATE OR REPLACE, converges driver-created names). fleet_set_updated_at
upgraded to exact MySQL semantics: touch only when the row changed and the
statement didn't assign updated_at itself. CI staleness check added.
TestPostgresUpdatedAtTriggers covers touch/no-op/explicit-wins plus a
coverage-breadth assertion (>=130 tables).

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Its host_disks scenario is covered by TestPostgresUpdatedAtTriggers.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…opy frozen

DRY pass from the branch quality review:

- tools/pgcompat/internal/allowlist: single implementation of the tagged
  (mysql-only:/pg-only:) and plain-lines allowlist formats, replacing three
  near-identical copies across check_column_drift, check_constraint_drift,
  check_bool_col_split and the inline loop in gen_bool_cols.
- 20260727170400's DO block is applied history — replace the stale
  'keep in sync' instruction with a frozen-snapshot note (the dialect
  re-canonicalizes on every swap cycle, so drift is harmless by design).

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…umn coverage

Coverage pass from the branch quality review:

- newPGTestHost collapses five identical fleet.Host literals in the PG smoke
  tests (the two tests that deliberately exercise raw NewHost keep theirs).
- TestMySQLUpsertDidUpdateParity: the promised MySQL twin of
  TestPostgresUpsertDidUpdate — OnDuplicateKeyGuarded must yield identical
  did-update semantics on both dialects (insert/changed → uploaded_at moves,
  identical re-upsert → preserved).
- TestPostgresUpdatedAtTriggers now also covers the fleet_touch_column path
  via sessions.accessed_at, one of the three non-updated_at auto-touch
  columns.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
tools/pgcompat/README.md documents all five validators, the three
CI-staleness-checked generators, and the shared internal/allowlist package;
docs/Deploy/postgresql.md's CI-gates section lists the full current step
sequence including constraint-drift, bool-split, and the updated_at trigger
generator.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
The PR #6 merge gate: the PG CI job now runs the entire datastore package
unfiltered — 121 top-level tests pass on PG, 59 MySQL-only suites skip
visibly (CreateMySQLDS now gates on MYSQL_TEST like CreateDS), zero failures.

Test-infra parity:
- No builtin-label pre-seeding on PG (MySQL test DBs are schema-only; tests
  create their own labels — the seeds collided on idx_label_unique_name).
- PG TruncateTables clears the in-process software-title cache like the
  MySQL path (stale cache made UpdateHostSoftware skip recreating titles).
- users_test/jobs_test converted to CreateDS; the users timestamp assertion
  gets an explicit 1s ceiling on PG (MySQL's whole-second TIMESTAMP gave the
  same slack implicitly).

Cross-dialect datastore fixes:
- MySQL-only 'DELETE alias FROM ... JOIN' → keyed subqueries (apple device
  names ×2, software title display names).
- Raw REGEXP → dialect.RegexpMatch with dialect-correct word boundary
  (MySQL \b, PG \y) in the device-name secret scan.
- Windows profile upsert bound command_uuid as an arg instead of a MySQL-only
  bare column reference in VALUES.
- Jobs: not_before defaults to an app-clock whole second and unpinned queue
  reads compare against DB NOW() — immune to container clock skew and
  NOW()-precision differences (MySQL rounds, PG keeps micros).
- query_results cleanup used LIMIT inside IN (MySQL error 1235 — a fork
  conversion bug on the MySQL path); wrapped in a derived table.

Driver:
- SUBSTRING_INDEX(x,d,1) → split_part; JSON_TYPE(x) → upper(jsonb_typeof(x))
  with balanced-paren scan; unique violations gain MySQL-style
  'table.constraint' phrasing via TranslateError (message-assertion parity).
- default_query_exec_mode=describe_exec: pgx's statement cache breaks under
  live DDL ('cached plan must not change result type') — our deploy shape.

Migration 20260729120000 ports the four remaining orphaned generated columns
(windows profile checksum, apple declaration token, software_titles
additional_identifier, calendar_events uuid) as triggers + backfill; baseline
regenerated, marker 20260729120000. operating_systems_test's raw ODKU became
dialect-neutral insert-if-missing.

MySQL parity verified on every touched suite; both dialects green.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…ace, migration-test gating

The first unfiltered CI run (correctly) caught what my local environment
masked:

- EnqueueDeviceLockCommand's FOR UPDATE guard relied on InnoDB gap locking
  when no host_mdm_actions row exists yet — PG has no gap locks, so all 20
  concurrent requests in the race test passed the existence check and 4
  commands landed. A transaction-scoped pg_advisory_xact_lock keyed on
  host_id serializes PG; MySQL keeps its gap-lock behavior. This is the
  first concrete instance of the review's finding-8 gap-lock class; the
  sibling sites (apple_psso, host_certificate_templates) remain under that
  finding's accepted-risk disposition pending the same treatment.
- migrations/tables tests and migrations_test.go dialed MySQL
  unconditionally (my MYSQL_TEST shell export hid it locally); both now
  gate with a visible skip, and the recursive ./server/datastore/mysql/...
  CI target is clean under a PG-only environment.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
newDSWithConfig and createTestDatabase (mysql_test.go) open MySQL directly;
four TLS/password-path tests failed on dial in the PG CI job while passing
locally against the dev container. Verified by running the full recursive
suite with the local MySQL container STOPPED — the exact CI shape — which
now passes clean.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…ations

Rebase was conflict-free. Upstream added two migrations timestamped BELOW
this fork's baseline marker — the exact scenario the below-marker backstop
exists for (it refuses deploys until their DDL lands). Migration
20260729190000 ports both on PG: re-runs 20260727083533's DDL through the
rebind driver (with a post-condition assert so a silent no-op can never
record as applied), and inlines 20260727084359's fleet-variable insert with
a boolean literal (the original's integer 0 for is_prefix is a 42804 on PG).

The generated updated_at trigger set now installs AFTER goose Up (in both
prepare and the test harness): it references tables that post-marker
migrations create.

Baseline regenerated through the port (marker 20260729190000, 223 tables);
gen files regenerated (128 identity tables, 140 touch triggers); all five
validators green; full unfiltered PG suite, driver, goose, migrations-on-
MySQL, and fresh+idempotent prepare all pass.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
@dnplkndll
dnplkndll force-pushed the feat/pg-compat-clean branch from aa253da to 3faeee0 Compare July 29, 2026 15:39
The fork's root go.mod adds pgx; the new upstream tool module's go.sum
needs the matching hashes (CI 'Test tools' gate).

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
The first Phase-4 prod migration was refused by checkPGBelowMarkerDrift:
prod's history has no rows for the two back-dated upstream migrations, and
the check runs before the goose Up that would execute their porting
wrapper. Local runs never hit this — fresh DBs seed history through the
marker.

PortedBelowMarker (declared next to the wrapper) maps each ported version
to its wrapper; the check now exempts a version whose wrapper is either
already applied or registered above the DB max (so the imminent goose Up
runs it). Anything else below the max still fails. The wrapper also records
the ported versions in goose history so steady state needs no exemption.

New TestPostgresPortBackdatedWrapper executes the wrapper's PG path against
a prod-shaped DB (DDL absent, history rows missing) — without it that path
would first run in production — plus exemption cases in the drift test.
Verified end-to-end: a DB reset to prod's exact pre-port state now runs
prepare db to completion (tables, vars, and history all land).

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Prod's second Phase-4 migration attempt failed differently: the
software_titles backfill touch fires the baseline-post unique_identifier
trigger, and recomputing stale stored values (the formula gained
application_id/upgrade_code terms after those rows were written) collided
on idx_unique_sw_titles. 95 duplicate titles had accumulated on prod that
MySQL's generated column + unique keys make impossible.

Up_20260729120000 now merges each duplicate group (grouped by the
trigger's current formula) into its lowest id before the touch: per-title
aggregates are deleted (crons rebuild), the 14 referencing tables are
repointed — deleting the loser's row first where the target's unique key
includes the title id — and losers removed. Verified against prod data in
a rolled-back transaction: 95 losers merged, 102 software rows repointed,
full recompute collision-free, zero orphans.

TestPostgresGeneratedColumnDedup fabricates the prod shape (stale
unique_identifier inserted with triggers disabled, references attached)
and executes the migration's PG path directly.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
…eview

Review of the Phase-4 delta (three parallel reviewers) found real gaps in
the two new migrations; all are closed here and the dedup coverage now
exercises every guarded branch.

- 20260727084359: is_prefix literal 0 -> false (valid on both dialects).
  On a PG database still below that version (older baseline markers) the
  ORIGINAL runs — the wrapper only covers databases already past it — and
  the integer literal is a 42804 deploy wedge there.
- Dedup migration: policies.patch_software_title_id was missed entirely
  (the reference scan required the column to be named exactly title_id /
  software_title_id) — a merged group would have orphaned patch policies
  silently, since PG carries no FK. software_title_team_pins repointed
  with no guard against its (team_id, title_id) PK. Both now use the
  guarded delete-before-repoint. The guard itself also handled only
  keeper-vs-loser collisions; two losers of one group landing on the same
  slot collided with each other. All five guarded tables now rank slot
  occupants (keeper's row first, then lowest loser id) and delete the
  rest. Prod audited: zero patch policies, zero orphans — the misses did
  no damage on the one database that already ran this migration.
- The backfill touch activates idx_software_titles_bundle_identifier for
  the first time (NULLs kept it vacuous), which the dedup key does not
  imply uniqueness under; a pre-flight DO block now fails with the
  colliding bundle list instead of an opaque 23505.
- Wrapper: guards and post-checks BOTH ported tables (a partial manual
  restore could previously record the port done with one table missing
  forever), and the fleet-variable insert is per-row idempotent instead
  of keyed on one variable's presence.
- checkPGBelowMarkerDrift: dropped the tautological registered-map lookup
  (PortedBelowMarker lives in the wrapper's own file).
- Tests: dedup test now covers keeper-collision, intra-loser collision,
  team-pin drop, and patch-policy repoint; stale test comments corrected.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
TranslateError had no direct tests. Table-driven coverage for the wrap
(message phrasing, Unwrap, SQLState passthrough and fallback) and the
pass-through paths; the wrap now also requires a non-empty ConstraintName
so it can never emit a malformed "table." key.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
Five copy-pasted env gates collapse into skipUnlessMySQLTest; the two new
PG tests share pgMustExec. GetFilteredQueuedJobs' comment claimed
not_before is written with NOW() — NewJob writes the truncated app clock;
the comment now states the real invariant and its skew window.

Claude-Session: https://claude.ai/code/session_01NrWVk8oMToieU6y3yDtYpY
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.

10 participants