Ir al contenido

Postgres-only, no MySQL

Esta página aún no está disponible en tu idioma.

Phase 0 brought up two databases: MySQL (for the legacy backend) and Postgres + pgvector (for new-feature data). The original rationale was that the legacy backend is mysqli-only and migrating to Postgres would be a “3-6 month multi-thousand-call-site rewrite.”

Closer inspection contradicted that estimate:

  • Direct mysqli_* calls are concentrated in ~68 sites across 10 files, most of them in low-level database wrapper files.
  • Application code routes through high-level helpers (ps_query, ps_value, sql_insert_id, etc.), so the call sites of those helpers are largely portable.
  • We have no real data yet. No data migration risk.
  • Operating two databases (MySQL for the legacy backend, Postgres for sidecars) doubles backup, monitoring, upgrade, and developer onboarding cost forever.

The user’s call: kill MySQL, migrate to Postgres now, before any sidecar code is written and before any production data exists.

Single database: PostgreSQL 16+ with pgvector

Section titled “Single database: PostgreSQL 16+ with pgvector”
  • Drop the mysql service from docker-compose.yml.
  • Point the legacy backend at the existing postgres container.
  • All future sidecars use the same Postgres instance.

Replace mysqli with PDO + pgsql. PDO is the right migration target because the existing helpers already use prepared statements with positional or named parameters — PDO’s prepare/bindValue/execute model fits cleanly. The pdo_pgsql extension is already baked into our image.

We will not introduce a heavy ORM (Doctrine, Eloquent) during the migration — that’s a separate refactor with different goals. Keep the existing helper function surface (ps_query, ps_value, etc.), just re-implement them against PDO+pgsql.

Schema improvements (“gold standard” calibration)

Section titled “Schema improvements (“gold standard” calibration)”

Where the upstream MySQL schema makes mediocre type choices, we upgrade to Postgres-native types. Names stay the same to minimize app-code churn; only types and constraints change.

MySQL upstreamPostgres in artist-alleyWhy
int(11) for IDsBIGINTFuture-proof; cost is negligible
tinyint(1) booleanBOOLEANTrue type, better tooling, plays well with ORM/codegen
datetimeTIMESTAMPTZTime-zone aware, UTC-stored, the only sane default in 2026
text holding serialized dataJSONB (case-by-case)Indexable, queryable, validatable
varchar(N) with arbitrary Nkeep VARCHAR(N) or promote to TEXTTEXT is identical in performance in PG; bounded varchar only where intent is clear
FULLTEXT indexes(none initially — see “Full-text search” below)The legacy backend uses MATCH ... AGAINST; Postgres uses tsvector. Not a 1:1 swap.

We do not retrofit foreign-key constraints onto every legacy table during this migration. The legacy code occasionally relies on the absence of FK enforcement (orphan rows during workflows). FKs come in a follow-up pass, selectively, after we know which relationships are safe to enforce.

