fix(oceanbase): preserve partition DDL and toggle storage options
This commit is contained in:
1 parent
8eba12ea2f
commit
fe0dd02f2b
36 files changed
+870
-125
No files matched your search
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user