diff --git a/.changeset/fresh-mice-introspect.md b/.changeset/fresh-mice-introspect.md new file mode 100644 index 00000000..d6a27ed9 --- /dev/null +++ b/.changeset/fresh-mice-introspect.md @@ -0,0 +1,5 @@ +--- +"@prisma/studio-core": patch +--- + +Fix MySQL introspection when JSON aggregated columns are returned as strings. diff --git a/data/mysql-core/adapter.ts b/data/mysql-core/adapter.ts index 2cdcf4b0..a24d9185 100644 --- a/data/mysql-core/adapter.ts +++ b/data/mysql-core/adapter.ts @@ -1,6 +1,5 @@ import { type Adapter, - type AdapterUpdateDetails, type AdapterDeleteResult, type AdapterError, type AdapterInsertResult, @@ -10,6 +9,7 @@ import { type AdapterRequirements, type AdapterSqlLintResult, type AdapterSqlSchemaResult, + type AdapterUpdateDetails, type AdapterUpdateManyResult, type AdapterUpdateResult, type Column, @@ -599,7 +599,7 @@ function createIntrospection(args: { const { schemas } = result; const { columns, name: tableName, schema } = table; - const columnsRecord = columns + const columnsRecord = normalizeColumns(columns) .sort((a, b) => a.position - b.position) .reduce( (columns, column) => { @@ -683,6 +683,26 @@ function createIntrospection(args: { ); } +function normalizeColumns( + columns: QueryResult[number]["columns"], +): QueryResult[number]["columns"] { + if (Array.isArray(columns)) { + return columns; + } + + if (typeof columns === "string") { + const parsedColumns: unknown = JSON.parse(columns); + + if (Array.isArray(parsedColumns)) { + return parsedColumns as QueryResult< + typeof getTablesQuery + >[number]["columns"]; + } + } + + throw new TypeError("Expected MySQL introspection columns to be an array"); +} + const filterOperators = [ "=", "!=", diff --git a/data/mysql-core/introspection-hardening.test.ts b/data/mysql-core/introspection-hardening.test.ts index 947612eb..db35a00c 100644 --- a/data/mysql-core/introspection-hardening.test.ts +++ b/data/mysql-core/introspection-hardening.test.ts @@ -5,6 +5,32 @@ import { createMySQLAdapter } from "./adapter"; import { mockTablesQuery } from "./introspection"; describe("mysql-core introspection hardening", () => { + it("parses JSON string columns returned by MariaDB introspection", async () => { + const tables = mockTablesQuery().map((table) => ({ + ...table, + columns: JSON.stringify(table.columns), + })); + const execute: SequenceExecutor["execute"] = (query) => { + if (query.sql.toLowerCase().includes("timezone")) { + return Promise.resolve([null, [{ timezone: "UTC" }]] as never); + } + + return Promise.resolve([null, tables] as never); + }; + const executor: SequenceExecutor = { + execute, + executeSequence: vi.fn() as SequenceExecutor["executeSequence"], + }; + const adapter = createMySQLAdapter({ executor }); + + const [error, result] = await adapter.introspect({}); + + expect(error).toBeNull(); + expect( + result?.schemas.studio?.tables.animals?.columns.id?.isAutoincrement, + ).toBe(true); + }); + it("falls back to UTC when timezone introspection fails", async () => { const execute: SequenceExecutor["execute"] = (query) => { if (query.sql.toLowerCase().includes("timezone")) {