Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(plugin-mysql): add OceanBase MySQL-mode connection type - #2741

Merged
datlechin merged 21 commits into
TableProApp:mainfrom
J2TeamNNL:cursor/oceanbase-mysql-flavor-c3a9
Sep 13, 2026
Merged

feat(plugin-mysql): add OceanBase MySQL-mode connection type#2741
datlechin merged 21 commits into
TableProApp:mainfrom
J2TeamNNL:cursor/oceanbase-mysql-flavor-c3a9

Conversation

@J2TeamNNL

@J2TeamNNL J2TeamNNL commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #1748

Summary

Adds OceanBase as a connection type on the bundled MySQL plugin (libmariadb), same family as TiDB and MariaDB. Type id OceanBase, default port 2881, scheme oceanbase://. Oracle compatibility mode is out of scope.

This is not a cloned MySQL snapshot. The flavor is resolved from @@version_comment when the connection type is OceanBase. The MySQL handshake reports 5.7.25 (or 5.6.25 through OBProxy) and does not name the engine, so a server opened as MySQL stays on the MySQL path. Choose OceanBase for the OceanBase sidebar, EXPLAIN, Stop, and timeout.

Grid save

OceanBase keeps a hidden __pk_increment column on a table declared without a primary key. The catalog never reports it and SELECT * never returns it, so TablePro edits such a table the way it edits one on MySQL: the UPDATE or DELETE matches every column of the row, and a save that would touch more than one row is rolled back.

A keyless save on FLOAT, DOUBLE or JSON compares through CONCAT(), same as MySQL, MariaDB and TiDB after the measurement on OceanBase CE 4.4.2.1 that landed on main.

Flavor

  • MySQLServerFlavor.oceanbase(version:) from @@version_comment: OceanBase_CE 4.4.2.1, OceanBase 3.1.3, OceanBase_CE-v4.3.5.
  • OceanBase-type connection whose comment is silent stays OceanBase. A comment from another engine is refused.
  • Databend opened as MySQL is still refused.
  • Stop uses KILL QUERY with the handshake thread id. Query timeout sets max_execution_time and ob_query_timeout together, because OceanBase's own 10 second default still fires on statements that are not read-only.
  • System databases: information_schema, mysql, oceanbase. User database test stays listed.
  • Maintenance: ANALYZE TABLE only, on the same PluginMaintenanceOperation descriptors as TiDB and Databend.
  • CHECK and GENERATION_EXPRESSION are gated on OceanBase 4.0+, not on the 5.7 banner.

Curated snapshot

Port 2881, oceanbase://, EXPLAIN as plain text (no FORMAT=JSON, no ANALYZE), no Spatial types, no Server Dashboard, no native dump, Compare only OceanBase-to-OceanBase, no SET FOREIGN_KEY_CHECKS, no Users & Roles connection limit, triggers browse without edit, routines yes. Idle-release field only; no AWS IAM or Unix socket.

iOS

Type routing only, the same shape as TiDB. The iOS MySQL driver has no flavor path. Databend stays Mac-only.

iOS compiles the shared MySQL maintenance descriptors and the PostgreSQL catalog quoting helper (PostgreSQLObjectQueries), so the flavor switch and the quoting change from #2777 build on iPhone. The iOS PostgreSQL driver passes the raw schema and table names and lets that helper quote them.

Verification

This environment has no Docker and no Xcode. Live OceanBase was not measured here; the FLOAT/DOUBLE/JSON row-match prefixes follow the measurement already on main. Unit tests cover version comments, flavor/SQL/Stop paths, registry count 36, allKnownTypes count 30, oceanbase://, Docker Compose oceanbase/oceanbase-ce as root@sys, iOS foreign-key quoting including a backslash, and OceanBase in the MySQL-family text row-match set.

Fork CI on 81597db was green (13 checks). This revision merges current main (theme rewrite, keyless FLOAT/JSON match) so the PR is no longer conflicted.

Docs house style and check-docs-against-source.py pass. swiftlint --strict and xcodebuild could not run here.