The legacy backend defines its schema in dbstruct/*.txt files (CSV format, loaded by CheckDBStruct()). This system is part of the plugin ecosystem the user wants to preserve: plugins drop their own dbstruct/ and the backend auto-creates the tables.

We keep the dbstruct/ system but rewrite its loader to emit Postgres DDL instead of MySQL. The on-disk CSV format stays unchanged, so existing plugins (and any third-party plugins someone might install later) “just work.”

For our own new tables (events outbox, review sessions, comments v2, annotations, embeddings, user mirror — see ADR 0004’s now-retired list), we use goose with plain .sql files in infra/db/migrations/. Two coexisting systems with clear ownership:

  • dbstruct/ — for legacy core tables and legacy plugin tables (Postgres-emitting loader). Compatible with the upstream plugin ecosystem.
  • infra/db/migrations/ — for artist-alley-specific tables. Versioned, reversible, the same migration runner the Go sidecars use.

Keep the legacy plugin system intact and use it where it fits. Sidecars (Go services in services/) handle workloads legacy plugins can’t — streaming, concurrency, ML, non-PHP runtimes.

Hybrid policy for new artist-alley capabilities:

  • Build as a legacy plugin when the capability is:
    • PHP-friendly (form handling, simple DB ops, HTML pages)
    • Tightly bound to legacy sessions, permissions, or hooks
    • Useful beyond artist-alley
    • Small enough that a separate service would be over-engineered
  • Build as a Go sidecar when the capability is:
    • Real-time / streaming (SSE review sessions, presence)
    • Compute-heavy (video transcoding, ML inference)
    • Language-native (Go’s HTTP stack, Python’s ML stack)
    • Provider-agnostic abstraction (AI gateway, embeddings)
    • Operationally distinct (separate scaling, separate deployment cadence)

We will create a single legacy plugin named artist_alley that holds the legacy-side bridge: an event-publishing helper (writes to the Postgres events table), auth-header injection for sidecar requests, and hooks into upload/comment/login. That plugin becomes the canonical home for legacy-side artist-alley logic; scattered // ARTIST-ALLEY: patches to legacy core files are minimized in favor of plugin hooks.

The legacy backend uses MySQL’s MATCH ... AGAINST syntax with FULLTEXT indexes. Postgres uses tsvector/tsquery — different API, different model.

A naive translation (e.g., trigram similarity via pg_trgm) would work for some queries but produce different ranking and missing features. We defer the proper full-text rewrite to a follow-up commit and accept that search is broken or degraded during the migration window. A TODO(ARTIST-ALLEY): full-text-search marker goes everywhere we leave a stub. The longer-term goal is a tsvector-based search plus an optional sidecar that does semantic search (pgvector ANN) — see ADR 0004’s now-retired embeddings table sketch.

Positive

  • One database to operate, monitor, back up, and reason about.
  • Postgres-native types from day one across both the legacy backend and our additions.
  • pgvector, pg_trgm, JSONB, LISTEN/NOTIFY available everywhere — no cross-DB acrobatics.
  • The events outbox + sidecars pattern (from ADR 0004) becomes cheaper since there’s no DB boundary inside the app.

Negative

  • The migration itself is real work — see “Strategy” below.
  • Full-text search is degraded until the rewrite lands.
  • Plugins that contain raw MySQL DDL or mysqli calls (the simplesaml plugin notably bundles vendored Symfony code that uses mysqli) will break when enabled. We accept that — enabling such plugins is a per-plugin port effort, on demand.

All work happens on feature branches (per the user’s preferred workflow) that merge into dev when each phase passes its smoke test, and dev into main when major milestones are stable.

Phase 0.5.A — Driver swap and connection layer

Section titled “Phase 0.5.A — Driver swap and connection layer”

Branch: feat/postgres-connection-layer

  1. Drop the mysql service from docker-compose.yml.
  2. Add a db service alias to postgres so the existing $mysql_server config name doesn’t have to change (or alternatively rename $mysql_* variables in config to $db_* for clarity). Decide during implementation.
  3. Rewrite the database helpers to use PDO+pgsql. Keep the public helper signatures (ps_query, ps_value, sql_insert_id, etc.) unchanged so callers don’t move.
  4. Re-implement identifier quoting (double-quotes), placeholder syntax (? works in PDO for both, prefer ?), and LASTVAL() / pg_get_serial_sequence for auto-increment ID recovery.
  5. Update the installer to install against Postgres. Most of the file becomes ARTIST-ALLEY-tagged divergence.
  6. Smoke test: ./scripts/bootstrap.sh succeeds, installer reaches the “configure base URL” step.

Branch: feat/postgres-schema-emitter

  1. Rewrite CheckDBStruct() (and database helpers) to translate dbstruct/*.txt rows into Postgres DDL.
  2. Mapping: MySQL types → Postgres-native types per the table above.
  3. Add a column-level override mechanism (a small config file or an extra column in dbstruct/*.txt) so we can opt specific columns into JSONB, BOOLEAN, etc. when the MySQL type was a poor fit.
  4. Smoke test: installer creates all 57 core tables on Postgres without errors.

Branch: feat/postgres-query-dialect (likely multiple sub-branches)

  1. Run the installer and click through the major pages. Each broken SQL query gets a focused patch.
  2. Common patterns:
    • IFNULL(a, b)COALESCE(a, b)
    • GROUP_CONCAT(...)STRING_AGG(...)
    • LIMIT a, bLIMIT b OFFSET a
    • ON DUPLICATE KEY UPDATEON CONFLICT (cols) DO UPDATE
    • Backtick identifiers → double-quoted
    • NOW() works in both — leave alone
    • DATE_FORMAT()TO_CHAR()
    • REGEXP~ / ~*
  3. Each fix is a small commit tagged // ARTIST-ALLEY: with the pattern referenced.

Phase 0.5.D — Smoke test the major flows

Section titled “Phase 0.5.D — Smoke test the major flows”

Branch: feat/postgres-smoke-tests

  1. Walk through: install → log in → upload an asset → browse → comment → search (degraded, that’s fine for now) → admin pages → user management.
  2. Each broken thing is a small commit.
  3. End state: the legacy backend works on Postgres for the core flows. Search is the known exception, awaiting Phase 1.

Phase 0.5.E (optional) — Goose for new migrations

Section titled “Phase 0.5.E (optional) — Goose for new migrations”

Branch: feat/goose-migrations

  1. Add infra/db/migrations/ with 0001_extensions.sql (moved from infra/postgres/init.sql).
  2. Add a goose-runner container or make target.
  3. No real artist-alley tables yet — that’s Phase 1.

None blocking. The questions from ADR 0004 about workspace hierarchy, user mirror sync, multi-tenancy, auth bridge, and embedding source storage all resurface when we get to Phase 1, but they don’t shape the Postgres migration itself.

ADR 0004 (“Adjunct Postgres data model”) is retired in the same commit that adds this ADR. Its content is partially absorbed here; the rest (specific table sketches) will return in a new ADR once we’re past the migration and actually designing artist-alley tables.

ADR 0003 (“Strangler Fig internal”) had a paragraph asserting that the legacy backend keeps its MySQL. That paragraph is now incorrect; this ADR supersedes it on the database question.