From fc1d90da7af4fd6aa29ffc485b7cbc774adb9f86 Mon Sep 17 00:00:00 2001 From: Jan Wedding Date: Mon, 18 Mar 2024 18:29:36 +0100 Subject: [PATCH] Allow defining default values on entity fields Fix #25094 --- generators/angular/generator.ts | 36 + .../update/_entityFile_-form.service.ts.ejs | 16 +- .../base-application/support/prepare-field.js | 2 +- generators/base-entity-changes/generator.ts | 39 + generators/base-entity-changes/types.d.ts | 2 + .../bootstrap-application/generator.spec.ts | 40 + .../incremental-liquibase.spec.ts.snap | 901 ++++++++++++++++-- generators/liquibase/generator.ts | 83 +- .../liquibase/incremental-liquibase.spec.ts | 172 ++++ generators/liquibase/support/prepare-field.js | 15 +- .../liquibase/changelog/added_entity.xml.ejs | 2 +- .../changelog/updated_entity.xml.ejs | 6 +- .../updated_entity_constraints.xml.ejs | 27 +- 13 files changed, 1229 insertions(+), 112 deletions(-) diff --git a/generators/angular/generator.ts b/generators/angular/generator.ts index 6fd522b6546b..53cb4aec1cc2 100644 --- a/generators/angular/generator.ts +++ b/generators/angular/generator.ts @@ -160,6 +160,42 @@ export default class AngularGenerator extends BaseApplicationGenerator { return this.delegateTasksToBlueprint(() => this.preparingEachEntity); } + get preparingEachEntityField() { + return this.asPreparingEachEntityFieldTaskGroup({ + prepareField({ field }) { + mutateData(field, { + fieldTsDefaultValue: ({ fieldTsDefaultValue, defaultValue, fieldTypeCharSequence, fieldTypeTimed }) => { + let returnValue: string | undefined; + if (fieldTsDefaultValue !== undefined || defaultValue !== undefined) { + let fieldDefaultValue; + if (fieldTsDefaultValue !== undefined) { + fieldDefaultValue = fieldTsDefaultValue; + } else { + fieldDefaultValue = defaultValue; + } + + fieldDefaultValue = String(fieldDefaultValue).replace(/'/g, "\\'"); + + if (fieldTypeCharSequence) { + returnValue = `'${fieldDefaultValue}'`; + } else if (fieldTypeTimed) { + returnValue = `dayjs('${fieldDefaultValue}')`; + } else { + returnValue = fieldDefaultValue; + } + } + + return returnValue; + }, + }); + }, + }); + } + + get [BaseApplicationGenerator.PREPARING_EACH_ENTITY_FIELD]() { + return this.delegateTasksToBlueprint(() => this.preparingEachEntityField); + } + get default() { return this.asDefaultTaskGroup({ loadEntities() { diff --git a/generators/angular/templates/src/main/webapp/app/entities/_entityFolder_/update/_entityFile_-form.service.ts.ejs b/generators/angular/templates/src/main/webapp/app/entities/_entityFolder_/update/_entityFile_-form.service.ts.ejs index d5d37a54e143..27978c472b17 100644 --- a/generators/angular/templates/src/main/webapp/app/entities/_entityFolder_/update/_entityFile_-form.service.ts.ejs +++ b/generators/angular/templates/src/main/webapp/app/entities/_entityFolder_/update/_entityFile_-form.service.ts.ejs @@ -200,12 +200,16 @@ _%> <%_ for (field of fields) { const { fieldName, fieldTypeBoolean, fieldTypeTimed, fieldTypeLocalDate } = field; _%> - <%_ if (field.id) { _%> - <%= fieldName %>: null, - <%_ } else if (fieldTypeBoolean) { _%> - <%= fieldName %>: false, - <%_ } else if (fieldTypeTimed) { _%> - <%= fieldName %>: currentTime, + <%_ if (field.fieldTsDefaultValue) { _%> + <%= fieldName %>: <%- field.fieldTsDefaultValue %>, + <%_ } else { _%> + <%_ if (field.id) { _%> + <%= fieldName %>: null, + <%_ } else if (fieldTypeBoolean) { _%> + <%= fieldName %>: false, + <%_ } else if (fieldTypeTimed) { _%> + <%= fieldName %>: currentTime, + <%_ } _%> <%_ } _%> <%_ } _%> <%_ for (const relationship of relationships.filter(({ persistableRelationship }) => persistableRelationship)) { diff --git a/generators/base-application/support/prepare-field.js b/generators/base-application/support/prepare-field.js index f1b5731589a4..d9ce539df671 100644 --- a/generators/base-application/support/prepare-field.js +++ b/generators/base-application/support/prepare-field.js @@ -242,7 +242,7 @@ function _derivedProperties(field) { fieldType === INTEGER || fieldType === LONG || fieldType === FLOAT || fieldType === DOUBLE || fieldType === BIG_DECIMAL, fieldTypeBinary: fieldType === BYTES || fieldType === BYTE_BUFFER, fieldTypeTimed: fieldType === ZONED_DATE_TIME || fieldType === INSTANT, - fieldTypeCharSequence: fieldType === STRING || fieldType === UUID, + fieldTypeCharSequence: fieldType === STRING || fieldType === UUID || fieldType === TEXT_BLOB, fieldTypeTemporal: fieldType === ZONED_DATE_TIME || fieldType === INSTANT || fieldType === LOCAL_DATE, fieldValidationRequired: validationRules.includes(REQUIRED), fieldValidationMin: validationRules.includes(MIN), diff --git a/generators/base-entity-changes/generator.ts b/generators/base-entity-changes/generator.ts index eff0b79fa539..1795e6c37e2f 100644 --- a/generators/base-entity-changes/generator.ts +++ b/generators/base-entity-changes/generator.ts @@ -36,6 +36,8 @@ const baseChangelog: () => Omit this.hasAnyDefaultValue(field)); + const newFieldsWithDefaultValues = newFields.filter(field => this.hasAnyDefaultValue(field)); + + // find the old fields that have not been deleted anyway or otherwise where the default value is different on the same new field + const removedDefaultValueFields = oldFieldsWithDefaultValues + .filter(oldField => !removedFieldNames.includes(oldField.fieldName)) + .filter( + // field was not removed, so check its default value + oldField => + this.doDefaultValuesDiffer( + oldField, + newFields.find(newField => newField.fieldName === oldField.fieldName), + ), + ); + + // find the new fields that have not been added newly anyway or otherwise where the old field had a different default value + const addedDefaultValueFields = newFieldsWithDefaultValues + .filter(newField => !addedFieldNames.includes(newField.fieldName)) + .filter( + // field was not added newly, so check its default value + newField => + this.doDefaultValuesDiffer( + oldFields.find(oldField => oldField.fieldName === newField.fieldName), + newField, + ), + ); + return { ...baseChangelog(), previousEntity: oldConfig, @@ -176,7 +205,17 @@ export default abstract class GeneratorBaseEntityChanges extends GeneratorBaseAp addedRelationships, removedRelationships, relationshipsToRecreateForeignKeysOnly, + removedDefaultValueFields, + addedDefaultValueFields, }; }); } + + private hasAnyDefaultValue(field) { + return field.defaultValue !== undefined || field.defaultValueComputed; + } + + private doDefaultValuesDiffer(field1, field2) { + return field1.defaultValue !== field2.defaultValue || field1.defaultValueComputed !== field2.defaultValueComputed; + } } diff --git a/generators/base-entity-changes/types.d.ts b/generators/base-entity-changes/types.d.ts index c33cc9f7ac1a..5067f9d08b8c 100644 --- a/generators/base-entity-changes/types.d.ts +++ b/generators/base-entity-changes/types.d.ts @@ -14,5 +14,7 @@ export type BaseChangelog = { addedRelationships: any[]; removedRelationships: any[]; relationshipsToRecreateForeignKeysOnly: any[]; + removedDefaultValueFields: any[]; + addedDefaultValueFields: any[]; changelogData: any; }; diff --git a/generators/bootstrap-application/generator.spec.ts b/generators/bootstrap-application/generator.spec.ts index 72ff27b82063..d484616863cc 100644 --- a/generators/bootstrap-application/generator.spec.ts +++ b/generators/bootstrap-application/generator.spec.ts @@ -275,6 +275,8 @@ describe(`generator - ${generator}`, () => { "createRandexp": Any, "dynamic": false, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "Id", "fieldIsEnum": false, "fieldName": "id", @@ -331,6 +333,8 @@ describe(`generator - ${generator}`, () => { "jpaGeneratedValue": true, "jpaGeneratedValueIdentity": false, "jpaGeneratedValueSequence": false, + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "\${uuidType}", "nullable": true, "path": [ @@ -359,6 +363,8 @@ describe(`generator - ${generator}`, () => { "columnType": "varchar(50)", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "Login", "fieldIsEnum": false, "fieldName": "login", @@ -418,6 +424,8 @@ describe(`generator - ${generator}`, () => { "javaValueGenerator": "UUID.randomUUID().toString()", "javaValueSample1": ""login1"", "javaValueSample2": ""login2"", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "string", "maxlength": 50, "nullable": false, @@ -446,6 +454,8 @@ describe(`generator - ${generator}`, () => { "columnType": "varchar(50)", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "FirstName", "fieldIsEnum": false, "fieldName": "firstName", @@ -501,6 +511,8 @@ describe(`generator - ${generator}`, () => { "javaValueGenerator": "UUID.randomUUID().toString()", "javaValueSample1": ""firstName1"", "javaValueSample2": ""firstName2"", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "string", "maxlength": 50, "nullable": true, @@ -528,6 +540,8 @@ describe(`generator - ${generator}`, () => { "columnType": "varchar(50)", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "LastName", "fieldIsEnum": false, "fieldName": "lastName", @@ -583,6 +597,8 @@ describe(`generator - ${generator}`, () => { "javaValueGenerator": "UUID.randomUUID().toString()", "javaValueSample1": ""lastName1"", "javaValueSample2": ""lastName2"", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "string", "maxlength": 50, "nullable": true, @@ -610,6 +626,8 @@ describe(`generator - ${generator}`, () => { "columnType": "varchar(191)", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "Email", "fieldIsEnum": false, "fieldName": "email", @@ -669,6 +687,8 @@ describe(`generator - ${generator}`, () => { "javaValueGenerator": "UUID.randomUUID().toString()", "javaValueSample1": ""email1"", "javaValueSample2": ""email2"", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "string", "maxlength": 191, "nullable": false, @@ -697,6 +717,8 @@ describe(`generator - ${generator}`, () => { "columnType": "varchar(256)", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "ImageUrl", "fieldIsEnum": false, "fieldName": "imageUrl", @@ -752,6 +774,8 @@ describe(`generator - ${generator}`, () => { "javaValueGenerator": "UUID.randomUUID().toString()", "javaValueSample1": ""imageUrl1"", "javaValueSample2": ""imageUrl2"", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "string", "maxlength": 256, "nullable": true, @@ -780,6 +804,8 @@ describe(`generator - ${generator}`, () => { "columnType": "boolean", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "Activated", "fieldIsEnum": false, "fieldName": "activated", @@ -828,6 +854,8 @@ describe(`generator - ${generator}`, () => { "fieldWithContentType": false, "generateFakeData": Any, "javaFieldType": "Boolean", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "boolean", "nullable": true, "path": [ @@ -854,6 +882,8 @@ describe(`generator - ${generator}`, () => { "columnType": "varchar(10)", "createRandexp": Any, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "LangKey", "fieldIsEnum": false, "fieldName": "langKey", @@ -909,6 +939,8 @@ describe(`generator - ${generator}`, () => { "javaValueGenerator": "UUID.randomUUID().toString()", "javaValueSample1": ""langKey1"", "javaValueSample2": ""langKey2"", + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "string", "maxlength": 10, "nullable": true, @@ -1140,6 +1172,8 @@ describe(`generator - ${generator}`, () => { "createRandexp": Any, "dynamic": false, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "Id", "fieldIsEnum": false, "fieldName": "id", @@ -1195,6 +1229,8 @@ describe(`generator - ${generator}`, () => { "jpaGeneratedValue": true, "jpaGeneratedValueIdentity": false, "jpaGeneratedValueSequence": false, + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "\${uuidType}", "nullable": true, "path": [ @@ -1479,6 +1515,8 @@ describe(`generator - ${generator}`, () => { "createRandexp": Any, "dynamic": false, "entity": Any, + "fieldDefaultValueDefined": false, + "fieldHasAnyDefaultValue": false, "fieldInJavaBeanMethod": "Id", "fieldIsEnum": false, "fieldName": "id", @@ -1534,6 +1572,8 @@ describe(`generator - ${generator}`, () => { "jpaGeneratedValue": true, "jpaGeneratedValueIdentity": false, "jpaGeneratedValueSequence": false, + "liquibaseDefaultValueAttributeName": undefined, + "liquibaseDefaultValueAttributeValue": undefined, "loadColumnType": "\${uuidType}", "nullable": true, "path": [ diff --git a/generators/liquibase/__snapshots__/incremental-liquibase.spec.ts.snap b/generators/liquibase/__snapshots__/incremental-liquibase.spec.ts.snap index cc5b819d1cfd..c76ae36465f0 100644 --- a/generators/liquibase/__snapshots__/incremental-liquibase.spec.ts.snap +++ b/generators/liquibase/__snapshots__/incremental-liquibase.spec.ts.snap @@ -766,7 +766,7 @@ ROLE_USER } `; -exports[`generator - app - --incremental-changelog when incremental liquibase files exists with --recreate-initial-changelog should match snapshot 1`] = ` +exports[`generator - app - --incremental-changelog when creating entities with default values should match snapshot 1`] = ` { "src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml": { "contents": " @@ -890,6 +890,117 @@ exports[`generator - app - --incremental-changelog when incremental liquibase fi +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200101000100_added_entity_One.xml": { + "contents": " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200101000200_added_entity_Two.xml": { + "contents": " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ", "stateCleared": "modified", }, @@ -912,6 +1023,36 @@ ROLE_USER 1;ROLE_ADMIN 1;ROLE_USER 2;ROLE_USER +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/fake-data/20200101000100_entity_one.csv": { + "contents": "uuid;active;some_long;some_date +a1df19f5-8443-40a3-8e7a-19565b40d496;true;22836;2019-12-31T15:06:18 +2c50c94b-7c95-4335-8cb9-118919bd4336;true;21605;2019-12-31T01:33:52 +b5f092aa-099b-490c-8c7c-07af81e531ef;false;14477;2019-12-31T06:33:30 +46457939-a951-4564-a1d4-d5d3b4dfec2f;true;18697;2019-12-31T17:28:06 +1e4e93b3-c2b8-4a6d-88e6-1a9b656300e5;false;11256;2019-12-31T00:06:24 +9e95158c-941f-41e8-bc32-202a03207d0a;true;19187;2019-12-31T10:13:10 +4b7a589a-4d97-4922-9d1e-62602b821ad1;true;16257;2019-12-31T14:34:15 +fc96da86-95ae-4017-9b20-ec7f7014e317;true;17776;2019-12-31T19:36:49 +308aac48-4491-45c0-a000-32f7c840f617;false;32679;2019-12-31T07:26:53 +6f25a7bc-f876-4d99-9f7a-93ea6a721621;false;18344;2019-12-31T06:30:28 +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/fake-data/20200101000200_entity_two.csv": { + "contents": "id;comment;computed_date +1;costly save gadzooks;2019-12-31T14:53:42 +2;of;2019-12-31T00:41:01 +3;drat;2019-12-31T20:58:51 +4;frown mini;2019-12-31T05:16:55 +5;insignificant wherever;2019-12-31T01:06:06 +6;rarely;2019-12-31T06:03:13 +7;until horsewhip;2019-12-31T09:19:14 +8;peace forenenst;2019-12-31T00:07:12 +9;sport boo;2019-12-31T03:46:22 +10;until;2019-12-31T06:13:57 ", "stateCleared": "modified", }, @@ -929,6 +1070,8 @@ ROLE_USER + + @@ -939,34 +1082,7 @@ ROLE_USER } `; -exports[`generator - app - --incremental-changelog when incremental liquibase files exists with default options should match snapshot 1`] = ` -{ - "src/main/resources/config/liquibase/data/authority.csv": { - "contents": "name -ROLE_ADMIN -ROLE_USER -", - "stateCleared": "modified", - }, - "src/main/resources/config/liquibase/data/user.csv": { - "contents": "id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by -1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system -2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system -", - "stateCleared": "modified", - }, - "src/main/resources/config/liquibase/data/user_authority.csv": { - "contents": "user_id;authority_name -1;ROLE_ADMIN -1;ROLE_USER -2;ROLE_USER -", - "stateCleared": "modified", - }, -} -`; - -exports[`generator - app - --incremental-changelog when initially creating an application with entities with relationships having on handlers should match snapshot 1`] = ` +exports[`generator - app - --incremental-changelog when incremental liquibase files exists with --recreate-initial-changelog should match snapshot 1`] = ` { "src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml": { "contents": " @@ -1093,59 +1209,82 @@ exports[`generator - app - --incremental-changelog when initially creating an ap ", "stateCleared": "modified", }, - "src/main/resources/config/liquibase/changelog/20200101000100_added_entity_One.xml": { + "src/main/resources/config/liquibase/data/authority.csv": { + "contents": "name +ROLE_ADMIN +ROLE_USER +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/user.csv": { + "contents": "id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system +2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/user_authority.csv": { + "contents": "user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +2;ROLE_USER +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/master.xml": { "contents": " - - - - - - - - - - - - - - - - - - + xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + + + + + + - - - - - - - - + + + + ", "stateCleared": "modified", }, - "src/main/resources/config/liquibase/changelog/20200101000100_added_entity_constraints_One.xml": { +} +`; + +exports[`generator - app - --incremental-changelog when incremental liquibase files exists with default options should match snapshot 1`] = ` +{ + "src/main/resources/config/liquibase/data/authority.csv": { + "contents": "name +ROLE_ADMIN +ROLE_USER +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/user.csv": { + "contents": "id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system +2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/user_authority.csv": { + "contents": "user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +2;ROLE_USER +", + "stateCleared": "modified", + }, +} +`; + +exports[`generator - app - --incremental-changelog when initially creating an application with entities with relationships having on handlers should match snapshot 1`] = ` +{ + "src/main/resources/config/liquibase/changelog/00000000000000_initial_schema.xml": { "contents": " - - - + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200101000100_added_entity_One.xml": { + "contents": " + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200101000100_added_entity_constraints_One.xml": { + "contents": " + + + + + + + ", "stateCleared": "modified", }, @@ -1421,6 +1737,419 @@ exports[`generator - app - --incremental-changelog when modifying an existing re } `; +exports[`generator - app - --incremental-changelog when modifying default values, fields with default values and relationships should match snapshot 1`] = ` +{ + "src/main/resources/config/liquibase/changelog/20200102000100_updated_entity_One.xml": { + "contents": " + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000100_updated_entity_migrate_One.xml": { + "contents": " + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000200_updated_entity_Two.xml": { + "contents": " + + + + + + + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000200_updated_entity_migrate_Two.xml": { + "contents": " + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000300_updated_entity_constraints_One.xml": { + "contents": " + + + + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000300_updated_entity_migrate_One.xml": { + "contents": " + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000400_updated_entity_Two.xml": { + "contents": " + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000400_updated_entity_constraints_Two.xml": { + "contents": " + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/changelog/20200102000400_updated_entity_migrate_Two.xml": { + "contents": " + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/authority.csv": { + "contents": "name +ROLE_ADMIN +ROLE_USER +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/user.csv": { + "contents": "id;login;password_hash;first_name;last_name;email;image_url;activated;lang_key;created_by;last_modified_by +1;admin;$2a$10$gSAhZrxMllrbgj/kkK9UceBPpChGWJA7SYIb1Mqo.n5aNLq1/oRrC;Administrator;Administrator;admin@localhost;;true;en;system;system +2;user;$2a$10$VEjxo0jq2YG9Rbk2HmX9S.k1uZBGYUHdUcid3g/vfiEl7lwWgOH/K;User;User;user@localhost;;true;en;system;system +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/data/user_authority.csv": { + "contents": "user_id;authority_name +1;ROLE_ADMIN +1;ROLE_USER +2;ROLE_USER +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/fake-data/20200102000100_entity_one.csv": { + "contents": "uuid;another_boolean +7b52c50c-94b7-4c95-8335-cb9118919bd4; +3633aeb5-f092-4aa0-b99b-90cc7c07af81; +531ef1f7-b464-4579-839a-9515641d4d5d; +b4dfec2f-a394-41e4-8e93-b3c2b8a6d8e6; +a9b65630-0e52-4a5f-b9e9-5158c941f1e8; +32202a03-207d-40ac-a599-4b7a589a4d97; +22d1e626-02b8-421a-9d17-376fc96da869; +ae017b20-ec7f-4701-94e3-176382308aac; +844915c0-0003-42f7-9c84-0f6179cfb6f2; +a7bcf876-d99f-47a9-93ea-6a721621598b; +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/fake-data/20200102000200_entity_two.csv": { + "contents": "id;comment_new +1; +2; +3; +4; +5; +6; +7; +8; +9; +10; +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/fake-data/20200102000300_entity_one.csv": { + "contents": "uuid +7b52c50c-94b7-4c95-8335-cb9118919bd4 +3633aeb5-f092-4aa0-b99b-90cc7c07af81 +531ef1f7-b464-4579-839a-9515641d4d5d +b4dfec2f-a394-41e4-8e93-b3c2b8a6d8e6 +a9b65630-0e52-4a5f-b9e9-5158c941f1e8 +32202a03-207d-40ac-a599-4b7a589a4d97 +22d1e626-02b8-421a-9d17-376fc96da869 +ae017b20-ec7f-4701-94e3-176382308aac +844915c0-0003-42f7-9c84-0f6179cfb6f2 +a7bcf876-d99f-47a9-93ea-6a721621598b +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/fake-data/20200102000400_entity_two.csv": { + "contents": "id +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +", + "stateCleared": "modified", + }, + "src/main/resources/config/liquibase/master.xml": { + "contents": " + + + + + + + + + + + + + + + + + + + + + + + + +", + "stateCleared": "modified", + }, +} +`; + exports[`generator - app - --incremental-changelog when modifying fields and relationships at the same time in different entities should match snapshot 1`] = ` { "src/main/resources/config/liquibase/changelog/20200102000100_updated_entity_One.xml": { diff --git a/generators/liquibase/generator.ts b/generators/liquibase/generator.ts index 09c1d5aef25d..0ae5509a3225 100644 --- a/generators/liquibase/generator.ts +++ b/generators/liquibase/generator.ts @@ -17,7 +17,7 @@ * limitations under the License. */ import fs from 'fs'; -import { min } from 'lodash-es'; +import { escape, min } from 'lodash-es'; import BaseEntityChangesGenerator from '../base-entity-changes/index.js'; import { liquibaseFiles } from './files.js'; @@ -132,6 +132,13 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { prepareFieldForLiquibase(entity, field); } }, + validateConsistencyOfField({ entity, field }) { + if (field.columnRequired && field.fieldHasAnyDefaultValue) { + this.handleCheckFailure( + `The field ${field.fieldName} in entity ${entity.name} has both columnRequired and a defaultValue, this can lead to unexpected behaviors.`, + ); + } + }, }); } @@ -228,6 +235,8 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { fieldChangelog: true, addedRelationships: [], removedRelationships: [], + removedDefaultValueFields: [], + addedDefaultValueFields: [], relationshipsToRecreateForeignKeysOnly: [], }, application, @@ -239,7 +248,10 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { for (const databaseChangelog of changes) { if ( databaseChangelog.incremental && - (databaseChangelog.addedRelationships.length > 0 || databaseChangelog.removedRelationships.length > 0) + (databaseChangelog.addedRelationships.length > 0 || + databaseChangelog.removedRelationships.length > 0 || + databaseChangelog.removedDefaultValueFields.length > 0 || + databaseChangelog.addedDefaultValueFields.length > 0) ) { this.databaseChangelogs.push( this.prepareChangelog({ @@ -587,6 +599,9 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { hasRelationshipConstraint, shouldWriteAnyRelationship, relationshipsToRecreateForeignKeysOnly, + hasDefaultValueChange, + removedDefaultValueFields, + addedDefaultValueFields, } = changelogData; const context = { @@ -603,35 +618,54 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { hasRelationshipConstraint, shouldWriteAnyRelationship, relationshipsToRecreateForeignKeysOnly, + hasDefaultValueChange, + removedDefaultValueFields, + addedDefaultValueFields, }; const promises: Promise[] = []; - promises.push(this.writeFiles({ sections: updateEntityFiles, context })); + if (this._isBasicEntityUpdate(changelogData)) { + promises.push(this.writeFiles({ sections: updateEntityFiles, context })); + } - if (!changelogData.skipFakeData && (changelogData.addedFields.length > 0 || shouldWriteAnyRelationship)) { + if (this._requiresWritingFakeData(changelogData)) { promises.push(this.writeFiles({ sections: fakeFiles, context })); promises.push(this.writeFiles({ sections: updateMigrateFiles, context })); } - if (hasFieldConstraint || shouldWriteAnyRelationship) { + if (this._requiresConstraintUpdates(changelogData)) { promises.push(this.writeFiles({ sections: updateConstraintsFiles, context })); } return Promise.all(promises); } + private _requiresConstraintUpdates(changelogData: any) { + return changelogData.hasFieldConstraint || changelogData.addedRelationships.length > 0 || changelogData.hasDefaultValueChange; + } + + private _isBasicEntityUpdate(changelogData: any) { + return changelogData.addedFields.length > 0 || changelogData.removedFields.length > 0 || changelogData.shouldWriteAnyRelationship; + } + + private _requiresWritingFakeData(changelogData: any) { + return !changelogData.skipFakeData && (changelogData.addedFields.length || changelogData.addedRelationships.length); + } + /** * Write files for updated entities. */ _addUpdateFilesReferences({ entity, databaseChangelog, changelogData, source }) { - source.addLiquibaseIncrementalChangelog({ changelogName: `${databaseChangelog.changelogDate}_updated_entity_${entity.entityClass}` }); + if (this._isBasicEntityUpdate(changelogData)) { + source.addLiquibaseIncrementalChangelog({ changelogName: `${databaseChangelog.changelogDate}_updated_entity_${entity.entityClass}` }); + } - if (!changelogData.skipFakeData && (changelogData.addedFields.length > 0 || changelogData.shouldWriteAnyRelationship)) { + if (this._requiresWritingFakeData(changelogData)) { source.addLiquibaseIncrementalChangelog({ changelogName: `${databaseChangelog.changelogDate}_updated_entity_migrate_${entity.entityClass}`, }); } - if (changelogData.hasFieldConstraint || changelogData.shouldWriteAnyRelationship) { + if (this._requiresConstraintUpdates(changelogData)) { source.addLiquibaseIncrementalChangelog({ changelogName: `${databaseChangelog.changelogDate}_updated_entity_constraints_${entity.entityClass}`, }); @@ -650,6 +684,21 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { return liquibaseComment(text, addRemarksTag); } + /** + * @private + * Create the fitting liquibase default value attribute for a field. + * @param field + * @param leadingWhitespace + * @returns + */ + createDefaultValueLiquibaseAttribute(field, leadingWhitespace = false) { + if (!field.defaultValueComputed && !field.fieldDefaultValueDefined) { + return ''; + } + + return `${leadingWhitespace ? ' ' : ''}${field.liquibaseDefaultValueAttributeName}="${escape(field.liquibaseDefaultValueAttributeValue.toString())}"`; + } + prepareChangelog({ databaseChangelog, application }) { if (!databaseChangelog.changelogDate) { databaseChangelog.changelogDate = this.dateFormatForLiquibase(); @@ -723,6 +772,8 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { entityChanges.addedRelationships = databaseChangelog.addedRelationships; entityChanges.removedRelationships = databaseChangelog.removedRelationships; entityChanges.relationshipsToRecreateForeignKeysOnly = databaseChangelog.relationshipsToRecreateForeignKeysOnly; + entityChanges.removedDefaultValueFields = databaseChangelog.removedDefaultValueFields; + entityChanges.addedDefaultValueFields = databaseChangelog.addedDefaultValueFields; } /* Required by the templates */ @@ -747,17 +798,23 @@ export default class LiquibaseGenerator extends BaseEntityChangesGenerator { entityChanges.addedFields.length > 0 || entityChanges.removedFields.length > 0 || entityChanges.addedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) || - entityChanges.removedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable); + entityChanges.removedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) || + entityChanges.addedDefaultValueFields.length > 0 || + entityChanges.removedDefaultValueFields.length > 0; if (entityChanges.requiresUpdateChangelogs) { - entityChanges.hasFieldConstraint = entityChanges.addedFields.some(field => field.unique || !field.nullable); + entityChanges.hasFieldConstraint = entityChanges.addedFields.some( + field => field.unique || (field.columnRequired && !field.fieldHasAnyDefaultValue) || field.shouldCreateContentType, + ); + entityChanges.hasDefaultValueChange = + entityChanges.addedDefaultValueFields.length > 0 || entityChanges.removedDefaultValueFields.length > 0; entityChanges.hasRelationshipConstraint = entityChanges.addedRelationships.some( relationship => (relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) && (relationship.unique || !relationship.nullable), ); - entityChanges.shouldWriteAnyRelationship = entityChanges.addedRelationships.some( - relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable, - ); + entityChanges.shouldWriteAnyRelationship = + entityChanges.addedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable) || + entityChanges.removedRelationships.some(relationship => relationship.shouldWriteRelationship || relationship.shouldWriteJoinTable); } return databaseChangelog; diff --git a/generators/liquibase/incremental-liquibase.spec.ts b/generators/liquibase/incremental-liquibase.spec.ts index d67457ffb118..26af72fb8660 100644 --- a/generators/liquibase/incremental-liquibase.spec.ts +++ b/generators/liquibase/incremental-liquibase.spec.ts @@ -115,6 +115,50 @@ relationship ManyToOne { One{anotherEnt} to @OnDelete("SET NULL") @OnUpdate("CASCADE") Another, }`; +const jdlApplicationWithEntitiesWithDefaultValues = ` +${jdlApplication} +entity One { + @Id uuid UUID + @defaultValue(true) + active Boolean + @defaultValue(42) + someLong Long + someDate Instant +} + +entity Two { + @defaultValue("some-default-string-value") + comment String + @defaultValueComputed("NOW(6)") + computedDate Instant +} +`; + +const jdlApplicationWithEntitiesWithChangedDefaultValuesAndNewRelationship = ` +${jdlApplication} +entity One { + @Id uuid UUID + @defaultValue(true) + active Boolean + @defaultValue(69) + someLong Long + @defaultValueComputed("NOW(6)") + someDate Instant + @defaultValue(true) + anotherBoolean Boolean +} + +entity Two { + @defaultValue("some-default-string-value") + commentNew String + computedDate Instant +} + +relationship ManyToOne { + Two to One +} +`; + const generatorPath = join(__dirname, '../server/index.js'); const mockedGenerators = ['jhipster:common']; @@ -1247,6 +1291,134 @@ entity Customer { expect(runResult.getSnapshot('**/src/main/resources/config/liquibase/**')).toMatchSnapshot(); }); }); + + describe('when creating entities with default values', () => { + let runResult; + before(async () => { + const baseName = 'JhipsterApp'; + const initialState = createImporterFromContent(jdlApplicationWithEntitiesWithDefaultValues, { + ...options, + creationTimestampConfig: options.creationTimestamp, + }).import(); + const applicationWithEntities = initialState.exportedApplicationsWithEntities[baseName]; + expect(applicationWithEntities).toBeTruthy(); + expect(applicationWithEntities.entities.length).toBe(2); + runResult = await helpers + .create(generatorPath) + .withJHipsterConfig(config) + .withOptions({ ...options, applicationWithEntities }) + .run(); + }); + + after(() => runResult.cleanup()); + + it('should create application', () => { + runResult.assertFile(['.yo-rc.json']); + }); + it('should create entity config file', () => { + runResult.assertFile([join('.jhipster', 'One.json'), join('.jhipster', 'Two.json')]); + }); + it('should create entity initial changelog', () => { + runResult.assertFile([ + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000100_added_entity_One.xml`, + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000200_added_entity_Two.xml`, + ]); + + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000100_added_entity_One.xml`, + 'column name="active" type="boolean" defaultValueBoolean="true"', + ); + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000100_added_entity_One.xml`, + 'column name="some_long" type="bigint" defaultValueNumeric="42"', + ); + + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000200_added_entity_Two.xml`, + 'column name="comment" type="varchar(255)" defaultValue="some-default-string-value"', + ); + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000200_added_entity_Two.xml`, + /* eslint-disable no-template-curly-in-string */ + 'column name="computed_date" type="${datetimeType}" defaultValueComputed="NOW(6)"', + ); + }); + + it('should match snapshot', () => { + expect(runResult.getSnapshot('**/src/main/resources/config/liquibase/**')).toMatchSnapshot(); + }); + }); + + describe('when modifying default values, fields with default values and relationships', () => { + let runResult; + before(async () => { + const baseName = 'JhipsterApp'; + const initialState = createImporterFromContent(jdlApplicationWithEntitiesWithDefaultValues, { + ...options, + creationTimestampConfig: options.creationTimestamp, + }).import(); + const applicationWithEntities = initialState.exportedApplicationsWithEntities[baseName]; + expect(applicationWithEntities).toBeTruthy(); + expect(applicationWithEntities.entities.length).toBe(2); + runResult = await helpers + .create(generatorPath) + .withJHipsterConfig(config) + .withOptions({ ...options, applicationWithEntities }) + .run(); + + const state = createImporterFromContent(jdlApplicationWithEntitiesWithChangedDefaultValuesAndNewRelationship, { + ...options, + }).import(); + + runResult = await runResult + .create(generatorPath) + .withOptions({ + ...options, + applicationWithEntities: state.exportedApplicationsWithEntities[baseName], + creationTimestamp: '2020-01-02', + }) + .run(); + }); + + after(() => runResult.cleanup()); + + it('should create application', () => { + runResult.assertFile(['.yo-rc.json']); + }); + it('should create entity config file', () => { + runResult.assertFile([join('.jhipster', 'One.json'), join('.jhipster', 'Two.json')]); + }); + it('should create entity initial changelog', () => { + runResult.assertFile([ + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000100_added_entity_One.xml`, + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000200_added_entity_Two.xml`, + ]); + + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000100_added_entity_One.xml`, + 'column name="active" type="boolean" defaultValueBoolean="true"', + ); + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000100_added_entity_One.xml`, + 'column name="some_long" type="bigint" defaultValueNumeric="42"', + ); + + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000200_added_entity_Two.xml`, + 'column name="comment" type="varchar(255)" defaultValue="some-default-string-value"', + ); + runResult.assertFileContent( + `${SERVER_MAIN_RES_DIR}config/liquibase/changelog/20200101000200_added_entity_Two.xml`, + /* eslint-disable no-template-curly-in-string */ + 'column name="computed_date" type="${datetimeType}" defaultValueComputed="NOW(6)"', + ); + }); + + it('should match snapshot', () => { + expect(runResult.getSnapshot('**/src/main/resources/config/liquibase/**')).toMatchSnapshot(); + }); + }); + describe('entities with/without byte fields should create fake data', () => { [ { diff --git a/generators/liquibase/support/prepare-field.js b/generators/liquibase/support/prepare-field.js index 6a48b2924644..a1f1c8205b45 100644 --- a/generators/liquibase/support/prepare-field.js +++ b/generators/liquibase/support/prepare-field.js @@ -139,14 +139,27 @@ function parseLiquibaseLoadColumnType(entity, field) { } export default function prepareField(entity, field) { + field.fieldDefaultValueDefined = field.defaultValue !== undefined; + field.fieldHasAnyDefaultValue = field.fieldDefaultValueDefined || field.defaultValueComputed !== undefined; + mutateData(field, { __override__: false, columnType: data => parseLiquibaseColumnType(entity, data), - shouldDropDefaultValue: data => data.fieldType === ZONED_DATE_TIME || data.fieldType === INSTANT, + shouldDropDefaultValue: data => !data.fieldHasAnyDefaultValue && (data.fieldType === ZONED_DATE_TIME || data.fieldType === INSTANT), shouldCreateContentType: data => data.fieldType === BYTES && data.fieldTypeBlobContent !== TEXT, columnRequired: data => data.nullable === false || (data.fieldValidate === true && data.fieldValidateRules.includes('required')), nullable: data => !data.columnRequired, loadColumnType: data => parseLiquibaseLoadColumnType(entity, data), + liquibaseDefaultValueAttributeValue: ({ defaultValue, defaultValueComputed }) => defaultValueComputed ?? defaultValue, + liquibaseDefaultValueAttributeName: ({ defaultValueComputed, liquibaseDefaultValueAttributeValue }) => { + if (liquibaseDefaultValueAttributeValue === undefined) return undefined; + if (defaultValueComputed) return 'defaultValueComputed'; + if (field.fieldTypeNumeric) return 'defaultValueNumeric'; + if (field.fieldTypeDateTime) return 'defaultValueDate'; + if (field.fieldTypeBoolean) return 'defaultValueBoolean'; + return 'defaultValue'; + }, }); + return field; } diff --git a/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs index 1db93f56d07c..fb221aff0b6e 100644 --- a/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs +++ b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/added_entity.xml.ejs @@ -30,7 +30,7 @@ > <%_ for (field of fields) { _%> - <% if (field.id && field.liquibaseAutoIncrement) { %> autoIncrement="true" startWith="1500"<%_ } %>> + <%- this.formatAsLiquibaseRemarks(field.documentation, true) %><% if (field.id && field.liquibaseAutoIncrement) { %> autoIncrement="true" startWith="1500"<%_ } %>> <%_ if (field.id) { _%> <%_ } else if (field.unique) { _%> diff --git a/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs index bbe10312c7f4..7c7776d26863 100644 --- a/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs +++ b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity.xml.ejs @@ -25,15 +25,15 @@ <%_ for (field of addedFields) { _%> - /> + <%- this.formatAsLiquibaseRemarks(field.documentation, true) %>/> <%_ if (field.shouldCreateContentType) { _%> <%_ } } // End for (field of addedFields) _%> <%_ for (field of addedFields) { - if (field.fieldType === 'ZonedDateTime' || field.fieldType === 'Instant') { _%> - + if (field.shouldDropDefaultValue) { _%> + <%_ } _%> <%_ } _%> diff --git a/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs index 845bddcaece6..8936a0c18643 100644 --- a/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs +++ b/generators/liquibase/templates/src/main/resources/config/liquibase/changelog/updated_entity_constraints.xml.ejs @@ -48,6 +48,31 @@ if (hasFieldConstraint) { _%> } _%> <% } _%> +<%_ if (removedDefaultValueFields && removedDefaultValueFields.length) { _%> + + + <%_ for (field of removedDefaultValueFields) { _%> + + <%_ } _%> + +<% } _%> +<%_ if (addedDefaultValueFields && addedDefaultValueFields.length) { _%> + + + <%_ for (field of addedDefaultValueFields) { _%> + /> + <%_ } _%> + +<% } _%> <%_ if (hasRelationshipConstraint) { _%>