feat(oracle): apply DDL storage stripping to Oracle
This commit is contained in:
19 files changed
+414
-29
No files matched your search
+178
-2
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { applyDdlStoragePreference } from "../ddlStorage";
|
||||
import { applyDdlStoragePreference, supportsDdlStoragePreference } from "../ddlStorage";
|
||||
|
||||
const raw = `CREATE TABLE "APP"."SALES" (
|
||||
"ID" NUMBER NOT NULL,
|
||||
@@ -24,7 +24,7 @@ describe("OceanBase Oracle DDL storage preference", () => {
|
||||
});
|
||||
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, "mysql")).toBe(raw);
|
||||
expect(applyDdlStoragePreference(raw, undefined)).toBe(raw);
|
||||
});
|
||||
it("preserves quoted identifiers, Oracle alternative strings, temporary-table semantics and comments", () => {
|
||||
@@ -38,3 +38,179 @@ describe("OceanBase Oracle DDL storage preference", () => {
|
||||
expect(applyDdlStoragePreference(broken, "oceanbase-oracle")).toBe(broken);
|
||||
});
|
||||
});
|
||||
|
||||
// Verbatim `DBMS_METADATA.GET_DDL` output captured from Oracle Database 11g XE, with the
|
||||
// leading tabs of the column list written as two spaces.
|
||||
const oracleHeapTableDdl = `CREATE TABLE "DBX_TEST"."DBX_DDL_PROBE"
|
||||
( "ID" NUMBER(10,0) NOT NULL ENABLE,
|
||||
"NAME" VARCHAR2(50),
|
||||
"CREATED_AT" DATE,
|
||||
CONSTRAINT "DBX_DDL_PROBE_PK" PRIMARY KEY ("ID")
|
||||
USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS" ENABLE
|
||||
) SEGMENT CREATION IMMEDIATE
|
||||
PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS";
|
||||
|
||||
CREATE INDEX "DBX_TEST"."DBX_DDL_PROBE_NAME_IDX" ON "DBX_TEST"."DBX_DDL_PROBE" ("NAME")
|
||||
PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS";`;
|
||||
|
||||
const oracleHeapTableDdlWithoutStorage = `CREATE TABLE "DBX_TEST"."DBX_DDL_PROBE"
|
||||
( "ID" NUMBER(10,0) NOT NULL ENABLE,
|
||||
"NAME" VARCHAR2(50),
|
||||
"CREATED_AT" DATE,
|
||||
CONSTRAINT "DBX_DDL_PROBE_PK" PRIMARY KEY ("ID")
|
||||
USING INDEX ENABLE
|
||||
) NOCOMPRESS;
|
||||
|
||||
CREATE INDEX "DBX_TEST"."DBX_DDL_PROBE_NAME_IDX" ON "DBX_TEST"."DBX_DDL_PROBE" ("NAME");`;
|
||||
|
||||
const oracleIndexOrganizedTableDdl = `CREATE TABLE "DBX_TEST"."DBX_IOT"
|
||||
( "ID" NUMBER(10,0),
|
||||
"NAME" VARCHAR2(50),
|
||||
CONSTRAINT "DBX_IOT_PK" PRIMARY KEY ("ID") ENABLE
|
||||
) ORGANIZATION INDEX NOCOMPRESS PCTFREE 10 INITRANS 2 MAXTRANS 255 LOGGING
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS"
|
||||
PCTTHRESHOLD 50 INCLUDING "NAME" OVERFLOW
|
||||
PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 LOGGING
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS";`;
|
||||
|
||||
const oracleLobTableDdl = `CREATE TABLE "DBX_TEST"."DBX_LOB"
|
||||
( "ID" NUMBER(10,0) NOT NULL ENABLE,
|
||||
"DOC" CLOB,
|
||||
CONSTRAINT "DBX_LOB_PK" PRIMARY KEY ("ID")
|
||||
USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS" ENABLE
|
||||
) SEGMENT CREATION IMMEDIATE
|
||||
PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS"
|
||||
LOB ("DOC") STORE AS BASICFILE (
|
||||
TABLESPACE "USERS" ENABLE STORAGE IN ROW CHUNK 8192 PCTVERSION 10
|
||||
NOCACHE LOGGING
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT));`;
|
||||
|
||||
const oracleQuotedIdentifierDdl = `CREATE TABLE "DBX_TEST"."DBX_QUOTED"
|
||||
( "PCTFREE" NUMBER(5,0),
|
||||
"LOGGING" VARCHAR2(20),
|
||||
"STORAGE" VARCHAR2(30) DEFAULT 'keep STORAGE(INITIAL 65536)',
|
||||
"ID" NUMBER(10,0) NOT NULL ENABLE,
|
||||
CONSTRAINT "DBX_QUOTED_CK" CHECK (ID > 0) ENABLE,
|
||||
CONSTRAINT "DBX_QUOTED_PK" PRIMARY KEY ("ID")
|
||||
USING INDEX PCTFREE 5 INITRANS 2 MAXTRANS 255
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS" ENABLE
|
||||
) SEGMENT CREATION IMMEDIATE
|
||||
PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
|
||||
STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
|
||||
PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT FLASH_CACHE DEFAULT CELL_FLASH_CACHE DEFAULT)
|
||||
TABLESPACE "USERS";
|
||||
|
||||
COMMENT ON TABLE "DBX_TEST"."DBX_QUOTED" IS 'keep STORAGE(INITIAL 65536) text';`;
|
||||
|
||||
const oracleSequenceDdl = `
|
||||
CREATE SEQUENCE "DBX_TEST"."DBX_SEQ" MINVALUE 1 MAXVALUE 9999999999999999999999999999 INCREMENT BY 2 START WITH 5 CACHE 20 NOORDER NOCYCLE `;
|
||||
|
||||
const oracleViewSource = `SELECT ID, "PCTFREE" FROM DBX_QUOTED`;
|
||||
|
||||
describe("Oracle DDL storage preference", () => {
|
||||
it("drops the physical attributes of a heap table and of its index, keeping the rest verbatim", () => {
|
||||
expect(applyDdlStoragePreference(oracleHeapTableDdl, "oracle")).toBe(oracleHeapTableDdlWithoutStorage);
|
||||
});
|
||||
|
||||
it("drops index-organized-table storage without touching OVERFLOW, INCLUDING or NOCOMPRESS", () => {
|
||||
const logical = applyDdlStoragePreference(oracleIndexOrganizedTableDdl, "oracle");
|
||||
expect(logical).not.toContain("STORAGE(");
|
||||
expect(logical).not.toContain("PCTFREE");
|
||||
expect(logical).not.toContain("PCTUSED");
|
||||
expect(logical).not.toContain("INITRANS");
|
||||
expect(logical).not.toContain("MAXTRANS");
|
||||
expect(logical).not.toContain("PCTTHRESHOLD");
|
||||
expect(logical).not.toContain("LOGGING");
|
||||
expect(logical).not.toContain('TABLESPACE "USERS"');
|
||||
expect(logical).toContain(') ORGANIZATION INDEX NOCOMPRESS INCLUDING "NAME" OVERFLOW;');
|
||||
});
|
||||
|
||||
it("drops the LOB storage clause the way SEGMENT_ATTRIBUTES=FALSE does", () => {
|
||||
const logical = applyDdlStoragePreference(oracleLobTableDdl, "oracle");
|
||||
expect(logical).not.toContain("LOB (");
|
||||
expect(logical).not.toContain("STORAGE IN ROW");
|
||||
expect(logical).not.toContain("PCTVERSION");
|
||||
expect(logical).toContain('"DOC" CLOB');
|
||||
expect(logical).toContain('CONSTRAINT "DBX_LOB_PK" PRIMARY KEY ("ID")');
|
||||
});
|
||||
|
||||
it("preserves quoted identifiers and string literals that spell storage keywords", () => {
|
||||
const logical = applyDdlStoragePreference(oracleQuotedIdentifierDdl, "oracle");
|
||||
expect(logical).toContain('"PCTFREE" NUMBER(5,0)');
|
||||
expect(logical).toContain('"LOGGING" VARCHAR2(20)');
|
||||
expect(logical).toContain("\"STORAGE\" VARCHAR2(30) DEFAULT 'keep STORAGE(INITIAL 65536)'");
|
||||
expect(logical).toContain('CONSTRAINT "DBX_QUOTED_CK" CHECK (ID > 0) ENABLE');
|
||||
expect(logical).toContain('COMMENT ON TABLE "DBX_TEST"."DBX_QUOTED" IS \'keep STORAGE(INITIAL 65536) text\';');
|
||||
expect(logical).not.toContain("STORAGE(INITIAL 65536 NEXT");
|
||||
expect(logical).not.toContain('TABLESPACE "USERS"');
|
||||
});
|
||||
|
||||
it("leaves DDL without table options untouched", () => {
|
||||
expect(applyDdlStoragePreference(oracleSequenceDdl, "oracle")).toBe(oracleSequenceDdl);
|
||||
expect(applyDdlStoragePreference(oracleViewSource, "oracle")).toBe(oracleViewSource);
|
||||
});
|
||||
|
||||
it("keeps partitioning, constraint state and a named USING INDEX index", () => {
|
||||
const ddl = `CREATE TABLE "S"."P_SALES" \n ( "ID" NUMBER(10,0) NOT NULL ENABLE, \n "REGION" VARCHAR2(30) \n ) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING\n STORAGE(INITIAL 65536 NEXT 1048576)\n TABLESPACE "USERS" \n PARTITION BY RANGE ("ID")\n (PARTITION "P1" VALUES LESS THAN (100) PCTFREE 5 INITRANS 2 STORAGE(INITIAL 65536) TABLESPACE "USERS", \n PARTITION "P2" VALUES LESS THAN (MAXVALUE) TABLESPACE "USERS")`;
|
||||
const logical = applyDdlStoragePreference(ddl, "oracle");
|
||||
expect(logical).not.toContain("STORAGE(");
|
||||
expect(logical).not.toContain("PCTFREE");
|
||||
expect(logical).not.toContain("MAXTRANS");
|
||||
expect(logical).not.toContain("LOGGING");
|
||||
expect(logical).not.toContain('TABLESPACE "USERS"');
|
||||
expect(logical).toContain('PARTITION BY RANGE ("ID")');
|
||||
expect(logical).toContain('PARTITION "P1" VALUES LESS THAN (100)');
|
||||
expect(logical).toContain('PARTITION "P2" VALUES LESS THAN (MAXVALUE)');
|
||||
expect(logical).toContain(") NOCOMPRESS");
|
||||
|
||||
const named = `CREATE TABLE "S"."T" ("A" NUMBER, CONSTRAINT "T_PK" PRIMARY KEY ("A") USING INDEX "T_IDX" ENABLE) PCTFREE 10 TABLESPACE "USERS";`;
|
||||
const namedLogical = applyDdlStoragePreference(named, "oracle");
|
||||
expect(namedLogical).toContain('USING INDEX "T_IDX" ENABLE');
|
||||
expect(namedLogical).not.toContain("PCTFREE");
|
||||
});
|
||||
|
||||
it("stops at the query body of a CREATE TABLE AS SELECT", () => {
|
||||
const ddl = `CREATE TABLE "S"."C" ("A" NUMBER) PCTFREE 10 TABLESPACE "USERS" AS SELECT COUNT(*), LOGGING, TABLESPACE FROM "S"."SRC";`;
|
||||
expect(applyDdlStoragePreference(ddl, "oracle")).toBe(`CREATE TABLE "S"."C" ("A" NUMBER) AS SELECT COUNT(*), LOGGING, TABLESPACE FROM "S"."SRC";`);
|
||||
// Without a column list there is no storage region to detect, so the source is returned
|
||||
// unchanged instead of guessing where the options end.
|
||||
const withoutColumns = `CREATE TABLE "S"."C" PCTFREE 10 TABLESPACE "USERS" AS SELECT LOGGING FROM "S"."SRC";`;
|
||||
expect(applyDdlStoragePreference(withoutColumns, "oracle")).toBe(withoutColumns);
|
||||
});
|
||||
|
||||
it("returns the raw DDL when disabled and for database types without such attributes", () => {
|
||||
expect(applyDdlStoragePreference(oracleHeapTableDdl, "oracle", false)).toBe(oracleHeapTableDdl);
|
||||
expect(applyDdlStoragePreference(oracleHeapTableDdl, "postgres")).toBe(oracleHeapTableDdl);
|
||||
const broken = `CREATE TABLE "S"."T" ("A" VARCHAR2(10) DEFAULT 'unclosed) PCTFREE 10`;
|
||||
expect(applyDdlStoragePreference(broken, "oracle")).toBe(broken);
|
||||
});
|
||||
|
||||
it("reports the database types whose DDL carries strippable storage attributes", () => {
|
||||
expect(supportsDdlStoragePreference("oracle")).toBe(true);
|
||||
expect(supportsDdlStoragePreference("oceanbase-oracle")).toBe(true);
|
||||
expect(supportsDdlStoragePreference("mysql")).toBe(false);
|
||||
expect(supportsDdlStoragePreference(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,94 @@
|
||||
import { tokenizeSqlSemantic } from "@/lib/sql/semantic/tokens";
|
||||
import type { SqlSemanticToken } from "@/lib/sql/semantic/types";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
/** Remove only known OceanBase table storage options, keeping the cached source intact. */
|
||||
type DdlRemoval = { start: number; end: number };
|
||||
type DdlTokenRemoval = { startIndex: number; endIndex: number };
|
||||
|
||||
/**
|
||||
* Database types whose generated DDL carries physical storage attributes that the
|
||||
* "exclude storage attributes" preference strips. OceanBase Oracle emits its own
|
||||
* table options; `oracle` covers both the native Oracle agent and the JDBC Oracle
|
||||
* plugin, whose `DBMS_METADATA.GET_DDL` output is the same.
|
||||
*/
|
||||
export function supportsDdlStoragePreference(databaseType: DatabaseType | undefined): boolean {
|
||||
return databaseType === "oceanbase-oracle" || databaseType === "oracle";
|
||||
}
|
||||
|
||||
/** Remove only known storage attributes from a DDL source, keeping the cached source intact. */
|
||||
export function applyDdlStoragePreference(sql: string, databaseType: DatabaseType | undefined, excludeStorage = true): string {
|
||||
if (databaseType !== "oceanbase-oracle" || !excludeStorage) return sql;
|
||||
if (!excludeStorage) return sql;
|
||||
if (databaseType === "oceanbase-oracle") return stripOceanBaseOracleStorageOptions(sql);
|
||||
if (databaseType === "oracle") return stripOracleStorageAttributes(sql);
|
||||
return sql;
|
||||
}
|
||||
|
||||
function tokenizeDdl(sql: string): SqlSemanticToken[] | null {
|
||||
const tokens = tokenizeSqlSemantic(sql, "oracle");
|
||||
if (tokens.some((token) => token.closed === false)) return sql;
|
||||
const removals: Array<{ start: number; end: number }> = [];
|
||||
return tokens.some((token) => token.closed === false) ? null : tokens;
|
||||
}
|
||||
|
||||
function spliceRemovals(sql: string, removals: DdlRemoval[]): string {
|
||||
let result = sql;
|
||||
for (const { start, end } of removals.reverse()) result = result.slice(0, start) + result.slice(end);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Characters that would merge into a neighbouring token if the gap between them went away. */
|
||||
const TOKEN_CHARACTER = /[A-Za-z0-9_$#"]/;
|
||||
/** Punctuation that reads better glued to the surviving text than separated by a space. */
|
||||
const TIGHT_AFTER_REMOVAL = new Set([";", ","]);
|
||||
|
||||
/** Whether the two surviving characters need a space between them once a clause is out. */
|
||||
function needsRemovalSeparator(before: string | undefined, after: string | undefined): boolean {
|
||||
if (before === undefined || after === undefined) return false;
|
||||
if (TIGHT_AFTER_REMOVAL.has(after) || before === "(" || after === ")") return false;
|
||||
return TOKEN_CHARACTER.test(before) || TOKEN_CHARACTER.test(after);
|
||||
}
|
||||
|
||||
/**
|
||||
* Splice out whole-token clause spans, absorbing the whitespace around each one and
|
||||
* merging neighbours, so a dropped clause leaves neither a blank line nor a double
|
||||
* space behind. Only inter-token whitespace is absorbed — strings, identifiers and
|
||||
* comments are never touched — and one space is kept wherever removing it would glue
|
||||
* the surviving tokens together.
|
||||
*/
|
||||
function spliceTightRemovals(sql: string, tokens: readonly SqlSemanticToken[], removals: DdlTokenRemoval[]): string {
|
||||
const merged: DdlRemoval[] = [];
|
||||
for (const { startIndex, endIndex } of removals) {
|
||||
const previousEnd = startIndex > 0 ? tokens[startIndex - 1]!.span.end : 0;
|
||||
const nextStart = endIndex + 1 < tokens.length ? tokens[endIndex + 1]!.span.start : sql.length;
|
||||
const start = /^\s*$/.test(sql.slice(previousEnd, tokens[startIndex]!.span.start)) ? previousEnd : tokens[startIndex]!.span.start;
|
||||
const end = /^\s*$/.test(sql.slice(tokens[endIndex]!.span.end, nextStart)) ? nextStart : tokens[endIndex]!.span.end;
|
||||
const last = merged[merged.length - 1];
|
||||
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
||||
else merged.push({ start, end });
|
||||
}
|
||||
let result = "";
|
||||
let cursor = 0;
|
||||
for (const { start, end } of merged) {
|
||||
result += sql.slice(cursor, start);
|
||||
if (needsRemovalSeparator(sql[start - 1], sql[end])) result += " ";
|
||||
cursor = end;
|
||||
}
|
||||
return result + sql.slice(cursor);
|
||||
}
|
||||
|
||||
/** Index of the `)` closing the `(` at `openIndex`, or null when it is unclosed. */
|
||||
function matchClosingParen(tokens: readonly SqlSemanticToken[], openIndex: number): number | null {
|
||||
for (let i = openIndex + 1; i < tokens.length; i++) {
|
||||
const token = tokens[i]!;
|
||||
if (token.text === "(") i = matchClosingParen(tokens, i) ?? tokens.length;
|
||||
else if (token.text === ")") return i;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Remove only known OceanBase table storage options, keeping the cached source intact. */
|
||||
function stripOceanBaseOracleStorageOptions(sql: string): string {
|
||||
const tokens = tokenizeDdl(sql);
|
||||
if (!tokens) return sql;
|
||||
const removals: DdlRemoval[] = [];
|
||||
let table = false;
|
||||
let options = false;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
@@ -45,7 +127,133 @@ export function applyDdlStoragePreference(sql: string, databaseType: DatabaseTyp
|
||||
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;
|
||||
return spliceRemovals(sql, removals);
|
||||
}
|
||||
|
||||
/** Clause keywords whose only operand is a bare number, so they are unambiguous. */
|
||||
const ORACLE_NUMERIC_STORAGE_CLAUSES = new Set(["pctfree", "pctused", "initrans", "maxtrans", "pctthreshold"]);
|
||||
/** Clause keywords that take no operand and therefore need a context guard. */
|
||||
const ORACLE_BARE_STORAGE_CLAUSES = new Set(["logging", "nologging", "monitoring", "nomonitoring", "parallel", "noparallel"]);
|
||||
/** Words that may be followed by an unquoted identifier, i.e. by a partition name. */
|
||||
const ORACLE_IDENTIFIER_LEAD_WORDS = new Set(["partition", "subpartition"]);
|
||||
/** Words allowed between `CREATE` and the object keyword it introduces. */
|
||||
const ORACLE_CREATE_MODIFIERS = new Set(["or", "replace", "force", "global", "temporary", "private", "public", "unique", "bitmap"]);
|
||||
const ORACLE_LOB_STORAGE_TYPES = new Set(["basicfile", "securefile"]);
|
||||
|
||||
function isIdentifierToken(token: SqlSemanticToken | undefined): boolean {
|
||||
return token?.kind === "word" || token?.kind === "quoted_identifier";
|
||||
}
|
||||
|
||||
/**
|
||||
* End index of a `LOB (…) STORE AS [BASICFILE|SECUREFILE] (…)` clause, or null when the
|
||||
* tokens are something else. `DBMS_METADATA` resolves the whole clause against the
|
||||
* storage clause and Oracle's own `SEGMENT_ATTRIBUTES=FALSE` transform drops it the
|
||||
* same way, so the column name list goes with the storage attributes.
|
||||
*/
|
||||
function oracleLobStorageClauseEnd(tokens: readonly SqlSemanticToken[], index: number): number | null {
|
||||
if (tokens[index + 1]?.text !== "(") return null;
|
||||
const columnListEnd = matchClosingParen(tokens, index + 1);
|
||||
if (columnListEnd === null) return null;
|
||||
if (tokens[columnListEnd + 1]?.normalized !== "store" || tokens[columnListEnd + 2]?.normalized !== "as") return null;
|
||||
let cursor = columnListEnd + 3;
|
||||
if (ORACLE_LOB_STORAGE_TYPES.has(tokens[cursor]?.normalized ?? "")) cursor++;
|
||||
if (tokens[cursor]?.text !== "(") return null;
|
||||
return matchClosingParen(tokens, cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* End index of the storage clause starting at `index`, or null when no clause starts
|
||||
* there. `previous` is the last significant token before it, which only the
|
||||
* operand-less keywords need: inside a column list or a partition definition the same
|
||||
* word is an identifier, never a clause.
|
||||
*/
|
||||
function oracleStorageClauseEnd(tokens: readonly SqlSemanticToken[], index: number, previous: SqlSemanticToken | undefined): number | null {
|
||||
const token = tokens[index]!;
|
||||
if (token.kind !== "word") return null;
|
||||
const keyword = token.normalized;
|
||||
const next = tokens[index + 1];
|
||||
if (ORACLE_NUMERIC_STORAGE_CLAUSES.has(keyword)) return next?.kind === "number" ? index + 1 : null;
|
||||
if (keyword === "storage") return next?.text === "(" ? matchClosingParen(tokens, index + 1) : null;
|
||||
if (keyword === "tablespace") return isIdentifierToken(next) ? index + 1 : null;
|
||||
// Deprecated index statistics clause; `SEGMENT_ATTRIBUTES=FALSE` drops it as well.
|
||||
if (keyword === "compute" && next?.normalized === "statistics") return index + 1;
|
||||
if (keyword === "segment") {
|
||||
return next?.normalized === "creation" && ["immediate", "deferred"].includes(tokens[index + 2]?.normalized ?? "") ? index + 2 : null;
|
||||
}
|
||||
if (keyword === "lob") return oracleLobStorageClauseEnd(tokens, index);
|
||||
if (!ORACLE_BARE_STORAGE_CLAUSES.has(keyword)) return null;
|
||||
if (keyword === "parallel" && next?.kind === "number") return index + 1;
|
||||
if (previous?.text === "(" || previous?.text === ",") return null;
|
||||
if (previous?.kind === "word" && ORACLE_IDENTIFIER_LEAD_WORDS.has(previous.normalized)) return null;
|
||||
return index;
|
||||
}
|
||||
|
||||
/** Whether the `CREATE` at `index` introduces a relation whose DDL carries table options. */
|
||||
function oracleCreatedRelation(tokens: readonly SqlSemanticToken[], index: number): boolean {
|
||||
let materialized = false;
|
||||
for (let i = index + 1; i < tokens.length; i++) {
|
||||
const token = tokens[i]!;
|
||||
if (token.kind !== "word") return false;
|
||||
if (token.normalized === "table" || token.normalized === "index") return true;
|
||||
if (token.normalized === "materialized") materialized = true;
|
||||
else if (token.normalized === "view") return materialized;
|
||||
else if (!ORACLE_CREATE_MODIFIERS.has(token.normalized)) return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the physical storage attributes `DBMS_METADATA.GET_DDL` emits — the same set
|
||||
* Oracle's `SEGMENT_ATTRIBUTES=FALSE` transform excludes — from table, index and
|
||||
* materialized-view DDL: PCTFREE/PCTUSED/INITRANS/MAXTRANS/PCTTHRESHOLD, `STORAGE(…)`,
|
||||
* `TABLESPACE`, `LOGGING`, `SEGMENT CREATION`, the attributes of a `USING INDEX`
|
||||
* constraint clause and LOB storage clauses. Columns, constraints, partitioning and
|
||||
* `COMPRESS` are preserved, and quoted identifiers never match a clause.
|
||||
*/
|
||||
function stripOracleStorageAttributes(sql: string): string {
|
||||
const tokens = tokenizeDdl(sql);
|
||||
if (!tokens) return sql;
|
||||
const removals: DdlTokenRemoval[] = [];
|
||||
let createsRelation = false;
|
||||
let inRelationOptions = false;
|
||||
let usingIndexDepth: number | null = null;
|
||||
let previous: SqlSemanticToken | undefined;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i]!;
|
||||
if (token.kind === "comment") continue;
|
||||
if (token.depth === 0) {
|
||||
if (token.text === ";") {
|
||||
createsRelation = false;
|
||||
inRelationOptions = false;
|
||||
usingIndexDepth = null;
|
||||
} else if (token.kind === "word" && token.normalized === "create") {
|
||||
createsRelation = oracleCreatedRelation(tokens, i);
|
||||
inRelationOptions = false;
|
||||
} else if (createsRelation && token.text === ")") {
|
||||
// The storage options follow the column list of the relation.
|
||||
inRelationOptions = true;
|
||||
} else if (createsRelation && token.kind === "word" && token.normalized === "as") {
|
||||
// `CREATE TABLE/… AS SELECT`: the query body is not a storage options region.
|
||||
createsRelation = false;
|
||||
inRelationOptions = false;
|
||||
}
|
||||
}
|
||||
if (usingIndexDepth !== null) {
|
||||
const endsClause = token.depth === usingIndexDepth && ((token.kind === "word" && (token.normalized === "enable" || token.normalized === "disable")) || token.text === "," || token.text === ";");
|
||||
if (token.depth < usingIndexDepth || endsClause) usingIndexDepth = null;
|
||||
}
|
||||
if (token.kind === "word" && token.normalized === "using" && tokens[i + 1]?.normalized === "index") {
|
||||
usingIndexDepth = token.depth;
|
||||
}
|
||||
const inStorageRegion = inRelationOptions || (usingIndexDepth !== null && token.depth >= usingIndexDepth);
|
||||
const clauseEnd = inStorageRegion ? oracleStorageClauseEnd(tokens, i, previous) : null;
|
||||
if (clauseEnd !== null) {
|
||||
removals.push({ startIndex: i, endIndex: clauseEnd });
|
||||
previous = tokens[clauseEnd];
|
||||
i = clauseEnd;
|
||||
continue;
|
||||
}
|
||||
previous = token;
|
||||
}
|
||||
return spliceTightRemovals(sql, tokens, removals);
|
||||
}
|
||||
Reference in new issue
Block a user