diff --git a/src/common/customizations/postgresql.ts b/src/common/customizations/postgresql.ts index 5098a1bc..7c5ad703 100644 --- a/src/common/customizations/postgresql.ts +++ b/src/common/customizations/postgresql.ts @@ -43,6 +43,7 @@ export const customizations: Customizations = { tableDuplicate: true, tableDdl: true, viewAdd: true, + materializedViewAdd: true, triggerAdd: true, triggerFunctionAdd: true, routineAdd: true, @@ -53,6 +54,7 @@ export const customizations: Customizations = { databaseEdit: false, tableSettings: true, viewSettings: true, + materializedViewSettings: true, triggerSettings: true, triggerFunctionSettings: true, routineSettings: true, diff --git a/src/common/interfaces/customizations.ts b/src/common/interfaces/customizations.ts index 404f6be1..49d253c4 100644 --- a/src/common/interfaces/customizations.ts +++ b/src/common/interfaces/customizations.ts @@ -46,6 +46,8 @@ export interface Customizations { tableDdl?: boolean; viewAdd?: boolean; viewSettings?: boolean; + materializedViewAdd?: boolean; + materializedViewSettings?: boolean; triggerAdd?: boolean; triggerFunctionAdd?: boolean; routineAdd?: boolean; diff --git a/src/main/ipc-handlers/views.ts b/src/main/ipc-handlers/views.ts index 128be228..dfd016aa 100644 --- a/src/main/ipc-handlers/views.ts +++ b/src/main/ipc-handlers/views.ts @@ -51,4 +51,52 @@ export default (connections: Record) => { return { status: 'error', response: err.toString() }; } }); + + ipcMain.handle('get-materialized-view-informations', async (event, params) => { + if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; + + try { + const result = await connections[params.uid].getMaterializedViewInformations(params); + return { status: 'success', response: result }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('drop-materialized-view', async (event, params) => { + if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; + + try { + await connections[params.uid].dropMaterializedView(params); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('alter-materialized-view', async (event, params) => { + if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; + + try { + await connections[params.uid].alterView(params); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); + + ipcMain.handle('create-materialized-view', async (event, params) => { + if (!validateSender(event.senderFrame)) return { status: 'error', response: 'Unauthorized process' }; + + try { + await connections[params.uid].createMaterializedView(params); + return { status: 'success' }; + } + catch (err) { + return { status: 'error', response: err.toString() }; + } + }); }; diff --git a/src/main/libs/clients/BaseClient.ts b/src/main/libs/clients/BaseClient.ts index 22bd63d9..0c79e5d5 100644 --- a/src/main/libs/clients/BaseClient.ts +++ b/src/main/libs/clients/BaseClient.ts @@ -234,6 +234,18 @@ export abstract class BaseClient { throw new Error('Method "getVariables" not implemented'); } + getMaterializedViewInformations (...args: any) { + throw new Error('Method "getMaterializedViewInformations" not implemented'); + } + + dropMaterializedView (...args: any) { + throw new Error('Method "dropMaterializedView" not implemented'); + } + + createMaterializedView (...args: any) { + throw new Error('Method "createMaterializedView" not implemented'); + } + getEventInformations (...args: any) { throw new Error('Method "getEventInformations" not implemented'); } diff --git a/src/main/libs/clients/PostgreSQLClient.ts b/src/main/libs/clients/PostgreSQLClient.ts index af20f8d6..df863e3a 100644 --- a/src/main/libs/clients/PostgreSQLClient.ts +++ b/src/main/libs/clients/PostgreSQLClient.ts @@ -335,6 +335,19 @@ export class PostgreSQLClient extends BaseClient { ORDER BY table_name `); + let { rows: matViews } = await this.raw>(` + SELECT schemaname AS schema_name, + matviewname AS table_name, + matviewowner AS owner, + ispopulated AS is_populated, + definition, + 'materializedview' AS table_type + FROM pg_matviews + WHERE schemaname = '${db.database}' + ORDER BY schema_name, + table_name; + `); + if (tables.length) { tables = tables.map(table => { table.Db = db.database; @@ -343,6 +356,14 @@ export class PostgreSQLClient extends BaseClient { tablesArr.push(...tables); } + if (matViews.length) { + matViews = matViews.map(view => { + view.Db = db.database; + return view; + }); + tablesArr.push(...matViews); + } + let { rows: triggers } = await this.raw>(` SELECT pg_class.relname AS table_name, @@ -378,7 +399,11 @@ export class PostgreSQLClient extends BaseClient { return { name: table.table_name, - type: table.table_type === 'VIEW' ? 'view' : 'table', + type: table.table_type === 'VIEW' + ? 'view' + : table.table_type === 'materializedview' + ? 'materializedview' + : 'table', rows: table.reltuples, size: tableSize, collation: table.Collation, @@ -1056,11 +1081,32 @@ export class PostgreSQLClient extends BaseClient { })[0]; } + async getMaterializedViewInformations ({ schema, view }: { schema: string; view: string }) { + const sql = `SELECT "definition" FROM "pg_matviews" WHERE "matviewname"='${view}' AND "schemaname"='${schema}'`; + const results = await this.raw(sql); + + return results.rows.map(row => { + return { + algorithm: '', + definer: '', + security: '', + updateOption: '', + sql: row.definition, + name: view + }; + })[0]; + } + async dropView (params: { schema: string; view: string }) { const sql = `DROP VIEW "${params.schema}"."${params.view}"`; return await this.raw(sql); } + async dropMaterializedView (params: { schema: string; view: string }) { + const sql = `DROP MATERIALIZED VIEW "${params.schema}"."${params.view}"`; + return await this.raw(sql); + } + async alterView ({ view }: { view: antares.AlterViewParams }) { let sql = `CREATE OR REPLACE VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`; @@ -1070,11 +1116,25 @@ export class PostgreSQLClient extends BaseClient { return await this.raw(sql); } + async alterMaterializedView ({ view }: { view: antares.AlterViewParams }) { + let sql = `CREATE OR REPLACE MATERIALIZED VIEW "${view.schema}"."${view.oldName}" AS ${view.sql}`; + + if (view.name !== view.oldName) + sql += `; ALTER VIEW "${view.schema}"."${view.oldName}" RENAME TO "${view.name}"`; + + return await this.raw(sql); + } + async createView (params: antares.CreateViewParams) { const sql = `CREATE VIEW "${params.schema}"."${params.name}" AS ${params.sql}`; return await this.raw(sql); } + async createMaterializedView (params: antares.CreateViewParams) { + const sql = `CREATE MATERIALIZED VIEW "${params.schema}"."${params.name}" AS ${params.sql}`; + return await this.raw(sql); + } + async getTriggerInformations ({ schema, trigger }: { schema: string; trigger: string }) { const [table, triggerName] = trigger.split('.'); diff --git a/src/renderer/components/Workspace.vue b/src/renderer/components/Workspace.vue index 80f74aad..d3a69be0 100644 --- a/src/renderer/components/Workspace.vue +++ b/src/renderer/components/Workspace.vue @@ -63,7 +63,7 @@ > @@ -80,7 +80,7 @@ @@ -157,6 +157,27 @@ + + + + {{ cutText(element.elementName, 20, true) }} + + + + + + + + {{ t('database.newMaterializedView') }} + + + + + + ({ get () { if (workspace.value.customizations.database) return workspace.value.tabs.filter(tab => tab.type === 'query' || tab.database === workspace.value.database); - else return workspace.value.tabs; }, @@ -689,6 +749,7 @@ const openAsPermanentTab = (tab: WorkspaceTab) => { const permanentTabs = { table: 'data', view: 'data', + materializedView: 'data', trigger: 'trigger-props', triggerFunction: 'trigger-function-props', function: 'function-props', diff --git a/src/renderer/components/WorkspaceExploreBar.vue b/src/renderer/components/WorkspaceExploreBar.vue index 219af6a4..bd48640b 100644 --- a/src/renderer/components/WorkspaceExploreBar.vue +++ b/src/renderer/components/WorkspaceExploreBar.vue @@ -111,6 +111,7 @@ @close-context="closeDatabaseContext" @open-create-table-tab="openCreateElementTab('table')" @open-create-view-tab="openCreateElementTab('view')" + @open-create-materialized-view-tab="openCreateElementTab('materialized-view')" @open-create-trigger-tab="openCreateElementTab('trigger')" @open-create-routine-tab="openCreateElementTab('routine')" @open-create-function-tab="openCreateElementTab('function')" @@ -142,6 +143,7 @@ :selected-schema="selectedSchema" :context-event="miscContextEvent" @open-create-view-tab="openCreateElementTab('view')" + @open-create-materializedview-tab="openCreateElementTab('materialized-view')" @open-create-trigger-tab="openCreateElementTab('trigger')" @open-create-trigger-function-tab="openCreateElementTab('trigger-function')" @open-create-routine-tab="openCreateElementTab('routine')" diff --git a/src/renderer/components/WorkspaceExploreBarMiscFolderContext.vue b/src/renderer/components/WorkspaceExploreBarMiscFolderContext.vue index 5d3b9803..e83984e9 100644 --- a/src/renderer/components/WorkspaceExploreBarMiscFolderContext.vue +++ b/src/renderer/components/WorkspaceExploreBarMiscFolderContext.vue @@ -15,6 +15,18 @@ :size="18" /> {{ t('database.createNewView') }} +
+ + {{ t('database.createNewMaterializedView') }} +
+
+
{ if (props.searchMethod === 'elements') return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'view'); else - return props.database.tables; + return props.database.tables.filter(table => table.type === 'view'); +}); + +const filteredMatViews = computed(() => { + if (props.searchMethod === 'elements') + return props.database.tables.filter(table => table.name.search(searchTerm.value) >= 0 && table.type === 'materializedview'); + else + return props.database.tables.filter(table => table.type === 'materializedview'); }); const filteredTriggers = computed(() => { diff --git a/src/renderer/components/WorkspaceExploreBarSchemaContext.vue b/src/renderer/components/WorkspaceExploreBarSchemaContext.vue index 1644d427..11ff8889 100644 --- a/src/renderer/components/WorkspaceExploreBarSchemaContext.vue +++ b/src/renderer/components/WorkspaceExploreBarSchemaContext.vue @@ -40,6 +40,18 @@ :size="18" /> {{ t('database.view') }}
+
+ + {{ t('database.materializedview') }} +
{ emit('open-create-view-tab'); }; +const openCreateMaterializedViewTab = () => { + emit('open-create-materialized-view-tab'); +}; + const openCreateTriggerTab = () => { emit('open-create-trigger-tab'); }; diff --git a/src/renderer/components/WorkspaceExploreBarTableContext.vue b/src/renderer/components/WorkspaceExploreBarTableContext.vue index e9741047..279584a1 100644 --- a/src/renderer/components/WorkspaceExploreBarTableContext.vue +++ b/src/renderer/components/WorkspaceExploreBarTableContext.vue @@ -47,6 +47,18 @@ :size="18" /> {{ t('application.settings') }}
+
+ + {{ t('application.settings') }} +
{ closeContext(); }; +const openMaterializedViewSettingTab = () => { + newTab({ + uid: selectedWorkspace.value, + elementType: 'table', + elementName: props.selectedTable.name, + schema: props.selectedSchema, + type: 'materialized-view-props' + }); + + changeBreadcrumbs({ + schema: props.selectedSchema, + view: props.selectedTable.name + }); + + closeContext(); +}; + const duplicateTable = () => { emit('duplicate-table', { schema: props.selectedSchema, table: props.selectedTable }); }; diff --git a/src/renderer/components/WorkspaceTabNewMaterializedView.vue b/src/renderer/components/WorkspaceTabNewMaterializedView.vue new file mode 100644 index 00000000..23c7f5d2 --- /dev/null +++ b/src/renderer/components/WorkspaceTabNewMaterializedView.vue @@ -0,0 +1,293 @@ + + + diff --git a/src/renderer/components/WorkspaceTabPropsMaterializedView.vue b/src/renderer/components/WorkspaceTabPropsMaterializedView.vue new file mode 100644 index 00000000..86176fac --- /dev/null +++ b/src/renderer/components/WorkspaceTabPropsMaterializedView.vue @@ -0,0 +1,316 @@ + + + diff --git a/src/renderer/components/WorkspaceTabTable.vue b/src/renderer/components/WorkspaceTabTable.vue index 5da75611..91fe1eda 100644 --- a/src/renderer/components/WorkspaceTabTable.vue +++ b/src/renderer/components/WorkspaceTabTable.vue @@ -305,7 +305,7 @@ const customizations = computed(() => { }); const isTable = computed(() => { - return !workspace.value.breadcrumbs.view; + return props.elementType === 'table'; }); const fields = computed(() => { @@ -499,8 +499,8 @@ const openTableSettingTab = () => { uid: workspace.value.uid, elementName: props.table, schema: props.schema, - type: isTable.value ? 'table-props' : 'view-props', - elementType: isTable.value ? 'table' : 'view' + type: isTable.value ? 'table-props' : props.elementType === 'view' ? 'view-props' : 'materialized-view-props', + elementType: props.elementType }); changeBreadcrumbs({ diff --git a/src/renderer/i18n/en-US.ts b/src/renderer/i18n/en-US.ts index 2f0f08e1..01757e1d 100644 --- a/src/renderer/i18n/en-US.ts +++ b/src/renderer/i18n/en-US.ts @@ -140,6 +140,7 @@ export const enUS = { total: 'Total', table: 'Table | Tables', view: 'View | Views', + materializedview: 'Materialized view | Materialized views', definer: 'Definer', algorithm: 'Algorithm', trigger: 'Trigger | Triggers', @@ -216,6 +217,7 @@ export const enUS = { updateOption: 'Update option', deleteView: 'Delete view', createNewView: 'Create new view', + createNewMaterializedView: 'Create new materialized view', deleteTrigger: 'Delete trigger', createNewTrigger: 'Create new trigger', currentUser: 'Current user', @@ -248,6 +250,7 @@ export const enUS = { thereAreNoTableFields: 'There are no table fields', newTable: 'New table', newView: 'New view', + newMaterializedView: 'New materialized view', newTrigger: 'New trigger', newRoutine: 'New routine', newFunction: 'New function', diff --git a/src/renderer/ipc-api/Views.ts b/src/renderer/ipc-api/Views.ts index 625c7b5d..18e5cba9 100644 --- a/src/renderer/ipc-api/Views.ts +++ b/src/renderer/ipc-api/Views.ts @@ -19,4 +19,20 @@ export default class { static createView (params: CreateViewParams & { uid: string }): Promise { return ipcRenderer.invoke('create-view', unproxify(params)); } + + static createMaterializedView (params: CreateViewParams & { uid: string }): Promise { + return ipcRenderer.invoke('create-materialized-view', unproxify(params)); + } + + static getMaterializedViewInformations (params: { uid: string; schema: string; view: string }): Promise { + return ipcRenderer.invoke('get-materialized-view-informations', unproxify(params)); + } + + static dropMaterializedView (params: { uid: string; schema: string; view: string }): Promise { + return ipcRenderer.invoke('drop-materialized-view', unproxify(params)); + } + + static alterMaterializedView (params: { view: AlterViewParams & { uid: string }}): Promise { + return ipcRenderer.invoke('alter-materialized-view', unproxify(params)); + } } diff --git a/src/renderer/stores/workspaces.ts b/src/renderer/stores/workspaces.ts index 9a5f06c0..591dff65 100644 --- a/src/renderer/stores/workspaces.ts +++ b/src/renderer/stores/workspaces.ts @@ -558,6 +558,8 @@ export const useWorkspacesStore = defineStore('workspaces', { switch (type) { case 'new-table': + case 'new-view': + case 'new-materialized-view': case 'new-trigger': case 'new-trigger-function': case 'new-function': @@ -659,6 +661,8 @@ export const useWorkspacesStore = defineStore('workspaces', { break; case 'data': case 'table-props': + case 'view-props': + case 'materialized-view-props': case 'trigger-props': case 'trigger-function-props': case 'function-props':