diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index be47a606..e86b64d0 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "adobe@2025", "boxlang-cfml@1"] + cfengine: ["lucee@5", "lucee@6", "lucee@6-full-null", "adobe@2021", "adobe@2023", "adobe@2025", "boxlang-cfml@1"] coldbox: ["coldbox@^7", "coldbox@^8"] experimental: [ false ] include: @@ -95,4 +95,4 @@ jobs: DB_NAME: quick DB_USER: quick DB_PASSWORD: quick - run: box testbox run \ No newline at end of file + run: box testbox run diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c636f8b3..2b597a4c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -19,7 +19,7 @@ jobs: strategy: fail-fast: false matrix: - cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "adobe@2025", "boxlang-cfml@1"] + cfengine: ["lucee@5", "lucee@6", "lucee@6-full-null", "adobe@2021", "adobe@2023", "adobe@2025", "boxlang-cfml@1"] coldbox: ["coldbox@^7", "coldbox@^8"] services: mysql: @@ -97,4 +97,4 @@ jobs: - name: Commit Format Changes uses: stefanzweifel/git-auto-commit-action@v5.2.0 with: - commit_message: Apply cfformat changes \ No newline at end of file + commit_message: Apply cfformat changes diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c7a0fcb..da7948b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,7 +14,7 @@ jobs: strategy: fail-fast: false matrix: - cfengine: ["lucee@5", "lucee@6", "adobe@2021", "adobe@2023", "adobe@2025", "boxlang-cfml@1"] + cfengine: ["lucee@5", "lucee@6", "lucee@6-full-null", "adobe@2021", "adobe@2023", "adobe@2025", "boxlang-cfml@1"] coldbox: ["coldbox@^7", "coldbox@^8"] services: mysql: diff --git a/server-lucee@6-full-null.json b/server-lucee@6-full-null.json new file mode 100644 index 00000000..6b6e7603 --- /dev/null +++ b/server-lucee@6-full-null.json @@ -0,0 +1,21 @@ +{ + "name":"quick-lucee@6-full-null", + "app":{ + "serverHomeDirectory":".engine/lucee6FullNull", + "cfengine":"lucee@6" + }, + "jvm":{ + "properties":{ + "lucee.full.null.support":"true" + } + }, + "web":{ + "http":{ + "port":"60299" + }, + "rewrites":{ + "enable":"true" + } + }, + "openBrowser":"false" +} diff --git a/tests/resources/ModuleIntegrationSpec.cfc b/tests/resources/ModuleIntegrationSpec.cfc index 22851440..65da67e3 100644 --- a/tests/resources/ModuleIntegrationSpec.cfc +++ b/tests/resources/ModuleIntegrationSpec.cfc @@ -72,4 +72,13 @@ component extends="coldbox.system.testing.BaseTestCase" appMapping="/app" { return arraySlice( createObject( "java", "java.util.HashSet" ).init( arguments.items ).toArray(), 1 ); } + /** + * Returns whether the current Lucee server is running with full null support. + */ + public boolean function hasFullNullSupport() { + return server.keyExists( "lucee" ) && + server.system.properties.keyExists( "lucee.full.null.support" ) && + server.system.properties[ "lucee.full.null.support" ] == "true"; + } + } diff --git a/tests/specs/integration/BaseEntity/AsQuerySpec.cfc b/tests/specs/integration/BaseEntity/AsQuerySpec.cfc index d2dc845a..2b754622 100644 --- a/tests/specs/integration/BaseEntity/AsQuerySpec.cfc +++ b/tests/specs/integration/BaseEntity/AsQuerySpec.cfc @@ -15,9 +15,8 @@ component extends="tests.resources.ModuleIntegrationSpec" { result.createdDate = dateTimeFormat( result.createdDate, "yyyy-mm-dd hh:nn:ss" ); result.modifiedDate = dateTimeFormat( result.modifiedDate, "yyyy-mm-dd hh:nn:ss" ); } - expect( results[ 1 ] ).toBeStruct(); - expect( results[ 1 ] ).toBe( { + var expectedFirstResult = { "city" : "Salt Lake City", "countryId" : "02B84D66-0AA0-F7FB-1F71AFC954843861", "createdDate" : "2017-07-28 02:06:36", @@ -36,9 +35,9 @@ component extends="tests.resources.ModuleIntegrationSpec" { "type" : "admin", "username" : "elpete", "zip" : "84123" - } ); + }; expect( results[ 2 ] ).toBeStruct(); - expect( results[ 2 ] ).toBe( { + var expectedSecondResult = { "city" : "Salt Lake City", "countryId" : "02B84D66-0AA0-F7FB-1F71AFC954843861", "createdDate" : "2017-07-28 02:07:16", @@ -57,7 +56,28 @@ component extends="tests.resources.ModuleIntegrationSpec" { "type" : "limited", "username" : "johndoe", "zip" : "84123" - } ); + }; + if ( hasFullNullSupport() ) { + expect( results[ 1 ].email ).toBeNull(); + expect( results[ 1 ].streetTwo ).toBeNull(); + expect( results[ 2 ].email ).toBeNull(); + expect( results[ 2 ].favoritePost_id ).toBeNull(); + expect( results[ 2 ].streetTwo ).toBeNull(); + results[ 1 ].delete( "email" ); + results[ 1 ].delete( "streetTwo" ); + results[ 2 ].delete( "email" ); + results[ 2 ].delete( "favoritePost_id" ); + results[ 2 ].delete( "streetTwo" ); + expectedFirstResult.delete( "email" ); + expectedFirstResult.delete( "streetTwo" ); + expectedSecondResult.delete( "email" ); + expectedSecondResult.delete( "favoritePost_id" ); + expectedSecondResult.delete( "streetTwo" ); + } + expect( results[ 1 ] ).toHaveLength( expectedFirstResult.count() ); + expect( results[ 2 ] ).toHaveLength( expectedSecondResult.count() ); + expectedFirstResult.each( ( key, value ) => expect( results[ 1 ][ key ] ).toBe( value ) ); + expectedSecondResult.each( ( key, value ) => expect( results[ 2 ][ key ] ).toBe( value ) ); } ); it( "can execute with subselects", function() { @@ -74,7 +94,11 @@ component extends="tests.resources.ModuleIntegrationSpec" { expect( results[ 1 ][ "latestPostId" ] ).toBe( 523526 ); expect( results[ 2 ] ).toBeStruct(); expect( results[ 2 ] ).toHaveKey( "latestPostId" ); - expect( results[ 2 ][ "latestPostId" ] ).toBe( "" ); + if ( hasFullNullSupport() ) { + expect( results[ 2 ][ "latestPostId" ] ).toBeNull(); + } else { + expect( results[ 2 ][ "latestPostId" ] ).toBe( "" ); + } } ); it( "can do eager loading", function() { diff --git a/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc b/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc index c5457845..641c2595 100644 --- a/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeCastsSpec.cfc @@ -100,12 +100,20 @@ component extends="tests.resources.ModuleIntegrationSpec" { it( "preserves null values when using BooleanCast", () => { var pn = getInstance( "PhoneNumber" ).find( 3 ); expect( pn.isNullAttribute( "confirmed" ) ).toBeTrue( "[confirmed] should be considered null" ); - expect( pn.getConfirmed() ).toBe( "" ); + if ( hasFullNullSupport() ) { + expect( pn.getConfirmed() ).toBeNull(); + } else { + expect( pn.getConfirmed() ).toBe( "" ); + } pn.update( { "active" : false } ).refresh(); expect( pn.isNullAttribute( "confirmed" ) ).toBeTrue( "[confirmed] should remain null after saving" ); - expect( pn.getConfirmed() ).toBe( "" ); + if ( hasFullNullSupport() ) { + expect( pn.getConfirmed() ).toBeNull(); + } else { + expect( pn.getConfirmed() ).toBe( "" ); + } } ); it( "allows custom casts to handle null database values", () => { diff --git a/tests/specs/integration/BaseEntity/AttributeSpec.cfc b/tests/specs/integration/BaseEntity/AttributeSpec.cfc index 5bf9abd3..c40c9611 100644 --- a/tests/specs/integration/BaseEntity/AttributeSpec.cfc +++ b/tests/specs/integration/BaseEntity/AttributeSpec.cfc @@ -178,7 +178,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { var memento = getInstance( "User" ).findOrFail( 1 ).getMemento(); memento.createdDate = dateTimeFormat( memento.createdDate, "yyyy-mm-dd hh:nn:ss" ); memento.modifiedDate = dateTimeFormat( memento.modifiedDate, "yyyy-mm-dd hh:nn:ss" ); - expect( memento ).toBe( { + var expected = { "id" : 1, "username" : "elpete", "firstName" : "Eric", @@ -190,7 +190,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { "modifiedDate" : "2017-07-28 02:06:36", "type" : "admin", "email" : "", - "externalId" : "1234", + "externalID" : "1234", "address" : { "streetOne" : "123 Elm Street", "streetTwo" : "", @@ -199,7 +199,13 @@ component extends="tests.resources.ModuleIntegrationSpec" { "zip" : "84123" }, "favoritePost_id" : "1245" - } ); + }; + if ( hasFullNullSupport() ) { + expect( memento.address.streetTwo ).toBeNull(); + memento.address.delete( "streetTwo" ); + expected.address.delete( "streetTwo" ); + } + expect( memento ).toBe( expected ); } ); // https://github.com/coldbox-modules/quick/issues/127 diff --git a/tests/specs/integration/BaseEntity/MementoSpec.cfc b/tests/specs/integration/BaseEntity/MementoSpec.cfc index 4792cd8d..aa7fba8a 100644 --- a/tests/specs/integration/BaseEntity/MementoSpec.cfc +++ b/tests/specs/integration/BaseEntity/MementoSpec.cfc @@ -48,7 +48,7 @@ component extends="tests.resources.ModuleIntegrationSpec" { memento.publishedDate = dateTimeFormat( memento.publishedDate, "yyyy-mm-dd hh:nn:ss" ); memento.author.createdDate = dateTimeFormat( memento.author.createdDate, "yyyy-mm-dd hh:nn:ss" ); memento.author.modifiedDate = dateTimeFormat( memento.author.modifiedDate, "yyyy-mm-dd hh:nn:ss" ); - expect( memento ).toBe( { + var expected = { "post_pk" : "1245", "body" : "My awesome post body", "createdDate" : "2017-07-28 02:07:00", @@ -77,7 +77,13 @@ component extends="tests.resources.ModuleIntegrationSpec" { }, "favoritePost_id" : "1245" } - } ); + }; + if ( hasFullNullSupport() ) { + expect( memento.author.address.streetTwo ).toBeNull(); + memento.author.address.delete( "streetTwo" ); + expected.author.address.delete( "streetTwo" ); + } + expect( memento ).toBe( expected ); } ); it( "can check if two entities are the same", function() { diff --git a/tests/specs/integration/BaseEntity/NullValuesSpec.cfc b/tests/specs/integration/BaseEntity/NullValuesSpec.cfc index d894fa79..6108ac3c 100644 --- a/tests/specs/integration/BaseEntity/NullValuesSpec.cfc +++ b/tests/specs/integration/BaseEntity/NullValuesSpec.cfc @@ -2,10 +2,19 @@ component extends="tests.resources.ModuleIntegrationSpec" { function run() { describe( "Null Values Spec", function() { - it( "returns null values as a string by default", function() { + it( "returns database nulls according to the engine null-support mode", function() { var user = getInstance( "User" ).findOrFail( 3 ); - expect( user.getCountryId() ).toBe( "" ); - expect( user.getMemento().countryId ).toBe( "" ); + + if ( hasFullNullSupport() ) { + expect( user.getCountryId() ).toBeNull( "The entity getter should preserve the database null." ); + expect( user.getMemento().countryId ).toBe( + "", + "Mementifier should apply its configured nullDefaultValue." + ); + } else { + expect( user.getCountryId() ).toBe( "" ); + expect( user.getMemento().countryId ).toBe( "" ); + } } ); it( "saves a column containing an empty string as null in the database by default", function() { diff --git a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc index 84a2aea1..340f190e 100644 --- a/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc +++ b/tests/specs/integration/BaseEntity/Relationships/BelongsToSpec.cfc @@ -142,7 +142,11 @@ component extends="tests.resources.ModuleIntegrationSpec" { post.author() .dissociate() .save(); - expect( post.retrieveAttribute( "user_id" ) ).toBe( "" ); + if ( hasFullNullSupport() ) { + expect( post.retrieveAttribute( "user_id" ) ).toBeNull(); + } else { + expect( post.retrieveAttribute( "user_id" ) ).toBe( "" ); + } expect( getInstance( "User" ) .find( userId ) diff --git a/tests/specs/integration/BaseEntity/SaveSpec.cfc b/tests/specs/integration/BaseEntity/SaveSpec.cfc index 61a9d2ef..973f4296 100644 --- a/tests/specs/integration/BaseEntity/SaveSpec.cfc +++ b/tests/specs/integration/BaseEntity/SaveSpec.cfc @@ -84,7 +84,11 @@ component extends="tests.resources.ModuleIntegrationSpec" { existingUser.save(); var userRowsPostSave = queryExecute( "SELECT * FROM users" ); expect( userRowsPostSave ).toHaveLength( 5 ); - expect( userRowsPostSave.email ).toBe( "" ); + if ( hasFullNullSupport() ) { + expect( userRowsPostSave.email ).toBeNull(); + } else { + expect( userRowsPostSave.email ).toBe( "" ); + } } ); it( "uses the sqltype attribute if present for each column", function() {