Skip to content

Commit df3f210

Browse files
sorenbsclaude
andauthored
fix: MySQL introspection failing on MariaDB servers (#1547)
* Fix MySQL introspection failing on MariaDB servers Studio's tables introspection aggregated column metadata with json_arrayagg, which does not exist before MariaDB 10.5 (#1511) and returns LONGTEXT rather than native JSON on any MariaDB version, while cast(... as json) is invalid syntax there (#1367). The MySQL adapter now detects the server flavor once per adapter via select version() and, on MariaDB, aggregates columns with coalesce(concat('[', group_concat(json_object(...) separator ','), ']'), '[]') instead. String-aggregated columns payloads are parsed back into arrays on the client, which also hardens introspection against MySQL transports that return json_arrayagg results as strings. Version detection failures fall back to the existing MySQL SQL. Verified end-to-end against MariaDB 10.4 and 10.11 containers and the Vitess-backed MySQL integration suite. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make MariaDB introspection independent of group_concat_max_len Addresses PR review feedback: the group_concat-based aggregation was capped by the session's group_concat_max_len (1MB default on MariaDB, sometimes configured much lower), which could silently truncate the JSON payload on very wide tables. The MariaDB flavor now uses a dedicated tables query (getMariaDBTablesQuery) that returns one row per column with no JSON functions and no aggregation at all, and groupMariaDBTablesQueryResult groups the rows into the aggregated shape on the client. This is immune to any aggregation size cap and works on every MariaDB version. The MySQL tables query is restored to its original json_arrayagg form. Includes a regression test grouping over 1MB of column metadata, and was verified against MariaDB 10.4/10.11 containers including with group_concat_max_len lowered to 128 bytes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 3fec72a commit df3f210

5 files changed

Lines changed: 643 additions & 8 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@prisma/studio-core": patch
3+
---
4+
5+
Fix MySQL introspection failing on MariaDB. The adapter now detects MariaDB via `select version()` and uses a dedicated tables query that returns one row per column and groups the result on the client, avoiding `json_arrayagg` (missing before MariaDB 10.5), JSON casts (invalid syntax on MariaDB), and any server-side string aggregation that would be truncated at `group_concat_max_len`. Introspection also parses string-encoded `json_arrayagg` payloads returned by some MySQL transports.

FEATURES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ A standalone `sslmode` without SSL file parameters is left in the connection str
1616
Studio introspects connected databases to build schemas, tables, columns, relationships, filter operators, and timezone metadata.
1717
This gives users an accurate live model of the database and keeps table navigation grounded in current structure.
1818
A fresh Studio mount performs this discovery once, while actual adapter or database-availability changes invalidate cached metadata and load it again.
19+
The MySQL adapter detects MariaDB servers via `select version()` and switches to a MariaDB-compatible tables query that returns one row per column and groups the result on the client, so introspection works on all supported MariaDB versions where `json_arrayagg` or JSON casts are unavailable and cannot be truncated by `group_concat_max_len`.
1920

2021
## Deployable Prisma Postgres Demo
2122

data/mysql-core/adapter.ts

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,16 @@ import {
4040
getUpdateRefetchQuery,
4141
} from "./dml";
4242
import {
43+
detectMySQLServerFlavor,
44+
getMariaDBTablesQuery,
45+
getServerVersionQuery,
4346
getTablesQuery,
4447
getTimezoneQuery,
48+
groupMariaDBTablesQueryResult,
4549
mockTablesQuery,
4650
mockTimezoneQuery,
51+
type MySQLServerFlavor,
52+
normalizeTablesQueryResult,
4753
} from "./introspection";
4854
import { lintMySQLWithExplainFallback } from "./sql-lint";
4955

@@ -148,18 +154,82 @@ export function createMySQLAdapter(
148154
}
149155
}
150156

157+
let cachedServerFlavor: MySQLServerFlavor | null = null;
158+
159+
async function detectServerFlavor(
160+
options: Parameters<Adapter["introspect"]>[0],
161+
): Promise<MySQLServerFlavor> {
162+
if (cachedServerFlavor) {
163+
return cachedServerFlavor;
164+
}
165+
166+
try {
167+
const [error, versions] = await executor.execute(
168+
getServerVersionQuery(otherRequirements),
169+
options,
170+
);
171+
172+
if (error) {
173+
// fall back to the MySQL introspection SQL without caching, so the
174+
// next introspection retries the detection.
175+
return "mysql";
176+
}
177+
178+
cachedServerFlavor = detectMySQLServerFlavor(versions[0]?.version);
179+
180+
return cachedServerFlavor;
181+
} catch {
182+
return "mysql";
183+
}
184+
}
185+
186+
/**
187+
* Fetches table metadata using the tables query that matches the server
188+
* flavor. MariaDB gets a one-row-per-column query grouped on the client, so
189+
* results can never be truncated by `group_concat_max_len`.
190+
*/
191+
async function fetchTables(
192+
serverFlavor: MySQLServerFlavor,
193+
options: Parameters<Adapter["introspect"]>[0],
194+
): Promise<{
195+
query: Query;
196+
result: Either<Error, QueryResult<typeof getTablesQuery>>;
197+
}> {
198+
if (serverFlavor === "mariadb") {
199+
const query = getMariaDBTablesQuery(otherRequirements);
200+
const [error, rows] = await executor.execute(query, options);
201+
202+
return {
203+
query,
204+
result: error ? [error] : [null, groupMariaDBTablesQueryResult(rows)],
205+
};
206+
}
207+
208+
const query = getTablesQuery(otherRequirements);
209+
const [error, rows] = await executor.execute(query, options);
210+
211+
return {
212+
query,
213+
result: error ? [error] : [null, normalizeTablesQueryResult(rows)],
214+
};
215+
}
216+
151217
async function introspectDatabase(
152218
options: Parameters<Adapter["introspect"]>[0],
153219
): Promise<Either<AdapterError, AdapterIntrospectResult>> {
154220
try {
155-
const tablesQuery = getTablesQuery(otherRequirements);
221+
const serverFlavor = await detectServerFlavor(options);
156222
const timezoneQuery = getTimezoneQuery(otherRequirements);
157223

158-
const [[tablesError, tables], [timezoneError, timezones]] =
159-
await Promise.all([
160-
executor.execute(tablesQuery, options),
161-
executor.execute(timezoneQuery, options),
162-
]);
224+
const [tablesFetch, [timezoneError, timezones]] = await Promise.all([
225+
fetchTables(serverFlavor, options),
226+
executor.execute(timezoneQuery, options),
227+
]);
228+
229+
const {
230+
query: tablesQuery,
231+
result: [tablesError, tables],
232+
} = tablesFetch;
163233

164234
if (tablesError) {
165235
return createMySQLAdapterError({

0 commit comments

Comments
 (0)