fix(oceanbase): preserve partition DDL and toggle storage options

This commit is contained in:
efeire authored and GitHub committed 2026-09-18 01:59:42 +08:00
1 parent 8eba12ea2f
commit fe0dd02f2b
36 files changed
+870 -125

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { applyDdlStoragePreference } from "../ddlStorage";
const raw = `CREATE TABLE "APP"."SALES" (
"ID" NUMBER NOT NULL,
"REGION" VARCHAR2(30) DEFAULT 'PCTFREE = 20',
CONSTRAINT "CK" CHECK ("ID" > 0)
) COMPRESS FOR ARCHIVE REPLICA_NUM = 1 BLOCK_SIZE = 16384 USE_BLOOM_FILTER = FALSE TABLET_SIZE = 134217728 PCTFREE = 0
partition by range("ID") subpartition by list("REGION")
(partition "P1" values less than (100) (subpartition "S1" values ('PCTFREE = 20')));
CREATE INDEX "I" ON "APP"."SALES"("REGION") LOCAL;
COMMENT ON TABLE "APP"."SALES" IS 'COMPRESS FOR ARCHIVE';`;
describe("OceanBase Oracle DDL storage preference", () => {
it("omits table storage by default and preserves the semantic DDL and comments", () => {
const logical = applyDdlStoragePreference(raw, "oceanbase-oracle");
expect(logical).not.toContain("REPLICA_NUM");
expect(logical).not.toContain("BLOCK_SIZE");
expect(logical).not.toContain("TABLET_SIZE");
expect(logical).not.toContain("PCTFREE = 0");
expect(logical).toContain("DEFAULT 'PCTFREE = 20'");
expect(logical.slice(logical.indexOf("partition by"))).toBe(raw.slice(raw.indexOf("partition by")));
expect(logical).toContain('CONSTRAINT "CK" CHECK ("ID" > 0)');
});
it("returns the complete original when disabled and leaves other databases unchanged", () => {
expect(applyDdlStoragePreference(raw, "oceanbase-oracle", false)).toBe(raw);
expect(applyDdlStoragePreference(raw, "oracle")).toBe(raw);
expect(applyDdlStoragePreference(raw, undefined)).toBe(raw);
});
it("preserves quoted identifiers, Oracle alternative strings, temporary-table semantics and comments", () => {
const ddl = `CREATE GLOBAL TEMPORARY TABLE "PCTFREE" ("A" VARCHAR2(100) DEFAULT q'[can't ) PCTFREE=42]') /* PCTFREE=9 */ PCTFREE=0 ON COMMIT PRESERVE ROWS;`;
expect(applyDdlStoragePreference(ddl, "oceanbase-oracle")).toBe(ddl.replace(" PCTFREE=0", " "));
});
it("filters each table without altering unknown attributes, non-table statements or incomplete SQL", () => {
const ddl = `CREATE TABLE T(A NUMBER) PCTFREE 0 TABLE_MODE='QUEUING'; CREATE TABLE U(A NUMBER) NOCOMPRESS PCTFREE=10; ALTER TABLE T PCTFREE 20;`;
expect(applyDdlStoragePreference(ddl, "oceanbase-oracle")).toBe(`CREATE TABLE T(A NUMBER) TABLE_MODE='QUEUING'; CREATE TABLE U(A NUMBER) ; ALTER TABLE T PCTFREE 20;`);
const broken = `CREATE TABLE T(A VARCHAR2(100) DEFAULT 'unclosed) PCTFREE=10`;
expect(applyDdlStoragePreference(broken, "oceanbase-oracle")).toBe(broken);
});
});
+51
View File
@@ -0,0 +1,51 @@
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
import type { DatabaseType } from "@/types/database";
/** Remove only known OceanBase table storage options, keeping the cached source intact. */
export function applyDdlStoragePreference(sql: string, databaseType: DatabaseType | undefined, excludeStorage = true): string {
if (databaseType !== "oceanbase-oracle" || !excludeStorage) return sql;
const tokens = tokenizeSqlSemantic(sql, "oracle");
if (tokens.some((token) => token.closed === false)) return sql;
const removals: Array<{ start: number; end: number }> = [];
let table = false;
let options = false;
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]!;
if (token.depth !== 0) continue;
if (token.text === ";") {
table = options = false;
continue;
}
if (token.kind === "word" && token.normalized === "create") {
const offset = tokens[i + 1]?.normalized === "global" && tokens[i + 2]?.normalized === "temporary" ? 3 : 1;
table = tokens[i + offset]?.normalized === "table";
options = false;
}
if (!table) continue;
if (token.text === ")") options = true;
if (!options || token.kind !== "word") continue;
// Partition expressions and bounds must never be treated as table options.
if (token.normalized === "partition") {
table = options = false;
continue;
}
let last = i;
if (token.normalized === "compress" && tokens[i + 1]?.normalized === "for" && tokens[i + 2]?.normalized === "archive") {
last = i + 2;
if (["high", "low"].includes(tokens[last + 1]?.normalized ?? "")) last++;
} else if (token.normalized === "nocompress") {
last = i;
} else if (["replica_num", "block_size", "tablet_size", "pctfree", "use_bloom_filter"].includes(token.normalized)) {
const valueIndex = tokens[i + 1]?.text === "=" ? i + 2 : i + 1;
const value = tokens[valueIndex];
const valid = token.normalized === "use_bloom_filter" ? value?.kind === "word" && ["true", "false"].includes(value.normalized) : value?.kind === "number" && /^\d+$/.test(value.text);
if (!valid) continue;
last = valueIndex;
} else continue;
removals.push({ start: token.span.start, end: tokens[last]!.span.end });
i = last;
}
let result = sql;
for (const { start, end } of removals.reverse()) result = result.slice(0, start) + result.slice(end);
return result;
}
@@ -149,6 +149,15 @@ export function tokenizeSqlSemantic(input: string, dialectId = "mysql", options?
continue;
}
if (dialectId === "oracle" && (ch === "q" || ch === "Q") && next === "'" && input[index + 2]) {
const opener = input[index + 2]!;
const closer = ({ "[": "]", "{": "}", "(": ")", "<": ">" } as Record<string, string>)[opener] ?? opener;
const end = input.indexOf(closer + "'", index + 3);
index = end < 0 ? input.length : end + 2;
tokens.push(token("string", input.slice(start, index), start, index, depth, "q'", end >= 0));
continue;
}
if (ch === "'") {
const quoted = readQuotedString(input, start, "'", mysqlBackslashEscape);
index = quoted.end;