From b87bf9498e281c46d794f44f6e5f86fa4615d4b6 Mon Sep 17 00:00:00 2001 From: Linsted Date: Tue, 25 Aug 2026 09:57:34 +0200 Subject: [PATCH 1/3] HCK-17344: Add Sequences configuration to containerLevelConfig.json --- .../container_level/containerLevelConfig.json | 144 ++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/properties_pane/container_level/containerLevelConfig.json b/properties_pane/container_level/containerLevelConfig.json index 6d834aa..169297b 100644 --- a/properties_pane/container_level/containerLevelConfig.json +++ b/properties_pane/container_level/containerLevelConfig.json @@ -206,5 +206,149 @@ making sure that you maintain a proper JSON format. } ], "containerLevelKeys": [] + }, + { + "lowerTab": "Sequences", + "structure": [ + { + "propertyName": "Sequences", + "propertyType": "group", + "propertyKeyword": "sequences", + "propertyTooltip": "CREATE SEQUENCE objects in this schema.", + "structure": [ + { + "propertyName": "Sequence name", + "propertyKeyword": "sequenceName", + "propertyTooltip": "Unqualified name in this schema. Must not identify an existing sequence at the current server, including Db2-generated names.", + "propertyType": "text", + "validation": { + "required": true + } + }, + { + "propertyName": "Data type", + "propertyKeyword": "dataType", + "propertyTooltip": "Exact numeric type with scale 0. Default INTEGER.", + "propertyType": "select", + "defaultValue": "INTEGER", + "options": ["SMALLINT", "INTEGER", "BIGINT", "DECIMAL"] + }, + { + "propertyName": "Precision", + "propertyKeyword": "precision", + "propertyTooltip": "DECIMAL precision (1–31). Scale is always 0. Default 5 when DECIMAL is specified without precision.", + "propertyType": "numeric", + "valueType": "integer", + "allowNegative": false, + "minValue": 1, + "maxValue": 31, + "defaultValue": 5, + "step": 1, + "dependency": { + "key": "dataType", + "value": "DECIMAL" + } + }, + { + "propertyName": "Start", + "propertyKeyword": "start", + "propertyTooltip": "Specifies the first value for the sequence. If the START WITH clause is not specified, the default is the MINVALUE for ascending sequences and the MAXVALUE for descending sequences. This value is not necessarily the value a sequence would cycle to; the cycle range is MINVALUE and MAXVALUE, and START WITH may lie outside that range. If MINVALUE and MAXVALUE are also omitted, the effective start is 1 (ascending) or -1 (descending).", + "propertyType": "numeric", + "valueType": "integer", + "allowNegative": true, + "step": 1 + }, + { + "propertyName": "Increment", + "propertyKeyword": "increment", + "propertyTooltip": "Interval between values. Positive = ascending, negative = descending, 0 = treated as ascending. Default 1.", + "propertyType": "numeric", + "valueType": "integer", + "allowNegative": true, + "step": 1 + }, + { + "propertyName": "No min value", + "propertyKeyword": "noMinValue", + "propertyTooltip": "Emits NO MINVALUE. Default is the START WITH value, or 1 if START WITH is omitted (ascending); the minimum value of the data type (descending).", + "propertyType": "checkbox" + }, + { + "propertyName": "Min value", + "propertyKeyword": "minValue", + "propertyTooltip": "Minimum of the cycle range. Must be ≤ Max value. Setting Min = Max = Start together with CYCLE is one way to define a constant sequence; INCREMENT 0 is the other and is usually preferred.", + "propertyType": "numeric", + "valueType": "integer", + "allowNegative": true, + "step": 1, + "dependency": { + "type": "not", + "values": { + "key": "noMinValue", + "value": true + } + } + }, + { + "propertyName": "No max value", + "propertyKeyword": "noMaxValue", + "propertyTooltip": "Emits NO MAXVALUE. Default is the maximum value of the data type (ascending); the START WITH value, or -1 if START WITH is omitted (descending).", + "propertyType": "checkbox" + }, + { + "propertyName": "Max value", + "propertyKeyword": "maxValue", + "propertyTooltip": "Maximum of the cycle range. Must be ≥ Min value.", + "propertyType": "numeric", + "valueType": "integer", + "allowNegative": true, + "step": 1, + "dependency": { + "type": "not", + "values": { + "key": "noMaxValue", + "value": true + } + } + }, + { + "propertyName": "Cycle", + "propertyKeyword": "cycle", + "propertyTooltip": "CYCLE wraps using Min/Max. NO CYCLE errors at the bound. Default NO CYCLE. CYCLE can generate duplicates.", + "propertyType": "select", + "options": ["", "CYCLE", "NO CYCLE"] + }, + { + "propertyName": "Cache", + "propertyKeyword": "cache", + "propertyTooltip": "CACHE n preallocates values (min 2, default 20). NO CACHE does not. Empty omits the clause (Db2 uses CACHE 20).", + "propertyType": "select", + "options": ["", "CACHE", "NO CACHE"] + }, + { + "propertyName": "Cache value", + "propertyKeyword": "cacheValue", + "propertyTooltip": "Upper bound on preallocated values. Minimum 2. Default 20.", + "propertyType": "numeric", + "valueType": "integer", + "allowNegative": false, + "minValue": 2, + "defaultValue": 20, + "step": 1, + "dependency": { + "key": "cache", + "value": "CACHE" + } + }, + { + "propertyName": "Order", + "propertyKeyword": "order", + "propertyTooltip": "ORDER assigns in request order. In data sharing, ORDER implies NO CACHE even if CACHE n is specified. Default NO ORDER.", + "propertyType": "select", + "options": ["", "ORDER", "NO ORDER"] + } + ] + } + ] } ] From 823499a36f9068a44cdc061110ed17ddd8fa78b2 Mon Sep 17 00:00:00 2001 From: Linsted Date: Tue, 25 Aug 2026 10:43:42 +0200 Subject: [PATCH 2/3] HCK-17344: Implement CREATE SEQUENCE functionality in DDL provider --- .../ddlHelpers/sequence/getSequencesScript.js | 153 ++++++++++++++++++ .../ddlProvider/ddlProvider.js | 22 ++- forward_engineering/ddlProvider/templates.js | 2 + forward_engineering/types/ddlProvider.d.ts | 24 ++- 4 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 forward_engineering/ddlProvider/ddlHelpers/sequence/getSequencesScript.js diff --git a/forward_engineering/ddlProvider/ddlHelpers/sequence/getSequencesScript.js b/forward_engineering/ddlProvider/ddlHelpers/sequence/getSequencesScript.js new file mode 100644 index 0000000..4543465 --- /dev/null +++ b/forward_engineering/ddlProvider/ddlHelpers/sequence/getSequencesScript.js @@ -0,0 +1,153 @@ +/** @import {SchemaSequence} from '../../../types/ddlProvider' */ + +const trim = require('lodash/trim'); +const templates = require('../../templates'); +const { assignTemplates } = require('../../../utils/assignTemplates'); +const { getNamePrefixedWithSchemaName, setTab } = require('../../../utils/general'); + +const DEFAULT_CACHE_SIZE = 20; + +/** + * Check whether a numeric field is present, including 0. + * + * @param {{ value?: number }} params Value to test. + * @returns {boolean} Whether the value is a number. + */ +const isPresentNumber = ({ value }) => typeof value === 'number' && !Number.isNaN(value); + +/** + * Check whether a sequence row has a usable name. + * + * @param {{ sequenceName?: string }} params Sequence name. + * @returns {boolean} Whether the name is non-blank. + */ +const hasSequenceName = ({ sequenceName }) => Boolean(trim(sequenceName ?? '')); + +/** + * Build the AS clause. + * + * @param {{ dataType?: string; precision?: number }} params Type fields. + * @returns {string} AS clause, or empty. + */ +const getAsClause = ({ dataType, precision }) => { + if (!dataType) { + return ''; + } + + if (dataType === 'DECIMAL' && isPresentNumber({ value: precision })) { + return `AS DECIMAL(${precision},0)`; + } + + return `AS ${dataType}`; +}; + +/** + * Build a keyword-plus-number clause, treating 0 as present. + * + * @param {{ keyword: string; value?: number }} params Clause parts. + * @returns {string} Clause, or empty. + */ +const getNumericClause = ({ keyword, value }) => { + return isPresentNumber({ value }) ? `${keyword} ${value}` : ''; +}; + +/** + * Build MINVALUE / MAXVALUE or the matching NO-* clause. + * + * @param {{ noBound?: boolean; value?: number; noKeyword: string; valueKeyword: string }} params Bound fields. + * @returns {string} Bound clause, or empty. + */ +const getBoundClause = ({ noBound, value, noKeyword, valueKeyword }) => { + if (noBound) { + return noKeyword; + } + + return getNumericClause({ keyword: valueKeyword, value }); +}; + +/** + * Build the CACHE / NO CACHE clause. + * + * @param {{ cache?: string; cacheValue?: number }} params Cache fields. + * @returns {string} Cache clause, or empty. + */ +const getCacheClause = ({ cache, cacheValue }) => { + if (cache === 'CACHE') { + const size = isPresentNumber({ value: cacheValue }) ? cacheValue : DEFAULT_CACHE_SIZE; + return `CACHE ${size}`; + } + + if (!cache) { + return ''; + } + + return cache; +}; + +/** + * Build CREATE SEQUENCE clauses in IBM order. + * + * @param {{ sequence: SchemaSequence }} params Sequence row. + * @returns {string[]} Non-empty clauses. + */ +const getSequenceClauses = ({ sequence }) => { + return [ + getAsClause({ dataType: sequence.dataType, precision: sequence.precision }), + getNumericClause({ keyword: 'START WITH', value: sequence.start }), + getNumericClause({ keyword: 'INCREMENT BY', value: sequence.increment }), + getBoundClause({ + noBound: sequence.noMinValue, + value: sequence.minValue, + noKeyword: 'NO MINVALUE', + valueKeyword: 'MINVALUE', + }), + getBoundClause({ + noBound: sequence.noMaxValue, + value: sequence.maxValue, + noKeyword: 'NO MAXVALUE', + valueKeyword: 'MAXVALUE', + }), + sequence.cycle ?? '', + getCacheClause({ cache: sequence.cache, cacheValue: sequence.cacheValue }), + sequence.order ?? '', + ].filter(Boolean); +}; + +/** + * Emit one CREATE SEQUENCE statement. + * + * @param {{ schemaName: string; sequence: SchemaSequence }} params Schema and row. + * @returns {string} CREATE SEQUENCE DDL. + */ +const createSequenceScript = ({ schemaName, sequence }) => { + const name = getNamePrefixedWithSchemaName({ name: sequence.sequenceName ?? '', schemaName }); + const clauses = getSequenceClauses({ sequence }); + const clausesBlock = clauses.length === 0 ? '' : `\n${setTab({ text: clauses.join('\n') })}`; + + return assignTemplates({ + template: templates.createSequence, + templateData: { + name, + clauses: clausesBlock, + }, + }); +}; + +/** + * Emit CREATE SEQUENCE statements for a schema’s sequence group. + * + * @param {{ schemaName?: string; sequences?: SchemaSequence[] }} params Hydrated schema fields. + * @returns {string} Joined CREATE SEQUENCE DDL, or empty. + */ +const getSequencesScript = ({ schemaName = '', sequences = [] }) => { + const sequenceRows = Array.isArray(sequences) ? sequences : []; + + return sequenceRows + .filter(sequence => hasSequenceName({ sequenceName: sequence.sequenceName })) + .map(sequence => createSequenceScript({ schemaName, sequence })) + .join('\n\n'); +}; + +module.exports = { + getSequencesScript, +}; diff --git a/forward_engineering/ddlProvider/ddlProvider.js b/forward_engineering/ddlProvider/ddlProvider.js index 4945c41..c9f2f6e 100644 --- a/forward_engineering/ddlProvider/ddlProvider.js +++ b/forward_engineering/ddlProvider/ddlProvider.js @@ -2,6 +2,7 @@ * @import { * CheckConstraintInput, * ContainerData, + * ContainerLevelPreparedData, * CreateSchemaParams, * CreateTableParams, * DdlProvider, @@ -62,6 +63,7 @@ const { joinActivatedAndDeactivatedStatements } = require('../utils/joinActivate const { getIndexName } = require('./ddlHelpers/index/getIndexName.js'); const { getIndexType } = require('./ddlHelpers/index/getIndexType.js'); const { getIndexOptions } = require('./ddlHelpers/index/getIndexOptions.js'); +const { getSequencesScript } = require('./ddlHelpers/sequence/getSequencesScript.js'); const { getNumericValue } = require('../utils/general.js'); /** @@ -121,12 +123,14 @@ const providerHasType = type => hasType({ descriptors, type }); * Hydrate schema data. * * @param {ContainerData} containerData Container data. + * @param {ContainerLevelPreparedData} [data] Prepared container-level data from Studio. * @returns {SchemaData} Hydrated schema. */ -const hydrateSchema = containerData => ({ +const hydrateSchema = (containerData, data) => ({ schemaName: containerData.name, isActivated: containerData.isActivated, description: containerData.description, + sequences: data?.sequences, }); /** @@ -150,6 +154,21 @@ const createSchema = ({ schemaName, isActivated = true }) => { return commentDeactivatedStatement(setSchemaStatement + '\n', { isActivated }); }; +/** + * Create schema-level CREATE SEQUENCE statements. + * + * @param {SchemaData} schemaData Hydrated schema. + * @returns {string} CREATE SEQUENCE DDL, or empty. + */ +const createSchemaSequences = ({ schemaName, sequences, isActivated = true }) => { + const script = getSequencesScript({ schemaName, sequences }); + if (!script) { + return ''; + } + + return commentDeactivatedStatement(script, { isActivated, isPartOfLine: false }); +}; + /** * Return no DDL for dropping a schema. Db2 for z/OS schemas are qualifiers rather than standalone objects that can be * dropped. This method remains in the provider for framework compatibility. @@ -980,6 +999,7 @@ module.exports = (_baseProvider, _options, _app) => ({ hasType: providerHasType, hydrateSchema, createSchema, + createSchemaSequences, dropSchema, alterSchema, createUdt, diff --git a/forward_engineering/ddlProvider/templates.js b/forward_engineering/ddlProvider/templates.js index 3f8be7b..18d9b00 100644 --- a/forward_engineering/ddlProvider/templates.js +++ b/forward_engineering/ddlProvider/templates.js @@ -1,6 +1,8 @@ module.exports = { setSchema: 'SET SCHEMA = ${schemaName};', + createSequence: 'CREATE SEQUENCE ${name}${clauses};', + createType: 'CREATE DISTINCT TYPE ${name} AS ${sourceType};', dropType: 'DROP TYPE ${name};', diff --git a/forward_engineering/types/ddlProvider.d.ts b/forward_engineering/types/ddlProvider.d.ts index 2962cc8..bb335bf 100644 --- a/forward_engineering/types/ddlProvider.d.ts +++ b/forward_engineering/types/ddlProvider.d.ts @@ -150,10 +150,31 @@ export type HydratedColumn = { itemsType?: string; }; +export type SchemaSequence = { + sequenceName?: string; + dataType?: string; + precision?: number; + start?: number; + increment?: number; + noMinValue?: boolean; + minValue?: number; + noMaxValue?: boolean; + maxValue?: number; + cycle?: string; + cache?: string; + cacheValue?: number; + order?: string; +}; + +export type ContainerLevelPreparedData = { + sequences?: SchemaSequence[]; +}; + export type SchemaData = { schemaName: string; isActivated?: boolean; description?: string; + sequences?: SchemaSequence[]; }; export type ContainerData = { @@ -739,8 +760,9 @@ export type DdlProvider = { getTypesDescriptors(): TypeDescriptors; hasType(type: string): boolean; - hydrateSchema(containerData: ContainerData, data?: unknown): SchemaData; + hydrateSchema(containerData: ContainerData, data?: ContainerLevelPreparedData): SchemaData; createSchema(params: CreateSchemaParams): string; + createSchemaSequences(schemaData: SchemaData): string; dropSchema(params: DropSchemaParams): string; alterSchema(schemaName: string, data?: unknown): string; createUdt(udt: HydratedColumn, dbData?: unknown): string; From 59a6d309dc7808807f6295b197ed257c96251156 Mon Sep 17 00:00:00 2001 From: Linsted Date: Tue, 25 Aug 2026 10:44:25 +0200 Subject: [PATCH 3/3] HCK-17344: add tests --- test/createSchemaSequences.test.js | 256 +++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 test/createSchemaSequences.test.js diff --git a/test/createSchemaSequences.test.js b/test/createSchemaSequences.test.js new file mode 100644 index 0000000..a425c19 --- /dev/null +++ b/test/createSchemaSequences.test.js @@ -0,0 +1,256 @@ +/** @import {SchemaSequence} from '../forward_engineering/types/ddlProvider' */ + +const assert = require("node:assert/strict"); +const { test } = require("node:test"); +const createDdlProvider = require("../forward_engineering/ddlProvider/ddlProvider"); + +const ddlProvider = createDdlProvider(null, null, null); + +const FILLED_SOME_SEQ = { + GUID: "06742052-5bca-42eb-8b3a-df5f7ec9cb94", + dataType: "BIGINT", + sequenceName: "some_seq", + increment: -1, + start: -100, + cache: "CACHE", + noMinValue: false, + noMaxValue: false, + cacheValue: 2, +}; + +const NEAR_EMPTY_SEQ_3 = { + GUID: "1994ec03-f740-4a9b-b0eb-ae43ce5662e0", + dataType: "INTEGER", + sequenceName: "seq_3", +}; + +const FULL_CLAUSE_SEQ = { + sequenceName: "full_seq", + dataType: "INTEGER", + start: 1, + increment: 1, + minValue: 1, + maxValue: 100, + cycle: "CYCLE", + cache: "CACHE", + cacheValue: 20, + order: "ORDER", +}; + +const FILLED_SOME_SEQ_SCRIPT = `CREATE SEQUENCE "new_schema"."some_seq" + AS BIGINT + START WITH -100 + INCREMENT BY -1 + CACHE 2;`; + +const NEAR_EMPTY_SEQ_3_SCRIPT = `CREATE SEQUENCE "new_schema"."seq_3" + AS INTEGER;`; + +const FULL_CLAUSE_AND_SEQ_3_SCRIPT = `CREATE SEQUENCE "new_schema"."full_seq" + AS INTEGER + START WITH 1 + INCREMENT BY 1 + MINVALUE 1 + MAXVALUE 100 + CYCLE + CACHE 20 + ORDER; + +CREATE SEQUENCE "new_schema"."seq_3" + AS INTEGER;`; + +/** + * Hydrate the schema then emit CREATE SEQUENCE, matching Studio’s call order. + * + * @param {{ sequences?: SchemaSequence[]; isActivated?: boolean; name?: string }} params Schema fields. + * @returns {string} Sequence DDL. + */ +const generateSequences = ({ + sequences, + isActivated = true, + name = "new_schema", +} = {}) => { + const schemaData = ddlProvider.hydrateSchema( + { name, isActivated }, + { sequences }, + ); + return ddlProvider.createSchemaSequences(schemaData); +}; + +/** + * Strip block comments so remaining text is live SQL. + * + * @param {string} script DDL text. + * @returns {string} Text outside block comments. + */ +const liveSql = (script) => script.replaceAll(/\/\*[\s\S]*?\*\//gu, ""); + +test("hydration forwards sequences from the second argument when they are present", () => { + const sequences = [NEAR_EMPTY_SEQ_3]; + const schemaData = ddlProvider.hydrateSchema( + { name: "new_schema", isActivated: true }, + { sequences }, + ); + + assert.equal(schemaData.sequences, sequences); +}); + +test("hydration preserves isActivated", () => { + const deactivated = ddlProvider.hydrateSchema( + { name: "new_schema", isActivated: false }, + {}, + ); + const activated = ddlProvider.hydrateSchema( + { name: "new_schema", isActivated: true }, + {}, + ); + + assert.equal(deactivated.isActivated, false); + assert.equal(activated.isActivated, true); +}); + +test("hydration does not throw when sequences are absent", () => { + assert.doesNotThrow(() => + ddlProvider.hydrateSchema({ name: "new_schema", isActivated: true }), + ); + assert.doesNotThrow(() => + ddlProvider.hydrateSchema( + { name: "new_schema", isActivated: true }, + {}, + ), + ); +}); + +test("filled some_seq fixture shape emits the CREATE contract", () => { + assert.equal( + generateSequences({ sequences: [FILLED_SOME_SEQ] }), + FILLED_SOME_SEQ_SCRIPT, + ); +}); + +test("near-empty seq_3 fixture shape emits the CREATE contract", () => { + assert.equal( + generateSequences({ sequences: [NEAR_EMPTY_SEQ_3] }), + NEAR_EMPTY_SEQ_3_SCRIPT, + ); +}); + +test("CACHE with no cacheValue emits CACHE 20, never bare CACHE", () => { + const script = generateSequences({ + sequences: [ + { + sequenceName: "cache_default", + dataType: "INTEGER", + cache: "CACHE", + }, + ], + }); + + assert.match(script, /CACHE 20/u); + assert.doesNotMatch(script, /\tCACHE;/u); + assert.doesNotMatch(script, /\tCACHE\n/u); +}); + +test("start 0 and increment 0 emit START WITH 0 and INCREMENT BY 0", () => { + const script = generateSequences({ + sequences: [ + { + sequenceName: "zero_seq", + dataType: "INTEGER", + start: 0, + increment: 0, + }, + ], + }); + + assert.match(script, /START WITH 0/u); + assert.match(script, /INCREMENT BY 0/u); +}); + +test("noMinValue true with a leftover minValue emits NO MINVALUE and ignores the number", () => { + const script = generateSequences({ + sequences: [ + { + sequenceName: "no_min_seq", + dataType: "INTEGER", + noMinValue: true, + minValue: 42, + }, + ], + }); + + assert.match(script, /NO MINVALUE/u); + assert.doesNotMatch(script, /MINVALUE 42/u); +}); + +test("DECIMAL with no precision emits AS DECIMAL", () => { + const script = generateSequences({ + sequences: [{ sequenceName: "dec_seq", dataType: "DECIMAL" }], + }); + + assert.match(script, /AS DECIMAL;/u); + assert.doesNotMatch(script, /AS DECIMAL\(/u); +}); + +test("DECIMAL with precision 10 emits AS DECIMAL(10,0)", () => { + const script = generateSequences({ + sequences: [ + { sequenceName: "dec_p_seq", dataType: "DECIMAL", precision: 10 }, + ], + }); + + assert.match(script, /AS DECIMAL\(10,0\)/u); +}); + +test("deactivated schema comments every CREATE SEQUENCE line, not only the first", () => { + const script = generateSequences({ + sequences: [FILLED_SOME_SEQ], + isActivated: false, + }); + + assert.match(script, /\/\*/u); + assert.match(script, /\*\//u); + assert.match(script, /AS BIGINT/u); + assert.match(script, /START WITH -100/u); + assert.match(script, /INCREMENT BY -1/u); + + const uncommented = liveSql(script); + assert.doesNotMatch(uncommented, /AS BIGINT/u); + assert.doesNotMatch(uncommented, /START WITH/u); + assert.doesNotMatch(uncommented, /INCREMENT BY/u); +}); + +test("a blank sequenceName produces no statement for that row", () => { + assert.equal( + generateSequences({ + sequences: [{ sequenceName: "", dataType: "INTEGER" }], + }), + "", + ); + assert.equal( + generateSequences({ + sequences: [{ sequenceName: " ", dataType: "INTEGER" }], + }), + "", + ); + assert.equal( + generateSequences({ sequences: [{ dataType: "INTEGER" }] }), + "", + ); + assert.equal( + generateSequences({ + sequences: [ + { sequenceName: "", dataType: "BIGINT" }, + NEAR_EMPTY_SEQ_3, + ], + }), + NEAR_EMPTY_SEQ_3_SCRIPT, + ); +}); + +test("full-clause row plus seq_3 emits clause order and a blank line between statements", () => { + assert.equal( + generateSequences({ sequences: [FULL_CLAUSE_SEQ, NEAR_EMPTY_SEQ_3] }), + FULL_CLAUSE_AND_SEQ_3_SCRIPT, + ); +});