diff --git a/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js new file mode 100644 index 00000000..ac29008a --- /dev/null +++ b/forward_engineering/ddlProvider/ddlHelpers/materializedViewHelper.js @@ -0,0 +1,41 @@ +/** + * @typedef {Record} ViewData + */ +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: 'view_tablespace_name', getValue: getBasicValue('TABLESPACE') }, + ]; + + const statements = configs + .map(config => config.getValue(viewData[config.key], viewData)) + .filter(Boolean) + .join('\n') + .trim(); + + return statements ? ` ${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..fc666754 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, + view_tablespace_name: detailsTab.view_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};', diff --git a/properties_pane/view_level/viewLevelConfig.json b/properties_pane/view_level/viewLevelConfig.json index 93fdbf12..303b106a 100644 --- a/properties_pane/view_level/viewLevelConfig.json +++ b/properties_pane/view_level/viewLevelConfig.json @@ -124,27 +124,456 @@ 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", + "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": "view_tablespace_name", + "propertyTooltip": "Enter the name of an existing tablespace location for the database, or 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 +604,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", @@ -188,11 +636,20 @@ making sure that you maintain a proper JSON format. "propertyType": "select", "options": ["", "LOCAL", "CASCADED"], "dependency": { - "type": "or", + "type": "and", "values": [ { "key": "withCheckOption", "value": true + }, + { + "type": "not", + "values": [ + { + "key": "materialized", + "value": true + } + ] } ] } @@ -224,6 +681,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", 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..b9569a8e 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), @@ -27,11 +29,22 @@ const prepareViewData = (viewData, viewOptions, triggers) => { recursive: isViewRecursive(viewData), description: viewOptions?.description, 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 ''; 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..aa331855 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', '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: ` 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,