Skip to content

feat(plugin-mysql): add TiDB and Databend connection types - #2711

Merged
datlechin merged 11 commits into
TableProApp:mainfrom
J2TeamNNL:cursor/tidb-databend-mysql-variants-135e
Sep 11, 2026
Merged

feat(plugin-mysql): add TiDB and Databend connection types#2711
datlechin merged 11 commits into
TableProApp:mainfrom
J2TeamNNL:cursor/tidb-databend-mysql-variants-135e

Conversation

@J2TeamNNL

@J2TeamNNL J2TeamNNL commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Fixes #1066
Fixes #2514

Summary

Adds TiDB and Databend as connection types served by the bundled MySQL plugin. The first version routed both to the unchanged MySQL driver. Measured against live TiDB 7.5.1 / 8.5.1 and Databend 1.2.881 servers, that broke saves, the Structure tab, Stop, Users & Roles and EXPLAIN on one or both. This revision gives the driver a server flavor and fixes each of those paths.

Root cause

The driver's only engine signal was isMariaDB, read from the version banner. TiDB reports 8.0.11-TiDB-v7.5.1 and Databend reports 8.0.90-v1.2.881-..., so every branch ran them as MySQL 8.0.x. The curated snapshots were copies of MySQL's, so they also inherited EXPLAIN FORMAT=JSON, lowercase system schemas, MySQL's type list and MySQL's capability flags.

What changed

Driver (Plugins/MySQLDriverPlugin)

  • MySQLServerFlavor (mysql, mariadb, tidb(version), databend) replaces isMariaDB. It is resolved once per connect from the banner plus the connection type. MySQLPlugin.driverVariant(for:) passes the type through the existing additionalFields["driverVariant"] channel, so there is no PluginKit change. TiDB is also recognised when it is opened as a MySQL connection. A Databend server opened as MySQL fails at connect and asks for the Databend type.
  • Stop: TiDB sends KILL TIDB QUERY <SELECT CONNECTION_ID()>. The handshake thread id is truncated to 32 bits, so it missed 64-bit ids. Databend sends KILL QUERY '<session uuid>'.
  • Stop on servers without TLS (pre-existing, all MySQL-family servers): the kill connection left the TLS options unset. The bundled libmariadb then requires TLS, so every kill against a server without TLS failed with 2026 and Stop did nothing. Measured on MariaDB 11.4 --skip-ssl, TiDB and Databend. The kill connection now copies the primary connection's negotiated transport.
  • TiDB: exact uppercase system schemas, sequences left out of the table list, principals read without max_user_connections, ANALYZE as the only maintenance operation.
  • TiDB CHECK constraints (7.2+) are parsed from SHOW CREATE TABLE. TiDB never lists them in TABLE_CONSTRAINTS, so MySQL's join always came back empty, and TIDB_CHECK_CONSTRAINTS does not exist on 7.5.
  • TiDB closes an idle session that receives KILL TIDB QUERY (measured on 7.5.1 and 8.5.1; MariaDB ignores it). A read that returns more rows than the cap therefore drains the rest on TiDB instead of killing a statement that may already be finished. Before, any SELECT ... LIMIT 5000 under a 1,000-row cap dropped the connection on the next statement, open transaction included.
  • Databend runs through its MySQL handler:
    • Parameters are inlined as escaped literals, because Databend refuses server-side prepare. The placeholder scanner treats backslash as an escape in '...' and "...", as Databend's MySQL dialect does, so a scanner and server disagreement can only produce a count mismatch, which is refused.
    • Identifiers are backtick-quoted. Databend does not read a doubled backtick as an escape, so a name containing a backtick is double-quoted instead. Doubling the backtick, as the MySQL path does, made DDL and view reads target a different table.
    • Transactions use BEGIN. START TRANSACTION is accepted but ignored, so a rollback kept the rows.
    • Affected rows are read from the number of rows ... result row.
    • Row caps use SET max_result_rows and timeouts use SET max_execute_time_in_seconds.
    • Catalog reads use system.columns and system.constraints. Routine and trigger reads return nothing instead of querying MySQL catalogs Databend lacks.
    • Structure DDL is Databend's: RENAME COLUMN, full MODIFY COLUMN restatement, no keys, indexes, charsets or AUTO_INCREMENT.
    • BOOLEAN cells show as true/false, which Databend accepts back. BINARY is decoded from the hex text Databend sends, and GEOMETRY is shown as GeoJSON text.

