Skip to content

Commit b0a5002

Browse files
committed
Add H5 probes for persister int overload, transient entity, date-string coercion
1 parent de7afab commit b0a5002

8 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
component extends="org.lucee.cfml.test.LuceeTestCase" labels="orm" {
2+
3+
function run( testResults, testBox ) {
4+
5+
describe( "string-to-date parameter coercion in finders + HQL", function() {
6+
7+
it( "entityLoad filter struct accepts a date string", function() {
8+
var result = _InternalRequest( template: "#uri()#/entityLoadStringDate.cfm" );
9+
expect( trim( result.filecontent ) ).toBe( "ok" );
10+
});
11+
12+
it( "ormExecuteQuery accepts a String for a Date param", function() {
13+
var result = _InternalRequest( template: "#uri()#/hqlStringDate.cfm" );
14+
expect( trim( result.filecontent ) ).toBe( "ok" );
15+
});
16+
17+
});
18+
19+
}
20+
21+
private string function uri() {
22+
return getDirectoryFromPath( contractPath( getCurrentTemplatePath() ) ) & "dateStringCoercion";
23+
}
24+
25+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
component {
2+
this.name = "test-dateStringCoercion-#hash( getCurrentTemplatePath() )#";
3+
this.datasource = server.getDatasource( "h2", server._getTempDir( "orm-dateStringCoercion" ) );
4+
this.ormEnabled = true;
5+
this.ormSettings = {
6+
dbcreate: "dropcreate",
7+
cfclocation: [ getDirectoryFromPath( getCurrentTemplatePath() ) ]
8+
};
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
component persistent="true" table="user_login" accessors="true" {
2+
3+
property name="id" fieldtype="id" ormtype="string";
4+
property name="userName" ormtype="string" length="50";
5+
property name="lastLogin" ormtype="date";
6+
7+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<cfscript>
2+
// Probe: entityLoad("User", { lastLogin: "01/01/2009" }) — filter-struct path.
3+
// On H5 the extension/Hibernate JdbcDateJavaType.wrap silently coerced the
4+
// String to java.sql.Date via Lucee's autocast. On H7 JdbcDateJavaType.wrap
5+
// rejects String outright with "Could not convert 'java.lang.String' to
6+
// 'java.sql.Date' ... argument [...] is not assignable to java.util.Date".
7+
//
8+
// Test corpus pre-this binds dates as createDate()/now() Java types only —
9+
// real-world apps (cborm dynamic finders, ColdBox controllers) routinely
10+
// pass user-typed CFML date strings.
11+
seed = entityNew( "User" );
12+
seed.setId( createUUID() );
13+
seed.setUserName( "Alice" );
14+
seed.setLastLogin( createDate( 2009, 1, 1 ) );
15+
entitySave( seed );
16+
ormFlush();
17+
ormClearSession();
18+
19+
// Filter-struct form — Hibernate builds a Criteria-equivalent and binds
20+
// lastLogin as a parameter. The String "01/01/2009" must coerce to a date.
21+
result = entityLoad( "User", { lastLogin: "01/01/2009" } );
22+
if ( !isArray( result ) )
23+
throw( message="entityLoad: expected array, got [#serializeJSON( result )#]" );
24+
if ( arrayLen( result ) != 1 )
25+
throw( message="entityLoad: expected 1 match for date string '01/01/2009', got [#arrayLen( result )#]" );
26+
if ( result[ 1 ].getUserName() != "Alice" )
27+
throw( message="entityLoad: expected user [Alice], got [#result[ 1 ].getUserName()#]" );
28+
29+
echo( "ok" );
30+
</cfscript>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<cfscript>
2+
// Probe: HQL named-parameter binding accepts a String for a Date column.
3+
// On H5 the parameter coercion path went through Hibernate's JavaTypeRegistry
4+
// which let strings pass via Lucee's autocoercion. On H7 strict typing in
5+
// JdbcDateJavaType.wrap throws on String input — fix path likely in the
6+
// extension's HQL parameter binding (where Lucee hands the param struct off
7+
// to Hibernate) wherever String→Date coercion previously happened implicitly.
8+
seed = entityNew( "User" );
9+
seed.setId( createUUID() );
10+
seed.setUserName( "Bob" );
11+
seed.setLastLogin( createDate( 2012, 1, 1 ) );
12+
entitySave( seed );
13+
ormFlush();
14+
ormClearSession();
15+
16+
result = ormExecuteQuery(
17+
"FROM User WHERE lastLogin = :p",
18+
{ p: "01/01/2012" }
19+
);
20+
if ( !isArray( result ) )
21+
throw( message="ormExecuteQuery: expected array, got [#serializeJSON( result )#]" );
22+
if ( arrayLen( result ) != 1 )
23+
throw( message="ormExecuteQuery: expected 1 match for HQL date string '01/01/2012', got [#arrayLen( result )#]" );
24+
if ( result[ 1 ].getUserName() != "Bob" )
25+
throw( message="ormExecuteQuery: expected user [Bob], got [#result[ 1 ].getUserName()#]" );
26+
27+
echo( "ok" );
28+
</cfscript>

tests/session/apiSmoke.cfc

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,16 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="orm" {
7979
expect( trim( result.filecontent ) ).toBe( "ok" );
8080
});
8181

82+
it( "EntityPersister.getSubclassPropertyName(int) resolves index to property name", function() {
83+
var result = _InternalRequest( template: "#uri()#/persisterPropertyByIndex.cfm" );
84+
expect( trim( result.filecontent ) ).toBe( "ok" );
85+
});
86+
87+
it( "session.getEntityName( transient ) throws TransientObjectException", function() {
88+
var result = _InternalRequest( template: "#uri()#/transientEntityName.cfm" );
89+
expect( trim( result.filecontent ) ).toBe( "ok" );
90+
});
91+
8292
});
8393

