feat(datastore): PostgreSQL compatibility layer#4
Closed
dnplkndll wants to merge 10 commits into
Closed
Conversation
This was referenced Apr 17, 2026
eab027d to
6f33aa0
Compare
c2a8b56 to
6116ebf
Compare
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.
Required by TestVersionsAbove_EmbeddedBaselineCoversAllCode now that
AddMissingPGIndexes (20260513210000) ships in code. Dump source is
fleet.hz.ledoweb.com fleet-db-1, which has all 532 indexes applied
(11 from the original baseline + 521 added by AddMissingPGIndexes
either via the SQL we ran manually tonight or via the migration on
future fresh applies). check-pg-compat validators pass:
schema-drift: 202 MySQL tables / 205 PG tables in sync (after allowlist)
primary-keys: every ON DUPLICATE KEY UPDATE site covered
column-drift: no drift between schema.sql and pg_baseline_schema.sql
Generated via the documented procedure in the file's header:
kubectl exec -n fleet --context hetzner-ledo fleet-db-1 -- \
pg_dump -U postgres -d fleet --schema-only --no-owner --no-privileges
Stripped the pg_dump-17 \restrict/\unrestrict meta-commands and the
SET search_path='' line per the same header comment. Header preserved
with the regen recipe and verification commands.
dbcec59 to
6062962
Compare
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a PostgreSQL compatibility layer to Fleet's datastore, enabling Fleet to run against PostgreSQL in addition to MySQL. Non-breaking — MySQL remains the default and is unaffected.
Upstream context: fleetdm/fleet#34025 — PostgreSQL support requested alongside MySQL. fleetdm/fleet#30286 — customer request due to MySQL 8.4 issues.
⬆️ Extractable upstream contribution:
tools/pgcompattools/pgcompatcontains two small Go programs that prevent silent PG-compat regressions. They have no build-tag constraints and no PG runtime dependency — they statically analyse Go source and SQL schema files. Any Fleet operator building PG support (or the upstream project, if PG support is ever officially scoped) would benefit from them.check_primary_keysScans non-test Go source for raw
ON DUPLICATE KEY UPDATESQL and verifies that every targeted table has an entry inknownPrimaryKeys(inserver/platform/postgres/rebind_driver.go). The rebind driver uses that map to emitON CONFLICT (<pk>) DO UPDATE SET …; a missing entry produces invalid PG SQL at runtime.check_schema_driftDiffs the
CREATE TABLEidentifier sets betweenserver/datastore/mysql/schema.sql(MySQL canonical) andserver/datastore/mysql/pg_baseline_schema.sql(PG baseline dump). Intentional drift — PG-specific tables, MySQL-only legacy tables — is recorded intools/pgcompat/known_schema_diff.txt; stale allowlist entries also fail so the file stays honest.CI gate
Both validators run in
.github/workflows/validate-pg-compat.ymlon every PR and push tomain/aggregated.validators_test.gois a gate-of-the-gate: an empty schema-diff allowlist must produce a non-zero exit.What changes for upstreaming
knownPrimaryKeyswould move fromrebind_driver.go(fork-only) to a standalonetools/pgcompat/primary_keys.gofile, with an upstream-appropriate starting set.pg_baseline_schema.sqlreference incheck_schema_driftwould become an optional flag; the tool still runs (and passes trivially) when the file is absent, so it can ship to upstream without requiring the full PG layer.validate-pg-compat.ymltrigger list would be adjusted tomain/patch-*/prepare-*(standard Fleet branch patterns).⬆️ Extractable upstream contribution:
tools/pg-compat-harnessA Playwright-API-mode regression 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, etc.).Read-only (HTTP
GETonly) — safe against prod. Runs ~220 probes in ~15s with 8 workers. Coverage:/hosts+/hosts/count: every status, low_disk_space, mdm_enrollment_status, os_settings/apple_settings/disk_encryption/bootstrap_package, populate_*, everyorder_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: cvss range, exploit, ordering, search./host_summary: every platform, low_disk_space, team./labels/:id/hosts,/hosts/:id/*(software/policies/activities/encryption_key)./config,/version,/labels,/teams,/me,/queries,/policies,/activities.This is the harness that found and gated the two driver bugs and the index-parity gap fixed in this PR — see the Driver rewrites table and the AddMissingPGIndexes migration below. It belongs upstream as an integration-test layer that complements
tools/pgcompat/'s static analysis.What changes for upstreaming
⬆️ Extractable upstream contribution:
tools/pg-index-translateA small Go program that parses
server/datastore/mysql/schema.sqland emitsCREATE INDEX IF NOT EXISTSstatements for every MySQLKEY/UNIQUE KEYclause, suitable for embedding into a PG-only migration. Handles balanced parens (expression bodies), theUSING BTREEMySQL hint, andDESCordering. SkipsFULLTEXT,SPATIAL, prefix-length, and expression indexes with a per-skip reason printed to stderr.go run ./tools/pg-index-translate \ -in server/datastore/mysql/schema.sql \ -out server/datastore/mysql/migrations/tables/{ts}_AddMissingPGIndexes.sqlUnit-tested in
tools/pg-index-translate/main_test.govia inline schema fixtures — no file I/O required. The translator output drives theAddMissingPGIndexesmigration described in Operator-visible additions below.Architecture
DialectHelperinterface (server/datastore/mysql/dialect.go) — abstracts SQL dialect differences (upserts, aggregates, JSON, error classification, swap tables).pgx-rebinddriver (server/platform/postgres/rebind_driver.go) — transparently translates MySQL SQL to PostgreSQL at query execution time (50+ pattern rewrites, all static regexes hoisted to package-level vars compiled once at startup; per-table-name regexes cached insync.Mapto avoid per-call compilation).server/datastore/mysql/pg_baseline_schema.sql) — generated from productionpg_dump, authoritative for fresh-deployment initialization.Key design decisions
ds.dialect.*methods when dynamic SQL construction requires it. Dialect dispatch usesds.dialect.IsPostgres()throughout — no type assertions against the concrete type leak past the interface.pg_dump: PG baseline is generated from a running production database, not hand-maintained. Bumping the baseline is a documented operator procedure.Operator-visible additions
pg_baseline_schema.sqlcarries apg-baseline-up-to-migration: <ts>marker. On startup Fleet seedsmigration_status_tablesfrom code on a fresh apply, and logs a loud warning whenever code carries migrations newer than the embedded baseline. A unit test (TestVersionsAbove_EmbeddedBaselineCoversAllCode) fails CI if the baseline is stale relative to code on the same branch — silent drift is no longer possible.pg_baseline_post.sqlruns every startup and reassigns ownership of all public-schema tables, sequences, and views tocurrent_user. Fixes the "must be owner of table" error from atomic table swaps when the baseline was loaded aspostgres.20260513210000_AddMissingPGIndexes). PG had ~11 indexes vs MySQL's ~354, because the original baseline dump captured PKs but missed the secondaryKEYclauses. The migration adds 349CREATE INDEX IF NOT EXISTSstatements generated bytools/pg-index-translateand executes them only on PG (UpFnPG) — first migration to use the dialect-specific fields ongoose.Migration; MySQLUpFnis a deliberate no-op since those indexes already exist there. 6 indexes are intentionally deferred (FULLTEXT × 2, expression-with-MySQL-funcs × 3, prefix-length × 1) and will move into their own targeted PG migrations.server/goose/migration.gohas carriedUpFnPG/UpFnMySQLfields since the original PG layer landed; no migration had used them yet.20260513210000_AddMissingPGIndexesis the documented example for future PG-only or MySQL-only DDL.Connection configuration
Set
FLEET_MYSQL_DRIVER=postgres(and the standardFLEET_MYSQL_ADDRESS,FLEET_MYSQL_USERNAME, etc. pointing at your PG cluster). Seedocs/Deploy/postgresql.mdfor the full operator guide.Security-audit fixes
aggregatedimage build.test-go-postgres.yamltriggers on theaggregatedbranch; companionbuild-ledo.yml(onledoent) refuses to publishghcr.io/ledoent/fleet:latestunless bothtest-go-postgresandvalidate-pg-compatsucceeded on the build SHA.knownPrimaryKeys+ schema-drift validators undertools/pgcompat/run on every PR:check_primary_keys— every rawON DUPLICATE KEY UPDATEsite has aknownPrimaryKeysentry.check_schema_drift—CREATE TABLEset diff betweenschema.sqlandpg_baseline_schema.sqlwith an allowlist for intentional divergence.validators_test.go— gate-of-the-gate: empty allowlist must produce non-zero exit.ledoentbranch.git-aggregatorpinned to4.1inweekly-aggregate.yml.sync-upstream.ymlhardened: paranoia check refuses to force-pushmainif there are commits not inupstream/mainfrom anyone other thangithub-actions[bot].Driver rewrites (selected)
UUID_TO_BIN(UUID(), true)gen_random_uuid()UNHEX(expr)decode(expr, 'hex')HEX(expr)upper(encode(expr::bytea, 'hex'))IF(cond, t, f)CASE WHEN cond THEN t ELSE f ENDFIND_IN_SET(val, col) > 0val = ANY(string_to_array(col, ','))COALESCE(token, '')/ds.token/hmae.tokenCOALESCE(..., ''::bytea)GROUP_CONCAT(expr SEPARATOR ',')STRING_AGG(expr::text, ',')UPDATE t1 JOIN t2 ON ... SET ...UPDATE t1 SET ... FROM t2 WHERE ...INTERVAL ? SECOND(?::bigint) * INTERVAL '1 second'CAST(NULL AS SIGNED)CAST(NULL AS integer)expired = ?expired = (CASE WHEN ?::text = 'true' THEN 1 ELSE 0 END)FOR UPDATE(with LEFT JOIN)DELETE FROM t USING t INNER JOIN j ON c WHERE fDELETE FROM t USING j WHERE c AND fOther PG-compat fixes in this PR (caught by
tools/pg-compat-harness)selectSoftwareSQLGROUP BY: the goqu-built fallback for/software/versions?vulnerable=true(and any non-fast-path filter) calledGroupBy(...)after an earlierGroupByAppend(shc.hosts_count, shc.updated_at, ...). In goqu,GroupByREPLACES the existing clause and dropped the appended columns. MySQL silently tolerates this under relaxedonly_full_group_by; PG rejects withSQLSTATE 42803("must appear in the GROUP BY"). Fixed atserver/datastore/mysql/software.go:2019by switching toGroupByAppend. Sibling of PG-compat round 11, which patched the same function forhs.last_opened_atbut missedshc.hosts_count.AppendListOptionsWithParamsSecureparsed every numeric-looking cursor asint64and bound it as such. For text columns (display_name,hostname, etc.) pgx then rejected withcannot find encode planbecause theint8OID didn't match the varchar column. Fix: optionaltextOrderKeys ...stringvarargs that mark which keys are text-typed; cursor stays a string for those. Non-breaking — existing callers see no behavior change. Wired intohostAllowedOrderKeysandbatchScriptHostAllowedOrderKeysfor the keys that needed it. Unit-tested inserver/platform/mysql/list_options_test.go(TestAppendListOptionsWithParamsSecure_TextOrderKeyCursorBinding, 4 cases including the non-breaking contract).Tests
TestPostgresHostSoftwareUpdate(B1 Tier 1) — 5 subtests exercisingUpdateHostSoftwareend-to-end, including theUPDATE...JOINpath that broke prod in A1.TestCarves(B1 Tier 2) — runs on both MySQL and PostgreSQL viaCreateDS(t). Surfacedbool → smallintgap oncarve_metadata.expired; fixed viarewriteSmallintBoolColumns.TestScripts(B1 Tier 3) — all 29 subtests converted toCreateDS(t), passing on both backends.TestRewriteUpdateJoin,TestRewriteDeleteUsing,TestRewriteGroupConcat,TestResolveOnConflictAmbiguity,TestRewriteSmallintBoolColumns,TestRewriteMaxBoolColumns,TestRewriteIntervalPlaceholder,TestRewriteCastNullAsSigned,TestRewriteFindInSet,TestRewriteCoalesceAliasedToken,TestStripNullBytes.TestPostgresDialectSQLandTestMysqlDialectSQLcover all methods including LAST_INSERT_ID stripping, no-op fallback, ReturningID, AtomicTableSwap, CreateTableLike.TestAppendListOptionsWithParamsSecure_SkipsOrderByOnAggregate(existing) plus newTestAppendListOptionsWithParamsSecure_TextOrderKeyCursorBindingcovering the int64-vs-string cursor binding contract.tools/pg-index-translate/main_test.godrives the parser with inline schema fixtures (no file I/O): plain/unique keys,DESCordering,USING BTREEsuffix, FULLTEXT/SPATIAL/prefix-length/expression skips, balanced parens, and theextractParenBody+quoteIdenthelpers.tools/pg-compat-harness/Playwright matrix; see the Extractable upstream contribution section above for coverage details. Reports a single passing line per probe (221+) and per-probe Postgres-error context on failure.htmlhintupgraded (0.11.0→1.9.2),yamlupgraded (1.10.2→1.10.3); npm audit step added totest-website.ymlCI.BenchmarkUpdateHostSoftware,BenchmarkListSoftware,BenchmarkListHostsinserver/datastore/mysql/benchmarks_test.go. Run withPOSTGRES_TEST=1 go test -bench=Benchmark -benchtime=5s -count=5 -run=^$ ./server/datastore/mysql/ > /tmp/pg.benchfor PG numbers; swap env var for MySQL baseline. Compare withbenchstat.