From 3066a8eaaa23eb6681dbad99d221cdd9ceb8065f Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 5 Mar 2025 14:24:32 +0200 Subject: [PATCH 1/8] HCK-10265: add materialized view property config --- .../view_level/viewLevelConfig.json | 476 +++++++++++++++++- 1 file changed, 472 insertions(+), 4 deletions(-) diff --git a/properties_pane/view_level/viewLevelConfig.json b/properties_pane/view_level/viewLevelConfig.json index 93fdbf12..ef5b9356 100644 --- a/properties_pane/view_level/viewLevelConfig.json +++ b/properties_pane/view_level/viewLevelConfig.json @@ -124,27 +124,458 @@ making sure that you maintain a proper JSON format. "addTimestampButton": false, "template": "textarea" }, + { + "propertyName": "Materialized", + "propertyKeyword": "materialized", + "propertyTooltip": "Specify whether to create materialized view.", + "propertyType": "checkbox" + }, + { + "propertyName": "If not exists", + "propertyKeyword": "ifNotExist", + "propertyTooltip": "When the IF NOT EXISTS clause is used, PostgreSQL will return a warning instead of an error if the specified view already exists.", + "propertyType": "checkbox", + "defaultValue": true, + "dependency": { + "key": "materialized", + "value": true + } + }, + { + "propertyName": "Using method", + "propertyKeyword": "usingMethod", + "propertyTooltip": "Optional clause to specify the view access method to use to store the contents for the new view; the method needs be an access method of type VIEW.", + "propertyType": "text", + "dependency": { + "key": "materialized", + "value": true + } + }, + { + "propertyName": "Storage parameters", + "propertyKeyword": "storage_parameter", + "propertyType": "block", + "propertyTooltip": "For each individual materialized view you create, you can set some materialized view options.", + "dependency": { + "key": "materialized", + "value": true + }, + "structure": [ + { + "propertyName": "Fill factor", + "propertyKeyword": "fillfactor", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "A percentage between 10 and 100. Complete packing (100) is the default.", + "minValue": 10, + "maxValue": 100, + "step": 1 + }, + { + "propertyName": "Parallel workers", + "propertyKeyword": "parallel_workers", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "This sets the number of workers that should be used to assist a parallel scan of this table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Enable autovacuum", + "propertyKeyword": "autovacuum_enabled", + "propertyType": "checkbox", + "propertyTooltip": "If true, the autovacuum daemon will perform automatic VACUUM and/or ANALYZE operations on this table.", + "defaultValue": false + }, + { + "propertyName": "Autovacuum params", + "propertyKeyword": "autovacuum", + "propertyType": "block", + "propertyTooltip": "Vacuum parameters", + "dependency": { + "key": "autovacuum_enabled", + "value": true + }, + "structure": [ + { + "propertyName": "Vacuum index cleanup", + "propertyKeyword": "vacuum_index_cleanup", + "propertyType": "checkbox", + "propertyTooltip": "Disabling index cleanup can speed up VACUUM very significantly, but may also lead to severely bloated indexes if table modifications are frequent." + }, + { + "propertyName": "Vacuum truncate", + "propertyKeyword": "vacuum_truncate", + "propertyType": "checkbox", + "propertyTooltip": "If true, VACUUM and autovacuum do the truncation and the disk space for the truncated pages is returned to the operating system." + }, + { + "propertyName": "Vacuum threshold", + "propertyKeyword": "autovacuum_vacuum_threshold", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the minimum number of updated or deleted tuples needed to trigger a VACUUM in any one table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Vacuum scale factor", + "propertyKeyword": "autovacuum_vacuum_scale_factor", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies a fraction of the table size to add to autovacuum_vacuum_threshold when deciding whether to trigger a VACUUM.", + "minValue": 0 + }, + { + "propertyName": "Insert threshold", + "propertyKeyword": "autovacuum_vacuum_insert_threshold", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the number of inserted tuples needed to trigger a VACUUM in any one table. ", + "minValue": -1, + "step": 1 + }, + { + "propertyName": "Insert scale factor", + "propertyKeyword": "autovacuum_vacuum_insert_scale_factor", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies a fraction of the table size to add to autovacuum_vacuum_insert_threshold when deciding whether to trigger a VACUUM.", + "minValue": 0 + }, + { + "propertyName": "Analyze threshold", + "propertyKeyword": "autovacuum_analyze_threshold", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the minimum number of inserted, updated or deleted tuples needed to trigger an ANALYZE in any one table. ", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Analyze scale factor", + "propertyKeyword": "autovacuum_analyze_scale_factor", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies a fraction of the table size to add to autovacuum_analyze_threshold when deciding whether to trigger an ANALYZE. ", + "minValue": 0 + }, + { + "propertyName": "Vacuum cost delay", + "propertyKeyword": "autovacuum_vacuum_cost_delay", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cost delay value that will be used in automatic VACUUM operations. If -1 is specified, the regular vacuum_cost_delay value will be used. The value is specified as milliseconds.", + "minValue": -1 + }, + { + "propertyName": "Vacuum cost limit", + "propertyKeyword": "autovacuum_vacuum_cost_limit", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cost limit value that will be used in automatic VACUUM operations. If -1 is specified (which is the default), the regular vacuum_cost_limit value will be used.", + "minValue": -1, + "step": 1 + }, + { + "propertyName": "Freeze min age", + "propertyKeyword": "autovacuum_freeze_min_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cutoff age (in transactions) that VACUUM should use to decide whether to freeze row versions while scanning a table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Freeze max age", + "propertyKeyword": "autovacuum_freeze_max_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the maximum age (in transactions) that a table's pg_class.relfrozenxid field can attain before a VACUUM operation is forced to prevent transaction ID wraparound within the table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Freeze table age", + "propertyKeyword": "autovacuum_freeze_table_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Although users can set this value anywhere from zero to two billion, VACUUM will silently limit the effective value to 95% of autovacuum_freeze_max_age.", + "minValue": 0, + "maxValue": 2000000000, + "step": 1 + }, + { + "propertyName": "Multixact freeze min age", + "propertyKeyword": "autovacuum_multixact_freeze_min_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cutoff age (in multixacts) that VACUUM should use to decide whether to replace multixact IDs with a newer transaction ID or multixact ID while scanning a table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Multixact freeze max age", + "propertyKeyword": "autovacuum_multixact_freeze_max_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the maximum age (in multixacts) that a table's pg_class.relminmxid field can attain before a VACUUM operation is forced to prevent multixact ID wraparound within the table.", + "minValue": 0, + "maxValue": 4000000000, + "step": 1 + }, + { + "propertyName": "Multixact freeze table age", + "propertyKeyword": "autovacuum_multixact_freeze_table_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Although users can set this value anywhere from zero to two billion, VACUUM will silently limit the effective value to 95% of autovacuum_multixact_freeze_max_age, so that a periodic manual VACUUM has a chance to run before an anti-wraparound is launched for the table.", + "minValue": 0 + }, + { + "propertyName": "Log min duration", + "propertyKeyword": "log_autovacuum_min_duration", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "It is taken as milliseconds. Causes each action executed by autovacuum to be logged if it ran for at least the specified amount of time. Setting this to zero logs all autovacuum actions. -1 (the default) disables logging autovacuum actions.", + "minValue": -1, + "step": 1 + } + ] + }, + { + "propertyName": "Enable TOAST autovacuum", + "propertyKeyword": "toast_autovacuum_enabled", + "propertyType": "checkbox", + "propertyTooltip": "If true, the autovacuum daemon will perform automatic VACUUM and/or ANALYZE operations on this table.", + "defaultValue": false + }, + { + "propertyName": "TOAST parameters", + "propertyKeyword": "toast", + "propertyType": "block", + "propertyTooltip": "Toast parameters", + "dependency": { + "key": "toast_autovacuum_enabled", + "value": true + }, + "structure": [ + { + "propertyName": "Toast tuple target", + "propertyKeyword": "toast_tuple_target", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "The toast_tuple_target specifies the minimum tuple length required before we try to compress and/or move long column values into TOAST tables, and is also the target length we try to reduce the length below once toasting begins.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Vacuum index cleanup", + "propertyKeyword": "toast_vacuum_index_cleanup", + "propertyType": "checkbox", + "propertyTooltip": "Disabling index cleanup can speed up VACUUM very significantly, but may also lead to severely bloated indexes if table modifications are frequent." + }, + { + "propertyName": "Vacuum truncate", + "propertyKeyword": "toast_vacuum_truncate", + "propertyType": "checkbox", + "propertyTooltip": "If true, VACUUM and autovacuum do the truncation and the disk space for the truncated pages is returned to the operating system." + }, + { + "propertyName": "Vacuum threshold", + "propertyKeyword": "toast_autovacuum_vacuum_threshold", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the minimum number of updated or deleted tuples needed to trigger a VACUUM in any one table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Vacuum scale factor", + "propertyKeyword": "toast_autovacuum_vacuum_scale_factor", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies a fraction of the table size to add to autovacuum_vacuum_threshold when deciding whether to trigger a VACUUM.", + "minValue": 0 + }, + { + "propertyName": "Insert threshold", + "propertyKeyword": "toast_autovacuum_vacuum_insert_threshold", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the number of inserted tuples needed to trigger a VACUUM in any one table. ", + "minValue": -1, + "step": 1 + }, + { + "propertyName": "Insert scale factor", + "propertyKeyword": "toast_autovacuum_vacuum_insert_scale_factor", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies a fraction of the table size to add to autovacuum_vacuum_insert_threshold when deciding whether to trigger a VACUUM.", + "minValue": 0 + }, + { + "propertyName": "Vacuum cost delay", + "propertyKeyword": "toast_autovacuum_vacuum_cost_delay", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cost delay value that will be used in automatic VACUUM operations. If -1 is specified, the regular vacuum_cost_delay value will be used. The value is specified as milliseconds.", + "minValue": -1 + }, + { + "propertyName": "Vacuum cost limit", + "propertyKeyword": "toast_autovacuum_vacuum_cost_limit", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cost limit value that will be used in automatic VACUUM operations. If -1 is specified (which is the default), the regular vacuum_cost_limit value will be used.", + "minValue": -1, + "step": 1 + }, + { + "propertyName": "Freeze min age", + "propertyKeyword": "toast_autovacuum_freeze_min_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cutoff age (in transactions) that VACUUM should use to decide whether to freeze row versions while scanning a table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Freeze max age", + "propertyKeyword": "toast_autovacuum_freeze_max_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the maximum age (in transactions) that a table's pg_class.relfrozenxid field can attain before a VACUUM operation is forced to prevent transaction ID wraparound within the table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Freeze table age", + "propertyKeyword": "toast_autovacuum_freeze_table_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Although users can set this value anywhere from zero to two billion, VACUUM will silently limit the effective value to 95% of autovacuum_freeze_max_age.", + "minValue": 0, + "maxValue": 2000000000, + "step": 1 + }, + { + "propertyName": "Multixact freeze min age", + "propertyKeyword": "toast_autovacuum_multixact_freeze_min_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the cutoff age (in multixacts) that VACUUM should use to decide whether to replace multixact IDs with a newer transaction ID or multixact ID while scanning a table.", + "minValue": 0, + "step": 1 + }, + { + "propertyName": "Multixact freeze max age", + "propertyKeyword": "toast_autovacuum_multixact_freeze_max_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Specifies the maximum age (in multixacts) that a table's pg_class.relminmxid field can attain before a VACUUM operation is forced to prevent multixact ID wraparound within the table.", + "minValue": 0, + "maxValue": 4000000000, + "step": 1 + }, + { + "propertyName": "Multixact freeze table age", + "propertyKeyword": "toast_autovacuum_multixact_freeze_table_age", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "Although users can set this value anywhere from zero to two billion, VACUUM will silently limit the effective value to 95% of autovacuum_multixact_freeze_max_age, so that a periodic manual VACUUM has a chance to run before an anti-wraparound is launched for the table.", + "minValue": 0, + "maxValue": 2000000000, + "step": 1 + }, + { + "propertyName": "Log min duration", + "propertyKeyword": "toast_log_autovacuum_min_duration", + "propertyType": "numeric", + "valueType": "number", + "propertyTooltip": "It is taken as milliseconds. Causes each action executed by autovacuum to be logged if it ran for at least the specified amount of time. Setting this to zero logs all autovacuum actions. -1 (the default) disables logging autovacuum actions.", + "minValue": -1, + "step": 1 + } + ] + }, + { + "propertyName": "User catalog table", + "propertyKeyword": "user_catalog_table", + "propertyType": "checkbox", + "propertyTooltip": "Declare the table as an additional catalog table for purposes of logical replication." + } + ] + }, + { + "propertyName": "Tablespace", + "propertyKeyword": "tablespace_name", + "propertyTooltip": "Enter the name of an existing tablespace location for the database, or pg_default", + "defaultValue": "pg_default", + "propertyType": "text", + "dependency": { + "key": "materialized", + "value": true + } + }, { "propertyName": "Or replace", "propertyKeyword": "orReplace", "defaultValue": false, - "propertyType": "checkbox" + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] + } }, { "propertyName": "Temporary", "propertyKeyword": "temporary", - "propertyType": "checkbox" + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] + } }, { "propertyName": "Recursive", "propertyKeyword": "recursive", - "propertyType": "checkbox" + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] + } }, { "propertyName": "View option", "propertyKeyword": "viewOptions", "propertyType": "block", "propertyTooltip": "This clause specifies optional parameters for a view", + "dependency": { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] + }, "structure": [ { "propertyName": "Check testing scope", @@ -175,11 +606,30 @@ making sure that you maintain a proper JSON format. "template": "textarea", "markdown": false }, + { + "propertyName": "Populated with data at creation", + "propertyKeyword": "withDataOption", + "propertyType": "checkbox", + "propertyTooltip": "This clause specifies whether or not the materialized view should be populated at creation time. If not, the materialized view will be flagged as unscannable and cannot be queried until REFRESH MATERIALIZED VIEW is used.", + "dependency": { + "key": "materialized", + "value": true + } + }, { "propertyName": "With check option", "propertyKeyword": "withCheckOption", "propertyTooltip": "This clause specifies optional parameters for a view; the following parameters are supported", - "propertyType": "checkbox" + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] + } }, { "propertyName": "Check testing scope", @@ -193,6 +643,15 @@ making sure that you maintain a proper JSON format. { "key": "withCheckOption", "value": true + }, + { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] } ] } @@ -224,6 +683,15 @@ making sure that you maintain a proper JSON format. "propertyType": "group", "propertyKeyword": "triggers", "propertyTooltip": "", + "dependency": { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] + }, "structure": [ { "propertyName": "Name", From 97d3694abdfeab06862eeebed26120bd65cb6a39 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 5 Mar 2025 14:25:12 +0200 Subject: [PATCH 2/8] HCK-10265: add create statement for materialized view --- .../ddlHelpers/materializedViewHelper.js | 41 +++++++++++++++++++ .../ddlProvider/ddlHelpers/tableHelper.js | 2 + .../ddlProvider/ddlProvider.js | 23 +++++++++++ forward_engineering/ddlProvider/templates.js | 3 ++ 4 files changed, 69 insertions(+) create mode 100644 forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js diff --git a/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js new file mode 100644 index 00000000..4a216e32 --- /dev/null +++ b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js @@ -0,0 +1,41 @@ +/** + * @typedef {Record} ViewData + */ +const { trim } = require('lodash'); +const { getStorageParameters, getBasicValue } = require('./tableHelper'); + +/** + * @param {{ viewData: ViewData }} + * @returns {string} + */ +const getOptions = ({ viewData }) => { + const configs = [ + { key: 'usingMethod', getValue: getBasicValue('USING') }, + { key: 'storage_parameter', getValue: getStorageParameters }, + { key: 'tablespace_name', getValue: getBasicValue('TABLESPACE') }, + ]; + + const statements = configs + .map(config => config.getValue(viewData[config.key], viewData)) + .filter(Boolean) + .join('\n'); + + return trim(statements) ? ` ${trim(statements)}` : ''; +}; + +/** + * @param {{ viewData: ViewData }} + * @returns {string} + */ +const getWithDataClause = ({ viewData }) => { + if (viewData.withDataOption) { + return '\nWITH DATA'; + } + + return '\nWITH NO DATA'; +}; + +module.exports = { + getOptions, + getWithDataClause, +}; diff --git a/forward_engineering/ddlProvider/ddlHelpers/tableHelper.js b/forward_engineering/ddlProvider/ddlHelpers/tableHelper.js index 7521a6c1..82fabf64 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/tableHelper.js +++ b/forward_engineering/ddlProvider/ddlHelpers/tableHelper.js @@ -119,6 +119,8 @@ const getStorageParameters = value => { }; module.exports = { + getBasicValue, getTableTemporaryValue, getTableOptions, + getStorageParameters, }; diff --git a/forward_engineering/ddlProvider/ddlProvider.js b/forward_engineering/ddlProvider/ddlProvider.js index 1b5e567f..47e77572 100644 --- a/forward_engineering/ddlProvider/ddlProvider.js +++ b/forward_engineering/ddlProvider/ddlProvider.js @@ -46,6 +46,7 @@ const { } = require('./ddlHelpers/columnDefinitionHelper'); const { getTriggersScript, hydrateTriggers } = require('./ddlHelpers/triggerHelper'); const { getLocaleProperties } = require('./ddlHelpers/databaseHelper'); +const materializedViewHelper = require('./ddlHelpers/materializedViewHelper'); module.exports = (baseProvider, options, app) => { return { @@ -494,6 +495,22 @@ module.exports = (baseProvider, options, app) => { } }; + if (viewData.materialized) { + const createViewScript = commentIfDeactivated( + assignTemplates(templates.createMaterializedView, { + name: viewName, + ifNotExist: viewData.ifNotExist ? ' IF NOT EXISTS' : '', + options: materializedViewHelper.getOptions({ viewData }), + comment: viewData.comment ? comment : '', + withDataClause: materializedViewHelper.getWithDataClause({ viewData }), + selectStatement, + }), + { isActivated: !deactivatedWholeStatement }, + ); + + return createViewScript + '\n'; + } + const createViewScript = commentIfDeactivated( assignTemplates(templates.createView, { name: viewName, @@ -741,6 +758,12 @@ module.exports = (baseProvider, options, app) => { withCheckOption: detailsTab.withCheckOption, checkTestingScope: detailsTab.withCheckOption ? detailsTab.checkTestingScope : '', schemaName: viewData.schemaData.schemaName, + materialized: detailsTab.materialized, + ifNotExist: detailsTab.ifNotExist, + usingMethod: detailsTab.usingMethod, + storage_parameter: detailsTab.storage_parameter, + tablespace_name: detailsTab.tablespace_name, + withDataOption: detailsTab.withDataOption, triggers, }; }, diff --git a/forward_engineering/ddlProvider/templates.js b/forward_engineering/ddlProvider/templates.js index a965e950..172e0a37 100644 --- a/forward_engineering/ddlProvider/templates.js +++ b/forward_engineering/ddlProvider/templates.js @@ -96,6 +96,9 @@ module.exports = { createView: 'CREATE${orReplace}${temporary} VIEW ${name}${withOptions}\nAS ${selectStatement}${checkOption};\n\n${comment}\n', + createMaterializedView: + 'CREATE MATERIALIZED VIEW${ifNotExist} ${name}${options}\nAS ${selectStatement}${withDataClause};\n\n${comment}\n', + viewSelectStatement: 'SELECT ${keys}\n\tFROM ${tableName}', dropView: 'DROP VIEW IF EXISTS ${viewName};', From bfd3d51f0d6d5822d876621dd725860fb4ff7651 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 5 Mar 2025 15:51:54 +0200 Subject: [PATCH 3/8] HCK-10265: rename tablespace property keyword --- .../ddlProvider/ddlHelpers/materializedViewHelper.js | 2 +- forward_engineering/ddlProvider/ddlProvider.js | 2 +- properties_pane/view_level/viewLevelConfig.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js index 4a216e32..b977a7ce 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js +++ b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js @@ -12,7 +12,7 @@ const getOptions = ({ viewData }) => { const configs = [ { key: 'usingMethod', getValue: getBasicValue('USING') }, { key: 'storage_parameter', getValue: getStorageParameters }, - { key: 'tablespace_name', getValue: getBasicValue('TABLESPACE') }, + { key: 'view_tablespace_name', getValue: getBasicValue('TABLESPACE') }, ]; const statements = configs diff --git a/forward_engineering/ddlProvider/ddlProvider.js b/forward_engineering/ddlProvider/ddlProvider.js index 47e77572..fc666754 100644 --- a/forward_engineering/ddlProvider/ddlProvider.js +++ b/forward_engineering/ddlProvider/ddlProvider.js @@ -762,7 +762,7 @@ module.exports = (baseProvider, options, app) => { ifNotExist: detailsTab.ifNotExist, usingMethod: detailsTab.usingMethod, storage_parameter: detailsTab.storage_parameter, - tablespace_name: detailsTab.tablespace_name, + view_tablespace_name: detailsTab.view_tablespace_name, withDataOption: detailsTab.withDataOption, triggers, }; diff --git a/properties_pane/view_level/viewLevelConfig.json b/properties_pane/view_level/viewLevelConfig.json index ef5b9356..a1f4bd15 100644 --- a/properties_pane/view_level/viewLevelConfig.json +++ b/properties_pane/view_level/viewLevelConfig.json @@ -510,7 +510,7 @@ making sure that you maintain a proper JSON format. }, { "propertyName": "Tablespace", - "propertyKeyword": "tablespace_name", + "propertyKeyword": "view_tablespace_name", "propertyTooltip": "Enter the name of an existing tablespace location for the database, or pg_default", "defaultValue": "pg_default", "propertyType": "text", From 6e19d0f173046f27f2271e081b32fd885d5b06f3 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 12 Mar 2025 18:45:24 +0200 Subject: [PATCH 4/8] HCK: 10265: add RE from instance --- reverse_engineering/constants/tableType.js | 14 ++++++ .../helpers/postgresHelpers/viewHelper.js | 11 ++++- .../helpers/postgresService.js | 14 ++++-- reverse_engineering/helpers/queryConstants.js | 49 ++++++++++++------- 4 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 reverse_engineering/constants/tableType.js diff --git a/reverse_engineering/constants/tableType.js b/reverse_engineering/constants/tableType.js new file mode 100644 index 00000000..7bc1237e --- /dev/null +++ b/reverse_engineering/constants/tableType.js @@ -0,0 +1,14 @@ +/** + * @enum {string} + */ +const TABLE_TYPE = { + foreignTable: 'f', + regularTable: 'r', + partitionedTable: 'p', + materializedView: 'm', + view: 'v', +}; + +module.exports = { + TABLE_TYPE, +}; diff --git a/reverse_engineering/helpers/postgresHelpers/viewHelper.js b/reverse_engineering/helpers/postgresHelpers/viewHelper.js index 032f540b..db670770 100644 --- a/reverse_engineering/helpers/postgresHelpers/viewHelper.js +++ b/reverse_engineering/helpers/postgresHelpers/viewHelper.js @@ -1,9 +1,11 @@ const _ = require('lodash'); const { clearEmptyPropertiesInObject, wrapInQuotes } = require('./common'); +const { prepareStorageParameters } = require('./tableHelper'); +const { TABLE_TYPE } = require('../../constants/tableType'); const VIEW_SUFFIX = ' (v)'; -const isViewByTableType = table_type => table_type === 'VIEW'; +const isViewByTableType = table_type => [TABLE_TYPE.view, TABLE_TYPE.materializedView].includes(table_type); const isViewByName = name => _.endsWith(name, VIEW_SUFFIX); const removeViewNameSuffix = name => name.slice(0, -VIEW_SUFFIX.length); const setViewSuffix = name => `${name}${VIEW_SUFFIX}`; @@ -18,7 +20,7 @@ const generateCreateViewScript = (viewName, viewData, viewDefinitionFallback = { return `CREATE VIEW ${wrapInQuotes(viewName)} AS ${selectStatement}`; }; -const prepareViewData = (viewData, viewOptions, triggers) => { +const prepareViewData = (viewData, viewOptions, triggers, tableToastOptions) => { const data = { withCheckOption: viewData.check_option !== 'NONE' || _.isNil(viewData.check_option), checkTestingScope: getCheckTestingScope(viewData.check_option), @@ -26,6 +28,11 @@ const prepareViewData = (viewData, viewOptions, triggers) => { temporary: viewOptions?.persistence === 't', recursive: isViewRecursive(viewData), description: viewOptions?.description, + materialized: viewData.table_type === TABLE_TYPE.materializedView, + view_tablespace_name: viewData.view_tablespace_name, + withDataOption: viewData.is_populated, + storage_parameter: prepareStorageParameters(viewOptions?.view_options, tableToastOptions), + triggers, }; diff --git a/reverse_engineering/helpers/postgresService.js b/reverse_engineering/helpers/postgresService.js index d46cd418..cb5577b7 100644 --- a/reverse_engineering/helpers/postgresService.js +++ b/reverse_engineering/helpers/postgresService.js @@ -28,6 +28,7 @@ const { getTriggers } = require('./postgresHelpers/triggerHelper'); const queryConstants = require('./queryConstants'); const { reorganizeConstraints } = require('./postgresHelpers/reorganizeConstraints'); const { mapSequenceData } = require('./postgresHelpers/sequenceHelper'); +const { TABLE_TYPE } = require('../constants/tableType'); let useSshTunnel = false; let logger = null; @@ -84,7 +85,7 @@ module.exports = { async getTablesNames(schemaName) { const tables = await db.query(queryConstants.GET_TABLE_NAMES, [schemaName]); - const tableTypesToExclude = ['FOREIGN TABLE']; + const tableTypesToExclude = [TABLE_TYPE.foreignTable]; return tables .filter(({ table_type }) => !_.includes(tableTypesToExclude, table_type)) @@ -372,7 +373,9 @@ module.exports = { viewName = removeViewNameSuffix(viewName); - const viewData = await db.query(queryConstants.GET_VIEW_DATA, [viewName, schemaName], true); + const viewData = + (await db.query(queryConstants.GET_VIEW_DATA, [viewName, schemaName], true)) ?? + (await db.query(queryConstants.GET_MATERIALIZED_VIEW_DATA, [viewName, schemaName], true)); const viewDefinitionFallback = !viewData.view_definition && (await db.queryTolerant(queryConstants.GET_VIEW_SELECT_STMT_FALLBACK, [viewName, schemaName], true)); @@ -384,9 +387,14 @@ module.exports = { viewOptions?.oid, ignoreUdfUdpTriggers, ); + const tableToastOptions = await db.queryTolerant( + queryConstants.GET_TABLE_TOAST_OPTIONS, + [viewName, schemaOid], + true, + ); const script = generateCreateViewScript(viewName, viewData, viewDefinitionFallback); - const data = prepareViewData(viewData, viewOptions, triggers); + const data = prepareViewData(viewData, viewOptions, triggers, tableToastOptions); if (!script) { logger.info('View select statement was not retrieved', { schemaName, viewName }); diff --git a/reverse_engineering/helpers/queryConstants.js b/reverse_engineering/helpers/queryConstants.js index 99d89018..9bba6488 100644 --- a/reverse_engineering/helpers/queryConstants.js +++ b/reverse_engineering/helpers/queryConstants.js @@ -133,23 +133,22 @@ const queryConstants = { GET_VERSION_AS_NUM: 'SHOW server_version_num;', GET_SCHEMA_NAMES: 'SELECT schema_name FROM information_schema.schemata;', GET_TABLE_NAMES: ` - SELECT tables.table_name, tables.table_type FROM information_schema.tables AS tables - INNER JOIN - (SELECT - pg_class.relname AS table_name, - pg_namespace.nspname AS table_schema - FROM pg_catalog.pg_class AS pg_class - INNER JOIN pg_catalog.pg_namespace AS pg_namespace - ON (pg_namespace.oid = pg_class.relnamespace) - WHERE pg_class.relispartition = false - AND pg_class.relkind = ANY('{"r","v","t","m","p"}')) - AS catalog_table_data - ON (catalog_table_data.table_name = tables.table_name AND catalog_table_data.table_schema = tables.table_schema) - LEFT JOIN (SELECT relname AS child_name FROM pg_catalog.pg_inherits AS inherit - LEFT JOIN pg_catalog.pg_class AS child ON (child.oid = inherit.inhrelid)) AS inherited_tables - ON (inherited_tables.child_name = tables.table_name) - WHERE inherited_tables.child_name IS NULL - AND tables.table_schema = $1;`, + SELECT + pg_catalog.pg_class.relname AS table_name, + pg_catalog.pg_class.relkind AS table_type + FROM pg_catalog.pg_class + INNER JOIN pg_catalog.pg_namespace + ON pg_catalog.pg_namespace.oid = pg_catalog.pg_class.relnamespace + LEFT JOIN (SELECT relname AS child_name + FROM pg_catalog.pg_inherits AS inherit + LEFT JOIN pg_catalog.pg_class AS child + ON child.oid = inherit.inhrelid) AS + inherited_tables + ON inherited_tables.child_name = pg_catalog.pg_class.relname + WHERE inherited_tables.child_name IS NULL + AND pg_catalog.pg_class.relispartition = false + AND pg_catalog.pg_class.relkind IN ( 'r', 'v', 'm', 'p' ) + AND pg_catalog.pg_namespace.nspname = $1;`, GET_NAMESPACE_OID: 'SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = $1', GET_TABLE_LEVEL_DATA: ` SELECT pc.oid, pc.relpersistence, pc.reloptions, pt.spcname, pg_get_expr(pc.relpartbound, pc.oid) AS partition_expr @@ -240,6 +239,22 @@ const queryConstants = { obj_description(oid, 'pg_class') AS description FROM pg_catalog.pg_class WHERE relname = $1 AND relnamespace = $2;`, + GET_MATERIALIZED_VIEW_DATA: ` + SELECT + pg_catalog.pg_class.reltablespace as view_tablespace_name, + pg_catalog.pg_class.relispopulated as is_populated, + pg_catalog.pg_class.relkind as table_type, + pg_catalog.pg_class.oid, + pg_catalog.pg_get_viewdef(pg_catalog.pg_class.oid, true) AS view_definition + FROM + pg_catalog.pg_class + JOIN + pg_catalog.pg_namespace + ON pg_catalog.pg_namespace.oid = pg_catalog.pg_class.relnamespace + WHERE + pg_catalog.pg_class.relkind = 'm' + and pg_catalog.pg_class.relname = $1 + AND pg_catalog.pg_namespace.nspname = $2;`, GET_FUNCTIONS_WITH_PROCEDURES: getGET_FUNCTIONS_WITH_PROCEDURES({ extensionsToExclude: ['vector'] }), GET_FUNCTIONS_WITH_PROCEDURES_ARGS: ` SELECT parameter_name, From b0eea977d122aebdbeb0b76e75910e3d3f1b2faa Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Wed, 12 Mar 2025 19:21:33 +0200 Subject: [PATCH 5/8] HCK: 10265: fix default values --- .../view_level/viewLevelConfig.json | 4 +--- .../helpers/postgresHelpers/viewHelper.js | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/properties_pane/view_level/viewLevelConfig.json b/properties_pane/view_level/viewLevelConfig.json index a1f4bd15..303b106a 100644 --- a/properties_pane/view_level/viewLevelConfig.json +++ b/properties_pane/view_level/viewLevelConfig.json @@ -135,7 +135,6 @@ making sure that you maintain a proper JSON format. "propertyKeyword": "ifNotExist", "propertyTooltip": "When the IF NOT EXISTS clause is used, PostgreSQL will return a warning instead of an error if the specified view already exists.", "propertyType": "checkbox", - "defaultValue": true, "dependency": { "key": "materialized", "value": true @@ -512,7 +511,6 @@ making sure that you maintain a proper JSON format. "propertyName": "Tablespace", "propertyKeyword": "view_tablespace_name", "propertyTooltip": "Enter the name of an existing tablespace location for the database, or pg_default", - "defaultValue": "pg_default", "propertyType": "text", "dependency": { "key": "materialized", @@ -638,7 +636,7 @@ making sure that you maintain a proper JSON format. "propertyType": "select", "options": ["", "LOCAL", "CASCADED"], "dependency": { - "type": "or", + "type": "and", "values": [ { "key": "withCheckOption", diff --git a/reverse_engineering/helpers/postgresHelpers/viewHelper.js b/reverse_engineering/helpers/postgresHelpers/viewHelper.js index db670770..b9569a8e 100644 --- a/reverse_engineering/helpers/postgresHelpers/viewHelper.js +++ b/reverse_engineering/helpers/postgresHelpers/viewHelper.js @@ -28,17 +28,23 @@ const prepareViewData = (viewData, viewOptions, triggers, tableToastOptions) => temporary: viewOptions?.persistence === 't', recursive: isViewRecursive(viewData), description: viewOptions?.description, - materialized: viewData.table_type === TABLE_TYPE.materializedView, - view_tablespace_name: viewData.view_tablespace_name, - withDataOption: viewData.is_populated, - storage_parameter: prepareStorageParameters(viewOptions?.view_options, tableToastOptions), - triggers, + ...prepareMaterializedViewData({ viewData, viewOptions, tableToastOptions }), }; - return clearEmptyPropertiesInObject(data); }; +const prepareMaterializedViewData = ({ viewData, viewOptions, tableToastOptions }) => { + return { + ...(viewData.table_type && { materialized: viewData.table_type === TABLE_TYPE.materializedView }), + ...(viewData.view_tablespace_name && { view_tablespace_name: viewData.view_tablespace_name }), + ...(viewData.is_populated && { withDataOption: viewData.is_populated }), + ...(viewOptions?.view_options && { + storage_parameter: prepareStorageParameters(viewOptions.view_options, tableToastOptions), + }), + }; +}; + const getCheckTestingScope = check_option => { if (check_option === 'NONE') { return ''; From 468cbd7d6792ab9b441ee3b2f3745b50680fc110 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Thu, 13 Mar 2025 08:25:29 +0200 Subject: [PATCH 6/8] HCK-10265: add missed table type --- reverse_engineering/helpers/queryConstants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reverse_engineering/helpers/queryConstants.js b/reverse_engineering/helpers/queryConstants.js index 9bba6488..a29df644 100644 --- a/reverse_engineering/helpers/queryConstants.js +++ b/reverse_engineering/helpers/queryConstants.js @@ -147,7 +147,7 @@ const queryConstants = { ON inherited_tables.child_name = pg_catalog.pg_class.relname WHERE inherited_tables.child_name IS NULL AND pg_catalog.pg_class.relispartition = false - AND pg_catalog.pg_class.relkind IN ( 'r', 'v', 'm', 'p' ) + AND pg_catalog.pg_class.relkind IN ( 'r', 'v', 't', 'm', 'p' ) AND pg_catalog.pg_namespace.nspname = $1;`, GET_NAMESPACE_OID: 'SELECT oid FROM pg_catalog.pg_namespace WHERE nspname = $1', GET_TABLE_LEVEL_DATA: ` From e82a5ceda257a64f32565c5b0e26d3116bcf40b4 Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Thu, 13 Mar 2025 08:27:27 +0200 Subject: [PATCH 7/8] HCK-10265: fix formatting --- reverse_engineering/helpers/queryConstants.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reverse_engineering/helpers/queryConstants.js b/reverse_engineering/helpers/queryConstants.js index a29df644..aa331855 100644 --- a/reverse_engineering/helpers/queryConstants.js +++ b/reverse_engineering/helpers/queryConstants.js @@ -253,7 +253,7 @@ const queryConstants = { ON pg_catalog.pg_namespace.oid = pg_catalog.pg_class.relnamespace WHERE pg_catalog.pg_class.relkind = 'm' - and pg_catalog.pg_class.relname = $1 + AND pg_catalog.pg_class.relname = $1 AND pg_catalog.pg_namespace.nspname = $2;`, GET_FUNCTIONS_WITH_PROCEDURES: getGET_FUNCTIONS_WITH_PROCEDURES({ extensionsToExclude: ['vector'] }), GET_FUNCTIONS_WITH_PROCEDURES_ARGS: ` From f6d420e77d4ae74ad1e87d4e0ea7cb01591781ff Mon Sep 17 00:00:00 2001 From: Serhii Filonenko Date: Thu, 13 Mar 2025 10:44:16 +0200 Subject: [PATCH 8/8] HCK-10265: remove double trimming --- .../ddlProvider/ddlHelpers/materializedViewHelper.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js index b977a7ce..ac29008a 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js +++ b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js @@ -1,7 +1,6 @@ /** * @typedef {Record} ViewData */ -const { trim } = require('lodash'); const { getStorageParameters, getBasicValue } = require('./tableHelper'); /** @@ -18,9 +17,10 @@ const getOptions = ({ viewData }) => { const statements = configs .map(config => config.getValue(viewData[config.key], viewData)) .filter(Boolean) - .join('\n'); + .join('\n') + .trim(); - return trim(statements) ? ` ${trim(statements)}` : ''; + return statements ? ` ${statements}` : ''; }; /**