App

  • Corrected curated TiDB and Databend snapshots:
    • Exact system databases.
    • EXPLAIN and EXPLAIN ANALYZE shown as plain text, never FORMAT=JSON.
    • Per-variant type lists, kept through registration by a new named adoption.
    • Rename table and view, the idle-release field, and case folding through LOWER() for Databend.
    • A curated rowMatchExcludedTypePrefixes for Databend.
  • Databend tables have no primary keys, so a keyless UPDATE or DELETE leaves ARRAY, MAP, TUPLE, VARIANT, JSON, BITMAP, BINARY, GEOMETRY, GEOGRAPHY and VECTOR columns out of the WHERE. These can't be compared to a literal. The statements still go through the app's generator, so the row-count verifier refuses a save that matches more than one row. Keyless deletes with excluded columns run one row per statement, so a batched OR cannot widen past what each row matched. A delete the generator cannot identify is refused instead of dropped. A row inserted with every column on its default is written as INSERT ... (col) VALUES (DEFAULT), since Databend rejects DEFAULT VALUES.
  • Copy Connection String on a Databend connection gives mysql://host:3307/..., port kept, instead of an empty :// scheme.
  • Docker Compose scanning recognises the databend-query image of a split deployment.
  • TiDB joins the MySQL type family, foreign key actions and default expressions. It has no Server Dashboard (the metrics come back empty), no native backup (mysqldump --single-transaction fails), no Create Table engine or charset pickers, and no MySQL-to-TiDB Compare scripts. Users & Roles hides the connection limit, which TiDB ignores.
  • Hand-listed engine checks now ask the owner of each question. Driver facts go through pluginTypeId, MySQL SQL-dialect facts through SqlDialect.from == .mysql (which drops Databend), and type spelling through SQLTypeFamily.
  • Merged with main. The importers keep main's registry-backed ForeignAppDatabaseType.resolve. The fictional TablePlus TiDB fixture and the unverified Navicat TIDB fixture are gone.
  • databend:// is Databend's HTTP connection string, so it stays unsupported. tidb:// resolves through the registry.
  • iOS gets TiDB, not Databend: its driver has no Databend path.

Verification

  • Reviews: security, app-side and driver-correctness reviewers ran over the rewrite (Codex was unavailable on a usage limit). Every confirmed finding is fixed above; the driver reviewer measured each claim against the live servers.
  • Driver harness running the real plugin sources against live servers:
    • Connect, flavor, system schemas, table list, columns, indexes and CHECK constraints.
    • Users, parameterized update with rollback, and row cap.
    • Stop on SLEEP(8) or a long scan.
    • Targets: TiDB 7.5 (as TiDB and as MySQL), TiDB 8.5, MariaDB 11.4 with and without TLS, and Databend. Stop now ends the query in about 1s on every one. Before the kill fix it took the full 8s on TiDB.
    • A 5,000-row read under a 10-row cap inside an open transaction, then another statement in the same transaction: passes on TiDB 7.5, 8.5, TiDB-as-MySQL and MariaDB.
    • TiDB CHECK constraints, including names with commas and backticks and a literal holding ,(, '' and \\: listed on 7.5 and 8.5 with the same text as CHECK_CLAUSE.
  • Unit tests: 856 cases across 57 touched suites pass. New suites cover MySQLServerFlavor, Databend literal inlining (including break-out payloads), identifier quoting, result shape, catalog SQL, TiDB check-constraint parsing, variant support decisions, and keyless row-match exclusion.
  • App build passes. All plugin targets compile except OracleDriver: the third-party oracle-nio fails under the local Xcode 27 beta toolchain with a @TaskLocal macro error. The same error blocks the iOS app target locally, so CI is the iOS compile check. The widget extension compiled.
  • swiftlint --strict is clean on the touched paths. The docs source check and house-style checks pass.
  • UI automation is not added: the flows need live TiDB and Databend servers, which the UI test runner does not have.

Not in this PR

  • A Databend HTTP (/v1/query) plugin.
  • A TiDB plan-tree parser (tidb_json).
  • A stable default order for unsorted paging on engines without a stable scan order. TiDB is measured to repeat and skip rows; the docs say to sort by a column.
  • A cluster-aware TiDB dashboard.
  • Stop behind a proxy when Global Kill is off.
  • Stop pressed just as a TiDB statement finishes can still close the session, since the kill then reaches an idle session. No client-side check closes that window; the TiDB page documents it.
  • Swift Bool parameters are sent as 1/0, which Databend refuses for a BOOLEAN column. The grid sends true/false text and is not affected.
  • The Databend SQL editor splits statements with the generic lexer, which does not treat backslash as an escape. Fixing it needs a new SqlDialect case, which is a PluginKit change; the docs say to write ''.

https://claude.ai/code/session_01EtSNyG8StyXn92NLdMFzsV

Co-authored-by: Nguyễn Nam Long <J2TeamNNL@users.noreply.github.com>
This was referenced Sep 10, 2026
@datlechin
datlechin merged commit 017775d into TableProApp:main Sep 11, 2026
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.

TiDB support feat: support databend and tidb

3 participants