diff --git a/apps/desktop/src/components/generate/DataGenerateDialog.vue b/apps/desktop/src/components/generate/DataGenerateDialog.vue index 3979aa77b7..205eff296a 100644 --- a/apps/desktop/src/components/generate/DataGenerateDialog.vue +++ b/apps/desktop/src/components/generate/DataGenerateDialog.vue @@ -607,8 +607,11 @@ function sqlStatementsForTable(r: GeneratedTableResult): string[] { stmts.push(...r.statements); } else { const colList = r.columns.map((c) => quoteTableIdentifier(dbType.value, c)).join(", "); + // Match by column name: rows may carry a leading tbname (TDengine stable), + // so positional indexes into resolvedColumns would drift. + const dataTypeByName = new Map(r.resolvedColumns.map((c) => [c.columnName.toLowerCase(), c.dataType])); for (const row of r.rows) { - const vals = row.map((value) => formatGeneratedValue(value)).join(", "); + const vals = row.map((value, index) => formatGeneratedValue(value, dbType.value, dataTypeByName.get(r.columns[index]?.toLowerCase() ?? ""))).join(", "); stmts.push(`INSERT INTO ${targetTable} (${colList}) VALUES (${vals});`); } } diff --git a/apps/desktop/src/lib/dataGrid/dataGenerate.ts b/apps/desktop/src/lib/dataGrid/dataGenerate.ts index 8e8571d365..66777e3069 100644 --- a/apps/desktop/src/lib/dataGrid/dataGenerate.ts +++ b/apps/desktop/src/lib/dataGrid/dataGenerate.ts @@ -1758,9 +1758,37 @@ export function formatGeneratedValue(value: unknown, databaseType?: DatabaseType const temporalValue = formatOracleTemporalValue(stringValue, dataType); if (temporalValue) return temporalValue; } + if (isJsonType(dataType)) { + // MySQL (and MariaDB) reject any non-JSON text in a JSON column, so free + // text from a string generator must land as a JSON string scalar; values + // that are already valid JSON pass through untouched. + const jsonText = isWellFormedJson(stringValue) ? stringValue : JSON.stringify(stringValue); + // MySQL's lexer consumes backslash escapes inside '...' literals, so the + // JSON escape sequences must double their backslashes to survive the trip. + if (databaseType === "mysql") return quoteGeneratedString(jsonText.replace(/\\/g, "\\\\")); + return quoteGeneratedString(jsonText); + } return quoteGeneratedString(stringValue); } +function isJsonType(dataType: string | undefined): boolean { + return !!dataType && dataType.toLowerCase().includes("json"); +} + +function isWellFormedJson(value: string): boolean { + // Cheap pre-filter: plain prose (the common case from string generators) + // can't be JSON, so skip the parse for it. + if (!/^".*"$/.test(value) && !/^\{.*\}$/.test(value) && !/^\[.*\]$/.test(value) && !/^(true|false|null|-?\d+(\.\d+)?([eE][+-]?\d+)?)$/.test(value)) { + return false; + } + try { + JSON.parse(value); + return true; + } catch { + return false; + } +} + export function displayGeneratedValue(value: unknown): string { if (isGeneratedSqlExpression(value)) return value.display; if (value === null || value === undefined) return "NULL"; diff --git a/packages/app-tests/dataGenerate.test.ts b/packages/app-tests/dataGenerate.test.ts index 3438a4ff44..1f735aec5b 100644 --- a/packages/app-tests/dataGenerate.test.ts +++ b/packages/app-tests/dataGenerate.test.ts @@ -1,6 +1,6 @@ import { strict as assert } from "node:assert"; import { test, vi } from "vitest"; -import { defaultGeneratorParams, displayGeneratedValue, findGeneratorKey, generateTableData, generateValue, supportsGeneratedMultiRowValues } from "../../apps/desktop/src/lib/dataGrid/dataGenerate.ts"; +import { defaultGeneratorParams, displayGeneratedValue, findGeneratorKey, formatGeneratedValue, generateTableData, generateValue, supportsGeneratedMultiRowValues } from "../../apps/desktop/src/lib/dataGrid/dataGenerate.ts"; test("recognizes Oracle-compatible NUMBER column types as numeric", () => { assert.equal(findGeneratorKey("value", "NUMBER"), "number"); @@ -423,3 +423,40 @@ test("keeps ordinary TDengine table generation unchanged", () => { assert.deepEqual(result.rows, [[1]]); assert.doesNotMatch(result.sql, /tbname/); }); + +test("formats generated values for MySQL JSON columns as JSON literals", () => { + const result = generateTableData( + { + tableName: "events", + schema: "app", + database: "app", + rowCount: 1, + columns: [ + { + columnName: "ext_json", + dataType: "json", + rowCount: 1, + generatorKey: "uuid", + }, + ], + }, + "mysql", + ); + + // The generated uuid must land as a JSON string scalar ("..."), not a bare + // SQL string — MySQL rejects the latter in a JSON column (#9011). + assert.match(result.sql, /VALUES\n\('"[0-9a-f-]{36}"'\);$/); +}); + +test("JSON-encodes generated strings for json columns but not for varchar", () => { + // Plain prose lands as a JSON string scalar (still a SQL string, so valid). + assert.equal(formatGeneratedValue("plain", "mysql", "json"), `'"plain"'`); + // Embedded double quotes are JSON-escaped; MySQL's lexer consumes '\' escapes + // inside '...' literals, so backslashes double to survive the roundtrip. + assert.equal(formatGeneratedValue('say "hi"', "mysql", "json"), `'"say \\\\"hi\\\\""'`); + // Already-valid JSON passes through with the same backslash doubling. + assert.equal(formatGeneratedValue('{"a":1}', "mysql", "json"), `'{"a":1}'`); + assert.equal(formatGeneratedValue('{"a":"b\\"c"}', "mysql", "json"), `'{"a":"b\\\\"c"}'`); + // Non-JSON columns keep plain string quoting. + assert.equal(formatGeneratedValue("plain", "mysql", "varchar(16)"), `'plain'`); +});