feat(db): add MySQL as metadata database option, with parity fixes and CI coverage - #49
Draft
lyingbug wants to merge 6 commits into
Draft
feat(db): add MySQL as metadata database option, with parity fixes and CI coverage#49lyingbug wants to merge 6 commits into
lyingbug wants to merge 6 commits into
Conversation
…tion Keeps both wiki_page_test.go additions that conflicted: main's TestListPagesCursorExcludesArchivedPages and Tencent#2235's TestFindSimilarPagesSQLiteReturnsRankedMatches. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The repository tests all run on SQLite, which accepts SQL that MySQL rejects. That gap is what let the bare-key `metadata->>'external_id'` form ship: SQLite treats a bare key as $.key, MySQL raises error 3143 as soon as a row holds non-null JSON. Add an integration suite that applies migrations/mysql to a throwaway database and drives the real repositories through the dialect-sensitive query paths, plus a CI job with a mysql:8.0 service so the suite is not permanently skipped. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
FindByMetadataKeyPrefix built the JSON extraction by hand as metadata->>'<key>'. MySQL requires a '$.key' path there and rejects the bare-key form with error 3143 once any row holds non-null JSON. The only caller logs and returns, so on MySQL the datasource re-sync silently stopped sweeping sub-items that had disappeared upstream, accumulating orphan knowledge rows behind a single warning line. Route the expression through the dialect helper. JSONPathExprIndexed additionally substitutes the generated column MySQL indexes, because MySQL only rewrites an indexed generated column for equality-shaped predicates and never for LIKE. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
Three divergences from the PostgreSQL schema, found by diffing information_schema against a database built from migrations/versioned: - chunks.seq_id and knowledge_tags.seq_id started at 1 instead of the sequence start values 100000000 and 10000000. FAQ import lets callers pin a seq_id below the start value, so that range is reserved for imports; generated values entering it collide with imported ones. - sessions.fallback_response defaulted to the empty string instead of the seeded answer, so a MySQL deployment answered nothing where PostgreSQL answers a message. - knowledges lacked an equivalent for the expression index behind the datasource external-ID lookups, and im_channel_sessions lacked the im_channel_id index, leaving both as full scans. The generated column is LONGTEXT rather than VARCHAR(n) on purpose: the extracted value is unbounded on PostgreSQL, and a typed generated column fails the whole INSERT with error 1406 once a value overflows it. Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
The portable-regex guard rejected all letter escapes, which took \d, \s
and \w with it. Those mean the same thing to PostgreSQL ARE, MySQL ICU
and Go RE2, so banning them only cost the agent expressiveness.
\b stays rejected, and it is the reason the guard exists: MySQL and RE2
read it as a word boundary while PostgreSQL ARE reads it as a literal
backspace, so one pattern matches different text per deployment.
Verified on both servers: MySQL REGEXP_LIKE('a rag b', '\\brag\\b')
returns 1, PostgreSQL 'a rag b' ~* '\brag\b' returns false.
Co-authored-by: lyingbug <lyingbug@users.noreply.github.com>
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.
Description
This builds on #2235 by @yuefanxiao, which adds MySQL as a business/metadata database option for issue Tencent#1418. That PR is the strongest implementation of the batch: it introduces a real dialect layer (
internal/database/dialect.goplus capability predicates such asdialectSupportsRowLocking) instead of scatteredif dialect == "mysql"branches, and its MySQL schema matches the PostgreSQL schema table-for-table and column-for-column.I merged it onto current
main(resolving one test-file conflict by keeping both sides' new tests) and then fixed the gaps that a review against a live MySQL 8.0 server and a live PostgreSQL 17 reference turned up.1. The metadata prefix sweep could not run on MySQL
FindByMetadataKeyPrefixbuilt its JSON extraction by hand asmetadata->>'<key>'. MySQL requires a'$.key'path there and rejects the bare-key form withError 3143as soon as any row holds non-null JSON. Its only caller logs and returns, so the failure was silent: on MySQL, re-syncing a datasource stopped sweeping sub-items that had disappeared upstream, accumulating orphan knowledge rows behind a single warning line.The expression now goes through the dialect helper. A new
JSONPathExprIndexedalso substitutes the generated column MySQL indexes, because MySQL rewrites an indexed generated column only for equality-shaped predicates and never forLIKE— the mapping lives in one place ininternal/databaserather than as a bare column name at each call site.2. Schema divergences from PostgreSQL
Found by diffing
information_schemabetween a MySQL database built frommigrations/mysqland a PostgreSQL database built frommigrations/versioned:chunks.seq_idandknowledge_tags.seq_idstarted at 1 instead of the sequence start values100000000and10000000. FAQ import lets a caller pin aseq_idbelow the start value (documented ontypes.FAQImportEntry.ID), so that range is reserved for imports and generated values must stay out of it.sessions.fallback_responsedefaulted to the empty string instead of the seeded answer, so a MySQL deployment answered nothing where PostgreSQL answers a message.knowledgeshad no equivalent of the expression index behind the datasource external-ID lookups, andim_channel_sessionswas missing theim_channel_idindex, leaving both as full scans. The datasource sync runs one such lookup per item.The generated column is
LONGTEXTrather thanVARCHAR(n)deliberately: the extracted value is unbounded on PostgreSQL, and a typed generated column fails the entireINSERTwithError 1406once a value overflows it (verified on the server).3. A PostgreSQL-side regression in the regex guard
The portable-regex guard rejected every letter escape, which took
\d,\sand\wwith it. Those mean the same thing to PostgreSQL ARE, MySQL ICU and Go RE2, so banning them only cost the agent expressiveness on deployments that already worked.\bstays rejected, and it is the reason the guard exists at all. Verified on both servers:One pattern, different matches per deployment — so it is genuinely non-portable, unlike the character classes.
4. The missing test dimension
Every repository test ran on SQLite, which accepts SQL that MySQL rejects — the bare-key
->>form above is exactly that shape of bug, and it passed the SQLite suite. So this adds an integration suite that appliesmigrations/mysqlto a throwaway database and drives the real repositories through the dialect-sensitive query paths, plus a CI job with amysql:8.0service so the suite is not permanently skipped.Type of Change
Related Issue
Closes Tencent#1418
Builds on #2235 (@yuefanxiao). Related implementations of the same issue: #2320 (@WHUTcjh-2024), #2264 (@mingri31164), #2132 (@wei-yan1), #1904 (@Arreboi06).
Testing
Verified against a real MySQL 8.0.46 server and a real ParadeDB/PostgreSQL 17 server, not just SQLite.
MySQL integration tests — the new suite, all passing:
The suite actually catches the bug it was written for. Reverting only the
FindByMetadataKeyPrefixfix and re-running:Schema parity — MySQL schema versus the PostgreSQL reference built from
migrations/versioned, comparing tables, columns, nullability and defaults:Migrations apply cleanly (51 tables) and
000000_init.down.sqldrops back to 0 tables. TheAUTO_INCREMENTstart values now readchunks=100000000,knowledge_tags=10000000,tenants=10000.Index usage confirmed with
EXPLAIN: the equality form reaches the index through generated-column substitution, and theLIKE-prefix form reaches it by naming the generated column.No PostgreSQL regression.
migrations/versioned/is untouched, and all 79 migrations still apply to a fresh PostgreSQL 17 database with 0 failures.Repository-wide checks:
golangci-lintis not installed in this environment, so diff-scoped lint was not run.Checklist
git diff --check origin/main...HEADpassesgolangci-lintunavailable in this environment;go vetis clean)docs/mysql-primary-database.md)Screenshots / Recordings
Not applicable — no user-visible UI changes.
Known follow-ups (not in this PR)
docs/mysql-primary-database.mddocuments the difference. An ngramFULLTEXTindex would close it but changes ranking semantics, so it belongs in its own change.utf8mb4_0900_ai_cicollation makes human-facing unique constraints (users.username,organizations.invite_code, variousnamecolumns) case-insensitive, where PostgreSQL is case-sensitive. This may well be the behavior MySQL users expect, but it deserves an explicit decision rather than being inherited from the database default.migrations/mysqlis a single consolidated baseline, so nothing fails the build when a newmigrations/versionedmigration lands without a MySQL counterpart. The parity diff used here is scriptable and would make a good CI guard.