mirror of
https://github.com/t8y2/dbx.git
synced 2026-10-21 03:59:03 +00:00
fix(mysql): prevalidate routine saves and support procedure refresh
This commit is contained in:
@@ -1035,6 +1035,10 @@ function refreshData(): boolean {
|
||||
void pluginFilesystemTabRef.value?.refresh();
|
||||
return true;
|
||||
}
|
||||
if (props.activeTab.objectSource || props.activeTab.sourceLoad) {
|
||||
if (queryStore.isTabDirty(props.activeTab) && !window.confirm(t("objects.refreshDiscardConfirm"))) return false;
|
||||
return queryStore.refreshObjectSourceTab(props.activeTab.id);
|
||||
}
|
||||
// Restored data tabs intentionally omit row data, so refresh must work before DataGrid mounts.
|
||||
if (canReloadUnavailableDataTab(props.activeTab)) {
|
||||
reloadUnavailableDataTab();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onUnmounted, ref, watch, watchEffect } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, Layers, MoreHorizontal, BetweenVerticalStart, Eye, WrapText } from "@lucide/vue";
|
||||
import { Play, CirclePlay, Loader2, Square, Database, Check, Table2, AlignLeft, GitBranch, Save, FolderOpen, X, Shield, Download, RotateCcw, AlertTriangle, ClipboardPaste, Minimize2, SpellCheck2, Layers, MoreHorizontal, BetweenVerticalStart, Eye, WrapText, RefreshCw } from "@lucide/vue";
|
||||
import { supportsInsertValueHints } from "@/lib/editor/codemirrorInsertValueHints";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SearchableSelect } from "@/components/ui/searchable-select";
|
||||
@@ -12,6 +12,7 @@ import DatabaseIcon from "@/components/icons/DatabaseIcon.vue";
|
||||
import ConnectionTreeSelect from "@/components/connection/ConnectionTreeSelect.vue";
|
||||
import ProductionContextBadge from "@/components/common/ProductionContextBadge.vue";
|
||||
import { useConnectionStore } from "@/stores/connectionStore";
|
||||
import { useQueryStore } from "@/stores/queryStore";
|
||||
import { useSettingsStore } from "@/stores/settingsStore";
|
||||
import { catalogDatabaseOptionsKey, databaseAfterCatalogChange, normalizedQueryTabCatalog, queryCatalogSelectorVisible, selectedQueryCatalogName, useDatabaseOptions } from "@/composables/useDatabaseOptions";
|
||||
import { useSchemaOptions } from "@/composables/useSchemaOptions";
|
||||
@@ -92,6 +93,7 @@ const emit = defineEmits<{
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const { databaseOptions, loadingDatabaseOptions, loadDatabaseOptions, catalogOptions, loadingCatalogOptions, loadCatalogOptions, catalogDatabaseOptions, loadingCatalogDatabaseOptions, loadCatalogDatabaseOptions } = useDatabaseOptions();
|
||||
const { loadSchemaOptions, getSchemaOptionsForDb, isLoadingSchemas, isSchemaAware } = useSchemaOptions();
|
||||
@@ -244,6 +246,14 @@ const saveTooltip = computed(() => {
|
||||
if (props.activeTab.externalSqlPath) return t("toolbar.saveSqlFile");
|
||||
return t("toolbar.saveSql");
|
||||
});
|
||||
const isObjectSourceTab = computed(() => !!props.activeTab.objectSource || !!props.activeTab.sourceLoad);
|
||||
const objectSourceRefreshing = computed(() => !!props.activeTab.sourceLoad && !props.activeTab.sourceLoad.error);
|
||||
|
||||
function refreshObjectSource() {
|
||||
if (!isObjectSourceTab.value) return;
|
||||
if (queryStore.isTabDirty(props.activeTab) && !window.confirm(t("objects.refreshDiscardConfirm"))) return;
|
||||
queryStore.refreshObjectSourceTab(props.activeTab.id);
|
||||
}
|
||||
const executeShortcutDisplay = computed(() => formatShortcutDisplay(settingsStore.editorSettings.shortcuts.executeSql));
|
||||
const executeShortcutTooltip = computed(() => t("toolbar.executeShortcut", { shortcut: executeShortcutDisplay.value }));
|
||||
// executableSql 在无选区时可能是整篇文档;只要有 DML 语句出现就显示预览按钮,
|
||||
@@ -606,6 +616,22 @@ async function changeCatalog(selectedCatalog: string) {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ saveTooltip }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="isObjectSourceTab">
|
||||
<TooltipTrigger as-child>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6 text-emerald-600 hover:bg-emerald-500/10 hover:text-emerald-700 dark:text-emerald-300 dark:hover:text-emerald-200"
|
||||
:disabled="objectSourceRefreshing || activeTab.isExecuting"
|
||||
:aria-label="t('structureEditor.refresh')"
|
||||
@click="refreshObjectSource"
|
||||
>
|
||||
<Loader2 v-if="objectSourceRefreshing" class="h-3.5 w-3.5 animate-spin" />
|
||||
<RefreshCw v-else class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{{ t("structureEditor.refresh") }}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="showOpenSqlButton">
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="icon" class="h-6 w-6 text-sky-600 hover:bg-sky-500/10 hover:text-sky-700 dark:text-sky-300 dark:hover:text-sky-200" @click="emit('openSql')">
|
||||
|
||||
@@ -1385,6 +1385,10 @@ async function fetchTableConstraints(force = false) {
|
||||
}
|
||||
|
||||
async function refreshActiveTableInfo() {
|
||||
if (sidePanelMode.value === "source") {
|
||||
await refreshActiveSource();
|
||||
return;
|
||||
}
|
||||
if (sidePanelMode.value !== "table-info" || !sidePanelRow.value) return;
|
||||
sidePanelGuard.bump();
|
||||
|
||||
@@ -1502,9 +1506,14 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
closeSidePanel();
|
||||
return;
|
||||
}
|
||||
await loadSourcePanel(row);
|
||||
}
|
||||
|
||||
async function loadSourcePanel(row: ObjectBrowserRow, options?: { preserveEditing?: boolean }) {
|
||||
// Starting a different object must invalidate slower source requests before
|
||||
// any state is reset, otherwise an old response can populate the new row.
|
||||
const epoch = sidePanelGuard.start();
|
||||
const preserveEditing = options?.preserveEditing === true && sourceEditing.value;
|
||||
sidePanelRow.value = row;
|
||||
sidePanelMode.value = "source";
|
||||
sourceRow.value = row;
|
||||
@@ -1538,7 +1547,9 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
sourceEditableText.value = editable;
|
||||
sourceContent.value = row.type === "SEQUENCE" ? result.source : editable;
|
||||
sourceDraft.value = editable;
|
||||
sourceEditing.value = sourceCanEdit.value && row.type !== "SEQUENCE";
|
||||
// Fresh open always enters edit when allowed; refresh preserves prior edit mode.
|
||||
const enterEditing = sourceCanEdit.value && row.type !== "SEQUENCE" && (options?.preserveEditing ? preserveEditing : true);
|
||||
sourceEditing.value = enterEditing;
|
||||
if (!sourceCanEdit.value && row.type !== "SEQUENCE") {
|
||||
toast(t("objects.sourceReadOnly"), 3000);
|
||||
}
|
||||
@@ -1550,6 +1561,13 @@ async function openSource(row: ObjectBrowserRow) {
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshActiveSource() {
|
||||
const row = sourceRow.value || (sidePanelMode.value === "source" ? sidePanelRow.value : null);
|
||||
if (!row || sidePanelMode.value !== "source") return;
|
||||
if (sourceEditing.value && !window.confirm(t("objects.refreshDiscardConfirm"))) return;
|
||||
await loadSourcePanel(row, { preserveEditing: true });
|
||||
}
|
||||
|
||||
function openEventEditor(row: ObjectBrowserRow) {
|
||||
sidePanelGuard.start();
|
||||
sidePanelRow.value = row;
|
||||
@@ -4039,6 +4057,9 @@ function getObjectBrowserMenuItems(item: ObjectBrowserRow): ContextMenuItem[] {
|
||||
<Button v-if="!sourceEditing && sourceCanEdit" variant="ghost" size="icon" class="h-5 w-5" :disabled="!sourceContent" @click="editSource">
|
||||
<PencilLine class="h-3 w-3" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" :disabled="sourceLoading || sourceSaving" :title="t('structureEditor.refresh')" :aria-label="t('structureEditor.refresh')" @click="refreshActiveSource">
|
||||
<RefreshCw class="h-3 w-3" :class="{ 'animate-spin': sourceLoading }" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-5 w-5" @click="closeSource">
|
||||
<X class="h-3 w-3" />
|
||||
</Button>
|
||||
|
||||
@@ -156,6 +156,12 @@ function cancelEditSource() {
|
||||
saveError.value = "";
|
||||
}
|
||||
|
||||
async function refreshSource() {
|
||||
if (loading.value || saving.value) return;
|
||||
if (editing.value && !window.confirm(t("objects.refreshDiscardConfirm"))) return;
|
||||
await loadSource(editing.value && canEdit.value);
|
||||
}
|
||||
|
||||
async function saveSource() {
|
||||
if (saving.value || loading.value || !props.open) return;
|
||||
if (!canEdit.value) {
|
||||
@@ -271,6 +277,10 @@ function closeDialog() {
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="closeDialog">{{ t("common.close") }}</Button>
|
||||
<Button variant="outline" :disabled="loading || saving" :title="t('structureEditor.refresh')" @click="refreshSource">
|
||||
<RefreshCw class="h-4 w-4" />
|
||||
{{ t("structureEditor.refresh") }}
|
||||
</Button>
|
||||
<Button v-if="!editing" variant="outline" :disabled="!content" @click="copySource">
|
||||
<Clipboard class="h-4 w-4" />
|
||||
{{ t("grid.copy") }}
|
||||
|
||||
@@ -1395,7 +1395,7 @@ function requestRefreshSelectedNode(): boolean {
|
||||
|
||||
function canRefreshTreeNodeShortcut(): boolean {
|
||||
const type = activeNode.value.type;
|
||||
if (type === "connection" || type === "database" || type === "schema" || type === "table" || type === "view") {
|
||||
if (type === "connection" || type === "database" || type === "schema" || type === "table" || type === "view" || type === "procedure" || type === "function") {
|
||||
return true;
|
||||
}
|
||||
return isGroupLabel(activeNode.value) && type !== "group-partitions";
|
||||
@@ -6394,6 +6394,12 @@ function buildObjectSidebarMenu(context: SidebarMenuFactoryContext): boolean {
|
||||
}
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({ label: t("contextMenu.changeOpenMode"), action: () => emit("open-settings", "navigation"), icon: Settings2 });
|
||||
items.push({
|
||||
label: t("contextMenu.refreshChildren"),
|
||||
action: refresh,
|
||||
icon: RefreshCw,
|
||||
shortcut: shortcutRefresh,
|
||||
});
|
||||
if (!isPackageMember) {
|
||||
items.push({ label: "", separator: true });
|
||||
items.push({
|
||||
|
||||
@@ -4690,6 +4690,7 @@ export default {
|
||||
cancelEdit: "Cancel",
|
||||
sourceSaved: "Source saved",
|
||||
sourceSaveFailed: "Failed to save source: {message}",
|
||||
refreshDiscardConfirm: "Refreshing will discard unsaved source edits. Continue?",
|
||||
postgresViewColumnChangeHint:
|
||||
"PostgreSQL cannot use CREATE OR REPLACE VIEW to remove, reorder, rename, or change the type of existing view columns. Keep existing column count, order, names, and types unchanged; new columns can only be appended. Use ALTER VIEW ... RENAME COLUMN to rename a column. To remove a column, review dependencies and recreate the view manually.",
|
||||
sourceReadOnly: "This source is read-only and cannot be edited.",
|
||||
|
||||
@@ -4262,6 +4262,7 @@ export default withEnglishFallback({
|
||||
validity: "validez",
|
||||
valid: "válido",
|
||||
invalid: "inválido",
|
||||
refreshDiscardConfirm: "Actualizar descartará los cambios sin guardar en el código fuente, ¿deseas continuar?",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
||||
@@ -4259,6 +4259,7 @@ export default withEnglishFallback({
|
||||
validity: "validità",
|
||||
valid: "valido",
|
||||
invalid: "non valido",
|
||||
refreshDiscardConfirm: "L'aggiornamento eliminerà le modifiche non salvate al codice sorgente. Continuare?",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
||||
@@ -4320,6 +4320,7 @@ export default withEnglishFallback({
|
||||
validity: "有効性",
|
||||
valid: "有効",
|
||||
invalid: "無効",
|
||||
refreshDiscardConfirm: "更新すると、保存されていないソースコードの変更が破棄されます。続行しますか?",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
||||
@@ -4247,6 +4247,7 @@ export default withEnglishFallback({
|
||||
sortAsc: "오름차순",
|
||||
sortDesc: "내림차순",
|
||||
sortBy: "정렬 기준",
|
||||
refreshDiscardConfirm: "새로 고침하면 저장되지 않은 소스 코드 변경 사항이 삭제됩니다. 계속하시겠습니까?",
|
||||
},
|
||||
structureEditor: {
|
||||
title: "테이블 구조 편집",
|
||||
|
||||
@@ -4261,6 +4261,7 @@ export default withEnglishFallback({
|
||||
validity: "Validade",
|
||||
valid: "Válido",
|
||||
invalid: "Inválido",
|
||||
refreshDiscardConfirm: "Atualizar descartará as modificações não salvas no código-fonte. Deseja continuar?",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
||||
@@ -4665,6 +4665,7 @@ export default withEnglishFallback({
|
||||
cancelEdit: "取消",
|
||||
sourceSaved: "源码已保存",
|
||||
sourceSaveFailed: "保存源码失败:{message}",
|
||||
refreshDiscardConfirm: "刷新将丢弃未保存的源码修改,是否继续?",
|
||||
postgresViewColumnChangeHint: "PostgreSQL 无法通过 CREATE OR REPLACE VIEW 删除、重排、重命名已有视图字段,或修改字段类型。请保持原字段的数量、顺序、名称和类型不变;新增字段只能追加在末尾。重命名字段请使用 ALTER VIEW ... RENAME COLUMN;删除字段前请检查依赖并手动重建视图。",
|
||||
sourceReadOnly: "该源码为只读,不能编辑。",
|
||||
schemaColumn: "Schema",
|
||||
|
||||
@@ -4000,6 +4000,7 @@ export default withEnglishFallback({
|
||||
validity: "有效性",
|
||||
valid: "有效",
|
||||
invalid: "無效",
|
||||
refreshDiscardConfirm: "重新整理將捨棄未儲存的原始碼修改,是否繼續?",
|
||||
},
|
||||
structureEditor: {
|
||||
mysqlDataTypeHelp: {
|
||||
|
||||
@@ -2346,7 +2346,19 @@ export const useConnectionStore = defineStore("connection", () => {
|
||||
|
||||
function invalidateMetadataCachesForNode(node: TreeNode, options?: { skipObjectCacheInvalidation?: boolean }) {
|
||||
if (!node.connectionId) return;
|
||||
const tableName = node.tableName || (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "dynamodb-table" ? node.label : undefined);
|
||||
const isObjectLeaf =
|
||||
node.type === "procedure" ||
|
||||
node.type === "function" ||
|
||||
node.type === "sequence" ||
|
||||
node.type === "synonym" ||
|
||||
node.type === "event" ||
|
||||
node.type === "job" ||
|
||||
node.type === "package" ||
|
||||
node.type === "package-body" ||
|
||||
node.type === "type" ||
|
||||
node.type === "type-body" ||
|
||||
node.type === "trigger";
|
||||
const tableName = isObjectLeaf ? node.objectName || node.label : node.tableName || (node.type === "table" || node.type === "view" || node.type === "materialized_view" || node.type === "mongo-collection" || node.type === "dynamodb-table" ? node.label : undefined);
|
||||
const match = {
|
||||
connectionId: node.connectionId,
|
||||
database: node.database || undefined,
|
||||
|
||||
@@ -2873,6 +2873,26 @@ export const useQueryStore = defineStore("query", () => {
|
||||
void loadObjectSourceIntoTab(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-reload an object-source tab from the database (user-triggered refresh).
|
||||
* Callers should confirm before discarding dirty edits.
|
||||
*/
|
||||
function refreshObjectSourceTab(id: string): boolean {
|
||||
const tab = tabs.value.find((candidate) => candidate.id === id);
|
||||
if (!tab) return false;
|
||||
if (tab.sourceLoad && !tab.sourceLoad.error) return true;
|
||||
const request = tab.sourceLoad?.request ? { ...tab.sourceLoad.request } : tab.objectSource ? { name: tab.objectSource.name, objectType: tab.objectSource.objectType, signature: tab.objectSource.signature } : null;
|
||||
if (!request) return false;
|
||||
sourceRevalidateInFlight.delete(id);
|
||||
tab.sourceLoad = {
|
||||
startedAt: Date.now(),
|
||||
initialEditing: tab.sourceLoad?.initialEditing ?? !!tab.objectSource,
|
||||
request,
|
||||
};
|
||||
void loadObjectSourceIntoTab(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
function clearObjectSourceLoad(tab: QueryTab) {
|
||||
tab.sourceLoad = undefined;
|
||||
}
|
||||
@@ -9006,6 +9026,7 @@ export const useQueryStore = defineStore("query", () => {
|
||||
openObjectSourceTab,
|
||||
openObjectSourceTabPending,
|
||||
retryObjectSourceTab,
|
||||
refreshObjectSourceTab,
|
||||
showExecutedQueryResults,
|
||||
focusGroup,
|
||||
activateTab,
|
||||
|
||||
@@ -701,14 +701,32 @@ fn executable_mysql_routine_statements(input: &EditableObjectSourceSqlInput, sou
|
||||
|
||||
let declaration = mysql_routine_declaration(source).filter(|declaration| declaration.kind == input.object_type);
|
||||
let create_name = declaration.as_ref().map(|declaration| declaration.name.as_str()).unwrap_or(&input.name);
|
||||
let mut statements = Vec::with_capacity(3);
|
||||
let is_rename =
|
||||
declaration.as_ref().is_some_and(|declaration| routine_name_changed(&declaration.name, &input.name));
|
||||
let mut statements = Vec::with_capacity(6);
|
||||
|
||||
// MySQL has no cross-version CREATE OR REPLACE for stored routines. Validate the CREATE
|
||||
// body under a temporary name first (same idea as Informix view saves) so a syntax error
|
||||
// cannot leave the original routine deleted after DROP.
|
||||
let validation_name = mysql_validation_routine_name(create_name);
|
||||
if let Some(validation_source) = replace_mysql_routine_declaration_name(source, &validation_name) {
|
||||
statements.push(mysql_drop_routine_if_exists(
|
||||
input.object_type.clone(),
|
||||
input.schema.as_deref(),
|
||||
&validation_name,
|
||||
));
|
||||
statements.push(ensure_semicolon(&validation_source));
|
||||
statements.push(mysql_drop_routine_if_exists(
|
||||
input.object_type.clone(),
|
||||
input.schema.as_deref(),
|
||||
&validation_name,
|
||||
));
|
||||
}
|
||||
|
||||
// MySQL has no cross-version CREATE OR REPLACE for stored routines; DBeaver also
|
||||
// replaces them by dropping the target routine before executing the CREATE body.
|
||||
statements.push(mysql_drop_routine_if_exists(input.object_type.clone(), input.schema.as_deref(), create_name));
|
||||
statements.push(ensure_semicolon(source));
|
||||
|
||||
if declaration.as_ref().is_some_and(|declaration| routine_name_changed(&declaration.name, &input.name)) {
|
||||
if is_rename {
|
||||
statements.push(mysql_drop_routine_if_exists(input.object_type.clone(), input.schema.as_deref(), &input.name));
|
||||
}
|
||||
|
||||
@@ -723,6 +741,15 @@ fn mysql_source_starts_with_create_routine(source: &str) -> bool {
|
||||
Regex::new(r"(?is)^\s*CREATE\s+(?:DEFINER\s*=.+?\s+)?(?:FUNCTION|PROCEDURE)\b").unwrap().is_match(source)
|
||||
}
|
||||
|
||||
fn mysql_validation_routine_name(target_name: &str) -> String {
|
||||
let mut hash = 0x811c9dc5u32;
|
||||
for byte in target_name.bytes() {
|
||||
hash ^= byte as u32;
|
||||
hash = hash.wrapping_mul(0x01000193);
|
||||
}
|
||||
format!("dbx_routine_check_{hash:08x}")
|
||||
}
|
||||
|
||||
fn informix_validation_view_name(target_name: &str) -> String {
|
||||
let mut hash = 0x811c9dc5u32;
|
||||
for byte in target_name.bytes() {
|
||||
@@ -2026,62 +2053,80 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn mysql_routine_rename_adds_drop_cleanup() {
|
||||
let source =
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END";
|
||||
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
||||
database_type: DatabaseType::Mysql,
|
||||
object_type: ObjectSourceKind::Procedure,
|
||||
schema: Some("app".to_string()),
|
||||
name: "refresh_cache".to_string(),
|
||||
source:
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END"
|
||||
.to_string(),
|
||||
source: source.to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
let validation_name = mysql_validation_routine_name("refresh_cache_v2");
|
||||
let validation_source = replace_mysql_routine_declaration_name(source, &validation_name).unwrap();
|
||||
assert_eq!(
|
||||
statements,
|
||||
vec![
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache_v2`;",
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END;",
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;",
|
||||
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
||||
ensure_semicolon(&validation_source),
|
||||
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache_v2`;".to_string(),
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache_v2`(IN mode_name varchar(20)) BEGIN SELECT 1; END;"
|
||||
.to_string(),
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_procedure_save_replaces_existing_routine() {
|
||||
let source = "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END";
|
||||
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
||||
database_type: DatabaseType::Mysql,
|
||||
object_type: ObjectSourceKind::Procedure,
|
||||
schema: Some("app".to_string()),
|
||||
name: "refresh_cache".to_string(),
|
||||
source: "CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END".to_string(),
|
||||
source: source.to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
let validation_name = mysql_validation_routine_name("refresh_cache");
|
||||
let validation_source = replace_mysql_routine_declaration_name(source, &validation_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
statements,
|
||||
vec![
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;",
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END;",
|
||||
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
||||
ensure_semicolon(&validation_source),
|
||||
format!("DROP PROCEDURE IF EXISTS `app`.`{validation_name}`;"),
|
||||
"DROP PROCEDURE IF EXISTS `app`.`refresh_cache`;".to_string(),
|
||||
"CREATE DEFINER=`root`@`%` PROCEDURE `refresh_cache`() BEGIN SELECT 1; END;".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mysql_function_save_replaces_existing_routine() {
|
||||
let source = "CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1";
|
||||
let statements = build_executable_object_source_statements(EditableObjectSourceSqlInput {
|
||||
database_type: DatabaseType::Mysql,
|
||||
object_type: ObjectSourceKind::Function,
|
||||
schema: Some("app".to_string()),
|
||||
name: "active_count".to_string(),
|
||||
source: "CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1".to_string(),
|
||||
source: source.to_string(),
|
||||
})
|
||||
.unwrap();
|
||||
let validation_name = mysql_validation_routine_name("active_count");
|
||||
let validation_source = replace_mysql_routine_declaration_name(source, &validation_name).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
statements,
|
||||
vec![
|
||||
"DROP FUNCTION IF EXISTS `app`.`active_count`;",
|
||||
"CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1;",
|
||||
format!("DROP FUNCTION IF EXISTS `app`.`{validation_name}`;"),
|
||||
ensure_semicolon(&validation_source),
|
||||
format!("DROP FUNCTION IF EXISTS `app`.`{validation_name}`;"),
|
||||
"DROP FUNCTION IF EXISTS `app`.`active_count`;".to_string(),
|
||||
"CREATE DEFINER=CURRENT_USER FUNCTION `active_count`() RETURNS INT RETURN 1;".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user