mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-02 02:34:42 +08:00
fix(sql): preserve Doris complex type field separators
This commit is contained in:
@@ -167,6 +167,39 @@ describe("useSqlExecution", () => {
|
||||
expect(executedSql).toContain("where fp.create_at < @date_start");
|
||||
});
|
||||
|
||||
it("sends Doris STRUCT DDL unchanged without opening the parameter dialog", async () => {
|
||||
const sql = `
|
||||
create table \`events\` (
|
||||
\`field0\` int not null comment 'field 0',
|
||||
\`field_list\` array<struct<field1:smallint, field2:int, field3:decimal(16,5), field4:varchar(255)>> comment 'field list'
|
||||
)
|
||||
engine = olap
|
||||
properties ("replication_num" = "1");
|
||||
`;
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("analytics"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("doris"));
|
||||
const activeOutputView = ref<"result" | "summary" | "explain" | "chart">("result");
|
||||
const queryStore = useQueryStore();
|
||||
const executeCurrentSql = vi.spyOn(queryStore, "executeCurrentSql").mockImplementation(async () => {
|
||||
if (activeTab.value) activeTab.value.result = { columns: ["ok"], rows: [[1]], affected_rows: 0, execution_time_ms: 1 };
|
||||
});
|
||||
vi.spyOn(useHistoryStore(), "add").mockResolvedValue(undefined);
|
||||
vi.spyOn(useConnectionStore(), "refreshObjectListTreeNode").mockResolvedValue(undefined);
|
||||
|
||||
const execution = useSqlExecution({
|
||||
activeTab: computed(() => activeTab.value),
|
||||
activeConnection: computed(() => activeConnection.value),
|
||||
executableSql: computed(() => sql),
|
||||
activeOutputView,
|
||||
});
|
||||
|
||||
await execution.tryExecute();
|
||||
|
||||
expect(execution.showSqlParameterDialog.value).toBe(false);
|
||||
expect(execution.sqlParameterNames.value).toEqual([]);
|
||||
expect(executeCurrentSql).toHaveBeenCalledWith(sql, {});
|
||||
});
|
||||
|
||||
it("records a later MySQL batch error and skips metadata refresh", async () => {
|
||||
const activeTab = ref<QueryTab | undefined>(queryTab("app"));
|
||||
const activeConnection = ref<ConnectionConfig | undefined>(connection("mysql"));
|
||||
|
||||
@@ -177,6 +177,93 @@ describe("extractSqlParameters", () => {
|
||||
expect(extractSqlParameters(sql)).toEqual(["actual_value"]);
|
||||
});
|
||||
|
||||
it("ignores Doris STRUCT field type separators", () => {
|
||||
const sql = `
|
||||
create table \`events\` (
|
||||
\`field0\` int not null comment 'field 0',
|
||||
\`field_list\` array<struct<field1:smallint, field2:int, field3:decimal(16,5), field4:varchar(255)>> comment 'field list'
|
||||
)
|
||||
engine = olap
|
||||
properties ("replication_num" = "1");
|
||||
`;
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "doris" })).toEqual([]);
|
||||
// SelectDB connections use the MySQL db type with a SelectDB driver profile.
|
||||
expect(extractSqlParameters(sql, { databaseType: "mysql" })).toEqual([]);
|
||||
expect(substituteSqlParameters(sql, {}, { databaseType: "doris" })).toBe(sql);
|
||||
});
|
||||
|
||||
it("keeps named parameters that are not STRUCT field type separators", () => {
|
||||
const sql = `
|
||||
create table \`events\` (
|
||||
\`field_list\` array<struct<
|
||||
field1:smallint,
|
||||
nested:struct<\`field2\` /* field type */ :decimal(:precision, :scale)>
|
||||
>>
|
||||
) properties ("buckets" = :bucket_count);
|
||||
`;
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "doris" })).toEqual(["precision", "scale", "bucket_count"]);
|
||||
expect(
|
||||
substituteSqlParameters(
|
||||
sql,
|
||||
{
|
||||
precision: { kind: "number", value: "16" },
|
||||
scale: { kind: "number", value: "5" },
|
||||
bucket_count: { kind: "number", value: "8" },
|
||||
},
|
||||
{ databaseType: "doris" },
|
||||
),
|
||||
).toBe(`
|
||||
create table \`events\` (
|
||||
\`field_list\` array<struct<
|
||||
field1:smallint,
|
||||
nested:struct<\`field2\` /* field type */ :decimal(16, 5)>
|
||||
>>
|
||||
) properties ("buckets" = 8);
|
||||
`);
|
||||
});
|
||||
|
||||
it("does not let an unterminated complex type hide a later named parameter", () => {
|
||||
const sql = "create table `broken` (value struct<field:int,\nselect :real;";
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "doris" })).toEqual(["real"]);
|
||||
expect(substituteSqlParameters(sql, { real: { kind: "number", value: "7" } }, { databaseType: "doris" })).toBe("create table `broken` (value struct<field:int,\nselect 7;");
|
||||
});
|
||||
|
||||
it("ignores Doris VARIANT field type separators", () => {
|
||||
const sql = `
|
||||
create table \`events\` (
|
||||
value variant<
|
||||
match_name 'path_1':decimal(:precision, :scale),
|
||||
match_name_glob 'meta*':bigint,
|
||||
properties('variant_max_subcolumns_count' = :property_value)
|
||||
>
|
||||
);
|
||||
`;
|
||||
|
||||
expect(extractSqlParameters(sql, { databaseType: "doris" })).toEqual(["precision", "scale", "property_value"]);
|
||||
expect(
|
||||
substituteSqlParameters(
|
||||
sql,
|
||||
{
|
||||
precision: { kind: "number", value: "16" },
|
||||
scale: { kind: "number", value: "5" },
|
||||
property_value: { kind: "string", value: "2048" },
|
||||
},
|
||||
{ databaseType: "doris" },
|
||||
),
|
||||
).toBe(`
|
||||
create table \`events\` (
|
||||
value variant<
|
||||
match_name 'path_1':decimal(16, 5),
|
||||
match_name_glob 'meta*':bigint,
|
||||
properties('variant_max_subcolumns_count' = '2048')
|
||||
>
|
||||
);
|
||||
`);
|
||||
});
|
||||
|
||||
it("ignores HANA SQLScript variable references", () => {
|
||||
const sql = "DO BEGIN Dummy1 = SELECT 1 FROM DUMMY; SELECT * FROM :Dummy1; END";
|
||||
expect(extractSqlParameters(sql, { databaseType: "saphana" })).toEqual([]);
|
||||
|
||||
@@ -21,6 +21,8 @@ interface ParameterOccurrence extends SqlParameterDescriptor {
|
||||
end: number;
|
||||
}
|
||||
|
||||
type ComplexTypeDeclarationKind = "struct" | "variant";
|
||||
|
||||
export interface SqlParameterOptions {
|
||||
databaseType?: DatabaseType;
|
||||
// Which placeholder syntaxes are recognized. Undefined enables all of them.
|
||||
@@ -82,6 +84,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
|
||||
const supportsNamedParameters = options?.databaseType !== "saphana";
|
||||
const enabledSyntaxes = options?.enabledSyntaxes ? new Set(options.enabledSyntaxes) : null;
|
||||
const isSyntaxEnabled = (syntax: SqlParameterSyntax) => !enabledSyntaxes || enabledSyntaxes.has(syntax);
|
||||
const complexTypeFieldSeparators = supportsNamedParameters && isSyntaxEnabled("named") ? collectComplexTypeFieldSeparators(sql) : new Set<number>();
|
||||
let i = 0;
|
||||
let dollarQuoteEnd = "";
|
||||
let positionalIndex = 0;
|
||||
@@ -123,7 +126,7 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
|
||||
}
|
||||
if (ch === ":" && supportsNamedParameters && isSyntaxEnabled("named")) {
|
||||
const name = readParameterName(sql, i + 1);
|
||||
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=") {
|
||||
if (name && sql[i - 1] !== ":" && sql[i + 1] !== "=" && !complexTypeFieldSeparators.has(i)) {
|
||||
occurrences.push({
|
||||
key: name,
|
||||
name,
|
||||
@@ -191,6 +194,198 @@ function findSqlParameterOccurrences(sql: string, options?: SqlParameterOptions)
|
||||
return occurrences;
|
||||
}
|
||||
|
||||
// Doris-style complex types use colons between field names and types; those are not bind parameters.
|
||||
function collectComplexTypeFieldSeparators(sql: string): Set<number> {
|
||||
const separators = new Set<number>();
|
||||
let i = 0;
|
||||
let dollarQuoteEnd = "";
|
||||
|
||||
while (i < sql.length) {
|
||||
if (dollarQuoteEnd) {
|
||||
const end = sql.indexOf(dollarQuoteEnd, i);
|
||||
if (end === -1) break;
|
||||
i = end + dollarQuoteEnd.length;
|
||||
dollarQuoteEnd = "";
|
||||
continue;
|
||||
}
|
||||
|
||||
const ch = sql[i];
|
||||
const next = sql[i + 1];
|
||||
if (ch === "'" || ch === '"' || ch === "`") {
|
||||
i = skipQuoted(sql, i, ch);
|
||||
continue;
|
||||
}
|
||||
if (ch === "[") {
|
||||
i = skipBracketIdentifier(sql, i);
|
||||
continue;
|
||||
}
|
||||
if (ch === "-" && next === "-") {
|
||||
i = skipLine(sql, i + 2);
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
i = skipBlockComment(sql, i + 2);
|
||||
continue;
|
||||
}
|
||||
if (isHashLineComment(sql, i)) {
|
||||
i = skipLine(sql, i + 1);
|
||||
continue;
|
||||
}
|
||||
if (ch === "$") {
|
||||
const marker = readDollarQuoteMarker(sql, i);
|
||||
if (marker) {
|
||||
dollarQuoteEnd = marker;
|
||||
i += marker.length;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const declaration = readComplexTypeDeclaration(sql, i);
|
||||
if (declaration) {
|
||||
i = collectComplexTypeFieldSeparatorsInDeclaration(sql, declaration.openingBracket + 1, declaration.kind, separators) + 1;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return separators;
|
||||
}
|
||||
|
||||
function collectComplexTypeFieldSeparatorsInDeclaration(sql: string, start: number, kind: ComplexTypeDeclarationKind, separators: Set<number>): number {
|
||||
let i = start;
|
||||
let genericDepth = 0;
|
||||
let parenthesisDepth = 0;
|
||||
let expectsFieldName = true;
|
||||
|
||||
while (i < sql.length) {
|
||||
if (expectsFieldName && genericDepth === 0 && parenthesisDepth === 0) {
|
||||
const fieldStart = skipSqlWhitespaceAndComments(sql, i);
|
||||
if (fieldStart !== i) {
|
||||
i = fieldStart;
|
||||
continue;
|
||||
}
|
||||
if (isLineStatementStart(sql, i) && isSqlStatementKeyword(sql, i)) return i;
|
||||
const fieldNameEnd = readComplexTypeFieldNameEnd(sql, i, kind);
|
||||
if (fieldNameEnd > i) {
|
||||
const separator = skipSqlWhitespaceAndComments(sql, fieldNameEnd);
|
||||
if (sql[separator] === ":") {
|
||||
separators.add(separator);
|
||||
i = separator + 1;
|
||||
expectsFieldName = false;
|
||||
continue;
|
||||
}
|
||||
i = fieldNameEnd;
|
||||
expectsFieldName = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const ch = sql[i];
|
||||
const next = sql[i + 1];
|
||||
if (ch === "'" || ch === '"' || ch === "`") {
|
||||
i = skipQuoted(sql, i, ch);
|
||||
continue;
|
||||
}
|
||||
if (ch === "[") {
|
||||
i = skipBracketIdentifier(sql, i);
|
||||
continue;
|
||||
}
|
||||
if (ch === "-" && next === "-") {
|
||||
i = skipLine(sql, i + 2);
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
i = skipBlockComment(sql, i + 2);
|
||||
continue;
|
||||
}
|
||||
if (isHashLineComment(sql, i)) {
|
||||
i = skipLine(sql, i + 1);
|
||||
continue;
|
||||
}
|
||||
const declaration = readComplexTypeDeclaration(sql, i);
|
||||
if (declaration) {
|
||||
i = collectComplexTypeFieldSeparatorsInDeclaration(sql, declaration.openingBracket + 1, declaration.kind, separators) + 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === ";" && genericDepth === 0 && parenthesisDepth === 0) return i;
|
||||
if (ch === "<") {
|
||||
genericDepth += 1;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === ">") {
|
||||
if (genericDepth === 0 && parenthesisDepth === 0) return i;
|
||||
if (genericDepth > 0) genericDepth -= 1;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "(") {
|
||||
parenthesisDepth += 1;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === ")") {
|
||||
if (parenthesisDepth > 0) parenthesisDepth -= 1;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "," && genericDepth === 0 && parenthesisDepth === 0) {
|
||||
expectsFieldName = true;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return sql.length;
|
||||
}
|
||||
|
||||
function readComplexTypeDeclaration(sql: string, start: number): { kind: ComplexTypeDeclarationKind; openingBracket: number } | null {
|
||||
const kind: ComplexTypeDeclarationKind | null = matchesWord(sql, start, "struct") ? "struct" : matchesWord(sql, start, "variant") ? "variant" : null;
|
||||
if (!kind) return null;
|
||||
|
||||
const openingBracket = skipSqlWhitespaceAndComments(sql, start + kind.length);
|
||||
return sql[openingBracket] === "<" ? { kind, openingBracket } : null;
|
||||
}
|
||||
|
||||
function readComplexTypeFieldNameEnd(sql: string, start: number, kind: ComplexTypeDeclarationKind): number {
|
||||
if (kind === "variant") return readVariantFieldNameEnd(sql, start);
|
||||
|
||||
const ch = sql[start];
|
||||
if (ch === '"' || ch === "`") return skipQuoted(sql, start, ch);
|
||||
if (ch === "[") return skipBracketIdentifier(sql, start);
|
||||
if (!PARAMETER_NAME_START_RE.test(ch ?? "")) return start;
|
||||
|
||||
let i = start + 1;
|
||||
while (i < sql.length && PARAMETER_NAME_CHAR_RE.test(sql[i])) i += 1;
|
||||
return i;
|
||||
}
|
||||
|
||||
function readVariantFieldNameEnd(sql: string, start: number): number {
|
||||
let i = start;
|
||||
const modifier = matchesWord(sql, i, "match_name") ? "match_name" : matchesWord(sql, i, "match_name_glob") ? "match_name_glob" : "";
|
||||
if (modifier) i = skipSqlWhitespaceAndComments(sql, i + modifier.length);
|
||||
return sql[i] === "'" ? skipQuoted(sql, i, "'") : start;
|
||||
}
|
||||
|
||||
function skipSqlWhitespaceAndComments(sql: string, start: number): number {
|
||||
let i = start;
|
||||
while (i < sql.length) {
|
||||
while (i < sql.length && /\s/.test(sql[i])) i += 1;
|
||||
if (sql[i] === "-" && sql[i + 1] === "-") {
|
||||
i = skipLine(sql, i + 2);
|
||||
continue;
|
||||
}
|
||||
if (sql[i] === "/" && sql[i + 1] === "*") {
|
||||
i = skipBlockComment(sql, i + 2);
|
||||
continue;
|
||||
}
|
||||
if (isHashLineComment(sql, i)) {
|
||||
i = skipLine(sql, i + 1);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
function collectNativeSqlServerParameters(sql: string): { declared: Set<string>; ignoredStarts: Set<number> } {
|
||||
const declared = new Set<string>();
|
||||
const ignoredStarts = new Set<number>();
|
||||
|
||||
Reference in New Issue
Block a user