feat(oracle): apply DDL storage stripping to Oracle

This commit is contained in:
zipg authored and GitHub committed 2026-09-22 11:55:05 +08:00
1 parent 770463e39a
commit 639abb3993
19 files changed
+414 -29

No files matched your search

+178 -2
View File
@@ -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);
});
});
+215 -7
View File
@@ -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);
}