8494
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<cfscript>
2+
// Locks down the H5.6 EntityPersister int-index property name lookup that
3+
// real-world CFML code (cborm BaseORMService.getDirtyPropertyNames,
4+
// basecfc, ColdMVC) depends on. The cborm flow is:
5+
//
6+
// var modified = persister.findModified( dbState, currentState, entity, session ); // int[]
7+
// return arrayMap( modified, function( i ){ return persister.getSubclassPropertyName( i ); } );
8+
//
9+
// On 5.6 getSubclassPropertyName(int) is a real method on
10+
// SingleTableEntityPersister. On 7.0+ that int overload is gone — only the
11+
// String overload survives. Same test must pass on both branches once the
12+
// extension-side persister shim re-exposes the int form.
13+
sf = ORMGetSessionFactory();
14+
md = sf.getClassMetadata( "SmokeEntity" );
15+
if ( isNull( md ) )
16+
throw( message="getClassMetadata: returned null for SmokeEntity" );
17+
18+
// getPropertyNames returns the entity's own (non-id) properties.
19+
propNames = md.getPropertyNames();
20+
if ( !isArray( propNames ) || arrayLen( propNames ) == 0 )
21+
throw( message="getPropertyNames: expected non-empty array, got [#serializeJSON( propNames )#]" );
22+
23+
// Walk every index Hibernate exposes via getSubclassPropertyName(int).
24+
// For a flat entity (no inheritance) the subclass property table mirrors
25+
// getPropertyNames(). The probe asserts both: (a) the int overload exists
26+
// and dispatches; (b) every index resolves to a non-empty String.
27+
for ( i = 0; i < arrayLen( propNames ); i++ ) {
28+
name = md.getSubclassPropertyName( javaCast( "int", i ) );
29+
if ( isNull( name ) || !isSimpleValue( name ) || len( name ) == 0 )
30+
throw( message="getSubclassPropertyName(#i#): expected non-empty String, got [#serializeJSON( name )#]" );
31+
// CFML 1-indexed vs Java 0-indexed — propNames[ i+1 ] is the same slot.
32+
if ( name != propNames[ i + 1 ] )
33+
throw( message="getSubclassPropertyName(#i#): expected [#propNames[ i + 1 ]#], got [#name#]" );
34+
}
35+
36+
echo( "ok" );
37+
</cfscript>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<cfscript>
2+
// Locks down the H5.6 contract that session.getEntityName( transient ) throws
3+
// org.hibernate.TransientObjectException — NOT IllegalArgumentException.
4+
// ColdBox's ObjectPopulator.getTargetName depends on this exact exception
5+
// type to short-circuit transient detection:
6+
//
7+
// try {
8+
// return ormGetSession().getEntityName( arguments.target );
9+
// } catch ( org.hibernate.TransientObjectException e ) {
10+
// // fall through to getMetadata( target ).name
11+
// }
12+
//
13+
// (cborm test-harness/coldbox/system/core/dynamic/ObjectPopulator.cfc#L655)
14+
//
15+
// On H5 the throw site is EntityIdentifierMapping.java#L116 — message format
16+
// "object references an unsaved transient instance of '<entity>'". TYPE:
17+
// org.hibernate.TransientObjectException.
18+
//
19+
// On H7 the same call throws java.lang.IllegalArgumentException ("Given
20+
// entity is not associated with the persistence context") from
21+
// SessionImpl.getEntityEntry → SessionImpl.getEntityName(Object) at
22+
// SessionImpl.java#L1798. The typed catch above no longer matches, the
23+
// exception propagates, and ColdBox surfaces it as "Error populating bean".
24+
//
25+
// Same test must pass on both branches once the extension-side compat session
26+
// wrapper catches IAE from getEntityName and rethrows as
27+
// TransientObjectException to preserve the H5 cborm/ColdBox/basecfc contract.
28+
e = entityNew( "SmokeEntity" );
29+
e.setId( createUUID() );
30+
e.setName( "Transient" );
31+
32+
ormSess = ormGetSession();
33+
threwTransient = false;
34+
otherErr = "";
35+
try {
36+
ormSess.getEntityName( e );
37+
} catch ( org.hibernate.TransientObjectException toe ) {
38+
threwTransient = true;
39+
} catch ( any other ) {
40+
otherErr = "type=[#other.type#] message=[#other.message#]";
41+
}
42+
43+
if ( !threwTransient )
44+
throw( message="getEntityName(transient): expected org.hibernate.TransientObjectException, got [#otherErr#]" );
45+
46+
echo( "ok" );
47+
</cfscript>

0 commit comments

Comments
 (0)