feat(oracle): apply DDL storage stripping to Oracle
This commit is contained in:
19 files changed
+414
-29
No files matched your search
@@ -3,16 +3,17 @@ import { useId } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { supportsDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import type { DatabaseType } from "@/types/database";
|
||||
|
||||
defineProps<{ databaseType?: DatabaseType; disabled?: boolean }>();
|
||||
const props = defineProps<{ databaseType?: DatabaseType; disabled?: boolean }>();
|
||||
const settings = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
const id = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="databaseType === 'oceanbase-oracle'" class="flex shrink-0 items-center gap-2 text-xs" :title="t('contextMenu.excludeDdlStorageHint')">
|
||||
<div v-if="supportsDdlStoragePreference(props.databaseType)" class="flex shrink-0 items-center gap-2 text-xs" :title="t('contextMenu.excludeDdlStorageHint')">
|
||||
<Switch :id="id" size="sm" :disabled="disabled" :model-value="settings.editorSettings.excludeDdlStorage !== false" @update:model-value="settings.updateEditorSettings({ excludeDdlStorage: $event })" />
|
||||
<label :for="id" class="cursor-pointer">{{ t("contextMenu.excludeDdlStorage") }}</label>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import DdlStorageToggle from "@/components/objects/DdlStorageToggle.vue";
|
||||
import { structurePreviewHasOceanBase } from "./sidebarTreeDialogState";
|
||||
import { structurePreviewDdlStorageType } from "./sidebarTreeDialogState";
|
||||
import { computed, toRefs, watch } from "vue";
|
||||
import { AlertTriangle, Check, Loader2, Clipboard, Plus, Trash2, Upload } from "@lucide/vue";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||
@@ -469,7 +469,7 @@ watch(
|
||||
<pre v-else class="max-h-[56vh] min-h-64 overflow-auto rounded bg-muted p-3 text-xs whitespace-pre-wrap" v-html="highlight(structurePreviewSql)"></pre>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DdlStorageToggle :database-type="structurePreviewHasOceanBase ? 'oceanbase-oracle' : undefined" :disabled="isLoadingStructurePreview || !!structurePreviewError" class="mr-auto" />
|
||||
<DdlStorageToggle :database-type="structurePreviewDdlStorageType" :disabled="isLoadingStructurePreview || !!structurePreviewError" class="mr-auto" />
|
||||
<Button variant="outline" @click="showStructurePreviewDialog = false">{{ t("dangerDialog.cancel") }}</Button>
|
||||
<Button variant="outline" :disabled="isLoadingStructurePreview || !structurePreviewSql" @click="copyStructurePreview">
|
||||
<Clipboard class="h-4 w-4" />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ref, shallowRef } from "vue";
|
||||
import type { TreeNode } from "@/types/database";
|
||||
import type { DatabaseType, TreeNode } from "@/types/database";
|
||||
import type { PasteTableMode } from "@/lib/table/tableClipboard";
|
||||
import { fallbackCreateDatabaseCharsetMetadata } from "@/lib/database/createDatabaseCharsetOptions";
|
||||
import type { DatabaseUserIdentity } from "@/lib/database/databaseUserAdmin";
|
||||
@@ -39,7 +39,7 @@ export const showBatchTruncateConfirm = ref(false);
|
||||
export const showStructurePreviewDialog = ref(false);
|
||||
export const showStructureDocCopyDialog = ref(false);
|
||||
export const structurePreviewSql = ref("");
|
||||
export const structurePreviewHasOceanBase = ref(false);
|
||||
export const structurePreviewDdlStorageType = ref<DatabaseType | undefined>(undefined);
|
||||
export const structurePreviewTitle = ref("");
|
||||
export const structurePreviewDefaultFileName = ref("structure.sql");
|
||||
export const structurePreviewError = ref("");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { applyDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import { applyDdlStoragePreference, supportsDdlStoragePreference } from "@/lib/sql/ddlStorage";
|
||||
import { watch, createApp, getCurrentScope, onScopeDispose, type ShallowRef } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import i18n from "@/i18n";
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
structurePreviewError,
|
||||
structurePreviewSql,
|
||||
structurePreviewTitle,
|
||||
structurePreviewHasOceanBase,
|
||||
structurePreviewDdlStorageType,
|
||||
} from "@/components/sidebar/sidebarTreeDialogState";
|
||||
import type { CsvQuoteMode } from "@/lib/export/csvQuoteMode";
|
||||
|
||||
@@ -171,7 +171,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
isLoadingStructurePreview.value = true;
|
||||
structurePreviewError.value = "";
|
||||
structurePreviewSql.value = "";
|
||||
structurePreviewHasOceanBase.value = false;
|
||||
structurePreviewDdlStorageType.value = undefined;
|
||||
structureSource = [];
|
||||
structurePreviewTitle.value = targets.length === 1 ? t("contextMenu.exportStructurePreviewTitle", { name: targets[0]!.label }) : t("contextMenu.exportStructurePreviewTitleMultiple", { count: targets.length });
|
||||
structurePreviewDefaultFileName.value = targets.length === 1 ? `${targets[0]!.label}.sql` : "structures.sql";
|
||||
@@ -186,7 +186,7 @@ export function useSidebarTreeExportRuntime(options: SidebarTreeExportRuntimeOpt
|
||||
requestSource.push({ ddl, databaseType });
|
||||
}
|
||||
structureSource = requestSource;
|
||||
structurePreviewHasOceanBase.value = requestSource.some(({ databaseType }) => databaseType === "oceanbase-oracle");
|
||||
structurePreviewDdlStorageType.value = requestSource.map(({ databaseType }) => databaseType).find(supportsDdlStoragePreference);
|
||||
renderStructurePreview();
|
||||
} catch (error: any) {
|
||||
if (requestId !== structureRequestId) return;
|
||||
|
||||
@@ -3480,7 +3480,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "DDL yüklənir...",
|
||||
ddlCopied: "DDL kopyalandı",
|
||||
excludeDdlStorage: "Saxlama atributlarını çıxar",
|
||||
excludeDdlStorageHint: "OceanBase Oracle DDL-in göstərilməsinə, kopyalanmasına və strukturun ixracına tətbiq olunur. Sıxılma, replika və saxlama parametrlərini saxlamaq üçün söndürün. Bölmələr və məhdudiyyətlər həmişə saxlanılır.",
|
||||
excludeDdlStorageHint: "Oracle və OceanBase Oracle DDL-in göstərilməsinə, kopyalanmasına və strukturun ixracına tətbiq olunur. Sıxılma, replika və saxlama parametrlərini saxlamaq üçün söndürün. Bölmələr və məhdudiyyətlər həmişə saxlanılır.",
|
||||
refreshDdlOnOpen: "Hər açılışda yenilə",
|
||||
refreshDdlOnOpenHint: "Aktiv olduqda DDL hər açılışda verilənlər bazasından yenidən yüklənir; deaktiv olduqda keşdəki məlumatlara üstünlük verilir",
|
||||
dropObject: "Obyekti sil",
|
||||
|
||||
@@ -3686,7 +3686,7 @@ export default {
|
||||
viewDdlLoading: "Loading DDL...",
|
||||
ddlCopied: "DDL copied",
|
||||
excludeDdlStorage: "Exclude storage attributes",
|
||||
excludeDdlStorageHint: "Applies to OceanBase Oracle DDL display, copying and structure export. Disable to retain compression, replica and storage options. Partitions and constraints are preserved.",
|
||||
excludeDdlStorageHint: "Applies to Oracle and OceanBase Oracle DDL display, copying and structure export. Disable to retain compression, replica and storage options. Partitions and constraints are preserved.",
|
||||
refreshDdlOnOpen: "Refresh each time it opens",
|
||||
refreshDdlOnOpenHint: "When enabled, DDL is reloaded from the database each time it opens; when disabled, cached data is preferred",
|
||||
dropObject: "Drop Object",
|
||||
|
||||
@@ -3540,7 +3540,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "Cargando DDL...",
|
||||
ddlCopied: "DDL copiado",
|
||||
excludeDdlStorage: "Excluir atributos de almacenamiento",
|
||||
excludeDdlStorageHint: "Se aplica a la visualización y copia de DDL y a la exportación de estructuras de OceanBase Oracle. Desactiva esta opción para conservar los parámetros de compresión, réplicas y almacenamiento. Las particiones y restricciones se conservan siempre.",
|
||||
excludeDdlStorageHint: "Se aplica a la visualización y copia de DDL y a la exportación de estructuras de Oracle y OceanBase Oracle. Desactiva esta opción para conservar los parámetros de compresión, réplicas y almacenamiento. Las particiones y restricciones se conservan siempre.",
|
||||
refreshDdlOnOpen: "Actualizar cada vez que se abra",
|
||||
refreshDdlOnOpenHint: "Si se activa, el DDL se vuelve a cargar de la base de datos cada vez que se abre; si se desactiva, se usan primero los datos en caché",
|
||||
dropObject: "Eliminar objeto",
|
||||
|
||||
@@ -3537,7 +3537,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "Caricamento DDL...",
|
||||
ddlCopied: "DDL copiato",
|
||||
excludeDdlStorage: "Escludi attributi di archiviazione",
|
||||
excludeDdlStorageHint: "Si applica alla visualizzazione e alla copia del DDL e all'esportazione della struttura in OceanBase Oracle. Disattiva questa opzione per mantenere i parametri di compressione, replica e archiviazione. Le partizioni e i vincoli vengono sempre mantenuti.",
|
||||
excludeDdlStorageHint: "Si applica alla visualizzazione e alla copia del DDL e all'esportazione della struttura in Oracle e OceanBase Oracle. Disattiva questa opzione per mantenere i parametri di compressione, replica e archiviazione. Le partizioni e i vincoli vengono sempre mantenuti.",
|
||||
refreshDdlOnOpen: "Aggiorna a ogni apertura",
|
||||
refreshDdlOnOpenHint: "Se attivo, il DDL viene ricaricato dal database a ogni apertura; se disattivo, vengono usati prima i dati memorizzati nella cache",
|
||||
dropObject: "Elimina Oggetto",
|
||||
|
||||
@@ -3774,7 +3774,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "DDLを読み込み中...",
|
||||
ddlCopied: "DDLをコピーしました",
|
||||
excludeDdlStorage: "ストレージ属性を除外",
|
||||
excludeDdlStorageHint: "OceanBase Oracle の DDL の表示・コピーと構造のエクスポートに適用されます。無効にすると、圧縮・レプリカ・ストレージのパラメーターを保持します。パーティションと制約は常に保持されます。",
|
||||
excludeDdlStorageHint: "Oracle と OceanBase Oracle の DDL の表示・コピーと構造のエクスポートに適用されます。無効にすると、圧縮・レプリカ・ストレージのパラメーターを保持します。パーティションと制約は常に保持されます。",
|
||||
refreshDdlOnOpen: "開くたびに更新",
|
||||
refreshDdlOnOpenHint: "有効にすると開くたびにデータベースから DDL を再取得し、無効にするとキャッシュを優先して使用します",
|
||||
manageExtension: "拡張機能を管理...",
|
||||
|
||||
@@ -3440,7 +3440,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "DDL을 불러오는 중...",
|
||||
ddlCopied: "DDL을 복사했습니다",
|
||||
excludeDdlStorage: "스토리지 속성 제외",
|
||||
excludeDdlStorageHint: "OceanBase Oracle의 DDL 표시, 복사 및 구조 내보내기에 적용됩니다. 끄면 압축, 복제본 및 스토리지 매개변수를 유지합니다. 파티션과 제약 조건은 항상 유지됩니다.",
|
||||
excludeDdlStorageHint: "Oracle 및 OceanBase Oracle의 DDL 표시, 복사 및 구조 내보내기에 적용됩니다. 끄면 압축, 복제본 및 스토리지 매개변수를 유지합니다. 파티션과 제약 조건은 항상 유지됩니다.",
|
||||
dropObject: "객체 삭제",
|
||||
dropView: "뷰 삭제",
|
||||
dropColumn: "컬럼 삭제",
|
||||
|
||||
@@ -3535,7 +3535,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "Carregando DDL...",
|
||||
ddlCopied: "DDL copiado",
|
||||
excludeDdlStorage: "Excluir atributos de armazenamento",
|
||||
excludeDdlStorageHint: "Aplica-se à exibição e cópia de DDL e à exportação de estruturas do OceanBase Oracle. Desative esta opção para manter os parâmetros de compressão, réplicas e armazenamento. As partições e restrições são sempre mantidas.",
|
||||
excludeDdlStorageHint: "Aplica-se à exibição e cópia de DDL e à exportação de estruturas do Oracle e OceanBase Oracle. Desative esta opção para manter os parâmetros de compressão, réplicas e armazenamento. As partições e restrições são sempre mantidas.",
|
||||
refreshDdlOnOpen: "Atualizar sempre que abrir",
|
||||
refreshDdlOnOpenHint: "Quando ativado, o DDL é recarregado do banco de dados sempre que é aberto; quando desativado, os dados em cache têm prioridade",
|
||||
dropObject: "Remover Objeto",
|
||||
|
||||
@@ -3452,7 +3452,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "DDL yükleniyor...",
|
||||
ddlCopied: "DDL kopyalandı",
|
||||
excludeDdlStorage: "Depolama özelliklerini hariç tut",
|
||||
excludeDdlStorageHint: "OceanBase Oracle DDL görüntüleme, kopyalama ve yapı dışa aktarma işlemlerine uygulanır. Sıkıştırma, replika ve depolama parametrelerini korumak için kapatın. Bölümler ve kısıtlamalar her zaman korunur.",
|
||||
excludeDdlStorageHint: "Oracle ve OceanBase Oracle DDL görüntüleme, kopyalama ve yapı dışa aktarma işlemlerine uygulanır. Sıkıştırma, replika ve depolama parametrelerini korumak için kapatın. Bölümler ve kısıtlamalar her zaman korunur.",
|
||||
refreshDdlOnOpen: "Her açılışta yenile",
|
||||
refreshDdlOnOpenHint: "Etkinleştirildiğinde DDL her açılışta veritabanından yeniden yüklenir; kapatıldığında önbellekteki veri tercih edilir",
|
||||
dropObject: "Nesneyi Sil",
|
||||
|
||||
@@ -3598,7 +3598,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "正在读取 DDL...",
|
||||
ddlCopied: "DDL 已复制",
|
||||
excludeDdlStorage: "去掉存储属性",
|
||||
excludeDdlStorageHint: "用于 OceanBase Oracle 的 DDL 显示、复制和结构导出。关闭后保留压缩、副本和存储参数;分区和约束始终保留。",
|
||||
excludeDdlStorageHint: "用于 Oracle 与 OceanBase Oracle 的 DDL 显示、复制和结构导出。关闭后保留压缩、副本和存储参数;分区和约束始终保留。",
|
||||
refreshDdlOnOpen: "每次打开时刷新",
|
||||
refreshDdlOnOpenHint: "开启后每次打开都会从数据库重新读取;关闭时优先使用缓存",
|
||||
dropObject: "删除对象",
|
||||
|
||||
@@ -3536,7 +3536,7 @@ export default withEnglishFallback({
|
||||
viewDdlLoading: "正在讀取 DDL...",
|
||||
ddlCopied: "DDL 已複製",
|
||||
excludeDdlStorage: "移除儲存屬性",
|
||||
excludeDdlStorageHint: "用於 OceanBase Oracle 的 DDL 顯示、複製和結構匯出。關閉後保留壓縮、副本和儲存參數;分割區和約束條件一律保留。",
|
||||
excludeDdlStorageHint: "用於 Oracle 與 OceanBase Oracle 的 DDL 顯示、複製和結構匯出。關閉後保留壓縮、副本和儲存參數;分割區和約束條件一律保留。",
|
||||
refreshDdlOnOpen: "每次開啟時重新整理",
|
||||
refreshDdlOnOpenHint: "開啟後每次開啟都會從資料庫重新讀取;關閉時優先使用快取",
|
||||
dropObject: "刪除物件",
|
||||
|
||||
+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