diff --git a/forward_engineering/alterScript/alterScriptFromDeltaHelper.js b/forward_engineering/alterScript/alterScriptFromDeltaHelper.js index 5dcd3e0..d9b3e76 100644 --- a/forward_engineering/alterScript/alterScriptFromDeltaHelper.js +++ b/forward_engineering/alterScript/alterScriptFromDeltaHelper.js @@ -20,7 +20,6 @@ const { getModifyForeignKeyScriptDtos, } = require('./alterScriptHelpers/alterForeignKeyHelper'); const { getViewsScripts } = require('./alterScriptHelpers/alterViewHelper'); -const { getAddVersioningScriptDto, getEnableArchiveScriptDto } = require('./alterScriptHelpers/alterVersioningHelper'); const { getSchemaOfAlterCollection, getSchemaNameFromCollection } = require('../utils/general'); /** @@ -154,27 +153,6 @@ const getAlterCollectionScriptDtos = ({ collection, app, inlineDeltaRelationship ]; }; -/** - * Build the ALTER TABLE ... ADD VERSIONING and ALTER TABLE ... ENABLE ARCHIVE statements linking a table to its history - * table or archive table, for every added or modified entity. Runs after all entities in the batch are created, so the - * referenced tables already exist by the time it runs. - * - * @param {{ collection: DeltaModel; relatedSchemas: Record }} params Delta model and sibling - * entities keyed by GUID. - * @returns {AlterScriptDto[]} Alter script DTOs. - */ -const getAlterVersioningScriptDtos = ({ collection, relatedSchemas }) => { - const { added, modified } = getSectionItems(collection.properties?.entities); - const entities = [...added, ...modified]; - const addVersioningScriptDto = getAddVersioningScriptDto(relatedSchemas); - const enableArchiveScriptDto = getEnableArchiveScriptDto(relatedSchemas); - - return [ - ...entities.map(item => addVersioningScriptDto(item)), - ...entities.map(item => enableArchiveScriptDto(item)), - ].filter(dto => dto !== undefined); -}; - /** * Build the view statements. * @@ -312,7 +290,6 @@ const getAlterScriptDtos = (data, app) => { ...containerScriptDtos, ...upsertedTypesScriptDtos, ...getAlterCollectionScriptDtos({ collection, app, inlineDeltaRelationships, relatedSchemas }), - ...getAlterVersioningScriptDtos({ collection, relatedSchemas }), ...getAlterRelationshipsScriptDtos({ collection, ignoreRelationshipIDs }), ...getAlterViewScriptDtos({ collection, app }), ...deletedTypesScriptDtos, diff --git a/forward_engineering/alterScript/alterScriptHelpers/alterVersioningHelper.js b/forward_engineering/alterScript/alterScriptHelpers/alterVersioningHelper.js deleted file mode 100644 index 245e712..0000000 --- a/forward_engineering/alterScript/alterScriptHelpers/alterVersioningHelper.js +++ /dev/null @@ -1,113 +0,0 @@ -/** - * @import { - * AlterScriptDto, - * AlterTable - * } from '../../types/alterScript' - * @import {PeriodConfig} from '../../types/ddlProvider' - */ - -const { createAlterScriptDto } = require('../dto/alterScriptDto'); -const { - getEntityName, - getSchemaNameFromCollection, - getSchemaOfAlterCollection, - getNamePrefixedWithSchemaName, -} = require('../../utils/general'); -const templates = require('../../ddlProvider/templates'); -const { assignTemplates } = require('../../utils/assignTemplates'); - -/** - * Read the single period config out of a group property, which the studio may serialize as an object or as a one-item - * array depending on how it was last edited. - * - * @param {PeriodConfig | PeriodConfig[] | undefined} period Period group value. - * @returns {PeriodConfig | undefined} Period config. - */ -const getPeriodConfig = period => (Array.isArray(period) ? period[0] : period); - -/** - * Resolve a cross-entity GUID reference (e.g. history table, archive table) to its schema-qualified name. - * - * @param {{ relatedSchemas: Record; entityId?: string }} params Related entities and the referenced - * entity's GUID. - * @returns {string | undefined} Schema-qualified table name. - */ -const resolveRelatedTableName = ({ relatedSchemas, entityId }) => { - const relatedSchema = entityId ? relatedSchemas[entityId] : undefined; - const name = relatedSchema ? getEntityName(relatedSchema) : undefined; - - if (!name) { - return void 0; - } - - return getNamePrefixedWithSchemaName({ name, schemaName: relatedSchema?.bucketName }); -}; - -/** - * Build the ALTER TABLE ... ADD VERSIONING statement linking a system-period temporal table to its history table. - * - * @param {Record} relatedSchemas Sibling entities of the model/container batch, keyed by entity - * GUID. - * @returns {(collection: AlterTable) => AlterScriptDto | undefined} Add versioning script builder. - */ -const getAddVersioningScriptDto = relatedSchemas => collection => { - const collectionSchema = getSchemaOfAlterCollection(collection); - const historyTableId = getPeriodConfig(collectionSchema.periodForSystemTime)?.historyTable; - const historyTableName = resolveRelatedTableName({ relatedSchemas, entityId: historyTableId }); - - if (!historyTableName) { - return void 0; - } - - const script = assignTemplates({ - template: templates.addVersioning, - templateData: { - tableName: getNamePrefixedWithSchemaName({ - name: getEntityName(collectionSchema), - schemaName: getSchemaNameFromCollection({ collection }), - }), - historyTableName, - }, - }); - - return createAlterScriptDto([script], collectionSchema.isActivated ?? true, false); -}; - -/** - * Build the ALTER TABLE ... ENABLE ARCHIVE statement linking a table to its archive table. - * - * @param {Record} relatedSchemas Sibling entities of the model/container batch, keyed by entity - * GUID. - * @returns {(collection: AlterTable) => AlterScriptDto | undefined} Enable archive script builder. - */ -const getEnableArchiveScriptDto = relatedSchemas => collection => { - const collectionSchema = getSchemaOfAlterCollection(collection); - - if (!collectionSchema.archiveEnabled) { - return void 0; - } - - const archiveTableName = resolveRelatedTableName({ relatedSchemas, entityId: collectionSchema.archiveTable }); - - if (!archiveTableName) { - return void 0; - } - - const script = assignTemplates({ - template: templates.enableArchive, - templateData: { - tableName: getNamePrefixedWithSchemaName({ - name: getEntityName(collectionSchema), - schemaName: getSchemaNameFromCollection({ collection }), - }), - archiveTableName, - }, - }); - - return createAlterScriptDto([script], collectionSchema.isActivated ?? true, false); -}; - -module.exports = { - getAddVersioningScriptDto, - getEnableArchiveScriptDto, -}; diff --git a/forward_engineering/ddlProvider/ddlHelpers/columnDefinition/getColumnDefault.js b/forward_engineering/ddlProvider/ddlHelpers/columnDefinition/getColumnDefault.js index e19d833..c72388c 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/columnDefinition/getColumnDefault.js +++ b/forward_engineering/ddlProvider/ddlHelpers/columnDefinition/getColumnDefault.js @@ -42,11 +42,22 @@ const isRowid = ({ type }) => toUpper(type) === DATA_TYPE.rowid; * @param {IdentityOptions} params Identity options. * @returns {string} Identity options clause. */ -const getIdentityOptions = ({ start, increment, minValue, maxValue, cycle, cache, cacheValue, order }) => { +const getIdentityOptions = ({ + start, + increment, + minValue, + maxValue, + noMinValue, + noMaxValue, + cycle, + cache, + cacheValue, + order, +}) => { const startWith = start ? `START WITH ${start}` : ''; const incrementBy = increment ? `INCREMENT BY ${increment}` : ''; - const minimumValue = minValue ? `MINVALUE ${minValue}` : ''; - const maximumValue = maxValue ? `MAXVALUE ${maxValue}` : ''; + const minimumValue = noMinValue ? 'NO MINVALUE' : minValue ? `MINVALUE ${minValue}` : ''; + const maximumValue = noMaxValue ? 'NO MAXVALUE' : maxValue ? `MAXVALUE ${maxValue}` : ''; const cacheOption = cacheValue ? `CACHE ${cacheValue}` : cache; return [startWith, incrementBy, cycle, minimumValue, maximumValue, cacheOption, order].filter(Boolean).join(', '); @@ -64,10 +75,27 @@ const getColumnDefault = ({ type, generated, generatedColumn, + generatedColumnType, + generatedColumnGenerated, columnGenerationExpression, + implicitlyHidden, }) => { + const hiddenClause = implicitlyHidden ? ' IMPLICITLY HIDDEN' : ''; + if (isRowid({ type }) && generated) { - return ` GENERATED ${generated}`; + return ` GENERATED ${generated}${hiddenClause}`; + } + + if (generatedColumn && generatedColumnType === 'ROW CHANGE TIMESTAMP') { + return ` GENERATED ${generatedColumnGenerated ?? 'ALWAYS'} FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP${hiddenClause}`; + } + + if ( + generatedColumn && + generatedColumnType && + ['ROW BEGIN', 'ROW END', 'TRANSACTION START ID'].includes(generatedColumnType) + ) { + return ` GENERATED ALWAYS AS ${generatedColumnType}${hiddenClause}`; } if (generatedColumn && columnGenerationExpression) { @@ -78,8 +106,9 @@ const getColumnDefault = ({ if (isGeneratedIdentity && identity) { const identityOptions = getIdentityOptions(identity); + const identityOptionsClause = identityOptions ? ` (${identityOptions})` : ''; - return ` GENERATED ${identity.generated} AS IDENTITY (${identityOptions})`; + return ` GENERATED ${identity.generated} AS IDENTITY${identityOptionsClause}${hiddenClause}`; } if (defaultValue || defaultValue === 0) { diff --git a/forward_engineering/ddlProvider/ddlHelpers/table/getTableOptions.js b/forward_engineering/ddlProvider/ddlHelpers/table/getTableOptions.js index 60ea998..6851b74 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/table/getTableOptions.js +++ b/forward_engineering/ddlProvider/ddlHelpers/table/getTableOptions.js @@ -4,8 +4,7 @@ * HydratedPartitioning, * InClauseParams, * OptionConfig, - * TableOptionsBlock, - * TemporalPeriodsParams + * TableOptionsBlock * } from '../../../types/ddlProvider' */ @@ -272,33 +271,6 @@ const getPartitioningClause = ({ partitioning }) => { return ''; }; -/** - * Build temporal period clauses. - * - * @param {TemporalPeriodsParams} params Period data. - * @returns {string} Period clauses. - */ -const getTemporalPeriodsClause = ({ periodForSystemTime, periodForBusinessTime }) => { - const clauses = []; - - if (periodForSystemTime?.startColumn && periodForSystemTime?.endColumn) { - clauses.push( - `PERIOD FOR SYSTEM_TIME (${wrapInQuotes(periodForSystemTime.startColumn)}, ${wrapInQuotes(periodForSystemTime.endColumn)})`, - ); - } - - if (periodForBusinessTime?.startColumn && periodForBusinessTime?.endColumn) { - const endInclusive = periodForBusinessTime.endInclusive - ? ` ${toUpper(periodForBusinessTime.endInclusive)}` - : ''; - clauses.push( - `PERIOD FOR BUSINESS_TIME (${wrapInQuotes(periodForBusinessTime.startColumn)}, ${wrapInQuotes(periodForBusinessTime.endColumn)}${endInclusive})`, - ); - } - - return clauses.join('\n\t'); -}; - /** * Build the AS (fullselect) clause and refresh/maintenance options of a materialized query table. * @@ -362,16 +334,9 @@ const getTableOptions = tableData => { const inClause = getInClause(tableData); const structuredOptions = getStructuredTableOptions(tableData); const partitioning = tableData.inClauseType === 'accelerator' ? '' : getPartitioningClause(tableData); - const temporal = - tableData.inClauseType === 'accelerator' - ? '' - : getTemporalPeriodsClause({ - periodForSystemTime: tableData.periodForSystemTime, - periodForBusinessTime: tableData.periodForBusinessTime, - }); const tableProperties = tableData.tableProperties ?? ''; - const statements = [mqtClause, inClause, structuredOptions.trim(), partitioning, temporal, tableProperties] + const statements = [mqtClause, inClause, structuredOptions.trim(), partitioning, tableProperties] .filter(Boolean) .join('\n\t'); @@ -382,6 +347,5 @@ module.exports = { getTableOptions, getInClause, getPartitioningClause, - getTemporalPeriodsClause, getMqtClause, }; diff --git a/forward_engineering/ddlProvider/ddlHelpers/table/getTableProps.js b/forward_engineering/ddlProvider/ddlHelpers/table/getTableProps.js index af05789..1897bbb 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/table/getTableProps.js +++ b/forward_engineering/ddlProvider/ddlHelpers/table/getTableProps.js @@ -3,10 +3,12 @@ * DividedConstraints, * ForeignKeyStatement, * KeyConstraint, - * TablePropsParams + * TablePropsParams, + * TemporalPeriodsParams * } from '../../../types/ddlProvider' */ +const toUpper = require('lodash/toUpper'); const templates = require('../../templates'); const { assignTemplates } = require('../../../utils/assignTemplates'); const { @@ -19,6 +21,34 @@ const { const { getOptionsString } = require('../constraint/getOptionsString'); const { joinActivatedAndDeactivatedStatements } = require('../../../utils/joinActivatedAndDeactivatedStatements'); +/** + * Build PERIOD SYSTEM_TIME / PERIOD BUSINESS_TIME table elements. Db2 for z/OS places these inline in the table-element + * list (no FOR keyword) - not as a trailing clause after the closing parenthesis. + * + * @param {TemporalPeriodsParams} params Period data. + * @returns {string[]} Period table elements. + */ +const getTemporalPeriodTableElements = ({ periodForSystemTime, periodForBusinessTime }) => { + const clauses = []; + + if (periodForSystemTime?.startColumn && periodForSystemTime?.endColumn) { + clauses.push( + `PERIOD SYSTEM_TIME (${wrapInQuotes(periodForSystemTime.startColumn)}, ${wrapInQuotes(periodForSystemTime.endColumn)})`, + ); + } + + if (periodForBusinessTime?.startColumn && periodForBusinessTime?.endColumn) { + const endInclusive = periodForBusinessTime.endInclusive + ? ` ${toUpper(periodForBusinessTime.endInclusive)}` + : ''; + clauses.push( + `PERIOD BUSINESS_TIME (${wrapInQuotes(periodForBusinessTime.startColumn)}, ${wrapInQuotes(periodForBusinessTime.endColumn)}${endInclusive})`, + ); + } + + return clauses; +}; + /** * Extract constraint statement text. * @@ -101,7 +131,15 @@ const getDividedForeignKeyConstraints = ({ foreignKeyConstraints }) => { * @param {TablePropsParams} params Table props input. * @returns {string} Table props DDL. */ -const getTableProps = ({ columns, foreignKeyConstraints, keyConstraints, checkConstraints, isActivated }) => { +const getTableProps = ({ + columns, + foreignKeyConstraints, + keyConstraints, + checkConstraints, + periodForSystemTime, + periodForBusinessTime, + isActivated, +}) => { const dividedKeysConstraints = getDividedKeysConstraints({ keyConstraints, isActivated }); const dividedForeignKeyConstraints = getDividedForeignKeyConstraints({ foreignKeyConstraints }); const keyConstraintsString = generateConstraintsString({ @@ -116,6 +154,13 @@ const getTableProps = ({ columns, foreignKeyConstraints, keyConstraints, checkCo dividedConstraints: { activatedItems: checkConstraints ?? [], deactivatedItems: [] }, isParentActivated: isActivated, }); + const temporalPeriodsString = generateConstraintsString({ + dividedConstraints: { + activatedItems: getTemporalPeriodTableElements({ periodForSystemTime, periodForBusinessTime }), + deactivatedItems: [], + }, + isParentActivated: isActivated, + }); const columnsString = joinActivatedAndDeactivatedStatements({ statements: columns, indent: '\n\t' }); const tableProps = assignTemplates({ @@ -125,6 +170,7 @@ const getTableProps = ({ columns, foreignKeyConstraints, keyConstraints, checkCo foreignKeyConstraints: foreignKeyConstraintsString, keyConstraints: keyConstraintsString, checkConstraints: checkConstraintsString, + temporalPeriods: temporalPeriodsString, }, }); diff --git a/forward_engineering/ddlProvider/ddlHelpers/table/hydrateZosTableData.js b/forward_engineering/ddlProvider/ddlHelpers/table/hydrateZosTableData.js index cbb05b3..daa45b4 100644 --- a/forward_engineering/ddlProvider/ddlHelpers/table/hydrateZosTableData.js +++ b/forward_engineering/ddlProvider/ddlHelpers/table/hydrateZosTableData.js @@ -72,6 +72,7 @@ const hydrateTemporalPeriod = ({ jsonSchema, period }) => { startColumn, endColumn, endInclusive: periodConfig.endInclusive, + historyTable: periodConfig.historyTable, }; }; diff --git a/forward_engineering/ddlProvider/ddlProvider.js b/forward_engineering/ddlProvider/ddlProvider.js index 22be9eb..d99056e 100644 --- a/forward_engineering/ddlProvider/ddlProvider.js +++ b/forward_engineering/ddlProvider/ddlProvider.js @@ -11,6 +11,7 @@ * HydratedCheckConstraint, * HydratedColumn, * HydratedTable, + * HydratedTemporalPeriod, * HydratedView, * HydratedViewColumn, * HydrateTableParams, @@ -134,6 +135,9 @@ const hydrateSchema = containerData => ({ * @returns {string} SET SCHEMA DDL. */ const createSchema = ({ schemaName, isActivated = true }) => { + // Studio always calls createSchema once at the start of a generation run, before any createTable call - the + // natural place to reset cross-table ADD VERSIONING/ENABLE ARCHIVE ordering state for this run. + resetVersioningTracking(); const wrappedSchemaName = wrapInQuotes(schemaName); const setSchemaStatement = assignTemplates({ template: templates.setSchema, @@ -226,8 +230,11 @@ const hydrateColumn = ({ columnDefinition, jsonSchema, schemaData, definitionJso ccsid: jsonSchema.ccsid, inlineLength: jsonSchema.inlineLength, generatedColumn: jsonSchema.generatedColumn, + generatedColumnType: jsonSchema.generatedColumnType, + generatedColumnGenerated: jsonSchema.generatedColumnGenerated, columnGenerationExpression: jsonSchema.columnGenerationExpression, generated: jsonSchema.generated, + implicitlyHidden: jsonSchema.implicitlyHidden, isUDTRef, itemsType, }; @@ -244,9 +251,8 @@ const hydrateJsonSchemaColumn = (jsonSchema, definitionJsonSchema) => { if (!jsonSchema.$ref || isEmpty(definitionJsonSchema)) { return jsonSchema; } - const { $ref: _ref, ...jsonSchemaWithoutRef } = jsonSchema; - return { ...definitionJsonSchema, ...jsonSchemaWithoutRef }; + return { ...definitionJsonSchema, ...jsonSchema }; }; /** @@ -470,6 +476,8 @@ const hydrateTable = ({ tableData, entityData, jsonSchema }) => { partitioning: partitioning ?? undefined, periodForSystemTime: periodForSystemTime ?? undefined, periodForBusinessTime: periodForBusinessTime ?? undefined, + archiveEnabled: detailsTab.archiveEnabled, + archiveTable: detailsTab.archiveTable, tableKind: detailsTab.tableKind, gttCcsid: detailsTab.gttCcsid, mqtQuery: detailsTab.mqtQuery, @@ -480,6 +488,100 @@ const hydrateTable = ({ tableData, entityData, jsonSchema }) => { }; }; +/** + * Tracks table names already rendered in the current generation run (unquoted `schema.table`, upper-cased), and any ADD + * VERSIONING/ENABLE ARCHIVE statements still waiting on their target table to be rendered. Db2 requires both tables to + * exist before the ALTER can run, but `historyTable`/`archiveTable` are plain text properties, invisible to Studio's + * relationship-based entity ordering - so a statement can't always be attached to its owning table's own CREATE TABLE + * safely. Reset per `createSchema` call, which Studio always calls once at the start of a generation run (container- or + * entity-level) before any `createTable` call. + * + * @type {Set} + */ +let renderedTableNames = new Set(); +/** @type {{ targetTableName: string; statement: string }[]} */ +let pendingVersioningStatements = []; + +/** + * Reset cross-table ADD VERSIONING/ENABLE ARCHIVE ordering state for a new generation run. + * + * @returns {void} + */ +const resetVersioningTracking = () => { + renderedTableNames = new Set(); + pendingVersioningStatements = []; +}; + +/** + * Build the ALTER TABLE ... ADD VERSIONING / ENABLE ARCHIVE statements that must follow a table's own CREATE TABLE, + * deferring any whose target table hasn't been rendered yet and flushing them once that table comes up - regardless of + * which of the two tables that turns out to be, so the statement always lands after both tables exist. + * + * @param {{ + * tableName: string; + * canonicalTableName: string; + * periodForSystemTime?: HydratedTemporalPeriod; + * archiveEnabled?: boolean; + * archiveTable?: string; + * }} params + * Quoted table name (for the ALTER statement text), unquoted `schema.table` key (for matching against + * `historyTable`/`archiveTable` text values), and temporal/archive linkage data. + * @returns {string} Statements ready to attach to this table's own CREATE TABLE, or an empty string. + */ +const getVersioningStatementsForTable = ({ + tableName, + canonicalTableName, + periodForSystemTime, + archiveEnabled, + archiveTable, +}) => { + /** @type {string[]} */ + const readyStatements = []; + + pendingVersioningStatements = pendingVersioningStatements.filter(pending => { + if (pending.targetTableName !== canonicalTableName) { + return true; + } + readyStatements.push(pending.statement); + return false; + }); + + /** @type {{ targetTableName: string; statement: string }[]} */ + const candidates = []; + + if (periodForSystemTime?.historyTable) { + candidates.push({ + targetTableName: toUpper(periodForSystemTime.historyTable), + statement: assignTemplates({ + template: templates.addVersioning, + templateData: { tableName, historyTableName: periodForSystemTime.historyTable }, + }), + }); + } + + if (archiveEnabled && archiveTable) { + candidates.push({ + targetTableName: toUpper(archiveTable), + statement: assignTemplates({ + template: templates.enableArchive, + templateData: { tableName, archiveTableName: archiveTable }, + }), + }); + } + + candidates.forEach(candidate => { + if (renderedTableNames.has(candidate.targetTableName)) { + readyStatements.push(candidate.statement); + } else { + pendingVersioningStatements.push(candidate); + } + }); + + renderedTableNames.add(canonicalTableName); + + return readyStatements.length > 0 ? '\n\n' + readyStatements.join('\n\n') : ''; +}; + /** * Create table DDL. * @@ -517,6 +619,8 @@ const createTable = (tableData, isActivated = true) => { partitioning, periodForSystemTime, periodForBusinessTime, + archiveEnabled, + archiveTable, description, tableProperties, } = tableData; @@ -574,13 +678,26 @@ const createTable = (tableData, isActivated = true) => { } const isMaterializedQuery = tableKind === 'materializedQuery'; + // PERIOD SYSTEM_TIME/BUSINESS_TIME must not be specified with IN ACCELERATOR. + const canHaveTemporalPeriods = inClauseType !== 'accelerator'; + // The optional CREATE TABLE tableName (col1, col2, ...) AS (fullselect) result-column list is derived + // from the modeled columns (rather than re-parsed from the fullselect), so it always reflects any + // renames made in Studio after reverse engineering. + const mqtResultColumns = (columnDefinitions ?? []) + .filter(columnDefinition => columnDefinition.isActivated ?? true) + .map(columnDefinition => wrapInQuotes(columnDefinition.name)) + .join(', '); const tableProps = isMaterializedQuery - ? '' + ? mqtResultColumns + ? `\n(${mqtResultColumns})` + : '' : getTableProps({ columns: columns ?? [], foreignKeyConstraints: foreignKeyConstraints ?? [], keyConstraints: keyConstraints ?? [], checkConstraints: checkConstraints ?? [], + periodForSystemTime: canHaveTemporalPeriods ? periodForSystemTime : undefined, + periodForBusinessTime: canHaveTemporalPeriods ? periodForBusinessTime : undefined, isActivated, }); const renderedTableOptions = getTableOptions({ @@ -596,8 +713,6 @@ const createTable = (tableData, isActivated = true) => { acceleratorName, tableOptions, partitioning, - periodForSystemTime, - periodForBusinessTime, tableProperties, }); @@ -613,8 +728,15 @@ const createTable = (tableData, isActivated = true) => { tableOptions: renderedTableOptions, }, }); + const versioningStatements = getVersioningStatementsForTable({ + tableName, + canonicalTableName: toUpper(`${schemaData.schemaName}.${name}`), + periodForSystemTime: canHaveTemporalPeriods ? periodForSystemTime : undefined, + archiveEnabled, + archiveTable, + }); - return commentDeactivatedStatement(createTableDdl + commentStatements, { + return commentDeactivatedStatement(createTableDdl + commentStatements + versioningStatements, { isActivated, }); }; diff --git a/forward_engineering/ddlProvider/templates.js b/forward_engineering/ddlProvider/templates.js index fe506eb..3f8be7b 100644 --- a/forward_engineering/ddlProvider/templates.js +++ b/forward_engineering/ddlProvider/templates.js @@ -1,7 +1,7 @@ module.exports = { setSchema: 'SET SCHEMA = ${schemaName};', - createType: 'CREATE TYPE ${name} AS ${sourceType};', + createType: 'CREATE DISTINCT TYPE ${name} AS ${sourceType};', dropType: 'DROP TYPE ${name};', @@ -21,7 +21,7 @@ module.exports = { comment: '\nCOMMENT ON ${objectType} ${objectName} IS ${comment};\n', - createTableProps: '${columns}${keyConstraints}${checkConstraints}${foreignKeyConstraints}', + createTableProps: '${columns}${keyConstraints}${checkConstraints}${foreignKeyConstraints}${temporalPeriods}', columnDefinition: '${name}${type}${nullability}${default}${constraints}', diff --git a/forward_engineering/types/ddlProvider.d.ts b/forward_engineering/types/ddlProvider.d.ts index 98741a8..cbede76 100644 --- a/forward_engineering/types/ddlProvider.d.ts +++ b/forward_engineering/types/ddlProvider.d.ts @@ -34,6 +34,8 @@ export type IdentityOptions = { cycle?: string; minValue?: number; maxValue?: number; + noMinValue?: boolean; + noMaxValue?: boolean; cache?: string; cacheValue?: number; order?: string; @@ -76,8 +78,11 @@ export type JsonSchemaColumn = { ccsid?: number; inlineLength?: number; generatedColumn?: boolean; + generatedColumnType?: string; + generatedColumnGenerated?: string; columnGenerationExpression?: string; generated?: string; + implicitlyHidden?: boolean; items?: JsonSchemaColumn | JsonSchemaColumn[]; ofType?: string; notPersistable?: boolean; @@ -133,8 +138,11 @@ export type HydratedColumn = { ccsid?: number; inlineLength?: number; generatedColumn?: boolean; + generatedColumnType?: string; + generatedColumnGenerated?: string; columnGenerationExpression?: string; generated?: string; + implicitlyHidden?: boolean; isUDTRef?: boolean; itemsType?: string; }; @@ -209,6 +217,7 @@ export type HydratedTemporalPeriod = { startColumn?: string; endColumn?: string; endInclusive?: string; + historyTable?: string; }; export type HydratedPartitionKey = { @@ -330,6 +339,8 @@ export type HydratedTable = { partitioning?: HydratedPartitioning; periodForSystemTime?: HydratedTemporalPeriod; periodForBusinessTime?: HydratedTemporalPeriod; + archiveEnabled?: boolean; + archiveTable?: string; columnDefinitions?: HydratedColumn[]; columns?: string[]; foreignKeyConstraints?: ForeignKeyStatement[]; @@ -366,6 +377,8 @@ export type CreateTableParams = { partitioning?: HydratedPartitioning; periodForSystemTime?: HydratedTemporalPeriod; periodForBusinessTime?: HydratedTemporalPeriod; + archiveEnabled?: boolean; + archiveTable?: string; }; export type HydratedViewColumn = { @@ -510,7 +523,10 @@ export type ColumnDefaultParams = { type: string; generated?: string; generatedColumn?: boolean; + generatedColumnType?: string; + generatedColumnGenerated?: string; columnGenerationExpression?: string; + implicitlyHidden?: boolean; }; export type HydratePartitioningParams = { @@ -561,6 +577,8 @@ export type TablePropsParams = { foreignKeyConstraints: ForeignKeyStatement[]; keyConstraints: KeyConstraint[]; checkConstraints?: string[]; + periodForSystemTime?: HydratedTemporalPeriod; + periodForBusinessTime?: HydratedTemporalPeriod; isActivated: boolean; }; diff --git a/package-lock.json b/package-lock.json index 64464ce..d6d329d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "Db2-zOS", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "Db2-zOS", - "version": "0.1.0", + "version": "0.2.0", "dependencies": { "lodash": "4.18.1" }, diff --git a/package.json b/package.json index 3e88de2..a9ef8a9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Db2-zOS", - "version": "0.1.0", + "version": "0.2.0", "type": "commonjs", "author": "hackolade", "engines": { diff --git a/properties_pane/entity_level/entityLevelConfig.json b/properties_pane/entity_level/entityLevelConfig.json index dece027..af67aa9 100644 --- a/properties_pane/entity_level/entityLevelConfig.json +++ b/properties_pane/entity_level/entityLevelConfig.json @@ -299,11 +299,8 @@ making sure that you maintain a proper JSON format. { "propertyName": "Archive table", "propertyKeyword": "archiveTable", - "propertyTooltip": "Table that stores rows deleted from this table. Generates an ALTER TABLE ... ENABLE ARCHIVE USE ARCHIVE TABLE statement once both tables exist.", - "propertyType": "selecthashed", - "template": "entities", - "withEmptyOption": true, - "excludeCurrent": true, + "propertyTooltip": "Schema-qualified table (schema.table) that stores rows deleted from this table. Generates an ALTER TABLE ... ENABLE ARCHIVE USE ARCHIVE TABLE statement once both tables exist.", + "propertyType": "text", "dependency": { "key": "archiveEnabled", "value": true @@ -683,11 +680,8 @@ making sure that you maintain a proper JSON format. { "propertyName": "History table", "propertyKeyword": "historyTable", - "propertyTooltip": "Optional table that stores prior row versions. When set, an ALTER TABLE ... ADD VERSIONING USE HISTORY TABLE statement is generated once both tables exist.", - "propertyType": "selecthashed", - "template": "entities", - "withEmptyOption": true, - "excludeCurrent": true + "propertyTooltip": "Optional schema-qualified table that stores prior row versions (schema.table). When set, an ALTER TABLE ... ADD VERSIONING USE HISTORY TABLE statement is generated once both tables exist.", + "propertyType": "text" } ] }, diff --git a/properties_pane/field_level/fieldLevelConfig.json b/properties_pane/field_level/fieldLevelConfig.json index cefafb3..103b830 100644 --- a/properties_pane/field_level/fieldLevelConfig.json +++ b/properties_pane/field_level/fieldLevelConfig.json @@ -400,6 +400,27 @@ making sure that you maintain a proper JSON format. "propertyType": "details", "template": "textarea" }, + { + "propertyName": "Generated column", + "propertyKeyword": "generatedColumn", + "propertyTooltip": "A generated column is always computed by Db2 from an expression.", + "propertyType": "checkbox" + }, + { + "propertyName": "Generation expression", + "propertyKeyword": "columnGenerationExpression", + "propertyTooltip": "An SQL statement for computing values for this column", + "propertyType": "details", + "template": "codeEditor", + "templateOptions": { + "editorDialect": "pgsql" + }, + "markdown": false, + "dependency": { + "key": "generatedColumn", + "value": true + } + }, { "propertyName": "Not null", "propertyKeyword": "required", @@ -978,6 +999,27 @@ making sure that you maintain a proper JSON format. "propertyType": "details", "template": "textarea" }, + { + "propertyName": "Generated column", + "propertyKeyword": "generatedColumn", + "propertyTooltip": "A generated column is always computed by Db2 from an expression.", + "propertyType": "checkbox" + }, + { + "propertyName": "Generation expression", + "propertyKeyword": "columnGenerationExpression", + "propertyTooltip": "An SQL statement for computing values for this column", + "propertyType": "details", + "template": "codeEditor", + "templateOptions": { + "editorDialect": "pgsql" + }, + "markdown": false, + "dependency": { + "key": "generatedColumn", + "value": true + } + }, { "propertyName": "Not null", "propertyKeyword": "required", @@ -1034,7 +1076,7 @@ making sure that you maintain a proper JSON format. "propertyName": "Identity", "propertyType": "block", "propertyKeyword": "identity", - "propertyTooltip": "Creates an identity column in a table", + "propertyTooltip": "Creates an identity column in a table. Cannot be combined with the column's Generated column/expression option.", "structure": [ { "propertyName": "Generated", @@ -1222,6 +1264,50 @@ making sure that you maintain a proper JSON format. ] } } + }, + { + "propertyName": "No min value", + "propertyKeyword": "noMinValue", + "propertyTooltip": "Emits NO MINVALUE instead of a numeric Min value. Mutually exclusive with Min value.", + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": { + "type": "or", + "values": [ + { + "key": "generated", + "exist": false + }, + { + "key": "generated", + "value": "" + } + ] + } + } + }, + { + "propertyName": "No max value", + "propertyKeyword": "noMaxValue", + "propertyTooltip": "Emits NO MAXVALUE instead of a numeric Max value. Mutually exclusive with Max value.", + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": { + "type": "or", + "values": [ + { + "key": "generated", + "exist": false + }, + { + "key": "generated", + "value": "" + } + ] + } + } } ], "dependency": { @@ -1246,6 +1332,28 @@ making sure that you maintain a proper JSON format. ] } }, + { + "propertyName": "Implicitly hidden", + "propertyKeyword": "implicitlyHidden", + "propertyTooltip": "Excludes this column from SELECT * results unless explicitly named (IMPLICITLY HIDDEN)", + "propertyType": "checkbox", + "dependency": { + "type": "not", + "values": { + "type": "or", + "values": [ + { + "key": "identity.generated", + "exist": false + }, + { + "key": "identity.generated", + "value": "" + } + ] + } + } + }, { "propertyName": "Primary key", "propertyKeyword": "compositePrimaryKey", @@ -1734,6 +1842,87 @@ making sure that you maintain a proper JSON format. "editorDialect": "markdown" } }, + { + "propertyName": "Generated column", + "propertyKeyword": "generatedColumn", + "propertyTooltip": "A generated column is always computed by Db2 - either from an expression, or as one of the system-period/transaction-timestamp/row-change-timestamp keyword forms used by temporal tables and optimistic locking.", + "propertyType": "checkbox" + }, + { + "propertyName": "Generation kind", + "propertyKeyword": "generatedColumnType", + "propertyTooltip": "Expression uses GENERATED ALWAYS AS (expression). The other options emit the Db2 for z/OS keyword forms used by system-period temporal tables (GENERATED ALWAYS AS ROW BEGIN / ROW END / TRANSACTION START ID) or row-change timestamps for optimistic locking (GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP).", + "propertyType": "select", + "options": ["", "ROW BEGIN", "ROW END", "TRANSACTION START ID", "ROW CHANGE TIMESTAMP"], + "dependency": { + "key": "generatedColumn", + "value": true + } + }, + { + "propertyName": "Generated", + "propertyKeyword": "generatedColumnGenerated", + "propertyTooltip": "ROW CHANGE TIMESTAMP columns may be GENERATED ALWAYS or GENERATED BY DEFAULT", + "propertyType": "select", + "options": ["ALWAYS", "BY DEFAULT"], + "defaultValue": "ALWAYS", + "dependency": { + "type": "and", + "values": [ + { + "key": "generatedColumn", + "value": true + }, + { + "key": "generatedColumnType", + "value": "ROW CHANGE TIMESTAMP" + } + ] + } + }, + { + "propertyName": "Generation expression", + "propertyKeyword": "columnGenerationExpression", + "propertyTooltip": "An SQL statement for computing values for this column", + "propertyType": "details", + "template": "codeEditor", + "templateOptions": { + "editorDialect": "pgsql" + }, + "markdown": false, + "dependency": { + "type": "and", + "values": [ + { + "key": "generatedColumn", + "value": true + }, + { + "key": "generatedColumnType", + "value": "" + } + ] + } + }, + { + "propertyName": "Implicitly hidden", + "propertyKeyword": "implicitlyHidden", + "propertyTooltip": "Excludes this column from SELECT * results unless explicitly named (IMPLICITLY HIDDEN)", + "propertyType": "checkbox", + "dependency": { + "type": "and", + "values": [ + { + "key": "generatedColumn", + "value": true + }, + { + "key": "generatedColumnType", + "value": ["ROW BEGIN", "ROW END", "TRANSACTION START ID", "ROW CHANGE TIMESTAMP"] + } + ] + } + }, { "propertyName": "Not null", "propertyKeyword": "required", @@ -2296,6 +2485,27 @@ making sure that you maintain a proper JSON format. "propertyType": "details", "template": "textarea" }, + { + "propertyName": "Generated column", + "propertyKeyword": "generatedColumn", + "propertyTooltip": "A generated column is always computed by Db2 from an expression.", + "propertyType": "checkbox" + }, + { + "propertyName": "Generation expression", + "propertyKeyword": "columnGenerationExpression", + "propertyTooltip": "An SQL statement for computing values for this column", + "propertyType": "details", + "template": "codeEditor", + "templateOptions": { + "editorDialect": "pgsql" + }, + "markdown": false, + "dependency": { + "key": "generatedColumn", + "value": true + } + }, { "propertyName": "Not null", "propertyKeyword": "required", @@ -2850,6 +3060,12 @@ making sure that you maintain a proper JSON format. "options": ["ALWAYS", "BY DEFAULT"], "defaultValue": "ALWAYS" }, + { + "propertyName": "Implicitly hidden", + "propertyKeyword": "implicitlyHidden", + "propertyTooltip": "Excludes this column from SELECT * results unless explicitly named (IMPLICITLY HIDDEN)", + "propertyType": "checkbox" + }, { "propertyName": "Comments", "propertyKeyword": "description", @@ -3355,6 +3571,27 @@ making sure that you maintain a proper JSON format. "editorDialect": "markdown" } }, + { + "propertyName": "Generated column", + "propertyKeyword": "generatedColumn", + "propertyTooltip": "A generated column is always computed by Db2 from an expression.", + "propertyType": "checkbox" + }, + { + "propertyName": "Generation expression", + "propertyKeyword": "columnGenerationExpression", + "propertyTooltip": "An SQL statement for computing values for this column", + "propertyType": "details", + "template": "codeEditor", + "templateOptions": { + "editorDialect": "pgsql" + }, + "markdown": false, + "dependency": { + "key": "generatedColumn", + "value": true + } + }, "minProperties", "maxProperties", "additionalProperties", diff --git a/reverse_engineering/api.js b/reverse_engineering/api.js index 0ac5ddc..190ef0a 100644 --- a/reverse_engineering/api.js +++ b/reverse_engineering/api.js @@ -59,7 +59,6 @@ const getDbCollectionsData = (_connectionInfo, _appLogger, callback, _app) => { module.exports = { disconnect, - // testConnection, getSchemaNames, getDbCollectionsNames, getDbCollectionsData, diff --git a/reverse_engineering/config.json b/reverse_engineering/config.json index f10bb54..25eb8a1 100644 --- a/reverse_engineering/config.json +++ b/reverse_engineering/config.json @@ -1,10 +1,4 @@ { - "errors": { - "NO_DATABASES": "There is no database in the Db2 instance", - "WRONG_CONNECTION": "Cannot connect to Db2 instance" - }, "defaultDdlType": "db2", - "excludeDocKind": ["id"], - "connectionList": ["name", "host", "port", "userName"], - "helpUrl": "" + "excludeDocKind": ["id"] } diff --git a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json b/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json deleted file mode 100644 index fe51488..0000000 --- a/reverse_engineering/connection_settings_modal/connectionSettingsModalConfig.json +++ /dev/null @@ -1 +0,0 @@ -[] diff --git a/test/columnDefinition.test.js b/test/columnDefinition.test.js index 5f8ec4e..0f3700f 100644 --- a/test/columnDefinition.test.js +++ b/test/columnDefinition.test.js @@ -52,3 +52,159 @@ void test('keeps inline key constraints after identity generation', () => { '"id" INTEGER NOT NULL GENERATED ALWAYS AS IDENTITY (START WITH 1) CONSTRAINT "sample_pk" PRIMARY KEY', ); }); + +void test('omits the empty parentheses when identity has no options', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'id', + type: 'BIGINT', + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + identity: { + generated: 'ALWAYS', + }, + }); + + assert.equal(columnDefinition, '"id" BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY'); +}); + +void test('emits the ROW CHANGE TIMESTAMP generated clause', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'last_mod', + type: 'TIMESTAMP', + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + generatedColumn: true, + generatedColumnType: 'ROW CHANGE TIMESTAMP', + }); + + assert.equal( + columnDefinition, + '"last_mod" TIMESTAMP NOT NULL GENERATED ALWAYS FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP', + ); +}); + +void test('emits the ROW BEGIN generated clause', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'sys_start', + type: 'TIMESTAMP', + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + generatedColumn: true, + generatedColumnType: 'ROW BEGIN', + }); + + assert.equal(columnDefinition, '"sys_start" TIMESTAMP NOT NULL GENERATED ALWAYS AS ROW BEGIN'); +}); + +void test('emits GENERATED BY DEFAULT for the ROW CHANGE TIMESTAMP generated clause', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'last_mod', + type: 'TIMESTAMP', + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + generatedColumn: true, + generatedColumnType: 'ROW CHANGE TIMESTAMP', + generatedColumnGenerated: 'BY DEFAULT', + }); + + assert.equal( + columnDefinition, + '"last_mod" TIMESTAMP NOT NULL GENERATED BY DEFAULT FOR EACH ROW ON UPDATE AS ROW CHANGE TIMESTAMP', + ); +}); + +void test('emits NO MAXVALUE / NO MINVALUE instead of numeric bounds', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'id', + type: 'BIGINT', + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + identity: { generated: 'ALWAYS', noMaxValue: true, noMinValue: true }, + }); + + assert.equal(columnDefinition, '"id" BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY (NO MINVALUE, NO MAXVALUE)'); +}); + +void test('emits IMPLICITLY HIDDEN after a ROWID generated clause', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'row_id', + type: 'ROWID', + primaryKey: false, + unique: false, + nullable: false, + generated: 'ALWAYS', + implicitlyHidden: true, + isActivated: true, + }); + + assert.equal(columnDefinition, '"row_id" ROWID NOT NULL GENERATED ALWAYS IMPLICITLY HIDDEN'); +}); + +void test('emits IMPLICITLY HIDDEN after an identity clause', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'id', + type: 'BIGINT', + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + identity: { generated: 'ALWAYS' }, + implicitlyHidden: true, + }); + + assert.equal(columnDefinition, '"id" BIGINT NOT NULL GENERATED ALWAYS AS IDENTITY IMPLICITLY HIDDEN'); +}); + +void test('does not emit IMPLICITLY HIDDEN for a plain expression-generated column', () => { + const columnDefinition = ddlProvider.convertColumnDefinition({ + name: 'full_name', + type: 'VARCHAR', + length: 100, + primaryKey: false, + unique: false, + nullable: false, + isActivated: true, + generatedColumn: true, + columnGenerationExpression: "first_name || ' ' || last_name", + implicitlyHidden: true, + }); + + assert.ok(!columnDefinition.includes('IMPLICITLY HIDDEN')); +}); + +void test('uses a schema-qualified UDT name for a column that references a model definition', () => { + const jsonSchema = ddlProvider.hydrateJsonSchemaColumn( + { + $ref: '#model/definitions/MONEY', + isActivated: true, + }, + { + type: 'numeric', + mode: 'integer', + }, + ); + const hydratedColumn = ddlProvider.hydrateColumn({ + columnDefinition: { + name: 'price', + type: 'MONEY', + nullable: true, + isActivated: true, + }, + jsonSchema, + schemaData: { schemaName: 'new_schema' }, + }); + + assert.equal(jsonSchema.$ref, '#model/definitions/MONEY'); + assert.equal(hydratedColumn.isUDTRef, true); + assert.equal(ddlProvider.convertColumnDefinition(hydratedColumn), '"price" "new_schema"."MONEY"'); +});