fix(generate): emit JSON literals for JSON columns
This commit is contained in:
3 files changed
+70
-2
No files matched your search
@@ -607,8 +607,11 @@ function sqlStatementsForTable(r: GeneratedTableResult): string[] {
|
|||||||
stmts.push(...r.statements);
|
stmts.push(...r.statements);
|
||||||
} else {
|
} else {
|
||||||
const colList = r.columns.map((c) => quoteTableIdentifier(dbType.value, c)).join(", ");
|
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) {
|
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});`);
|
stmts.push(`INSERT INTO ${targetTable} (${colList}) VALUES (${vals});`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1758,9 +1758,37 @@ export function formatGeneratedValue(value: unknown, databaseType?: DatabaseType
|
|||||||
const temporalValue = formatOracleTemporalValue(stringValue, dataType);
|
const temporalValue = formatOracleTemporalValue(stringValue, dataType);
|
||||||
if (temporalValue) return temporalValue;
|
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);
|
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 {
|
export function displayGeneratedValue(value: unknown): string {
|
||||||
if (isGeneratedSqlExpression(value)) return value.display;
|
if (isGeneratedSqlExpression(value)) return value.display;
|
||||||
if (value === null || value === undefined) return "NULL";
|
if (value === null || value === undefined) return "NULL";
|
||||||
|
|||||||
+38
-1
@@ -1,6 +1,6 @@
|
|||||||
import { strict as assert } from "node:assert";
|
import { strict as assert } from "node:assert";
|
||||||
import { test, vi } from "vitest";
|
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", () => {
|
test("recognizes Oracle-compatible NUMBER column types as numeric", () => {
|
||||||
assert.equal(findGeneratorKey("value", "NUMBER"), "number");
|
assert.equal(findGeneratorKey("value", "NUMBER"), "number");
|
||||||
@@ -423,3 +423,40 @@ test("keeps ordinary TDengine table generation unchanged", () => {
|
|||||||
assert.deepEqual(result.rows, [[1]]);
|
assert.deepEqual(result.rows, [[1]]);
|
||||||
assert.doesNotMatch(result.sql, /tbname/);
|
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'`);
|
||||||
|
});
|
||||||
Reference in new issue
Block a user