Not in this PR

  • Oracle compatibility mode
  • Cluster tenant admin UI
  • Rewriting saved MySQL connections after a comment probe
  • PluginKit bump (follows main, currently 29)
  • Weaviate (Database request: Weaviate #1724), already on main

@cursor
cursor Bot force-pushed the cursor/oceanbase-mysql-flavor-c3a9 branch from e3e5e51 to e163916 Compare September 11, 2026 15:43
@datlechin

Copy link
Copy Markdown
Member

Thanks for this. I reviewed it, then measured the hidden-primary-key mechanism against a live OceanBase CE 4.4.2.1 (Docker, oceanbase/oceanbase-ce) using this branch's own driver through a swiftc harness. The measurements contradict what it is built on, so I pushed two commits: the flavor, Stop, snapshot and iOS work stays, the hidden-PK machinery is gone, and tables without a primary key are edited the way they are on MySQL.

What the server actually does

Claim Measured on 4.4.2.1
SHOW INDEX lists the hidden PK It does not. SHOW INDEX on a keyless table returns zero rows. Only SHOW EXTENDED INDEX shows it
__pk_cluster_column exists on some clusters It exists in no version. ob_define.h defines one hidden name, __pk_increment
The hint makes SELECT * include the hidden key It does not. Star expansion passes include_hidden=false, so SELECT /*+ ... */ * returns the visible columns only
The hint is needed for the save It is not. UPDATE/DELETE ... WHERE __pk_increment = ? work with no hint. INSERT naming the column fails even with it
__pk_increment identifies one row Not on a partitioned table. It is a per-tablet sequence: UPDATE ... WHERE __pk_increment = 1 on a 2-partition table changed 2 rows
A MySQL connection to OceanBase is detected from the banner It is not. The handshake banner is 5.7.25 (5.6.25 through OBProxy) and names no engine. Only @@version_comment does

Two more defects showed up in the branch as written. After the sidebar's bulk column load (fetchAllColumns, which autocomplete calls on every connect) the cache held [] for every table, so the grid's SELECT * stopped being projected while the metadata still named __pk_increment as the primary key: generateUpdateSQL then returned nil and the save silently did nothing, which is the bug the PR exists to fix. And the rewrite ran on every SELECT * FROM <table> the driver executed, so CSV/JSON/XLSX/SQL exports, the MCP export tool and the foreign-key preview all gained a __pk_increment column; the SQL export emitted an INSERT naming it, which the server rejects on restore.

What changed

Removed the hidden-PK machinery (MySQLPluginDriver+OceanBase.swift, OceanBaseSQL.swift, OceanBaseHiddenPrimaryKey.swift, the cache, the SELECT * rewrite and the hint injection). A keyless OceanBase table now edits like a keyless MySQL one: the WHERE matches every column, and a save that would touch more than one row is rolled back. That is what OceanBase's own client ODC, DBeaver, Navicat and TablePlus do; none of them use __pk_increment. It also removes 12 of the 15 review findings by deletion.

Identity. An OceanBase connection probes @@version_comment. A server that answers with another engine is refused with a notOceanBase mismatch error, the way the Databend arm already worked (verified against a plain MySQL 8.4). A probe that does not answer at all is not treated as evidence, so a transient failure on an automatic reconnect cannot lock the user out of a connection that worked a minute ago.

Stop uses the handshake thread id, as on MySQL. On a direct connection it equals CONNECTION_ID() and is not truncated; KILL QUERY with it interrupts the query (errno 1317, verified through the driver's cancel path). The previous CONNECTION_ID() route fell back to a raw thread id when the probe failed, which on OceanBase can name another live session.

Query timeout. max_execution_time governs read-only statements only: an UPDATE under a 30 s setting still failed at 10 s with error 4012, OceanBase's own ob_query_timeout default. The statement now sets both, so a 60 s timeout runs an 11 s UPDATE to completion, and the "no limit" setting that exports rely on lifts both.

Docker Compose import. oceanbase as a substring matched ocp-ce, obagent and miniob, none of which speak the MySQL protocol. It now matches the repository, puts OBProxy on 2883, and reads OB_TENANT_NAME, OB_TENANT_PASSWORD, OB_SYS_PASSWORD, OB_DATABASE and OB_CLUSTER_NAME instead of hardcoding an empty root@sys.

Also: one shared mysqlColumnTypesWithoutSpatial helper for TiDB and OceanBase, the iOS and Shortcuts docs now list OceanBase (the picker already offered it), and the swiftlint --strict failure on the snapshot addition is fixed, which would have blocked the lint job.

Verified

Build, swiftlint --strict on the changed paths, both docs checks, and 102 tests across the flavor, variant, registry, compose and type suites. Live: connect as OceanBase and as MySQL, the mismatch against plain MySQL 8.4, fetchColumns on keyless, partitioned, keyed, view and generated/CHECK tables, a keyless save, Stop, and the DML timeout.

datlechin and others added 8 commits September 12, 2026 09:30
# Conflicts:
#	CHANGELOG.md
#	TablePro/Core/Plugins/ImportTypeMapper.swift
#	TableProTests/Core/Plugins/PluginMetadataRegistryTypeCountTests.swift
#	docs/snippets/driver-counts.mdx
@datlechin

Copy link
Copy Markdown
Member

Thanks for this. I merged current main into the branch and pushed fixes on top. Everything below was measured on OceanBase CE 4.4.2.1 and 4.0.0.0 in Docker, directly and through OBProxy CE 4.3.5, with MySQL 8.4 as the control, using a probe linked against the libmariadb the app ships.

Merge with main

  • main moved maintenance operations into MySQLMaintenance.swift (fix(ios): state the isolation of every plugin source the iOS app compiles #2811). The OceanBase arm now lives there, and the three plugin files this branch had added to the iOS target are gone again, along with their nonisolated edits.
  • The unrelated test and helper edits (Weaviate manifest, data grid strike test, Vim settings UI test, the iOS PostgreSQL quoting catch-up) take main's versions.
  • CHANGELOG: one Added line under [Unreleased]. The two Fixed lines described regressions that never shipped.

What changed and why

  • Column defaults were misread. OceanBase's catalog reports DEFAULT (uuid()) and DEFAULT 'uuid()' as the same uuid(), with no DEFAULT_GENERATED marker. The MySQL rule quoted the expression, so the next structure edit wrote the constant string 'uuid()'.
    • When it looks it up: only when the catalog text contains a parenthesis, on a column type that can hold an expression default (not numeric, BIT, ENUM or SET), and the text is not CURRENT_TIMESTAMP. OceanBase stores a constant expression as a plain literal, and every real expression default keeps a parenthesis in the catalog. A table with no such column costs no extra query.
    • How: the default is read from SHOW CREATE TABLE on a scanner shared with the TiDB CHECK parser. It accepts backticked names, bare names (sql_quote_show_create = 0) and double-quoted names (ANSI_QUOTES). Each clause is checked against the catalog value before it is used, and a clause that does not match leaves the catalog value in place with a logged warning.
    • What it skips: views, picked out through information_schema.TABLES, because SHOW CREATE TABLE on a view needs SHOW VIEW.
    • Precision: a bare CURRENT_TIMESTAMP takes the column's precision, the only one OceanBase accepts (error 1067 otherwise).
    • Binary defaults are quoted from the catalog text without a lookup.
  • Views. On a view, SHOW FULL COLUMNS returns NULL or '' as every default. OceanBase column reads now use information_schema.COLUMNS for both the per-table and the bulk read.
  • Keyless saves. OceanBase joins the engines whose row counts are trusted. Affected rows are identical to MySQL, so a keyless save that matched no row is refused instead of committed.
  • Identity. @@version_comment has to start with OceanBase or OceanBase_CE followed by a version. A probe that fails now fails the connect, instead of continuing with an unknown version that silently disabled the CHECK and generated-column reads.
  • Timeout. ob_query_timeout and max_execution_time = 0 go as two statements. ob_query_timeout = 0 times out immediately, and a global max_execution_time still kills SELECT unless the session clears it. A multi-assignment SET fails whole when either variable is unknown. iOS now lifts the 10 s server default on connect as well.
  • Capabilities. SET FOREIGN_KEY_CHECKS=0 is honoured and MAX_USER_CONNECTIONS is enforced (error 1226) on 4.0 and 4.4, so both flags are on.
  • System databases. __recyclebin, __public, SYS, LBACSYS and ORAAUDITOR, the server's own inner databases, are added to the hidden list.
  • Maintenance. Bare ANALYZE TABLE t is a syntax error before 4.2.2, so it is offered from 4.2.2 only.
  • Catalog gates. GENERATION_EXPRESSION exists on every published CE version. CHECK constraints stay gated at 4.0, which was confirmed on a live 4.0.0.0.
  • Spatial types. GEOMETRY, POINT and friends work, with wire bytes identical to MySQL, so the Spatial group is back.
  • Docker Compose.
    • root@sys#cluster is refused on the observer port, so observers never get a cluster suffix.
    • The tenant defaults to test, as the image does. OB_SYS_PASSWORD (or the older OB_ROOT_PASSWORD) means the sys tenant, except in SLIM mode.
    • OBProxy services import on 2883 as root@<tenant>#<OB_CLUSTER>, with the tenant of the observer their RS_LIST names.
  • Parity test. A new test pins the MySQL-protocol variants and fails if one goes missing from a family list. That is how the row-count gate was missed.
  • The explanatory comments are gone, per the repo's no-comments rule.

Verified

  • Live, against the real MySQL driver (a swiftc test program built from the plugin sources), on OceanBase 4.4.2.1 directly and through OBProxy:
    • the flavor resolves to 4.4.2
    • with a global max_execution_time = 1000, No limit still runs a 3 s statement and a 2 s timeout stops a 4 s one
    • fetchColumns and fetchAllColumns agree on every default, views included
    • constant expressions, function defaults, binary defaults, CURRENT_TIMESTAMP(3) and SHOW CREATE TABLE without backticks all resolve correctly
    • a user with only SELECT reads the view and the whole schema without error
    • editing only a column's comment keeps DEFAULT (uuid()), and a new row gets a real UUID
    • CHECK constraints, Stop (KILL QUERY, 1317) and the system database list all work
  • Refusal: MySQL 8.4 opened as OceanBase is refused with the connection error. Opened as MySQL, it behaves as before.
  • Unit tests: 536 cases across the 27 suites that cover these files pass, including the new OceanBaseColumnDefaultsTests (measured SHOW CREATE TABLE output as fixtures) and MySQLProtocolVariantParityTests.
  • Build and checks:
    • App Debug build: passes.
    • PluginKit ABI against the merge base: unchanged.
    • AllPlugins: every plugin compiles except Oracle, which fails in oracle-nio on an Xcode-beta @TaskLocal macro bug that is on main too.
    • Also passing: swiftlint on the changed files, the docs checks, and check-ios-shared-isolation.py.
  • Not run locally: the iOS test build stops in that same oracle-nio target on this toolchain, so the iOS change is covered only by CI.

@datlechin
datlechin merged commit 035ffe8 into TableProApp:main Sep 13, 2026
6 of 7 checks passed
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.

Database request: Oceanbase

3 participants