Merge branch 'master' into patch-1
diff --git a/README.md b/README.md
index 0f215a4..8f51996 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@
 Apache Cayenne
 ==============
 
-[![Maven Central](https://img.shields.io/maven-central/v/org.apache.cayenne/cayenne-server/4.1.RC2.svg)](https://cayenne.apache.org/download/)
+[![Maven Central](https://img.shields.io/maven-central/v/org.apache.cayenne/cayenne-server/4.1.svg)](https://cayenne.apache.org/download/)
 [![Build Status](https://travis-ci.org/apache/cayenne.svg)](https://travis-ci.org/apache/cayenne)
 
 <!-- Broken maven badge: [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.apache.cayenne/cayenne-server/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.apache.cayenne/cayenne-server/) -->
@@ -75,7 +75,7 @@
 <plugin>
     <groupId>org.apache.cayenne.plugins</groupId>
     <artifactId>cayenne-maven-plugin</artifactId>
-    <version>4.1.RC2</version>
+    <version>4.1</version>
 
     <dependencies>
         <dependency>
@@ -118,7 +118,7 @@
         mavenCentral()
     }
     dependencies {
-        classpath group: 'org.apache.cayenne.plugins', name: 'cayenne-gradle-plugin', version: '4.1.RC2'
+        classpath group: 'org.apache.cayenne.plugins', name: 'cayenne-gradle-plugin', version: '4.1'
         classpath 'mysql:mysql-connector-java:8.0.13'
     }
 }
@@ -159,7 +159,7 @@
     <dependency>
         <groupId>org.apache.cayenne</groupId>
         <artifactId>cayenne-server</artifactId>
-        <version>4.1.RC2</version>
+        <version>4.1</version>
     </dependency>
 </dependencies>
 ```
@@ -167,7 +167,7 @@
 ##### Gradle
 
 ```gradle
-compile group: 'org.apache.cayenne', name: 'cayenne-server', version: '4.1.RC2'
+compile group: 'org.apache.cayenne', name: 'cayenne-server', version: '4.1'
  
 // or, if Gradle plugin is used
 compile cayenne.dependency('server')
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 8e19a74..0872506 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -8,8 +8,31 @@
 https://issues.apache.org/jira/browse/CAY
 
 ----------------------------------
+Release: 4.2.M2
+Date:
+----------------------------------
+Changes/New Features:
+
+CAY-2338 Support comments in cgen and default templates
+CAY-2656 Modeler: option to download required jars directly from maven central
+CAY-2659 Use new SQLBuilder utility to generate SQL for batch queries
+CAY-2662 Use custom interface for SQL tree processor instead of a Function<Node, Node>
+CAY-2663 Support for custom SQL operators
+CAY-2664 Add methods to EntityProperty to allow direct usage of primary key values
+CAY-2665 Support for widespread SQL types that falls into Jdbc type OTHER
+CAY-2668 Experimental graph-based db operations sorter
+CAY-2670 CommitLog does not include FKs for deleted objects with one-way relationships
+CAY-2673 Support ordering by aggregate functions
+CAY-2674 Support in-memory evaluation of aggregate functions
+
+Bug Fixes:
+
+CAY-2591 Modeler: project becomes dirty after click on dbImport or cgen tab
+CAY-2671 QualifierTranslator fails to translate expressions with compound PKs/FKs
+
+----------------------------------
 Release: 4.2.M1
-Date: March 24, 2020
+Date: April 7, 2020
 ----------------------------------
 Changes/New Features:
 
@@ -114,570 +137,5 @@
 CAY-2648 Whitespace symbols in JDBC Driver and DB URL lines lead to incorrect driver loading
 CAY-2653 No methods for queries with qualifier parameters generated
 CAY-2654 Exception in dbimport when relationships should be imported, but no explicit configuration exists
+CAY-2655 AutoAdapter missing supportsGeneratedKeysForBatchInserts() method
 
-----------------------------------
-Release: 4.1.B1
-Date: March 7, 2019
-----------------------------------
-Changes/New Features:
-
-CAY-2446 Run Disjoint By Id queries outside of synchronized block
-CAY-2447 Crypto support for LocalDateTime
-CAY-2471 Support multiple XML project versions
-CAY-2473 Modeler: cleanup attributes and relationship editors
-CAY-2474 Modeler: swap buttons in dialog toolbar
-CAY-2475 Modeler: move inheritance icon to name column in objAttr table and objRel table
-CAY-2478 cgen: Generate properties for PK
-CAY-2481 Support for Object[] as return type in SQLTemplate and SQLExec
-CAY-2485 Compact Slf4j Logger
-CAY-2487 Removed usage of CayenneException.
-CAY-2489 Add validation to the case of not to PK relationships
-CAY-2491 Remaster Db Import View
-CAY-2493 Save cgen configuration with datamap XML
-CAY-2494 Rename dbImport tag from 'config' to 'dbImport'
-CAY-2499 Support for COUNT(DISTINCT(column)) function aggregate
-CAY-2514 Set SERVER_CONTEXTS_SYNC_PROPERTY default value to false
-
-Bug Fixes:
-
-CAY-2320 Modeler: Limit input into numeric fields to 10 digits
-CAY-2444 Change URI from http:// to https:// in xsi:schemaLocation
-CAY-2445 Oracle: Problem with ExpressionFactory.notInExp()
-CAY-2449 Modeler: Needless scrollbar in Generate DB Schema result menu
-CAY-2450 Modeler: Impossible to update Attribute title after syncing ObjEntity
-CAY-2451 Modeler: ObjEntity "Edit" button doesn't open editor for Relationship
-CAY-2454 Modeler: Unable to read validation message if it's truncated
-CAY-2455 Modeler: The width of the pop-up window is very large
-CAY-2459 Modeler: DataMap paste function is not working
-CAY-2462 Modeler: Clipboard holds old content after application was reloaded
-CAY-2463 Modeler: DB Schema generation doesn't work
-CAY-2464 ClassCastException when returning PRUNED_NODE in expression transformer
-CAY-2470 Can't bind SQLExec parameters in a loop
-CAY-2472 Clear cached replacement query on mutation in all indirect queries
-CAY-2476 Modeller: Fixed wrong behaviour of code generation dialog
-CAY-2480 cayenne:cdbgen and cayenne:cgen have identical text in cayenne-maven-plugin
-CAY-2484 maven plugins missing in 4.1.M2 release
-CAY-2490 Added dbEntities from other datamaps in dbRelationship dialog.
-CAY-2496 Fixed wrong table behavior: focus color, cleanup DBAttribute Path cell on select.
-CAY-2497 Modeler: SQL Scripts tab isn't scrollable
-CAY-2501 Modeler: DbImport ui not loading columns for MySQL connector v8.0
-CAY-2502 DataMap in DataNode tree view disappears after dbImport
-CAY-2504 Broken detection logic of NoopEventBridge in DataRowStoreFactory
-CAY-2505 EventBridge providers should be bound without scope
-
-----------------------------------
-Release: 4.1.M2
-Date: July 11, 2018
-----------------------------------
-Changes/New Features:
-
-CAY-1683 Injectable PkGenerator
-CAY-2304 Custom ClassLoader for Maven and Gradle plugins that use project dependencies
-CAY-2334 cgen: option to force run from maven/gradle
-CAY-2337 Save reverse engineering configuration with datamap XML
-CAY-2372 Extract new modules from cayenne-server
-CAY-2377 Cleanup deprecated code.
-CAY-2391 cdbimport: add option to skip user-defined relationships
-CAY-2393 Add sqlserver-docker profile to automate tests on SQLServer
-CAY-2394 Upgrade to Apache Velocity 2.0
-CAY-2395 cdbimport: add option to create project file
-CAY-2396 Upgrade maven-assembly-plugin to 3.1.0
-CAY-2398 Deprecate cayenne-joda
-CAY-2400 Deprecate cayenne-dbcp2
-CAY-2403 Extract eventbridges to top level
-CAY-2404 Move itests to maven-plugins
-CAY-2406 Add prefetch-related API to SQLSelect
-CAY-2407 Modeler: add prefetch support for the SQLTemplate query
-CAY-2410 Add prefetch type support for SQLTemplate query and SelectQuery
-CAY-2414 Modeler: new icon design
-CAY-2415 Transaction isolation and propagation support
-CAY-2416 Change TreeMap for HashMap to store data in Cayenne model classes
-CAY-2422 Modeler: Open driver setup window on driver load error
-CAY-2438 Split DataChannel filter into two independent filters
-CAY-2440 cdbimport: allow cross-schema relationships
-CAY-2443 Make SqlTemplate and SqlExec possible to return generated keys
-
-Bug Fixes:
-
-CAY-2282 Various Update Issues With Vertical Inheritance
-CAY-2370 ValueObjectType for byte[] fails lookup
-CAY-2380 ReferenceMap should not store or return null values
-CAY-2381 cgen: meaningful PK with boxed type ends up with primitive type in generated source
-CAY-2382 Lack of synchronization in DataContext serialization
-CAY-2387 Can't select byte[] property with ColumnSelect
-CAY-2388 Modeler: Visualization issues with undo/redo actions for attributes and relationships
-CAY-2389 DbEntity qualifier with DbPath expression translates into wrong SQL
-CAY-2392 Modeler: Unable to remove DataNode
-CAY-2397 Modeler: Unable to set enum:value as Entity qualifier
-CAY-2401 Modeler: NPE in ObjEntity sync action
-CAY-2405 Broken prefetch of entity with inheritance and attribute with custom java type
-CAY-2408 Cayenne JDK 10 compatibility
-CAY-2411 Wrong resolution of ExtendedType with ValueObjectType for inherited class
-CAY-2418 Modeler: unable to edit entity selected via Search
-CAY-2419 Modeler: Not changing highlight on selecting search results within one entity
-CAY-2420 Modeler: search is not performed for Stored Procedures
-CAY-2425 Modeler: Migrate DB Direction field is locked if no option was selected in dropdown list
-CAY-2427 Modeler: Undo throws exception
-CAY-2429 Generate classes: Invalid template type: EMBEDDABLE_SINGLE_CLASS
-CAY-2430 Modeler: Redo throws NPE
-CAY-2435 cdbimport: procedure parameters are not imported
-CAY-2436 NPE in CayenneRuntimeException constructor
-CAY-2439 Modeler: Error deleting dbEntity when show only dbEntities filter is set
-CAY-2442 Broken EventBridge providers implementations
-
-----------------------------------
-Release: 4.1.M1
-Date: October 14, 2017
-----------------------------------
-Changes/New Features:
-
-CAY-2152 Redesign project file upgrade system
-CAY-2329 Update project dependencies
-CAY-2330 Field based data objects
-CAY-2335 New XML loading/saving mechanics with support of plugable handlers
-CAY-2336 Support for comments in Modeler
-CAY-2339 Compatibility module to support old versions of projects at runtime
-CAY-2344 Modeler: Save ER-Graph and class diagram
-CAY-2345 Own template renderer as a replacement for Velocity
-CAY-2346 Field-based data object with Map-based storage fallback
-CAY-2351 Remove commons-collections usage completely
-
-Bug Fixes:
-
-CAY-2312 Modeler: Undo does not work for checkboxes
-CAY-2318 Modeler: Query. Exception after Undo clicking
-CAY-2319 Modeler: Embeddable > Attributes. Undo does not cancel pasted objects
-CAY-2321 cdbimport: Reverse relationship is not created after adding and rev engineeering new db table
-CAY-2323 Modeler: Graph. No warning while saving the image with existing name
-CAY-2331 cgen: broken templates for data map
-CAY-2347 cdbimport: can't get all relationships on the first pass
-CAY-2349 Cache issue: 'SelectQuery' with prefetches loses relationships
-CAY-2350 Expression: NotIn with empty collection returns empty result
-CAY-2353 Broken paginated column select with only one entity in the result
-CAY-2354 DbGenerator.runGenerator must commit its connection
-CAY-2356 EJBQL: Incorrect COUNT() on outer joined table
-CAY-2357 Generic select queries silently convert result to nulls if no PK column found
-CAY-2358 NPE when callbacks invoked on null objects
-CAY-2359 EJBQL: db path in not supported in ORDER BY
-CAY-2361 PostgreSQL DbGenerator issues
-CAY-2362 ColumnSelect: unable to use Property without type
-CAY-2363 ColumnSelect: unable to use from nested context
-CAY-2364 Wrong logging in SQLTemplate
-CAY-2365 SQLExec query tries to convert (unexpected) result set into objects
-CAY-2366 Incorrect EJBQL COUNT translation
-CAY-2367 ClassCastException reading object with an attribute of type 'char'
-CAY-2368 ColumnSelect: Property.self() translates into wrong SQL code
-
-----------------------------------
-Release: 4.0.B1
-Date: June 12, 2017
-----------------------------------
-Changes/New Features:
-
-CAY-1873 Move DataDomain cache configuration from the Modeler and into DI
-CAY-1891 Modeler: Add To-Many Warning
-CAY-1892 Modeler: Add Relationship Data Type Warning
-CAY-2057 Modeler: Clean up OS X version
-CAY-2109 cayenne-crypto: add value authentication (HMAC)
-CAY-2210 Query cache: incorrect cache key for queries with custom value objects
-CAY-2255 ObjectSelect improvement: columns as full entities
-CAY-2258 DI: type-safe binding of List and Map
-CAY-2259 QueryCache: support for referencing type-safe caches
-CAY-2261 Replace NamedQuery with MappedXYZ in *datamap.vm
-CAY-2262 Module auto-loading
-CAY-2266 Move EventBridge implementations into autoloadable modules
-CAY-2267 Contribute lifecycle events listeners via DI
-CAY-2268 DI: Refactor ListBuilder API ambiguities for before() / after() bindings
-CAY-2269 Add support for date/time components extraction in expression functions
-CAY-2270 Update function support in expression parser
-CAY-2271 ColumnSelect: support for prefetch and limit
-CAY-2272 ColumnSelect: methods to manually control DISTINCT clause
-CAY-2274 Modeler: Validate case when dependent PK is marked as “generated”
-CAY-2277 Create ClientRuntime with ClientRuntimeBuilder just like ServerRuntime
-CAY-2278 Extract cayenne-postcommit module from cayenne-lifecycle
-CAY-2280 Switch from commons-logging to slf4j
-CAY-2295 "Sync ObjEntity with DbEntity" and "View related DbEntity" buttons aren't disabled, if DbEntity doesn't have ObjEntity
-CAY-2296 cayenne-crypto: Get java type for DbAttribute bound to ObjAttributes with the same type
-CAY-2300 Modeler: New icons and design improvements
-CAY-2302 Rename postcommit module and its content to commitlog
-
-Bug Fixes:
-
-CAY-2021 cdbimport: detect when same FK constraint is defined twice
-CAY-2077 Bug in CayenneRuntimeException using wrong specified string in Formatter
-CAY-2094 SelectById query doesn't work from ROP client
-CAY-2161 'Not for Client Use' option is ignored at Class Generation
-CAY-2171 Modeler: Undo db Entity Sync throws error
-CAY-2208 SQLTemplate: LEFT JOIN to a subset of a table returns nulls for entries that don't have a match in the subset
-CAY-2230 Error using connection to postgresql with db schema in DB URL
-CAY-2240 Modeler: issue with cursor rendering for EJBQL query
-CAY-2243 ObjectContext.getGraphManager().unregisterObject() inconsistencies
-CAY-2250 Remove: Incorrect text in Confirm Remove message when cursor is set on attribute row
-CAY-2256 Cannot Save/Insert an Object With null Flattened (complex) toOne Relationship (see also CAY-2146)
-CAY-2265 ServerRuntime.builder() fails to set default runtime name when a the project file doesn't follow recognized pattern
-CAY-2273 Modeler: default suggested cgen location is rooted in subpackage
-CAY-2275 Documentation: tutorial is out of sync with 4.0.M5 version
-CAY-2276 PrePersist listener registered as PostPersist in LifecycleCallbackRegistry.addListener(Class<?>, LifecycleListener)
-CAY-2279 cdbimport: skip PK comparison for VIEWs
-CAY-2281 ObjEntity attribute overrides are never deleted
-CAY-2284 Expression likeIgnoreCase can't handle unicode chars in in-memory evaluation
-CAY-2286 Filter out inherited ObjEntities from sync with DbEntity
-CAY-2326 DI: can't override List/Map elements from another Module
-
-----------------------------------
-Release: 4.0.M5
-Date: March 6, 2017
-----------------------------------
-Changes/New Features:
-
-CAY-2139 Upgrade HSQLDB dependency to the most recent version (2.3.4)
-CAY-2150 Refactoring: ParameterBinding to contain ExtendedType property
-CAY-2163 Property.path() , ExpressionFactory.pathExp()
-CAY-2164 Relocate builder bootstrap methods from ServerRuntimeBuilder to ServerRuntime
-CAY-2165 Explicit "contribution" API for easier expansion of DI collections and maps
-CAY-2166 Auto-loading of Cayenne modules
-CAY-2168 Split DbLoader to parts and clean it up
-CAY-2169 Split DbMerger to parts and clean it up
-CAY-2170 MergeToken sorting is highly unstable
-CAY-2172 Cleanup Modeler import and migrate db actions
-CAY-2176 Java 7 diamond class generation templates
-CAY-2177 Sync auto generated state of PK between model and DB
-CAY-2187 Support for the scalar and aggregate SQL functions in ObjectSelect API
-CAY-2197 Update sqlite version and enable in-memory default config
-CAY-2212 cdbimport cleanup and configuration schema refactoring
-CAY-2223 JCacheQueryCache - a query cache provider to plug in JCache implementers
-CAY-2225 Extensible CacheInvalidationFilter logic
-CAY-2228 Deprecate multiple cache groups in caching and query API
-CAY-2231 Support for collections in new functional expressions and old math expressions
-CAY-2232 Proper conversion to String for new functional expressions
-CAY-2235 Deprecate Query.getDataMap() method
-
-Bug Fixes:
-
-CAY-2032 SelectAction: DistinctResultIterator ignores flattened relationships
-CAY-2137 When generating SQL from EJBQL, use "AND" to separate multiple join conditions
-CAY-2174 Change FK attribute name cause ObjAttribute appear after Reverse Engineering
-CAY-2175 AliasName used in EJBQLQuery is not working if it contains mixed case
-CAY-2183 Newly created DbRelationship is unexpectedly renamed by the Modeler
-CAY-2199 Modeler on Windows: The same project is displayed twice in "Recent Projects"
-CAY-2207 Modeler: "Java Type" and "DbAttribute Path" are not saved with using TAB to move forward
-CAY-2221 In-memory expression evaluation gives different result than select query
-CAY-2236 Modeler Migrate DB Schema: unable to Reverse All Operations
-CAY-2238 Modeler: Preserve manually set DbRelationship name when syncing with ObjEntity
-CAY-2242 Vertical Inheritance: Cannot Insert Record For Implementing Class with Attribute And Relationship
-
-----------------------------------
-Release: 4.0.M4
-Date: December 13, 2016
-----------------------------------
-Changes/New Features:
-
-CAY-2051 Applying new Reverse Engineering to the Modeler
-CAY-2053 SQLExec fluent query API
-CAY-2060 Replace Query objects in DataMap with query descriptors
-CAY-2062 MappedSelect and MappedExec fluent query API
-CAY-2063 ProcedureCall fluent query API
-CAY-2065 Pluggable serialization and connectivity layers for ROP
-CAY-2073 Ordering.orderedList methods
-CAY-2074 Support for catalogs in stored procedures
-CAY-2076 Implement Jetty HTTP/1.1 and HTTP/2 Client support for ROP Client
-CAY-2083 Implement Protostuff as serialization service for Cayenne ROP
-CAY-2090 Untangle HttpRemoteService from ServiceContext thread local setup
-CAY-2100 Add supporting generated keys for PostgreSQL
-CAY-2102 EJBQL: db: path not supported in select columns
-CAY-2103 cayenne-crypto: support for mapping non-String and non-binary types
-CAY-2106 cayenne-crypto: allow DI contribution of type converters inside ValueTransformerFactory
-CAY-2107 cayenne-crypto: Lazy initialization of crypto subsystem
-CAY-2111 Unbind transaction object from the current thread for iterated queries
-CAY-2112 Expose callback for "performInTransaction"
-CAY-2113 cdbimport: Reverse-engineering reinstates previously ignored columns
-CAY-2114 cdbimport: object layer settings are not respected
-CAY-2115 DbLoader - allow loading DataMap without Obj layer
-CAY-2116 Split schema synchronization code in a separate module
-CAY-2118 cdbimport: drop support for the old style of table filtering
-CAY-2129 Modeler: reengineer dialog improvements
-CAY-2130 Stripping common name prefixes on reverse engineering
-CAY-2132 Adding SybaseSelectTranslator to support TOP/DISTINCT TOP in limited queries
-CAY-2133 ObjectNameGenerator refactoring - unifying relationship name generation
-CAY-2135 cdbimport: reset DbEntity catalogs / schemas to DataMap defaults
-CAY-2136 Allow Ordering.orderedList(…) methods to accept a Collection rather than only a List
-CAY-2160 Modeler: new welcome screen
-CAY-2222 MySQLAdapter should not create indexes on FK columns
-
-Bug Fixes:
-
-CAY-2016 cdbimport: Rename table with toMany relationship causes migration error
-CAY-2064 Issue with BeanAccessor for classes with complex inheritance
-CAY-2066 Fixes for inner enums handling in ExtendedTypeMap
-CAY-2067 Cayenne 4.0 connection pool is occasionally running out of connections
-CAY-2070 Modeler sync function adds extraneous ObjRelationships inside the class hierarchy
-CAY-2078 Client code gen bug. Unnecessary DataMap class generation setting datamap gen to false.
-CAY-2080 Cayenne doesn't pick up reverse engineering file changes
-CAY-2084 ObjectIdQuery - no cache access polymorphism
-CAY-2086 SelectById.selectFirst stack overflow
-CAY-2087 PostCommitFilter is confused about changes made by Pre* listeners
-CAY-2089 HTTP connections aren't always closed in new ROP implementation
-CAY-2097 NullPointerException while updating relationships for entities with vertical inheritance
-CAY-2101 DataContext.currentSnapshot() doesn't set snapshot entity name
-CAY-2105 Add missing elements to the reverseEngineering.xsd
-CAY-2108 cayenne-di: StackOverflow for decorator that takes Provider of the delegate
-CAY-2110 Obfuscated exception when processing iterated results
-CAY-2119 ProjectUpgrader test failure (Windows)
-CAY-2122 Vertical Inheritance: Cannot Insert Record For Implementing Class with Attribute And Relationship
-CAY-2125 SchemaUpdateStrategy doesn't work with multiple DataNodes
-CAY-2126 Modeler cannot upgrade project from v7 to v9
-CAY-2128 Modeler stored procedures are not imported
-CAY-2131 Modeler NullPointerException in reverse engineering when importing different catalogs in one datamap
-CAY-2138 NVARCHAR, LONGNVARCHAR and NCLOB types are missing from Firebird types.xml
-CAY-2141 Disjoint-by-id prefetch generates repeating ID conditions
-CAY-2143 NPE in BaseSchemaUpdateStrategy
-CAY-2144 cdbimport always fails for databases which don't support catalogs
-CAY-2146 Vertical inheritance: record still inserted into parent db table when child validation fails
-CAY-2148 Failure upgrading from 3.1 to M4
-CAY-2150 UI bug: PK generation custom sequence is getting reset
-CAY-2151 Migrate Database Schema: issue when no db is specified
-CAY-2153 Modeler Exception in save action after reverse engineering some complex DB schema
-CAY-2154 Migrate db: queries order
-CAY-2226 PK generation for Frontbase: PK cache size must be ignored
-
-----------------------------------
-Release: 4.0.M3
-Date: February 12, 2016
-----------------------------------
-Changes/New Features:
-
-CAY-1626 Add JodaTime DateTime support
-CAY-1902 Implement resolving Db paths for DataObjects
-CAY-1991 More control over generated String property names
-CAY-1992 Allow to exclude DataMap java class from Modeler class generation
-CAY-1995 Add support for iterators to Select
-CAY-2001 Saving a display state of Project
-CAY-2004 EJBQL: Support for ordering on aggregate expressions
-CAY-2007 Refactoring SelectTranslator for better extensibility
-CAY-2008 Connection pool refactoring and validation query support in Cayenne DataSource
-CAY-2009 Non-blocking connection pool
-CAY-2010 DataSourceBuilder to help users create pooling and non-pooling DataSources
-CAY-2011 Support for Java 8 date and time types
-CAY-2012 ObjectSelect, SelectById: eliminating methods that reset query state
-CAY-2013 In-memory evaluation of DB expressions - non-id attributes
-CAY-2023 Decouple the use of ResourceLocator
-CAY-2025 Support for DBCP2
-CAY-2026 Java 7
-CAY-2027 Support for Expression outer join syntax in EJBQL
-CAY-2028 Wrap DataChannelFilter calls in the main transaction
-CAY-2029 Allow out-of-order insertion into DI lists
-CAY-2030 Capturing a stream of commit changes
-CAY-2035 Autobind items added to collections (Cayenne DI)
-CAY-2042 Remove an arbitrary limitation on 1000 runtime DbRelationships
-CAY-2043 ServerRuntimeBuilder: use DataDomain name for the default DataNode
-CAY-2044 Collection setter for to-many relationships
-CAY-2045 Add autosuggestion fields to choose attributes and relationships
-
-Bug Fixes:
-
-CAY-1977 Cleanup Modeler reverse engineering functionality
-CAY-1987 Widen types before performing in-memory evaluation of qualifiers using j.l.Number subclasses
-CAY-1990 Incorrect display of the raw SQL query in Modeler
-CAY-1993 Reverse Engineering does not work with PostgreSQL database
-CAY-1994 Modeler Migration Tool Shows No Changes
-CAY-1997 Difference in NULL handling inside the path between PropertyUtils and DataObject.readNestedProperty
-CAY-1998 Speeding up PropertyUtils
-CAY-1999 Unneeded Property import for superclasses with no properties
-CAY-2003 cdbimport doesn't work properly with several includeTable tags
-CAY-2015 Joint prefetches combined with DisjointById prefetches return null incorrectly
-CAY-2020 typo: correction to upper alpha range in Rot13PasswordEncoder
-CAY-2041 "cayenne.jdbc.max_connections" and "cayenne.jdbc.min_connections" command line options are ignored
-CAY-2047 Relationship mapping with target inheritance
-CAY-2049 Changing the Relationship name in ObjRelationship Inspector has no effect
-
-----------------------------------
-Release: 4.0.M2
-Date: March 18, 2015
-----------------------------------
-Changes/New Features:
-
-CAY-1267 Some changes to LogDialog
-CAY-1826 Merge Entity Attributes and Relationships tabs together with one toolbar.
-CAY-1839 Allow to link DataMaps to DataNodes from DataNode editor.
-CAY-1841 Filters for Left-hand project navigator
-CAY-1842 Remove Listeners support from the Modeler
-CAY-1843 DataMap v7: Stop saving listeners in DataMap, add upgrade handler
-CAY-1845 Upgrade javadoc plugin to 2.9.1
-CAY-1846 Reworking of callback mapping
-CAY-1847 Make ConverterFactory extensible
-CAY-1848 New method: ObjectContext.selectOne(Select query)
-CAY-1851 Generate default serialVersionUID for generated java classes to avoid eclipse warnings
-CAY-1852 Straighten thread model and synchronization in the Modeler
-CAY-1855 Iterated and paginated queries must print result counts
-CAY-1856 Expression.expWithParameters does not work when parameters are placed in the inline collection
-CAY-1860 In-memory matching of DataObjects against ObjectId or int
-CAY-1861 Remove runtime relationships
-CAY-1870 cgen - smarter default for 'superPkg' and 'destDir'
-CAY-1882 Porting to OSGi environment
-CAY-1883 Clean up Cayenne maven structure
-CAY-1886 cayenne-di module reorg, new exceptions
-CAY-1890 Remove Cayenne-level buffering when retrieving LOBs
-CAY-1894 Support native PK generation using sequences for H2 databases
-CAY-1899 ServerRuntimeBuilder
-CAY-1900 Allow DataNode name to be used as a root of SQLTemplate
-CAY-1901 Config-free ServerRuntime
-CAY-1904 Simple injection-friendly constructor for AuditableFilter
-CAY-1907 RowReaderFactory
-CAY-1908 Refactor all SQLActions to work with DataNode
-CAY-1911 BatchQuery refactoring - make Iterable
-CAY-1912 BatchQueryBuilder refactoring
-CAY-1913 Refactor org.apache.cayenne.access.trans into query-specific packages
-CAY-1914 Refactor EJBQL-related translators to a standalone 'org.apache.cayenne.access.translator.ejbql' package
-CAY-1915 BatchTranslator instead of performing bindings should return binding object whose values can be altered
-CAY-1916 cayenne-crypto module that enables data encryption for certain model attributes
-CAY-1918 Replace Oracle LOB hacks with JDBC 4.0 API
-CAY-1919 Split DataNode creation into a separate DataNodeFactory
-CAY-1920 DI: add support for decorators
-CAY-1921 Support for schema selection in 'Migrate Database Schema'
-CAY-1923 Optimize BatchTranslator - use fixed size array of BatchParameterBinding
-CAY-1925 cayenne-crypto: add optional compression to the encryption pipeline
-CAY-1928 Second INNER join generated for OUTER flattended relationships in disjoint prefetches
-CAY-1929 Property.outer method to build OUTER join properties
-CAY-1932 Improved Handling for Scalar Parameters Converting Expressions to EJBQL
-CAY-1933 Problems in Evaluating EJBQL Statements with Integral Literals > Integer.MAX_VALUE
-CAY-1934 A problem exists where the escape character is not conveyed in the EJBQL when toEJBQL() is invoked on the expression.
-CAY-1936 ServerRuntime.getDataSource() returning DataSource of a default DataNode
-CAY-1937 Make Transaction an interface
-CAY-1938 Create a DI factory for transactions, get rid of TransactionDelegate and modeler config for tx policies
-CAY-1939 DataDomain must use injectable TransactionManager
-CAY-1946 CDbimport improvements
-CAY-1949 Search in configuration fields (Catalog, Schema) in DbEntity
-CAY-1952 Undeprecate (actually restore) ObjectContext.deleteObject(..)
-CAY-1953 Redo ResultIteratorCallback to handle single row callback instead of iterator
-CAY-1954 Make Cayenne class constructor protected
-CAY-1958 SelectById - a new full-featured select query to get objects by id
-CAY-1959 ObjectSelect query - a fluent API alternative to SelectQuery
-CAY-1960 ExpressionFactory.exp(..) , and(..), or(..)
-CAY-1962 Implement CayenneTable column resize on double-click on the header separator
-CAY-1965 Change version from 3.2 to 4.0
-CAY-1966 SQLTemplate/SQLSelect positional parameter binding
-CAY-1967 Deprecate SQLTemplate parameter batches
-CAY-1968 SQLSelect cleanup and omissions
-CAY-1971 Variants of Property.like(..) : contains(..), startsWith(..), endsWith(..)
-CAY-1972 A property to override DataSources of multi-module projects
-CAY-1981 Add support of JDBC 4.0 N-types (nchar, nvarchar, longnvarchar, nclob)
-CAY-1984 cdbimport doesn't flatten many to many relationships
-
-Bug Fixes:
-
-CAy-1988 ServerRuntimeBuilder: synthetic DataNode does not have domain's DataMaps linked
-CAY-1480 Implement cross-db functional expressions
-CAY-1695 Unexpected null value in bidirectional one-to-one prefetch
-CAY-1736 IllegalArgumentException when synchronizing entities in the Modeler
-CAY-1795 "Invisible" ObjAttribute in subclass
-CAY-1796 ROP: All entity's to-many relationships getting faulted from database when using it as a parameter in qualifier expression
-CAY-1797 NPE importing DataMap
-CAY-1798 ROP: Reverse relationships of prefetched entity objects are not filled during server to client objects conversion
-CAY-1799 ROP: Server can't deserialize LIKE expression with pattern already compiled
-CAY-1818 Fix copyright year in the Modeler "about" panel
-CAY-1834 Exception: ToManyList cannot be cast to DataObject
-CAY-1857 Problem with hotkeys
-CAY-1859 NullPointerException when importing EOModel
-CAY-1863 Make determining whether a particular database type supports length adapter-specific not universal
-CAY-1866 Change in General Modeler Preferences reverts old settings to default value
-CAY-1868 Select contention with multiple contexts
-CAY-1869 ResultIterator from cayenne-client dependency is subclassed from org.apache.cayenne.access.ResultIterator which is present only in cayenne-server dependency
-CAY-1874 DB2 Procedure action ignores the first result set
-CAY-1877 In-memory evaluation of expression may fail with UnsupportedOpeartionException depending on order of nodes
-CAY-1880 objectStore snapshots never cleared from RefreshQuery when "use shared cache" unchecked
-CAY-1881 CayenneModeler (Mac version) doesn't work with Java 7
-CAY-1885 Null value in subclass's field.
-CAY-1905 Multi-step prefetching NPE : 1..N..1 with absent N and root with no qualifier
-CAY-1943 XML file not deleted when a DataMap is deleted from the project
-CAY-1961 Fix RemoveAction for DataMaps in ProjectTree
-CAY-1964 Fix convertAdditionalDataMaps() in CayenneGeneratorMojo.java
-CAY-1973 error while generating classes
-CAY-1974 Copy/Paste DbEntiry throws exception
-CAY-1978 ESCAPE clause should be included in LIKE parenthesis
-CAY-1979 Prefetches on Many-to-Many Relationships with Longvarchar
-CAY-1980 'mvn cayenne-modeler:run' seems to be broken in 4.0
-
-----------------------------------
-Release: 3.2M1
-Date: July 19, 2013
-----------------------------------
-Changes/New Features:
-
-CAY-1294 Generify query
-CAY-1646 Synchronize tabs in Modeler between ObjEntity and DbEntity editor panes
-CAY-1647 Easily switch to/from ObjEntity and DbEntity in Modeler
-CAY-1717 [PATCH] Implement JDBC compatibility layer methods
-CAY-1718 Remove everything deprecated in 3.1
-CAY-1724 Add 'Property' class for easier and better Expression creation
-CAY-1726 Expression parser support for bit operators, support for << and >>
-CAY-1737 ObjectContexts listening to DataChannel events must be non-blocking
-CAY-1748 IdCoder/EntityIdCoder improvements to work with ObjectIds, including temp ones
-CAY-1753 remove light-superclass.vm class template
-CAY-1754 Modeler suggested URL for SQLServer should start with 'jdbc:sqlserver:' instead of 'jdbc:microsoft:sqlserver:'
-CAY-1758 cdbimport improvements
-CAY-1759 cdbimport improvements: add 'catalog', rename 'schemaName' to 'schema'
-CAY-1760 cdbimport improvements: Default adapter (if none specified) must be AutoAdapter, not JdbcAdapter
-CAY-1761 cdbimport improvements: DbLoader must reverse engineer PK auto-increment state.
-CAY-1762 cdbimport improvements: Support for "defaultPackage" parameter, as the new DataMaps ends up placing entities in the root package
-CAY-1763 cdbimport improvements: specified "schema" should become the default schema of the generated DataMap
-CAY-1764 cdbimport improvements: "overwrite" flag
-CAY-1765 cdbimport improvements: add excludeTables/includeTables parameters
-CAY-1766 Deprecating DataPort ant task
-CAY-1768 cdbimport improvements: DataMap "project-version" attribute is skipped when DM is saved
-CAY-1769 cdbimport improvements: meaningfulPk flag must be turned into a pattern
-CAY-1771 cdbimport improvements: "usePrimitives" flag
-CAY-1772 Real support for DbEntity catalogs
-CAY-1778 TransactionManager to simplify user-managed transactions
-CAY-1779 Flatten object entities for many to many relationships on reverse engineering
-CAY-1781 Add StatelessContextRequestHandler as an alternative to the Session-based one
-CAY-1785 SelectQuery<T> for DataRows
-CAY-1789 Lock-free EntityResolver
-CAY-1792 [PATCH] Supply additional factory methods for generic SelectQueries
-CAY-1803 Optimize Expression conversion to String and EJBQL
-CAY-1809 Remove 'final' modifier from Cayenne, HessianUtil, PropertyComparator, ConversionUtil, and LinkedDeque
-CAY-1813 Missing ObjEntity Attribute Validation with Duplicate DbEntity Columns
-CAY-1814 Support Property.nin
-CAY-1819 When adding a filter, auto-register it as a listener
-CAY-1820 DataDomain.addListener(Object) - a shortcut for adding annotated listeners
-CAY-1821 AuditableFilter and friends should explicitly work with Persistent instead of Object
-CAY-1822 Make DataMap editor fields wider
-CAY-1823 remove ":sync w/DbEntity" button from ObjEntity - it is redundant and already present on the entity toolbar.
-CAY-1825 Simplify API for setting up query caching
-CAY-1828 SQLSelect - generics friendly fluent selecting sql query
-CAY-1829 Make ResultIterator implement Iterable<T>, create ObjectContext.iterate method
-CAY-1836 Firebird Adapter
-CAY-1838 Deprecate EntityResolver.indexedByClassProperty
-CAY-1840 Conditionally log slow / long-running queries
-CAY-1844 Configuration for maximum time to wait for an available DB connection
-CAY-1862 MySQL - allow specifying a length for TIMESTAMP and TIME columns
-
-Bug Fixes:
-
-CAY-957 Deadlock in nested contexts
-CAY-1522 EJBQL query don't support quotes
-CAY-1677 Modeler: text fields discard input unless you press enter
-CAY-1701 Modeler cannot undo pasting of obj (db) entity
-CAY-1708 Modeler error when pasting datamap with EJBQL query
-CAY-1714 ROP: Cayenne tries to build a query for non committed object when using more than 2 nested contexts
-CAY-1721 Writing blobs fails (Oracle)
-CAY-1725 NullPointerException from call to removeToManyTarget
-CAY-1719 Modeler - Obj Attribute Java Type editor won't focus sometimes
-CAY-1727 Modeler thinks entity is using inheritance when it doesn't
-CAY-1729 PersistentDescriptor must have predictable property iteration order
-CAY-1738 Tutorial cayenne-rop-server should be packaged as a war
-CAY-1739 Cayenne ROP server resets session on every request if BASIC auth is used
-CAY-1742 ObjRelationship inspector says "ObjAttribute Inspector"
-CAY-1744 Unexpected read-only relationships in vertical inheritance mapping
-CAY-1749 NPE on simple nested context commit
-CAY-1755 FaultFailureException resolving relationships to UNIQUE non-PK columns
-CAY-1757 ROP: Faulting entity relationship resets uncommitted modifications made to its reverse relationship
-CAY-1774 EhCacheQueryCache.get(QueryMetadata, QueryCacheEntryFactory) returns null if EhCache instance for group is not present
-CAY-1780 cdbimport do not create xml file in resource folder
-CAY-1782 Deadlock when performing many concurrent inserts
-CAY-1783 JdbcPkGenerator.longPkFromDatabase would throw an exception if the PK value exceeds a range of Java int
-CAY-1794 Duplicate attributes in discriminator columns of PersistentDescriptor
-CAY-1804 Serialisation of long[] type was not working correctly.
-CAY-1806 Error importing eomodel
-CAY-1817 NPE during Validate Project
-CAY-1827 EhCache region corresponding to a cache group loses its settings after 'removeGroup'
-CAY-1832 Exception when modifying objects in postLoad callback
diff --git a/UPGRADE.txt b/UPGRADE.txt
index 250250c..e909b00 100644
--- a/UPGRADE.txt
+++ b/UPGRADE.txt
@@ -5,6 +5,12 @@
            current release and the release you are upgrading to.
 -------------------------------------------------------------------------------
 
+UPGRADING TO 4.2.M2
+
+* Per CAY-2659 All batch translators (`InsertBatchTranslator`, `UpdateBatchTranslator`, etc.) are updated to the new SQLBuilder utility.
+If you are using customized versions of these classes you should either update them accordingly, or you could keep using
+old versions witch are moved to the `org.apache.cayenne.access.translator.batch.legacy` package.
+
 UPGRADING TO 4.2.M1
 
 * Per CAY-2520 ObjectId can't be instantiated directly, ObjectId.of(..) methods should be used.
diff --git a/assembly/pom.xml b/assembly/pom.xml
index e69c043..8723c72 100644
--- a/assembly/pom.xml
+++ b/assembly/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<groupId>org.apache.cayenne.assembly</groupId>
diff --git a/build-tools/cayenne-checkers/pom.xml b/build-tools/cayenne-checkers/pom.xml
index 99929ea..d2d9613 100644
--- a/build-tools/cayenne-checkers/pom.xml
+++ b/build-tools/cayenne-checkers/pom.xml
@@ -22,7 +22,7 @@
 
     <groupId>org.apache.cayenne.build-tools</groupId>
     <artifactId>cayenne-checkers</artifactId>
-    <version>4.2.M1-SNAPSHOT</version>
+    <version>4.2.M2-SNAPSHOT</version>
     <packaging>jar</packaging>
 
     <name>cayenne-checkers: Cayenne Code Checkers</name>
diff --git a/build-tools/cayenne-legal/pom.xml b/build-tools/cayenne-legal/pom.xml
index bea7fc9..611ee40 100644
--- a/build-tools/cayenne-legal/pom.xml
+++ b/build-tools/cayenne-legal/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.build-tools</groupId>
 		<artifactId>build-tools-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-legal</artifactId>
diff --git a/build-tools/cayenne-test-utilities/pom.xml b/build-tools/cayenne-test-utilities/pom.xml
index 68f5026..0281d54 100644
--- a/build-tools/cayenne-test-utilities/pom.xml
+++ b/build-tools/cayenne-test-utilities/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.build-tools</groupId>
 		<artifactId>build-tools-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<description>Common Unit Test Utilities</description>
diff --git a/build-tools/pom.xml b/build-tools/pom.xml
index a6cf7fa..827c4e2 100644
--- a/build-tools/pom.xml
+++ b/build-tools/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<groupId>org.apache.cayenne.build-tools</groupId>
diff --git a/cayenne-ant/pom.xml b/cayenne-ant/pom.xml
index 91b7f4e..2bfe1a8 100644
--- a/cayenne-ant/pom.xml
+++ b/cayenne-ant/pom.xml
@@ -22,7 +22,7 @@
 	<parent>
 		<artifactId>cayenne-parent</artifactId>
 		<groupId>org.apache.cayenne</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<modelVersion>4.0.0</modelVersion>
diff --git a/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java b/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
index d6411b0..121b287 100644
--- a/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
+++ b/cayenne-ant/src/main/java/org/apache/cayenne/tools/CayenneGeneratorTask.java
@@ -88,6 +88,12 @@
      */
     protected Boolean createpkproperties;
 
+    /**
+     * Optional path (classpath or filesystem) to external velocity tool configuration file
+     * @since 4.2 
+     */
+    protected String externaltoolconfig;
+
     public CayenneGeneratorTask() {
     }
 
@@ -158,7 +164,7 @@
                 makepairs != null || mode != null || outputPattern != null || overwrite != null || superpkg != null ||
                 supertemplate != null || template != null || embeddabletemplate != null || embeddablesupertemplate != null ||
                 usepkgpath != null || createpropertynames != null || querytemplate != null ||
-                querysupertemplate != null || createpkproperties != null || force;
+                querysupertemplate != null || createpkproperties != null || force || externaltoolconfig != null;
     }
 
     private CgenConfiguration buildConfiguration(DataMap dataMap) {
@@ -200,6 +206,7 @@
         cgenConfiguration.setQueryTemplate(querytemplate != null ? querytemplate : cgenConfiguration.getQueryTemplate());
         cgenConfiguration.setQuerySuperTemplate(querysupertemplate != null ? querysupertemplate : cgenConfiguration.getQuerySuperTemplate());
         cgenConfiguration.setCreatePKProperties(createpkproperties != null ? createpkproperties : cgenConfiguration.isCreatePKProperties());
+        cgenConfiguration.setExternalToolConfig(externaltoolconfig != null ? externaltoolconfig : cgenConfiguration.getExternalToolConfig());
         if(!cgenConfiguration.isMakePairs()) {
             if(template == null) {
                 cgenConfiguration.setTemplate(cgenConfiguration.isClient() ? ClientClassGenerationAction.SINGLE_CLASS_TEMPLATE : ClassGenerationAction.SINGLE_CLASS_TEMPLATE);
@@ -394,6 +401,13 @@
     }
 
     /**
+     * @since 4.2
+     */
+    public void setExternaltoolconfig(String externaltoolconfig) {
+    	this.externaltoolconfig = externaltoolconfig;
+    }
+    
+    /**
      * Provides a <code>VPPConfig</code> object to configure. (Written with createConfig()
      * instead of addConfig() to avoid run-time dependency on VPP).
      */
diff --git a/cayenne-cache-invalidation/pom.xml b/cayenne-cache-invalidation/pom.xml
index 417621b..a1ece35 100644
--- a/cayenne-cache-invalidation/pom.xml
+++ b/cayenne-cache-invalidation/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-cgen/pom.xml b/cayenne-cgen/pom.xml
index 718a76f..a8c929b 100644
--- a/cayenne-cgen/pom.xml
+++ b/cayenne-cgen/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/Artifact.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/Artifact.java
index f3b57d8..9b8f0c3 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/Artifact.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/Artifact.java
@@ -44,6 +44,7 @@
     String CREATE_PROPERTY_NAMES = "createPropertyNames";
     String CREATE_PK_PROPERTIES = "createPKProperties";
     String PROPERTY_UTILS_KEY = "propertyUtils";
+    String METADATA_UTILS_KEY = "metadataUtils";
 
     TemplateType[] getTemplateTypes(ArtifactGenerationMode mode);
 
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
index 2f6e2e4..1912b95 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenConfiguration.java
@@ -81,7 +81,15 @@
 
     private boolean client;
 
+    /**
+     * @since 4.2
+     */
+    private String externalToolConfig;
+    
     public CgenConfiguration(boolean client) {
+        /**
+         * {@link #isDefault()} method should be in sync with the following values
+         */
         this.outputPattern = "*.java";
         this.timestamp = 0L;
         this.usePkgPath = true;
@@ -318,6 +326,14 @@
         this.client = client;
     }
 
+    public String getExternalToolConfig() {
+    	return externalToolConfig;
+    }
+    
+    public void setExternalToolConfig(String config) {
+    	this.externalToolConfig = config;
+    }
+    
     void addArtifact(Artifact artifact) {
         artifacts.add(artifact);
     }
@@ -404,7 +420,28 @@
                 .simpleTag("superPkg", this.superPkg)
                 .simpleTag("createPKProperties", Boolean.toString(this.createPKProperties))
                 .simpleTag("client", Boolean.toString(client))
+                .simpleTag("externalToolConfig", this.externalToolConfig)
                 .end();
     }
 
+    /**
+     * @return is this configuration with all values set to the default
+     */
+    public boolean isDefault() {
+        // this must be is sync with actual default values
+        return isMakePairs()
+                && usePkgPath
+                && !overwrite
+                && !createPKProperties
+                && !createPropertyNames
+                && "*.java".equals(outputPattern)
+                && (template.equals(ClassGenerationAction.SUBCLASS_TEMPLATE)
+                    || template.equals(ClientClassGenerationAction.SUBCLASS_TEMPLATE))
+                && (superTemplate.equals(ClassGenerationAction.SUPERCLASS_TEMPLATE)
+                    || superTemplate.equals(ClientClassGenerationAction.SUPERCLASS_TEMPLATE))
+                && (superPkg == null
+                    || superPkg.isEmpty())
+                && (externalToolConfig == null
+                    || externalToolConfig.isEmpty());
+    }
 }
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java
index 42391c8..7ac73a7 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/CgenModule.java
@@ -32,11 +32,12 @@
 import org.apache.cayenne.gen.property.StringPropertyDescriptorCreator;
 import org.apache.cayenne.gen.xml.CgenExtension;
 import org.apache.cayenne.project.ProjectModule;
+import org.apache.cayenne.project.extension.info.InfoExtension;
 
 /**
  * @since 4.1
  */
-public class CgenModule implements Module{
+public class CgenModule implements Module {
 
     @Override
     public void configure(Binder binder) {
@@ -44,7 +45,11 @@
         binder.bind(ClassGenerationActionFactory.class).to(DefaultClassGenerationActionFactory.class);
         binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class);
         binder.bind(ToolsUtilsFactory.class).to(DefaultToolsUtilsFactory.class);
-        ProjectModule.contributeExtensions(binder).add(CgenExtension.class);
+        binder.bind(MetadataUtils.class).to(MetadataUtils.class);
+
+        ProjectModule.contributeExtensions(binder)
+                .add(CgenExtension.class)
+                .add(InfoExtension.class); // info extension needed to get comments and other metadata
 
         contributeUserProperties(binder)
                 .add(NumericPropertyDescriptorCreator.class)
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
index 85cade1..d876fc1 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClassGenerationAction.java
@@ -34,7 +34,6 @@
 import java.util.stream.Collectors;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.gen.ImportUtils;
 import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.QueryDescriptor;
@@ -43,6 +42,8 @@
 import org.apache.velocity.app.VelocityEngine;
 import org.apache.velocity.context.Context;
 import org.apache.velocity.tools.ToolManager;
+import org.apache.velocity.tools.config.ConfigurationUtils;
+import org.apache.velocity.tools.config.FactoryConfiguration;
 import org.slf4j.Logger;
 
 public class ClassGenerationAction {
@@ -75,20 +76,29 @@
     protected Map<String, Template> templateCache;
 
     private ToolsUtilsFactory utilsFactory;
+	private MetadataUtils metadataUtils;
 
 	/**
 	Optionally allows user-defined tools besides {@link ImportUtils} for working with velocity templates.<br/>
-	To use this feature, set the java system property {@code -Dorg.apache.velocity.tools=tools.properties}
-	And create the file "tools.properties" in the working directory or in the 
-	root of the classpath with content like this: 
+	To use this feature, either set the java system property {@code -Dorg.apache.velocity.tools=tools.properties}
+	or set the {@code externalToolConfig} property to "tools.properties" in {@code CgenConfiguration}. Then 
+	create the file "tools.properties" in the working directory or in the root of the classpath with content 
+	like this: 
 	<pre>
 	tools.toolbox = application
 	tools.application.myTool = com.mycompany.MyTool</pre>
 	Then the methods in the MyTool class will be available for use in the template like ${myTool.myMethod(arg)}
 	 */
-	public ClassGenerationAction() {
-		if (System.getProperty("org.apache.velocity.tools") != null) {
+	public ClassGenerationAction(CgenConfiguration cgenConfig) {
+		this.cgenConfiguration = cgenConfig;
+		String toolConfigFile = cgenConfig.getExternalToolConfig();
+		
+		if (System.getProperty("org.apache.velocity.tools") != null || toolConfigFile != null) {
 			ToolManager manager = new ToolManager(true, true);
+			if (toolConfigFile != null) {
+				FactoryConfiguration config = ConfigurationUtils.find(toolConfigFile);
+				manager.getToolboxFactory().configure(config);
+			}
 			this.context = manager.createContext();
 		} else {
 			this.context = new VelocityContext();
@@ -191,6 +201,7 @@
         ImportUtils importUtils = utilsFactory.createImportUtils();
         context.put(Artifact.IMPORT_UTILS_KEY, importUtils);
 		context.put(Artifact.PROPERTY_UTILS_KEY, utilsFactory.createPropertyUtils(logger, importUtils));
+		context.put(Artifact.METADATA_UTILS_KEY, metadataUtils);
 		artifact.postInitContext(context);
 	}
 
@@ -502,4 +513,12 @@
 	public void setUtilsFactory(ToolsUtilsFactory utilsFactory) {
 		this.utilsFactory = utilsFactory;
 	}
+
+	public void setMetadataUtils(MetadataUtils metadataUtils) {
+		this.metadataUtils = metadataUtils;
+	}
+
+	public MetadataUtils getMetadataUtils() {
+		return metadataUtils;
+	}
 }
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java
index 9e3927b..886383d 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ClientClassGenerationAction.java
@@ -41,8 +41,8 @@
 
     public static final String CLIENT_SUPERCLASS_PREFIX = "_Client";
 
-    public ClientClassGenerationAction() {
-        super();
+    public ClientClassGenerationAction(CgenConfiguration config) {
+        super(config);
     }
 
     @Override
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/DefaultClassGenerationActionFactory.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/DefaultClassGenerationActionFactory.java
index b561547..aa0be09 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/DefaultClassGenerationActionFactory.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/DefaultClassGenerationActionFactory.java
@@ -29,13 +29,16 @@
     @Inject
     private ToolsUtilsFactory utilsFactory;
 
+    @Inject
+    private MetadataUtils metadataUtils;
+
     @Override
     public ClassGenerationAction createAction(CgenConfiguration cgenConfiguration) {
         ClassGenerationAction classGenerationAction = cgenConfiguration.isClient() ?
-                new ClientClassGenerationAction() :
-                new ClassGenerationAction();
-        classGenerationAction.setCgenConfiguration(cgenConfiguration);
+                new ClientClassGenerationAction(cgenConfiguration) :
+                new ClassGenerationAction(cgenConfiguration);
         classGenerationAction.setUtilsFactory(utilsFactory);
+        classGenerationAction.setMetadataUtils(metadataUtils);
         return classGenerationAction;
     }
 
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java
index 24d8b91..0d7d986 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/ImportUtils.java
@@ -36,11 +36,11 @@
  */
 public class ImportUtils {
 
-	public static final String importOrdering[] = { "java.", "javax.", "org.", "com." };
+	public static final String[] importOrdering = { "java.", "javax.", "org.", "com." };
 
-	static final String primitives[] = { "long", "double", "byte", "boolean", "float", "short", "int", "char" };
+	static final String[] primitives = { "long", "double", "byte", "boolean", "float", "short", "int", "char" };
 
-	static final String primitiveClasses[] = new String[] { Long.class.getName(), Double.class.getName(),
+	static final String[] primitiveClasses = new String[] { Long.class.getName(), Double.class.getName(),
 			Byte.class.getName(), Boolean.class.getName(), Float.class.getName(), Short.class.getName(),
 			Integer.class.getName(), Character.class.getName() };
 
@@ -220,7 +220,7 @@
 	 * @since 4.1
 	 */
 	public boolean canUsePrimitive(ObjAttribute attribute) {
-        return attribute.isMandatory() && isPrimitive(attribute.getType());
+        return !attribute.isLazy() && attribute.isMandatory() && isPrimitive(attribute.getType());
     }
 
 	public boolean canUsePrimitive(EmbeddableAttribute attribute) {
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/MetadataUtils.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/MetadataUtils.java
new file mode 100644
index 0000000..76c9c1a
--- /dev/null
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/MetadataUtils.java
@@ -0,0 +1,42 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.gen;
+
+import org.apache.cayenne.configuration.ConfigurationNode;
+import org.apache.cayenne.configuration.xml.DataChannelMetaData;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.project.extension.info.ObjectInfo;
+
+/**
+ * @since 4.2
+ */
+public class MetadataUtils {
+
+    @Inject
+    private DataChannelMetaData metaData;
+
+    public String getComment(ConfigurationNode node) {
+        return getInfo(node, ObjectInfo.COMMENT);
+    }
+
+    public String getInfo(ConfigurationNode node, String key) {
+        return ObjectInfo.getFromMetaData(metaData, node, key);
+    }
+}
diff --git a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java
index e337889..795599f 100644
--- a/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java
+++ b/cayenne-cgen/src/main/java/org/apache/cayenne/gen/PropertyUtils.java
@@ -28,6 +28,7 @@
 import java.util.Optional;
 
 import org.apache.cayenne.EmbeddableObject;
+import org.apache.cayenne.Fault;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.dba.TypesMapping;
 import org.apache.cayenne.di.AdhocObjectFactory;
@@ -133,6 +134,9 @@
         importUtils.addType(PropertyFactory.class.getName());
         importUtils.addType(attribute.getType());
         importUtils.addType(getPropertyDescriptor(attribute.getType()).getPropertyType());
+        if(attribute.isLazy()) {
+            importUtils.addType(Fault.class.getName());
+        }
     }
 
     public void addImport(EmbeddedAttribute attribute) throws ClassNotFoundException {
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/client-subclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/client-subclass.vm
index 7cae896..5994c98 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/client-subclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/client-subclass.vm
@@ -44,7 +44,7 @@
  */
 public#if("true" == "${object.getIsAbstract()}") abstract#end class ${subClassName} extends ${superClassName} {
 
-     private static final long serialVersionUID = 1L; 
+     private static final long serialVersionUID = 1L;
      
 ##callback methods
 #foreach( $cbname in ${entityUtils.callbackNames})
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm
index 74c1729..14a4e1e 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/client-superclass.vm
@@ -78,8 +78,12 @@
 
 ## Create ivars
 #foreach( $attr in ${object.DeclaredAttributes} )
+#if ($attr.Lazy)
+    protected Object ${attr.Name};
+#else
     protected $importUtils.formatJavaType(${attr.Type}) ${attr.Name};
 #end
+#end
 #foreach( $rel in ${object.DeclaredRelationships} )
 #if( $rel.ToMany )
 #if ( ${rel.CollectionType} == "java.util.Map")
@@ -110,7 +114,17 @@
             objectContext.prepareForAccess(this, "${attr.Name}", false);
         }
 
+#if ($attr.Lazy)
+        if(this.$attrName instanceof Fault) {
+            this.$attrName = ((Fault) this.$attrName).resolveFault(this, "$attr.Name");
+        }
+#end
+
+#if ($attr.Lazy)
+        return ($importUtils.formatJavaType(${attr.Type})) $attrName;
+#else
         return $attrName;
+#end
     }
 
 #end
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/singleclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/singleclass.vm
index ed607be..f1cf6d6 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/singleclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/singleclass.vm
@@ -50,6 +50,12 @@
 #end
 ${importUtils.generate()}
 
+#set ( $comment = ${metadataUtils.getComment($object)} )
+#if ( $comment )
+/**
+ * $comment
+ */
+#end
 public#if("true" == "${object.isAbstract()}") abstract#end class ${subClassName} extends ${baseClassName} {
 
     private static final long serialVersionUID = 1L;
@@ -94,10 +100,14 @@
 ## Create Fields ##
 ###################
 #foreach( $attr in ${object.DeclaredAttributes} )
+#if ($attr.Lazy)
+    protected Object $stringUtils.formatVariableName(${attr.Name});
+#else
 #set ( $flag = $importUtils.canUsePrimitive($attr) )
 #set ( $type = "$importUtils.formatJavaType(${attr.Type}, $flag)")
     protected $type $stringUtils.formatVariableName(${attr.Name});
 #end
+#end
 
 #foreach( $rel in ${object.DeclaredRelationships} )
     protected Object $stringUtils.formatVariableName(${rel.Name});
@@ -128,6 +138,11 @@
     public $type get${stringUtils.capitalized($attr.Name)}() {
 #end
         beforePropertyRead("${attr.Name}");
+#if ($attr.Lazy)
+        if(this.$name instanceof Fault) {
+            this.$name = ((Fault) this.$name).resolveFault(this, "$attr.Name");
+        }
+#end
 #if ($importUtils.isPrimitive($type) && !$attr.isMandatory())
         if(this.$name == null) {
 #if ($importUtils.isBoolean($type))
@@ -137,7 +152,11 @@
 #end
         }
 #end
+#if ($attr.Lazy)
+        return ($type)this.$name;
+#else
         return this.$name;
+#end
     }
 
 #end## of foreach declared attribute
@@ -229,7 +248,11 @@
 #elseif ($importUtils.isPrimitive($type))
                 this.${name} = val == null ? 0 : ($type)val;
 #else
+    #if ($attr.Lazy)
+                this.${name} = val;
+    #else
                 this.${name} = ($type)val;
+    #end
 #end
                 break;
 #end
@@ -279,7 +302,9 @@
 #set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
 #set ( $flag = $importUtils.canUsePrimitive($attr) )
 #set ( $type = "$importUtils.formatJavaType(${attr.Type}, $flag)")
-#if($importUtils.isPrimitive($type))
+#if($attr.Lazy)
+        this.$name = in.readObject();
+#elseif($importUtils.isPrimitive($type))
         this.$name = in.read${stringUtils.capitalized($type)}();
 #else
         this.$name = ($type)in.readObject();
@@ -290,4 +315,5 @@
         this.$name = in.readObject();
 #end
     }
+
 }
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/subclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/subclass.vm
index 1a538b0..6aca1ad 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/subclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/subclass.vm
@@ -34,7 +34,7 @@
 
 public#if("true" == "${object.isAbstract()}") abstract#end class ${subClassName} extends ${superClassName} {
 
-    private static final long serialVersionUID = 1L; 
+    private static final long serialVersionUID = 1L;
 
 ##callbacks
 #foreach($cbname in ${entityUtils.callbackNames})
diff --git a/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm b/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm
index a269bbb..f96be5d 100644
--- a/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm
+++ b/cayenne-cgen/src/main/resources/templates/v4_1/superclass.vm
@@ -57,10 +57,16 @@
  * It is probably a good idea to avoid changing this class manually,
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
+#set ( $comment = ${metadataUtils.getComment($object)} )
+#if ( $comment )
+ *
+ * $comment
+ *
+#end
  */
 public abstract class ${superClassName} extends ${baseClassName} {
 
-    private static final long serialVersionUID = 1L; 
+    private static final long serialVersionUID = 1L;
 
 ###########################
 ## Create property names ##
@@ -101,10 +107,14 @@
 ## Create Fields ##
 ###################
 #foreach( $attr in ${object.DeclaredAttributes} )
+#if ($attr.Lazy)
+    protected Object $stringUtils.formatVariableName(${attr.Name});
+#else
 #set ( $flag = $importUtils.canUsePrimitive($attr) )
 #set ( $type = "$importUtils.formatJavaType(${attr.Type}, $flag)" )
     protected $type $stringUtils.formatVariableName(${attr.Name});
 #end
+#end
 
 #foreach( $rel in ${object.DeclaredRelationships} )
     protected Object $stringUtils.formatVariableName(${rel.Name});
@@ -135,6 +145,11 @@
     public $type get${stringUtils.capitalized($attr.Name)}() {
 #end
         beforePropertyRead("${attr.Name}");
+#if ($attr.Lazy)
+        if(this.$name instanceof Fault) {
+            this.$name = ((Fault) this.$name).resolveFault(this, "$attr.Name");
+        }
+#end
 #if ($importUtils.isPrimitive($type) && !$attr.isMandatory())
         if(this.$name == null) {
 #if ($importUtils.isBoolean($type))
@@ -144,7 +159,11 @@
 #end
         }
 #end
+#if ($attr.Lazy)
+        return ($type)this.$name;
+#else
         return this.$name;
+#end
     }
 
 #end## of foreach declared attribute
@@ -234,7 +253,11 @@
 #elseif ($importUtils.isPrimitive($type))
                 this.${name} = val == null ? 0 : ($type)val;
 #else
+    #if ($attr.Lazy)
+                this.${name} = val;
+    #else
                 this.${name} = ($type)val;
+    #end
 #end
                 break;
 #end
@@ -284,7 +307,9 @@
 #set ( $name = "$stringUtils.formatVariableName(${attr.Name})")
 #set ( $flag = $importUtils.canUsePrimitive($attr) )
 #set ( $type = "$importUtils.formatJavaType(${attr.Type}, $flag)")
-#if($importUtils.isPrimitive($type))
+#if($attr.Lazy)
+        this.$name = in.readObject();
+#elseif($importUtils.isPrimitive($type))
         this.$name = in.read${stringUtils.capitalized($type)}();
 #else
         this.$name = ($type)in.readObject();
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/BaseTemplatesGenerationTest.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/BaseTemplatesGenerationTest.java
new file mode 100644
index 0000000..14e9b20
--- /dev/null
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/BaseTemplatesGenerationTest.java
@@ -0,0 +1,158 @@
+package org.apache.cayenne.gen;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.DataMap;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.QueryDescriptor;
+import org.apache.cayenne.map.SQLTemplateDescriptor;
+import org.apache.cayenne.map.SelectQueryDescriptor;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+
+import static org.junit.Assert.assertEquals;
+
+public class BaseTemplatesGenerationTest {
+
+    @Rule
+    public TemporaryFolder folder= new TemporaryFolder();
+
+    protected CgenConfiguration cgenConfiguration;
+    protected ClassGenerationAction action;
+    protected DataMap dataMap;
+    protected ObjEntity objEntity;
+
+    @Before
+    public void setUp() {
+        cgenConfiguration = new CgenConfiguration(false);
+        action = new ClassGenerationAction(cgenConfiguration);
+        dataMap = new DataMap();
+        dataMap.setDefaultPackage("test");
+        objEntity = new ObjEntity();
+    }
+
+    @Test
+    public void testSelectQuery() throws Exception {
+        dataMap.setName("SelectQuery");
+
+        String param = "param";
+        String qualifierString = "name = $" + param;
+
+        DbEntity dbEntity = new DbEntity();
+        ObjAttribute attribute = new ObjAttribute("name");
+        attribute.setDbAttributePath("testKey");
+        attribute.setType("java.lang.String");
+        objEntity.addAttribute(attribute);
+        objEntity.setDbEntity(dbEntity);
+        objEntity.setClassName("Test");
+
+        SelectQueryDescriptor selectQueryDescriptor = new SelectQueryDescriptor();
+        Expression exp = ExpressionFactory.exp(qualifierString);
+        selectQueryDescriptor.setQualifier(exp);
+        selectQueryDescriptor.setName("select");
+        selectQueryDescriptor.setRoot(objEntity);
+
+        Collection<QueryDescriptor> descriptors = new ArrayList<>();
+        descriptors.add(selectQueryDescriptor);
+
+        DataMapArtifact dataMapArtifact = new DataMapArtifact(dataMap, descriptors);
+
+        execute(dataMapArtifact);
+    }
+
+    @Test
+    public void testSQLTemplate() throws Exception {
+        dataMap.setName("SQLTemplate");
+
+        DbEntity dbEntity = new DbEntity();
+        objEntity.setDbEntity(dbEntity);
+        objEntity.setClassName("Test");
+
+        SQLTemplateDescriptor sqlTemplateDescriptor = new SQLTemplateDescriptor();
+        sqlTemplateDescriptor.setSql("SELECT * FROM table");
+        sqlTemplateDescriptor.setRoot(objEntity);
+        sqlTemplateDescriptor.setName("select");
+        sqlTemplateDescriptor.setRoot(objEntity);
+
+        Collection<QueryDescriptor> descriptors = new ArrayList<>();
+        descriptors.add(sqlTemplateDescriptor);
+
+        DataMapArtifact dataMapArtifact = new DataMapArtifact(dataMap, descriptors);
+
+        execute(dataMapArtifact);
+    }
+
+    @Test
+    public void testGenClass() throws Exception {
+        dataMap.setName("ObjEntity");
+
+        DbEntity dbEntity = new DbEntity();
+        dbEntity.setName("EntityTest");
+        objEntity.setDbEntity(dbEntity);
+        objEntity.setClassName("test.ObjEntity");
+        objEntity.setDataMap(dataMap);
+
+        EntityArtifact entityArtifact = new EntityArtifact(objEntity);
+
+        execute(entityArtifact);
+    }
+
+    public void execute(Artifact artifact) throws Exception{
+        cgenConfiguration.addArtifact(artifact);
+
+        cgenConfiguration.setRootPath(folder.getRoot().toPath());
+        cgenConfiguration.setRelPath(Paths.get("."));
+        cgenConfiguration.loadEntity(objEntity);
+        cgenConfiguration.setDataMap(dataMap);
+
+        action.setUtilsFactory(new DefaultToolsUtilsFactory());
+        action.execute();
+
+        String targetName = dataMap.getName();
+
+        fileComparison(targetName);
+        fileComparison("auto/_" + targetName);
+    }
+
+    private void fileComparison(String fileName) throws IOException {
+        String expected = readResource(fileName);
+
+        StringBuilder generated = new StringBuilder();
+        Files.readAllLines(new File(folder.getRoot() + "/test/" + fileName + ".java").toPath())
+                .forEach(generated::append);
+
+        assertEquals(expected, generated.toString());
+    }
+
+    private String readResource(String name) throws IOException {
+        String resourceName = "templateTest/" + name + ".java";
+        InputStream stream = getClass().getClassLoader().getResourceAsStream(resourceName);
+        if(stream == null) {
+            throw new FileNotFoundException("Resource not found: " + resourceName);
+        }
+        StringBuilder expected = new StringBuilder();
+        try(BufferedReader resource = new BufferedReader(new InputStreamReader(stream))) {
+            String line;
+            while ((line = resource.readLine()) != null) {
+                expected.append(line);
+            }
+        }
+
+        return expected.toString();
+    }
+}
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenCase.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenCase.java
index 5514945..b8f773a 100644
--- a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenCase.java
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/CgenCase.java
@@ -19,21 +19,26 @@
 
 package org.apache.cayenne.gen;
 
+import org.apache.cayenne.dbsync.reverse.configuration.ToolsModule;
 import org.apache.cayenne.di.DIBootstrap;
 import org.apache.cayenne.di.Injector;
 import org.apache.cayenne.di.spi.DefaultScope;
 import org.apache.cayenne.unit.di.DICase;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * @since 4.2
  */
 public class CgenCase extends DICase {
 
+    private final static Logger LOGGER = LoggerFactory.getLogger(CgenCase.class);
+
     private static final Injector injector;
 
     static {
         DefaultScope testScope = new DefaultScope();
-        injector = DIBootstrap.createInjector(new CgenCaseModule(testScope), new CgenModule());
+        injector = DIBootstrap.createInjector(new CgenCaseModule(testScope), new CgenModule(), new ToolsModule(LOGGER));
     }
 
     @Override
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
index 58a1489..ef9d5e6 100644
--- a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/ClassGenerationActionTest.java
@@ -19,8 +19,7 @@
 
 package org.apache.cayenne.gen;
 
-import java.io.File;
-import java.io.StringWriter;
+import java.io.*;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
@@ -34,17 +33,19 @@
 import org.apache.cayenne.map.QueryDescriptor;
 import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.*;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 public class ClassGenerationActionTest extends CgenCase {
 
+	@Rule
+	public TemporaryFolder tempFolder = new TemporaryFolder();
+
 	protected ClassGenerationAction action;
 	protected Collection<StringWriter> writers;
 
@@ -172,7 +173,7 @@
 
 	private void runDataMapTest(boolean client) throws Exception {
 		QueryDescriptor descriptor = QueryDescriptor.selectQueryDescriptor();
-        descriptor.setName("TestQuery");
+		descriptor.setName("TestQuery");
 
 		DataMap map = new DataMap();
 		map.addQueryDescriptor(descriptor);
@@ -282,4 +283,54 @@
 
 		assertTrue(action.fileNeedUpdate(file, null));
 	}
-}
+
+	@Test
+	public void testFileForSuperclass() throws Exception {
+
+		TemplateType templateType = TemplateType.DATAMAP_SUPERCLASS;
+
+		cgenConfiguration.setRootPath(tempFolder.getRoot().toPath());
+		cgenConfiguration.setRelPath(".");
+		action = new ClassGenerationAction(cgenConfiguration);
+		ObjEntity testEntity1 = new ObjEntity("TEST");
+		testEntity1.setClassName("TestClass1");
+		action.context.put(Artifact.SUPER_PACKAGE_KEY, "");
+		action.context.put(Artifact.SUPER_CLASS_KEY, "TestClass1");
+
+		File outFile = new File(tempFolder.getRoot() + "/TestClass1.java");
+		assertFalse(outFile.exists());
+
+		action.openWriter(templateType);
+		assertTrue(outFile.exists());
+
+		assertNull(action.openWriter(templateType));
+	}
+
+	@Test
+	public void testFileForClass() throws Exception {
+
+		TemplateType templateType = TemplateType.DATAMAP_SINGLE_CLASS;
+
+		cgenConfiguration.setRootPath(tempFolder.getRoot().toPath());
+		cgenConfiguration.setRelPath(".");
+		action = new ClassGenerationAction(cgenConfiguration);
+		ObjEntity testEntity1 = new ObjEntity("TEST");
+		testEntity1.setClassName("TestClass1");
+		action.context.put(Artifact.SUB_PACKAGE_KEY, "");
+		action.context.put(Artifact.SUB_CLASS_KEY, "TestClass1");
+
+		File outFile = new File(tempFolder.getRoot() + "/TestClass1.java");
+		assertFalse(outFile.exists());
+
+		action.openWriter(templateType);
+		assertTrue(outFile.exists());
+
+		assertNull(action.openWriter(templateType));
+
+		cgenConfiguration.setMakePairs(false);
+		assertNull(action.openWriter(templateType));
+
+		cgenConfiguration.setOverwrite(true);
+		assertNull(action.openWriter(templateType));
+	}
+}
\ No newline at end of file
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/DataMapUtilsTest.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/DataMapUtilsTest.java
new file mode 100644
index 0000000..32361fc
--- /dev/null
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/DataMapUtilsTest.java
@@ -0,0 +1,91 @@
+package org.apache.cayenne.gen;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.*;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.*;
+
+import static org.junit.Assert.assertEquals;
+
+public class DataMapUtilsTest {
+
+    protected DataMapUtils dataMapUtils = null;
+    protected ObjEntity objEntity = null;
+
+    @Before
+    public void setUp() {
+        dataMapUtils = new DataMapUtils();
+        objEntity = new ObjEntity();
+    }
+
+    @After
+    public void tearDown() {
+        dataMapUtils = null;
+        objEntity = null;
+    }
+
+    @Test
+    public void testGetParameterNamesWithFilledQueriesMap() {
+
+        String param = "param";
+        String qualifierString = "name = $" + param;
+
+        SelectQueryDescriptor selectQueryDescriptor = new SelectQueryDescriptor();
+
+        Set<String> result = new LinkedHashSet<>();
+        assertEquals(result, dataMapUtils.getParameterNames(selectQueryDescriptor));
+
+        Expression exp = ExpressionFactory.exp(qualifierString);
+        selectQueryDescriptor.setQualifier(exp);
+        selectQueryDescriptor.setName("name");
+
+        Map<String, String> map = new HashMap<>();
+        map.put(param, "java.lang.String");
+
+        dataMapUtils.queriesMap.put("name", map);
+        Collection collection = dataMapUtils.getParameterNames(selectQueryDescriptor);
+
+        result.add(param);
+
+        assertEquals(collection, result);
+    }
+
+    @Test
+    public void testGetParameterNamesWithEmptyQueriesMap() {
+
+        DbEntity dbEntity = new DbEntity("test");
+        ObjAttribute attribute = new ObjAttribute("name");
+        attribute.setDbAttributePath("testKey");
+        attribute.setType("java.lang.String");
+        objEntity.addAttribute(attribute);
+        objEntity.setName("test");
+        objEntity.setDbEntity(dbEntity);
+
+        String param = "param";
+        String qualifierString = "name = $" + param;
+
+        SelectQueryDescriptor selectQueryDescriptor = new SelectQueryDescriptor();
+        Expression exp = ExpressionFactory.exp(qualifierString);
+        selectQueryDescriptor.setQualifier(exp);
+        selectQueryDescriptor.setName("name");
+        selectQueryDescriptor.setRoot(objEntity);
+
+        Collection collection = dataMapUtils.getParameterNames(selectQueryDescriptor);
+
+        Map<String, Map<String, String>> queriesMap = new HashMap<>();
+        Map<String, String> map = new HashMap<>();
+        map.put(param, "java.lang.String");
+        queriesMap.put("name", map);
+
+        assertEquals(dataMapUtils.queriesMap, queriesMap);
+
+        Set<String> result = new LinkedHashSet<>();
+        result.add(param);
+
+        assertEquals(collection, result);
+    }
+}
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/mock/TestClassGenerationAction.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/mock/TestClassGenerationAction.java
index c16aabc..b58b518 100644
--- a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/mock/TestClassGenerationAction.java
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/mock/TestClassGenerationAction.java
@@ -33,8 +33,7 @@
     private Collection<StringWriter> writers;
 
     public TestClassGenerationAction(ClassGenerationAction classGenerationAction, Collection<StringWriter> writers){
-        super();
-        setCgenConfiguration(classGenerationAction.getCgenConfiguration());
+        super(classGenerationAction.getCgenConfiguration());
         setUtilsFactory(classGenerationAction.getUtilsFactory());
         this.writers = writers;
     }
diff --git a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/xml/CgenSaverDelegateTest.java b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/xml/CgenSaverDelegateTest.java
index 41dd931..37c910b 100644
--- a/cayenne-cgen/src/test/java/org/apache/cayenne/gen/xml/CgenSaverDelegateTest.java
+++ b/cayenne-cgen/src/test/java/org/apache/cayenne/gen/xml/CgenSaverDelegateTest.java
@@ -36,12 +36,12 @@
     public void testExistingRootOverride() throws Exception {
         CgenConfiguration config = new CgenConfiguration(false);
 
-        config.setRootPath(Paths.get("/tmp/src/main/java"));
+        config.setRootPath(Paths.get("/tmp/src/main/java").toAbsolutePath());
         URL baseURL = Paths.get("/tmp/src/main/resources").toUri().toURL();
 
         CgenSaverDelegate.resolveOutputDir(baseURL, config);
 
-        assertEquals(Paths.get("/tmp/src/main/resources"), config.getRootPath());
+        assertEquals(Paths.get("/tmp/src/main/resources").toAbsolutePath(), config.getRootPath());
         assertEquals(Paths.get("../java"), config.getRelPath());
     }
 
@@ -49,14 +49,14 @@
     public void testExistingRootAndRelPath() throws Exception {
         CgenConfiguration config = new CgenConfiguration(false);
 
-        config.setRootPath(Paths.get("/tmp/src/main/java"));
+        config.setRootPath(Paths.get("/tmp/src/main/java").toAbsolutePath());
         config.setRelPath(Paths.get(""));
 
         URL baseURL = Paths.get("/tmp/src/main/resources").toUri().toURL();
 
         CgenSaverDelegate.resolveOutputDir(baseURL, config);
 
-        assertEquals(Paths.get("/tmp/src/main/resources"), config.getRootPath());
+        assertEquals(Paths.get("/tmp/src/main/resources").toAbsolutePath(), config.getRootPath());
         assertEquals(Paths.get("../java"), config.getRelPath());
     }
 
@@ -68,7 +68,7 @@
 
         CgenSaverDelegate.resolveOutputDir(baseURL, config);
 
-        assertEquals(Paths.get("/tmp/src/main/resources"), config.getRootPath());
+        assertEquals(Paths.get("/tmp/src/main/resources").toAbsolutePath(), config.getRootPath());
         assertEquals(Paths.get(""), config.getRelPath());
     }
 
diff --git a/cayenne-cgen/src/test/resources/templateTest/ObjEntity.java b/cayenne-cgen/src/test/resources/templateTest/ObjEntity.java
new file mode 100644
index 0000000..1a036c0
--- /dev/null
+++ b/cayenne-cgen/src/test/resources/templateTest/ObjEntity.java
@@ -0,0 +1,10 @@
+package test;
+
+import test.auto._ObjEntity;
+
+public class ObjEntity extends _ObjEntity {
+
+    private static final long serialVersionUID = 1L;
+
+}
+
diff --git a/cayenne-cgen/src/test/resources/templateTest/SQLTemplate.java b/cayenne-cgen/src/test/resources/templateTest/SQLTemplate.java
new file mode 100644
index 0000000..53109a6
--- /dev/null
+++ b/cayenne-cgen/src/test/resources/templateTest/SQLTemplate.java
@@ -0,0 +1,18 @@
+package test;
+
+import test.auto._SQLTemplate;
+
+public class SQLTemplate extends _SQLTemplate {
+
+    private static SQLTemplate instance;
+
+    private SQLTemplate() {}
+
+    public static SQLTemplate getInstance() {
+        if(instance == null) {
+            instance = new SQLTemplate();
+        }
+
+        return instance;
+    }
+}
diff --git a/cayenne-cgen/src/test/resources/templateTest/SelectQuery.java b/cayenne-cgen/src/test/resources/templateTest/SelectQuery.java
new file mode 100644
index 0000000..49144de
--- /dev/null
+++ b/cayenne-cgen/src/test/resources/templateTest/SelectQuery.java
@@ -0,0 +1,18 @@
+package test;
+
+import test.auto._SelectQuery;
+
+public class SelectQuery extends _SelectQuery {
+
+    private static SelectQuery instance;
+
+    private SelectQuery() {}
+
+    public static SelectQuery getInstance() {
+        if(instance == null) {
+            instance = new SelectQuery();
+        }
+
+        return instance;
+    }
+}
diff --git a/cayenne-cgen/src/test/resources/templateTest/auto/_ObjEntity.java b/cayenne-cgen/src/test/resources/templateTest/auto/_ObjEntity.java
new file mode 100644
index 0000000..83d68ff
--- /dev/null
+++ b/cayenne-cgen/src/test/resources/templateTest/auto/_ObjEntity.java
@@ -0,0 +1,65 @@
+package test.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+
+/**
+ * Class _ObjEntity was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _ObjEntity extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L;
+
+
+
+
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+    }
+
+}
diff --git a/cayenne-cgen/src/test/resources/templateTest/auto/_SQLTemplate.java b/cayenne-cgen/src/test/resources/templateTest/auto/_SQLTemplate.java
new file mode 100644
index 0000000..e143491
--- /dev/null
+++ b/cayenne-cgen/src/test/resources/templateTest/auto/_SQLTemplate.java
@@ -0,0 +1,23 @@
+package test.auto;
+
+import java.util.Map;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.QueryResult;
+import org.apache.cayenne.query.MappedExec;
+
+/**
+ * This class was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public class _SQLTemplate {
+
+    public static final String SELECT_QUERYNAME = "select";
+    public QueryResult<?> performSelect(ObjectContext context, Map<String, ?> parameters) {
+        MappedExec query = MappedExec.query(SELECT_QUERYNAME).params(parameters);
+        return query.execute(context);
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-cgen/src/test/resources/templateTest/auto/_SelectQuery.java b/cayenne-cgen/src/test/resources/templateTest/auto/_SelectQuery.java
new file mode 100644
index 0000000..7b99f0e
--- /dev/null
+++ b/cayenne-cgen/src/test/resources/templateTest/auto/_SelectQuery.java
@@ -0,0 +1,24 @@
+package test.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.query.MappedSelect;
+
+/**
+ * This class was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public class _SelectQuery {
+
+    public static final String SELECT_QUERYNAME = "select";
+
+    public List<Test> performSelect(ObjectContext context, String param) {
+        MappedSelect<Test> query = MappedSelect.query(SELECT_QUERYNAME, Test.class);
+        query.param("param", param);
+        return query.select(context);
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-client-jetty/pom.xml b/cayenne-client-jetty/pom.xml
index 5cf9131..5b5a4c4 100644
--- a/cayenne-client-jetty/pom.xml
+++ b/cayenne-client-jetty/pom.xml
@@ -15,7 +15,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <artifactId>cayenne-client-jetty</artifactId>
diff --git a/cayenne-client/pom.xml b/cayenne-client/pom.xml
index 2aaace6..9d74a58 100644
--- a/cayenne-client/pom.xml
+++ b/cayenne-client/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<artifactId>cayenne-client</artifactId>
 	<packaging>jar</packaging>
diff --git a/cayenne-client/src/test/java/org/apache/cayenne/cay_2641/Cay2641IT.java b/cayenne-client/src/test/java/org/apache/cayenne/cay_2641/Cay2641IT.java
new file mode 100644
index 0000000..3b2d148
--- /dev/null
+++ b/cayenne-client/src/test/java/org/apache/cayenne/cay_2641/Cay2641IT.java
@@ -0,0 +1,97 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.cay_2641;
+
+import org.apache.cayenne.CayenneContext;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.testdo.cay_2641.client.ArtistLazy;
+import org.apache.cayenne.testdo.cay_2641.client.PaintingLazy;
+import org.apache.cayenne.unit.di.client.ClientCase;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.List;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.CAY_2641)
+public class Cay2641IT extends ClientCase {
+
+    @Inject
+    private CayenneContext context;
+
+    @Before
+    public void setup() {
+        ArtistLazy artistLazy = context.newObject(ArtistLazy.class);
+        artistLazy.setName("Test");
+        artistLazy.setSurname("Test1");
+
+        PaintingLazy paintingLazy = context.newObject(PaintingLazy.class);
+        paintingLazy.setName("Test");
+        paintingLazy.setArtist(artistLazy);
+
+        context.commitChanges();
+    }
+
+    @Test
+    public void testSampleSelect() {
+        List<ArtistLazy> artists = ObjectSelect.query(ArtistLazy.class).select(context);
+
+        assertEquals(artists.size(), 1);
+        assertEquals(artists.get(0).getSurname(), "Test1");
+
+        assertTrue(artists.get(0).readPropertyDirectly("name") instanceof Fault);
+
+        assertEquals(artists.get(0).getName(), "Test");
+    }
+
+    @Test
+    public void testColumnSelect() {
+        List<String> strings = ObjectSelect.columnQuery(ArtistLazy.class, ArtistLazy.NAME).select(context);
+
+        assertEquals(strings.size(), 1);
+        assertEquals(strings.get(0), "Test");
+    }
+
+    @Test
+    public void testPrefetchSelect() {
+        List<PaintingLazy> paintingLazyList1 = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.joint()).select(context);
+
+        assertEquals(paintingLazyList1.size(), 1);
+        assertTrue(paintingLazyList1.get(0).getArtist().readPropertyDirectly("name") instanceof Fault);
+
+        List<PaintingLazy> paintingLazyList2 = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.disjoint()).select(context);
+
+        assertEquals(paintingLazyList2.size(), 1);
+        assertTrue(paintingLazyList1.get(0).getArtist().readPropertyDirectly("name") instanceof Fault);
+
+        List<PaintingLazy> paintingLazyList3 = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.disjointById()).select(context);
+
+        assertEquals(paintingLazyList3.size(), 1);
+        assertTrue(paintingLazyList1.get(0).getArtist().readPropertyDirectly("name") instanceof Fault);
+    }
+
+}
diff --git a/cayenne-commitlog/pom.xml b/cayenne-commitlog/pom.xml
index 6fb5f32..e79dd3a 100644
--- a/cayenne-commitlog/pom.xml
+++ b/cayenne-commitlog/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java b/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java
index a283ef5..deafacf 100644
--- a/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java
+++ b/cayenne-commitlog/src/main/java/org/apache/cayenne/commitlog/DeletedDiffProcessor.java
@@ -18,28 +18,25 @@
  ****************************************************************/
 package org.apache.cayenne.commitlog;
 
-import java.util.List;
-
 import org.apache.cayenne.DataChannel;
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.QueryResponse;
-import org.apache.cayenne.graph.ArcId;
-import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.commitlog.meta.CommitLogEntity;
+import org.apache.cayenne.commitlog.meta.CommitLogEntityFactory;
 import org.apache.cayenne.commitlog.model.MutableChangeMap;
 import org.apache.cayenne.commitlog.model.MutableObjectChange;
 import org.apache.cayenne.commitlog.model.ObjectChangeType;
-import org.apache.cayenne.commitlog.meta.CommitLogEntity;
-import org.apache.cayenne.commitlog.meta.CommitLogEntityFactory;
+import org.apache.cayenne.graph.ArcId;
+import org.apache.cayenne.graph.GraphChangeHandler;
+import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.query.ObjectIdQuery;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.reflect.*;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.util.List;
+
 class DeletedDiffProcessor implements GraphChangeHandler {
 
 	private static final Logger LOGGER = LoggerFactory.getLogger(DeletedDiffProcessor.class);
@@ -102,7 +99,21 @@
 
 			@Override
 			public boolean visitToOne(ToOneProperty property) {
-				// TODO record FK changes?
+                if (!entity.isIncluded(property.getName())) {
+                    return true;
+                }
+
+                // TODO: is there such a thing as "confidential" relationship that we need to hide?
+
+                DbRelationship dbRelationship = property.getRelationship().getDbRelationships().get(0);
+
+                ObjectId value = row.createTargetObjectId(
+                        property.getTargetDescriptor().getEntity().getName(),
+                        dbRelationship);
+
+                if (value != null) {
+                    objectChangeSet.toOneRelationshipDisconnected(property.getName(), value);
+                }
 				return true;
 			}
 
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java
new file mode 100644
index 0000000..f3a6302
--- /dev/null
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilterIT.java
@@ -0,0 +1,318 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.commitlog;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.commitlog.db.Auditable1;
+import org.apache.cayenne.commitlog.db.AuditableChild1;
+import org.apache.cayenne.commitlog.db.AuditableChild1x;
+import org.apache.cayenne.commitlog.model.*;
+import org.apache.cayenne.commitlog.unit.AuditableServerCase;
+import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
+import org.apache.cayenne.query.SelectById;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.sql.SQLException;
+
+import static org.junit.Assert.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+public class CommitLogFilterIT extends AuditableServerCase {
+
+    protected ObjectContext context;
+    protected CommitLogListener mockListener;
+
+    @Override
+    protected ServerRuntimeBuilder configureCayenne() {
+        this.mockListener = mock(CommitLogListener.class);
+        return super.configureCayenne().addModule(CommitLogModule.extend().addListener(mockListener).module());
+    }
+
+    @Before
+    public void before() {
+        context = runtime.newContext();
+    }
+
+    @Test
+    public void testPostCommit_Insert() {
+
+        Auditable1 a1 = context.newObject(Auditable1.class);
+        a1.setCharProperty1("yy");
+        ObjectId preCommitId = a1.getObjectId();
+
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(2, changeMap.getValue().getChanges().size());
+        assertEquals(1, changeMap.getValue().getUniqueChanges().size());
+
+        ObjectChange c = changeMap.getValue().getUniqueChanges().iterator().next();
+        assertNotNull(c);
+        assertEquals(ObjectChangeType.INSERT, c.getType());
+        assertEquals(1, c.getAttributeChanges().size());
+        assertEquals("yy", c.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName()).getNewValue());
+
+        assertNotEquals(preCommitId, a1.getObjectId());
+        assertEquals(preCommitId, c.getPreCommitId());
+        assertEquals(a1.getObjectId(), c.getPostCommitId());
+    }
+
+    @Test
+    public void testPostCommit_Update() throws SQLException {
+
+        auditable1.insert(1, "xx");
+
+        Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
+        a1.setCharProperty1("yy");
+
+        ObjectId preCommitId = a1.getObjectId();
+
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(1, changeMap.getValue().getUniqueChanges().size());
+
+        ObjectChange c = changeMap.getValue().getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+        assertNotNull(c);
+        assertEquals(ObjectChangeType.UPDATE, c.getType());
+        assertEquals(1, c.getAttributeChanges().size());
+        AttributeChange pc = c.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName());
+        assertNotNull(pc);
+        assertEquals("xx", pc.getOldValue());
+        assertEquals("yy", pc.getNewValue());
+
+        assertEquals(preCommitId, a1.getObjectId());
+        assertEquals(preCommitId, c.getPreCommitId());
+        assertEquals(preCommitId, c.getPostCommitId());
+    }
+
+    @Test
+    public void testPostCommit_Delete() throws SQLException {
+        auditable1.insert(1, "xx");
+        auditableChild1.insert(1, 1, "cc1");
+        auditableChild1.insert(2, 1, "cc2");
+
+        Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
+        context.deleteObjects(a1.getChildren1());
+        context.deleteObject(a1);
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(3, changeMap.getValue().getUniqueChanges().size());
+
+        // check from the perspective of the master object
+        ObjectChange masterChange = changeMap.getValue().getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+        assertNotNull(masterChange);
+        assertEquals(ObjectChangeType.DELETE, masterChange.getType());
+
+        assertEquals(1, masterChange.getAttributeChanges().size());
+        assertEquals("xx", masterChange.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName()).getOldValue());
+        assertNull(masterChange.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName()).getNewValue());
+
+        assertEquals("1..N was explicitly unset as a part of delete. Expected to be recorded in changes",
+                1, masterChange.getToManyRelationshipChanges().size());
+        assertTrue("No N..1 relationships in the entity", masterChange.getToOneRelationshipChanges().isEmpty());
+
+        // check from the perspective of the child object
+        ObjectChange childChange = changeMap.getValue().getChanges().get(ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 2));
+        assertNotNull(childChange);
+        assertEquals(ObjectChangeType.DELETE, childChange.getType());
+
+        assertEquals(1, childChange.getAttributeChanges().size());
+        assertEquals("cc2", childChange.getAttributeChanges().get(AuditableChild1.CHAR_PROPERTY1.getName()).getOldValue());
+        assertNull(childChange.getAttributeChanges().get(AuditableChild1.CHAR_PROPERTY1.getName()).getNewValue());
+
+        assertTrue("No 1..N relationships in the entity", childChange.getToManyRelationshipChanges().isEmpty());
+        assertEquals("N..1 was explicitly unset as a part of delete. Expected to be recorded in changes",
+                1, childChange.getToOneRelationshipChanges().size());
+    }
+
+    @Test
+    public void testPostCommit_Delete_ToOneNullify() throws SQLException {
+        auditable1.insert(1, "xx");
+        auditableChild1.insert(1, 1, "cc1");
+        auditableChild1.insert(2, 1, "cc2");
+
+        AuditableChild1 ac1 = SelectById.query(AuditableChild1.class, 2).selectOne(context);
+        context.deleteObject(ac1);
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(2, changeMap.getValue().getUniqueChanges().size());
+
+        ObjectChange change = changeMap.getValue().getChanges().get(ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 2));
+        assertNotNull(change);
+        assertEquals(ObjectChangeType.DELETE, change.getType());
+
+        assertEquals(1, change.getAttributeChanges().size());
+        assertEquals("cc2", change.getAttributeChanges().get(AuditableChild1.CHAR_PROPERTY1.getName()).getOldValue());
+        assertNull(change.getAttributeChanges().get(AuditableChild1.CHAR_PROPERTY1.getName()).getNewValue());
+
+        assertTrue("No 1..N relationships in the entity", change.getToManyRelationshipChanges().isEmpty());
+        assertEquals("N..1 state was not captured", 1, change.getToOneRelationshipChanges().size());
+        assertEquals(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1),
+                change.getToOneRelationshipChanges().get(AuditableChild1.PARENT.getName()).getOldValue());
+    }
+
+    @Test
+    public void testPostCommit_Delete_ToOne_OneWay() throws SQLException {
+        auditable1.insert(1, "xx");
+        auditableChild1x.insert(1, 1, "cc1");
+        auditableChild1x.insert(2, 1, "cc2");
+
+        AuditableChild1x ac1 = SelectById.query(AuditableChild1x.class, 2).selectOne(context);
+        context.deleteObject(ac1);
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(1, changeMap.getValue().getUniqueChanges().size());
+
+        ObjectChange change = changeMap.getValue().getChanges().get(ObjectId.of("AuditableChild1x", AuditableChild1x.ID_PK_COLUMN, 2));
+        assertNotNull(change);
+        assertEquals(ObjectChangeType.DELETE, change.getType());
+
+        assertEquals(1, change.getAttributeChanges().size());
+        assertEquals("cc2", change.getAttributeChanges().get(AuditableChild1x.CHAR_PROPERTY1.getName()).getOldValue());
+        assertNull(change.getAttributeChanges().get(AuditableChild1x.CHAR_PROPERTY1.getName()).getNewValue());
+
+        assertTrue("No 1..N relationships in the entity", change.getToManyRelationshipChanges().isEmpty());
+        assertEquals("N..1 state was not captured", 1, change.getToOneRelationshipChanges().size());
+        assertEquals(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1),
+                change.getToOneRelationshipChanges().get(AuditableChild1x.PARENT.getName()).getOldValue());
+    }
+
+
+    @Test
+    public void testPostCommit_UpdateToOne() throws SQLException {
+        auditable1.insert(1, "xx");
+        auditable1.insert(2, "yy");
+
+        auditableChild1.insert(1, 1, "cc1");
+        auditableChild1.insert(2, 2, "cc2");
+        auditableChild1.insert(3, null, "cc3");
+
+        AuditableChild1 ac1 = SelectById.query(AuditableChild1.class, 1).selectOne(context);
+        AuditableChild1 ac2 = SelectById.query(AuditableChild1.class, 2).selectOne(context);
+        AuditableChild1 ac3 = SelectById.query(AuditableChild1.class, 3).selectOne(context);
+
+        Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
+        Auditable1 a2 = SelectById.query(Auditable1.class, 2).selectOne(context);
+
+        a1.removeFromChildren1(ac1);
+        a1.addToChildren1(ac2);
+        a1.addToChildren1(ac3);
+
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(4, changeMap.getValue().getUniqueChanges().size());
+
+        ObjectChange ac1c = changeMap.getValue().getChanges().get(
+                ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 1));
+        assertNotNull(ac1c);
+        assertEquals(ObjectChangeType.UPDATE, ac1c.getType());
+        ToOneRelationshipChange ac1c1 = ac1c.getToOneRelationshipChanges().get(AuditableChild1.PARENT.getName());
+        assertEquals(a1.getObjectId(), ac1c1.getOldValue());
+        assertNull(ac1c1.getNewValue());
+
+        ObjectChange ac2c = changeMap.getValue().getChanges().get(
+                ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 2));
+        assertNotNull(ac2c);
+        assertEquals(ObjectChangeType.UPDATE, ac2c.getType());
+        ToOneRelationshipChange ac2c1 = ac2c.getToOneRelationshipChanges()
+                .get(AuditableChild1.PARENT.getName());
+        assertEquals(a2.getObjectId(), ac2c1.getOldValue());
+        assertEquals(a1.getObjectId(), ac2c1.getNewValue());
+
+        ObjectChange ac3c = changeMap.getValue().getChanges().get(
+                ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 3));
+        assertNotNull(ac3c);
+        assertEquals(ObjectChangeType.UPDATE, ac3c.getType());
+        ToOneRelationshipChange ac3c1 = ac3c.getToOneRelationshipChanges()
+                .get(AuditableChild1.PARENT.getName());
+        assertNull(ac3c1.getOldValue());
+        assertEquals(a1.getObjectId(), ac3c1.getNewValue());
+    }
+
+    @Test
+    public void testPostCommit_UpdateToMany() throws SQLException {
+        auditable1.insert(1, "xx");
+        auditableChild1.insert(1, 1, "cc1");
+        auditableChild1.insert(2, null, "cc2");
+        auditableChild1.insert(3, null, "cc3");
+
+        AuditableChild1 ac1 = SelectById.query(AuditableChild1.class, 1).selectOne(context);
+        AuditableChild1 ac2 = SelectById.query(AuditableChild1.class, 2).selectOne(context);
+        AuditableChild1 ac3 = SelectById.query(AuditableChild1.class, 3).selectOne(context);
+
+        Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
+
+        a1.removeFromChildren1(ac1);
+        a1.addToChildren1(ac2);
+        a1.addToChildren1(ac3);
+
+        context.commitChanges();
+
+        ArgumentCaptor<ChangeMap> changeMap = ArgumentCaptor.forClass(ChangeMap.class);
+        verify(mockListener).onPostCommit(any(ObjectContext.class), changeMap.capture());
+
+        assertNotNull(changeMap.getValue());
+        assertEquals(4, changeMap.getValue().getUniqueChanges().size());
+
+        ObjectChange a1c = changeMap.getValue().getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+        assertNotNull(a1c);
+        assertEquals(ObjectChangeType.UPDATE, a1c.getType());
+        assertEquals(0, a1c.getAttributeChanges().size());
+
+        assertEquals(1, a1c.getToManyRelationshipChanges().size());
+
+        ToManyRelationshipChange a1c1 = a1c.getToManyRelationshipChanges().get(Auditable1.CHILDREN1.getName());
+        assertNotNull(a1c1);
+
+        assertEquals(2, a1c1.getAdded().size());
+        assertTrue(a1c1.getAdded().contains(ac2.getObjectId()));
+        assertTrue(a1c1.getAdded().contains(ac3.getObjectId()));
+
+        assertEquals(1, a1c1.getRemoved().size());
+        assertTrue(a1c1.getRemoved().contains(ac1.getObjectId()));
+
+    }
+}
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java
deleted file mode 100644
index c8d3a5c..0000000
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_AllIT.java
+++ /dev/null
@@ -1,290 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-package org.apache.cayenne.commitlog;
-
-import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.ObjectId;
-import org.apache.cayenne.commitlog.db.Auditable1;
-import org.apache.cayenne.commitlog.db.AuditableChild1;
-import org.apache.cayenne.commitlog.model.AttributeChange;
-import org.apache.cayenne.commitlog.model.ChangeMap;
-import org.apache.cayenne.commitlog.model.ObjectChange;
-import org.apache.cayenne.commitlog.model.ObjectChangeType;
-import org.apache.cayenne.commitlog.model.ToManyRelationshipChange;
-import org.apache.cayenne.commitlog.model.ToOneRelationshipChange;
-import org.apache.cayenne.commitlog.unit.AuditableServerCase;
-import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
-import org.apache.cayenne.query.SelectById;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
-import org.mockito.stubbing.Answer;
-
-import java.sql.SQLException;
-
-import static org.junit.Assert.*;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.*;
-
-public class CommitLogFilter_AllIT extends AuditableServerCase {
-
-	protected ObjectContext context;
-	protected CommitLogListener mockListener;
-
-	@Override
-	protected ServerRuntimeBuilder configureCayenne() {
-		this.mockListener = mock(CommitLogListener.class);
-		return super.configureCayenne().addModule(CommitLogModule.extend().addListener(mockListener).module());
-	}
-
-	@Before
-	public void before() {
-		context = runtime.newContext();
-	}
-
-	@Test
-	public void testPostCommit_Insert() throws SQLException {
-
-		final Auditable1 a1 = context.newObject(Auditable1.class);
-		a1.setCharProperty1("yy");
-		final ObjectId preCommitId = a1.getObjectId();
-
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
-
-				assertSame(context, invocation.getArguments()[0]);
-
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(2, changes.getChanges().size());
-				assertEquals(1, changes.getUniqueChanges().size());
-
-				ObjectChange c = changes.getUniqueChanges().iterator().next();
-				assertNotNull(c);
-				assertEquals(ObjectChangeType.INSERT, c.getType());
-				assertEquals(1, c.getAttributeChanges().size());
-				assertEquals("yy", c.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName()).getNewValue());
-
-				assertNotEquals(preCommitId, a1.getObjectId());
-				assertEquals(preCommitId, c.getPreCommitId());
-				assertEquals(a1.getObjectId(), c.getPostCommitId());
-
-				return null;
-			}
-		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-
-		context.commitChanges();
-
-		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-	}
-
-	@Test
-	public void testPostCommit_Update() throws SQLException {
-
-		auditable1.insert(1, "xx");
-
-		final Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
-		a1.setCharProperty1("yy");
-
-		final ObjectId preCommitId = a1.getObjectId();
-
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
-
-				assertSame(context, invocation.getArguments()[0]);
-
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(1, changes.getUniqueChanges().size());
-
-				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
-				assertNotNull(c);
-				assertEquals(ObjectChangeType.UPDATE, c.getType());
-				assertEquals(1, c.getAttributeChanges().size());
-				AttributeChange pc = c.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName());
-				assertNotNull(pc);
-				assertEquals("xx", pc.getOldValue());
-				assertEquals("yy", pc.getNewValue());
-
-				assertEquals(preCommitId, a1.getObjectId());
-				assertEquals(preCommitId, c.getPreCommitId());
-				assertEquals(preCommitId, c.getPostCommitId());
-
-				return null;
-			}
-		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-
-		context.commitChanges();
-
-		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-	}
-
-	@Test
-	public void testPostCommit_Delete() throws SQLException {
-		auditable1.insert(1, "xx");
-
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
-
-				assertSame(context, invocation.getArguments()[0]);
-
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(1, changes.getUniqueChanges().size());
-
-				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
-				assertNotNull(c);
-				assertEquals(ObjectChangeType.DELETE, c.getType());
-				assertEquals(1, c.getAttributeChanges().size());
-				assertEquals("xx", c.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName()).getOldValue());
-				assertNull(c.getAttributeChanges().get(Auditable1.CHAR_PROPERTY1.getName()).getNewValue());
-
-				return null;
-			}
-		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-
-		Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
-		context.deleteObject(a1);
-		context.commitChanges();
-
-		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-	}
-
-	@Test
-	public void testPostCommit_UpdateToOne() throws SQLException {
-		auditable1.insert(1, "xx");
-		auditable1.insert(2, "yy");
-
-		auditableChild1.insert(1, 1, "cc1");
-		auditableChild1.insert(2, 2, "cc2");
-		auditableChild1.insert(3, null, "cc3");
-
-		final AuditableChild1 ac1 = SelectById.query(AuditableChild1.class, 1).selectOne(context);
-		final AuditableChild1 ac2 = SelectById.query(AuditableChild1.class, 2).selectOne(context);
-		final AuditableChild1 ac3 = SelectById.query(AuditableChild1.class, 3).selectOne(context);
-
-		final Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
-		final Auditable1 a2 = SelectById.query(Auditable1.class, 2).selectOne(context);
-
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
-
-				assertSame(context, invocation.getArguments()[0]);
-
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(4, changes.getUniqueChanges().size());
-
-				ObjectChange ac1c = changes.getChanges().get(
-						ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 1));
-				assertNotNull(ac1c);
-				assertEquals(ObjectChangeType.UPDATE, ac1c.getType());
-				ToOneRelationshipChange ac1c1 = ac1c.getToOneRelationshipChanges()
-						.get(AuditableChild1.PARENT.getName());
-				assertEquals(a1.getObjectId(), ac1c1.getOldValue());
-                assertNull(ac1c1.getNewValue());
-
-				ObjectChange ac2c = changes.getChanges().get(
-						ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 2));
-				assertNotNull(ac2c);
-				assertEquals(ObjectChangeType.UPDATE, ac2c.getType());
-				ToOneRelationshipChange ac2c1 = ac2c.getToOneRelationshipChanges()
-						.get(AuditableChild1.PARENT.getName());
-				assertEquals(a2.getObjectId(), ac2c1.getOldValue());
-				assertEquals(a1.getObjectId(), ac2c1.getNewValue());
-
-				ObjectChange ac3c = changes.getChanges().get(
-						ObjectId.of("AuditableChild1", AuditableChild1.ID_PK_COLUMN, 3));
-				assertNotNull(ac3c);
-				assertEquals(ObjectChangeType.UPDATE, ac3c.getType());
-				ToOneRelationshipChange ac3c1 = ac3c.getToOneRelationshipChanges()
-						.get(AuditableChild1.PARENT.getName());
-				assertEquals(null, ac3c1.getOldValue());
-				assertEquals(a1.getObjectId(), ac3c1.getNewValue());
-
-				return null;
-			}
-		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-
-		a1.removeFromChildren1(ac1);
-		a1.addToChildren1(ac2);
-		a1.addToChildren1(ac3);
-
-		context.commitChanges();
-
-		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-	}
-
-	@Test
-	public void testPostCommit_UpdateToMany() throws SQLException {
-		auditable1.insert(1, "xx");
-		auditableChild1.insert(1, 1, "cc1");
-		auditableChild1.insert(2, null, "cc2");
-		auditableChild1.insert(3, null, "cc3");
-
-		final AuditableChild1 ac1 = SelectById.query(AuditableChild1.class, 1).selectOne(context);
-		final AuditableChild1 ac2 = SelectById.query(AuditableChild1.class, 2).selectOne(context);
-		final AuditableChild1 ac3 = SelectById.query(AuditableChild1.class, 3).selectOne(context);
-
-		final Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
-
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
-
-				assertSame(context, invocation.getArguments()[0]);
-
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(4, changes.getUniqueChanges().size());
-
-				ObjectChange a1c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
-				assertNotNull(a1c);
-				assertEquals(ObjectChangeType.UPDATE, a1c.getType());
-				assertEquals(0, a1c.getAttributeChanges().size());
-
-				assertEquals(1, a1c.getToManyRelationshipChanges().size());
-
-				ToManyRelationshipChange a1c1 = a1c.getToManyRelationshipChanges().get(Auditable1.CHILDREN1.getName());
-				assertNotNull(a1c1);
-
-				assertEquals(2, a1c1.getAdded().size());
-				assertTrue(a1c1.getAdded().contains(ac2.getObjectId()));
-				assertTrue(a1c1.getAdded().contains(ac3.getObjectId()));
-
-				assertEquals(1, a1c1.getRemoved().size());
-				assertTrue(a1c1.getRemoved().contains(ac1.getObjectId()));
-
-				return null;
-			}
-		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-
-		a1.removeFromChildren1(ac1);
-		a1.addToChildren1(ac2);
-		a1.addToChildren1(ac3);
-
-		context.commitChanges();
-
-		verify(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
-	}
-}
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
index 6bfd28c..79d2e89 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_All_FlattenedIT.java
@@ -31,7 +31,6 @@
 import org.apache.cayenne.query.SelectById;
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
 import java.sql.SQLException;
@@ -63,65 +62,62 @@
 		e4.insert(12);
 		e34.insert(1, 11);
 
-		final E3 e3 = SelectById.query(E3.class, 1).selectOne(context);
-		final E4 e4_1 = SelectById.query(E4.class, 11).selectOne(context);
-		final E4 e4_2 = SelectById.query(E4.class, 12).selectOne(context);
+		E3 e3 = SelectById.query(E3.class, 1).selectOne(context);
+		E4 e4_1 = SelectById.query(E4.class, 11).selectOne(context);
+		E4 e4_2 = SelectById.query(E4.class, 12).selectOne(context);
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(3, changes.getUniqueChanges().size());
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNotNull(changes);
+			assertEquals(3, changes.getUniqueChanges().size());
 
-				ObjectChange e3c = changes.getChanges().get(ObjectId.of("E3", E3.ID_PK_COLUMN, 1));
-				assertNotNull(e3c);
-				assertEquals(ObjectChangeType.UPDATE, e3c.getType());
-				assertEquals(0, e3c.getAttributeChanges().size());
-				assertEquals(1, e3c.getToManyRelationshipChanges().size());
+			ObjectChange e3c = changes.getChanges().get(ObjectId.of("E3", E3.ID_PK_COLUMN, 1));
+			assertNotNull(e3c);
+			assertEquals(ObjectChangeType.UPDATE, e3c.getType());
+			assertEquals(0, e3c.getAttributeChanges().size());
+			assertEquals(1, e3c.getToManyRelationshipChanges().size());
 
-				ToManyRelationshipChange e3c1 = e3c.getToManyRelationshipChanges().get(E3.E4S.getName());
-				assertNotNull(e3c1);
+			ToManyRelationshipChange e3c1 = e3c.getToManyRelationshipChanges().get(E3.E4S.getName());
+			assertNotNull(e3c1);
 
-				assertEquals(1, e3c1.getAdded().size());
-				assertTrue(e3c1.getAdded().contains(e4_2.getObjectId()));
+			assertEquals(1, e3c1.getAdded().size());
+			assertTrue(e3c1.getAdded().contains(e4_2.getObjectId()));
 
-				assertEquals(1, e3c1.getRemoved().size());
-				assertTrue(e3c1.getRemoved().contains(e4_1.getObjectId()));
-				
-				ObjectChange e41c = changes.getChanges().get(ObjectId.of("E4", E4.ID_PK_COLUMN, 11));
-				assertNotNull(e41c);
-				assertEquals(ObjectChangeType.UPDATE, e41c.getType());
-				assertEquals(0, e41c.getAttributeChanges().size());
-				assertEquals(1, e41c.getToManyRelationshipChanges().size());
+			assertEquals(1, e3c1.getRemoved().size());
+			assertTrue(e3c1.getRemoved().contains(e4_1.getObjectId()));
 
-				ToManyRelationshipChange e41c1 = e41c.getToManyRelationshipChanges().get(E4.E3S.getName());
-				assertNotNull(e41c);
+			ObjectChange e41c = changes.getChanges().get(ObjectId.of("E4", E4.ID_PK_COLUMN, 11));
+			assertNotNull(e41c);
+			assertEquals(ObjectChangeType.UPDATE, e41c.getType());
+			assertEquals(0, e41c.getAttributeChanges().size());
+			assertEquals(1, e41c.getToManyRelationshipChanges().size());
 
-				assertEquals(0, e41c1.getAdded().size());
+			ToManyRelationshipChange e41c1 = e41c.getToManyRelationshipChanges().get(E4.E3S.getName());
+			assertNotNull(e41c);
 
-				assertEquals(1, e41c1.getRemoved().size());
-				assertTrue(e41c1.getRemoved().contains(e3.getObjectId()));
-				
-				ObjectChange e42c = changes.getChanges().get(ObjectId.of("E4", E4.ID_PK_COLUMN, 12));
-				assertNotNull(e42c);
-				assertEquals(ObjectChangeType.UPDATE, e42c.getType());
-				assertEquals(0, e42c.getAttributeChanges().size());
-				assertEquals(1, e42c.getToManyRelationshipChanges().size());
+			assertEquals(0, e41c1.getAdded().size());
 
-				ToManyRelationshipChange e42c1 = e42c.getToManyRelationshipChanges().get(E4.E3S.getName());
-				assertNotNull(e42c);
+			assertEquals(1, e41c1.getRemoved().size());
+			assertTrue(e41c1.getRemoved().contains(e3.getObjectId()));
 
-				assertEquals(0, e42c1.getRemoved().size());
+			ObjectChange e42c = changes.getChanges().get(ObjectId.of("E4", E4.ID_PK_COLUMN, 12));
+			assertNotNull(e42c);
+			assertEquals(ObjectChangeType.UPDATE, e42c.getType());
+			assertEquals(0, e42c.getAttributeChanges().size());
+			assertEquals(1, e42c.getToManyRelationshipChanges().size());
 
-				assertEquals(1, e42c1.getAdded().size());
-				assertTrue(e42c1.getAdded().contains(e3.getObjectId()));
+			ToManyRelationshipChange e42c1 = e42c.getToManyRelationshipChanges().get(E4.E3S.getName());
+			assertNotNull(e42c);
 
-				return null;
-			}
+			assertEquals(0, e42c1.getRemoved().size());
+
+			assertEquals(1, e42c1.getAdded().size());
+			assertTrue(e42c1.getAdded().contains(e3.getObjectId()));
+
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		e3.removeFromE4s(e4_1);
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java
index 5821f46..a19f24a 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_FilteredIT.java
@@ -35,7 +35,6 @@
 import org.apache.cayenne.query.SelectById;
 import org.junit.Before;
 import org.junit.Test;
-import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
 import java.sql.SQLException;
@@ -62,29 +61,26 @@
 	}
 
 	@Test
-	public void testPostCommit_Insert() throws SQLException {
+	public void testPostCommit_Insert() {
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(2, changes.getChanges().size());
-				assertEquals(1, changes.getUniqueChanges().size());
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNotNull(changes);
+			assertEquals(2, changes.getChanges().size());
+			assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getUniqueChanges().iterator().next();
-				assertNotNull(c);
-				assertEquals(ObjectChangeType.INSERT, c.getType());
-				assertEquals(1, c.getAttributeChanges().size());
+			ObjectChange c = changes.getUniqueChanges().iterator().next();
+			assertNotNull(c);
+			assertEquals(ObjectChangeType.INSERT, c.getType());
+			assertEquals(1, c.getAttributeChanges().size());
 
-				assertEquals(Confidential.getInstance(),
-						c.getAttributeChanges().get(Auditable2.CHAR_PROPERTY2.getName()).getNewValue());
+			assertEquals(Confidential.getInstance(),
+					c.getAttributeChanges().get(Auditable2.CHAR_PROPERTY2.getName()).getNewValue());
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		Auditable2 a1 = context.newObject(Auditable2.class);
@@ -99,27 +95,24 @@
 	public void testPostCommit_Update() throws SQLException {
 		auditable2.insert(1, "P1_1", "P2_1");
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(1, changes.getUniqueChanges().size());
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNotNull(changes);
+			assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable2", Auditable2.ID_PK_COLUMN, 1));
-				assertNotNull(c);
-				assertEquals(ObjectChangeType.UPDATE, c.getType());
-				assertEquals(1, c.getAttributeChanges().size());
-				AttributeChange pc = c.getAttributeChanges().get(Auditable2.CHAR_PROPERTY2.getName());
-				assertNotNull(pc);
-				assertEquals(Confidential.getInstance(), pc.getOldValue());
-				assertEquals(Confidential.getInstance(), pc.getNewValue());
+			ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable2", Auditable2.ID_PK_COLUMN, 1));
+			assertNotNull(c);
+			assertEquals(ObjectChangeType.UPDATE, c.getType());
+			assertEquals(1, c.getAttributeChanges().size());
+			AttributeChange pc = c.getAttributeChanges().get(Auditable2.CHAR_PROPERTY2.getName());
+			assertNotNull(pc);
+			assertEquals(Confidential.getInstance(), pc.getOldValue());
+			assertEquals(Confidential.getInstance(), pc.getNewValue());
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		Auditable2 a1 = SelectById.query(Auditable2.class, 1).selectOne(context);
@@ -134,25 +127,22 @@
 	public void testPostCommit_Delete() throws SQLException {
 		auditable2.insert(1, "P1_1", "P2_1");
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(1, changes.getUniqueChanges().size());
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNotNull(changes);
+			assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable2", Auditable2.ID_PK_COLUMN, 1));
-				assertNotNull(c);
-				assertEquals(ObjectChangeType.DELETE, c.getType());
-				assertEquals(1, c.getAttributeChanges().size());
-				assertEquals(Confidential.getInstance(),
-						c.getAttributeChanges().get(Auditable2.CHAR_PROPERTY2.getName()).getOldValue());
+			ObjectChange c = changes.getChanges().get(ObjectId.of("Auditable2", Auditable2.ID_PK_COLUMN, 1));
+			assertNotNull(c);
+			assertEquals(ObjectChangeType.DELETE, c.getType());
+			assertEquals(1, c.getAttributeChanges().size());
+			assertEquals(Confidential.getInstance(),
+					c.getAttributeChanges().get(Auditable2.CHAR_PROPERTY2.getName()).getOldValue());
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		Auditable2 a1 = SelectById.query(Auditable2.class, 1).selectOne(context);
@@ -175,35 +165,32 @@
 
 		final Auditable1 a1 = SelectById.query(Auditable1.class, 1).selectOne(context);
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNotNull(changes);
-				assertEquals(1, changes.getUniqueChanges().size());
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNotNull(changes);
+			assertEquals(1, changes.getUniqueChanges().size());
 
-				ObjectChange a1c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
-				assertNotNull(a1c);
-				assertEquals(ObjectChangeType.UPDATE, a1c.getType());
-				assertEquals(0, a1c.getAttributeChanges().size());
+			ObjectChange a1c = changes.getChanges().get(ObjectId.of("Auditable1", Auditable1.ID_PK_COLUMN, 1));
+			assertNotNull(a1c);
+			assertEquals(ObjectChangeType.UPDATE, a1c.getType());
+			assertEquals(0, a1c.getAttributeChanges().size());
 
-				assertEquals(1, a1c.getToManyRelationshipChanges().size());
+			assertEquals(1, a1c.getToManyRelationshipChanges().size());
 
-				ToManyRelationshipChange a1c1 = a1c.getToManyRelationshipChanges().get(Auditable1.CHILDREN1.getName());
-				assertNotNull(a1c1);
+			ToManyRelationshipChange a1c1 = a1c.getToManyRelationshipChanges().get(Auditable1.CHILDREN1.getName());
+			assertNotNull(a1c1);
 
-				assertEquals(2, a1c1.getAdded().size());
-				assertTrue(a1c1.getAdded().contains(ac2.getObjectId()));
-				assertTrue(a1c1.getAdded().contains(ac3.getObjectId()));
+			assertEquals(2, a1c1.getAdded().size());
+			assertTrue(a1c1.getAdded().contains(ac2.getObjectId()));
+			assertTrue(a1c1.getAdded().contains(ac3.getObjectId()));
 
-				assertEquals(1, a1c1.getRemoved().size());
-				assertTrue(a1c1.getRemoved().contains(ac1.getObjectId()));
+			assertEquals(1, a1c1.getRemoved().size());
+			assertTrue(a1c1.getRemoved().contains(ac1.getObjectId()));
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		a1.removeFromChildren1(ac1);
@@ -222,17 +209,14 @@
 
 		final Auditable3 a3 = SelectById.query(Auditable3.class, 1).selectOne(context);
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNull(changes.getChanges().get(ObjectId.of("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNull(changes.getChanges().get(ObjectId.of("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		a3.setCharProperty1("33");
@@ -254,17 +238,14 @@
 		final Auditable4 a41 = SelectById.query(Auditable4.class, 11).selectOne(context);
 		final Auditable4 a42 = SelectById.query(Auditable4.class, 12).selectOne(context);
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNull(changes.getChanges().get(ObjectId.of("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNull(changes.getChanges().get(ObjectId.of("Auditable3", Auditable3.ID_PK_COLUMN, 1)));
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		a3.removeFromAuditable4s(a41);
@@ -286,17 +267,14 @@
 
 		final Auditable4 a4 = SelectById.query(Auditable4.class, 11).selectOne(context);
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertSame(context, invocation.getArguments()[0]);
+			assertSame(context, invocation.getArguments()[0]);
 
-				ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
-				assertNull(changes.getChanges().get(ObjectId.of("Auditable4", Auditable4.ID_PK_COLUMN, 11)));
+			ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
+			assertNull(changes.getChanges().get(ObjectId.of("Auditable4", Auditable4.ID_PK_COLUMN, 11)));
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		a4.setAuditable3(a32);
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_ListenerInducedChangesIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_ListenerInducedChangesIT.java
index e84f6dd..7a401d1 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_ListenerInducedChangesIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_ListenerInducedChangesIT.java
@@ -38,7 +38,6 @@
 
 import java.sql.SQLException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 
@@ -66,7 +65,7 @@
 	}
 
 	@Test
-	public void testPostCommit_Insert() throws SQLException {
+	public void testPostCommit_Insert() {
 
 		final InsertListener listener = new InsertListener();
 		runtime.getDataDomain().addListener(listener);
@@ -74,29 +73,26 @@
 		final Auditable1 a1 = context.newObject(Auditable1.class);
 		a1.setCharProperty1("yy");
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertNotNull(listener.c);
+			assertNotNull(listener.c);
 
-				List<ObjectChange> sortedChanges = sortedChanges(invocation);
+			List<ObjectChange> sortedChanges = sortedChanges(invocation);
 
-				assertEquals(2, sortedChanges.size());
+			assertEquals(2, sortedChanges.size());
 
-				assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
-				assertEquals(ObjectChangeType.INSERT, sortedChanges.get(0).getType());
+			assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
+			assertEquals(ObjectChangeType.INSERT, sortedChanges.get(0).getType());
 
-				assertEquals(listener.c.getObjectId(), sortedChanges.get(1).getPostCommitId());
-				assertEquals(ObjectChangeType.INSERT, sortedChanges.get(1).getType());
+			assertEquals(listener.c.getObjectId(), sortedChanges.get(1).getPostCommitId());
+			assertEquals(ObjectChangeType.INSERT, sortedChanges.get(1).getType());
 
-				AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
-						.get(AuditableChild1.CHAR_PROPERTY1.getName());
-				assertNotNull(listenerInducedChange);
-				assertEquals("c1", listenerInducedChange.getNewValue());
+			AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
+					.get(AuditableChild1.CHAR_PROPERTY1.getName());
+			assertNotNull(listenerInducedChange);
+			assertEquals("c1", listenerInducedChange.getNewValue());
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		context.commitChanges();
@@ -117,30 +113,27 @@
 				.selectFirst(context);
 		a1.setCharProperty1("zz");
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertNotNull(listener.toDelete);
-				assertEquals(1, listener.toDelete.size());
+			assertNotNull(listener.toDelete);
+			assertEquals(1, listener.toDelete.size());
 
-				List<ObjectChange> sortedChanges = sortedChanges(invocation);
+			List<ObjectChange> sortedChanges = sortedChanges(invocation);
 
-				assertEquals(2, sortedChanges.size());
+			assertEquals(2, sortedChanges.size());
 
-				assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(0).getType());
-				assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
+			assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(0).getType());
+			assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
 
-				assertEquals(ObjectChangeType.DELETE, sortedChanges.get(1).getType());
-				assertEquals(listener.toDelete.get(0).getObjectId(), sortedChanges.get(1).getPostCommitId());
+			assertEquals(ObjectChangeType.DELETE, sortedChanges.get(1).getType());
+			assertEquals(listener.toDelete.get(0).getObjectId(), sortedChanges.get(1).getPostCommitId());
 
-				AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
-						.get(AuditableChild1.CHAR_PROPERTY1.getName());
-				assertNotNull(listenerInducedChange);
-				assertEquals("yyc", listenerInducedChange.getOldValue());
+			AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
+					.get(AuditableChild1.CHAR_PROPERTY1.getName());
+			assertNotNull(listenerInducedChange);
+			assertEquals("yyc", listenerInducedChange.getOldValue());
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		context.commitChanges();
@@ -161,31 +154,28 @@
 				.selectFirst(context);
 		a1.setCharProperty1("zz");
 
-		doAnswer(new Answer<Object>() {
-			@Override
-			public Object answer(InvocationOnMock invocation) throws Throwable {
+		doAnswer((Answer<Object>) invocation -> {
 
-				assertNotNull(listener.toUpdate);
-				assertEquals(1, listener.toUpdate.size());
+			assertNotNull(listener.toUpdate);
+			assertEquals(1, listener.toUpdate.size());
 
-				List<ObjectChange> sortedChanges = sortedChanges(invocation);
+			List<ObjectChange> sortedChanges = sortedChanges(invocation);
 
-				assertEquals(2, sortedChanges.size());
+			assertEquals(2, sortedChanges.size());
 
-				assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(0).getType());
-				assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
+			assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(0).getType());
+			assertEquals(a1.getObjectId(), sortedChanges.get(0).getPostCommitId());
 
-				assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(1).getType());
-				assertEquals(listener.toUpdate.get(0).getObjectId(), sortedChanges.get(1).getPostCommitId());
+			assertEquals(ObjectChangeType.UPDATE, sortedChanges.get(1).getType());
+			assertEquals(listener.toUpdate.get(0).getObjectId(), sortedChanges.get(1).getPostCommitId());
 
-				AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
-						.get(AuditableChild1.CHAR_PROPERTY1.getName());
-				assertNotNull(listenerInducedChange);
-				assertEquals("yyc", listenerInducedChange.getOldValue());
-				assertEquals("yyc_", listenerInducedChange.getNewValue());
+			AttributeChange listenerInducedChange = sortedChanges.get(1).getAttributeChanges()
+					.get(AuditableChild1.CHAR_PROPERTY1.getName());
+			assertNotNull(listenerInducedChange);
+			assertEquals("yyc", listenerInducedChange.getOldValue());
+			assertEquals("yyc_", listenerInducedChange.getNewValue());
 
-				return null;
-			}
+			return null;
 		}).when(mockListener).onPostCommit(any(ObjectContext.class), any(ChangeMap.class));
 
 		context.commitChanges();
@@ -199,11 +189,7 @@
 		ChangeMap changes = (ChangeMap) invocation.getArguments()[1];
 
 		List<ObjectChange> sortedChanges = new ArrayList<>(changes.getUniqueChanges());
-		Collections.sort(sortedChanges, new Comparator<ObjectChange>() {
-			public int compare(ObjectChange o1, ObjectChange o2) {
-				return o1.getPostCommitId().getEntityName().compareTo(o2.getPostCommitId().getEntityName());
-			}
-		});
+		sortedChanges.sort(Comparator.comparing(o -> o.getPostCommitId().getEntityName()));
 
 		return sortedChanges;
 	}
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_OutsideTxIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_OutsideTxIT.java
index 2a7cdd5..45513ed 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_OutsideTxIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_OutsideTxIT.java
@@ -21,7 +21,6 @@
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.commitlog.db.AuditLog;
 import org.apache.cayenne.commitlog.db.Auditable2;
-import org.apache.cayenne.commitlog.model.ChangeMap;
 import org.apache.cayenne.commitlog.model.ObjectChange;
 import org.apache.cayenne.commitlog.unit.AuditableServerCase;
 import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
@@ -42,19 +41,15 @@
 
 	@Override
 	protected ServerRuntimeBuilder configureCayenne() {
-		this.listener = new CommitLogListener() {
+		this.listener = (originatingContext, changes) -> {
 
-			@Override
-			public void onPostCommit(ObjectContext originatingContext, ChangeMap changes) {
+			// assert we are inside transaction
+			assertNull(BaseTransaction.getThreadTransaction());
 
-				// assert we are inside transaction
-				assertNull(BaseTransaction.getThreadTransaction());
-
-				for (ObjectChange c : changes.getUniqueChanges()) {
-					AuditLog log = runtime.newContext().newObject(AuditLog.class);
-					log.setLog("DONE: " + c.getPostCommitId());
-					log.getObjectContext().commitChanges();
-				}
+			for (ObjectChange c : changes.getUniqueChanges()) {
+				AuditLog log = runtime.newContext().newObject(AuditLog.class);
+				log.setLog("DONE: " + c.getPostCommitId());
+				log.getObjectContext().commitChanges();
 			}
 		};
 		return super.configureCayenne().addModule(
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_TxIT.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_TxIT.java
index d76963d..63a9b6e 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_TxIT.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/CommitLogFilter_TxIT.java
@@ -21,7 +21,6 @@
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.commitlog.db.AuditLog;
 import org.apache.cayenne.commitlog.db.Auditable2;
-import org.apache.cayenne.commitlog.model.ChangeMap;
 import org.apache.cayenne.commitlog.model.ObjectChange;
 import org.apache.cayenne.commitlog.unit.AuditableServerCase;
 import org.apache.cayenne.configuration.server.ServerRuntimeBuilder;
@@ -42,19 +41,15 @@
 
 	@Override
 	protected ServerRuntimeBuilder configureCayenne() {
-		this.listener = new CommitLogListener() {
+		this.listener = (originatingContext, changes) -> {
 
-			@Override
-			public void onPostCommit(ObjectContext originatingContext, ChangeMap changes) {
+			// assert we are inside transaction
+			assertNotNull(BaseTransaction.getThreadTransaction());
 
-				// assert we are inside transaction
-				assertNotNull(BaseTransaction.getThreadTransaction());
-
-				for (ObjectChange c : changes.getUniqueChanges()) {
-					AuditLog log = runtime.newContext().newObject(AuditLog.class);
-					log.setLog("DONE: " + c.getPostCommitId());
-					log.getObjectContext().commitChanges();
-				}
+			for (ObjectChange c : changes.getUniqueChanges()) {
+				AuditLog log = runtime.newContext().newObject(AuditLog.class);
+				log.setLog("DONE: " + c.getPostCommitId());
+				log.getObjectContext().commitChanges();
 			}
 		};
 		return super.configureCayenne().addModule(
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/AuditableChild1x.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/AuditableChild1x.java
new file mode 100644
index 0000000..baeae14
--- /dev/null
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/AuditableChild1x.java
@@ -0,0 +1,27 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.commitlog.db;
+
+import org.apache.cayenne.commitlog.db.auto._AuditableChild1x;
+
+public class AuditableChild1x extends _AuditableChild1x {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/auto/_AuditableChild1x.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/auto/_AuditableChild1x.java
new file mode 100644
index 0000000..6889a4b
--- /dev/null
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/db/auto/_AuditableChild1x.java
@@ -0,0 +1,106 @@
+package org.apache.cayenne.commitlog.db.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.commitlog.db.Auditable1;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _AuditableChild1x was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _AuditableChild1x extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final StringProperty<String> CHAR_PROPERTY1 = PropertyFactory.createString("charProperty1", String.class);
+    public static final EntityProperty<Auditable1> PARENT = PropertyFactory.createEntity("parent", Auditable1.class);
+
+    protected String charProperty1;
+
+    protected Object parent;
+
+    public void setCharProperty1(String charProperty1) {
+        beforePropertyWrite("charProperty1", this.charProperty1, charProperty1);
+        this.charProperty1 = charProperty1;
+    }
+
+    public String getCharProperty1() {
+        beforePropertyRead("charProperty1");
+        return this.charProperty1;
+    }
+
+    public void setParent(Auditable1 parent) {
+        setToOneTarget("parent", parent, true);
+    }
+
+    public Auditable1 getParent() {
+        return (Auditable1)readProperty("parent");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "charProperty1":
+                return this.charProperty1;
+            case "parent":
+                return this.parent;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "charProperty1":
+                this.charProperty1 = (String)val;
+                break;
+            case "parent":
+                this.parent = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.charProperty1);
+        out.writeObject(this.parent);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.charProperty1 = (String)in.readObject();
+        this.parent = in.readObject();
+    }
+
+}
diff --git a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java
index 570a54f..67ee30d 100644
--- a/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java
+++ b/cayenne-commitlog/src/test/java/org/apache/cayenne/commitlog/unit/AuditableServerCase.java
@@ -34,6 +34,7 @@
 
 	protected TableHelper auditable1;
 	protected TableHelper auditableChild1;
+	protected TableHelper auditableChild1x;
 
 	protected TableHelper auditable2;
 	protected TableHelper auditableChild3;
@@ -55,6 +56,8 @@
 
 		this.auditableChild1 = new TableHelper(dbHelper, "AUDITABLE_CHILD1").setColumns("ID", "AUDITABLE1_ID",
 				"CHAR_PROPERTY1");
+		this.auditableChild1x = new TableHelper(dbHelper, "AUDITABLE_CHILD1X").setColumns("ID", "AUDITABLE1_ID",
+				"CHAR_PROPERTY1");
 
 		this.auditable2 = new TableHelper(dbHelper, "AUDITABLE2").setColumns("ID", "CHAR_PROPERTY1", "CHAR_PROPERTY2");
 
@@ -66,6 +69,7 @@
 				"AUDITABLE3_ID");
 
 		this.auditableChild1.deleteAll();
+		this.auditableChild1x.deleteAll();
 		this.auditable1.deleteAll();
 		this.auditableChild3.deleteAll();
 		this.auditable2.deleteAll();
diff --git a/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml b/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml
index 4144737..0858b03 100644
--- a/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml
+++ b/cayenne-commitlog/src/test/resources/cayenne-lifecycle.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <domain xmlns="http://cayenne.apache.org/schema/10/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
 	 project-version="10">
 	<map name="lifecycle-map"/>
 	<node name="lifecycle-db"
diff --git a/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml b/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml
index db86a62..fc3bb20 100644
--- a/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml
+++ b/cayenne-commitlog/src/test/resources/lifecycle-map.map.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
 	 project-version="10">
 	<property name="defaultPackage" value="org.apache.cayenne.commitlog.db"/>
 	<db-entity name="AUDITABLE1">
@@ -29,6 +29,11 @@
 		<db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/>
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
+	<db-entity name="AUDITABLE_CHILD1X">
+		<db-attribute name="AUDITABLE1_ID" type="INTEGER"/>
+		<db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	</db-entity>
 	<db-entity name="AUDITABLE_CHILD3">
 		<db-attribute name="AUDITABLE2_ID" type="INTEGER"/>
 		<db-attribute name="CHAR_PROPERTY1" type="VARCHAR" length="200"/>
@@ -76,6 +81,9 @@
 	<obj-entity name="AuditableChild1" className="org.apache.cayenne.commitlog.db.AuditableChild1" dbEntityName="AUDITABLE_CHILD1">
 		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
 	</obj-entity>
+	<obj-entity name="AuditableChild1x" className="org.apache.cayenne.commitlog.db.AuditableChild1x" dbEntityName="AUDITABLE_CHILD1X">
+		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
+	</obj-entity>
 	<obj-entity name="AuditableChild3" className="org.apache.cayenne.commitlog.db.AuditableChild3" dbEntityName="AUDITABLE_CHILD3">
 		<obj-attribute name="charProperty1" type="java.lang.String" db-attribute-path="CHAR_PROPERTY1"/>
 		<obj-attribute name="charProperty2" type="java.lang.String" db-attribute-path="CHAR_PROPERTY2"/>
@@ -87,6 +95,9 @@
 	<db-relationship name="children1" source="AUDITABLE1" target="AUDITABLE_CHILD1" toMany="true">
 		<db-attribute-pair source="ID" target="AUDITABLE1_ID"/>
 	</db-relationship>
+	<db-relationship name="children1x" source="AUDITABLE1" target="AUDITABLE_CHILD1X" toMany="true">
+		<db-attribute-pair source="ID" target="AUDITABLE1_ID"/>
+	</db-relationship>
 	<db-relationship name="children" source="AUDITABLE2" target="AUDITABLE_CHILD3" toMany="true">
 		<db-attribute-pair source="ID" target="AUDITABLE2_ID"/>
 	</db-relationship>
@@ -99,6 +110,9 @@
 	<db-relationship name="parent" source="AUDITABLE_CHILD1" target="AUDITABLE1">
 		<db-attribute-pair source="AUDITABLE1_ID" target="ID"/>
 	</db-relationship>
+	<db-relationship name="parent" source="AUDITABLE_CHILD1X" target="AUDITABLE1">
+		<db-attribute-pair source="AUDITABLE1_ID" target="ID"/>
+	</db-relationship>
 	<db-relationship name="parent" source="AUDITABLE_CHILD3" target="AUDITABLE2">
 		<db-attribute-pair source="AUDITABLE2_ID" target="ID"/>
 	</db-relationship>
@@ -119,7 +133,21 @@
 	<obj-relationship name="auditable4s" source="Auditable3" target="Auditable4" deleteRule="Deny" db-relationship-path="auditable4s"/>
 	<obj-relationship name="auditable3" source="Auditable4" target="Auditable3" deleteRule="Nullify" db-relationship-path="auditable3"/>
 	<obj-relationship name="parent" source="AuditableChild1" target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/>
+	<obj-relationship name="parent" source="AuditableChild1x" target="Auditable1" deleteRule="Nullify" db-relationship-path="parent"/>
 	<obj-relationship name="parent" source="AuditableChild3" target="Auditable2" deleteRule="Nullify" db-relationship-path="parent"/>
 	<obj-relationship name="e4s" source="E3" target="E4" deleteRule="Deny" db-relationship-path="e34s.e4"/>
 	<obj-relationship name="e3s" source="E4" target="E3" deleteRule="Deny" db-relationship-path="e34s.e3"/>
+	<cgen xmlns="http://cayenne.apache.org/schema/10/cgen">
+		<destDir>../java</destDir>
+		<mode>entity</mode>
+		<template>templates/v4_1/subclass.vm</template>
+		<superTemplate>templates/v4_1/superclass.vm</superTemplate>
+		<outputPattern>*.java</outputPattern>
+		<makePairs>true</makePairs>
+		<usePkgPath>true</usePkgPath>
+		<overwrite>false</overwrite>
+		<createPropertyNames>false</createPropertyNames>
+		<createPKProperties>false</createPKProperties>
+		<client>false</client>
+	</cgen>
 </data-map>
diff --git a/cayenne-crypto/pom.xml b/cayenne-crypto/pom.xml
index cfe954d..9469ab5 100644
--- a/cayenne-crypto/pom.xml
+++ b/cayenne-crypto/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<artifactId>cayenne-parent</artifactId>
 		<groupId>org.apache.cayenne</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<artifactId>cayenne-crypto</artifactId>
 	<name>cayenne-crypto: Cayenne Cryptography Extensions</name>
diff --git a/cayenne-dbcp2/pom.xml b/cayenne-dbcp2/pom.xml
index 9b091bf..4cb3837 100644
--- a/cayenne-dbcp2/pom.xml
+++ b/cayenne-dbcp2/pom.xml
@@ -14,7 +14,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <artifactId>cayenne-dbcp2</artifactId>
     <name>cayenne-dbcp2: Cayenne DBCP2 Extension</name>
diff --git a/cayenne-dbsync/pom.xml b/cayenne-dbsync/pom.xml
index 995d747..9df483c 100644
--- a/cayenne-dbsync/pom.xml
+++ b/cayenne-dbsync/pom.xml
@@ -22,7 +22,7 @@
 	<parent>
 		<artifactId>cayenne-parent</artifactId>
 		<groupId>org.apache.cayenne</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<modelVersion>4.0.0</modelVersion>
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java
index 156d516..1df920d 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbAttributeMerger.java
@@ -30,7 +30,7 @@
 import org.apache.cayenne.dbsync.merge.token.MergerToken;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 
 class DbAttributeMerger extends AbstractMerger<DbEntity, DbAttribute> {
 
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java
index 541a817..114e8e1 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/DbEntityMerger.java
@@ -34,7 +34,7 @@
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 
 class DbEntityMerger extends AbstractMerger<DataMap, DbEntity> {
 
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
index 3e60891..93c9a25 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
@@ -20,7 +20,10 @@
 package org.apache.cayenne.dbsync.merge.context;
 
 import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.value.Json;
+import org.apache.cayenne.value.Wkt;
 import org.apache.cayenne.dbsync.filter.NameFilter;
+import org.apache.cayenne.dbsync.model.DetectedDbAttribute;
 import org.apache.cayenne.dbsync.naming.NameBuilder;
 import org.apache.cayenne.dbsync.naming.ObjectNameGenerator;
 import org.apache.cayenne.map.DataMap;
@@ -60,6 +63,11 @@
      */
     private static final Map<Integer, String> SQL_TYPE_TO_JAVA8_TYPE = new HashMap<>();
 
+    /**
+     * Type conversion for the most spread DB data types that are out of the standard
+     */
+    private static final Map<String, String> SQL_ADDITIONAL_TYPES_TO_JAVA_TYPE = new HashMap<>();
+
     static {
         CLASS_TO_PRIMITIVE.put(Byte.class.getName(), "byte");
         CLASS_TO_PRIMITIVE.put(Long.class.getName(), "long");
@@ -72,6 +80,9 @@
         SQL_TYPE_TO_JAVA8_TYPE.put(Types.DATE,      "java.time.LocalDate");
         SQL_TYPE_TO_JAVA8_TYPE.put(Types.TIME,      "java.time.LocalTime");
         SQL_TYPE_TO_JAVA8_TYPE.put(Types.TIMESTAMP, "java.time.LocalDateTime");
+
+        SQL_ADDITIONAL_TYPES_TO_JAVA_TYPE.put("json",       Json.class.getName());
+        SQL_ADDITIONAL_TYPES_TO_JAVA_TYPE.put("geometry",   Wkt.class.getName());
     }
 
     private ObjectNameGenerator nameGenerator;
@@ -285,6 +296,19 @@
         if(!usingJava7Types && (java8Type = SQL_TYPE_TO_JAVA8_TYPE.get(dbAttribute.getType())) != null) {
             return java8Type;
         }
+
+        // Check additional common DB types, like 'json' or 'geometry'
+        if(dbAttribute.getType() == Types.OTHER) {
+            if(dbAttribute instanceof DetectedDbAttribute) {
+                DetectedDbAttribute detectedDbAttribute = (DetectedDbAttribute)dbAttribute;
+                String jdbcTypeName = detectedDbAttribute.getJdbcTypeName();
+                String type = SQL_ADDITIONAL_TYPES_TO_JAVA_TYPE.get(jdbcTypeName.toLowerCase());
+                if(type != null) {
+                    return type;
+                }
+            }
+        }
+
         String type = TypesMapping.getJavaBySqlType(dbAttribute.getType());
         String primitiveType;
         if (usingPrimitives && (primitiveType = CLASS_TO_PRIMITIVE.get(type)) != null) {
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/model/DetectedDbAttribute.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/model/DetectedDbAttribute.java
new file mode 100644
index 0000000..93822c5
--- /dev/null
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/model/DetectedDbAttribute.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.dbsync.model;
+
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * Wrapper for DbAttribute that holds additional JDBC metadata.
+ *
+ * @since 4.2
+ */
+public class DetectedDbAttribute extends DbAttribute {
+
+    private String jdbcTypeName;
+
+    public DetectedDbAttribute(DbAttribute attr) {
+        setName(attr.getName());
+        setType(attr.getType());
+        setMandatory(attr.isMandatory());
+        setMaxLength(attr.getMaxLength());
+        setScale(attr.getScale());
+        setAttributePrecision(attr.getAttributePrecision());
+        setPrimaryKey(attr.isPrimaryKey());
+        setGenerated(attr.isGenerated());
+    }
+
+    public void setJdbcTypeName(String jdbcTypeName) {
+        this.jdbcTypeName = jdbcTypeName;
+    }
+
+    public String getJdbcTypeName() {
+        return jdbcTypeName;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/DetectedDbEntity.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/model/DetectedDbEntity.java
similarity index 92%
rename from cayenne-server/src/main/java/org/apache/cayenne/map/DetectedDbEntity.java
rename to cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/model/DetectedDbEntity.java
index 868096a..7fad180 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/DetectedDbEntity.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/model/DetectedDbEntity.java
@@ -16,10 +16,13 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.map;
+package org.apache.cayenne.dbsync.model;
+
+import org.apache.cayenne.map.DbEntity;
 
 /**
  * A {@link DbEntity} subclass used to hold extra JDBC metadata.
+ * @since 4.2 moved from org.apache.cayenne.map package
  */
 public class DetectedDbEntity extends DbEntity {
 
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
index ae963ef..b47debc 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/configuration/ToolsModule.java
@@ -71,6 +71,8 @@
 import org.apache.cayenne.log.Slf4jJdbcEventLogger;
 import org.apache.cayenne.project.ProjectModule;
 import org.apache.cayenne.project.extension.ExtensionAwareHandlerFactory;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
+import org.apache.cayenne.reflect.generic.DefaultValueComparisonStrategyFactory;
 import org.apache.cayenne.resource.ClassLoaderResourceLocator;
 import org.apache.cayenne.resource.ResourceLocator;
 import org.slf4j.Logger;
@@ -104,6 +106,7 @@
         ServerModule.contributeValueObjectTypes(binder);
 
         binder.bind(ValueObjectTypeRegistry.class).to(DefaultValueObjectTypeRegistry.class);
+        binder.bind(ValueComparisonStrategyFactory.class).to(DefaultValueComparisonStrategyFactory.class);
 
         binder.bind(ClassLoaderManager.class).to(DefaultClassLoaderManager.class);
         binder.bind(AdhocObjectFactory.class).to(DefaultAdhocObjectFactory.class);
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoader.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoader.java
index d24f19b..9ca2d05 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoader.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/AttributeLoader.java
@@ -26,6 +26,7 @@
 
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.dbsync.model.DetectedDbAttribute;
 import org.apache.cayenne.dbsync.reverse.filters.CatalogFilter;
 import org.apache.cayenne.dbsync.reverse.filters.PatternFilter;
 import org.apache.cayenne.dbsync.reverse.filters.SchemaFilter;
@@ -119,19 +120,23 @@
         }
 
         // create attribute delegating this task to adapter
-        DbAttribute attr = adapter.buildAttribute(
+        DetectedDbAttribute detectedDbAttribute = new DetectedDbAttribute(adapter.buildAttribute(
                 rs.getString("COLUMN_NAME"),
                 rs.getString("TYPE_NAME"),
                 columnType,
                 rs.getInt("COLUMN_SIZE"),
                 decimalDigits,
-                rs.getBoolean("NULLABLE"));
+                rs.getBoolean("NULLABLE")));
+
+        // store raw type name
+        detectedDbAttribute.setJdbcTypeName(rs.getString("TYPE_NAME"));
 
         if (supportAutoIncrement) {
             if ("YES".equals(rs.getString("IS_AUTOINCREMENT"))) {
-                attr.setGenerated(true);
+                detectedDbAttribute.setGenerated(true);
             }
         }
-        return attr;
+
+        return detectedDbAttribute;
     }
 }
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
index 1ab1e12..c6513d7 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/DbLoadDataStore.java
@@ -26,7 +26,7 @@
 
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 import org.apache.cayenne.map.Procedure;
 
 /**
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/EntityLoader.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/EntityLoader.java
index f878358..7124078 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/EntityLoader.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/EntityLoader.java
@@ -29,7 +29,7 @@
 import org.apache.cayenne.dbsync.reverse.filters.CatalogFilter;
 import org.apache.cayenne.dbsync.reverse.filters.SchemaFilter;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/PrimaryKeyLoader.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/PrimaryKeyLoader.java
index 6feee97..36938ef 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/PrimaryKeyLoader.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbload/PrimaryKeyLoader.java
@@ -25,7 +25,7 @@
 
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/BaseLoaderIT.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/BaseLoaderIT.java
index 74197dd..2a775ba 100644
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/BaseLoaderIT.java
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/reverse/dbload/BaseLoaderIT.java
@@ -25,7 +25,7 @@
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 import org.apache.cayenne.unit.UnitDbAdapter;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
diff --git a/cayenne-di/pom.xml b/cayenne-di/pom.xml
index 5586e3b..8c17842 100644
--- a/cayenne-di/pom.xml
+++ b/cayenne-di/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<artifactId>cayenne-di</artifactId>
 	<packaging>jar</packaging>
diff --git a/cayenne-gradle-plugin/pom.xml b/cayenne-gradle-plugin/pom.xml
index f9aec36e..3ec854a 100644
--- a/cayenne-gradle-plugin/pom.xml
+++ b/cayenne-gradle-plugin/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>
diff --git a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
index a0842b3..d3eee61 100644
--- a/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
+++ b/cayenne-gradle-plugin/src/main/java/org/apache/cayenne/tools/CgenTask.java
@@ -142,6 +142,14 @@
     @Optional
     private Boolean createPKProperties;
 
+    /**
+     * Optional path (classpath or filesystem) to external velocity tool configuration file
+     * @since 4.2
+     */
+    @Input
+    @Optional
+    private String externalToolConfig;
+
     private String destDirName;
 
     private DataChannelMetaData metaData;
@@ -253,6 +261,7 @@
         cgenConfiguration.setQueryTemplate(queryTemplate != null ? queryTemplate : cgenConfiguration.getQueryTemplate());
         cgenConfiguration.setQuerySuperTemplate(querySuperTemplate != null ? querySuperTemplate : cgenConfiguration.getQuerySuperTemplate());
         cgenConfiguration.setCreatePKProperties(createPKProperties != null ? createPKProperties : cgenConfiguration.isCreatePKProperties());
+        cgenConfiguration.setExternalToolConfig(externalToolConfig != null ? externalToolConfig : cgenConfiguration.getExternalToolConfig());
         if(!cgenConfiguration.isMakePairs()) {
             if(template == null) {
                 cgenConfiguration.setTemplate(cgenConfiguration.isClient() ? ClientClassGenerationAction.SINGLE_CLASS_TEMPLATE : ClassGenerationAction.SINGLE_CLASS_TEMPLATE);
@@ -279,7 +288,7 @@
                 makePairs != null || mode != null || outputPattern != null || overwrite != null || superPkg != null ||
                 superTemplate != null || template != null || embeddableTemplate != null || embeddableSuperTemplate != null ||
                 usePkgPath != null || createPropertyNames != null || force || queryTemplate != null ||
-                querySuperTemplate != null || createPKProperties != null;
+                querySuperTemplate != null || createPKProperties != null || externalToolConfig != null;
     }
 
     @OutputDirectory
@@ -581,5 +590,12 @@
     public void createPKProperties(boolean createPKProperties) {
         setCreatePKProperties(createPKProperties);
     }
+    
+    public void setExternalToolConfig(String externalToolConfig) {
+    	this.externalToolConfig = externalToolConfig;
+    }
 
+    public void externalToolConfig(String externalToolConfig) {
+    	setExternalToolConfig(externalToolConfig);
+    }
 }
\ No newline at end of file
diff --git a/cayenne-gradle-plugin/src/test/java/org/apache/cayenne/tools/CgenTaskTest.java b/cayenne-gradle-plugin/src/test/java/org/apache/cayenne/tools/CgenTaskTest.java
index 00b1484..9269a65 100644
--- a/cayenne-gradle-plugin/src/test/java/org/apache/cayenne/tools/CgenTaskTest.java
+++ b/cayenne-gradle-plugin/src/test/java/org/apache/cayenne/tools/CgenTaskTest.java
@@ -88,8 +88,7 @@
         task.setUsePkgPath(true);
 
         CgenConfiguration configuration = task.buildConfiguration(dataMap);
-        ClassGenerationAction createdAction = new ClassGenerationAction();
-        createdAction.setCgenConfiguration(configuration);
+        ClassGenerationAction createdAction = new ClassGenerationAction(configuration);
 
         CgenConfiguration cgenConfiguration = createdAction.getCgenConfiguration();
         assertEquals(cgenConfiguration.getEmbeddableSuperTemplate(), "superTemplate");
diff --git a/cayenne-jcache/pom.xml b/cayenne-jcache/pom.xml
index ad67f29..407c956 100644
--- a/cayenne-jcache/pom.xml
+++ b/cayenne-jcache/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-jgroups/pom.xml b/cayenne-jgroups/pom.xml
index 3597ec7..d3c9f3e 100644
--- a/cayenne-jgroups/pom.xml
+++ b/cayenne-jgroups/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-jms/pom.xml b/cayenne-jms/pom.xml
index 0aff8ad..5f93f06 100644
--- a/cayenne-jms/pom.xml
+++ b/cayenne-jms/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-joda/pom.xml b/cayenne-joda/pom.xml
index 392d50e..bfa2902 100644
--- a/cayenne-joda/pom.xml
+++ b/cayenne-joda/pom.xml
@@ -22,7 +22,7 @@
 	<parent>
 		<artifactId>cayenne-parent</artifactId>
 		<groupId>org.apache.cayenne</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<artifactId>cayenne-joda</artifactId>
 	<name>cayenne-joda: Cayenne Joda Extensions</name>
diff --git a/cayenne-lifecycle/pom.xml b/cayenne-lifecycle/pom.xml
index 46148a9..23f89fd 100644
--- a/cayenne-lifecycle/pom.xml
+++ b/cayenne-lifecycle/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<artifactId>cayenne-parent</artifactId>
 		<groupId>org.apache.cayenne</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<artifactId>cayenne-lifecycle</artifactId>
 	<name>cayenne-lifecycle: Cayenne Lifecycle Utilities</name>
diff --git a/cayenne-osgi/pom.xml b/cayenne-osgi/pom.xml
index 55b0913..9911154 100644
--- a/cayenne-osgi/pom.xml
+++ b/cayenne-osgi/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-project-compatibility/pom.xml b/cayenne-project-compatibility/pom.xml
index 9047e57..be572da 100644
--- a/cayenne-project-compatibility/pom.xml
+++ b/cayenne-project-compatibility/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-project/pom.xml b/cayenne-project/pom.xml
index 59d585f..a7bcf20 100644
--- a/cayenne-project/pom.xml
+++ b/cayenne-project/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <groupId>org.apache.cayenne</groupId>
         <artifactId>cayenne-parent</artifactId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <artifactId>cayenne-project</artifactId>
diff --git a/cayenne-protostuff/pom.xml b/cayenne-protostuff/pom.xml
index e2daf3f..b252067 100644
--- a/cayenne-protostuff/pom.xml
+++ b/cayenne-protostuff/pom.xml
@@ -14,7 +14,7 @@
     <parent>
         <groupId>org.apache.cayenne</groupId>
         <artifactId>cayenne-parent</artifactId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <artifactId>cayenne-protostuff</artifactId>
     <packaging>jar</packaging>
diff --git a/cayenne-rop-server/pom.xml b/cayenne-rop-server/pom.xml
index e5aae6a..a1f0315 100644
--- a/cayenne-rop-server/pom.xml
+++ b/cayenne-rop-server/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
index 21fd347..3c245d1 100644
--- a/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/CayenneContextMergeHandler.java
@@ -27,7 +27,6 @@
 import org.apache.cayenne.reflect.ClassDescriptor;
 import org.apache.cayenne.reflect.PropertyDescriptor;
 import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.util.Util;
 
 /**
  * An object that merges "backdoor" modifications of the object graph coming from the
@@ -165,8 +164,7 @@
 
             // do not override local changes....
             PropertyDescriptor p = propertyForId(nodeId, property);
-            if (Util.nullSafeEquals(p.readPropertyDirectly(object), oldValue)) {
-
+            if (p.equals(p.readPropertyDirectly(object), oldValue)) {
                 p.writePropertyDirectly(object, oldValue, newValue);
             }
         }
diff --git a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
index 615474d..39e502e 100644
--- a/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
+++ b/cayenne-rop-server/src/main/java/org/apache/cayenne/remote/hessian/HessianConfig.java
@@ -82,7 +82,7 @@
             Collection<String> additionalWhitelist) {
 
         SerializerFactory factory = new CayenneSerializerFactory();
-
+        factory.setAllowNonSerializable(true);
         List<String> whitelist = new ArrayList<>(additionalWhitelist);
         if(resolver != null) {
             whitelist.add("org.apache.cayenne.*");
diff --git a/cayenne-server/pom.xml b/cayenne-server/pom.xml
index 92896a7..a2b5842 100644
--- a/cayenne-server/pom.xml
+++ b/cayenne-server/pom.xml
@@ -22,7 +22,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<artifactId>cayenne-server</artifactId>
 	<name>cayenne-server: Cayenne Server</name>
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java
index d741f10..221ed10 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/ObjectIdTmp.java
@@ -111,7 +111,7 @@
         }
 
         ObjectIdTmp that = (ObjectIdTmp) o;
-        if (!Arrays.equals(id, that.id)) {
+        if (id != that.id && !Arrays.equals(id, that.id)) {
             return false;
         }
         return entityName.equals(that.entityName);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/AttributeFault.java b/cayenne-server/src/main/java/org/apache/cayenne/access/AttributeFault.java
new file mode 100644
index 0000000..24869e4
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/AttributeFault.java
@@ -0,0 +1,49 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.reflect.AttributeProperty;
+
+/**
+ * @since 4.2
+ */
+public class AttributeFault extends Fault {
+
+    private final AttributeProperty property;
+
+    public AttributeFault(AttributeProperty property) {
+        this.property = property;
+    }
+
+    @Override
+    public Object resolveFault(Persistent sourceObject, String attributeName) {
+        return ObjectSelect
+                .columnQuery(sourceObject.getClass(), PropertyFactory.createBase(attributeName, property.getAttribute().getJavaClass()))
+                .where(ExpressionFactory.matchAllDbExp(sourceObject.getObjectId().getIdSnapshot(), Expression.EQUAL_TO))
+                .selectOne(sourceObject.getObjectContext());
+    }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java
index d010459..1a15d9b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataContextMergeHandler.java
@@ -35,7 +35,6 @@
 import org.apache.cayenne.reflect.PropertyVisitor;
 import org.apache.cayenne.reflect.ToManyProperty;
 import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.Util;
 
 /**
  * A listener of GraphEvents sent by the DataChannel that merges changes to the DataContext.
@@ -175,7 +174,7 @@
 
             // do not override local changes....
             PropertyDescriptor p = propertyForId(nodeId, property);
-            if (Util.nullSafeEquals(p.readPropertyDirectly(object), oldValue)) {
+            if (p.equals(p.readPropertyDirectly(object), oldValue)) {
                 p.writePropertyDirectly(object, oldValue, newValue);
             }
         }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java
index a30376b..b93be16 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/DataRowUtils.java
@@ -35,7 +35,6 @@
 import org.apache.cayenne.reflect.PropertyVisitor;
 import org.apache.cayenne.reflect.ToManyProperty;
 import org.apache.cayenne.reflect.ToOneProperty;
-import org.apache.cayenne.util.Util;
 
 /**
  * DataRowUtils contains a number of static methods to work with DataRows. This is a
@@ -96,8 +95,13 @@
                 // case, as NULL value is entirely valid; still save a map lookup by
                 // checking for the null value first
                 if (value == null && !snapshot.containsKey(dbAttrPath)) {
-                    isPartialSnapshot[0] = true;
+                    if(attr.isLazy()) {
+                        property.writePropertyDirectly(object, null, new AttributeFault(property));
+                    } else {
+                        isPartialSnapshot[0] = true;
+                    }
                 }
+
                 return true;
             }
 
@@ -151,8 +155,8 @@
 
                     // if value not modified, update it from snapshot,
                     // otherwise leave it alone
-                    if (Util.nullSafeEquals(curValue, oldValue)
-                            && !Util.nullSafeEquals(newValue, curValue)) {
+                    if (property.equals(curValue, oldValue)
+                            && !property.equals(newValue, curValue)) {
                         property.writePropertyDirectly(object, oldValue, newValue);
                     }
                 }
@@ -187,7 +191,7 @@
 
                             if (diff == null
                                     || !diff.containsArcSnapshot(relationship.getName())
-                                    || !Util.nullSafeEquals(id, diff
+                                    || !property.equals(id, diff
                                             .getArcSnapshotValue(relationship.getName()))) {
 
                                 if (id == null) {
@@ -258,7 +262,7 @@
         }
 
         ObjectId targetId = diff.getArcSnapshotValue(property.getName());
-        return !Util.nullSafeEquals(currentId, targetId);
+        return !property.equals(currentId, targetId);
     }
 
     // not for instantiation
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
index 6c38a39..9b2b68d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/ObjectDiff.java
@@ -344,7 +344,7 @@
                 Object oldValue = snapshot.get(property.getName());
                 Object newValue = property.readProperty(object);
 
-                if (!Util.nullSafeEquals(oldValue, newValue)) {
+                if (!property.equals(oldValue, newValue)) {
                     modFound[0] = true;
                 }
 
@@ -369,7 +369,7 @@
                 }
 
                 Object oldValue = arcSnapshot.get(property.getName());
-                if (!Util.nullSafeEquals(oldValue, newValue != null ? ((Persistent) newValue).getObjectId() : null)) {
+                if (!property.equals(oldValue, newValue != null ? ((Persistent) newValue).getObjectId() : null)) {
                     modFound[0] = true;
                 }
 
@@ -424,7 +424,7 @@
                 else {
                     Object oldValue = snapshot.get(property.getName());
 
-                    if (!Util.nullSafeEquals(oldValue, newValue)) {
+                    if (!property.equals(oldValue, newValue)) {
                         handler.nodePropertyChanged(nodeId, property.getName(), oldValue, newValue);
                     }
                 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
index e69ee7f..c42f819 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/DefaultDataDomainFlushAction.java
@@ -107,7 +107,9 @@
         Set<ArcTarget> processedArcs = new HashSet<>();
 
         DbRowOpFactory factory = new DbRowOpFactory(resolver, objectStore, processedArcs);
-        changesByObjectId.forEach((obj, diff) -> ops.addAll(factory.createRows(diff)));
+        // ops.addAll() method is slower in this case as it will allocate new array for all values
+        //noinspection UseBulkOperation
+        changesByObjectId.forEach((obj, diff) -> factory.createRows(diff).forEach(ops::add));
 
         return ops;
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
index f61b35e..5b4d048 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/EffectiveOpId.java
@@ -23,29 +23,61 @@
 import java.util.Map;
 import java.util.function.Supplier;
 
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 
 /**
  * Helper value-object class that used to compare operations by "effective" id (i.e. by id snapshot,
  * that will include replacement id if any).
- * This class is not used directly by Cayenne, it's designed to ease custom implementations.
+ *
  * @since 4.2
  */
-@SuppressWarnings("unused")
 public class EffectiveOpId {
+    private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000;
+
     private final String entityName;
     private final Map<String, Object> snapshot;
     private final ObjectId id;
 
     public EffectiveOpId(ObjectId id) {
+        this(id, id.getEntityName(), id.getIdSnapshot());
+    }
+
+    public EffectiveOpId(String entityName, ObjectId id) {
+        this(id, entityName, id.getIdSnapshot());
+    }
+
+    public EffectiveOpId(String entityName, Map<String, Object> idSnapshot) {
+        this(null, entityName, idSnapshot);
+    }
+
+    private EffectiveOpId(ObjectId id, String entityName, Map<String, Object> idSnapshot) {
+        this.entityName = entityName;
+        if(idSnapshot.size() == 1 && !(idSnapshot.values().iterator().next() instanceof Supplier)) {
+            this.snapshot = idSnapshot;
+        } else {
+            this.snapshot = new HashMap<>(idSnapshot.size());
+            idSnapshot.forEach((key, value) -> {
+                Object initial = value;
+                int safeguard = 0;
+                while (value instanceof Supplier && safeguard < MAX_NESTED_SUPPLIER_LEVEL) {
+                    value = ((Supplier) value).get();
+                    safeguard++;
+                }
+
+                // simple guard from recursive Suppliers
+                if (safeguard == MAX_NESTED_SUPPLIER_LEVEL) {
+                    throw new CayenneRuntimeException("Possible recursive supplier chain for PK value: key '%s'", key);
+                }
+
+                if (value != null) {
+                    this.snapshot.put(key, value);
+                } else {
+                    this.snapshot.put(key, initial);
+                }
+            });
+        }
         this.id = id;
-        this.entityName = id.getEntityName();
-        this.snapshot = new HashMap<>(id.getIdSnapshot());
-        this.snapshot.entrySet().forEach(entry -> {
-            if(entry.getValue() instanceof Supplier) {
-                entry.setValue(((Supplier) entry.getValue()).get());
-            }
-        });
     }
 
     @Override
@@ -72,4 +104,9 @@
         result = 31 * result + snapshot.hashCode();
         return result;
     }
+
+    @Override
+    public String toString() {
+        return "EffectiveOpId{" + entityName + ": " + snapshot + '}';
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java
new file mode 100644
index 0000000..1519556
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/IdGenerationMarker.java
@@ -0,0 +1,52 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.io.Serializable;
+
+import org.apache.cayenne.ObjectId;
+
+/**
+ * Special value that denotes generated id attribute
+ *
+ * @since 4.2
+ */
+class IdGenerationMarker implements Serializable {
+    private static final long serialVersionUID = -5339942931435878094L;
+
+    private final int id;
+
+    IdGenerationMarker(ObjectId id) {
+        this.id = id.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        IdGenerationMarker that = (IdGenerationMarker) o;
+        return id == that.id;
+    }
+
+    @Override
+    public int hashCode() {
+        return id;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java
index bc757efa..c0884ba 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/PermanentObjectIdVisitor.java
@@ -26,6 +26,7 @@
 import org.apache.cayenne.access.DataDomain;
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.flush.operation.DbRowOpVisitor;
+import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
 import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
 import org.apache.cayenne.dba.PkGenerator;
 import org.apache.cayenne.exp.parser.ASTDbPath;
@@ -83,6 +84,15 @@
         return null;
     }
 
+    @Override
+    public Void visitDelete(DeleteDbRowOp dbRow) {
+        // flattened ids will be temporary for delete operation, replace it
+        if(dbRow.getChangeId().isTemporary() && dbRow.getChangeId().isReplacementIdAttached()) {
+            dbRow.setChangeId(dbRow.getChangeId().createReplacementId());
+        }
+        return null;
+    }
+
     private void createPermanentId(InsertDbRowOp dbRow) {
         ObjectId id = dbRow.getChangeId();
         boolean supportsGeneratedKeys = lastNode.getAdapter().supportsGeneratedKeys();
@@ -118,6 +128,8 @@
 
             // skip db-generated
             if (supportsGeneratedKeys && dbAttr.isGenerated()) {
+                // mark that this attribute should be generated at insert time
+                idMap.put(dbAttrName, new IdGenerationMarker(id));
                 continue;
             }
 
@@ -136,4 +148,5 @@
             }
         }
     }
+
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
index a3c00f4..0f21b3f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/ReplacementIdVisitor.java
@@ -79,15 +79,22 @@
 
     private void updateId(DbRowOp dbRow) {
         ObjectId id = dbRow.getChangeId();
+        Persistent object = dbRow.getObject();
+
+        // check that PK was generated or set properly
         if (!id.isReplacementIdAttached()) {
             if (id == dbRow.getObject().getObjectId() && id.isTemporary()) {
-                throw new CayenneRuntimeException("PK for the object %s is not set during insert.", dbRow.getObject());
+                throw new CayenneRuntimeException("PK for the object %s is not set during insert.", object);
             }
             return;
         }
-
-        Persistent object = dbRow.getObject();
         Map<String, Object> replacement = id.getReplacementIdMap();
+        replacement.forEach((attr, val) -> {
+            if(val instanceof IdGenerationMarker) {
+                throw new CayenneRuntimeException("PK for the object %s is not set during insert.", object);
+            }
+        });
+
         ObjectId replacementId = id.createReplacementId();
         if (object.getObjectId() == id && !replacementId.getEntityName().startsWith(ASTDbPath.DB_PREFIX)) {
             object.setObjectId(replacementId);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
index d27a406..12c1b42 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/RootRowOpProcessor.java
@@ -29,6 +29,7 @@
 import org.apache.cayenne.access.flush.operation.DeleteDbRowOp;
 import org.apache.cayenne.access.flush.operation.InsertDbRowOp;
 import org.apache.cayenne.access.flush.operation.UpdateDbRowOp;
+import org.apache.cayenne.graph.GraphChangeHandler;
 import org.apache.cayenne.map.ObjEntity;
 
 /**
@@ -40,10 +41,16 @@
  */
 class RootRowOpProcessor implements DbRowOpVisitor<Void> {
     private final DbRowOpFactory dbRowOpFactory;
+    private final GraphChangeHandler insertHandler;
+    private final GraphChangeHandler updateHandler;
+    private final GraphChangeHandler deleteHandler;
     private ObjectDiff diff;
 
     RootRowOpProcessor(DbRowOpFactory dbRowOpFactory) {
         this.dbRowOpFactory = dbRowOpFactory;
+        this.insertHandler = new ValuesCreationHandler(dbRowOpFactory, DbRowOpType.INSERT);
+        this.updateHandler = new ValuesCreationHandler(dbRowOpFactory, DbRowOpType.UPDATE);
+        this.deleteHandler = new ArcValuesCreationHandler(dbRowOpFactory, DbRowOpType.DELETE);
     }
 
     void setDiff(ObjectDiff diff) {
@@ -52,13 +59,13 @@
 
     @Override
     public Void visitInsert(InsertDbRowOp dbRow) {
-        diff.apply(new ValuesCreationHandler(dbRowOpFactory, DbRowOpType.INSERT));
+        diff.apply(insertHandler);
         return null;
     }
 
     @Override
     public Void visitUpdate(UpdateDbRowOp dbRow) {
-        diff.apply(new ValuesCreationHandler(dbRowOpFactory, DbRowOpType.UPDATE));
+        diff.apply(updateHandler);
         if (dbRowOpFactory.getDescriptor().getEntity().getDeclaredLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC) {
             dbRowOpFactory.getDescriptor().visitAllProperties(new OptimisticLockQualifierBuilder(dbRow, diff));
         }
@@ -71,7 +78,7 @@
             throw new CayenneRuntimeException("Attempt to modify object(s) mapped to a read-only entity: '%s'. " +
                     "Can't commit changes.", dbRowOpFactory.getDescriptor().getEntity().getName());
         }
-        diff.apply(new ArcValuesCreationHandler(dbRowOpFactory, DbRowOpType.DELETE));
+        diff.apply(deleteHandler);
         Collection<ObjectId> flattenedIds = dbRowOpFactory.getStore().getFlattenedIds(dbRow.getChangeId());
         flattenedIds.forEach(id -> dbRowOpFactory.getOrCreate(dbRowOpFactory.getDbEntity(id), id, DbRowOpType.DELETE));
         if (dbRowOpFactory.getDescriptor().getEntity().getDeclaredLockType() == ObjEntity.LOCK_TYPE_OPTIMISTIC) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java
index 9666b76..1602aca 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/BaseDbRowOp.java
@@ -33,12 +33,14 @@
     protected final Persistent object;
     protected final DbEntity entity;
     // Can be ObjEntity id or a DB row id for flattened rows
-    protected final ObjectId changeId;
+    protected ObjectId changeId;
+    protected int hashCode;
 
     protected BaseDbRowOp(Persistent object, DbEntity entity, ObjectId id) {
         this.object = Objects.requireNonNull(object);
         this.entity = Objects.requireNonNull(entity);
         this.changeId = Objects.requireNonNull(id);
+        this.hashCode = changeId.hashCode();
     }
 
     @Override
@@ -67,7 +69,7 @@
 
     @Override
     public int hashCode() {
-        return changeId.hashCode();
+        return hashCode;
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpGraph.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpGraph.java
new file mode 100644
index 0000000..0d78e44
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpGraph.java
@@ -0,0 +1,119 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Graph sorting, copy of DIGraph implementation from cayenne-di.
+ */
+class DbRowOpGraph {
+
+	/**
+	 * {@link LinkedHashMap} is used for supporting insertion order.
+	 */
+	private final Map<DbRowOp, List<DbRowOp>> neighbors = new LinkedHashMap<>();
+
+	DbRowOpGraph() {
+	}
+
+	/**
+	 * Add a vertex to the graph. Nothing happens if vertex is already in graph.
+	 */
+	void add(DbRowOp vertex) {
+		neighbors.putIfAbsent(vertex, new ArrayList<>(0));
+	}
+
+	/**
+	 * Add an edge to the graph; if either vertex does not exist, it's added.
+	 * This implementation allows the creation of multi-edges and self-loops.
+	 */
+	void add(DbRowOp from, DbRowOp to) {
+		neighbors.computeIfAbsent(from, k -> new ArrayList<>(4)).add(to);
+		this.add(to);
+	}
+
+	/**
+	 * Return (as a Map) the in-degree of each vertex.
+	 */
+	private Map<DbRowOp, Integer> inDegree() {
+		Map<DbRowOp, Integer> result = new LinkedHashMap<>(neighbors.size());
+
+		neighbors.forEach((from, neighbors) -> {
+			neighbors.forEach(to -> result.compute(to, (k, old) -> {
+				if(old == null) {
+					return 1;
+				}
+				return old + 1;
+			}));
+			result.putIfAbsent(from, 0);
+		});
+
+		return result;
+	}
+
+	/**
+	 * Return (as a List) the topological sort of the vertices. Throws an exception if cycles are detected.
+	 */
+	List<DbRowOp> topSort() {
+		Map<DbRowOp, Integer> degree = inDegree();
+		Deque<DbRowOp> zeroDegree = new ArrayDeque<>(neighbors.size() / 2);
+		ArrayList<DbRowOp> result = new ArrayList<>(neighbors.size());
+
+		degree.forEach((k, v) -> {
+			if(v == 0) {
+				zeroDegree.push(k);
+			}
+		});
+
+		while (!zeroDegree.isEmpty()) {
+			DbRowOp v = zeroDegree.removeFirst();
+			result.add(v);
+
+			neighbors.get(v).forEach(neighbor ->
+					degree.computeIfPresent(neighbor, (k, oldValue) -> {
+						int newValue = --oldValue;
+						if(newValue == 0) {
+							zeroDegree.addLast(neighbor);
+						}
+						return newValue;
+					})
+			);
+		}
+
+		// Check that we have used the entire graph (if not, there was a cycle)
+		if (result.size() != neighbors.size()) {
+			Set<DbRowOp> remainingKeys = new HashSet<>(neighbors.keySet());
+			remainingKeys.removeIf(result::contains);
+			throw new IllegalStateException("Cycle detected in list for keys: " + remainingKeys);
+		}
+
+		Collections.reverse(result);
+		return result;
+	}
+}
\ No newline at end of file
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java
index 51edacf..d17eb8a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DbRowOpMerger.java
@@ -32,7 +32,7 @@
 
     private DbRowOp dbRow;
 
-    public DbRowOpMerger() {
+    DbRowOpMerger() {
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java
index 11306d7..fc53280 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/DeleteDbRowOp.java
@@ -40,6 +40,11 @@
         return visitor.visitDelete(this);
     }
 
+    public void setChangeId(ObjectId changeId) {
+        this.changeId = changeId;
+        this.hashCode = changeId.hashCode();
+    }
+
     @Override
     public boolean equals(Object o) {
         if(!(o instanceof DbRowOpWithQualifier)) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java
new file mode 100644
index 0000000..8ed9617
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/flush/operation/GraphBasedDbRowOpSorter.java
@@ -0,0 +1,314 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush.operation;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.QueryResponse;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.access.ObjectStore;
+import org.apache.cayenne.access.flush.EffectiveOpId;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.di.Provider;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.graph.GraphManager;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ObjectIdQuery;
+import org.apache.cayenne.util.SingleEntryMap;
+
+/**
+ * Db operation sorted that builds dependency graph and uses topological sort to get final order.
+ * This in general slower than {@link DefaultDbRowOpSorter} but can handle more edge cases (like multiple meaningful PKs/FKs).
+ *
+ * TODO: possible optimizations could be optional logic parts (e.g. detecting effective id intersections,
+ *       reflexive dependencies, etc.)
+ *
+ * @since 4.2
+ */
+public class GraphBasedDbRowOpSorter implements DbRowOpSorter {
+
+    private final DbRowOpTypeVisitor rowOpTypeVisitor = new DbRowOpTypeVisitor();
+    private final Provider<DataDomain> dataDomainProvider;
+
+    private volatile Map<DbEntity, List<DbRelationship>> relationships;
+
+    public GraphBasedDbRowOpSorter(@Inject Provider<DataDomain> dataDomainProvider) {
+        this.dataDomainProvider = dataDomainProvider;
+    }
+
+    private void initDataSync() {
+        Map<DbEntity, List<DbRelationship>> localRelationships = relationships;
+        if(localRelationships == null) {
+            synchronized (this) {
+                localRelationships = relationships;
+                if(localRelationships == null) {
+                    initDataNoSync();
+                }
+            }
+        }
+    }
+
+    /**
+     * Init all the data we need for faster processing actual rows.
+     */
+    private void initDataNoSync() {
+        relationships = new HashMap<>();
+        EntityResolver resolver = dataDomainProvider.get().getEntityResolver();
+
+        resolver.getDbEntities().forEach(entity ->
+            entity.getRelationships().forEach(dbRelationship -> {
+                if(dbRelationship.isToMany() || !dbRelationship.isToPK() || dbRelationship.isToDependentPK()) {
+                    // TODO: can we ignore all of these relationships?
+                    return;
+                }
+
+                relationships
+                        .computeIfAbsent(entity, e -> new ArrayList<>())
+                        .add(dbRelationship);
+            })
+        );
+    }
+
+    @Override
+    public List<DbRowOp> sort(List<DbRowOp> dbRows) {
+        // lazy init Cayenne model data
+        initDataSync();
+
+        // build index op by ID
+        Map<EffectiveOpId, List<DbRowOp>> indexById = new HashMap<>(dbRows.size());
+        dbRows.forEach(op -> indexById
+                .computeIfAbsent(effectiveIdFor(op), id -> new ArrayList<>(1))
+                .add(op)
+        );
+        boolean hasMeaningfulIds = indexById.size() != dbRows.size();
+
+        // build ops dependency graph
+        DbRowOpGraph graph = new DbRowOpGraph();
+        dbRows.forEach(op -> {
+            processRelationships(indexById, graph, op);
+            if(hasMeaningfulIds) {
+                processMeaningfulIds(indexById, graph, op);
+            }
+            graph.add(op);
+        });
+
+        // sort
+        return graph.topSort();
+    }
+
+    private void processRelationships(Map<EffectiveOpId, List<DbRowOp>> indexByDbId, DbRowOpGraph graph, DbRowOp op) {
+        // get graph edges for reflexive relationships
+        DbRowOpType opType = op.accept(rowOpTypeVisitor);
+        relationships.getOrDefault(op.getEntity(), Collections.emptyList()).forEach(relationship ->
+            getParentsOpId(op, relationship).forEach(parentOpId ->
+                indexByDbId.getOrDefault(parentOpId, Collections.emptyList()).forEach(parentOp -> {
+                    if(op == parentOp) {
+                        return;
+                    }
+                    DbRowOpType parentOpType = parentOp.accept(rowOpTypeVisitor);
+                    // 1. Our insert can depend on others insert or update
+                    // 2. Our update can depend on others insert or update, or others delete can depend on our update
+                    // 3. Others delete can depend on our delete
+                    switch (opType) {
+                        case INSERT:
+                            if(parentOpType != DbRowOpType.DELETE) {
+                                graph.add(op, parentOp);
+                            }
+                            break;
+                        case UPDATE:
+                            if(parentOpType != DbRowOpType.DELETE) {
+                                graph.add(op, parentOp);
+                            } else {
+                                graph.add(parentOp, op);
+                            }
+                            break;
+                        case DELETE:
+                            if(parentOpType == DbRowOpType.DELETE) {
+                                graph.add(parentOp, op);
+                            }
+                    }
+                })
+            )
+        );
+    }
+
+    private void processMeaningfulIds(Map<EffectiveOpId, List<DbRowOp>> indexById, DbRowOpGraph graph, DbRowOp op) {
+        // get graph edges from same ID operations, for such operations delete depends on other operations
+        indexById.get(effectiveIdFor(op)).forEach(sameIdOp -> {
+            if(op == sameIdOp) {
+                return;
+            }
+            DbRowOpType sameIdOpType = sameIdOp.accept(rowOpTypeVisitor);
+            if(sameIdOpType == DbRowOpType.DELETE) {
+                graph.add(op, sameIdOp);
+            }
+        });
+    }
+
+    private List<EffectiveOpId> getParentsOpId(DbRowOp op, DbRelationship relationship) {
+        List<Map<String, Object>> parentIdSnapshots = op.accept(new DbRowOpSnapshotVisitor(relationship));
+        if(parentIdSnapshots.size() == 1) {
+            EffectiveOpId id = effectiveIdFor(relationship, parentIdSnapshots.get(0));
+            if(id != null) {
+                return Collections.singletonList(id);
+            } else {
+                return Collections.emptyList();
+            }
+        } else {
+            List<EffectiveOpId> effectiveOpIds = new ArrayList<>(parentIdSnapshots.size());
+            parentIdSnapshots.forEach(snapshot -> {
+                EffectiveOpId id = this.effectiveIdFor(relationship, snapshot);
+                if(id != null) {
+                    effectiveOpIds.add(id);
+                }
+            });
+            return effectiveOpIds;
+        }
+    }
+
+    private EffectiveOpId effectiveIdFor(DbRowOp op) {
+        return new EffectiveOpId(op.getEntity().getName(), op.getChangeId());
+    }
+
+    private EffectiveOpId effectiveIdFor(DbRelationship relationship, Map<String, Object> opSnapshot) {
+        int len = relationship.getJoins().size();
+        Map<String, Object> idMap = len == 1
+                ? new SingleEntryMap<>(relationship.getJoins().get(0).getTargetName())
+                : new HashMap<>(len);
+        relationship.getJoins().forEach(join -> {
+            Object value = opSnapshot.get(join.getSourceName());
+            if(value == null) {
+                return;
+            }
+            idMap.put(join.getTargetName(), value);
+        });
+        if(idMap.size() != len) {
+            return null;
+        }
+        return new EffectiveOpId(relationship.getTargetEntityName(), idMap);
+    }
+
+    private static class DbRowOpSnapshotVisitor implements DbRowOpVisitor<List<Map<String, Object>>> {
+
+        private final DbRelationship relationship;
+
+        private DbRowOpSnapshotVisitor(DbRelationship relationship) {
+            this.relationship = relationship;
+        }
+
+        @Override
+        public List<Map<String, Object>> visitInsert(InsertDbRowOp dbRow) {
+            return Collections.singletonList(dbRow.getValues().getSnapshot());
+        }
+
+        @Override
+        public List<Map<String, Object>> visitUpdate(UpdateDbRowOp dbRow) {
+            List<Map<String, Object>> result;
+            Map<String, Object> updatedSnapshot = dbRow.getValues().getSnapshot();
+            if(dbRow.getChangeId().getEntityName().startsWith(ASTDbPath.DB_PREFIX)) {
+                return Collections.singletonList(updatedSnapshot);
+            }
+            result = new ArrayList<>(2);
+            // get updated state from operation
+            result.add(updatedSnapshot);
+            // get previous state from cache, but only for update attributes
+            Map<String, Object> cachedSnapshot = getCachedSnapshot(dbRow.getObject());
+            cachedSnapshot.entrySet().forEach(entry -> {
+                if(!updatedSnapshot.containsKey(entry.getKey())) {
+                    entry.setValue(null);
+                }
+            });
+            result.add(cachedSnapshot);
+            return result;
+        }
+
+        @Override
+        public List<Map<String, Object>> visitDelete(DeleteDbRowOp dbRow) {
+            Map<String, Object> cachedSnapshot = getCachedSnapshot(dbRow.getObject());
+            return Collections.singletonList(cachedSnapshot);
+        }
+
+        private Map<String, Object> getCachedSnapshot(Persistent object) {
+            ObjectIdQuery query = new ObjectIdQuery(object.getObjectId(), true, ObjectIdQuery.CACHE);
+            QueryResponse response = object.getObjectContext().getChannel().onQuery(null, query);
+            @SuppressWarnings("unchecked")
+            List<DataRow> result = (List<DataRow>) response.firstList();
+            if (result == null || result.size() == 0) {
+                return Collections.emptyMap();
+            }
+
+            // copy snapshot as we can modify it later
+            DataRow dataRow = result.get(0);
+            int joinSize = relationship.getJoins().size();
+            Map<String, Object> snapshot = joinSize == 1
+                    ? new SingleEntryMap<>(relationship.getJoins().get(0).getSourceName())
+                    : new HashMap<>(joinSize);
+            relationship.getJoins().forEach(join -> {
+                Object value = dataRow.get(join.getSourceName());
+                if(value != null) {
+                    snapshot.put(join.getSourceName(), value);
+                }
+            });
+
+            // check and merge flattened IDs snapshots
+            GraphManager graphManager = object.getObjectContext().getGraphManager();
+            if(graphManager instanceof ObjectStore) {
+                ObjectStore store = (ObjectStore)graphManager;
+                store.getFlattenedIds(object.getObjectId()).forEach(flattenedId -> {
+                    // map values of flattened ids from target to source
+                    Map<String, Object> idSnapshot = flattenedId.getIdSnapshot();
+                    relationship.getJoins().forEach(join -> {
+                        Object value = idSnapshot.get(join.getTargetName());
+                        if(value != null) {
+                            snapshot.put(join.getSourceName(), value);
+                        }
+                    });
+                });
+            }
+
+            return snapshot;
+        }
+    }
+
+    private static class DbRowOpTypeVisitor implements DbRowOpVisitor<DbRowOpType> {
+        @Override
+        public DbRowOpType visitDelete(DeleteDbRowOp dbRow) {
+            return DbRowOpType.DELETE;
+        }
+
+        @Override
+        public DbRowOpType visitInsert(InsertDbRowOp dbRow) {
+            return DbRowOpType.INSERT;
+        }
+
+        @Override
+        public DbRowOpType visitUpdate(UpdateDbRowOp dbRow) {
+            return DbRowOpType.UPDATE;
+        }
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
index ad54cbb..6f798e3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/jdbc/BatchAction.java
@@ -158,6 +158,10 @@
 	protected void runAsIndividualQueries(Connection connection, BatchTranslator translator,
 			OperationObserver delegate, boolean generatesKeys) throws SQLException, Exception {
 
+		if(query.getRows().isEmpty()) {
+			return;
+		}
+
 		JdbcEventLogger logger = dataNode.getJdbcEventLogger();
 		boolean useOptimisticLock = query.isUsingOptimisticLocking();
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/BaseBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/BaseBuilder.java
new file mode 100644
index 0000000..1f8ea45
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/BaseBuilder.java
@@ -0,0 +1,66 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import java.util.function.Supplier;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+
+/**
+ * @since 4.2
+ */
+public abstract class BaseBuilder implements NodeBuilder {
+    /**
+     * Main root of this query
+     */
+    protected final Node root;
+
+    /*
+     * Following nodes are all children of root,
+     * but we keep them here for quick access.
+     */
+    protected final Node[] nodes;
+
+    public BaseBuilder(Node root, int size) {
+        this.root = root;
+        this.nodes = new Node[size];
+    }
+
+    protected Node node(int idx, Supplier<Node> nodeSupplier) {
+        if(nodes[idx] == null) {
+            nodes[idx] = nodeSupplier.get();
+        }
+        return nodes[idx];
+    }
+
+    @Override
+    public Node build() {
+        for (Node next : nodes) {
+            if (next != null) {
+                root.addChild(next);
+            }
+        }
+        return root;
+    }
+
+    public Node getRoot() {
+        return root;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/DeleteBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/DeleteBuilder.java
new file mode 100644
index 0000000..9d00f2e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/DeleteBuilder.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.DeleteNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TableNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.WhereNode;
+
+/**
+ * @since 4.2
+ */
+public class DeleteBuilder extends BaseBuilder {
+
+    private static final int TABLE_NODE = 0;
+    private static final int WHERE_NODE = 1;
+
+    public DeleteBuilder(String table) {
+        super(new DeleteNode(), WHERE_NODE + 1);
+        node(TABLE_NODE, () -> new TableNode(table, null));
+    }
+
+    public DeleteBuilder where(NodeBuilder expression) {
+        if(expression != null) {
+            node(WHERE_NODE, WhereNode::new).addChild(expression.build());
+        }
+        return this;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/InsertBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/InsertBuilder.java
new file mode 100644
index 0000000..fa1e3c7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/InsertBuilder.java
@@ -0,0 +1,54 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.InsertColumnsNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InsertNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TableNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.InsertValuesNode;
+
+/**
+ * @since 4.2
+ */
+public class InsertBuilder extends BaseBuilder {
+
+    /*
+    INSERT INTO AUTO_PK_SUPPORT (TABLE_NAME, NEXT_ID) VALUES ('X_AUTHOR', 200)
+    */
+
+    private static final int TABLE_NODE   = 0;
+    private static final int COLUMNS_NODE = 1;
+    private static final int VALUES_NODE  = 2;
+
+    public InsertBuilder(String table) {
+        super(new InsertNode(), VALUES_NODE + 1);
+        node(TABLE_NODE, () -> new TableNode(table, null));
+    }
+
+    public InsertBuilder column(ColumnNodeBuilder columnNode) {
+        node(COLUMNS_NODE, InsertColumnsNode::new).addChild(columnNode.build());
+        return this;
+    }
+
+    public InsertBuilder value(ValueNodeBuilder valueNode) {
+        node(VALUES_NODE, InsertValuesNode::new).addChild(valueNode.build());
+        return this;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
index ac84aeb..b5b92b3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLBuilder.java
@@ -36,6 +36,18 @@
         return new SelectBuilder(params);
     }
 
+    public static InsertBuilder insert(String table) {
+        return new InsertBuilder(table);
+    }
+
+    public static UpdateBuilder update(String table) {
+        return new UpdateBuilder(table);
+    }
+
+    public static DeleteBuilder delete(String table) {
+        return new DeleteBuilder(table);
+    }
+
     public static TableNodeBuilder table(String table) {
         return new TableNodeBuilder(table);
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLGenerationContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLGenerationContext.java
index 02090c5..7e0b759 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLGenerationContext.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SQLGenerationContext.java
@@ -23,6 +23,8 @@
 
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.map.DbEntity;
 
 /**
  * @since 4.2
@@ -32,4 +34,8 @@
     DbAdapter getAdapter();
 
     Collection<DbAttributeBinding> getBindings();
+
+    QuotingStrategy getQuotingStrategy();
+
+    DbEntity getRootDbEntity();
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SelectBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SelectBuilder.java
index 702a68c..6517d0d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SelectBuilder.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/SelectBuilder.java
@@ -19,8 +19,6 @@
 
 package org.apache.cayenne.access.sqlbuilder;
 
-import java.util.function.Supplier;
-
 import org.apache.cayenne.access.sqlbuilder.sqltree.DistinctNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.FromNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.GroupByNode;
@@ -36,7 +34,7 @@
 /**
  * @since 4.2
  */
-public class SelectBuilder implements NodeBuilder {
+public class SelectBuilder extends BaseBuilder {
 
     private static final int SELECT_NODE    = 0;
     private static final int FROM_NODE      = 1;
@@ -47,19 +45,8 @@
     private static final int ORDERBY_NODE   = 6;
     private static final int LIMIT_NODE     = 7;
 
-    /**
-     * Main root of this query
-     */
-    private Node root;
-
-    /*
-     * Following nodes are all children of root,
-     * but we keep them here for quick access.
-     */
-    private Node[] nodes = new Node[LIMIT_NODE + 1];
-
     SelectBuilder(NodeBuilder... selectExpressions) {
-        root = new SelectNode();
+        super(new SelectNode(), LIMIT_NODE + 1);
         for(NodeBuilder exp : selectExpressions) {
             node(SELECT_NODE, SelectResultNode::new).addChild(exp.build());
         }
@@ -145,25 +132,4 @@
         return this;
     }
 
-    @Override
-    public Node build() {
-        for (Node next : nodes) {
-            if (next != null) {
-                root.addChild(next);
-            }
-        }
-        return root;
-    }
-
-    public Node getRoot() {
-        return root;
-    }
-
-    private Node node(int idx, Supplier<Node> nodeSupplier) {
-        if(nodes[idx] == null) {
-            nodes[idx] = nodeSupplier.get();
-        }
-        return nodes[idx];
-    }
-
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/UpdateBuilder.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/UpdateBuilder.java
new file mode 100644
index 0000000..4dad52a
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/UpdateBuilder.java
@@ -0,0 +1,53 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.UpdateSetNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.TableNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.UpdateNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.WhereNode;
+
+/**
+ * @since 4.2
+ */
+public class UpdateBuilder extends BaseBuilder {
+
+    private static final int TABLE_NODE = 0;
+    private static final int SET_NODE   = 1;
+    private static final int WHERE_NODE = 2;
+
+    public UpdateBuilder(String table) {
+        super(new UpdateNode(), WHERE_NODE + 1);
+        node(TABLE_NODE, () -> new TableNode(table, null));
+    }
+
+    public UpdateBuilder set(NodeBuilder setExpression) {
+        node(SET_NODE, UpdateSetNode::new).addChild(setExpression.build());
+        return this;
+    }
+
+    public UpdateBuilder where(NodeBuilder expression) {
+        if(expression != null) {
+            node(WHERE_NODE, WhereNode::new).addChild(expression.build());
+        }
+        return this;
+    }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ChildProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ChildProcessor.java
new file mode 100644
index 0000000..d229112
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ChildProcessor.java
@@ -0,0 +1,35 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import java.util.Optional;
+
+/**
+ * @since 4.2
+ * @param <T> type of child node to process
+ */
+@FunctionalInterface
+public interface ChildProcessor<T extends Node> {
+
+    ChildProcessor<?> EMPTY = (p,c,i) -> Optional.empty();
+
+    Optional<Node> process(Node parent, T child, int index);
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/DeleteNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/DeleteNode.java
new file mode 100644
index 0000000..98f73d9
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/DeleteNode.java
@@ -0,0 +1,36 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class DeleteNode extends Node {
+    @Override
+    public Node copy() {
+        return new DeleteNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer.append("DELETE FROM");
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/EqualNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/EqualNode.java
index 50b4ba0..fc64616 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/EqualNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/EqualNode.java
@@ -33,7 +33,10 @@
     @Override
     public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
         Node child = getChild(1);
-        if (child.getType() == NodeType.VALUE && ((ValueNode) child).getValue() == null) {
+        if (child.getType() == NodeType.VALUE
+                && ((ValueNode) child).getValue() == null
+                && getParent() != null
+                && getParent().getType() != NodeType.UPDATE_SET) {
             buffer.append(" IS");
         } else {
             buffer.append(" =");
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
index 0199ec3..cf4a219 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ExpressionNode.java
@@ -41,14 +41,20 @@
 
     @Override
     public void appendChildrenStart(QuotingAppendable buffer) {
-        if(parent != null && parent.type != NodeType.WHERE && parent.type != NodeType.JOIN) {
+        if(parent != null
+                && parent.type != NodeType.WHERE
+                && parent.type != NodeType.JOIN
+                && parent.type != NodeType.UPDATE_SET) {
             buffer.append(" (");
         }
     }
 
     @Override
     public void appendChildrenEnd(QuotingAppendable buffer) {
-        if(parent != null && parent.type != NodeType.WHERE && parent.type != NodeType.JOIN) {
+        if(parent != null
+                && parent.type != NodeType.WHERE
+                && parent.type != NodeType.JOIN
+                && parent.type != NodeType.UPDATE_SET) {
             buffer.append(" )");
         }
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
index 778cc63..390287a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/FunctionNode.java
@@ -31,6 +31,12 @@
     private final boolean needParentheses;
     private String alias;
 
+    static public FunctionNode wrap(Node node, String functionName) {
+        FunctionNode functionNode = new FunctionNode(functionName, null);
+        functionNode.addChild(node);
+        return functionNode;
+    }
+
     public FunctionNode(String functionName, String alias) {
         this(functionName, alias, true);
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertColumnsNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertColumnsNode.java
new file mode 100644
index 0000000..6b6aaa9
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertColumnsNode.java
@@ -0,0 +1,57 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class InsertColumnsNode extends Node {
+
+    public InsertColumnsNode() {
+        super(NodeType.INSERT_COLUMNS);
+    }
+
+    @Override
+    public Node copy() {
+        return new InsertColumnsNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer;
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childInd) {
+        buffer.append(',');
+    }
+
+    @Override
+    public void appendChildrenEnd(QuotingAppendable buffer) {
+        buffer.append(')');
+    }
+
+    @Override
+    public void appendChildrenStart(QuotingAppendable buffer) {
+        buffer.append('(');
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertNode.java
new file mode 100644
index 0000000..69a8f1d
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertNode.java
@@ -0,0 +1,37 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class InsertNode extends Node {
+    @Override
+    public Node copy() {
+        return new InsertNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer.append("INSERT INTO");
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertValuesNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertValuesNode.java
new file mode 100644
index 0000000..695af52
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/InsertValuesNode.java
@@ -0,0 +1,57 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class InsertValuesNode extends Node {
+
+    public InsertValuesNode() {
+        super(NodeType.INSERT_VALUES);
+    }
+
+    @Override
+    public Node copy() {
+        return new InsertValuesNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer.append(" VALUES");
+    }
+
+    @Override
+    public void appendChildrenStart(QuotingAppendable buffer) {
+        buffer.append('(');
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childInd) {
+        buffer.append(',');
+    }
+
+    @Override
+    public void appendChildrenEnd(QuotingAppendable buffer) {
+        buffer.append(')');
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeType.java
index b5a6a63..575d330 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeType.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/NodeType.java
@@ -34,5 +34,9 @@
     IN,
     RESULT,
     WHERE,
-    JOIN, FROM
+    JOIN,
+    FROM,
+    UPDATE_SET,
+    INSERT_COLUMNS,
+    INSERT_VALUES
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/PerAttributeChildProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/PerAttributeChildProcessor.java
new file mode 100644
index 0000000..3ecf5c5
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/PerAttributeChildProcessor.java
@@ -0,0 +1,51 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Function;
+
+import org.apache.cayenne.map.DbAttribute;
+
+/**
+ * @since 4.2
+ * @param <T> type of the node to process
+ */
+public class PerAttributeChildProcessor<T extends Node> implements ChildProcessor<T> {
+
+    private final Map<DbAttribute, ChildProcessor<T>> processorByAttribute = new HashMap<>();
+    private final Function<T, DbAttribute> attributeMapper;
+    private final Function<DbAttribute, ChildProcessor<T>> processorFactory;
+
+    public PerAttributeChildProcessor(Function<T, DbAttribute> attributeMapper,
+                                      Function<DbAttribute, ChildProcessor<T>> processorFactory) {
+        this.processorFactory = processorFactory;
+        this.attributeMapper = attributeMapper;
+    }
+
+    @Override
+    public Optional<Node> process(Node parent, T child, int index) {
+        return processorByAttribute
+                .computeIfAbsent(attributeMapper.apply(child), processorFactory)
+                .process(parent, child, index);
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/SQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/SQLTreeProcessor.java
new file mode 100644
index 0000000..b8d94b7
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/SQLTreeProcessor.java
@@ -0,0 +1,28 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+/**
+ * @since 4.2
+ */
+@FunctionalInterface
+public interface SQLTreeProcessor {
+    Node process(Node node);
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java
index d8830f0..2e9e68e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/TrimmingColumnNode.java
@@ -19,10 +19,10 @@
 
 package org.apache.cayenne.access.sqlbuilder.sqltree;
 
-import java.sql.Types;
-
 import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
 
+import java.sql.Types;
+
 /**
  * @since 4.2
  */
@@ -41,7 +41,7 @@
             if(isCharType() && isAllowedForTrimming()) {
                 appendRtrim(buffer);
                 appendAlias(buffer, isResult);
-            } else if(isComparisionWithClob()) {
+            } else if(isComparisonWithClob()) {
                 appendClobColumnNode(buffer);
                 appendAlias(buffer, isResult);
             } else {
@@ -54,7 +54,10 @@
         return buffer;
     }
 
-    private boolean isComparisionWithClob() {
+    private boolean isComparisonWithClob() {
+        if(isInsertOrUpdateSet()) {
+            return false;
+        }
         return (getParent().getType() == NodeType.EQUALITY
                 || getParent().getType() == NodeType.LIKE)
                 && columnNode.getAttribute() != null
@@ -75,7 +78,10 @@
     protected boolean isAllowedForTrimming() {
         Node parent = getParent();
         while(parent != null) {
-            if(parent.getType() == NodeType.JOIN || parent.getType() == NodeType.FUNCTION) {
+            if(parent.getType() == NodeType.JOIN
+                    || parent.getType() == NodeType.FUNCTION
+                    || parent.getType() == NodeType.UPDATE_SET
+                    || parent.getType() == NodeType.INSERT_COLUMNS) {
                 return false;
             }
             parent = parent.getParent();
@@ -84,9 +90,17 @@
     }
 
     protected boolean isResultNode() {
+        return isParentOfType(NodeType.RESULT);
+    }
+
+    protected boolean isInsertOrUpdateSet() {
+        return isParentOfType(NodeType.UPDATE_SET) || isParentOfType(NodeType.INSERT_COLUMNS);
+    }
+
+    protected boolean isParentOfType(NodeType nodeType) {
         Node parent = getParent();
         while(parent != null) {
-            if(parent.getType() == NodeType.RESULT) {
+            if(parent.getType() == nodeType) {
                 return true;
             }
             parent = parent.getParent();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/UpdateNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/UpdateNode.java
new file mode 100644
index 0000000..a5f0782
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/UpdateNode.java
@@ -0,0 +1,38 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class UpdateNode extends Node {
+
+    @Override
+    public Node copy() {
+        return new UpdateNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer.append("UPDATE");
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/UpdateSetNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/UpdateSetNode.java
new file mode 100644
index 0000000..7c20a60
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/UpdateSetNode.java
@@ -0,0 +1,47 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+
+/**
+ * @since 4.2
+ */
+public class UpdateSetNode extends Node {
+
+    public UpdateSetNode() {
+        super(NodeType.UPDATE_SET);
+    }
+
+    @Override
+    public Node copy() {
+        return new UpdateSetNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer.append(" SET");
+    }
+
+    @Override
+    public void appendChildrenSeparator(QuotingAppendable buffer, int childIdx) {
+        buffer.append(',');
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
index 0a52b8b..055ac2d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/ValueNode.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.access.sqlbuilder.sqltree;
 
+import java.util.function.Supplier;
+
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
@@ -101,6 +103,8 @@
                 appendValue((Persistent) val, buffer);
             } else if(val instanceof ObjectId) {
                 appendValue((ObjectId) val, buffer);
+            } else if(val instanceof Supplier) {
+                appendValue(((Supplier<?>) val).get(), buffer);
             } else if(val instanceof CharSequence) {
                 appendStringValue(buffer, (CharSequence)val);
             } else {
@@ -135,7 +139,7 @@
         // value can't be null here
         SQLGenerationContext context = buffer.getContext();
         // allow translation in out-of-context scope, to be able to use as a standalone SQL generator
-        ExtendedType extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
+        ExtendedType<?> extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
         DbAttributeBinding binding = new DbAttributeBinding(attribute);
         binding.setStatementPosition(context.getBindings().size() + 1);
         binding.setExtendedType(extendedType);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java
new file mode 100644
index 0000000..92966ed
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BaseBatchTranslator.java
@@ -0,0 +1,89 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch;
+
+import java.util.List;
+
+import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.NodeBuilder;
+import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
+import org.apache.cayenne.access.sqlbuilder.SQLGenerationVisitor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.translator.select.DefaultQuotingAppendable;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQuery;
+
+/**
+ * @since 4.2
+ * @param <T> type of the batch query to translate
+ */
+public abstract class BaseBatchTranslator<T extends BatchQuery> {
+
+    protected final BatchTranslatorContext<T> context;
+
+    protected DbAttributeBinding[] bindings;
+
+    public BaseBatchTranslator(T query, DbAdapter adapter) {
+        this.context = new BatchTranslatorContext<>(query, adapter);
+    }
+
+    public DbAttributeBinding[] getBindings() {
+        return bindings;
+    }
+
+    /**
+     * This method applies {@link org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor} to the
+     * provided SQL tree node and generates SQL string from it.
+     *
+     * @param nodeBuilder SQL tree node builder
+     * @return SQL string
+     */
+    protected String doTranslate(NodeBuilder nodeBuilder) {
+        Node node = nodeBuilder.build();
+        // convert to database flavour
+        node = context.getAdapter().getSqlTreeProcessor().process(node);
+        // generate SQL
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new DefaultQuotingAppendable(context));
+        node.visit(visitor);
+
+        bindings = context.getBindings().toArray(new DbAttributeBinding[0]);
+        return visitor.getSQLString();
+    }
+
+    abstract protected boolean isNullAttribute(DbAttribute attribute);
+
+    protected ExpressionNodeBuilder buildQualifier(List<DbAttribute> attributeList) {
+        ExpressionNodeBuilder eq = null;
+        for (DbAttribute attr : attributeList) {
+            Integer value = isNullAttribute(attr) ? null : 1;
+            ExpressionNodeBuilder next = SQLBuilder
+                    .column(attr.getName()).attribute(attr)
+                    .eq(SQLBuilder.value(value).attribute(attr));
+            if(eq == null) {
+                eq = next;
+            } else {
+                eq = eq.and(next);
+            }
+        }
+        return eq;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslatorContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslatorContext.java
new file mode 100644
index 0000000..17352dd
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/BatchTranslatorContext.java
@@ -0,0 +1,72 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import org.apache.cayenne.access.sqlbuilder.SQLGenerationContext;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.BatchQuery;
+
+/**
+ * @since 4.2
+ * @param <T> type of the {@link BatchQuery}
+ */
+class BatchTranslatorContext<T extends BatchQuery> implements SQLGenerationContext {
+
+    private final T query;
+    private final DbAdapter adapter;
+    private final List<DbAttributeBinding> bindings;
+
+    BatchTranslatorContext(T query, DbAdapter adapter) {
+        this.query = query;
+        this.adapter = adapter;
+        this.bindings = new ArrayList<>();
+    }
+
+    @Override
+    public DbAdapter getAdapter() {
+        return adapter;
+    }
+
+    @Override
+    public Collection<DbAttributeBinding> getBindings() {
+        return bindings;
+    }
+
+    @Override
+    public QuotingStrategy getQuotingStrategy() {
+        return adapter.getQuotingStrategy();
+    }
+
+    @Override
+    public DbEntity getRootDbEntity() {
+        return query.getDbEntity();
+    }
+
+    public T getQuery() {
+        return query;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorFactory.java
index 639bded..b0d8f3c 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorFactory.java
@@ -28,7 +28,7 @@
 /**
  * Default implementation of {@link BatchTranslatorFactory}.
  * 
- * @since 4.0
+ * @since 4.2
  */
 public class DefaultBatchTranslatorFactory implements BatchTranslatorFactory {
 
@@ -37,24 +37,24 @@
         if (query instanceof InsertBatchQuery) {
             return insertTranslator((InsertBatchQuery) query, adapter);
         } else if (query instanceof UpdateBatchQuery) {
-            return updateTranslator((UpdateBatchQuery) query, adapter, trimFunction);
+            return updateTranslator((UpdateBatchQuery) query, adapter);
         } else if (query instanceof DeleteBatchQuery) {
-            return deleteTranslator((DeleteBatchQuery) query, adapter, trimFunction);
+            return deleteTranslator((DeleteBatchQuery) query, adapter);
         } else {
             throw new CayenneRuntimeException("Unsupported batch query: %s", query);
         }
     }
 
-    protected BatchTranslator deleteTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction) {
-        return new DeleteBatchTranslator(query, adapter, trimFunction);
+    protected BatchTranslator deleteTranslator(DeleteBatchQuery query, DbAdapter adapter) {
+        return new DeleteBatchTranslator(query, adapter);
     }
 
     protected BatchTranslator insertTranslator(InsertBatchQuery query, DbAdapter adapter) {
         return new InsertBatchTranslator(query, adapter);
     }
 
-    protected BatchTranslator updateTranslator(UpdateBatchQuery query, DbAdapter adapter, String trimFunction) {
-        return new UpdateBatchTranslator(query, adapter, trimFunction);
+    protected BatchTranslator updateTranslator(UpdateBatchQuery query, DbAdapter adapter) {
+        return new UpdateBatchTranslator(query, adapter);
     }
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
index e5915a3..87441ac 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslator.java
@@ -19,98 +19,52 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import java.util.Iterator;
-import java.util.List;
-
+import org.apache.cayenne.access.sqlbuilder.DeleteBuilder;
+import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.query.BatchQueryRow;
 import org.apache.cayenne.query.DeleteBatchQuery;
 
 /**
- * Translator for delete BatchQueries. Creates parameterized DELETE SQL
- * statements.
+ * @since 4.2
  */
-public class DeleteBatchTranslator extends DefaultBatchTranslator {
+public class DeleteBatchTranslator extends BaseBatchTranslator<DeleteBatchQuery> implements BatchTranslator {
 
-    public DeleteBatchTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction) {
-        super(query, adapter, trimFunction);
+    public DeleteBatchTranslator(DeleteBatchQuery query, DbAdapter adapter) {
+        super(query, adapter);
     }
 
     @Override
-    protected String createSql() {
-
-        QuotingStrategy strategy = adapter.getQuotingStrategy();
-
-        StringBuilder buffer = new StringBuilder("DELETE FROM ");
-        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
-
-        applyQualifier(buffer);
-
-        return buffer.toString();
-    }
-
-    /**
-     * Appends WHERE clause to SQL string
-     */
-    protected void applyQualifier(StringBuilder buffer) {
-        buffer.append(" WHERE ");
-
-        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
-        Iterator<DbAttribute> i = deleteBatch.getDbAttributes().iterator();
-        while (i.hasNext()) {
-            DbAttribute attribute = i.next();
-            appendDbAttribute(buffer, attribute);
-            buffer.append(deleteBatch.isNull(attribute) ? " IS NULL" : " = ?");
-
-            if (i.hasNext()) {
-                buffer.append(" AND ");
-            }
-        }
+    public String getSql() {
+        DeleteBuilder deleteBuilder = SQLBuilder
+                .delete(context.getRootDbEntity().getFullyQualifiedName())
+                .where(buildQualifier(context.getQuery().getDbAttributes()));
+        return doTranslate(deleteBuilder);
     }
 
     @Override
-    protected DbAttributeBinding[] createBindings() {
-        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
-        List<DbAttribute> attributes = deleteBatch.getDbAttributes();
-        int len = attributes.size();
+    protected boolean isNullAttribute(DbAttribute attribute) {
+        return context.getQuery().isNull(attribute);
+    }
 
-        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
-
-        for (int i = 0; i < len; i++) {
-            bindings[i] = new DbAttributeBinding(attributes.get(i));
+    @Override
+    public DbAttributeBinding[] updateBindings(BatchQueryRow row) {
+        DeleteBatchQuery deleteBatch = context.getQuery();
+        for(int i=0, position=0; i<deleteBatch.getDbAttributes().size(); i++) {
+            position = updateBinding(row.getValue(i), position);
         }
-
         return bindings;
     }
 
-    @Override
-    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
-
-        int len = bindings.length;
-
-        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
-
-        for (int i = 0, j = 1; i < len; i++) {
-
-            DbAttributeBinding b = bindings[i];
-
-            // skip null attributes... they are translated as "IS NULL"
-            if (deleteBatch.isNull(b.getAttribute())) {
-                b.exclude();
-            } else {
-                Object value = row.getValue(i);
-                ExtendedType extendedType = value != null
-                        ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
-                        : adapter.getExtendedTypes().getDefaultType();
-
-                b.include(j++, value, extendedType);
-            }
+    protected int updateBinding(Object value, int position) {
+        // skip null attributes... they are translated as "IS NULL"
+        if(value != null) {
+            ExtendedType<?> extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
+            bindings[position].include(++position, value, extendedType);
         }
-
-        return bindings;
+        return position;
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
index 8c70532..aff48d7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslator.java
@@ -19,129 +19,73 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import java.util.List;
-
+import org.apache.cayenne.access.sqlbuilder.InsertBuilder;
+import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.query.BatchQueryRow;
 import org.apache.cayenne.query.InsertBatchQuery;
 
 /**
- * Translator of InsertBatchQueries.
+ * @since 4.2
  */
-public class InsertBatchTranslator extends DefaultBatchTranslator {
+public class InsertBatchTranslator extends BaseBatchTranslator<InsertBatchQuery> implements BatchTranslator {
 
     public InsertBatchTranslator(InsertBatchQuery query, DbAdapter adapter) {
-        // no trimming is needed here, so passing hardcoded NULL for trim
-        // function
-        super(query, adapter, null);
+        super(query, adapter);
     }
 
     @Override
-    protected String createSql() {
+    public String getSql() {
+        InsertBatchQuery query = context.getQuery();
+        InsertBuilder insertBuilder = SQLBuilder.insert(context.getRootDbEntity().getFullyQualifiedName());
 
-        List<DbAttribute> dbAttributes = query.getDbAttributes();
-        QuotingStrategy strategy = adapter.getQuotingStrategy();
-
-        StringBuilder buffer = new StringBuilder("INSERT INTO ");
-        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
-        buffer.append(" (");
-
-        int columnCount = 0;
-        for (DbAttribute attribute : dbAttributes) {
-
-            // attribute inclusion rule - one of the rules below must be true:
-            // (1) attribute not generated
-            // (2) attribute is generated and PK and adapter does not support
-            // generated
-            // keys
-
-            if (includeInBatch(attribute)) {
-
-                if (columnCount > 0) {
-                    buffer.append(", ");
-                }
-                buffer.append(strategy.quotedName(attribute));
-                columnCount++;
+        for(DbAttribute attribute : query.getDbAttributes()) {
+            // skip generated attributes, if needed
+            if(excludeInBatch(attribute)) {
+                continue;
             }
+            insertBuilder
+                    .column(SQLBuilder.column(attribute.getName()).attribute(attribute))
+                    // We can use here any non-null value, to create attribute binding,
+                    // actual value and ExtendedType will be set at updateBindings() call.
+                    .value(SQLBuilder.value(1).attribute(attribute));
         }
 
-        buffer.append(") VALUES (");
-
-        for (int i = 0; i < columnCount; i++) {
-            if (i > 0) {
-                buffer.append(", ");
-            }
-
-            buffer.append('?');
-        }
-        buffer.append(')');
-        return buffer.toString();
+        return doTranslate(insertBuilder);
     }
 
     @Override
-    protected DbAttributeBinding[] createBindings() {
-        List<DbAttribute> attributes = query.getDbAttributes();
-        int len = attributes.size();
-
-        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
-
-        for (int i = 0; i < len; i++) {
-            DbAttribute a = attributes.get(i);
-
-            bindings[i] = new DbAttributeBinding(a);
-
-            // include/exclude state depends on DbAttribute only and can be
-            // precompiled here
-            if (includeInBatch(a)) {
-                // setting fake position here... all we care about is that it is
-                // > -1
-                bindings[i].include(1, null, null);
-            } else {
-                bindings[i].exclude();
+    public DbAttributeBinding[] updateBindings(BatchQueryRow row) {
+        InsertBatchQuery query = context.getQuery();
+        int i=0;
+        int j=0;
+        for(DbAttribute attribute : query.getDbAttributes()) {
+            if(excludeInBatch(attribute)) {
+                i++;
+                continue;
             }
-        }
 
+            Object value = row.getValue(i++);
+            ExtendedType<?> extendedType = value != null
+                    ? context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass())
+                    : context.getAdapter().getExtendedTypes().getDefaultType();
+            bindings[j].include(++j, value, extendedType);
+        }
         return bindings;
     }
 
-    @Override
-    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
-        int len = bindings.length;
-
-        for (int i = 0, j = 1; i < len; i++) {
-
-            DbAttributeBinding b = bindings[i];
-
-            // exclusions are permanent
-            if (!b.isExcluded()) {
-                Object value = row.getValue(i);
-                ExtendedType extendedType = value != null
-                        ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
-                        : adapter.getExtendedTypes().getDefaultType();
-
-                b.include(j++, value, extendedType);
-            }
-        }
-
-        return bindings;
-    }
-
-    /**
-     * Returns true if an attribute should be included in the batch.
-     * 
-     * @since 1.2
-     */
-    protected boolean includeInBatch(DbAttribute attribute) {
+    protected boolean excludeInBatch(DbAttribute attribute) {
         // attribute inclusion rule - one of the rules below must be true:
-        // (1) attribute not generated
-        // (2) attribute is generated and PK and adapter does not support
-        // generated
-        // keys
+        //  (1) attribute not generated
+        //  (2) attribute is generated and PK and adapter does not support generated keys
+        return attribute.isGenerated() && (!attribute.isPrimaryKey() || context.getAdapter().supportsGeneratedKeys());
+    }
 
-        return !attribute.isGenerated() || (attribute.isPrimaryKey() && !adapter.supportsGeneratedKeys());
+    @Override
+    protected boolean isNullAttribute(DbAttribute attribute) {
+        return false;
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
index 12dfe8d..5c1bc2a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslator.java
@@ -1,105 +1,75 @@
-/*****************************************************************

- *   Licensed to the Apache Software Foundation (ASF) under one

- *  or more contributor license agreements.  See the NOTICE file

- *  distributed with this work for additional information

- *  regarding copyright ownership.  The ASF licenses this file

- *  to you under the Apache License, Version 2.0 (the

- *  "License"); you may not use this file except in compliance

- *  with the License.  You may obtain a copy of the License at

- *

- *    https://www.apache.org/licenses/LICENSE-2.0

- *

- *  Unless required by applicable law or agreed to in writing,

- *  software distributed under the License is distributed on an

- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY

- *  KIND, either express or implied.  See the License for the

- *  specific language governing permissions and limitations

- *  under the License.

- ****************************************************************/

-package org.apache.cayenne.access.translator.batch;

-

-import org.apache.cayenne.access.translator.DbAttributeBinding;

-import org.apache.cayenne.access.types.ExtendedType;

-import org.apache.cayenne.dba.DbAdapter;

-import org.apache.cayenne.dba.QuotingStrategy;

-import org.apache.cayenne.dba.TypesMapping;

-import org.apache.cayenne.map.DbAttribute;

-import org.apache.cayenne.query.BatchQueryRow;

-import org.apache.cayenne.query.DeleteBatchQuery;

-

-/**

- * Implementation of {@link DeleteBatchTranslator}, which uses 'soft' delete

- * (runs UPDATE and sets 'deleted' field to true instead-of running SQL DELETE)

- */

-public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {

-

-    private String deletedFieldName;

-

-    public SoftDeleteBatchTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction,

-            String deletedFieldName) {

-        super(query, adapter, trimFunction);

-        this.deletedFieldName = deletedFieldName;

-    }

-

-    @Override

-    protected String createSql() {

-

-        QuotingStrategy strategy = adapter.getQuotingStrategy();

-

-        StringBuilder buffer = new StringBuilder("UPDATE ");

-        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));

-        buffer.append(" SET ").append(strategy.quotedIdentifier(query.getDbEntity(), deletedFieldName)).append(" = ?");

-

-        applyQualifier(buffer);

-

-        return buffer.toString();

-    }

-

-    @Override

-    protected DbAttributeBinding[] createBindings() {

-

-        DbAttributeBinding[] superBindings = super.createBindings();

-

-        int slen = superBindings.length;

-

-        DbAttributeBinding[] bindings = new DbAttributeBinding[slen + 1];

-

-        DbAttribute deleteAttribute = query.getDbEntity().getAttribute(deletedFieldName);

-        String typeName = TypesMapping.getJavaBySqlType(deleteAttribute.getType());

-        ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);

-

-        bindings[0] = new DbAttributeBinding(deleteAttribute);

-        bindings[0].include(1, true, extendedType);

-        

-        System.arraycopy(superBindings, 0, bindings, 1, slen);

-

-        return bindings;

-    }

-

-    @Override

-    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {

-        int len = bindings.length;

-

-        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;

-

-        // skip position 0... Otherwise follow super algorithm

-        for (int i = 1, j = 2; i < len; i++) {

-

-            DbAttributeBinding b = bindings[i];

-

-            // skip null attributes... they are translated as "IS NULL"

-            if (deleteBatch.isNull(b.getAttribute())) {

-                b.exclude();

-            } else {

-                Object value = row.getValue(i - 1);

-                ExtendedType extendedType = value != null

-                        ? adapter.getExtendedTypes().getRegisteredType(value.getClass())

-                        : adapter.getExtendedTypes().getDefaultType();

-

-                b.include(j++, value, extendedType);

-            }

-        }

-

-        return bindings;

-    }

-}

+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch;
+
+import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
+import org.apache.cayenne.access.sqlbuilder.UpdateBuilder;
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQueryRow;
+import org.apache.cayenne.query.DeleteBatchQuery;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+
+/**
+ * @since 4.2
+ */
+public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
+
+    private final String deletedFieldName;
+
+    public SoftDeleteBatchTranslator(DeleteBatchQuery query, DbAdapter adapter, String deletedFieldName) {
+        super(query, adapter);
+        this.deletedFieldName = deletedFieldName;
+    }
+
+    @Override
+    public String getSql() {
+        DeleteBatchQuery query = context.getQuery();
+        DbAttribute deleteAttribute = query.getDbEntity().getAttribute(deletedFieldName);
+
+        UpdateBuilder updateBuilder = update(context.getRootDbEntity().getFullyQualifiedName())
+                .set(column(deletedFieldName).attribute(deleteAttribute)
+                        .eq(SQLBuilder.value(true).attribute(deleteAttribute)))
+                .where(buildQualifier(query.getDbAttributes()));
+
+        String sql = doTranslate(updateBuilder);
+
+        String typeName = TypesMapping.getJavaBySqlType(deleteAttribute.getType());
+        ExtendedType<?> extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(typeName);
+        bindings[0].include(1, true, extendedType);
+
+        return sql;
+    }
+
+    @Override
+    public DbAttributeBinding[] updateBindings(BatchQueryRow row) {
+        DeleteBatchQuery deleteBatch = context.getQuery();
+
+        for(int i=0, position=1; i<deleteBatch.getDbAttributes().size(); i++) {
+            position = updateBinding(row.getValue(i), position);
+        }
+
+        return bindings;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteTranslatorFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteTranslatorFactory.java
index 10054ba..c0eec5a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteTranslatorFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/SoftDeleteTranslatorFactory.java
@@ -1,69 +1,69 @@
-/*****************************************************************

- *   Licensed to the Apache Software Foundation (ASF) under one

- *  or more contributor license agreements.  See the NOTICE file

- *  distributed with this work for additional information

- *  regarding copyright ownership.  The ASF licenses this file

- *  to you under the Apache License, Version 2.0 (the

- *  "License"); you may not use this file except in compliance

- *  with the License.  You may obtain a copy of the License at

- *

- *    https://www.apache.org/licenses/LICENSE-2.0

- *

- *  Unless required by applicable law or agreed to in writing,

- *  software distributed under the License is distributed on an

- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY

- *  KIND, either express or implied.  See the License for the

- *  specific language governing permissions and limitations

- *  under the License.

- ****************************************************************/

-package org.apache.cayenne.access.translator.batch;

-

-import java.sql.Types;

-

-import org.apache.cayenne.access.translator.batch.BatchTranslator;

-import org.apache.cayenne.dba.DbAdapter;

-import org.apache.cayenne.map.DbAttribute;

-import org.apache.cayenne.query.DeleteBatchQuery;

-

-/**

- * Implementation of {link #BatchTranslator}, which uses 'soft' delete

- * (runs UPDATE and sets 'deleted' field to true instead-of running SQL DELETE)

- * 

- * @since 4.0

- */

-public class SoftDeleteTranslatorFactory extends DefaultBatchTranslatorFactory {

-    /**

-     * Default name of 'deleted' field

-     */

-    public static final String DEFAULT_DELETED_FIELD_NAME = "DELETED";

-

-    /**

-     * Name of 'deleted' field

-     */

-    private String deletedFieldName;

-

-    public SoftDeleteTranslatorFactory() {

-        this(DEFAULT_DELETED_FIELD_NAME);

-    }

-

-    public SoftDeleteTranslatorFactory(String deletedFieldName) {

-        this.deletedFieldName = deletedFieldName;

-    }

-

-    @Override

-    protected BatchTranslator deleteTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction) {

-

-        DbAttribute attr = query.getDbEntity().getAttribute(deletedFieldName);

-        boolean needsSoftDelete = attr != null && attr.getType() == Types.BOOLEAN;

-

-        return needsSoftDelete ? new SoftDeleteBatchTranslator(query, adapter, trimFunction, deletedFieldName) : super

-                .deleteTranslator(query, adapter, trimFunction);

-    }

-

-    /**

-     * @return name of 'deleted' field

-     */

-    public String getDeletedFieldName() {

-        return deletedFieldName;

-    }

-}

+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.batch;
+
+import java.sql.Types;
+
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.DeleteBatchQuery;
+
+/**
+ * Implementation of {link #BatchTranslator}, which uses 'soft' delete
+ * (runs UPDATE and sets 'deleted' field to true instead-of running SQL DELETE)
+ * 
+ * @since 4.2
+ */
+public class SoftDeleteTranslatorFactory extends DefaultBatchTranslatorFactory {
+    /**
+     * Default name of 'deleted' field
+     */
+    public static final String DEFAULT_DELETED_FIELD_NAME = "DELETED";
+
+    /**
+     * Name of 'deleted' field
+     */
+    private String deletedFieldName;
+
+    public SoftDeleteTranslatorFactory() {
+        this(DEFAULT_DELETED_FIELD_NAME);
+    }
+
+    public SoftDeleteTranslatorFactory(String deletedFieldName) {
+        this.deletedFieldName = deletedFieldName;
+    }
+
+    @Override
+    protected BatchTranslator deleteTranslator(DeleteBatchQuery query, DbAdapter adapter) {
+
+        DbAttribute attr = query.getDbEntity().getAttribute(deletedFieldName);
+        boolean needsSoftDelete = attr != null && attr.getType() == Types.BOOLEAN;
+
+        return needsSoftDelete
+                ? new SoftDeleteBatchTranslator(query, adapter, deletedFieldName)
+                : super.deleteTranslator(query, adapter);
+    }
+
+    /**
+     * @return name of 'deleted' field
+     */
+    public String getDeletedFieldName() {
+        return deletedFieldName;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
index 8b1c558..0f4e385 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslator.java
@@ -19,128 +19,69 @@
 
 package org.apache.cayenne.access.translator.batch;
 
-import java.util.Iterator;
-import java.util.List;
-
+import org.apache.cayenne.access.sqlbuilder.SQLBuilder;
+import org.apache.cayenne.access.sqlbuilder.UpdateBuilder;
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
-import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.query.BatchQueryRow;
 import org.apache.cayenne.query.UpdateBatchQuery;
 
 /**
- * A translator for UpdateBatchQueries that produces parameterized SQL.
+ * @since 4.2
  */
-public class UpdateBatchTranslator extends DefaultBatchTranslator {
+public class UpdateBatchTranslator extends BaseBatchTranslator<UpdateBatchQuery> implements BatchTranslator {
 
-    public UpdateBatchTranslator(UpdateBatchQuery query, DbAdapter adapter, String trimFunction) {
-        super(query, adapter, trimFunction);
+    public UpdateBatchTranslator(UpdateBatchQuery query, DbAdapter adapter) {
+        super(query, adapter);
     }
 
     @Override
-    protected String createSql() {
-        UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
+    public String getSql() {
+        UpdateBatchQuery query = context.getQuery();
 
-        QuotingStrategy strategy = adapter.getQuotingStrategy();
-
-        List<DbAttribute> qualifierAttributes = updateBatch.getQualifierAttributes();
-        List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
-
-        StringBuilder buffer = new StringBuilder("UPDATE ");
-        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
-        buffer.append(" SET ");
-
-        int len = updatedDbAttributes.size();
-        for (int i = 0; i < len; i++) {
-            if (i > 0) {
-                buffer.append(", ");
-            }
-
-            DbAttribute attribute = updatedDbAttributes.get(i);
-            buffer.append(strategy.quotedName(attribute));
-            buffer.append(" = ?");
+        UpdateBuilder updateBuilder = SQLBuilder.update(context.getRootDbEntity().getFullyQualifiedName());
+        for (DbAttribute attr : query.getUpdatedAttributes()) {
+            updateBuilder.set(SQLBuilder
+                    .column(attr.getName()).attribute(attr)
+                    .eq(SQLBuilder.value(1).attribute(attr))
+            );
         }
+        updateBuilder.where(buildQualifier(query.getQualifierAttributes()));
 
-        buffer.append(" WHERE ");
-
-        Iterator<DbAttribute> i = qualifierAttributes.iterator();
-        while (i.hasNext()) {
-            DbAttribute attribute = i.next();
-            appendDbAttribute(buffer, attribute);
-            buffer.append(updateBatch.isNull(attribute) ? " IS NULL" : " = ?");
-
-            if (i.hasNext()) {
-                buffer.append(" AND ");
-            }
-        }
-
-        return buffer.toString();
+        return doTranslate(updateBuilder);
     }
 
     @Override
-    protected DbAttributeBinding[] createBindings() {
-        UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
-
-        List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
-        List<DbAttribute> qualifierAttributes = updateBatch.getQualifierAttributes();
-
-        int ul = updatedDbAttributes.size();
-        int ql = qualifierAttributes.size();
-
-        DbAttributeBinding[] bindings = new DbAttributeBinding[ul + ql];
-
-        for (int i = 0; i < ul; i++) {
-            bindings[i] = new DbAttributeBinding(updatedDbAttributes.get(i));
-        }
-
-        for (int i = 0; i < ql; i++) {
-            bindings[ul + i] = new DbAttributeBinding(qualifierAttributes.get(i));
-        }
-
-        return bindings;
+    protected boolean isNullAttribute(DbAttribute attribute) {
+        return context.getQuery().isNull(attribute);
     }
 
     @Override
-    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+    public DbAttributeBinding[] updateBindings(BatchQueryRow row) {
+        UpdateBatchQuery updateBatch = context.getQuery();
 
-        UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
-
-        List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
-        List<DbAttribute> qualifierAttributes = updateBatch.getQualifierAttributes();
-
-        int ul = updatedDbAttributes.size();
-        int ql = qualifierAttributes.size();
-
-        int j = 1;
-
-        for (int i = 0; i < ul; i++) {
+        int i = 0;
+        int j = 0;
+        for(; i < updateBatch.getUpdatedAttributes().size(); i++) {
             Object value = row.getValue(i);
-            ExtendedType extendedType = value != null
-                    ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
-                    : adapter.getExtendedTypes().getDefaultType();
-
-            bindings[i].include(j++, value, extendedType);
+            ExtendedType<?> extendedType = value == null
+                ? context.getAdapter().getExtendedTypes().getDefaultType()
+                : context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
+            bindings[j].include(++j, value, extendedType);
         }
 
-        for (int i = 0; i < ql; i++) {
-
-            DbAttribute a = qualifierAttributes.get(i);
-
-            // skip null attributes... they are translated as "IS NULL"
-            if (updateBatch.isNull(a)) {
+        for(DbAttribute attribute : updateBatch.getQualifierAttributes()) {
+            if(updateBatch.isNull(attribute)) {
+                i++;
                 continue;
             }
-
-            Object value = row.getValue(ul + i);
-            ExtendedType extendedType = value != null
-                    ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
-                    : adapter.getExtendedTypes().getDefaultType();
-
-            bindings[ul + i].include(j++, value, extendedType);
+            Object value = row.getValue(i);
+            ExtendedType<?> extendedType = context.getAdapter().getExtendedTypes().getRegisteredType(value.getClass());
+            bindings[j].include(++j, value, extendedType);
+            i++;
         }
-
         return bindings;
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslator.java
similarity index 95%
rename from cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
rename to cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslator.java
index 79e749c..381db10 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslator.java
@@ -16,11 +16,12 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.access.translator.batch;
+package org.apache.cayenne.access.translator.batch.legacy;
 
 import java.sql.Types;
 
 import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.translator.batch.BatchTranslator;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
 import org.apache.cayenne.map.DbAttribute;
@@ -31,7 +32,9 @@
  * Superclass of batch query translators.
  * 
  * @since 4.0
+ * @deprecated since 4.2
  */
+@Deprecated
 public abstract class DefaultBatchTranslator implements BatchTranslator {
 
     protected BatchQuery query;
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslatorFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslatorFactory.java
new file mode 100644
index 0000000..d246fe3
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslatorFactory.java
@@ -0,0 +1,64 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import org.apache.cayenne.CayenneRuntimeException;
+import org.apache.cayenne.access.translator.batch.BatchTranslator;
+import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.query.BatchQuery;
+import org.apache.cayenne.query.DeleteBatchQuery;
+import org.apache.cayenne.query.InsertBatchQuery;
+import org.apache.cayenne.query.UpdateBatchQuery;
+
+/**
+ * Default implementation of {@link BatchTranslatorFactory}.
+ * 
+ * @since 4.0
+ * @deprecated since 4.2
+ */
+@Deprecated
+public class DefaultBatchTranslatorFactory implements BatchTranslatorFactory {
+
+    @Override
+    public BatchTranslator translator(BatchQuery query, DbAdapter adapter, String trimFunction) {
+        if (query instanceof InsertBatchQuery) {
+            return insertTranslator((InsertBatchQuery) query, adapter);
+        } else if (query instanceof UpdateBatchQuery) {
+            return updateTranslator((UpdateBatchQuery) query, adapter, trimFunction);
+        } else if (query instanceof DeleteBatchQuery) {
+            return deleteTranslator((DeleteBatchQuery) query, adapter, trimFunction);
+        } else {
+            throw new CayenneRuntimeException("Unsupported batch query: %s", query);
+        }
+    }
+
+    protected BatchTranslator deleteTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction) {
+        return new DeleteBatchTranslator(query, adapter, trimFunction);
+    }
+
+    protected BatchTranslator insertTranslator(InsertBatchQuery query, DbAdapter adapter) {
+        return new InsertBatchTranslator(query, adapter);
+    }
+
+    protected BatchTranslator updateTranslator(UpdateBatchQuery query, DbAdapter adapter, String trimFunction) {
+        return new UpdateBatchTranslator(query, adapter, trimFunction);
+    }
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DeleteBatchTranslator.java
new file mode 100644
index 0000000..afe7196
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/DeleteBatchTranslator.java
@@ -0,0 +1,118 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQueryRow;
+import org.apache.cayenne.query.DeleteBatchQuery;
+
+/**
+ * Translator for delete BatchQueries. Creates parameterized DELETE SQL
+ * statements.
+ * @deprecated since 4.2
+ */
+@Deprecated
+public class DeleteBatchTranslator extends DefaultBatchTranslator {
+
+    public DeleteBatchTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction) {
+        super(query, adapter, trimFunction);
+    }
+
+    @Override
+    protected String createSql() {
+
+        QuotingStrategy strategy = adapter.getQuotingStrategy();
+
+        StringBuilder buffer = new StringBuilder("DELETE FROM ");
+        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
+
+        applyQualifier(buffer);
+
+        return buffer.toString();
+    }
+
+    /**
+     * Appends WHERE clause to SQL string
+     */
+    protected void applyQualifier(StringBuilder buffer) {
+        buffer.append(" WHERE ");
+
+        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
+        Iterator<DbAttribute> i = deleteBatch.getDbAttributes().iterator();
+        while (i.hasNext()) {
+            DbAttribute attribute = i.next();
+            appendDbAttribute(buffer, attribute);
+            buffer.append(deleteBatch.isNull(attribute) ? " IS NULL" : " = ?");
+
+            if (i.hasNext()) {
+                buffer.append(" AND ");
+            }
+        }
+    }
+
+    @Override
+    protected DbAttributeBinding[] createBindings() {
+        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
+        List<DbAttribute> attributes = deleteBatch.getDbAttributes();
+        int len = attributes.size();
+
+        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
+
+        for (int i = 0; i < len; i++) {
+            bindings[i] = new DbAttributeBinding(attributes.get(i));
+        }
+
+        return bindings;
+    }
+
+    @Override
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+
+        int len = bindings.length;
+
+        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
+
+        for (int i = 0, j = 1; i < len; i++) {
+
+            DbAttributeBinding b = bindings[i];
+
+            // skip null attributes... they are translated as "IS NULL"
+            if (deleteBatch.isNull(b.getAttribute())) {
+                b.exclude();
+            } else {
+                Object value = row.getValue(i);
+                ExtendedType extendedType = value != null
+                        ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
+                        : adapter.getExtendedTypes().getDefaultType();
+
+                b.include(j++, value, extendedType);
+            }
+        }
+
+        return bindings;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/InsertBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/InsertBatchTranslator.java
new file mode 100644
index 0000000..3bc5927
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/InsertBatchTranslator.java
@@ -0,0 +1,149 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import java.util.List;
+
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQueryRow;
+import org.apache.cayenne.query.InsertBatchQuery;
+
+/**
+ * Translator of InsertBatchQueries.
+ * @deprecated since 4.2
+ */
+@Deprecated
+public class InsertBatchTranslator extends DefaultBatchTranslator {
+
+    public InsertBatchTranslator(InsertBatchQuery query, DbAdapter adapter) {
+        // no trimming is needed here, so passing hardcoded NULL for trim
+        // function
+        super(query, adapter, null);
+    }
+
+    @Override
+    protected String createSql() {
+
+        List<DbAttribute> dbAttributes = query.getDbAttributes();
+        QuotingStrategy strategy = adapter.getQuotingStrategy();
+
+        StringBuilder buffer = new StringBuilder("INSERT INTO ");
+        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
+        buffer.append(" (");
+
+        int columnCount = 0;
+        for (DbAttribute attribute : dbAttributes) {
+
+            // attribute inclusion rule - one of the rules below must be true:
+            // (1) attribute not generated
+            // (2) attribute is generated and PK and adapter does not support
+            // generated
+            // keys
+
+            if (includeInBatch(attribute)) {
+
+                if (columnCount > 0) {
+                    buffer.append(", ");
+                }
+                buffer.append(strategy.quotedName(attribute));
+                columnCount++;
+            }
+        }
+
+        buffer.append(") VALUES (");
+
+        for (int i = 0; i < columnCount; i++) {
+            if (i > 0) {
+                buffer.append(", ");
+            }
+
+            buffer.append('?');
+        }
+        buffer.append(')');
+        return buffer.toString();
+    }
+
+    @Override
+    protected DbAttributeBinding[] createBindings() {
+        List<DbAttribute> attributes = query.getDbAttributes();
+        int len = attributes.size();
+
+        DbAttributeBinding[] bindings = new DbAttributeBinding[len];
+
+        for (int i = 0; i < len; i++) {
+            DbAttribute a = attributes.get(i);
+
+            bindings[i] = new DbAttributeBinding(a);
+
+            // include/exclude state depends on DbAttribute only and can be
+            // precompiled here
+            if (includeInBatch(a)) {
+                // setting fake position here... all we care about is that it is
+                // > -1
+                bindings[i].include(1, null, null);
+            } else {
+                bindings[i].exclude();
+            }
+        }
+
+        return bindings;
+    }
+
+    @Override
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+        int len = bindings.length;
+
+        for (int i = 0, j = 1; i < len; i++) {
+
+            DbAttributeBinding b = bindings[i];
+
+            // exclusions are permanent
+            if (!b.isExcluded()) {
+                Object value = row.getValue(i);
+                ExtendedType extendedType = value != null
+                        ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
+                        : adapter.getExtendedTypes().getDefaultType();
+
+                b.include(j++, value, extendedType);
+            }
+        }
+
+        return bindings;
+    }
+
+    /**
+     * Returns true if an attribute should be included in the batch.
+     * 
+     * @since 1.2
+     */
+    protected boolean includeInBatch(DbAttribute attribute) {
+        // attribute inclusion rule - one of the rules below must be true:
+        // (1) attribute not generated
+        // (2) attribute is generated and PK and adapter does not support
+        // generated
+        // keys
+
+        return !attribute.isGenerated() || (attribute.isPrimaryKey() && !adapter.supportsGeneratedKeys());
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteBatchTranslator.java
new file mode 100644
index 0000000..b68017f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteBatchTranslator.java
@@ -0,0 +1,110 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import java.util.Objects;
+
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.dba.TypesMapping;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQueryRow;
+import org.apache.cayenne.query.DeleteBatchQuery;
+
+/**
+ * Implementation of {@link DeleteBatchTranslator}, which uses 'soft' delete
+ * (runs UPDATE and sets 'deleted' field to true instead-of running SQL DELETE)
+ *
+ * @deprecated since 4.2
+ */
+@Deprecated
+public class SoftDeleteBatchTranslator extends DeleteBatchTranslator {
+
+    private String deletedFieldName;
+
+    public SoftDeleteBatchTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction,
+            String deletedFieldName) {
+        super(query, adapter, trimFunction);
+        this.deletedFieldName = Objects.requireNonNull(deletedFieldName);
+    }
+
+    @Override
+    protected String createSql() {
+
+        QuotingStrategy strategy = adapter.getQuotingStrategy();
+
+        StringBuilder buffer = new StringBuilder("UPDATE ");
+        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
+        buffer.append(" SET ").append(strategy.quotedIdentifier(query.getDbEntity(), deletedFieldName)).append(" = ?");
+
+        applyQualifier(buffer);
+
+        return buffer.toString();
+    }
+
+    @Override
+    protected DbAttributeBinding[] createBindings() {
+
+        DbAttributeBinding[] superBindings = super.createBindings();
+
+        int slen = superBindings.length;
+
+        DbAttributeBinding[] bindings = new DbAttributeBinding[slen + 1];
+
+        DbAttribute deleteAttribute = Objects.requireNonNull(query.getDbEntity().getAttribute(deletedFieldName));
+        String typeName = TypesMapping.getJavaBySqlType(deleteAttribute.getType());
+        ExtendedType extendedType = adapter.getExtendedTypes().getRegisteredType(typeName);
+
+        bindings[0] = new DbAttributeBinding(deleteAttribute);
+        bindings[0].include(1, true, extendedType);
+        
+        System.arraycopy(superBindings, 0, bindings, 1, slen);
+
+        return bindings;
+    }
+
+    @Override
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+        int len = bindings.length;
+
+        DeleteBatchQuery deleteBatch = (DeleteBatchQuery) query;
+
+        // skip position 0... Otherwise follow super algorithm
+        for (int i = 1, j = 2; i < len; i++) {
+
+            DbAttributeBinding b = bindings[i];
+
+            // skip null attributes... they are translated as "IS NULL"
+            if (deleteBatch.isNull(b.getAttribute())) {
+                b.exclude();
+            } else {
+                Object value = row.getValue(i - 1);
+                ExtendedType extendedType = value != null
+                        ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
+                        : adapter.getExtendedTypes().getDefaultType();
+
+                b.include(j++, value, extendedType);
+            }
+        }
+
+        return bindings;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteTranslatorFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteTranslatorFactory.java
new file mode 100644
index 0000000..1fa838e
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteTranslatorFactory.java
@@ -0,0 +1,73 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import java.sql.Types;
+import java.util.Objects;
+
+import org.apache.cayenne.access.translator.batch.BatchTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.DeleteBatchQuery;
+
+/**
+ * Implementation of {link #BatchTranslator}, which uses 'soft' delete
+ * (runs UPDATE and sets 'deleted' field to true instead-of running SQL DELETE)
+ * 
+ * @since 4.0
+ * @deprecated since 4.2
+ */
+@Deprecated
+public class SoftDeleteTranslatorFactory extends DefaultBatchTranslatorFactory {
+    /**
+     * Default name of 'deleted' field
+     */
+    public static final String DEFAULT_DELETED_FIELD_NAME = "DELETED";
+
+    /**
+     * Name of 'deleted' field
+     */
+    private String deletedFieldName;
+
+    public SoftDeleteTranslatorFactory() {
+        this(DEFAULT_DELETED_FIELD_NAME);
+    }
+
+    public SoftDeleteTranslatorFactory(String deletedFieldName) {
+        this.deletedFieldName = Objects.requireNonNull(deletedFieldName);
+    }
+
+    @Override
+    protected BatchTranslator deleteTranslator(DeleteBatchQuery query, DbAdapter adapter, String trimFunction) {
+
+        DbAttribute attr = query.getDbEntity().getAttribute(deletedFieldName);
+        boolean needsSoftDelete = attr != null && attr.getType() == Types.BOOLEAN;
+
+        return needsSoftDelete
+                ? new SoftDeleteBatchTranslator(query, adapter, trimFunction, deletedFieldName)
+                : super.deleteTranslator(query, adapter, trimFunction);
+    }
+
+    /**
+     * @return name of 'deleted' field
+     */
+    public String getDeletedFieldName() {
+        return deletedFieldName;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/UpdateBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/UpdateBatchTranslator.java
new file mode 100644
index 0000000..36e851b
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/batch/legacy/UpdateBatchTranslator.java
@@ -0,0 +1,148 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.cayenne.access.translator.DbAttributeBinding;
+import org.apache.cayenne.access.types.ExtendedType;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.QuotingStrategy;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.query.BatchQueryRow;
+import org.apache.cayenne.query.UpdateBatchQuery;
+
+/**
+ * A translator for UpdateBatchQueries that produces parameterized SQL.
+ * @deprecated since 4.2
+ */
+@Deprecated
+public class UpdateBatchTranslator extends DefaultBatchTranslator {
+
+    public UpdateBatchTranslator(UpdateBatchQuery query, DbAdapter adapter, String trimFunction) {
+        super(query, adapter, trimFunction);
+    }
+
+    @Override
+    protected String createSql() {
+        UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
+
+        QuotingStrategy strategy = adapter.getQuotingStrategy();
+
+        List<DbAttribute> qualifierAttributes = updateBatch.getQualifierAttributes();
+        List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
+
+        StringBuilder buffer = new StringBuilder("UPDATE ");
+        buffer.append(strategy.quotedFullyQualifiedName(query.getDbEntity()));
+        buffer.append(" SET ");
+
+        int len = updatedDbAttributes.size();
+        for (int i = 0; i < len; i++) {
+            if (i > 0) {
+                buffer.append(", ");
+            }
+
+            DbAttribute attribute = updatedDbAttributes.get(i);
+            buffer.append(strategy.quotedName(attribute));
+            buffer.append(" = ?");
+        }
+
+        buffer.append(" WHERE ");
+
+        Iterator<DbAttribute> i = qualifierAttributes.iterator();
+        while (i.hasNext()) {
+            DbAttribute attribute = i.next();
+            appendDbAttribute(buffer, attribute);
+            buffer.append(updateBatch.isNull(attribute) ? " IS NULL" : " = ?");
+
+            if (i.hasNext()) {
+                buffer.append(" AND ");
+            }
+        }
+
+        return buffer.toString();
+    }
+
+    @Override
+    protected DbAttributeBinding[] createBindings() {
+        UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
+
+        List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
+        List<DbAttribute> qualifierAttributes = updateBatch.getQualifierAttributes();
+
+        int ul = updatedDbAttributes.size();
+        int ql = qualifierAttributes.size();
+
+        DbAttributeBinding[] bindings = new DbAttributeBinding[ul + ql];
+
+        for (int i = 0; i < ul; i++) {
+            bindings[i] = new DbAttributeBinding(updatedDbAttributes.get(i));
+        }
+
+        for (int i = 0; i < ql; i++) {
+            bindings[ul + i] = new DbAttributeBinding(qualifierAttributes.get(i));
+        }
+
+        return bindings;
+    }
+
+    @Override
+    protected DbAttributeBinding[] doUpdateBindings(BatchQueryRow row) {
+
+        UpdateBatchQuery updateBatch = (UpdateBatchQuery) query;
+
+        List<DbAttribute> updatedDbAttributes = updateBatch.getUpdatedAttributes();
+        List<DbAttribute> qualifierAttributes = updateBatch.getQualifierAttributes();
+
+        int ul = updatedDbAttributes.size();
+        int ql = qualifierAttributes.size();
+
+        int j = 1;
+
+        for (int i = 0; i < ul; i++) {
+            Object value = row.getValue(i);
+            ExtendedType extendedType = value != null
+                    ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
+                    : adapter.getExtendedTypes().getDefaultType();
+
+            bindings[i].include(j++, value, extendedType);
+        }
+
+        for (int i = 0; i < ql; i++) {
+
+            DbAttribute a = qualifierAttributes.get(i);
+
+            // skip null attributes... they are translated as "IS NULL"
+            if (updateBatch.isNull(a)) {
+                continue;
+            }
+
+            Object value = row.getValue(ul + i);
+            ExtendedType extendedType = value != null
+                    ? adapter.getExtendedTypes().getRegisteredType(value.getClass())
+                    : adapter.getExtendedTypes().getDefaultType();
+
+            bindings[ul + i].include(j++, value, extendedType);
+        }
+
+        return bindings;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java
index 36e25fa..5491b48 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/BaseSQLTreeProcessor.java
@@ -19,8 +19,6 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.util.function.Function;
-
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.DistinctNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
@@ -28,6 +26,7 @@
 import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.sqlbuilder.sqltree.SimpleNodeTreeVisitor;
 import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
 
@@ -35,10 +34,10 @@
 /**
  * @since 4.2
  */
-public class BaseSQLTreeProcessor extends SimpleNodeTreeVisitor implements Function<Node, Node> {
+public class BaseSQLTreeProcessor extends SimpleNodeTreeVisitor implements SQLTreeProcessor {
 
     @Override
-    public Node apply(Node node) {
+    public Node process(Node node) {
         node.visit(this);
         return node;
     }
@@ -70,11 +69,11 @@
     protected void onUndefinedNode(Node parent, Node child, int index) {
     }
 
-    protected void replaceChild(Node parent, int index, Node newChild) {
+    protected static void replaceChild(Node parent, int index, Node newChild) {
         replaceChild(parent, index, newChild, true);
     }
 
-    protected void replaceChild(Node parent, int index, Node newChild, boolean transferChildren) {
+    protected static void replaceChild(Node parent, int index, Node newChild, boolean transferChildren) {
         if (transferChildren) {
             Node oldChild = parent.getChild(index);
             for (int i = 0; i < oldChild.getChildrenCount(); i++) {
@@ -84,6 +83,12 @@
         parent.replaceChild(index, newChild);
     }
 
+    protected static Node wrapInFunction(Node node, String function) {
+        FunctionNode functionNode = new FunctionNode(function, null);
+        functionNode.addChild(node);
+        return functionNode;
+    }
+
     @Override
     public boolean onChildNodeStart(Node parent, Node child, int index, boolean hasMore) {
         switch (child.getType()) {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java
index 6d35c64..134fd36 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultQuotingAppendable.java
@@ -20,6 +20,7 @@
 package org.apache.cayenne.access.translator.select;
 
 import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.SQLGenerationContext;
 import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable;
 
 /**
@@ -27,9 +28,9 @@
  */
 public class DefaultQuotingAppendable extends StringBuilderAppendable {
 
-    private final TranslatorContext context;
+    private final SQLGenerationContext context;
 
-    public DefaultQuotingAppendable(TranslatorContext context) {
+    public DefaultQuotingAppendable(SQLGenerationContext context) {
         super();
         this.context = context;
     }
@@ -41,7 +42,7 @@
     }
 
     @Override
-    public TranslatorContext getContext() {
+    public SQLGenerationContext getContext() {
         return context;
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
index c1e3b7b..faa7fb4 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DefaultSelectTranslator.java
@@ -77,6 +77,10 @@
         this.context = new TranslatorContext(query, adapter, entityResolver, null);
     }
 
+    /**
+     * @deprecated since 4.2 as {@link SelectQuery} is deprecated.
+     */
+    @Deprecated
     public DefaultSelectTranslator(SelectQuery<?> query, DbAdapter adapter, EntityResolver entityResolver) {
         this(new SelectQueryWrapper(query), adapter, entityResolver);
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
index 5db957c..7f80259 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractor.java
@@ -109,6 +109,10 @@
     @Override
     public boolean visitAttribute(AttributeProperty property) {
         ObjAttribute oa = property.getAttribute();
+        if(oa.isLazy()) {
+            return true;
+        }
+
         PathTranslationResult result = pathTranslator.translatePath(oa.getEntity(), property.getName(), prefix);
 
         int count = result.getDbAttributes().size();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java
index 42be789..211a55a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/GroupByStage.java
@@ -19,6 +19,9 @@
 
 package org.apache.cayenne.access.translator.select;
 
+import org.apache.cayenne.exp.parser.ASTAggregateFunctionCall;
+import org.apache.cayenne.query.Ordering;
+
 /**
  * @since 4.2
  */
@@ -49,6 +52,14 @@
             }
         }
 
+        if(context.getQuery().getOrderings() != null) {
+            for(Ordering ordering : context.getQuery().getOrderings()) {
+                if(ordering.getSortSpec() instanceof ASTAggregateFunctionCall) {
+                    return true;
+                }
+            }
+        }
+
         return false;
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java
index 7cde628..d3085d6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/OrderingStage.java
@@ -55,10 +55,13 @@
         }
 
         // If query is DISTINCT then we need to add all ORDER BY clauses as result columns
-        if(shouldAddToResult(context, exp)) {
+        if(!context.isDistinctSuppression()) {
             // TODO: need to check duplicates?
             // need UPPER() function here too, as some DB expect exactly the same expression in select and in ordering
-            context.addResultNode(nodeBuilder.build().deepCopy());
+            ResultNodeDescriptor descriptor = context.addResultNode(nodeBuilder.build().deepCopy());
+            if(exp instanceof ASTAggregateFunctionCall) {
+                descriptor.setAggregate(true);
+            }
         }
 
         OrderingNodeBuilder orderingNodeBuilder = order(nodeBuilder);
@@ -68,14 +71,4 @@
         context.getSelectBuilder().orderBy(orderingNodeBuilder);
     }
 
-    private boolean shouldAddToResult(TranslatorContext context, Expression exp) {
-        if(context.isDistinctSuppression()) {
-            return false;
-        }
-        if(exp instanceof ASTAggregateFunctionCall) {
-            return false;
-        }
-        return true;
-    }
-
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
index 885c916..3aa795d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/PrefetchNodeStage.java
@@ -32,8 +32,11 @@
 import org.apache.cayenne.map.ObjRelationship;
 import org.apache.cayenne.query.PrefetchSelectQuery;
 import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.QueryMetadata;
 import org.apache.cayenne.query.Select;
 import org.apache.cayenne.reflect.ClassDescriptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.table;
 
@@ -42,6 +45,8 @@
  */
 class PrefetchNodeStage implements TranslationStage {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(SelectTranslator.class);
+
     @Override
     public void perform(TranslatorContext context) {
         updatePrefetchNodes(context);
@@ -62,12 +67,14 @@
     }
 
     private void processJoint(TranslatorContext context) {
-        PrefetchTreeNode prefetch = context.getMetadata().getPrefetchTree();
+        QueryMetadata queryMetadata = context.getMetadata();
+        PrefetchTreeNode prefetch = queryMetadata.getPrefetchTree();
         if(prefetch == null) {
             return;
         }
 
-        ObjEntity objEntity = context.getMetadata().getObjEntity();
+        ObjEntity objEntity = queryMetadata.getObjEntity();
+        boolean warnPrefetchWithLimit = false;
 
         for(PrefetchTreeNode node : prefetch.adjacentJointNodes()) {
             Expression prefetchExp = ExpressionFactory.exp(node.getPath());
@@ -94,6 +101,17 @@
 
             DescriptorColumnExtractor columnExtractor = new DescriptorColumnExtractor(context, prefetchClassDescriptor);
             columnExtractor.extract("p:" + dbPath);
+
+            if(!warnPrefetchWithLimit && targetRel.isToMany()
+                    && (queryMetadata.getFetchLimit() > 0 || queryMetadata.getFetchOffset() > 0)) {
+                warnPrefetchWithLimit = true;
+            }
+        }
+
+        // warn about a potentially faulty joint prefetch + limit combination
+        if(warnPrefetchWithLimit) {
+            LOGGER.warn("The query uses both limit/offset and a joint prefetch, this most probably will lead to an incorrect result. " +
+                    "Either use disjointById prefetch or get a full result set.");
         }
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
index 64ae657..765b149 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/QualifierTranslator.java
@@ -19,34 +19,17 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.util.ArrayDeque;
-import java.util.Collection;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.EmbeddableObject;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.access.sqlbuilder.ExpressionNodeBuilder;
 import org.apache.cayenne.access.sqlbuilder.ValueNodeBuilder;
-import org.apache.cayenne.access.sqlbuilder.sqltree.BetweenNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.BitwiseNotNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.EmptyNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.EqualNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.access.sqlbuilder.sqltree.NotEqualNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.NotNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.*;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.TraversalHandler;
+import org.apache.cayenne.exp.parser.ASTCustomOperator;
 import org.apache.cayenne.exp.parser.ASTDbIdPath;
 import org.apache.cayenne.exp.parser.ASTDbPath;
 import org.apache.cayenne.exp.parser.ASTFullObject;
@@ -55,17 +38,15 @@
 import org.apache.cayenne.exp.parser.ASTSubquery;
 import org.apache.cayenne.exp.parser.PatternMatchNode;
 import org.apache.cayenne.exp.parser.SimpleNode;
-import org.apache.cayenne.exp.property.BaseProperty;
 import org.apache.cayenne.exp.property.Property;
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
 import org.apache.cayenne.map.Embeddable;
 
-import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.aliased;
-import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.function;
-import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.table;
-import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.value;
+import java.util.*;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
 import static org.apache.cayenne.exp.Expression.*;
 
 /**
@@ -211,6 +192,9 @@
             case ASTERISK:
                 return new TextNode(' ' + expToStr(node.getType()));
 
+            case CUSTOM_OP:
+                return new OpExpressionNode(((ASTCustomOperator)node).getOperator());
+
             case EXISTS:
                 return new FunctionNode("EXISTS", null, false);
             case NOT_EXISTS:
@@ -274,7 +258,7 @@
         expressionsToSkip.add(node);
         expressionsToSkip.add(parentNode);
 
-        return buildMultiValueComparision(result, valueSnapshot);
+        return buildMultiValueComparison(result, valueSnapshot);
     }
 
     private Map<String, Object> getEmbeddableValueSnapshot(Embeddable embeddable, Expression node, Expression parentNode) {
@@ -315,10 +299,23 @@
             valueSnapshot = relationship.srcFkSnapshotWithTargetSnapshot(valueSnapshot);
         }
 
+        // build compound PK/FK comparison node
+        Node multiValueComparison = buildMultiValueComparison(result, valueSnapshot);
+
+        // replace current node with multi value comparison
+        Node currentNodeParent = currentNode.getParent();
+        currentNodeParent.replaceChild(currentNodeParent.getChildrenCount() - 1, multiValueComparison);
+        multiValueComparison.setParent(currentNodeParent);
+        currentNode = currentNodeParent;
+
+        // we should skip all related nodes as we build this part of the tree manually
         expressionsToSkip.add(node);
         expressionsToSkip.add(parentNode);
+        for(int i=0; i<parentNode.getOperandCount(); i++) {
+            expressionsToSkip.add(parentNode.getOperand(i));
+        }
 
-        return buildMultiValueComparision(result, valueSnapshot);
+        return null;
     }
 
     private Map<String, Object> getMultiAttributeValueSnapshot(Expression node, Expression parentNode) {
@@ -334,8 +331,8 @@
             } else if(operand instanceof ObjectId) {
                 return  ((ObjectId) operand).getIdSnapshot();
             } else if(operand instanceof ASTObjPath) {
-                // TODO: support comparision of multi attribute ObjPath with other multi attribute ObjPath
-                throw new UnsupportedOperationException("Comparision of multiple attributes not supported for ObjPath");
+                // TODO: support comparison of multi attribute ObjPath with other multi attribute ObjPath
+                throw new UnsupportedOperationException("Comparison of multiple attributes not supported for ObjPath");
             }
         }
 
@@ -343,7 +340,7 @@
                 "List or Persistent object required.");
     }
 
-    private Node buildMultiValueComparision(PathTranslationResult result, Map<String, Object> valueSnapshot) {
+    private Node buildMultiValueComparison(PathTranslationResult result, Map<String, Object> valueSnapshot) {
         ExpressionNodeBuilder expressionNodeBuilder = null;
         ExpressionNodeBuilder eq;
 
@@ -369,7 +366,7 @@
             case NOT_IN: case IN: case NOT_BETWEEN: case BETWEEN: case NOT:
             case BITWISE_NOT: case EQUAL_TO: case NOT_EQUAL_TO: case LIKE: case NOT_LIKE:
             case LIKE_IGNORE_CASE: case NOT_LIKE_IGNORE_CASE: case OBJ_PATH: case DBID_PATH: case DB_PATH:
-            case FUNCTION_CALL: case ADD: case SUBTRACT: case MULTIPLY: case DIVIDE: case NEGATIVE:
+            case FUNCTION_CALL: case ADD: case SUBTRACT: case MULTIPLY: case DIVIDE: case NEGATIVE: case CUSTOM_OP:
             case BITWISE_AND: case BITWISE_LEFT_SHIFT: case BITWISE_OR: case BITWISE_RIGHT_SHIFT: case BITWISE_XOR:
             case OR: case AND: case LESS_THAN: case LESS_THAN_EQUAL_TO: case GREATER_THAN: case GREATER_THAN_EQUAL_TO:
             case TRUE: case FALSE: case ASTERISK: case EXISTS: case NOT_EXISTS: case SUBQUERY: case ENCLOSING_OBJECT: case FULL_OBJECT:
@@ -466,14 +463,14 @@
                 return ">=";
             case ADD:
                 return "+";
+            case NEGATIVE:
             case SUBTRACT:
                 return "-";
             case MULTIPLY:
+            case ASTERISK:
                 return "*";
             case DIVIDE:
                 return "/";
-            case NEGATIVE:
-                return "-";
             case BITWISE_AND:
                 return "&";
             case BITWISE_OR:
@@ -490,8 +487,6 @@
                 return "1=1";
             case FALSE:
                 return "1=0";
-            case ASTERISK:
-                return "*";
             default:
                 return "{other}";
         }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
index 4eb03d7..805210f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/ResultNodeDescriptor.java
@@ -34,9 +34,9 @@
 class ResultNodeDescriptor {
     private final Node node;
     private final boolean inDataRow;
-    private final boolean isAggregate;
     private final Property<?> property;
 
+    private boolean isAggregate;
     private String dataRowKey;
     private DbAttribute dbAttribute;
     private String javaType;
@@ -50,6 +50,10 @@
                 && property.getExpression() instanceof ASTAggregateFunctionCall;
     }
 
+    public void setAggregate(boolean aggregate) {
+        isAggregate = aggregate;
+    }
+
     public boolean isAggregate() {
         return isAggregate;
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
index 8cda370..a8458f6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/SQLGenerationStage.java
@@ -35,7 +35,7 @@
         // Build final SQL tree
         Node node = context.getSelectBuilder().build();
         // convert to database flavour
-        node = context.getAdapter().getSqlTreeProcessor().apply(node);
+        node = context.getAdapter().getSqlTreeProcessor().process(node);
         // generate SQL
         SQLGenerationVisitor visitor = new SQLGenerationVisitor(new DefaultQuotingAppendable(context));
         node.visit(visitor);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
index 2928257..702bc3a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TranslatorContext.java
@@ -188,7 +188,7 @@
         return adapter;
     }
 
-    DbEntity getRootDbEntity() {
+    public DbEntity getRootDbEntity() {
         return metadata.getDbEntity();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TypeAwareSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TypeAwareSQLTreeProcessor.java
new file mode 100644
index 0000000..7e48aed
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/translator/select/TypeAwareSQLTreeProcessor.java
@@ -0,0 +1,158 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.select;
+
+import java.sql.Types;
+import java.util.EnumMap;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ChildProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
+import org.apache.cayenne.access.sqlbuilder.sqltree.PerAttributeChildProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SimpleNodeTreeVisitor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.map.ObjAttribute;
+import org.apache.cayenne.map.ObjEntity;
+
+/**
+ * @since 4.2
+ */
+public class TypeAwareSQLTreeProcessor extends SimpleNodeTreeVisitor implements SQLTreeProcessor {
+
+    protected static final Class<?> DEFAULT_TYPE = DefaultColumnTypeMarker.class;
+    protected static final String DEFAULT_TYPE_NAME = DEFAULT_TYPE.getName();
+
+    protected final Map<String, ChildProcessor<ColumnNode>> byColumnTypeProcessors = new HashMap<>();
+    protected final Map<String, ChildProcessor<ValueNode>> byValueTypeProcessors = new HashMap<>();
+    protected final Map<NodeType, ChildProcessor<Node>> byNodeTypeProcessors = new EnumMap<>(NodeType.class);
+
+    public TypeAwareSQLTreeProcessor() {
+        registerProcessor(NodeType.COLUMN, new PerAttributeChildProcessor<>(this::getColumnAttribute, this::getColumnProcessor));
+        registerProcessor(NodeType.VALUE, new PerAttributeChildProcessor<>(this::getValueAttribute, this::getValueProcessor));
+    }
+
+    @Override
+    public Node process(Node node) {
+        node.visit(this);
+        return node;
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    protected void registerProcessor(NodeType nodeType, ChildProcessor childProcessor) {
+        byNodeTypeProcessors.put(nodeType, childProcessor);
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    protected void registerColumnProcessor(Class<?> columnType, ChildProcessor childProcessor) {
+        byColumnTypeProcessors.put(columnType.getName(), childProcessor);
+    }
+
+    @SuppressWarnings({"unchecked", "rawtypes"})
+    protected void registerValueProcessor(Class<?> columnType, ChildProcessor childProcessor) {
+        byValueTypeProcessors.put(columnType.getName(), childProcessor);
+    }
+
+    protected Optional<Node> defaultProcess(Node parent, Node child, int index) {
+        return Optional.empty();
+    }
+
+    @Override
+    public boolean onChildNodeStart(Node parent, Node child, int index, boolean hasMore) {
+        byNodeTypeProcessors
+                .getOrDefault(child.getType(), this::defaultProcess)
+                .process(parent, child, index)
+                .ifPresent(node -> replaceChild(parent, index, node));
+        return true;
+    }
+
+    protected DbAttribute getColumnAttribute(ColumnNode node) {
+        DbAttribute attribute = node.getAttribute();
+        if(attribute.getType() == Types.OTHER
+                && node.getParent() != null
+                && node.getParent().getType() == NodeType.RESULT) {
+            return attribute;
+        }
+        return null;
+    }
+
+    protected ChildProcessor<ColumnNode> getColumnProcessor(DbAttribute attr) {
+        String type = getObjAttributeFor(attr).map(ObjAttribute::getType).orElse(DEFAULT_TYPE_NAME);
+        return byColumnTypeProcessors
+                .getOrDefault(type, this::defaultProcess);
+    }
+
+    protected DbAttribute getValueAttribute(ValueNode node) {
+        DbAttribute attribute = node.getAttribute();
+        if(attribute == null) {
+            return null;
+        }
+        if(attribute.getType() == Types.OTHER
+                && node.getParent() != null
+                && (node.getParent().getType() == NodeType.EQUALITY || node.getParent().getType() == NodeType.INSERT_VALUES)) {
+            return attribute;
+        }
+        return null;
+    }
+
+    protected ChildProcessor<ValueNode> getValueProcessor(DbAttribute attr) {
+        String type = getObjAttributeFor(attr).map(ObjAttribute::getType).orElse(DEFAULT_TYPE_NAME);
+        return byValueTypeProcessors
+                .getOrDefault(type, this::defaultProcess);
+    }
+
+    protected static void replaceChild(Node parent, int index, Node newChild) {
+        Node oldChild = parent.getChild(index);
+        for (int i = 0; i < oldChild.getChildrenCount(); i++) {
+            newChild.addChild(oldChild.getChild(i));
+        }
+        parent.replaceChild(index, newChild);
+    }
+
+    protected static Node wrapInFunction(Node node, String function) {
+        FunctionNode functionNode = new FunctionNode(function, null);
+        functionNode.addChild(node);
+        return functionNode;
+    }
+
+    protected static Optional<ObjAttribute> getObjAttributeFor(DbAttribute dbAttribute) {
+        if(dbAttribute == null) {
+            return Optional.empty();
+        }
+        DbEntity dbEntity = dbAttribute.getEntity();
+        for(ObjEntity objEntity: dbEntity.getDataMap().getObjEntities()) {
+            ObjAttribute objAttribute = objEntity.getAttributeForDbAttribute(dbAttribute);
+            if(objAttribute != null) {
+                return Optional.of(objAttribute);
+            }
+        }
+        return Optional.empty();
+    }
+
+    private static class DefaultColumnTypeMarker {
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/BigDecimalValueType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/BigDecimalValueType.java
new file mode 100644
index 0000000..df9dde0
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/BigDecimalValueType.java
@@ -0,0 +1,65 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.types;
+
+import java.math.BigDecimal;
+
+/**
+ * @since 4.2
+ */
+public class BigDecimalValueType implements ValueObjectType<BigDecimal, BigDecimal> {
+
+    @Override
+    public Class<BigDecimal> getTargetType() {
+        return BigDecimal.class;
+    }
+
+    @Override
+    public Class<BigDecimal> getValueType() {
+        return BigDecimal.class;
+    }
+
+    @Override
+    public BigDecimal toJavaObject(BigDecimal value) {
+        return value;
+    }
+
+    @Override
+    public BigDecimal fromJavaObject(BigDecimal object) {
+        return object;
+    }
+
+    @Override
+    public String toCacheKey(BigDecimal object) {
+        return object.toString();
+    }
+
+    @Override
+    public boolean equals(BigDecimal value1, BigDecimal value2) {
+        //noinspection NumberEquality
+        if(value1 == value2) {
+            return true;
+        }
+        if(value1 == null || value2 == null) {
+            return false;
+        }
+        return value1.compareTo(value2) == 0;
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/DefaultValueObjectTypeRegistry.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/DefaultValueObjectTypeRegistry.java
index 0f545ae..400b095 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/types/DefaultValueObjectTypeRegistry.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/DefaultValueObjectTypeRegistry.java
@@ -31,7 +31,7 @@
  */
 public class DefaultValueObjectTypeRegistry implements ValueObjectTypeRegistry {
 
-    final Map<String, ValueObjectType<?,?>> typeCache;
+    final Map<String, ValueObjectType> typeCache;
 
     public DefaultValueObjectTypeRegistry(@Inject List<ValueObjectType<?, ?>> valueObjectTypeList) {
         typeCache = new ConcurrentHashMap<>();
@@ -43,17 +43,10 @@
             typeCache.put(valueObjectType.getValueType().getName(), valueObjectType);
         }
     }
-
-    /**
-     * @inheritDoc
-     */
+    
     @SuppressWarnings("unchecked")
     @Override
     public <T> ValueObjectType<T, ?> getValueType(Class<? extends T> valueClass) {
-        ValueObjectType type = typeCache.get(valueClass.getName());
-        if(type == null) {
-            return null;
-        }
-        return type;
+        return typeCache.get(valueClass.getName());
     }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedType.java
index 0dabbeb..ce695dc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedType.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/ExtendedType.java
@@ -22,6 +22,10 @@
 import java.sql.CallableStatement;
 import java.sql.PreparedStatement;
 import java.sql.ResultSet;
+import java.util.Optional;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ChildProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
 
 /**
  * Defines methods to read Java objects from JDBC ResultSets and write as parameters of PreparedStatements.
@@ -75,4 +79,19 @@
      */
     String toString(T value);
 
+    /**
+     * @since 4.2
+     * @return
+     */
+    default ChildProcessor<?> readProcessor() {
+        return ChildProcessor.EMPTY;
+    }
+
+    /**
+     * @since 4.2
+     * @return
+     */
+    default ChildProcessor<?> writeProcessor() {
+        return ChildProcessor.EMPTY;
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/GeoJsonType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/GeoJsonType.java
new file mode 100644
index 0000000..5d95075
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/GeoJsonType.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.access.types;
+
+import org.apache.cayenne.value.GeoJson;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Types;
+
+/**
+ * @since 4.2
+ */
+public class GeoJsonType implements ExtendedType<GeoJson> {
+
+    private CharType delegate;
+
+    public GeoJsonType() {
+        this.delegate = new CharType(true, false);
+    }
+
+    @Override
+    public String getClassName() {
+        return GeoJson.class.getName();
+    }
+
+    @Override
+    public void setJdbcObject(PreparedStatement statement, GeoJson geometry, int pos, int type, int scale) throws Exception {
+        String value = geometry != null ? geometry.getGeometry() : null;
+        delegate.setJdbcObject(statement, value, pos, Types.VARCHAR, scale);
+    }
+
+    @Override
+    public GeoJson materializeObject(ResultSet rs, int index, int type) throws Exception {
+        String value = delegate.materializeObject(rs, index, Types.VARCHAR);
+        return value != null ? new GeoJson(value) : null;
+    }
+
+    @Override
+    public GeoJson materializeObject(CallableStatement rs, int index, int type) throws Exception {
+        String value = delegate.materializeObject(rs, index, Types.VARCHAR);
+        return value != null ? new GeoJson(value) : null;
+    }
+
+    @Override
+    public String toString(GeoJson value) {
+        return value != null ? value.getGeometry() : null;
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/JsonType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/JsonType.java
new file mode 100644
index 0000000..f14b509
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/JsonType.java
@@ -0,0 +1,67 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.access.types;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Types;
+
+import org.apache.cayenne.value.Json;
+
+/**
+ * @since 4.2
+ */
+public class JsonType implements ExtendedType<Json> {
+
+    private CharType delegate;
+
+    public JsonType() {
+        this.delegate = new CharType(true, false);
+    }
+
+    @Override
+    public String getClassName() {
+        return Json.class.getName();
+    }
+
+    @Override
+    public void setJdbcObject(PreparedStatement statement, Json json, int pos, int type, int scale) throws Exception {
+        String value = json != null ? json.getRawJson() : null;
+        delegate.setJdbcObject(statement, value, pos, Types.OTHER, scale);
+    }
+
+    @Override
+    public Json materializeObject(ResultSet rs, int index, int type) throws Exception {
+        String value = delegate.materializeObject(rs, index, Types.OTHER);
+        return value != null ? new Json(value) : null;
+    }
+
+    @Override
+    public Json materializeObject(CallableStatement rs, int index, int type) throws Exception {
+        String value = delegate.materializeObject(rs, index, Types.OTHER);
+        return value != null ? new Json(value) : null;
+    }
+
+    @Override
+    public String toString(Json value) {
+        return value != null ? value.getRawJson() : null;
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/ValueObjectType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/ValueObjectType.java
index 3c00bc8..8f61b96 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/access/types/ValueObjectType.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/ValueObjectType.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.access.types;
 
+import java.util.Objects;
+
 /**
  * Descriptor and serialization helper for custom value objects that can be safely stored in the DB.
  * Lightweight alternative for the {@link ExtendedType}.
@@ -60,4 +62,16 @@
      */
     String toCacheKey(V object);
 
+    /**
+     * Allows to use special logic to compare values for equality
+     * as in rare cases it is not suffice to use default equals() method.
+     * Default implementation uses {@link Objects#equals(Object, Object)} method.
+     *
+     * @param value1 to compare
+     * @param value2 to compare
+     * @return true if given values are equal
+     */
+    default boolean equals(V value1, V value2) {
+        return Objects.equals(value1, value2);
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/access/types/WktType.java b/cayenne-server/src/main/java/org/apache/cayenne/access/types/WktType.java
new file mode 100644
index 0000000..a857a64
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/access/types/WktType.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.access.types;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ChildProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.value.Wkt;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.Types;
+import java.util.Optional;
+
+/**
+ * @since 4.2
+ */
+public class WktType implements ExtendedType<Wkt> {
+
+    private CharType delegate;
+
+    public WktType() {
+        this.delegate = new CharType(true, false);
+    }
+
+    @Override
+    public String getClassName() {
+        return Wkt.class.getName();
+    }
+
+    @Override
+    public void setJdbcObject(PreparedStatement statement, Wkt wkt, int pos, int type, int scale) throws Exception {
+        String value = wkt != null ? wkt.getWkt() : null;
+        delegate.setJdbcObject(statement, value, pos, Types.VARCHAR, scale);
+    }
+
+    @Override
+    public Wkt materializeObject(ResultSet rs, int index, int type) throws Exception {
+        String value = delegate.materializeObject(rs, index, Types.VARCHAR);
+        return value != null ? new Wkt(value) : null;
+    }
+
+    @Override
+    public Wkt materializeObject(CallableStatement rs, int index, int type) throws Exception {
+        String value = delegate.materializeObject(rs, index, Types.VARCHAR);
+        return value != null ? new Wkt(value) : null;
+    }
+
+    @Override
+    public String toString(Wkt value) {
+        return value != null ? value.getWkt() : null;
+    }
+
+    @Override
+    public ChildProcessor<? extends Node> readProcessor() {
+        return (p, c, i) -> Optional.of(FunctionNode.wrap(c, "ST_AsText"));
+    }
+
+    @Override
+    public ChildProcessor<? extends Node> writeProcessor() {
+        return (p, c, i) -> Optional.of(FunctionNode.wrap(c, "ST_GeomFromText"));
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java
index 12e013d..69219e2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/DataDomainProvider.java
@@ -42,6 +42,7 @@
 import org.apache.cayenne.event.EventManager;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.EntitySorter;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
 import org.apache.cayenne.resource.Resource;
 import org.apache.cayenne.resource.ResourceLocator;
 import org.slf4j.Logger;
@@ -110,6 +111,12 @@
 	@Inject
 	protected ValueObjectTypeRegistry valueObjectTypeRegistry;
 
+	/**
+	 * @since 4.2
+	 */
+	@Inject
+	protected ValueComparisonStrategyFactory valueComparisonStrategyFactory;
+
 	@Override
 	public DataDomain get() throws ConfigurationException {
 
@@ -150,6 +157,7 @@
 
 		dataDomain.getEntityResolver().applyDBLayerDefaults();
 		dataDomain.getEntityResolver().setValueObjectTypeRegistry(valueObjectTypeRegistry);
+		dataDomain.getEntityResolver().setValueComparisionStrategyFactory(valueComparisonStrategyFactory);
 
 		for (DataNodeDescriptor nodeDescriptor : descriptor.getNodeDescriptors()) {
 			addDataNode(dataDomain, nodeDescriptor);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java
index 61e2d59..d7eb35a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/server/ServerModule.java
@@ -44,6 +44,7 @@
 import org.apache.cayenne.access.translator.select.DefaultSelectTranslatorFactory;
 import org.apache.cayenne.access.translator.select.SelectTranslatorFactory;
 import org.apache.cayenne.access.types.BigDecimalType;
+import org.apache.cayenne.access.types.BigDecimalValueType;
 import org.apache.cayenne.access.types.BigIntegerValueType;
 import org.apache.cayenne.access.types.BooleanType;
 import org.apache.cayenne.access.types.ByteArrayType;
@@ -58,7 +59,9 @@
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.FloatType;
+import org.apache.cayenne.access.types.GeoJsonType;
 import org.apache.cayenne.access.types.IntegerType;
+import org.apache.cayenne.access.types.JsonType;
 import org.apache.cayenne.access.types.LocalDateTimeValueType;
 import org.apache.cayenne.access.types.LocalDateValueType;
 import org.apache.cayenne.access.types.LocalTimeValueType;
@@ -72,6 +75,7 @@
 import org.apache.cayenne.access.types.ValueObjectType;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.access.types.VoidType;
+import org.apache.cayenne.access.types.WktType;
 import org.apache.cayenne.ashwood.AshwoodEntitySorter;
 import org.apache.cayenne.cache.MapQueryCacheProvider;
 import org.apache.cayenne.cache.QueryCache;
@@ -149,6 +153,8 @@
 import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.log.Slf4jJdbcEventLogger;
 import org.apache.cayenne.map.EntitySorter;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
+import org.apache.cayenne.reflect.generic.DefaultValueComparisonStrategyFactory;
 import org.apache.cayenne.resource.ClassLoaderResourceLocator;
 import org.apache.cayenne.resource.ResourceLocator;
 import org.apache.cayenne.template.CayenneSQLTemplateProcessor;
@@ -423,13 +429,18 @@
                 // should be converted from ExtendedType to ValueType
                 .add(new UtilDateType())
                 .add(new CalendarType<>(GregorianCalendar.class))
-                .add(new CalendarType<>(Calendar.class));
+                .add(new CalendarType<>(Calendar.class))
+                // non-standard types
+                .add(GeoJsonType.class)
+                .add(WktType.class)
+                .add(JsonType.class);
         contributeUserTypes(binder);
         contributeTypeFactories(binder);
 
         // Custom ValueObjects types contribution
         contributeValueObjectTypes(binder)
                 .add(BigIntegerValueType.class)
+                .add(BigDecimalValueType.class)
                 .add(UUIDValueType.class)
                 .add(LocalDateValueType.class)
                 .add(LocalTimeValueType.class)
@@ -438,6 +449,7 @@
                 .add(CharacterValueType.class);
 
         binder.bind(ValueObjectTypeRegistry.class).to(DefaultValueObjectTypeRegistry.class);
+        binder.bind(ValueComparisonStrategyFactory.class).to(DefaultValueComparisonStrategyFactory.class);
 
         // configure explicit configurations
         contributeProjectLocations(binder);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java
index 8a335bb..2cfc679 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/configuration/xml/ObjEntityHandler.java
@@ -153,6 +153,7 @@
         lastAttribute = new ObjAttribute(attributes.getValue("name"));
         lastAttribute.setType(attributes.getValue("type"));
         lastAttribute.setUsedForLocking(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("lock")));
+        lastAttribute.setLazy(DataMapHandler.TRUE.equalsIgnoreCase(attributes.getValue("lazy")));
         lastAttribute.setDbAttributePath(dbPath);
         entity.addAttribute(lastAttribute);
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
index 7c856f0..9109288 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/AutoAdapter.java
@@ -23,11 +23,10 @@
 import java.sql.SQLException;
 import java.util.Collection;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -121,7 +120,7 @@
 	}
 
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return getAdapter().getSqlTreeProcessor();
 	}
 
@@ -145,6 +144,14 @@
 		return getAdapter().supportsGeneratedKeys();
 	}
 
+	/**
+	 * @since 4.2
+	 */
+	@Override
+	public boolean supportsGeneratedKeysForBatchInserts() {
+		return getAdapter().supportsGeneratedKeysForBatchInserts();
+	}
+
 	@Override
 	public boolean supportsBatchUpdates() {
 		return getAdapter().supportsBatchUpdates();
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
index c657d6d..a043e2f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/DbAdapter.java
@@ -22,10 +22,9 @@
 import java.sql.SQLException;
 import java.util.Collection;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.select.SelectTranslator;
@@ -55,12 +54,15 @@
 	 */
 	String getBatchTerminator();
 
-	/**
-	 * Returns a SelectTranslator that works with the adapter target database.
-	 *
-	 * @since 4.0
-	 */
-	SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver);
+    /**
+     * Returns a SelectTranslator that works with the adapter target database.
+     *
+     * @since 4.0
+     * @deprecated since 4.2 as {@link SelectQuery} is deprecated.
+     * {@link #getSelectTranslator(FluentSelect, EntityResolver)} replaces this method.
+     */
+    @Deprecated
+    SelectTranslator getSelectTranslator(SelectQuery<?> query, EntityResolver entityResolver);
 
 	/**
 	 * @since 4.2
@@ -69,9 +71,9 @@
 
 	/**
 	 * @since 4.2
-	 * @return {@link Function} that can adjust SQL tree to specific database flavour
+	 * @return {@link SQLTreeProcessor} that can adjust SQL tree to specific database flavour
 	 */
-	Function<Node, Node> getSqlTreeProcessor();
+	SQLTreeProcessor getSqlTreeProcessor();
 
 	/**
 	 * Returns an instance of SQLAction that should handle the query.
@@ -104,12 +106,14 @@
 	boolean supportsGeneratedKeys();
 
     /**
+	 * Returns true if a target database supports key autogeneration in a batch insert.
+	 * @see #supportsGeneratedKeys()
      * @since 4.2
      */
     default boolean supportsGeneratedKeysForBatchInserts() {
     	return supportsGeneratedKeys();
     }
-    
+
 	/**
 	 * Returns <code>true</code> if the target database supports batch updates.
 	 */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
index 88217ca..c71e063 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/JdbcAdapter.java
@@ -27,11 +27,10 @@
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
@@ -397,9 +396,17 @@
     }
 
     public static String getType(DbAdapter adapter, DbAttribute column) {
-        String[] types = adapter.externalTypesForJdbcType(column.getType());
+        int columnType = column.getType();
+        if(columnType == Types.OTHER) {
+            // TODO: warn that this is unsupported yet
+            return "OTHER";
+        }
+
+        String[] types = adapter.externalTypesForJdbcType(columnType);
         if (types == null || types.length == 0) {
-            String entityName = column.getEntity() != null ? column.getEntity().getFullyQualifiedName() : "<null>";
+            String entityName = column.getEntity() != null
+                    ? column.getEntity().getFullyQualifiedName()
+                    : "<null>";
             throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s."
                     , entityName, column.getName(), column.getType());
         }
@@ -544,8 +551,8 @@
     }
 
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
-        return Function.identity();
+    public SQLTreeProcessor getSqlTreeProcessor() {
+        return node -> node;
     }
 
     @SuppressWarnings("unchecked")
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
index 5717b5f..bdbd376 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/db2/DB2Adapter.java
@@ -22,11 +22,10 @@
 import java.sql.PreparedStatement;
 import java.sql.Types;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
@@ -98,13 +97,7 @@
      */
     @Override
     public void createTableAppendColumn(StringBuffer sqlBuffer, DbAttribute column) {
-        String[] types = externalTypesForJdbcType(column.getType());
-        if (types == null || types.length == 0) {
-            String entityName = column.getEntity() != null ? column.getEntity().getFullyQualifiedName() : "<null>";
-            throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s"
-                    , entityName, column.getName(), column.getType());
-        }
-        String type = types[0];
+        String type = getType(this, column);
 
         sqlBuffer.append(quotingStrategy.quotedName(column)).append(' ');
 
@@ -148,7 +141,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new DB2SQLTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
index aaa4864..81cb1bd 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/derby/DerbyAdapter.java
@@ -21,7 +21,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
@@ -46,7 +46,6 @@
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.List;
-import java.util.function.Function;
 
 /**
  * DbAdapter implementation for the <a href="http://db.apache.org/derby/"> Derby RDBMS
@@ -130,21 +129,12 @@
      */
     @Override
     public void createTableAppendColumn(StringBuffer sqlBuffer, DbAttribute column) {
-
-        String[] types = externalTypesForJdbcType(column.getType());
-        if (types == null || types.length == 0) {
-            String entityName = column.getEntity() != null ? (column.getEntity()).getFullyQualifiedName() : "<null>";
-            throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s"
-                    , entityName, column.getName(), column.getType());
-        }
-
+        String type = getType(this, column);
+        String length = sizeAndPrecision(this, column);
 
         sqlBuffer.append(quotingStrategy.quotedName(column));
         sqlBuffer.append(' ');
 
-        String type = types[0];
-        String length = sizeAndPrecision(this, column);
-
         // assemble...
         // note that max length for types like XYZ FOR BIT DATA must be entered in the
         // middle of type name, e.g. VARCHAR (100) FOR BIT DATA.
@@ -181,7 +171,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new DerbySQLTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
index 4702fa8..8a7bed5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/firebird/FirebirdAdapter.java
@@ -21,7 +21,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.types.ByteArrayType;
 import org.apache.cayenne.access.types.CharType;
@@ -39,7 +39,6 @@
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.util.List;
-import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.firebirdsql.org">FirebirdSQL
@@ -85,20 +84,12 @@
     }
 
     public void createTableAppendColumn(StringBuffer sqlBuffer, DbAttribute column) {
-
-        String[] types = externalTypesForJdbcType(column.getType());
-        if (types == null || types.length == 0) {
-            String entityName = column.getEntity() != null ? column.getEntity().getFullyQualifiedName() : "<null>";
-            throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s"
-                    , entityName, column.getName(), column.getType());
-        }
+        String type = getType(this, column);
+        String length = sizeAndPrecision(this, column);
 
         sqlBuffer.append(quotingStrategy.quotedName(column));
         sqlBuffer.append(' ');
 
-        String type = types[0];
-        String length = sizeAndPrecision(this, column);
-
         int suffixIndex = type.indexOf(NCHAR_SUFFIX);
         if (!length.isEmpty() && suffixIndex > 0) {
             sqlBuffer.append(type.substring(0, suffixIndex)).append(length).append(NCHAR_SUFFIX);
@@ -113,7 +104,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new FirebirdSQLTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
index b1426f4..d2f40e1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseAdapter.java
@@ -24,10 +24,9 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
@@ -80,7 +79,7 @@
      * @since 4.2
      */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return new FrontBaseSQLTreeProcessor();
 	}
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
index 4ba94d6..04a04c7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/h2/H2Adapter.java
@@ -20,7 +20,7 @@
 package org.apache.cayenne.dba.h2;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
@@ -35,7 +35,6 @@
 import org.apache.cayenne.resource.ResourceLocator;
 
 import java.util.List;
-import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a href="http://www.h2database.com/">H2
@@ -75,7 +74,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new H2SQLTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
index b279e3c..a2273c0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/hsqldb/HSQLDBAdapter.java
@@ -21,7 +21,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
 import org.apache.cayenne.access.types.CharType;
@@ -45,7 +45,6 @@
 import java.util.Collection;
 import java.util.Iterator;
 import java.util.List;
-import java.util.function.Function;
 
 /**
  * DbAdapter implementation for the <a href="http://hsqldb.sourceforge.net/">
@@ -88,7 +87,7 @@
 	 * @since 4.2
 	 */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return new HSQLTreeProcessor();
 	}
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
index e2e68d0..ffa1aab 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/ingres/IngresAdapter.java
@@ -21,7 +21,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
@@ -42,7 +42,6 @@
 import java.sql.SQLException;
 import java.sql.Types;
 import java.util.List;
-import java.util.function.Function;
 
 /**
  * DbAdapter implementation for <a
@@ -75,7 +74,7 @@
      * @since 4.2
      */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return new IngressSQLTreeProcessor();
 	}
 
@@ -114,14 +113,7 @@
 
 	@Override
 	public void createTableAppendColumn(StringBuffer buf, DbAttribute at) {
-
-		String[] types = externalTypesForJdbcType(at.getType());
-		if (types == null || types.length == 0) {
-			throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s"
-					, at.getEntity().getFullyQualifiedName(), at.getName(), at.getType());
-		}
-
-		String type = types[0];
+		String type = getType(this, at);
 		buf.append(quotingStrategy.quotedName(at)).append(' ').append(type);
 
 		// append size and precision (if applicable)
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
index b1792f4..6c9eee9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLAdapter.java
@@ -25,15 +25,12 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Comparator;
 import java.util.Iterator;
 import java.util.List;
-import java.util.function.Function;
 
-import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.translator.ejbql.JdbcEJBQLTranslatorFactory;
@@ -80,11 +77,10 @@
 public class MySQLAdapter extends JdbcAdapter {
 
 	static final String DEFAULT_STORAGE_ENGINE = "InnoDB";
+	static final List<String> SYSTEM_CATALOGS = Arrays.asList("sys", "information_schema", "mysql", "performance_schema");
 
 	protected String storageEngine;
 
-	private String[] SYSTEM_CATALOGS = new String[]{"sys", "information_schema", "mysql", "performance_schema"};
-
 	public MySQLAdapter(@Inject RuntimeProperties runtimeProperties,
 						@Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
 						@Inject(Constants.SERVER_USER_TYPES_LIST) List<ExtendedType> userExtendedTypes,
@@ -110,7 +106,7 @@
      * @since 4.2
      */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return MySQLTreeProcessor.getInstance();
 	}
 
@@ -273,7 +269,7 @@
 
 		// must move generated to the front...
 		List<DbAttribute> pkList = new ArrayList<>(entity.getPrimaryKeys());
-		Collections.sort(pkList, new PKComparator());
+		pkList.sort(PKComparator.INSTANCE);
 
 		Iterator<DbAttribute> pkit = pkList.iterator();
 		if (pkit.hasNext()) {
@@ -301,15 +297,8 @@
 	@Override
 	public void createTableAppendColumn(StringBuffer sqlBuffer, DbAttribute column) {
 
-		String[] types = externalTypesForJdbcType(column.getType());
-		if (types == null || types.length == 0) {
-			String entityName = column.getEntity() != null
-					? column.getEntity().getFullyQualifiedName() : "<null>";
-			throw new CayenneRuntimeException("Undefined type for attribute '%s.%s': %s"
-					, entityName, column.getName(), column.getType());
-		}
+		String type = getType(this, column);
 
-		String type = types[0];
 		sqlBuffer.append(quotingStrategy.quotedName(column));
 		sqlBuffer.append(' ').append(type);
 
@@ -358,18 +347,7 @@
 
 	@Override
 	public List<String> getSystemCatalogs() {
-		return Arrays.asList(SYSTEM_CATALOGS);
-	}
-
-	final class PKComparator implements Comparator<DbAttribute> {
-
-		public int compare(DbAttribute a1, DbAttribute a2) {
-			if (a1.isGenerated() != a2.isGenerated()) {
-				return a1.isGenerated() ? -1 : 1;
-			} else {
-				return a1.getName().compareTo(a2.getName());
-			}
-		}
+		return SYSTEM_CATALOGS;
 	}
 
 	/**
@@ -385,4 +363,17 @@
 	public void setStorageEngine(String engine) {
 		this.storageEngine = engine;
 	}
+
+	static final class PKComparator implements Comparator<DbAttribute> {
+
+		static final PKComparator INSTANCE = new PKComparator();
+
+		public int compare(DbAttribute a1, DbAttribute a2) {
+			if (a1.isGenerated() != a2.isGenerated()) {
+				return a1.isGenerated() ? -1 : 1;
+			} else {
+				return a1.getName().compareTo(a2.getName());
+			}
+		}
+	}
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java
index 2aeae10..c1cfe7d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/MySQLTreeProcessor.java
@@ -19,18 +19,26 @@
 
 package org.apache.cayenne.dba.mysql;
 
+import java.util.Optional;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.ChildProcessor;
 import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
+import org.apache.cayenne.access.translator.select.TypeAwareSQLTreeProcessor;
+import org.apache.cayenne.dba.mysql.sqltree.ConvertNode;
 import org.apache.cayenne.dba.mysql.sqltree.MysqlLikeNode;
 import org.apache.cayenne.dba.mysql.sqltree.MysqlLimitOffsetNode;
+import org.apache.cayenne.value.GeoJson;
+import org.apache.cayenne.value.Json;
+import org.apache.cayenne.value.Wkt;
 
 /**
  * @since 4.2
  */
-public class MySQLTreeProcessor extends BaseSQLTreeProcessor {
+public class MySQLTreeProcessor extends TypeAwareSQLTreeProcessor {
 
     private static final MySQLTreeProcessor INSTANCE = new MySQLTreeProcessor();
 
@@ -39,30 +47,46 @@
     }
 
     private MySQLTreeProcessor() {
+        registerProcessor(NodeType.LIKE, (ChildProcessor<LikeNode>) this::onLikeNode);
+        registerProcessor(NodeType.LIMIT_OFFSET, (ChildProcessor<LimitOffsetNode>) this::onLimitOffsetNode);
+        registerProcessor(NodeType.FUNCTION, (ChildProcessor<FunctionNode>) this::onFunctionNode);
+
+        registerColumnProcessor(Wkt.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_AsText")));
+        registerColumnProcessor(GeoJson.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_AsGeoJSON")));
+
+        registerValueProcessor(Wkt.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_GeomFromText")));
+        registerValueProcessor(GeoJson.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_GeomFromGeoJSON")));
+
+        registerValueProcessor(Json.class, (parent, child, i) -> {
+            ConvertNode node = new ConvertNode();
+            node.addChild(child);
+            return Optional.of(node);
+        });
     }
 
-    @Override
-    protected void onLikeNode(Node parent, LikeNode child, int index) {
+    protected Optional<Node> onLikeNode(Node parent, LikeNode child, int index) {
         if(!child.isIgnoreCase()) {
-            replaceChild(parent, index, new MysqlLikeNode(child.isNot(), child.getEscape()));
+            return Optional.of(new MysqlLikeNode(child.isNot(), child.getEscape()));
         }
+        return Optional.empty();
     }
 
-    @Override
-    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
-        Node replacement = new MysqlLimitOffsetNode(child.getLimit(), child.getOffset());
-        replaceChild(parent, index, replacement, false);
+    protected Optional<Node> onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        return Optional.of(new MysqlLimitOffsetNode(child.getLimit(), child.getOffset()));
     }
 
-    @Override
-    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+    protected Optional<Node> onFunctionNode(Node parent, FunctionNode child, int index) {
         String functionName = child.getFunctionName();
         if("DAY_OF_MONTH".equals(functionName)
                 || "DAY_OF_WEEK".equals(functionName)
                 || "DAY_OF_YEAR".equals(functionName)) {
-            FunctionNode replacement = new FunctionNode(functionName.replace("_", ""), child.getAlias(), true);
-            replaceChild(parent, index, replacement);
+            return Optional.of(new FunctionNode(functionName.replace("_", ""), child.getAlias(), true));
         }
+        return Optional.empty();
     }
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/ConvertNode.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/ConvertNode.java
new file mode 100644
index 0000000..afed052
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/mysql/sqltree/ConvertNode.java
@@ -0,0 +1,44 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.dba.mysql.sqltree;
+
+import org.apache.cayenne.access.sqlbuilder.QuotingAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.dba.mysql.MySQLTreeProcessor;
+
+/**
+ * @since 4.2
+ */
+public class ConvertNode extends Node {
+
+    @Override
+    public Node copy() {
+        return new ConvertNode();
+    }
+
+    @Override
+    public QuotingAppendable append(QuotingAppendable buffer) {
+        return buffer.append("CONVERT(");
+    }
+
+    @Override
+    public void appendChildrenEnd(QuotingAppendable buffer) {
+        buffer.append(" USING utf8mb4)");
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
index aa8e2f8..91c2a3a 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/openbase/OpenBaseAdapter.java
@@ -25,10 +25,9 @@
 import java.sql.Types;
 import java.util.Iterator;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.types.ByteType;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -80,7 +79,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new OpenBaseSQLTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
index 822dde7..eb58e71 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/Oracle8LOBBatchTranslator.java
@@ -25,7 +25,7 @@
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.translator.DbAttributeBinding;
-import org.apache.cayenne.access.translator.batch.DefaultBatchTranslator;
+import org.apache.cayenne.access.translator.batch.legacy.DefaultBatchTranslator;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.QuotingStrategy;
@@ -36,7 +36,7 @@
 
 /**
  * Superclass of query builders for the DML operations involving LOBs.
- * 
+ * TODO: update to the new batch translation logic
  */
 abstract class Oracle8LOBBatchTranslator extends DefaultBatchTranslator {
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
index 453aae8..ccb93e7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleAdapter.java
@@ -29,11 +29,10 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.types.ByteType;
@@ -83,13 +82,13 @@
 
 	protected static boolean supportsOracleLOB;
 
-	private String[] SYSTEM_SCHEMAS = new String[]{
+	private List<String> SYSTEM_SCHEMAS = Arrays.asList(
 			"ANONYMOUS", "APPQOSSYS", "AUDSYS", "CTXSYS", "DBSFWUSER",
 			"DBSNMP", "DIP", "DVF", "GGSYS", "DVSYS", "GSMADMIN_INTERNAL",
 			"GSMCATUSER", "GSMUSER", "LBACSYS", "MDDATA", "MDSYS", "OJVMSYS",
 			"OLAPSYS", "ORACLE_OCM", "ORDDATA", "ORDPLUGINS", "ORDSYS", "OUTLN",
 			"REMOTE_SCHEDULER_AGENT", "SYSTEM", "WMSYS", "SI_INFORMTN_SCHEMA",
-			"SYS", "SYSBACKUP", "SYSDG", "SYSKM", "SYSRAC", "SYS$UMF", "XDB", "XS$NULL"};
+			"SYS", "SYSBACKUP", "SYSDG", "SYSKM", "SYSRAC", "SYS$UMF", "XDB", "XS$NULL");
 
 	static {
 		// TODO: as CAY-234 shows, having such initialization done in a static
@@ -180,7 +179,7 @@
 	 * @since 4.2
 	 */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return new OracleSQLTreeProcessor();
 	}
 
@@ -302,7 +301,7 @@
 
 	@Override
 	public List<String> getSystemSchemas() {
-		return Arrays.asList(SYSTEM_SCHEMAS);
+		return SYSTEM_SCHEMAS;
 	}
 
 	/**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java
index 7ba9ef7..6a3401d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/oracle/OracleSQLTreeProcessor.java
@@ -230,9 +230,9 @@
     }
 
     @Override
-    public Node apply(Node node) {
+    public Node process(Node node) {
         root = node;
-        super.apply(node);
+        super.process(node);
         if(selectBuilder != null) {
             return selectBuilder.build();
         }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java
index a9402fa..b49d664 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgreSQLTreeProcessor.java
@@ -21,48 +21,64 @@
 
 import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Optional;
 import java.util.Set;
 
+import org.apache.cayenne.access.sqlbuilder.sqltree.ChildProcessor;
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.LimitOffsetNode;
 import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.NodeType;
 import org.apache.cayenne.access.sqlbuilder.sqltree.TrimmingColumnNode;
-import org.apache.cayenne.access.translator.select.BaseSQLTreeProcessor;
+import org.apache.cayenne.access.translator.select.TypeAwareSQLTreeProcessor;
 import org.apache.cayenne.dba.postgres.sqltree.PositionFunctionNode;
 import org.apache.cayenne.dba.postgres.sqltree.PostgresExtractFunctionNode;
 import org.apache.cayenne.dba.postgres.sqltree.PostgresLikeNode;
 import org.apache.cayenne.dba.postgres.sqltree.PostgresLimitOffsetNode;
+import org.apache.cayenne.value.GeoJson;
+import org.apache.cayenne.value.Wkt;
 
 /**
  * @since 4.2
  */
-public class PostgreSQLTreeProcessor extends BaseSQLTreeProcessor {
+public class PostgreSQLTreeProcessor extends TypeAwareSQLTreeProcessor {
 
     private static final Set<String> EXTRACT_FUNCTION_NAMES = new HashSet<>(Arrays.asList(
         "DAY_OF_MONTH", "DAY", "MONTH", "HOUR", "WEEK", "YEAR", "DAY_OF_WEEK", "DAY_OF_YEAR", "MINUTE", "SECOND"
     ));
 
-    @Override
-    protected void onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
-        replaceChild(parent, index, new PostgresLimitOffsetNode(child), false);
+    public PostgreSQLTreeProcessor() {
+        registerProcessor(NodeType.LIMIT_OFFSET,    (ChildProcessor<LimitOffsetNode>)this::onLimitOffsetNode);
+        registerProcessor(NodeType.LIKE,            (ChildProcessor<LikeNode>) this::onLikeNode);
+        registerProcessor(NodeType.FUNCTION,        (ChildProcessor<FunctionNode>) this::onFunctionNode);
+
+        registerColumnProcessor(DEFAULT_TYPE, (ChildProcessor<ColumnNode>)(p, c, i)
+                -> Optional.of(new TrimmingColumnNode(c)));
+
+        registerColumnProcessor(Wkt.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_AsText")));
+        registerColumnProcessor(GeoJson.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_AsGeoJSON")));
+
+        registerValueProcessor(Wkt.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_GeomFromText")));
+        registerValueProcessor(GeoJson.class, (parent, child, i)
+                -> Optional.of(wrapInFunction(child, "ST_GeomFromGeoJSON")));
     }
 
-    @Override
-    protected void onColumnNode(Node parent, ColumnNode child, int index) {
-        replaceChild(parent, index, new TrimmingColumnNode(child));
+    protected Optional<Node> onLimitOffsetNode(Node parent, LimitOffsetNode child, int index) {
+        return Optional.of(new PostgresLimitOffsetNode(child));
     }
 
-    @Override
-    protected void onLikeNode(Node parent, LikeNode child, int index) {
-        if(child.isIgnoreCase()) {
-            replaceChild(parent, index, new PostgresLikeNode(child.isNot(), child.getEscape()));
-        }
+    protected Optional<Node> onLikeNode(Node parent, LikeNode child, int index) {
+        return child.isIgnoreCase()
+                ? Optional.of(new PostgresLikeNode(child.isNot(), child.getEscape()))
+                : Optional.empty();
     }
 
-    @Override
-    protected void onFunctionNode(Node parent, FunctionNode child, int index) {
+    protected Optional<Node> onFunctionNode(Node parent, FunctionNode child, int index) {
         Node replacement = null;
         String functionName = child.getFunctionName();
         if(EXTRACT_FUNCTION_NAMES.contains(functionName)) {
@@ -75,9 +91,7 @@
             replacement = new PositionFunctionNode(child.getAlias());
         }
 
-        if(replacement != null) {
-            replaceChild(parent, index, replacement);
-        }
+        return Optional.ofNullable(replacement);
     }
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
index edf5e79..9ebd27f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/postgres/PostgresAdapter.java
@@ -27,11 +27,10 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.types.CharType;
 import org.apache.cayenne.access.types.ExtendedType;
@@ -67,7 +66,7 @@
 
 	public static final String BYTEA = "bytea";
 
-	private String[] SYSTEM_SCHEMAS = new String[]{"information_schema", "pg_catalog"};
+	private List<String> SYSTEM_SCHEMAS = Arrays.asList("information_schema", "pg_catalog");
 
 	public PostgresAdapter(@Inject RuntimeProperties runtimeProperties,
 						   @Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
@@ -84,7 +83,7 @@
      * @since 4.2
      */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return new PostgreSQLTreeProcessor();
 	}
 
@@ -272,7 +271,7 @@
 
 	@Override
 	public List<String> getSystemSchemas() {
-		return Arrays.asList(SYSTEM_SCHEMAS);
+		return SYSTEM_SCHEMAS;
 	}
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
index a1defb1..d86f7d3 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlite/SQLiteAdapter.java
@@ -19,7 +19,7 @@
 package org.apache.cayenne.dba.sqlite;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ExtendedTypeMap;
@@ -39,7 +39,6 @@
 import java.util.Collection;
 import java.util.GregorianCalendar;
 import java.util.List;
-import java.util.function.Function;
 
 /**
  * A SQLite database adapter that works with Zentus JDBC driver. See
@@ -89,7 +88,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new SQLiteTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
index 470444f..92fcd46 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sqlserver/SQLServerAdapter.java
@@ -21,10 +21,9 @@
 
 import java.util.Arrays;
 import java.util.List;
-import java.util.function.Function;
 
 import org.apache.cayenne.access.DataNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.types.ExtendedType;
 import org.apache.cayenne.access.types.ExtendedTypeFactory;
 import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
@@ -81,10 +80,10 @@
 	@Deprecated
 	public static final String TRIM_FUNCTION = "RTRIM";
 
-	private String[] SYSTEM_SCHEMAS = new String[]{"db_accessadmin", "db_backupoperator",
+	private List<String> SYSTEM_SCHEMAS = Arrays.asList("db_accessadmin", "db_backupoperator",
 			"db_datareader", "db_datawriter", "db_ddladmin", "db_denydatareader",
 			"db_denydatawriter","dbo", "sys", "db_owner", "db_securityadmin", "guest",
-			"INFORMATION_SCHEMA"};
+			"INFORMATION_SCHEMA");
 
 	public SQLServerAdapter(@Inject RuntimeProperties runtimeProperties,
 							@Inject(Constants.SERVER_DEFAULT_TYPES_LIST) List<ExtendedType> defaultExtendedTypes,
@@ -109,7 +108,7 @@
 	 * @since 4.2
 	 */
 	@Override
-	public Function<Node, Node> getSqlTreeProcessor() {
+	public SQLTreeProcessor getSqlTreeProcessor() {
 		return new SQLServerTreeProcessor();
 	}
 
@@ -125,7 +124,7 @@
 
 	@Override
 	public List<String> getSystemSchemas() {
-		return Arrays.asList(SYSTEM_SCHEMAS);
+		return SYSTEM_SCHEMAS;
 	}
 
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
index c32c5b0..303381f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/dba/sybase/SybaseAdapter.java
@@ -22,9 +22,8 @@
 import java.sql.PreparedStatement;
 import java.sql.Types;
 import java.util.List;
-import java.util.function.Function;
 
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.SQLTreeProcessor;
 import org.apache.cayenne.access.translator.ParameterBinding;
 import org.apache.cayenne.access.translator.ejbql.EJBQLTranslatorFactory;
 import org.apache.cayenne.access.types.ByteArrayType;
@@ -78,7 +77,7 @@
      * @since 4.2
      */
     @Override
-    public Function<Node, Node> getSqlTreeProcessor() {
+    public SQLTreeProcessor getSqlTreeProcessor() {
         return new SybaseSQLTreeProcessor();
     }
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
index 415659f..05debf5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/Expression.java
@@ -189,6 +189,11 @@
 	 */
 	public static final int DBID_PATH = 52;
 
+	/**
+	 * @since 4.2
+	 */
+	public static final int CUSTOM_OP = 53;
+
 	protected int type;
 
 	/**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java
index 8e93c7c..cf777d8 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/FunctionExpressionFactory.java
@@ -27,6 +27,7 @@
 import org.apache.cayenne.exp.parser.ASTCurrentTime;
 import org.apache.cayenne.exp.parser.ASTCurrentTimestamp;
 import org.apache.cayenne.exp.parser.ASTCustomFunction;
+import org.apache.cayenne.exp.parser.ASTCustomOperator;
 import org.apache.cayenne.exp.parser.ASTDistinct;
 import org.apache.cayenne.exp.parser.ASTExtract;
 import org.apache.cayenne.exp.parser.ASTFunctionCall;
@@ -538,6 +539,17 @@
         return new ASTCustomFunction(function, args);
     }
 
+    /**
+     * @param operator to call
+     * @param args arguments
+     * @return expression to use custom "operator" with provided arguments
+     *
+     * @since 4.2
+     */
+    public static Expression operator(String operator, Object... args) {
+        return new ASTCustomOperator(operator, args);
+    }
+
     static Expression extractExp(String path, ASTExtract.DateTimePart part) {
         return extractExp(ExpressionFactory.pathExp(path), part);
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java
index f74e904..37209bc 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAggregateFunctionCall.java
@@ -19,6 +19,9 @@
 
 package org.apache.cayenne.exp.parser;
 
+import java.util.Collection;
+import java.util.Map;
+
 /**
  * Base class for all aggregation functions expressions
  * It's more like marker interface for now.
@@ -40,6 +43,30 @@
     }
 
     @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        int len = jjtGetNumChildren();
+        if(len == 0) {
+            throw new UnsupportedOperationException("Aggregate functions can be calculated only for Collection or Map.");
+        }
+
+        Object firstChild = evaluateChild(0, o);
+        Collection<?> values;
+        if(firstChild instanceof Map) {
+            values = ((Map<?, ?>) firstChild).values();
+        } else if (firstChild instanceof Collection) {
+            values = (Collection<?>) firstChild;
+        } else {
+            throw new UnsupportedOperationException("Aggregate functions can be calculated only for Collection or Map.");
+        }
+
+        return evaluateCollection(values);
+    }
+
+    protected Object evaluateCollection(Collection<?> values) {
+        throw new UnsupportedOperationException("In-memory evaluation of aggregate functions not implemented yet.");
+    }
+
+    @Override
     protected Object evaluateSubNode(Object o, Object[] evaluatedChildren) throws Exception {
         throw new UnsupportedOperationException("In-memory evaluation of aggregate functions not implemented yet.");
     }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java
index 9ad3029..b72f703 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTAvg.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.exp.parser;
 
+import java.util.Collection;
+
 import org.apache.cayenne.exp.Expression;
 
 /**
@@ -38,4 +40,22 @@
     public Expression shallowCopy() {
         return new ASTAvg(id);
     }
+
+    @Override
+    protected Object evaluateCollection(Collection<?> values) {
+        if(values.isEmpty()) {
+            return 0.0;
+        }
+
+        double sum = 0;
+        for(Object value : values) {
+            if(value instanceof Number) {
+                sum += ((Number) value).doubleValue();
+            } else {
+                throw new UnsupportedOperationException("Can't calculate average for non-numeric type.");
+            }
+        }
+
+        return sum / values.size();
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java
index ebfe691..75fa77e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCount.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.exp.parser;
 
+import java.util.Collection;
+
 import org.apache.cayenne.exp.Expression;
 
 /**
@@ -42,4 +44,9 @@
     public Expression shallowCopy() {
         return new ASTCount(id);
     }
+
+    @Override
+    protected Object evaluateCollection(Collection<?> values) {
+        return values.size();
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCustomOperator.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCustomOperator.java
new file mode 100644
index 0000000..891f4d9
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTCustomOperator.java
@@ -0,0 +1,108 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.exp.parser;
+
+import java.io.IOException;
+
+import org.apache.cayenne.exp.Expression;
+
+/**
+ * @since 4.2
+ */
+public class ASTCustomOperator extends SimpleNode {
+
+    private String operator;
+
+    public ASTCustomOperator(int id) {
+        super(id);
+    }
+
+    public ASTCustomOperator(String operator) {
+        super(ExpressionParser.JJTCUSTOMOPERATOR);
+        this.operator = operator;
+    }
+
+    public ASTCustomOperator(String operator, Object[] nodes) {
+        super(ExpressionParser.JJTCUSTOMOPERATOR);
+        this.operator = operator;
+        int len = nodes.length;
+        for (int i = 0; i < len; i++) {
+            jjtAddChild(wrapChild(nodes[i]), i);
+        }
+
+        connectChildren();
+    }
+
+    @Override
+    public void jjtAddChild(Node n, int i) {
+        // First argument should be used as an operator when created by the expression parser
+        if(operator == null && i == 0) {
+            if(!(n instanceof ASTScalar)) {
+                throw new IllegalArgumentException("ASTScalar expected, got " + n);
+            }
+            this.operator = ((ASTScalar) n).getValue().toString();
+            return;
+        }
+        super.jjtAddChild(n, operator != null ? i : --i);
+    }
+
+    @Override
+    protected Object evaluateNode(Object o) throws Exception {
+        throw new UnsupportedOperationException("Can't evaluate custom operator in memory");
+    }
+
+    public void setOperator(String operator) {
+        this.operator = operator;
+    }
+
+    @Override
+    public void appendAsString(Appendable out) throws IOException {
+        out.append("op(\"").append(operator).append("\"");
+        if ((children != null) && (children.length > 0)) {
+            for (Node child : children) {
+                out.append(", ");
+                if (child == null) {
+                    out.append("null");
+                } else {
+                    ((SimpleNode) child).appendAsString(out);
+                }
+            }
+        }
+        out.append(")");
+    }
+
+    @Override
+    public int getType() {
+        return Expression.CUSTOM_OP;
+    }
+
+    public String getOperator() {
+        return operator;
+    }
+
+    @Override
+    protected String getExpressionOperator(int index) {
+        return operator;
+    }
+
+    @Override
+    public Expression shallowCopy() {
+        return new ASTCustomOperator(operator);
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
index 8fdfe06..2e4bc65 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTEqual.java
@@ -124,23 +124,22 @@
 	}
 
 	public void injectValue(Object o) {
-		// try to inject value, if one of the operands is scalar, and other is a
-		// path
+		// try to inject value, if one of the operands is scalar, and other is a path
+		ASTScalar scalar = null;
+		ASTObjPath path = null;
 
-		Node[] args = new Node[] { jjtGetChild(0), jjtGetChild(1) };
-
-		int scalarIndex = -1;
-		if (args[0] instanceof ASTScalar) {
-			scalarIndex = 0;
-		} else if (args[1] instanceof ASTScalar) {
-			scalarIndex = 1;
+		for(int i=0; i<=1; i++) {
+			Node node = jjtGetChild(i);
+			if(node instanceof ASTScalar) {
+				scalar = (ASTScalar)node;
+			} else if(node instanceof ASTObjPath) {
+				path = (ASTObjPath) node;
+			}
 		}
 
-		if (scalarIndex != -1 && args[1 - scalarIndex] instanceof ASTObjPath) {
-			// inject
-			ASTObjPath path = (ASTObjPath) args[1 - scalarIndex];
+		if (scalar != null && path != null) {
 			try {
-				path.injectValue(o, evaluateChild(scalarIndex, o));
+				path.injectValue(o, scalar.getValue());
 			} catch (Exception ex) {
 				LOGGER.warn("Failed to inject value " + " on path " + path.getPath() + " to " + o, ex);
 			}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java
index fdfe063..17b27b5 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMax.java
@@ -19,6 +19,9 @@
 
 package org.apache.cayenne.exp.parser;
 
+import java.util.Collection;
+import java.util.Collections;
+
 import org.apache.cayenne.exp.Expression;
 
 /**
@@ -38,4 +41,10 @@
     public Expression shallowCopy() {
         return new ASTMax(id);
     }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    @Override
+    protected Object evaluateCollection(Collection<?> values) {
+        return Collections.max((Collection)values);
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java
index 209dbb2..e11dd6d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ASTMin.java
@@ -19,6 +19,9 @@
 
 package org.apache.cayenne.exp.parser;
 
+import java.util.Collection;
+import java.util.Collections;
+
 import org.apache.cayenne.exp.Expression;
 
 /**
@@ -38,4 +41,10 @@
     public Expression shallowCopy() {
         return new ASTMin(id);
     }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    @Override
+    protected Object evaluateCollection(Collection<?> values) {
+        return Collections.min((Collection)values);
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParser.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParser.java
index ed46175..ece560d 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParser.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParser.java
@@ -21,8 +21,6 @@
 
 package org.apache.cayenne.exp.parser;
 
-import java.math.BigDecimal;
-import java.math.BigInteger;
 import org.apache.cayenne.exp.Expression;
 
 /**
@@ -36,30 +34,31 @@
   final public Expression expression() throws ParseException {
     orCondition();
     jj_consume_token(0);
-        {if (true) return (Expression) jjtree.rootNode();}
+{if ("" != null) return (Expression) jjtree.rootNode();}
     throw new Error("Missing return statement in function");
-  }
+}
 
   final public void orCondition() throws ParseException {
     andCondition();
     label_1:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 1:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 1:{
         ;
         break;
+        }
       default:
         jj_la1[0] = jj_gen;
         break label_1;
       }
       jj_consume_token(1);
-                                ASTOr jjtn001 = new ASTOr(JJTOR);
+ASTOr jjtn001 = new ASTOr(JJTOR);
                                 boolean jjtc001 = true;
                                 jjtree.openNodeScope(jjtn001);
       try {
         andCondition();
       } catch (Throwable jjte001) {
-                                if (jjtc001) {
+if (jjtc001) {
                                   jjtree.clearNodeScope(jjtn001);
                                   jjtc001 = false;
                                 } else {
@@ -73,33 +72,34 @@
                                 }
                                 {if (true) throw (Error)jjte001;}
       } finally {
-                                if (jjtc001) {
+if (jjtc001) {
                                   jjtree.closeNodeScope(jjtn001,  2);
                                 }
       }
     }
-  }
+}
 
   final public void andCondition() throws ParseException {
     notCondition();
     label_2:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 2:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 2:{
         ;
         break;
+        }
       default:
         jj_la1[1] = jj_gen;
         break label_2;
       }
       jj_consume_token(2);
-                                 ASTAnd jjtn001 = new ASTAnd(JJTAND);
+ASTAnd jjtn001 = new ASTAnd(JJTAND);
                                  boolean jjtc001 = true;
                                  jjtree.openNodeScope(jjtn001);
       try {
         notCondition();
       } catch (Throwable jjte001) {
-                                 if (jjtc001) {
+if (jjtc001) {
                                    jjtree.clearNodeScope(jjtn001);
                                    jjtc001 = false;
                                  } else {
@@ -113,36 +113,38 @@
                                  }
                                  {if (true) throw (Error)jjte001;}
       } finally {
-                                 if (jjtc001) {
+if (jjtc001) {
                                    jjtree.closeNodeScope(jjtn001,  2);
                                  }
       }
     }
-  }
+}
 
   final public void notCondition() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case 3:
-    case 4:
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 3:
+    case 4:{
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 3:{
         jj_consume_token(3);
         break;
-      case 4:
+        }
+      case 4:{
         jj_consume_token(4);
         break;
+        }
       default:
         jj_la1[2] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
-                                  ASTNot jjtn001 = new ASTNot(JJTNOT);
+ASTNot jjtn001 = new ASTNot(JJTNOT);
                                   boolean jjtc001 = true;
                                   jjtree.openNodeScope(jjtn001);
       try {
         simpleCondition();
       } catch (Throwable jjte001) {
-                                  if (jjtc001) {
+if (jjtc001) {
                                     jjtree.clearNodeScope(jjtn001);
                                     jjtc001 = false;
                                   } else {
@@ -156,11 +158,12 @@
                                   }
                                   {if (true) throw (Error)jjte001;}
       } finally {
-                                  if (jjtc001) {
+if (jjtc001) {
                                     jjtree.closeNodeScope(jjtn001,  1);
                                   }
       }
       break;
+      }
     case 16:
     case 25:
     case 26:
@@ -173,7 +176,6 @@
     case MAX:
     case SUM:
     case COUNT:
-    case FUNCTION:
     case CONCAT:
     case SUBSTRING:
     case TRIM:
@@ -197,51 +199,56 @@
     case HOUR:
     case MINUTE:
     case SECOND:
-    case 66:
+    case FUNCTION:
+    case OPERATOR:
     case 67:
     case 68:
     case 69:
     case 70:
+    case 71:
     case PROPERTY_PATH:
     case SINGLE_QUOTED_STRING:
     case DOUBLE_QUOTED_STRING:
     case INT_LITERAL:
-    case FLOAT_LITERAL:
+    case FLOAT_LITERAL:{
       simpleCondition();
       break;
+      }
     default:
       jj_la1[3] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void simpleCondition() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case TRUE:
-            ASTTrue jjtn001 = new ASTTrue(JJTTRUE);
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case TRUE:{
+ASTTrue jjtn001 = new ASTTrue(JJTTRUE);
             boolean jjtc001 = true;
             jjtree.openNodeScope(jjtn001);
       try {
         jj_consume_token(TRUE);
       } finally {
-            if (jjtc001) {
+if (jjtc001) {
               jjtree.closeNodeScope(jjtn001, true);
             }
       }
       break;
-    case FALSE:
-            ASTFalse jjtn002 = new ASTFalse(JJTFALSE);
+      }
+    case FALSE:{
+ASTFalse jjtn002 = new ASTFalse(JJTFALSE);
             boolean jjtc002 = true;
             jjtree.openNodeScope(jjtn002);
       try {
         jj_consume_token(FALSE);
       } finally {
-            if (jjtc002) {
+if (jjtc002) {
               jjtree.closeNodeScope(jjtn002, true);
             }
       }
       break;
+      }
     case 16:
     case 25:
     case 26:
@@ -252,7 +259,6 @@
     case MAX:
     case SUM:
     case COUNT:
-    case FUNCTION:
     case CONCAT:
     case SUBSTRING:
     case TRIM:
@@ -276,18 +282,20 @@
     case HOUR:
     case MINUTE:
     case SECOND:
-    case 66:
+    case FUNCTION:
+    case OPERATOR:
     case 67:
     case 68:
     case 69:
     case 70:
+    case 71:
     case PROPERTY_PATH:
     case SINGLE_QUOTED_STRING:
     case DOUBLE_QUOTED_STRING:
     case INT_LITERAL:
-    case FLOAT_LITERAL:
+    case FLOAT_LITERAL:{
       conditionExpression();
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case 3:
       case 4:
       case 5:
@@ -301,29 +309,31 @@
       case 13:
       case 14:
       case 15:
-      case 18:
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      case 18:{
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
         case 5:
-        case 6:
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case 5:
+        case 6:{
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+          case 5:{
             jj_consume_token(5);
             break;
-          case 6:
+            }
+          case 6:{
             jj_consume_token(6);
             break;
+            }
           default:
             jj_la1[4] = jj_gen;
             jj_consume_token(-1);
             throw new ParseException();
           }
-                          ASTEqual jjtn003 = new ASTEqual(JJTEQUAL);
+ASTEqual jjtn003 = new ASTEqual(JJTEQUAL);
                           boolean jjtc003 = true;
                           jjtree.openNodeScope(jjtn003);
           try {
             scalarExpression();
           } catch (Throwable jjte003) {
-                          if (jjtc003) {
+if (jjtc003) {
                             jjtree.clearNodeScope(jjtn003);
                             jjtc003 = false;
                           } else {
@@ -337,32 +347,35 @@
                           }
                           {if (true) throw (Error)jjte003;}
           } finally {
-                          if (jjtc003) {
+if (jjtc003) {
                             jjtree.closeNodeScope(jjtn003,  2);
                           }
           }
           break;
+          }
         case 7:
-        case 8:
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case 7:
+        case 8:{
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+          case 7:{
             jj_consume_token(7);
             break;
-          case 8:
+            }
+          case 8:{
             jj_consume_token(8);
             break;
+            }
           default:
             jj_la1[5] = jj_gen;
             jj_consume_token(-1);
             throw new ParseException();
           }
-                           ASTNotEqual jjtn004 = new ASTNotEqual(JJTNOTEQUAL);
+ASTNotEqual jjtn004 = new ASTNotEqual(JJTNOTEQUAL);
                            boolean jjtc004 = true;
                            jjtree.openNodeScope(jjtn004);
           try {
             scalarExpression();
           } catch (Throwable jjte004) {
-                           if (jjtc004) {
+if (jjtc004) {
                              jjtree.clearNodeScope(jjtn004);
                              jjtc004 = false;
                            } else {
@@ -376,20 +389,21 @@
                            }
                            {if (true) throw (Error)jjte004;}
           } finally {
-                           if (jjtc004) {
+if (jjtc004) {
                              jjtree.closeNodeScope(jjtn004,  2);
                            }
           }
           break;
-        case 9:
+          }
+        case 9:{
           jj_consume_token(9);
-                 ASTLessOrEqual jjtn005 = new ASTLessOrEqual(JJTLESSOREQUAL);
+ASTLessOrEqual jjtn005 = new ASTLessOrEqual(JJTLESSOREQUAL);
                  boolean jjtc005 = true;
                  jjtree.openNodeScope(jjtn005);
           try {
             scalarExpression();
           } catch (Throwable jjte005) {
-                 if (jjtc005) {
+if (jjtc005) {
                    jjtree.clearNodeScope(jjtn005);
                    jjtc005 = false;
                  } else {
@@ -403,20 +417,21 @@
                  }
                  {if (true) throw (Error)jjte005;}
           } finally {
-                 if (jjtc005) {
+if (jjtc005) {
                    jjtree.closeNodeScope(jjtn005,  2);
                  }
           }
           break;
-        case 10:
+          }
+        case 10:{
           jj_consume_token(10);
-                ASTLess jjtn006 = new ASTLess(JJTLESS);
+ASTLess jjtn006 = new ASTLess(JJTLESS);
                 boolean jjtc006 = true;
                 jjtree.openNodeScope(jjtn006);
           try {
             scalarExpression();
           } catch (Throwable jjte006) {
-                if (jjtc006) {
+if (jjtc006) {
                   jjtree.clearNodeScope(jjtn006);
                   jjtc006 = false;
                 } else {
@@ -430,20 +445,21 @@
                 }
                 {if (true) throw (Error)jjte006;}
           } finally {
-                if (jjtc006) {
+if (jjtc006) {
                   jjtree.closeNodeScope(jjtn006,  2);
                 }
           }
           break;
-        case 11:
+          }
+        case 11:{
           jj_consume_token(11);
-                 ASTGreater jjtn007 = new ASTGreater(JJTGREATER);
+ASTGreater jjtn007 = new ASTGreater(JJTGREATER);
                  boolean jjtc007 = true;
                  jjtree.openNodeScope(jjtn007);
           try {
             scalarExpression();
           } catch (Throwable jjte007) {
-                 if (jjtc007) {
+if (jjtc007) {
                    jjtree.clearNodeScope(jjtn007);
                    jjtc007 = false;
                  } else {
@@ -457,20 +473,21 @@
                  }
                  {if (true) throw (Error)jjte007;}
           } finally {
-                 if (jjtc007) {
+if (jjtc007) {
                    jjtree.closeNodeScope(jjtn007,  2);
                  }
           }
           break;
-        case 12:
+          }
+        case 12:{
           jj_consume_token(12);
-                 ASTGreaterOrEqual jjtn008 = new ASTGreaterOrEqual(JJTGREATEROREQUAL);
+ASTGreaterOrEqual jjtn008 = new ASTGreaterOrEqual(JJTGREATEROREQUAL);
                  boolean jjtc008 = true;
                  jjtree.openNodeScope(jjtn008);
           try {
             scalarExpression();
           } catch (Throwable jjte008) {
-                 if (jjtc008) {
+if (jjtc008) {
                    jjtree.clearNodeScope(jjtn008);
                    jjtc008 = false;
                  } else {
@@ -484,20 +501,21 @@
                  }
                  {if (true) throw (Error)jjte008;}
           } finally {
-                 if (jjtc008) {
+if (jjtc008) {
                    jjtree.closeNodeScope(jjtn008,  2);
                  }
           }
           break;
-        case 13:
+          }
+        case 13:{
           jj_consume_token(13);
-                   ASTLike jjtn009 = new ASTLike(JJTLIKE);
+ASTLike jjtn009 = new ASTLike(JJTLIKE);
                    boolean jjtc009 = true;
                    jjtree.openNodeScope(jjtn009);
           try {
             scalarExpression();
           } catch (Throwable jjte009) {
-                   if (jjtc009) {
+if (jjtc009) {
                      jjtree.clearNodeScope(jjtn009);
                      jjtc009 = false;
                    } else {
@@ -511,20 +529,21 @@
                    }
                    {if (true) throw (Error)jjte009;}
           } finally {
-                   if (jjtc009) {
+if (jjtc009) {
                      jjtree.closeNodeScope(jjtn009,  2);
                    }
           }
           break;
-        case 14:
+          }
+        case 14:{
           jj_consume_token(14);
-                              ASTLikeIgnoreCase jjtn010 = new ASTLikeIgnoreCase(JJTLIKEIGNORECASE);
+ASTLikeIgnoreCase jjtn010 = new ASTLikeIgnoreCase(JJTLIKEIGNORECASE);
                               boolean jjtc010 = true;
                               jjtree.openNodeScope(jjtn010);
           try {
             scalarExpression();
           } catch (Throwable jjte010) {
-                              if (jjtc010) {
+if (jjtc010) {
                                 jjtree.clearNodeScope(jjtn010);
                                 jjtc010 = false;
                               } else {
@@ -538,33 +557,36 @@
                               }
                               {if (true) throw (Error)jjte010;}
           } finally {
-                              if (jjtc010) {
+if (jjtc010) {
                                 jjtree.closeNodeScope(jjtn010,  2);
                               }
           }
           break;
-        case 15:
+          }
+        case 15:{
           jj_consume_token(15);
-                 ASTIn jjtn011 = new ASTIn(JJTIN);
+ASTIn jjtn011 = new ASTIn(JJTIN);
                  boolean jjtc011 = true;
                  jjtree.openNodeScope(jjtn011);
           try {
-            switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-            case 66:
+            switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+            case 67:{
               namedParameter();
               break;
-            case 16:
+              }
+            case 16:{
               jj_consume_token(16);
               scalarCommaList();
               jj_consume_token(17);
               break;
+              }
             default:
               jj_la1[6] = jj_gen;
               jj_consume_token(-1);
               throw new ParseException();
             }
           } catch (Throwable jjte011) {
-                 if (jjtc011) {
+if (jjtc011) {
                    jjtree.clearNodeScope(jjtn011);
                    jjtc011 = false;
                  } else {
@@ -578,22 +600,23 @@
                  }
                  {if (true) throw (Error)jjte011;}
           } finally {
-                 if (jjtc011) {
+if (jjtc011) {
                    jjtree.closeNodeScope(jjtn011,  2);
                  }
           }
           break;
-        case 18:
+          }
+        case 18:{
           jj_consume_token(18);
           scalarExpression();
           jj_consume_token(2);
-                                                ASTBetween jjtn012 = new ASTBetween(JJTBETWEEN);
+ASTBetween jjtn012 = new ASTBetween(JJTBETWEEN);
                                                 boolean jjtc012 = true;
                                                 jjtree.openNodeScope(jjtn012);
           try {
             scalarExpression();
           } catch (Throwable jjte012) {
-                                                if (jjtc012) {
+if (jjtc012) {
                                                   jjtree.clearNodeScope(jjtn012);
                                                   jjtc012 = false;
                                                 } else {
@@ -607,56 +630,62 @@
                                                 }
                                                 {if (true) throw (Error)jjte012;}
           } finally {
-                                                if (jjtc012) {
+if (jjtc012) {
                                                   jjtree.closeNodeScope(jjtn012,  3);
                                                 }
           }
           break;
+          }
         case 3:
-        case 4:
+        case 4:{
           simpleNotCondition();
           break;
+          }
         default:
           jj_la1[7] = jj_gen;
           jj_consume_token(-1);
           throw new ParseException();
         }
         break;
+        }
       default:
         jj_la1[8] = jj_gen;
         ;
       }
       break;
+      }
     default:
       jj_la1[9] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void simpleNotCondition() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 3:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case 3:{
       jj_consume_token(3);
       break;
-    case 4:
+      }
+    case 4:{
       jj_consume_token(4);
       break;
+      }
     default:
       jj_la1[10] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 13:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case 13:{
       jj_consume_token(13);
-                         ASTNotLike jjtn001 = new ASTNotLike(JJTNOTLIKE);
+ASTNotLike jjtn001 = new ASTNotLike(JJTNOTLIKE);
                          boolean jjtc001 = true;
                          jjtree.openNodeScope(jjtn001);
       try {
         scalarExpression();
       } catch (Throwable jjte001) {
-                         if (jjtc001) {
+if (jjtc001) {
                            jjtree.clearNodeScope(jjtn001);
                            jjtc001 = false;
                          } else {
@@ -670,20 +699,21 @@
                          }
                          {if (true) throw (Error)jjte001;}
       } finally {
-                         if (jjtc001) {
+if (jjtc001) {
                            jjtree.closeNodeScope(jjtn001,  2);
                          }
       }
       break;
-    case 14:
+      }
+    case 14:{
       jj_consume_token(14);
-                                ASTNotLikeIgnoreCase jjtn002 = new ASTNotLikeIgnoreCase(JJTNOTLIKEIGNORECASE);
+ASTNotLikeIgnoreCase jjtn002 = new ASTNotLikeIgnoreCase(JJTNOTLIKEIGNORECASE);
                                 boolean jjtc002 = true;
                                 jjtree.openNodeScope(jjtn002);
       try {
         scalarExpression();
       } catch (Throwable jjte002) {
-                                if (jjtc002) {
+if (jjtc002) {
                                   jjtree.clearNodeScope(jjtn002);
                                   jjtc002 = false;
                                 } else {
@@ -697,33 +727,36 @@
                                 }
                                 {if (true) throw (Error)jjte002;}
       } finally {
-                                if (jjtc002) {
+if (jjtc002) {
                                   jjtree.closeNodeScope(jjtn002,  2);
                                 }
       }
       break;
-    case 15:
+      }
+    case 15:{
       jj_consume_token(15);
-                       ASTNotIn jjtn003 = new ASTNotIn(JJTNOTIN);
+ASTNotIn jjtn003 = new ASTNotIn(JJTNOTIN);
                        boolean jjtc003 = true;
                        jjtree.openNodeScope(jjtn003);
       try {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 66:
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case 67:{
           namedParameter();
           break;
-        case 16:
+          }
+        case 16:{
           jj_consume_token(16);
           scalarCommaList();
           jj_consume_token(17);
           break;
+          }
         default:
           jj_la1[11] = jj_gen;
           jj_consume_token(-1);
           throw new ParseException();
         }
       } catch (Throwable jjte003) {
-                       if (jjtc003) {
+if (jjtc003) {
                          jjtree.clearNodeScope(jjtn003);
                          jjtc003 = false;
                        } else {
@@ -737,22 +770,23 @@
                        }
                        {if (true) throw (Error)jjte003;}
       } finally {
-                       if (jjtc003) {
+if (jjtc003) {
                          jjtree.closeNodeScope(jjtn003,  2);
                        }
       }
       break;
-    case 18:
+      }
+    case 18:{
       jj_consume_token(18);
       scalarExpression();
       jj_consume_token(2);
-                                                      ASTNotBetween jjtn004 = new ASTNotBetween(JJTNOTBETWEEN);
+ASTNotBetween jjtn004 = new ASTNotBetween(JJTNOTBETWEEN);
                                                       boolean jjtc004 = true;
                                                       jjtree.openNodeScope(jjtn004);
       try {
         scalarExpression();
       } catch (Throwable jjte004) {
-                                                      if (jjtc004) {
+if (jjtc004) {
                                                         jjtree.clearNodeScope(jjtn004);
                                                         jjtc004 = false;
                                                       } else {
@@ -766,30 +800,32 @@
                                                       }
                                                       {if (true) throw (Error)jjte004;}
       } finally {
-                                                      if (jjtc004) {
+if (jjtc004) {
                                                         jjtree.closeNodeScope(jjtn004,  3);
                                                       }
       }
       break;
+      }
     default:
       jj_la1[12] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void scalarCommaList() throws ParseException {
-          ASTList jjtn001 = new ASTList(JJTLIST);
+ASTList jjtn001 = new ASTList(JJTLIST);
           boolean jjtc001 = true;
           jjtree.openNodeScope(jjtn001);
     try {
       scalarConstExpression();
       label_3:
       while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 19:
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case 19:{
           ;
           break;
+          }
         default:
           jj_la1[13] = jj_gen;
           break label_3;
@@ -798,7 +834,7 @@
         scalarConstExpression();
       }
     } catch (Throwable jjte001) {
-          if (jjtc001) {
+if (jjtc001) {
             jjtree.clearNodeScope(jjtn001);
             jjtc001 = false;
           } else {
@@ -812,19 +848,18 @@
           }
           {if (true) throw (Error)jjte001;}
     } finally {
-          if (jjtc001) {
+if (jjtc001) {
             jjtree.closeNodeScope(jjtn001, true);
           }
     }
-  }
+}
 
   final public void conditionExpression() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case 16:
     case 25:
     case 26:
     case 28:
-    case FUNCTION:
     case LENGTH:
     case LOCATE:
     case ABS:
@@ -840,142 +875,155 @@
     case HOUR:
     case MINUTE:
     case SECOND:
-    case 66:
+    case FUNCTION:
+    case OPERATOR:
     case 67:
     case 68:
     case 69:
     case 70:
+    case 71:
     case PROPERTY_PATH:
     case INT_LITERAL:
-    case FLOAT_LITERAL:
+    case FLOAT_LITERAL:{
       numericExpression();
       break;
+      }
     case CONCAT:
     case SUBSTRING:
     case TRIM:
     case LOWER:
     case UPPER:
     case SINGLE_QUOTED_STRING:
-    case DOUBLE_QUOTED_STRING:
+    case DOUBLE_QUOTED_STRING:{
       stringExpression();
       break;
-    case NULL:
-                  ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
+      }
+    case NULL:{
+ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
                   boolean jjtc001 = true;
                   jjtree.openNodeScope(jjtn001);
       try {
         jj_consume_token(NULL);
       } finally {
-                  if (jjtc001) {
+if (jjtc001) {
                     jjtree.closeNodeScope(jjtn001,  0);
                   }
       }
       break;
+      }
     case AVG:
     case MIN:
     case MAX:
     case SUM:
-    case COUNT:
+    case COUNT:{
       aggregateExpression();
       break;
+      }
     case CURRENT_DATE:
     case CURRENT_TIME:
-    case CURRENT_TIMESTAMP:
+    case CURRENT_TIMESTAMP:{
       dateTimeFunction();
       break;
+      }
     default:
       jj_la1[14] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void stringParameter() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 67:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case 68:
     case 69:
     case 70:
-    case PROPERTY_PATH:
+    case 71:
+    case PROPERTY_PATH:{
       pathExpression();
       break;
+      }
     case CONCAT:
     case SUBSTRING:
     case TRIM:
     case LOWER:
     case UPPER:
     case SINGLE_QUOTED_STRING:
-    case DOUBLE_QUOTED_STRING:
+    case DOUBLE_QUOTED_STRING:{
       stringExpression();
       break;
+      }
     default:
       jj_la1[15] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void stringLiteral() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case SINGLE_QUOTED_STRING:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case SINGLE_QUOTED_STRING:{
       jj_consume_token(SINGLE_QUOTED_STRING);
-                             ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
                              boolean jjtc001 = true;
                              jjtree.openNodeScope(jjtn001);
       try {
-                             jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                              jjtc001 = false;
-                             jjtn001.setValue(token_source.literalValue);
+jjtn001.setValue(token_source.literalValue);
       } finally {
-                             if (jjtc001) {
+if (jjtc001) {
                                jjtree.closeNodeScope(jjtn001,  0);
                              }
       }
       break;
-    case DOUBLE_QUOTED_STRING:
+      }
+    case DOUBLE_QUOTED_STRING:{
       jj_consume_token(DOUBLE_QUOTED_STRING);
-                             ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
                              boolean jjtc002 = true;
                              jjtree.openNodeScope(jjtn002);
       try {
-                             jjtree.closeNodeScope(jjtn002,  0);
+jjtree.closeNodeScope(jjtn002,  0);
                              jjtc002 = false;
-                             jjtn002.setValue(token_source.literalValue);
+jjtn002.setValue(token_source.literalValue);
       } finally {
-                             if (jjtc002) {
+if (jjtc002) {
                                jjtree.closeNodeScope(jjtn002,  0);
                              }
       }
       break;
+      }
     default:
       jj_la1[16] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void stringExpression() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case SINGLE_QUOTED_STRING:
-    case DOUBLE_QUOTED_STRING:
+    case DOUBLE_QUOTED_STRING:{
       stringLiteral();
       break;
+      }
     case CONCAT:
     case SUBSTRING:
     case TRIM:
     case LOWER:
-    case UPPER:
+    case UPPER:{
       functionsReturningStrings();
       break;
+      }
     default:
       jj_la1[17] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void scalarExpression() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case 16:
     case 25:
     case 26:
@@ -986,7 +1034,6 @@
     case MAX:
     case SUM:
     case COUNT:
-    case FUNCTION:
     case CONCAT:
     case SUBSTRING:
     case TRIM:
@@ -1010,181 +1057,194 @@
     case HOUR:
     case MINUTE:
     case SECOND:
-    case 66:
+    case FUNCTION:
+    case OPERATOR:
     case 67:
     case 68:
     case 69:
     case 70:
+    case 71:
     case PROPERTY_PATH:
     case SINGLE_QUOTED_STRING:
     case DOUBLE_QUOTED_STRING:
     case INT_LITERAL:
-    case FLOAT_LITERAL:
+    case FLOAT_LITERAL:{
       conditionExpression();
       break;
-    case TRUE:
+      }
+    case TRUE:{
       jj_consume_token(TRUE);
-                   ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
                    boolean jjtc001 = true;
                    jjtree.openNodeScope(jjtn001);
       try {
-                   jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                    jjtc001 = false;
-                   jjtn001.setValue(true);
+jjtn001.setValue(true);
       } finally {
-                   if (jjtc001) {
+if (jjtc001) {
                      jjtree.closeNodeScope(jjtn001,  0);
                    }
       }
       break;
-    case FALSE:
+      }
+    case FALSE:{
       jj_consume_token(FALSE);
-                    ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
                     boolean jjtc002 = true;
                     jjtree.openNodeScope(jjtn002);
       try {
-                    jjtree.closeNodeScope(jjtn002,  0);
+jjtree.closeNodeScope(jjtn002,  0);
                     jjtc002 = false;
-                    jjtn002.setValue(false);
+jjtn002.setValue(false);
       } finally {
-                    if (jjtc002) {
+if (jjtc002) {
                       jjtree.closeNodeScope(jjtn002,  0);
                     }
       }
       break;
+      }
     default:
       jj_la1[18] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void scalarConstExpression() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case SINGLE_QUOTED_STRING:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case SINGLE_QUOTED_STRING:{
       jj_consume_token(SINGLE_QUOTED_STRING);
-                                   ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
                                    boolean jjtc001 = true;
                                    jjtree.openNodeScope(jjtn001);
       try {
-                                   jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                                    jjtc001 = false;
-                                   jjtn001.setValue(token_source.literalValue);
+jjtn001.setValue(token_source.literalValue);
       } finally {
-                                   if (jjtc001) {
+if (jjtc001) {
                                      jjtree.closeNodeScope(jjtn001,  0);
                                    }
       }
       break;
-    case DOUBLE_QUOTED_STRING:
+      }
+    case DOUBLE_QUOTED_STRING:{
       jj_consume_token(DOUBLE_QUOTED_STRING);
-                                   ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
                                    boolean jjtc002 = true;
                                    jjtree.openNodeScope(jjtn002);
       try {
-                                   jjtree.closeNodeScope(jjtn002,  0);
+jjtree.closeNodeScope(jjtn002,  0);
                                    jjtc002 = false;
-                                   jjtn002.setValue(token_source.literalValue);
+jjtn002.setValue(token_source.literalValue);
       } finally {
-                                   if (jjtc002) {
+if (jjtc002) {
                                      jjtree.closeNodeScope(jjtn002,  0);
                                    }
       }
       break;
-    case 66:
+      }
+    case 67:{
       namedParameter();
       break;
-    case INT_LITERAL:
+      }
+    case INT_LITERAL:{
       jj_consume_token(INT_LITERAL);
-                            ASTScalar jjtn003 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn003 = new ASTScalar(JJTSCALAR);
                             boolean jjtc003 = true;
                             jjtree.openNodeScope(jjtn003);
       try {
-                            jjtree.closeNodeScope(jjtn003,  0);
+jjtree.closeNodeScope(jjtn003,  0);
                             jjtc003 = false;
-                            jjtn003.setValue(token_source.literalValue);
+jjtn003.setValue(token_source.literalValue);
       } finally {
-                            if (jjtc003) {
+if (jjtc003) {
                               jjtree.closeNodeScope(jjtn003,  0);
                             }
       }
       break;
-    case FLOAT_LITERAL:
+      }
+    case FLOAT_LITERAL:{
       jj_consume_token(FLOAT_LITERAL);
-                            ASTScalar jjtn004 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn004 = new ASTScalar(JJTSCALAR);
                             boolean jjtc004 = true;
                             jjtree.openNodeScope(jjtn004);
       try {
-                            jjtree.closeNodeScope(jjtn004,  0);
+jjtree.closeNodeScope(jjtn004,  0);
                             jjtc004 = false;
-                            jjtn004.setValue(token_source.literalValue);
+jjtn004.setValue(token_source.literalValue);
       } finally {
-                            if (jjtc004) {
+if (jjtc004) {
                               jjtree.closeNodeScope(jjtn004,  0);
                             }
       }
       break;
-    case TRUE:
+      }
+    case TRUE:{
       jj_consume_token(TRUE);
-                    ASTScalar jjtn005 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn005 = new ASTScalar(JJTSCALAR);
                     boolean jjtc005 = true;
                     jjtree.openNodeScope(jjtn005);
       try {
-                    jjtree.closeNodeScope(jjtn005,  0);
+jjtree.closeNodeScope(jjtn005,  0);
                     jjtc005 = false;
-                    jjtn005.setValue(true);
+jjtn005.setValue(true);
       } finally {
-                    if (jjtc005) {
+if (jjtc005) {
                       jjtree.closeNodeScope(jjtn005,  0);
                     }
       }
       break;
-    case FALSE:
+      }
+    case FALSE:{
       jj_consume_token(FALSE);
-                    ASTScalar jjtn006 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn006 = new ASTScalar(JJTSCALAR);
                     boolean jjtc006 = true;
                     jjtree.openNodeScope(jjtn006);
       try {
-                    jjtree.closeNodeScope(jjtn006,  0);
+jjtree.closeNodeScope(jjtn006,  0);
                     jjtc006 = false;
-                    jjtn006.setValue(false);
+jjtn006.setValue(false);
       } finally {
-                    if (jjtc006) {
+if (jjtc006) {
                       jjtree.closeNodeScope(jjtn006,  0);
                     }
       }
       break;
+      }
     default:
       jj_la1[19] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void numericExpression() throws ParseException {
     bitwiseOr();
-  }
+}
 
   final public void bitwiseOr() throws ParseException {
     bitwiseXor();
     label_4:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 20:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 20:{
         ;
         break;
+        }
       default:
         jj_la1[20] = jj_gen;
         break label_4;
       }
       jj_consume_token(20);
-              ASTBitwiseOr jjtn001 = new ASTBitwiseOr(JJTBITWISEOR);
+ASTBitwiseOr jjtn001 = new ASTBitwiseOr(JJTBITWISEOR);
               boolean jjtc001 = true;
               jjtree.openNodeScope(jjtn001);
       try {
         bitwiseXor();
       } catch (Throwable jjte001) {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.clearNodeScope(jjtn001);
                 jjtc001 = false;
               } else {
@@ -1198,33 +1258,34 @@
               }
               {if (true) throw (Error)jjte001;}
       } finally {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.closeNodeScope(jjtn001,  2);
               }
       }
     }
-  }
+}
 
   final public void bitwiseXor() throws ParseException {
     bitwiseAnd();
     label_5:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 21:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 21:{
         ;
         break;
+        }
       default:
         jj_la1[21] = jj_gen;
         break label_5;
       }
       jj_consume_token(21);
-              ASTBitwiseXor jjtn001 = new ASTBitwiseXor(JJTBITWISEXOR);
+ASTBitwiseXor jjtn001 = new ASTBitwiseXor(JJTBITWISEXOR);
               boolean jjtc001 = true;
               jjtree.openNodeScope(jjtn001);
       try {
         bitwiseAnd();
       } catch (Throwable jjte001) {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.clearNodeScope(jjtn001);
                 jjtc001 = false;
               } else {
@@ -1238,33 +1299,34 @@
               }
               {if (true) throw (Error)jjte001;}
       } finally {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.closeNodeScope(jjtn001,  2);
               }
       }
     }
-  }
+}
 
   final public void bitwiseAnd() throws ParseException {
     bitwiseShift();
     label_6:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 22:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 22:{
         ;
         break;
+        }
       default:
         jj_la1[22] = jj_gen;
         break label_6;
       }
       jj_consume_token(22);
-              ASTBitwiseAnd jjtn001 = new ASTBitwiseAnd(JJTBITWISEAND);
+ASTBitwiseAnd jjtn001 = new ASTBitwiseAnd(JJTBITWISEAND);
               boolean jjtc001 = true;
               jjtree.openNodeScope(jjtn001);
       try {
         bitwiseShift();
       } catch (Throwable jjte001) {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.clearNodeScope(jjtn001);
                 jjtc001 = false;
               } else {
@@ -1278,36 +1340,37 @@
               }
               {if (true) throw (Error)jjte001;}
       } finally {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.closeNodeScope(jjtn001,  2);
               }
       }
     }
-  }
+}
 
   final public void bitwiseShift() throws ParseException {
     arithmeticExp();
     label_7:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case 23:
-      case 24:
+      case 24:{
         ;
         break;
+        }
       default:
         jj_la1[23] = jj_gen;
         break label_7;
       }
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 23:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 23:{
         jj_consume_token(23);
-               ASTBitwiseLeftShift jjtn001 = new ASTBitwiseLeftShift(JJTBITWISELEFTSHIFT);
+ASTBitwiseLeftShift jjtn001 = new ASTBitwiseLeftShift(JJTBITWISELEFTSHIFT);
                boolean jjtc001 = true;
                jjtree.openNodeScope(jjtn001);
         try {
           arithmeticExp();
         } catch (Throwable jjte001) {
-               if (jjtc001) {
+if (jjtc001) {
                  jjtree.clearNodeScope(jjtn001);
                  jjtc001 = false;
                } else {
@@ -1321,20 +1384,21 @@
                }
                {if (true) throw (Error)jjte001;}
         } finally {
-               if (jjtc001) {
+if (jjtc001) {
                  jjtree.closeNodeScope(jjtn001,  2);
                }
         }
         break;
-      case 24:
+        }
+      case 24:{
         jj_consume_token(24);
-               ASTBitwiseRightShift jjtn002 = new ASTBitwiseRightShift(JJTBITWISERIGHTSHIFT);
+ASTBitwiseRightShift jjtn002 = new ASTBitwiseRightShift(JJTBITWISERIGHTSHIFT);
                boolean jjtc002 = true;
                jjtree.openNodeScope(jjtn002);
         try {
           arithmeticExp();
         } catch (Throwable jjte002) {
-               if (jjtc002) {
+if (jjtc002) {
                  jjtree.clearNodeScope(jjtn002);
                  jjtc002 = false;
                } else {
@@ -1348,42 +1412,44 @@
                }
                {if (true) throw (Error)jjte002;}
         } finally {
-               if (jjtc002) {
+if (jjtc002) {
                  jjtree.closeNodeScope(jjtn002,  2);
                }
         }
         break;
+        }
       default:
         jj_la1[24] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
     }
-  }
+}
 
   final public void arithmeticExp() throws ParseException {
     multiplySubtractExp();
     label_8:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case 25:
-      case 26:
+      case 26:{
         ;
         break;
+        }
       default:
         jj_la1[25] = jj_gen;
         break label_8;
       }
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 25:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 25:{
         jj_consume_token(25);
-              ASTAdd jjtn001 = new ASTAdd(JJTADD);
+ASTAdd jjtn001 = new ASTAdd(JJTADD);
               boolean jjtc001 = true;
               jjtree.openNodeScope(jjtn001);
         try {
           multiplySubtractExp();
         } catch (Throwable jjte001) {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.clearNodeScope(jjtn001);
                 jjtc001 = false;
               } else {
@@ -1397,20 +1463,21 @@
               }
               {if (true) throw (Error)jjte001;}
         } finally {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.closeNodeScope(jjtn001,  2);
               }
         }
         break;
-      case 26:
+        }
+      case 26:{
         jj_consume_token(26);
-              ASTSubtract jjtn002 = new ASTSubtract(JJTSUBTRACT);
+ASTSubtract jjtn002 = new ASTSubtract(JJTSUBTRACT);
               boolean jjtc002 = true;
               jjtree.openNodeScope(jjtn002);
         try {
           multiplySubtractExp();
         } catch (Throwable jjte002) {
-              if (jjtc002) {
+if (jjtc002) {
                 jjtree.clearNodeScope(jjtn002);
                 jjtc002 = false;
               } else {
@@ -1424,42 +1491,44 @@
               }
               {if (true) throw (Error)jjte002;}
         } finally {
-              if (jjtc002) {
+if (jjtc002) {
                 jjtree.closeNodeScope(jjtn002,  2);
               }
         }
         break;
+        }
       default:
         jj_la1[26] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
     }
-  }
+}
 
   final public void multiplySubtractExp() throws ParseException {
     numericTermExt();
     label_9:
     while (true) {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case 27:
-      case ASTERISK:
+      case ASTERISK:{
         ;
         break;
+        }
       default:
         jj_la1[27] = jj_gen;
         break label_9;
       }
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case ASTERISK:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case ASTERISK:{
         jj_consume_token(ASTERISK);
-                     ASTMultiply jjtn001 = new ASTMultiply(JJTMULTIPLY);
+ASTMultiply jjtn001 = new ASTMultiply(JJTMULTIPLY);
                      boolean jjtc001 = true;
                      jjtree.openNodeScope(jjtn001);
         try {
           numericTermExt();
         } catch (Throwable jjte001) {
-                     if (jjtc001) {
+if (jjtc001) {
                        jjtree.clearNodeScope(jjtn001);
                        jjtc001 = false;
                      } else {
@@ -1473,20 +1542,21 @@
                      }
                      {if (true) throw (Error)jjte001;}
         } finally {
-                     if (jjtc001) {
+if (jjtc001) {
                        jjtree.closeNodeScope(jjtn001,  2);
                      }
         }
         break;
-      case 27:
+        }
+      case 27:{
         jj_consume_token(27);
-              ASTDivide jjtn002 = new ASTDivide(JJTDIVIDE);
+ASTDivide jjtn002 = new ASTDivide(JJTDIVIDE);
               boolean jjtc002 = true;
               jjtree.openNodeScope(jjtn002);
         try {
           numericTermExt();
         } catch (Throwable jjte002) {
-              if (jjtc002) {
+if (jjtc002) {
                 jjtree.clearNodeScope(jjtn002);
                 jjtc002 = false;
               } else {
@@ -1500,25 +1570,25 @@
               }
               {if (true) throw (Error)jjte002;}
         } finally {
-              if (jjtc002) {
+if (jjtc002) {
                 jjtree.closeNodeScope(jjtn002,  2);
               }
         }
         break;
+        }
       default:
         jj_la1[28] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
     }
-  }
+}
 
   final public void numericTermExt() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case 16:
     case 25:
     case 26:
-    case FUNCTION:
     case LENGTH:
     case LOCATE:
     case ABS:
@@ -1534,25 +1604,28 @@
     case HOUR:
     case MINUTE:
     case SECOND:
-    case 66:
+    case FUNCTION:
+    case OPERATOR:
     case 67:
     case 68:
     case 69:
     case 70:
+    case 71:
     case PROPERTY_PATH:
     case INT_LITERAL:
-    case FLOAT_LITERAL:
+    case FLOAT_LITERAL:{
       numericTerm();
       break;
-    case 28:
+      }
+    case 28:{
       jj_consume_token(28);
-              ASTBitwiseNot jjtn001 = new ASTBitwiseNot(JJTBITWISENOT);
+ASTBitwiseNot jjtn001 = new ASTBitwiseNot(JJTBITWISENOT);
               boolean jjtc001 = true;
               jjtree.openNodeScope(jjtn001);
       try {
         numericTerm();
       } catch (Throwable jjte001) {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.clearNodeScope(jjtn001);
                 jjtc001 = false;
               } else {
@@ -1566,23 +1639,23 @@
               }
               {if (true) throw (Error)jjte001;}
       } finally {
-              if (jjtc001) {
+if (jjtc001) {
                 jjtree.closeNodeScope(jjtn001,  1);
               }
       }
       break;
+      }
     default:
       jj_la1[29] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void numericTerm() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
     case 16:
     case 25:
-    case FUNCTION:
     case LENGTH:
     case LOCATE:
     case ABS:
@@ -1598,33 +1671,37 @@
     case HOUR:
     case MINUTE:
     case SECOND:
-    case 66:
+    case FUNCTION:
+    case OPERATOR:
     case 67:
     case 68:
     case 69:
     case 70:
+    case 71:
     case PROPERTY_PATH:
     case INT_LITERAL:
-    case FLOAT_LITERAL:
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 25:
+    case FLOAT_LITERAL:{
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 25:{
         jj_consume_token(25);
         break;
+        }
       default:
         jj_la1[30] = jj_gen;
         ;
       }
       numericPrimary();
       break;
-    case 26:
+      }
+    case 26:{
       jj_consume_token(26);
-               ASTNegate jjtn001 = new ASTNegate(JJTNEGATE);
+ASTNegate jjtn001 = new ASTNegate(JJTNEGATE);
                boolean jjtc001 = true;
                jjtree.openNodeScope(jjtn001);
       try {
         numericTerm();
       } catch (Throwable jjte001) {
-               if (jjtc001) {
+if (jjtc001) {
                  jjtree.clearNodeScope(jjtn001);
                  jjtc001 = false;
                } else {
@@ -1638,58 +1715,63 @@
                }
                {if (true) throw (Error)jjte001;}
       } finally {
-               if (jjtc001) {
+if (jjtc001) {
                  jjtree.closeNodeScope(jjtn001,  1);
                }
       }
       break;
+      }
     default:
       jj_la1[31] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void numericPrimary() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case 16:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case 16:{
       jj_consume_token(16);
       orCondition();
       jj_consume_token(17);
       break;
-    case INT_LITERAL:
+      }
+    case INT_LITERAL:{
       jj_consume_token(INT_LITERAL);
-                           ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn001 = new ASTScalar(JJTSCALAR);
                            boolean jjtc001 = true;
                            jjtree.openNodeScope(jjtn001);
       try {
-                           jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                            jjtc001 = false;
-                           jjtn001.setValue(token_source.literalValue);
+jjtn001.setValue(token_source.literalValue);
       } finally {
-                           if (jjtc001) {
+if (jjtc001) {
                              jjtree.closeNodeScope(jjtn001,  0);
                            }
       }
       break;
-    case FLOAT_LITERAL:
+      }
+    case FLOAT_LITERAL:{
       jj_consume_token(FLOAT_LITERAL);
-                            ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
+ASTScalar jjtn002 = new ASTScalar(JJTSCALAR);
                             boolean jjtc002 = true;
                             jjtree.openNodeScope(jjtn002);
       try {
-                            jjtree.closeNodeScope(jjtn002,  0);
+jjtree.closeNodeScope(jjtn002,  0);
                             jjtc002 = false;
-                            jjtn002.setValue(token_source.literalValue);
+jjtn002.setValue(token_source.literalValue);
       } finally {
-                            if (jjtc002) {
+if (jjtc002) {
                               jjtree.closeNodeScope(jjtn002,  0);
                             }
       }
       break;
-    case 66:
+      }
+    case 67:{
       namedParameter();
       break;
+      }
     case LENGTH:
     case LOCATE:
     case ABS:
@@ -1704,52 +1786,63 @@
     case DAY_OF_WEEK:
     case HOUR:
     case MINUTE:
-    case SECOND:
+    case SECOND:{
       functionsReturningNumerics();
       break;
-    case 67:
+      }
     case 68:
     case 69:
     case 70:
-    case PROPERTY_PATH:
+    case 71:
+    case PROPERTY_PATH:{
       pathExpression();
       break;
-    case FUNCTION:
+      }
+    case FUNCTION:{
       customFunction();
       break;
+      }
+    case OPERATOR:{
+      customOperator();
+      break;
+      }
     default:
       jj_la1[32] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   final public void functionsReturningStrings() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case CONCAT:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case CONCAT:{
       concat();
       break;
-    case SUBSTRING:
+      }
+    case SUBSTRING:{
       substring();
       break;
-    case TRIM:
+      }
+    case TRIM:{
       trim();
       break;
-    case LOWER:
+      }
+    case LOWER:{
       lower();
       break;
-    case UPPER:
+      }
+    case UPPER:{
       upper();
       break;
+      }
     default:
       jj_la1[33] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
-  final public void customFunction() throws ParseException {
-                                         /*@bgen(jjtree) CustomFunction */
+  final public void customFunction() throws ParseException {/*@bgen(jjtree) CustomFunction */
   ASTCustomFunction jjtn000 = new ASTCustomFunction(JJTCUSTOMFUNCTION);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -1759,30 +1852,31 @@
       stringLiteral();
       label_10:
       while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 19:
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case 19:{
           ;
           break;
+          }
         default:
           jj_la1[34] = jj_gen;
           break label_10;
         }
         jj_consume_token(19);
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
         case CONCAT:
         case SUBSTRING:
         case TRIM:
         case LOWER:
         case UPPER:
         case SINGLE_QUOTED_STRING:
-        case DOUBLE_QUOTED_STRING:
+        case DOUBLE_QUOTED_STRING:{
           stringExpression();
           break;
+          }
         case 16:
         case 25:
         case 26:
         case 28:
-        case FUNCTION:
         case LENGTH:
         case LOCATE:
         case ABS:
@@ -1798,16 +1892,19 @@
         case HOUR:
         case MINUTE:
         case SECOND:
-        case 66:
+        case FUNCTION:
+        case OPERATOR:
         case 67:
         case 68:
         case 69:
         case 70:
+        case 71:
         case PROPERTY_PATH:
         case INT_LITERAL:
-        case FLOAT_LITERAL:
+        case FLOAT_LITERAL:{
           numericExpression();
           break;
+          }
         default:
           jj_la1[35] = jj_gen;
           jj_consume_token(-1);
@@ -1816,7 +1913,7 @@
       }
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -1830,14 +1927,104 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
-  final public void concat() throws ParseException {
-                         /*@bgen(jjtree) Concat */
+  final public void customOperator() throws ParseException {/*@bgen(jjtree) CustomOperator */
+  ASTCustomOperator jjtn000 = new ASTCustomOperator(JJTCUSTOMOPERATOR);
+  boolean jjtc000 = true;
+  jjtree.openNodeScope(jjtn000);
+    try {
+      jj_consume_token(OPERATOR);
+      jj_consume_token(16);
+      stringLiteral();
+      label_11:
+      while (true) {
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case 19:{
+          ;
+          break;
+          }
+        default:
+          jj_la1[36] = jj_gen;
+          break label_11;
+        }
+        jj_consume_token(19);
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case CONCAT:
+        case SUBSTRING:
+        case TRIM:
+        case LOWER:
+        case UPPER:
+        case SINGLE_QUOTED_STRING:
+        case DOUBLE_QUOTED_STRING:{
+          stringExpression();
+          break;
+          }
+        case 16:
+        case 25:
+        case 26:
+        case 28:
+        case LENGTH:
+        case LOCATE:
+        case ABS:
+        case SQRT:
+        case MOD:
+        case YEAR:
+        case MONTH:
+        case WEEK:
+        case DAY_OF_YEAR:
+        case DAY:
+        case DAY_OF_MONTH:
+        case DAY_OF_WEEK:
+        case HOUR:
+        case MINUTE:
+        case SECOND:
+        case FUNCTION:
+        case OPERATOR:
+        case 67:
+        case 68:
+        case 69:
+        case 70:
+        case 71:
+        case PROPERTY_PATH:
+        case INT_LITERAL:
+        case FLOAT_LITERAL:{
+          numericExpression();
+          break;
+          }
+        default:
+          jj_la1[37] = jj_gen;
+          jj_consume_token(-1);
+          throw new ParseException();
+        }
+      }
+      jj_consume_token(17);
+    } catch (Throwable jjte000) {
+if (jjtc000) {
+        jjtree.clearNodeScope(jjtn000);
+        jjtc000 = false;
+      } else {
+        jjtree.popNode();
+      }
+      if (jjte000 instanceof RuntimeException) {
+        {if (true) throw (RuntimeException)jjte000;}
+      }
+      if (jjte000 instanceof ParseException) {
+        {if (true) throw (ParseException)jjte000;}
+      }
+      {if (true) throw (Error)jjte000;}
+    } finally {
+if (jjtc000) {
+        jjtree.closeNodeScope(jjtn000, true);
+      }
+    }
+}
+
+  final public void concat() throws ParseException {/*@bgen(jjtree) Concat */
   ASTConcat jjtn000 = new ASTConcat(JJTCONCAT);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -1845,22 +2032,23 @@
       jj_consume_token(CONCAT);
       jj_consume_token(16);
       stringParameter();
-      label_11:
+      label_12:
       while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case 19:
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case 19:{
           ;
           break;
+          }
         default:
-          jj_la1[36] = jj_gen;
-          break label_11;
+          jj_la1[38] = jj_gen;
+          break label_12;
         }
         jj_consume_token(19);
         stringParameter();
       }
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -1874,14 +2062,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void substring() throws ParseException {
-                               /*@bgen(jjtree) Substring */
+  final public void substring() throws ParseException {/*@bgen(jjtree) Substring */
   ASTSubstring jjtn000 = new ASTSubstring(JJTSUBSTRING);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -1891,18 +2078,19 @@
       stringParameter();
       jj_consume_token(19);
       numericExpression();
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 19:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 19:{
         jj_consume_token(19);
         numericExpression();
         break;
+        }
       default:
-        jj_la1[37] = jj_gen;
+        jj_la1[39] = jj_gen;
         ;
       }
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -1916,14 +2104,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void trim() throws ParseException {
-                     /*@bgen(jjtree) Trim */
+  final public void trim() throws ParseException {/*@bgen(jjtree) Trim */
   ASTTrim jjtn000 = new ASTTrim(JJTTRIM);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -1933,7 +2120,7 @@
       stringParameter();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -1947,14 +2134,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void lower() throws ParseException {
-                       /*@bgen(jjtree) Lower */
+  final public void lower() throws ParseException {/*@bgen(jjtree) Lower */
   ASTLower jjtn000 = new ASTLower(JJTLOWER);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -1964,7 +2150,7 @@
       stringParameter();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -1978,14 +2164,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void upper() throws ParseException {
-                       /*@bgen(jjtree) Upper */
+  final public void upper() throws ParseException {/*@bgen(jjtree) Upper */
   ASTUpper jjtn000 = new ASTUpper(JJTUPPER);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -1995,7 +2180,7 @@
       stringParameter();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2009,29 +2194,34 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
   final public void functionsReturningNumerics() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case LENGTH:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case LENGTH:{
       length();
       break;
-    case LOCATE:
+      }
+    case LOCATE:{
       locate();
       break;
-    case ABS:
+      }
+    case ABS:{
       abs();
       break;
-    case SQRT:
+      }
+    case SQRT:{
       sqrt();
       break;
-    case MOD:
+      }
+    case MOD:{
       mod();
       break;
+      }
     case YEAR:
     case MONTH:
     case WEEK:
@@ -2041,18 +2231,18 @@
     case DAY_OF_WEEK:
     case HOUR:
     case MINUTE:
-    case SECOND:
+    case SECOND:{
       dateTimeExtractingFunction();
       break;
+      }
     default:
-      jj_la1[38] = jj_gen;
+      jj_la1[40] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
-  final public void length() throws ParseException {
-                         /*@bgen(jjtree) Length */
+  final public void length() throws ParseException {/*@bgen(jjtree) Length */
   ASTLength jjtn000 = new ASTLength(JJTLENGTH);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2062,7 +2252,7 @@
       stringParameter();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2076,14 +2266,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void locate() throws ParseException {
-                         /*@bgen(jjtree) Locate */
+  final public void locate() throws ParseException {/*@bgen(jjtree) Locate */
   ASTLocate jjtn000 = new ASTLocate(JJTLOCATE);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2093,18 +2282,19 @@
       stringParameter();
       jj_consume_token(19);
       stringParameter();
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case 19:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case 19:{
         jj_consume_token(19);
         numericExpression();
         break;
+        }
       default:
-        jj_la1[39] = jj_gen;
+        jj_la1[41] = jj_gen;
         ;
       }
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2118,14 +2308,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void abs() throws ParseException {
-                   /*@bgen(jjtree) Abs */
+  final public void abs() throws ParseException {/*@bgen(jjtree) Abs */
   ASTAbs jjtn000 = new ASTAbs(JJTABS);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2135,7 +2324,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2149,14 +2338,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void sqrt() throws ParseException {
-                     /*@bgen(jjtree) Sqrt */
+  final public void sqrt() throws ParseException {/*@bgen(jjtree) Sqrt */
   ASTSqrt jjtn000 = new ASTSqrt(JJTSQRT);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2166,7 +2354,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2180,14 +2368,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void mod() throws ParseException {
-                   /*@bgen(jjtree) Mod */
+  final public void mod() throws ParseException {/*@bgen(jjtree) Mod */
   ASTMod jjtn000 = new ASTMod(JJTMOD);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2199,7 +2386,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2213,80 +2400,86 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
   final public void aggregateExpression() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case AVG:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case AVG:{
       avg();
       break;
-    case MAX:
+      }
+    case MAX:{
       max();
       break;
-    case MIN:
+      }
+    case MIN:{
       min();
       break;
-    case SUM:
+      }
+    case SUM:{
       sum();
       break;
-    case COUNT:
+      }
+    case COUNT:{
       count();
       break;
+      }
     default:
-      jj_la1[40] = jj_gen;
+      jj_la1[42] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
-  final public void asterisk() throws ParseException {
-                             /*@bgen(jjtree) Asterisk */
+  final public void asterisk() throws ParseException {/*@bgen(jjtree) Asterisk */
   ASTAsterisk jjtn000 = new ASTAsterisk(JJTASTERISK);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
     try {
       jj_consume_token(ASTERISK);
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
-  final public void count() throws ParseException {
-                       /*@bgen(jjtree) Count */
+  final public void count() throws ParseException {/*@bgen(jjtree) Count */
   ASTCount jjtn000 = new ASTCount(JJTCOUNT);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
     try {
       jj_consume_token(COUNT);
       jj_consume_token(16);
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case ASTERISK:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case ASTERISK:{
         asterisk();
         break;
-      case 67:
+        }
       case 68:
       case 69:
       case 70:
-      case PROPERTY_PATH:
+      case 71:
+      case PROPERTY_PATH:{
         pathExpression();
         break;
-      case DISTINCT:
+        }
+      case DISTINCT:{
         distinct();
         break;
+        }
       default:
-        jj_la1[41] = jj_gen;
+        jj_la1[43] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2300,14 +2493,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void avg() throws ParseException {
-                   /*@bgen(jjtree) Avg */
+  final public void avg() throws ParseException {/*@bgen(jjtree) Avg */
   ASTAvg jjtn000 = new ASTAvg(JJTAVG);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2317,7 +2509,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2331,14 +2523,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void max() throws ParseException {
-                   /*@bgen(jjtree) Max */
+  final public void max() throws ParseException {/*@bgen(jjtree) Max */
   ASTMax jjtn000 = new ASTMax(JJTMAX);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2348,7 +2539,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2362,14 +2553,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void min() throws ParseException {
-                   /*@bgen(jjtree) Min */
+  final public void min() throws ParseException {/*@bgen(jjtree) Min */
   ASTMin jjtn000 = new ASTMin(JJTMIN);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2379,7 +2569,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2393,14 +2583,13 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void sum() throws ParseException {
-                   /*@bgen(jjtree) Sum */
+  final public void sum() throws ParseException {/*@bgen(jjtree) Sum */
   ASTSum jjtn000 = new ASTSum(JJTSUM);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2410,7 +2599,7 @@
       numericExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2424,32 +2613,34 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
   final public void dateTimeFunction() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case CURRENT_DATE:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case CURRENT_DATE:{
       currentDate();
       break;
-    case CURRENT_TIME:
+      }
+    case CURRENT_TIME:{
       currentTime();
       break;
-    case CURRENT_TIMESTAMP:
+      }
+    case CURRENT_TIMESTAMP:{
       currentTimestamp();
       break;
+      }
     default:
-      jj_la1[42] = jj_gen;
+      jj_la1[44] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
-  final public void currentDate() throws ParseException {
-                                   /*@bgen(jjtree) CurrentDate */
+  final public void currentDate() throws ParseException {/*@bgen(jjtree) CurrentDate */
   ASTCurrentDate jjtn000 = new ASTCurrentDate(JJTCURRENTDATE);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2458,14 +2649,13 @@
       jj_consume_token(16);
       jj_consume_token(17);
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
-  final public void currentTime() throws ParseException {
-                                   /*@bgen(jjtree) CurrentTime */
+  final public void currentTime() throws ParseException {/*@bgen(jjtree) CurrentTime */
   ASTCurrentTime jjtn000 = new ASTCurrentTime(JJTCURRENTTIME);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2474,14 +2664,13 @@
       jj_consume_token(16);
       jj_consume_token(17);
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
-  final public void currentTimestamp() throws ParseException {
-                                             /*@bgen(jjtree) CurrentTimestamp */
+  final public void currentTimestamp() throws ParseException {/*@bgen(jjtree) CurrentTimestamp */
   ASTCurrentTimestamp jjtn000 = new ASTCurrentTimestamp(JJTCURRENTTIMESTAMP);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2490,61 +2679,70 @@
       jj_consume_token(16);
       jj_consume_token(17);
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /* Date/time parts extracting function */
-  final public void dateTimeExtractingFunction() throws ParseException {
-                                                 /*@bgen(jjtree) #Extract( 1) */
+  final public void dateTimeExtractingFunction() throws ParseException {/*@bgen(jjtree) #Extract( 1) */
     ASTExtract jjtn000 = new ASTExtract(JJTEXTRACT);
     boolean jjtc000 = true;
     jjtree.openNodeScope(jjtn000);Token t;
     try {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case YEAR:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case YEAR:{
         t = jj_consume_token(YEAR);
         break;
-      case MONTH:
+        }
+      case MONTH:{
         t = jj_consume_token(MONTH);
         break;
-      case WEEK:
+        }
+      case WEEK:{
         t = jj_consume_token(WEEK);
         break;
-      case DAY_OF_YEAR:
+        }
+      case DAY_OF_YEAR:{
         t = jj_consume_token(DAY_OF_YEAR);
         break;
-      case DAY:
+        }
+      case DAY:{
         t = jj_consume_token(DAY);
         break;
-      case DAY_OF_MONTH:
+        }
+      case DAY_OF_MONTH:{
         t = jj_consume_token(DAY_OF_MONTH);
         break;
-      case DAY_OF_WEEK:
+        }
+      case DAY_OF_WEEK:{
         t = jj_consume_token(DAY_OF_WEEK);
         break;
-      case HOUR:
+        }
+      case HOUR:{
         t = jj_consume_token(HOUR);
         break;
-      case MINUTE:
+        }
+      case MINUTE:{
         t = jj_consume_token(MINUTE);
         break;
-      case SECOND:
+        }
+      case SECOND:{
         t = jj_consume_token(SECOND);
         break;
+        }
       default:
-        jj_la1[43] = jj_gen;
+        jj_la1[45] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
-        jjtn000.setPartToken(t.image);
+jjtn000.setPartToken(t.image);
       jj_consume_token(16);
       pathExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -2558,14 +2756,13 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000,  1);
       }
     }
-  }
+}
 
-  final public void distinct() throws ParseException {
-                             /*@bgen(jjtree) Distinct */
+  final public void distinct() throws ParseException {/*@bgen(jjtree) Distinct */
   ASTDistinct jjtn000 = new ASTDistinct(JJTDISTINCT);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -2575,7 +2772,7 @@
       pathExpression();
       jj_consume_token(17);
     } catch (Throwable jjte000) {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.clearNodeScope(jjtn000);
             jjtc000 = false;
           } else {
@@ -2589,118 +2786,121 @@
           }
           {if (true) throw (Error)jjte000;}
     } finally {
-          if (jjtc000) {
+if (jjtc000) {
             jjtree.closeNodeScope(jjtn000, true);
           }
     }
-  }
+}
 
-  final public void namedParameter() throws ParseException {
-        Token t;
-    jj_consume_token(66);
+  final public void namedParameter() throws ParseException {Token t;
+    jj_consume_token(67);
     t = jj_consume_token(PROPERTY_PATH);
-                                  ASTNamedParameter jjtn001 = new ASTNamedParameter(JJTNAMEDPARAMETER);
+ASTNamedParameter jjtn001 = new ASTNamedParameter(JJTNAMEDPARAMETER);
                                   boolean jjtc001 = true;
                                   jjtree.openNodeScope(jjtn001);
     try {
-                                  jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                                   jjtc001 = false;
-                                  jjtn001.setValue(t.image);
+jjtn001.setValue(t.image);
     } finally {
-                                  if (jjtc001) {
+if (jjtc001) {
                                     jjtree.closeNodeScope(jjtn001,  0);
                                   }
     }
-  }
+}
 
-  final public void pathExpression() throws ParseException {
-   Token t;
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case PROPERTY_PATH:
+  final public void pathExpression() throws ParseException {Token t;
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case PROPERTY_PATH:{
       t = jj_consume_token(PROPERTY_PATH);
-                                   ASTObjPath jjtn001 = new ASTObjPath(JJTOBJPATH);
+ASTObjPath jjtn001 = new ASTObjPath(JJTOBJPATH);
                                    boolean jjtc001 = true;
                                    jjtree.openNodeScope(jjtn001);
       try {
-                                   jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                                    jjtc001 = false;
-                                   ExpressionUtils.parsePath(jjtn001, t.image);
+ExpressionUtils.parsePath(jjtn001, t.image);
       } finally {
-                                   if (jjtc001) {
+if (jjtc001) {
                                      jjtree.closeNodeScope(jjtn001,  0);
                                    }
       }
       break;
-    case 67:
-      jj_consume_token(67);
+      }
+    case 68:{
+      jj_consume_token(68);
       t = jj_consume_token(PROPERTY_PATH);
-                                   ASTObjPath jjtn002 = new ASTObjPath(JJTOBJPATH);
+ASTObjPath jjtn002 = new ASTObjPath(JJTOBJPATH);
                                    boolean jjtc002 = true;
                                    jjtree.openNodeScope(jjtn002);
       try {
-                                   jjtree.closeNodeScope(jjtn002,  0);
+jjtree.closeNodeScope(jjtn002,  0);
                                    jjtc002 = false;
-                                   ExpressionUtils.parsePath(jjtn002, t.image);
+ExpressionUtils.parsePath(jjtn002, t.image);
       } finally {
-                                   if (jjtc002) {
+if (jjtc002) {
                                      jjtree.closeNodeScope(jjtn002,  0);
                                    }
       }
       break;
-    case 68:
-      jj_consume_token(68);
+      }
+    case 69:{
+      jj_consume_token(69);
       t = jj_consume_token(PROPERTY_PATH);
-                                   ASTDbPath jjtn003 = new ASTDbPath(JJTDBPATH);
+ASTDbPath jjtn003 = new ASTDbPath(JJTDBPATH);
                                    boolean jjtc003 = true;
                                    jjtree.openNodeScope(jjtn003);
       try {
-                                   jjtree.closeNodeScope(jjtn003,  0);
+jjtree.closeNodeScope(jjtn003,  0);
                                    jjtc003 = false;
-                                   ExpressionUtils.parsePath(jjtn003, t.image);
+ExpressionUtils.parsePath(jjtn003, t.image);
       } finally {
-                                   if (jjtc003) {
+if (jjtc003) {
                                      jjtree.closeNodeScope(jjtn003,  0);
                                    }
       }
       break;
-    case 69:
-      jj_consume_token(69);
+      }
+    case 70:{
+      jj_consume_token(70);
       t = jj_consume_token(PROPERTY_PATH);
-                                   ASTEnum jjtn004 = new ASTEnum(JJTENUM);
+ASTEnum jjtn004 = new ASTEnum(JJTENUM);
                                    boolean jjtc004 = true;
                                    jjtree.openNodeScope(jjtn004);
       try {
-                                   jjtree.closeNodeScope(jjtn004,  0);
+jjtree.closeNodeScope(jjtn004,  0);
                                    jjtc004 = false;
-                                   jjtn004.setEnumValue(t.image);
+jjtn004.setEnumValue(t.image);
       } finally {
-                                   if (jjtc004) {
+if (jjtc004) {
                                      jjtree.closeNodeScope(jjtn004,  0);
                                    }
       }
       break;
-    case 70:
-      jj_consume_token(70);
+      }
+    case 71:{
+      jj_consume_token(71);
       t = jj_consume_token(PROPERTY_PATH);
-                                   ASTDbIdPath jjtn005 = new ASTDbIdPath(JJTDBIDPATH);
+ASTDbIdPath jjtn005 = new ASTDbIdPath(JJTDBIDPATH);
                                    boolean jjtc005 = true;
                                    jjtree.openNodeScope(jjtn005);
       try {
-                                   jjtree.closeNodeScope(jjtn005,  0);
+jjtree.closeNodeScope(jjtn005,  0);
                                    jjtc005 = false;
-                                   ExpressionUtils.parsePath(jjtn005, t.image);
+ExpressionUtils.parsePath(jjtn005, t.image);
       } finally {
-                                   if (jjtc005) {
+if (jjtc005) {
                                      jjtree.closeNodeScope(jjtn005,  0);
                                    }
       }
       break;
+      }
     default:
-      jj_la1[44] = jj_gen;
+      jj_la1[46] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
   /** Generated Token Manager. */
   public ExpressionParserTokenManager token_source;
@@ -2711,133 +2911,141 @@
   public Token jj_nt;
   private int jj_ntk;
   private int jj_gen;
-  final private int[] jj_la1 = new int[45];
+  final private int[] jj_la1 = new int[47];
   static private int[] jj_la1_0;
   static private int[] jj_la1_1;
   static private int[] jj_la1_2;
   static {
-      jj_la1_init_0();
-      jj_la1_init_1();
-      jj_la1_init_2();
-   }
-   private static void jj_la1_init_0() {
-      jj_la1_0 = new int[] {0x2,0x4,0x18,0x16010018,0x60,0x180,0x10000,0x4fff8,0x4fff8,0x16010000,0x18,0x10000,0x4e000,0x80000,0x16010000,0x0,0x0,0x0,0x16010000,0x0,0x100000,0x200000,0x400000,0x1800000,0x1800000,0x6000000,0x6000000,0x8000000,0x8000000,0x16010000,0x2000000,0x6010000,0x10000,0x0,0x80000,0x16010000,0x80000,0x80000,0x0,0x80000,0x0,0x0,0x0,0x0,0x0,};
-   }
-   private static void jj_la1_init_1() {
-      jj_la1_1 = new int[] {0x0,0x0,0x0,0xfffffdfe,0x0,0x0,0x0,0x0,0x0,0xfffffdfe,0x0,0x0,0x0,0x0,0xfffffdf2,0xf800,0x0,0xf800,0xfffffdfe,0xc,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff1f0400,0x0,0xff1f0400,0xff1f0400,0xf800,0x0,0xff1ffc00,0x0,0x0,0xff1f0000,0x0,0x1f0,0x200,0xe00000,0xff000000,0x0,};
-   }
-   private static void jj_la1_init_2() {
-      jj_la1_2 = new int[] {0x0,0x0,0x0,0x39017f,0x0,0x0,0x4,0x0,0x0,0x39017f,0x0,0x4,0x0,0x0,0x39017f,0x90178,0x90000,0x90000,0x39017f,0x390004,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x80,0x80,0x30017f,0x0,0x30017f,0x30017f,0x0,0x0,0x39017f,0x0,0x0,0x3,0x0,0x0,0x1f8,0x0,0x3,0x178,};
-   }
+	   jj_la1_init_0();
+	   jj_la1_init_1();
+	   jj_la1_init_2();
+	}
+	private static void jj_la1_init_0() {
+	   jj_la1_0 = new int[] {0x2,0x4,0x18,0x16010018,0x60,0x180,0x10000,0x4fff8,0x4fff8,0x16010000,0x18,0x10000,0x4e000,0x80000,0x16010000,0x0,0x0,0x0,0x16010000,0x0,0x100000,0x200000,0x400000,0x1800000,0x1800000,0x6000000,0x6000000,0x8000000,0x8000000,0x16010000,0x2000000,0x6010000,0x10000,0x0,0x80000,0x16010000,0x80000,0x16010000,0x80000,0x80000,0x0,0x80000,0x0,0x0,0x0,0x0,0x0,};
+	}
+	private static void jj_la1_init_1() {
+	   jj_la1_1 = new int[] {0x0,0x0,0x0,0xfffffdfe,0x0,0x0,0x0,0x0,0x0,0xfffffdfe,0x0,0x0,0x0,0x0,0xfffffdf2,0x7c00,0x0,0x7c00,0xfffffdfe,0xc,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0xff8f8000,0x0,0xff8f8000,0xff8f8000,0x7c00,0x0,0xff8ffc00,0x0,0xff8ffc00,0x0,0x0,0xff8f8000,0x0,0x1f0,0x200,0x700000,0xff800000,0x0,};
+	}
+	private static void jj_la1_init_2() {
+	   jj_la1_2 = new int[] {0x0,0x0,0x0,0xe402ff,0x0,0x0,0x8,0x0,0x0,0xe402ff,0x0,0x8,0x0,0x0,0xe402ff,0x2402f0,0x240000,0x240000,0xe402ff,0xe40008,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x100,0x100,0xc002ff,0x0,0xc002ff,0xc002ff,0x0,0x0,0xe402ff,0x0,0xe402ff,0x0,0x0,0x1,0x0,0x0,0x3f0,0x0,0x1,0x2f0,};
+	}
 
   /** Constructor with InputStream. */
   public ExpressionParser(java.io.InputStream stream) {
-     this(stream, null);
+	  this(stream, null);
   }
   /** Constructor with InputStream and supplied encoding */
   public ExpressionParser(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream = new JavaCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source = new ExpressionParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 45; i++) jj_la1[i] = -1;
+	 try { jj_input_stream = new JavaCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+	 token_source = new ExpressionParserTokenManager(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jj_gen = 0;
+	 for (int i = 0; i < 47; i++) jj_la1[i] = -1;
   }
 
   /** Reinitialise. */
   public void ReInit(java.io.InputStream stream) {
-     ReInit(stream, null);
+	  ReInit(stream, null);
   }
   /** Reinitialise. */
   public void ReInit(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 45; i++) jj_la1[i] = -1;
+	 try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+	 token_source.ReInit(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jjtree.reset();
+	 jj_gen = 0;
+	 for (int i = 0; i < 47; i++) jj_la1[i] = -1;
   }
 
   /** Constructor. */
   public ExpressionParser(java.io.Reader stream) {
-    jj_input_stream = new JavaCharStream(stream, 1, 1);
-    token_source = new ExpressionParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 45; i++) jj_la1[i] = -1;
+	 jj_input_stream = new JavaCharStream(stream, 1, 1);
+	 token_source = new ExpressionParserTokenManager(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jj_gen = 0;
+	 for (int i = 0; i < 47; i++) jj_la1[i] = -1;
   }
 
   /** Reinitialise. */
   public void ReInit(java.io.Reader stream) {
-    jj_input_stream.ReInit(stream, 1, 1);
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 45; i++) jj_la1[i] = -1;
+	if (jj_input_stream == null) {
+	   jj_input_stream = new JavaCharStream(stream, 1, 1);
+	} else {
+	   jj_input_stream.ReInit(stream, 1, 1);
+	}
+	if (token_source == null) {
+ token_source = new ExpressionParserTokenManager(jj_input_stream);
+	}
+
+	 token_source.ReInit(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jjtree.reset();
+	 jj_gen = 0;
+	 for (int i = 0; i < 47; i++) jj_la1[i] = -1;
   }
 
   /** Constructor with generated Token Manager. */
   public ExpressionParser(ExpressionParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 45; i++) jj_la1[i] = -1;
+	 token_source = tm;
+	 token = new Token();
+	 jj_ntk = -1;
+	 jj_gen = 0;
+	 for (int i = 0; i < 47; i++) jj_la1[i] = -1;
   }
 
   /** Reinitialise. */
   public void ReInit(ExpressionParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 45; i++) jj_la1[i] = -1;
+	 token_source = tm;
+	 token = new Token();
+	 jj_ntk = -1;
+	 jjtree.reset();
+	 jj_gen = 0;
+	 for (int i = 0; i < 47; i++) jj_la1[i] = -1;
   }
 
   private Token jj_consume_token(int kind) throws ParseException {
-    Token oldToken;
-    if ((oldToken = token).next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    if (token.kind == kind) {
-      jj_gen++;
-      return token;
-    }
-    token = oldToken;
-    jj_kind = kind;
-    throw generateParseException();
+	 Token oldToken;
+	 if ((oldToken = token).next != null) token = token.next;
+	 else token = token.next = token_source.getNextToken();
+	 jj_ntk = -1;
+	 if (token.kind == kind) {
+	   jj_gen++;
+	   return token;
+	 }
+	 token = oldToken;
+	 jj_kind = kind;
+	 throw generateParseException();
   }
 
 
 /** Get the next Token. */
   final public Token getNextToken() {
-    if (token.next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    jj_gen++;
-    return token;
+	 if (token.next != null) token = token.next;
+	 else token = token.next = token_source.getNextToken();
+	 jj_ntk = -1;
+	 jj_gen++;
+	 return token;
   }
 
 /** Get the specific Token. */
   final public Token getToken(int index) {
-    Token t = token;
-    for (int i = 0; i < index; i++) {
-      if (t.next != null) t = t.next;
-      else t = t.next = token_source.getNextToken();
-    }
-    return t;
+	 Token t = token;
+	 for (int i = 0; i < index; i++) {
+	   if (t.next != null) t = t.next;
+	   else t = t.next = token_source.getNextToken();
+	 }
+	 return t;
   }
 
-  private int jj_ntk() {
-    if ((jj_nt=token.next) == null)
-      return (jj_ntk = (token.next=token_source.getNextToken()).kind);
-    else
-      return (jj_ntk = jj_nt.kind);
+  private int jj_ntk_f() {
+	 if ((jj_nt=token.next) == null)
+	   return (jj_ntk = (token.next=token_source.getNextToken()).kind);
+	 else
+	   return (jj_ntk = jj_nt.kind);
   }
 
   private java.util.List<int[]> jj_expentries = new java.util.ArrayList<int[]>();
@@ -2846,39 +3054,46 @@
 
   /** Generate ParseException. */
   public ParseException generateParseException() {
-    jj_expentries.clear();
-    boolean[] la1tokens = new boolean[90];
-    if (jj_kind >= 0) {
-      la1tokens[jj_kind] = true;
-      jj_kind = -1;
-    }
-    for (int i = 0; i < 45; i++) {
-      if (jj_la1[i] == jj_gen) {
-        for (int j = 0; j < 32; j++) {
-          if ((jj_la1_0[i] & (1<<j)) != 0) {
-            la1tokens[j] = true;
-          }
-          if ((jj_la1_1[i] & (1<<j)) != 0) {
-            la1tokens[32+j] = true;
-          }
-          if ((jj_la1_2[i] & (1<<j)) != 0) {
-            la1tokens[64+j] = true;
-          }
-        }
-      }
-    }
-    for (int i = 0; i < 90; i++) {
-      if (la1tokens[i]) {
-        jj_expentry = new int[1];
-        jj_expentry[0] = i;
-        jj_expentries.add(jj_expentry);
-      }
-    }
-    int[][] exptokseq = new int[jj_expentries.size()][];
-    for (int i = 0; i < jj_expentries.size(); i++) {
-      exptokseq[i] = jj_expentries.get(i);
-    }
-    return new ParseException(token, exptokseq, tokenImage);
+	 jj_expentries.clear();
+	 boolean[] la1tokens = new boolean[92];
+	 if (jj_kind >= 0) {
+	   la1tokens[jj_kind] = true;
+	   jj_kind = -1;
+	 }
+	 for (int i = 0; i < 47; i++) {
+	   if (jj_la1[i] == jj_gen) {
+		 for (int j = 0; j < 32; j++) {
+		   if ((jj_la1_0[i] & (1<<j)) != 0) {
+			 la1tokens[j] = true;
+		   }
+		   if ((jj_la1_1[i] & (1<<j)) != 0) {
+			 la1tokens[32+j] = true;
+		   }
+		   if ((jj_la1_2[i] & (1<<j)) != 0) {
+			 la1tokens[64+j] = true;
+		   }
+		 }
+	   }
+	 }
+	 for (int i = 0; i < 92; i++) {
+	   if (la1tokens[i]) {
+		 jj_expentry = new int[1];
+		 jj_expentry[0] = i;
+		 jj_expentries.add(jj_expentry);
+	   }
+	 }
+	 int[][] exptokseq = new int[jj_expentries.size()][];
+	 for (int i = 0; i < jj_expentries.size(); i++) {
+	   exptokseq[i] = jj_expentries.get(i);
+	 }
+	 return new ParseException(token, exptokseq, tokenImage);
+  }
+
+  private boolean trace_enabled;
+
+/** Trace enabled. */
+  final public boolean trace_enabled() {
+	 return trace_enabled;
   }
 
   /** Enable tracing. */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserConstants.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserConstants.java
index ee0b22b..eb99db0 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserConstants.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserConstants.java
@@ -49,83 +49,87 @@
   /** RegularExpression Id. */
   int DISTINCT = 41;
   /** RegularExpression Id. */
-  int FUNCTION = 42;
+  int CONCAT = 42;
   /** RegularExpression Id. */
-  int CONCAT = 43;
+  int SUBSTRING = 43;
   /** RegularExpression Id. */
-  int SUBSTRING = 44;
+  int TRIM = 44;
   /** RegularExpression Id. */
-  int TRIM = 45;
+  int LOWER = 45;
   /** RegularExpression Id. */
-  int LOWER = 46;
+  int UPPER = 46;
   /** RegularExpression Id. */
-  int UPPER = 47;
+  int LENGTH = 47;
   /** RegularExpression Id. */
-  int LENGTH = 48;
+  int LOCATE = 48;
   /** RegularExpression Id. */
-  int LOCATE = 49;
+  int ABS = 49;
   /** RegularExpression Id. */
-  int ABS = 50;
+  int SQRT = 50;
   /** RegularExpression Id. */
-  int SQRT = 51;
+  int MOD = 51;
   /** RegularExpression Id. */
-  int MOD = 52;
+  int CURRENT_DATE = 52;
   /** RegularExpression Id. */
-  int CURRENT_DATE = 53;
+  int CURRENT_TIME = 53;
   /** RegularExpression Id. */
-  int CURRENT_TIME = 54;
+  int CURRENT_TIMESTAMP = 54;
   /** RegularExpression Id. */
-  int CURRENT_TIMESTAMP = 55;
+  int YEAR = 55;
   /** RegularExpression Id. */
-  int YEAR = 56;
+  int MONTH = 56;
   /** RegularExpression Id. */
-  int MONTH = 57;
+  int WEEK = 57;
   /** RegularExpression Id. */
-  int WEEK = 58;
+  int DAY_OF_YEAR = 58;
   /** RegularExpression Id. */
-  int DAY_OF_YEAR = 59;
+  int DAY = 59;
   /** RegularExpression Id. */
-  int DAY = 60;
+  int DAY_OF_MONTH = 60;
   /** RegularExpression Id. */
-  int DAY_OF_MONTH = 61;
+  int DAY_OF_WEEK = 61;
   /** RegularExpression Id. */
-  int DAY_OF_WEEK = 62;
+  int HOUR = 62;
   /** RegularExpression Id. */
-  int HOUR = 63;
+  int MINUTE = 63;
   /** RegularExpression Id. */
-  int MINUTE = 64;
+  int SECOND = 64;
   /** RegularExpression Id. */
-  int SECOND = 65;
+  int FUNCTION = 65;
   /** RegularExpression Id. */
-  int ASTERISK = 71;
+  int OPERATOR = 66;
   /** RegularExpression Id. */
-  int PROPERTY_PATH = 72;
+  int ASTERISK = 72;
   /** RegularExpression Id. */
-  int IDENTIFIER = 73;
+  int PROPERTY_PATH = 73;
   /** RegularExpression Id. */
-  int LETTER = 74;
+  int IDENTIFIER = 74;
   /** RegularExpression Id. */
-  int DIGIT = 75;
+  int LETTER = 75;
   /** RegularExpression Id. */
-  int ESC = 78;
+  int DIGIT = 76;
   /** RegularExpression Id. */
-  int SINGLE_QUOTED_STRING = 80;
+  int DOLLAR_SIGN = 77;
   /** RegularExpression Id. */
-  int STRING_ESC = 81;
+  int ESC = 80;
   /** RegularExpression Id. */
-  int DOUBLE_QUOTED_STRING = 83;
+  int SINGLE_QUOTED_STRING = 82;
   /** RegularExpression Id. */
-  int INT_LITERAL = 84;
+  int STRING_ESC = 83;
   /** RegularExpression Id. */
-  int FLOAT_LITERAL = 85;
+  int DOUBLE_QUOTED_STRING = 85;
   /** RegularExpression Id. */
-  int DEC_FLT = 86;
+  int INT_LITERAL = 86;
   /** RegularExpression Id. */
-  int DEC_DIGITS = 87;
+  int FLOAT_LITERAL = 87;
   /** RegularExpression Id. */
-  int EXPONENT = 88;
+  int DEC_FLT = 88;
   /** RegularExpression Id. */
-  int FLT_SUFF = 89;
+  int DEC_DIGITS = 89;
+  /** RegularExpression Id. */
+  int EXPONENT = 90;
+  /** RegularExpression Id. */
+  int FLT_SUFF = 91;
 
   /** Lexical state. */
   int DEFAULT = 0;
@@ -178,7 +182,6 @@
     "\"sum\"",
     "\"count\"",
     "\"distinct\"",
-    "\"fn\"",
     "\"concat\"",
     "\"substring\"",
     "\"trim\"",
@@ -202,6 +205,8 @@
     "\"hour\"",
     "\"minute\"",
     "\"second\"",
+    "\"fn\"",
+    "\"op\"",
     "\"$\"",
     "\"obj:\"",
     "\"db:\"",
@@ -212,13 +217,14 @@
     "<IDENTIFIER>",
     "<LETTER>",
     "<DIGIT>",
+    "<DOLLAR_SIGN>",
     "\"\\\'\"",
     "\"\\\"\"",
     "<ESC>",
-    "<token of kind 79>",
+    "<token of kind 81>",
     "\"\\\'\"",
     "<STRING_ESC>",
-    "<token of kind 82>",
+    "<token of kind 84>",
     "\"\\\"\"",
     "<INT_LITERAL>",
     "<FLOAT_LITERAL>",
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTokenManager.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTokenManager.java
index 268e281..6976880 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTokenManager.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTokenManager.java
@@ -25,8 +25,8 @@
 import org.apache.cayenne.exp.Expression;
 
 /** Token Manager. */
-public class ExpressionParserTokenManager implements ExpressionParserConstants
-{
+@SuppressWarnings ("unused")
+public class ExpressionParserTokenManager implements ExpressionParserConstants {
       /** Holds the last value computed by a constant token. */
     Object literalValue;
 
@@ -38,18 +38,18 @@
     {
         int ofs = image.length() - 1;
         switch ( image.charAt(ofs) ) {
-            case 'n':   return '\u005cn';
-            case 'r':   return '\u005cr';
-            case 't':   return '\u005ct';
-            case 'b':   return '\u005cb';
-            case 'f':   return '\u005cf';
-            case '\u005c\u005c':  return '\u005c\u005c';
-            case '\u005c'':  return '\u005c'';
-            case '\u005c"':  return '\u005c"';
+            case 'n':   return '\n';
+            case 'r':   return '\r';
+            case 't':   return '\t';
+            case 'b':   return '\b';
+            case 'f':   return '\f';
+            case '\\':  return '\\';
+            case '\'':  return '\'';
+            case '\"':  return '\"';
         }
 
           // Otherwise, it's an octal number.  Find the backslash and convert.
-        while ( image.charAt(--ofs) != '\u005c\u005c' )
+        while ( image.charAt(--ofs) != '\\' )
           {}
         int value = 0;
         while ( ++ofs < image.length() )
@@ -103,137 +103,136 @@
   public  java.io.PrintStream debugStream = System.out;
   /** Set debug output. */
   public  void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
-private final int jjStopStringLiteralDfa_0(int pos, long active0, long active1)
-{
+private final int jjStopStringLiteralDfa_0(int pos, long active0, long active1){
    switch (pos)
    {
       case 0:
-         if ((active0 & 0x60090000000000L) != 0L)
+         if ((active0 & 0x30050000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             return 36;
          }
          if ((active0 & 0x8L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             return 63;
          }
-         if ((active0 & 0x200000000000L) != 0L)
+         if ((active0 & 0x100000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             return 6;
          }
-         if ((active0 & 0x40000000000L) != 0L)
+         if ((active1 & 0x2L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             return 15;
          }
-         if ((active0 & 0xff1fd2f00004e006L) != 0L || (active1 & 0x7bL) != 0L)
+         if ((active0 & 0xff8feaf00004e006L) != 0L || (active1 & 0xf5L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             return 83;
          }
          return -1;
       case 1:
-         if ((active0 & 0x40000008002L) != 0L)
+         if ((active0 & 0x8002L) != 0L || (active1 & 0x6L) != 0L)
             return 83;
-         if ((active0 & 0x200000000000L) != 0L)
+         if ((active0 & 0x100000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 1;
             return 5;
          }
-         if ((active0 & 0xff1fdbf000046004L) != 0L || (active1 & 0x7bL) != 0L)
+         if ((active0 & 0xff8feff000046004L) != 0L || (active1 & 0xf1L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 1;
             return 83;
          }
-         if ((active0 & 0x60000000000000L) != 0L)
+         if ((active0 & 0x30000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 1;
             return 35;
          }
          if ((active0 & 0x8L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 1;
             return 64;
          }
          return -1;
       case 2:
-         if ((active0 & 0x781400f00000000cL) != 0L || (active1 & 0x1L) != 0L)
-            return 83;
-         if ((active0 & 0x60000000000000L) != 0L)
+         if ((active0 & 0x30000000000000L) != 0L)
          {
             if (jjmatchedPos != 2)
             {
-               jjmatchedKind = 72;
+               jjmatchedKind = 73;
                jjmatchedPos = 2;
             }
             return 34;
          }
-         if ((active0 & 0x870bfb0000046000L) != 0L || (active1 & 0x6aL) != 0L)
+         if ((active0 & 0xbc0a00f00000000cL) != 0L)
+            return 83;
+         if ((active0 & 0x4385ff0000046000L) != 0L || (active1 & 0xd1L) != 0L)
          {
             if (jjmatchedPos != 2)
             {
-               jjmatchedKind = 72;
+               jjmatchedKind = 73;
                jjmatchedPos = 2;
             }
             return 83;
          }
          return -1;
       case 3:
-         if ((active0 & 0x8508200000006000L) != 0L)
+         if ((active0 & 0x4284100000006000L) != 0L)
             return 83;
-         if ((active0 & 0x6a03db0000040000L) != 0L || (active1 & 0x63L) != 0L)
+         if ((active0 & 0xb501ef0000040000L) != 0L || (active1 & 0xc1L) != 0L)
          {
             if (jjmatchedPos != 3)
             {
-               jjmatchedKind = 72;
+               jjmatchedKind = 73;
                jjmatchedPos = 3;
             }
             return 83;
          }
-         if ((active0 & 0x60000000000000L) != 0L)
+         if ((active0 & 0x30000000000000L) != 0L)
          {
             if (jjmatchedPos != 3)
             {
-               jjmatchedKind = 72;
+               jjmatchedKind = 73;
                jjmatchedPos = 3;
             }
             return 33;
          }
          return -1;
       case 4:
-         if ((active0 & 0x200c10000000000L) != 0L)
+         if ((active0 & 0x100610000000000L) != 0L)
             return 83;
-         if ((active0 & 0x60000000000000L) != 0L)
+         if ((active0 & 0x30000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 4;
             return 32;
          }
-         if ((active0 & 0x68031a0000044000L) != 0L || (active1 & 0x3L) != 0L)
+         if ((active0 & 0xb4018e0000044000L) != 0L || (active1 & 0x1L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 4;
             return 83;
          }
          return -1;
       case 5:
-         if ((active0 & 0x6800120000044000L) != 0L)
+         if ((active0 & 0x34000a0000044000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 5;
             return 83;
          }
-         if ((active0 & 0x3080000000000L) != 0L || (active1 & 0x3L) != 0L)
+         if ((active0 & 0x8001840000000000L) != 0L || (active1 & 0x1L) != 0L)
             return 83;
-         if ((active0 & 0x60000000000000L) != 0L)
+         if ((active0 & 0x30000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 5;
             return 31;
          }
@@ -241,75 +240,75 @@
       case 6:
          if ((active0 & 0x40000L) != 0L)
             return 83;
-         if ((active0 & 0x6800120000004000L) != 0L)
+         if ((active0 & 0x30000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
-            jjmatchedPos = 6;
-            return 83;
-         }
-         if ((active0 & 0x60000000000000L) != 0L)
-         {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 6;
             return 30;
          }
+         if ((active0 & 0x34000a0000004000L) != 0L)
+         {
+            jjmatchedKind = 73;
+            jjmatchedPos = 6;
+            return 83;
+         }
          return -1;
       case 7:
          if ((active0 & 0x20000000000L) != 0L)
             return 83;
-         if ((active0 & 0x40000000000000L) != 0L)
+         if ((active0 & 0x20000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 7;
             return 29;
          }
-         if ((active0 & 0x6820100000004000L) != 0L)
+         if ((active0 & 0x3410080000004000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 7;
             return 83;
          }
          return -1;
       case 8:
-         if ((active0 & 0x4800100000000000L) != 0L)
+         if ((active0 & 0x2400080000000000L) != 0L)
             return 83;
-         if ((active0 & 0x2020000000004000L) != 0L)
+         if ((active0 & 0x1010000000004000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 8;
             return 83;
          }
-         if ((active0 & 0x40000000000000L) != 0L)
+         if ((active0 & 0x20000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 8;
             return 28;
          }
          return -1;
       case 9:
-         if ((active0 & 0x2000000000000000L) != 0L)
+         if ((active0 & 0x1000000000000000L) != 0L)
             return 83;
-         if ((active0 & 0x40000000000000L) != 0L)
+         if ((active0 & 0x20000000000000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 9;
             return 27;
          }
-         if ((active0 & 0x20000000004000L) != 0L)
+         if ((active0 & 0x10000000004000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 9;
             return 83;
          }
          return -1;
       case 10:
-         if ((active0 & 0x20000000000000L) != 0L)
+         if ((active0 & 0x10000000000000L) != 0L)
             return 83;
-         if ((active0 & 0x40000000000000L) != 0L)
+         if ((active0 & 0x20000000000000L) != 0L)
             return 26;
          if ((active0 & 0x4000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 10;
             return 83;
          }
@@ -317,7 +316,7 @@
       case 11:
          if ((active0 & 0x4000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 11;
             return 83;
          }
@@ -325,7 +324,7 @@
       case 12:
          if ((active0 & 0x4000L) != 0L)
          {
-            jjmatchedKind = 72;
+            jjmatchedKind = 73;
             jjmatchedPos = 12;
             return 83;
          }
@@ -334,8 +333,7 @@
          return -1;
    }
 }
-private final int jjStartNfa_0(int pos, long active0, long active1)
-{
+private final int jjStartNfa_0(int pos, long active0, long active1){
    return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0, active1), pos + 1);
 }
 private int jjStopAtPos(int pos, int kind)
@@ -344,27 +342,26 @@
    jjmatchedPos = pos;
    return pos + 1;
 }
-private int jjMoveStringLiteralDfa0_0()
-{
+private int jjMoveStringLiteralDfa0_0(){
    switch(curChar)
    {
       case 33:
          jjmatchedKind = 4;
          return jjMoveStringLiteralDfa1_0(0x80L, 0x0L);
       case 34:
-         return jjStopAtPos(0, 77);
+         return jjStopAtPos(0, 79);
       case 36:
-         return jjStopAtPos(0, 66);
+         return jjStopAtPos(0, 67);
       case 38:
          return jjStopAtPos(0, 22);
       case 39:
-         return jjStopAtPos(0, 76);
+         return jjStopAtPos(0, 78);
       case 40:
          return jjStopAtPos(0, 16);
       case 41:
          return jjStopAtPos(0, 17);
       case 42:
-         return jjStopAtPos(0, 71);
+         return jjStopAtPos(0, 72);
       case 43:
          return jjStopAtPos(0, 25);
       case 44:
@@ -385,39 +382,39 @@
       case 94:
          return jjStopAtPos(0, 21);
       case 97:
-         return jjMoveStringLiteralDfa1_0(0x4001000000004L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x2001000000004L, 0x0L);
       case 98:
          return jjMoveStringLiteralDfa1_0(0x40000L, 0x0L);
       case 99:
-         return jjMoveStringLiteralDfa1_0(0x60090000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x30050000000000L, 0x0L);
       case 100:
-         return jjMoveStringLiteralDfa1_0(0x7800020000000000L, 0x50L);
+         return jjMoveStringLiteralDfa1_0(0x3c00020000000000L, 0xa0L);
       case 101:
-         return jjMoveStringLiteralDfa1_0(0x0L, 0x20L);
+         return jjMoveStringLiteralDfa1_0(0x0L, 0x40L);
       case 102:
-         return jjMoveStringLiteralDfa1_0(0x40000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x0L, 0x2L);
       case 104:
-         return jjMoveStringLiteralDfa1_0(0x8000000000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x4000000000000000L, 0x0L);
       case 105:
          return jjMoveStringLiteralDfa1_0(0x8000L, 0x0L);
       case 108:
-         return jjMoveStringLiteralDfa1_0(0x3400000006000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x1a00000006000L, 0x0L);
       case 109:
-         return jjMoveStringLiteralDfa1_0(0x210006000000000L, 0x1L);
+         return jjMoveStringLiteralDfa1_0(0x8108006000000000L, 0x0L);
       case 110:
          return jjMoveStringLiteralDfa1_0(0x8L, 0x0L);
       case 111:
-         return jjMoveStringLiteralDfa1_0(0x2L, 0x8L);
+         return jjMoveStringLiteralDfa1_0(0x2L, 0x14L);
       case 115:
-         return jjMoveStringLiteralDfa1_0(0x8108000000000L, 0x2L);
+         return jjMoveStringLiteralDfa1_0(0x4088000000000L, 0x1L);
       case 116:
-         return jjMoveStringLiteralDfa1_0(0x200000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x100000000000L, 0x0L);
       case 117:
-         return jjMoveStringLiteralDfa1_0(0x800000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x400000000000L, 0x0L);
       case 119:
-         return jjMoveStringLiteralDfa1_0(0x400000000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x200000000000000L, 0x0L);
       case 121:
-         return jjMoveStringLiteralDfa1_0(0x100000000000000L, 0x0L);
+         return jjMoveStringLiteralDfa1_0(0x80000000000000L, 0x0L);
       case 124:
          return jjStopAtPos(0, 20);
       case 126:
@@ -426,8 +423,7 @@
          return jjMoveNfa_0(3, 0);
    }
 }
-private int jjMoveStringLiteralDfa1_0(long active0, long active1)
-{
+private int jjMoveStringLiteralDfa1_0(long active0, long active1){
    try { curChar = input_stream.readChar(); }
    catch(java.io.IOException e) {
       jjStopStringLiteralDfa_0(0, active0, active1);
@@ -456,31 +452,33 @@
             return jjStopAtPos(1, 24);
          break;
       case 97:
-         return jjMoveStringLiteralDfa2_0(active0, 0x7800004000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x3c00004000000000L, active1, 0L);
       case 98:
-         return jjMoveStringLiteralDfa2_0(active0, 0x4000000000000L, active1, 0x58L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x2000000000000L, active1, 0xb0L);
       case 101:
-         return jjMoveStringLiteralDfa2_0(active0, 0x501000000040000L, active1, 0x2L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x280800000040000L, active1, 0x1L);
       case 105:
-         return jjMoveStringLiteralDfa2_0(active0, 0x22000006000L, active1, 0x1L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x8000022000006000L, active1, 0L);
       case 110:
          if ((active0 & 0x8000L) != 0L)
             return jjStartNfaWithStates_0(1, 15, 83);
-         else if ((active0 & 0x40000000000L) != 0L)
-            return jjStartNfaWithStates_0(1, 42, 83);
-         return jjMoveStringLiteralDfa2_0(active0, 0x4L, active1, 0x20L);
+         else if ((active1 & 0x2L) != 0L)
+            return jjStartNfaWithStates_0(1, 65, 83);
+         return jjMoveStringLiteralDfa2_0(active0, 0x4L, active1, 0x40L);
       case 111:
-         return jjMoveStringLiteralDfa2_0(active0, 0x8212490000000008L, active1, 0L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x4109250000000008L, active1, 0L);
       case 112:
-         return jjMoveStringLiteralDfa2_0(active0, 0x800000000000L, active1, 0L);
+         if ((active1 & 0x4L) != 0L)
+            return jjStartNfaWithStates_0(1, 66, 83);
+         return jjMoveStringLiteralDfa2_0(active0, 0x400000000000L, active1, 0L);
       case 113:
-         return jjMoveStringLiteralDfa2_0(active0, 0x8000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x4000000000000L, active1, 0L);
       case 114:
          if ((active0 & 0x2L) != 0L)
             return jjStartNfaWithStates_0(1, 1, 83);
-         return jjMoveStringLiteralDfa2_0(active0, 0x200000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x100000000000L, active1, 0L);
       case 117:
-         return jjMoveStringLiteralDfa2_0(active0, 0x60108000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa2_0(active0, 0x30088000000000L, active1, 0L);
       case 118:
          return jjMoveStringLiteralDfa2_0(active0, 0x1000000000L, active1, 0L);
       default :
@@ -488,8 +486,7 @@
    }
    return jjStartNfa_0(0, active0, active1);
 }
-private int jjMoveStringLiteralDfa2_0(long old0, long active0, long old1, long active1)
-{
+private int jjMoveStringLiteralDfa2_0(long old0, long active0, long old1, long active1){
    if (((active0 &= old0) | (active1 &= old1)) == 0L)
       return jjStartNfa_0(0, old0, old1);
    try { curChar = input_stream.readChar(); }
@@ -500,31 +497,31 @@
    switch(curChar)
    {
       case 58:
-         if ((active1 & 0x10L) != 0L)
-            return jjStopAtPos(2, 68);
+         if ((active1 & 0x20L) != 0L)
+            return jjStopAtPos(2, 69);
          break;
       case 97:
-         return jjMoveStringLiteralDfa3_0(active0, 0x100000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x80000000000000L, active1, 0L);
       case 98:
-         return jjMoveStringLiteralDfa3_0(active0, 0x100000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x80000000000L, active1, 0L);
       case 99:
-         return jjMoveStringLiteralDfa3_0(active0, 0x2000000000000L, active1, 0x2L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x1000000000000L, active1, 0x1L);
       case 100:
          if ((active0 & 0x4L) != 0L)
             return jjStartNfaWithStates_0(2, 2, 83);
-         else if ((active0 & 0x10000000000000L) != 0L)
-            return jjStartNfaWithStates_0(2, 52, 83);
+         else if ((active0 & 0x8000000000000L) != 0L)
+            return jjStartNfaWithStates_0(2, 51, 83);
          break;
       case 101:
-         return jjMoveStringLiteralDfa3_0(active0, 0x400000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x200000000000000L, active1, 0L);
       case 103:
          if ((active0 & 0x1000000000L) != 0L)
             return jjStartNfaWithStates_0(2, 36, 83);
          break;
       case 105:
-         return jjMoveStringLiteralDfa3_0(active0, 0x200000000000L, active1, 0x40L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x100000000000L, active1, 0x80L);
       case 106:
-         return jjMoveStringLiteralDfa3_0(active0, 0L, active1, 0x8L);
+         return jjMoveStringLiteralDfa3_0(active0, 0L, active1, 0x10L);
       case 107:
          return jjMoveStringLiteralDfa3_0(active0, 0x6000L, active1, 0L);
       case 109:
@@ -537,41 +534,40 @@
             jjmatchedKind = 37;
             jjmatchedPos = 2;
          }
-         return jjMoveStringLiteralDfa3_0(active0, 0x201080000000000L, active1, 0x1L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x8100840000000000L, active1, 0L);
       case 112:
-         return jjMoveStringLiteralDfa3_0(active0, 0x800000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x400000000000L, active1, 0L);
       case 114:
-         return jjMoveStringLiteralDfa3_0(active0, 0x68000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x34000000000000L, active1, 0L);
       case 115:
-         if ((active0 & 0x4000000000000L) != 0L)
-            return jjStartNfaWithStates_0(2, 50, 83);
+         if ((active0 & 0x2000000000000L) != 0L)
+            return jjStartNfaWithStates_0(2, 49, 83);
          return jjMoveStringLiteralDfa3_0(active0, 0x20000000000L, active1, 0L);
       case 116:
          if ((active0 & 0x8L) != 0L)
             return jjStartNfaWithStates_0(2, 3, 83);
          return jjMoveStringLiteralDfa3_0(active0, 0x40000L, active1, 0L);
       case 117:
-         return jjMoveStringLiteralDfa3_0(active0, 0x8000010000000000L, active1, 0x20L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x4000010000000000L, active1, 0x40L);
       case 119:
-         return jjMoveStringLiteralDfa3_0(active0, 0x400000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x200000000000L, active1, 0L);
       case 120:
          if ((active0 & 0x4000000000L) != 0L)
             return jjStartNfaWithStates_0(2, 38, 83);
          break;
       case 121:
-         if ((active0 & 0x1000000000000000L) != 0L)
+         if ((active0 & 0x800000000000000L) != 0L)
          {
-            jjmatchedKind = 60;
+            jjmatchedKind = 59;
             jjmatchedPos = 2;
          }
-         return jjMoveStringLiteralDfa3_0(active0, 0x6800000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa3_0(active0, 0x3400000000000000L, active1, 0L);
       default :
          break;
    }
    return jjStartNfa_0(1, active0, active1);
 }
-private int jjMoveStringLiteralDfa3_0(long old0, long active0, long old1, long active1)
-{
+private int jjMoveStringLiteralDfa3_0(long old0, long active0, long old1, long active1){
    if (((active0 &= old0) | (active1 &= old1)) == 0L)
       return jjStartNfa_0(1, old0, old1);
    try { curChar = input_stream.readChar(); }
@@ -582,52 +578,52 @@
    switch(curChar)
    {
       case 58:
-         if ((active1 & 0x8L) != 0L)
-            return jjStopAtPos(3, 67);
+         if ((active1 & 0x10L) != 0L)
+            return jjStopAtPos(3, 68);
          break;
       case 79:
-         return jjMoveStringLiteralDfa4_0(active0, 0x6800000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa4_0(active0, 0x3400000000000000L, active1, 0L);
       case 97:
-         return jjMoveStringLiteralDfa4_0(active0, 0x2000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa4_0(active0, 0x1000000000000L, active1, 0L);
       case 99:
-         return jjMoveStringLiteralDfa4_0(active0, 0x80000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa4_0(active0, 0x40000000000L, active1, 0L);
       case 100:
-         return jjMoveStringLiteralDfa4_0(active0, 0L, active1, 0x40L);
+         return jjMoveStringLiteralDfa4_0(active0, 0L, active1, 0x80L);
       case 101:
          if ((active0 & 0x2000L) != 0L)
          {
             jjmatchedKind = 13;
             jjmatchedPos = 3;
          }
-         return jjMoveStringLiteralDfa4_0(active0, 0xc00000004000L, active1, 0L);
+         return jjMoveStringLiteralDfa4_0(active0, 0x600000004000L, active1, 0L);
       case 103:
-         return jjMoveStringLiteralDfa4_0(active0, 0x1000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa4_0(active0, 0x800000000000L, active1, 0L);
       case 107:
-         if ((active0 & 0x400000000000000L) != 0L)
-            return jjStartNfaWithStates_0(3, 58, 83);
+         if ((active0 & 0x200000000000000L) != 0L)
+            return jjStartNfaWithStates_0(3, 57, 83);
          break;
       case 109:
-         if ((active0 & 0x200000000000L) != 0L)
-            return jjStartNfaWithStates_0(3, 45, 83);
-         return jjMoveStringLiteralDfa4_0(active0, 0L, active1, 0x20L);
+         if ((active0 & 0x100000000000L) != 0L)
+            return jjStartNfaWithStates_0(3, 44, 83);
+         return jjMoveStringLiteralDfa4_0(active0, 0L, active1, 0x40L);
       case 110:
          return jjMoveStringLiteralDfa4_0(active0, 0x10000000000L, active1, 0L);
       case 111:
-         return jjMoveStringLiteralDfa4_0(active0, 0L, active1, 0x2L);
-      case 114:
-         if ((active0 & 0x100000000000000L) != 0L)
-            return jjStartNfaWithStates_0(3, 56, 83);
-         else if ((active0 & 0x8000000000000000L) != 0L)
-            return jjStartNfaWithStates_0(3, 63, 83);
-         return jjMoveStringLiteralDfa4_0(active0, 0x60000000000000L, active1, 0L);
-      case 115:
-         return jjMoveStringLiteralDfa4_0(active0, 0x100000000000L, active1, 0L);
-      case 116:
-         if ((active0 & 0x8000000000000L) != 0L)
-            return jjStartNfaWithStates_0(3, 51, 83);
-         return jjMoveStringLiteralDfa4_0(active0, 0x200020000000000L, active1, 0L);
-      case 117:
          return jjMoveStringLiteralDfa4_0(active0, 0L, active1, 0x1L);
+      case 114:
+         if ((active0 & 0x80000000000000L) != 0L)
+            return jjStartNfaWithStates_0(3, 55, 83);
+         else if ((active0 & 0x4000000000000000L) != 0L)
+            return jjStartNfaWithStates_0(3, 62, 83);
+         return jjMoveStringLiteralDfa4_0(active0, 0x30000000000000L, active1, 0L);
+      case 115:
+         return jjMoveStringLiteralDfa4_0(active0, 0x80000000000L, active1, 0L);
+      case 116:
+         if ((active0 & 0x4000000000000L) != 0L)
+            return jjStartNfaWithStates_0(3, 50, 83);
+         return jjMoveStringLiteralDfa4_0(active0, 0x100020000000000L, active1, 0L);
+      case 117:
+         return jjMoveStringLiteralDfa4_0(active0, 0x8000000000000000L, active1, 0L);
       case 119:
          return jjMoveStringLiteralDfa4_0(active0, 0x40000L, active1, 0L);
       default :
@@ -635,8 +631,7 @@
    }
    return jjStartNfa_0(2, active0, active1);
 }
-private int jjMoveStringLiteralDfa4_0(long old0, long active0, long old1, long active1)
-{
+private int jjMoveStringLiteralDfa4_0(long old0, long active0, long old1, long active1){
    if (((active0 &= old0) | (active1 &= old1)) == 0L)
       return jjStartNfa_0(2, old0, old1);
    try { curChar = input_stream.readChar(); }
@@ -647,44 +642,43 @@
    switch(curChar)
    {
       case 58:
-         if ((active1 & 0x20L) != 0L)
-            return jjStopAtPos(4, 69);
-         else if ((active1 & 0x40L) != 0L)
+         if ((active1 & 0x40L) != 0L)
             return jjStopAtPos(4, 70);
+         else if ((active1 & 0x80L) != 0L)
+            return jjStopAtPos(4, 71);
          break;
       case 73:
          return jjMoveStringLiteralDfa5_0(active0, 0x4000L, active1, 0L);
       case 97:
-         return jjMoveStringLiteralDfa5_0(active0, 0x80000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa5_0(active0, 0x40000000000L, active1, 0L);
       case 101:
-         return jjMoveStringLiteralDfa5_0(active0, 0x60000000040000L, active1, 0L);
+         return jjMoveStringLiteralDfa5_0(active0, 0x30000000040000L, active1, 0L);
       case 102:
-         return jjMoveStringLiteralDfa5_0(active0, 0x6800000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa5_0(active0, 0x3400000000000000L, active1, 0L);
       case 104:
-         if ((active0 & 0x200000000000000L) != 0L)
-            return jjStartNfaWithStates_0(4, 57, 83);
+         if ((active0 & 0x100000000000000L) != 0L)
+            return jjStartNfaWithStates_0(4, 56, 83);
          break;
       case 105:
          return jjMoveStringLiteralDfa5_0(active0, 0x20000000000L, active1, 0L);
       case 110:
-         return jjMoveStringLiteralDfa5_0(active0, 0L, active1, 0x2L);
+         return jjMoveStringLiteralDfa5_0(active0, 0L, active1, 0x1L);
       case 114:
-         if ((active0 & 0x400000000000L) != 0L)
+         if ((active0 & 0x200000000000L) != 0L)
+            return jjStartNfaWithStates_0(4, 45, 83);
+         else if ((active0 & 0x400000000000L) != 0L)
             return jjStartNfaWithStates_0(4, 46, 83);
-         else if ((active0 & 0x800000000000L) != 0L)
-            return jjStartNfaWithStates_0(4, 47, 83);
          break;
       case 116:
          if ((active0 & 0x10000000000L) != 0L)
             return jjStartNfaWithStates_0(4, 40, 83);
-         return jjMoveStringLiteralDfa5_0(active0, 0x3100000000000L, active1, 0x1L);
+         return jjMoveStringLiteralDfa5_0(active0, 0x8001880000000000L, active1, 0L);
       default :
          break;
    }
    return jjStartNfa_0(3, active0, active1);
 }
-private int jjMoveStringLiteralDfa5_0(long old0, long active0, long old1, long active1)
-{
+private int jjMoveStringLiteralDfa5_0(long old0, long active0, long old1, long active1){
    if (((active0 &= old0) | (active1 &= old1)) == 0L)
       return jjStartNfa_0(3, old0, old1);
    try { curChar = input_stream.readChar(); }
@@ -695,42 +689,41 @@
    switch(curChar)
    {
       case 77:
-         return jjMoveStringLiteralDfa6_0(active0, 0x2000000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa6_0(active0, 0x1000000000000000L, active1, 0L);
       case 87:
-         return jjMoveStringLiteralDfa6_0(active0, 0x4000000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa6_0(active0, 0x2000000000000000L, active1, 0L);
       case 89:
-         return jjMoveStringLiteralDfa6_0(active0, 0x800000000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa6_0(active0, 0x400000000000000L, active1, 0L);
       case 100:
-         if ((active1 & 0x2L) != 0L)
-            return jjStartNfaWithStates_0(5, 65, 83);
+         if ((active1 & 0x1L) != 0L)
+            return jjStartNfaWithStates_0(5, 64, 83);
          break;
       case 101:
-         if ((active0 & 0x2000000000000L) != 0L)
-            return jjStartNfaWithStates_0(5, 49, 83);
-         else if ((active1 & 0x1L) != 0L)
-            return jjStartNfaWithStates_0(5, 64, 83);
+         if ((active0 & 0x1000000000000L) != 0L)
+            return jjStartNfaWithStates_0(5, 48, 83);
+         else if ((active0 & 0x8000000000000000L) != 0L)
+            return jjStartNfaWithStates_0(5, 63, 83);
          return jjMoveStringLiteralDfa6_0(active0, 0x40000L, active1, 0L);
       case 103:
          return jjMoveStringLiteralDfa6_0(active0, 0x4000L, active1, 0L);
       case 104:
-         if ((active0 & 0x1000000000000L) != 0L)
-            return jjStartNfaWithStates_0(5, 48, 83);
+         if ((active0 & 0x800000000000L) != 0L)
+            return jjStartNfaWithStates_0(5, 47, 83);
          break;
       case 110:
-         return jjMoveStringLiteralDfa6_0(active0, 0x60020000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa6_0(active0, 0x30020000000000L, active1, 0L);
       case 114:
-         return jjMoveStringLiteralDfa6_0(active0, 0x100000000000L, active1, 0L);
+         return jjMoveStringLiteralDfa6_0(active0, 0x80000000000L, active1, 0L);
       case 116:
-         if ((active0 & 0x80000000000L) != 0L)
-            return jjStartNfaWithStates_0(5, 43, 83);
+         if ((active0 & 0x40000000000L) != 0L)
+            return jjStartNfaWithStates_0(5, 42, 83);
          break;
       default :
          break;
    }
    return jjStartNfa_0(4, active0, active1);
 }
-private int jjMoveStringLiteralDfa6_0(long old0, long active0, long old1, long active1)
-{
+private int jjMoveStringLiteralDfa6_0(long old0, long active0, long old1, long active1){
    if (((active0 &= old0) | (active1 &= old1)) == 0L)
       return jjStartNfa_0(4, old0, old1);
    try { curChar = input_stream.readChar(); }
@@ -743,24 +736,23 @@
       case 99:
          return jjMoveStringLiteralDfa7_0(active0, 0x20000000000L);
       case 101:
-         return jjMoveStringLiteralDfa7_0(active0, 0x4800000000000000L);
+         return jjMoveStringLiteralDfa7_0(active0, 0x2400000000000000L);
       case 105:
-         return jjMoveStringLiteralDfa7_0(active0, 0x100000000000L);
+         return jjMoveStringLiteralDfa7_0(active0, 0x80000000000L);
       case 110:
          if ((active0 & 0x40000L) != 0L)
             return jjStartNfaWithStates_0(6, 18, 83);
          return jjMoveStringLiteralDfa7_0(active0, 0x4000L);
       case 111:
-         return jjMoveStringLiteralDfa7_0(active0, 0x2000000000000000L);
+         return jjMoveStringLiteralDfa7_0(active0, 0x1000000000000000L);
       case 116:
-         return jjMoveStringLiteralDfa7_0(active0, 0x60000000000000L);
+         return jjMoveStringLiteralDfa7_0(active0, 0x30000000000000L);
       default :
          break;
    }
    return jjStartNfa_0(5, active0, 0L);
 }
-private int jjMoveStringLiteralDfa7_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa7_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(5, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -771,15 +763,15 @@
    switch(curChar)
    {
       case 68:
-         return jjMoveStringLiteralDfa8_0(active0, 0x20000000000000L);
+         return jjMoveStringLiteralDfa8_0(active0, 0x10000000000000L);
       case 84:
-         return jjMoveStringLiteralDfa8_0(active0, 0x40000000000000L);
+         return jjMoveStringLiteralDfa8_0(active0, 0x20000000000000L);
       case 97:
-         return jjMoveStringLiteralDfa8_0(active0, 0x800000000000000L);
+         return jjMoveStringLiteralDfa8_0(active0, 0x400000000000000L);
       case 101:
-         return jjMoveStringLiteralDfa8_0(active0, 0x4000000000000000L);
+         return jjMoveStringLiteralDfa8_0(active0, 0x2000000000000000L);
       case 110:
-         return jjMoveStringLiteralDfa8_0(active0, 0x2000100000000000L);
+         return jjMoveStringLiteralDfa8_0(active0, 0x1000080000000000L);
       case 111:
          return jjMoveStringLiteralDfa8_0(active0, 0x4000L);
       case 116:
@@ -791,8 +783,7 @@
    }
    return jjStartNfa_0(6, active0, 0L);
 }
-private int jjMoveStringLiteralDfa8_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa8_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(6, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -803,30 +794,29 @@
    switch(curChar)
    {
       case 97:
-         return jjMoveStringLiteralDfa9_0(active0, 0x20000000000000L);
+         return jjMoveStringLiteralDfa9_0(active0, 0x10000000000000L);
       case 103:
-         if ((active0 & 0x100000000000L) != 0L)
-            return jjStartNfaWithStates_0(8, 44, 83);
+         if ((active0 & 0x80000000000L) != 0L)
+            return jjStartNfaWithStates_0(8, 43, 83);
          break;
       case 105:
-         return jjMoveStringLiteralDfa9_0(active0, 0x40000000000000L);
+         return jjMoveStringLiteralDfa9_0(active0, 0x20000000000000L);
       case 107:
-         if ((active0 & 0x4000000000000000L) != 0L)
-            return jjStartNfaWithStates_0(8, 62, 83);
+         if ((active0 & 0x2000000000000000L) != 0L)
+            return jjStartNfaWithStates_0(8, 61, 83);
          break;
       case 114:
-         if ((active0 & 0x800000000000000L) != 0L)
-            return jjStartNfaWithStates_0(8, 59, 83);
+         if ((active0 & 0x400000000000000L) != 0L)
+            return jjStartNfaWithStates_0(8, 58, 83);
          return jjMoveStringLiteralDfa9_0(active0, 0x4000L);
       case 116:
-         return jjMoveStringLiteralDfa9_0(active0, 0x2000000000000000L);
+         return jjMoveStringLiteralDfa9_0(active0, 0x1000000000000000L);
       default :
          break;
    }
    return jjStartNfa_0(7, active0, 0L);
 }
-private int jjMoveStringLiteralDfa9_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa9_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(7, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -839,20 +829,19 @@
       case 101:
          return jjMoveStringLiteralDfa10_0(active0, 0x4000L);
       case 104:
-         if ((active0 & 0x2000000000000000L) != 0L)
-            return jjStartNfaWithStates_0(9, 61, 83);
+         if ((active0 & 0x1000000000000000L) != 0L)
+            return jjStartNfaWithStates_0(9, 60, 83);
          break;
       case 109:
-         return jjMoveStringLiteralDfa10_0(active0, 0x40000000000000L);
-      case 116:
          return jjMoveStringLiteralDfa10_0(active0, 0x20000000000000L);
+      case 116:
+         return jjMoveStringLiteralDfa10_0(active0, 0x10000000000000L);
       default :
          break;
    }
    return jjStartNfa_0(8, active0, 0L);
 }
-private int jjMoveStringLiteralDfa10_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa10_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(8, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -865,18 +854,17 @@
       case 67:
          return jjMoveStringLiteralDfa11_0(active0, 0x4000L);
       case 101:
-         if ((active0 & 0x20000000000000L) != 0L)
-            return jjStartNfaWithStates_0(10, 53, 83);
-         else if ((active0 & 0x40000000000000L) != 0L)
-            return jjStartNfaWithStates_0(10, 54, 26);
+         if ((active0 & 0x10000000000000L) != 0L)
+            return jjStartNfaWithStates_0(10, 52, 83);
+         else if ((active0 & 0x20000000000000L) != 0L)
+            return jjStartNfaWithStates_0(10, 53, 26);
          break;
       default :
          break;
    }
    return jjStartNfa_0(9, active0, 0L);
 }
-private int jjMoveStringLiteralDfa11_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa11_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(9, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -893,8 +881,7 @@
    }
    return jjStartNfa_0(10, active0, 0L);
 }
-private int jjMoveStringLiteralDfa12_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa12_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(10, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -911,8 +898,7 @@
    }
    return jjStartNfa_0(11, active0, 0L);
 }
-private int jjMoveStringLiteralDfa13_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa13_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(11, old0, 0L);
    try { curChar = input_stream.readChar(); }
@@ -958,546 +944,546 @@
             switch(jjstateSet[--i])
             {
                case 33:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 5:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 28:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 64:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 32:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 36:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 27:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 3:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddStates(7, 12);
+                     { jjCheckNAddStates(7, 12); }
                   else if (curChar == 46)
-                     jjCheckNAdd(42);
+                     { jjCheckNAdd(42); }
                   if ((0x3fe000000000000L & l) != 0L)
                   {
-                     if (kind > 84)
-                        kind = 84;
-                     jjCheckNAddTwoStates(39, 40);
+                     if (kind > 86)
+                        kind = 86;
+                     { jjCheckNAddTwoStates(39, 40); }
                   }
                   else if (curChar == 48)
                   {
-                     if (kind > 84)
-                        kind = 84;
-                     jjCheckNAddStates(13, 15);
+                     if (kind > 86)
+                        kind = 86;
+                     { jjCheckNAddStates(13, 15); }
                   }
                   break;
                case 31:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 63:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 15:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 35:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 26:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 30:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 83:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 34:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 6:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 29:
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 73)
-                        kind = 73;
+                     if (kind > 74)
+                        kind = 74;
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   else if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
-                  if ((0x3ff000000000000L & l) != 0L)
+                  if ((0x3ff001000000000L & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   else if (curChar == 43)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAdd(72);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAdd(72); }
                   }
                   else if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
@@ -1505,169 +1491,169 @@
                case 38:
                   if ((0x3fe000000000000L & l) == 0L)
                      break;
-                  if (kind > 84)
-                     kind = 84;
-                  jjCheckNAddTwoStates(39, 40);
+                  if (kind > 86)
+                     kind = 86;
+                  { jjCheckNAddTwoStates(39, 40); }
                   break;
                case 39:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
-                  if (kind > 84)
-                     kind = 84;
-                  jjCheckNAddTwoStates(39, 40);
+                  if (kind > 86)
+                     kind = 86;
+                  { jjCheckNAddTwoStates(39, 40); }
                   break;
                case 41:
                   if (curChar == 46)
-                     jjCheckNAdd(42);
+                     { jjCheckNAdd(42); }
                   break;
                case 42:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
-                  if (kind > 85)
-                     kind = 85;
-                  jjCheckNAddStates(16, 18);
+                  if (kind > 87)
+                     kind = 87;
+                  { jjCheckNAddStates(16, 18); }
                   break;
                case 44:
                   if ((0x280000000000L & l) != 0L)
-                     jjCheckNAdd(45);
+                     { jjCheckNAdd(45); }
                   break;
                case 45:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
-                  if (kind > 85)
-                     kind = 85;
-                  jjCheckNAddTwoStates(45, 46);
+                  if (kind > 87)
+                     kind = 87;
+                  { jjCheckNAddTwoStates(45, 46); }
                   break;
                case 47:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddStates(7, 12);
+                     { jjCheckNAddStates(7, 12); }
                   break;
                case 48:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddTwoStates(48, 49);
+                     { jjCheckNAddTwoStates(48, 49); }
                   break;
                case 49:
                   if (curChar != 46)
                      break;
-                  if (kind > 85)
-                     kind = 85;
-                  jjCheckNAddStates(19, 21);
+                  if (kind > 87)
+                     kind = 87;
+                  { jjCheckNAddStates(19, 21); }
                   break;
                case 50:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
-                  if (kind > 85)
-                     kind = 85;
-                  jjCheckNAddStates(19, 21);
+                  if (kind > 87)
+                     kind = 87;
+                  { jjCheckNAddStates(19, 21); }
                   break;
                case 51:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddTwoStates(51, 52);
+                     { jjCheckNAddTwoStates(51, 52); }
                   break;
                case 53:
                   if ((0x280000000000L & l) != 0L)
-                     jjCheckNAdd(54);
+                     { jjCheckNAdd(54); }
                   break;
                case 54:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
-                  if (kind > 85)
-                     kind = 85;
-                  jjCheckNAddTwoStates(54, 46);
+                  if (kind > 87)
+                     kind = 87;
+                  { jjCheckNAddTwoStates(54, 46); }
                   break;
                case 55:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddTwoStates(55, 46);
+                     { jjCheckNAddTwoStates(55, 46); }
                   break;
                case 56:
                   if (curChar != 48)
                      break;
-                  if (kind > 84)
-                     kind = 84;
-                  jjCheckNAddStates(13, 15);
+                  if (kind > 86)
+                     kind = 86;
+                  { jjCheckNAddStates(13, 15); }
                   break;
                case 57:
                   if ((0xff000000000000L & l) == 0L)
                      break;
-                  if (kind > 84)
-                     kind = 84;
-                  jjCheckNAddTwoStates(57, 40);
+                  if (kind > 86)
+                     kind = 86;
+                  { jjCheckNAddTwoStates(57, 40); }
                   break;
                case 59:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
-                  if (kind > 84)
-                     kind = 84;
-                  jjCheckNAddTwoStates(59, 40);
+                  if (kind > 86)
+                     kind = 86;
+                  { jjCheckNAddTwoStates(59, 40); }
                   break;
                case 67:
-                  if ((0x3ff000000000000L & l) == 0L)
+                  if ((0x3ff001000000000L & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(3, 6);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(3, 6); }
                   break;
                case 68:
                   if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 69;
                   break;
                case 70:
-                  if ((0x3ff000000000000L & l) == 0L)
+                  if ((0x3ff001000000000L & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(22, 24);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(22, 24); }
                   break;
                case 71:
                   if (curChar != 43)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAdd(72);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAdd(72); }
                   break;
                case 72:
                   if (curChar == 46)
                      jjstateSet[jjnewStateCnt++] = 73;
                   break;
                case 74:
-                  if ((0x3ff000000000000L & l) == 0L)
+                  if ((0x3ff001000000000L & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(25, 28);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(25, 28); }
                   break;
                case 75:
                   if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 76;
                   break;
                case 77:
-                  if ((0x3ff000000000000L & l) == 0L)
-                     break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(29, 31);
-                  break;
-               case 78:
-                  if ((0x3ff000000000000L & l) == 0L)
+                  if ((0x3ff001000000000L & l) == 0L)
                      break;
                   if (kind > 73)
                      kind = 73;
-                  jjCheckNAddStates(0, 2);
+                  { jjCheckNAddStates(29, 31); }
+                  break;
+               case 78:
+                  if ((0x3ff001000000000L & l) == 0L)
+                     break;
+                  if (kind > 74)
+                     kind = 74;
+                  { jjCheckNAddStates(0, 2); }
                   break;
                case 79:
                   if (curChar == 35)
                      jjstateSet[jjnewStateCnt++] = 80;
                   break;
                case 81:
-                  if ((0x3ff000000000000L & l) == 0L)
+                  if ((0x3ff001000000000L & l) == 0L)
                      break;
-                  if (kind > 73)
-                     kind = 73;
-                  jjCheckNAddTwoStates(81, 82);
+                  if (kind > 74)
+                     kind = 74;
+                  { jjCheckNAddTwoStates(81, 82); }
                   break;
                case 82:
-                  if (curChar == 43 && kind > 73)
-                     kind = 73;
+                  if (curChar == 43 && kind > 74)
+                     kind = 74;
                   break;
                default : break;
             }
@@ -1683,15 +1669,15 @@
                case 33:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 101)
                      jjstateSet[jjnewStateCnt++] = 32;
@@ -1699,15 +1685,15 @@
                case 5:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 117)
                      jjstateSet[jjnewStateCnt++] = 4;
@@ -1715,15 +1701,15 @@
                case 28:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 109)
                      jjstateSet[jjnewStateCnt++] = 27;
@@ -1731,34 +1717,34 @@
                case 64:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 119)
                   {
-                     if (kind > 55)
-                        kind = 55;
+                     if (kind > 54)
+                        kind = 54;
                   }
                   break;
                case 32:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 110)
                      jjstateSet[jjnewStateCnt++] = 31;
@@ -1766,15 +1752,15 @@
                case 36:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 117)
                      jjstateSet[jjnewStateCnt++] = 35;
@@ -1782,15 +1768,15 @@
                case 27:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 101)
                      jjstateSet[jjnewStateCnt++] = 26;
@@ -1798,12 +1784,12 @@
                case 3:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(32, 38);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(32, 38); }
                   }
                   if (curChar == 110)
-                     jjAddStates(39, 40);
+                     { jjAddStates(39, 40); }
                   else if (curChar == 99)
                      jjstateSet[jjnewStateCnt++] = 36;
                   else if (curChar == 70)
@@ -1820,15 +1806,15 @@
                case 31:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 116)
                      jjstateSet[jjnewStateCnt++] = 30;
@@ -1836,15 +1822,15 @@
                case 63:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 111)
                      jjstateSet[jjnewStateCnt++] = 64;
@@ -1854,15 +1840,15 @@
                case 15:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 97)
                      jjstateSet[jjnewStateCnt++] = 14;
@@ -1870,15 +1856,15 @@
                case 35:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 114)
                      jjstateSet[jjnewStateCnt++] = 34;
@@ -1886,15 +1872,15 @@
                case 26:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 115)
                      jjstateSet[jjnewStateCnt++] = 25;
@@ -1902,15 +1888,15 @@
                case 30:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 84)
                      jjstateSet[jjnewStateCnt++] = 29;
@@ -1918,29 +1904,29 @@
                case 83:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   break;
                case 34:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 114)
                      jjstateSet[jjnewStateCnt++] = 33;
@@ -1948,15 +1934,15 @@
                case 6:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 114)
                      jjstateSet[jjnewStateCnt++] = 5;
@@ -1964,15 +1950,15 @@
                case 29:
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 73)
-                        kind = 73;
-                     jjCheckNAddStates(0, 2);
+                     if (kind > 74)
+                        kind = 74;
+                     { jjCheckNAddStates(0, 2); }
                   }
                   if ((0x7fffffe87fffffeL & l) != 0L)
                   {
-                     if (kind > 72)
-                        kind = 72;
-                     jjCheckNAddStates(3, 6);
+                     if (kind > 73)
+                        kind = 73;
+                     { jjCheckNAddStates(3, 6); }
                   }
                   if (curChar == 105)
                      jjstateSet[jjnewStateCnt++] = 28;
@@ -2050,8 +2036,8 @@
                      jjstateSet[jjnewStateCnt++] = 20;
                   break;
                case 22:
-                  if (curChar == 112 && kind > 55)
-                     kind = 55;
+                  if (curChar == 112 && kind > 54)
+                     kind = 54;
                   break;
                case 23:
                   if (curChar == 109)
@@ -2070,35 +2056,35 @@
                      jjstateSet[jjnewStateCnt++] = 36;
                   break;
                case 40:
-                  if ((0x110000001100L & l) != 0L && kind > 84)
-                     kind = 84;
+                  if ((0x110000001100L & l) != 0L && kind > 86)
+                     kind = 86;
                   break;
                case 43:
                   if ((0x2000000020L & l) != 0L)
-                     jjAddStates(41, 42);
+                     { jjAddStates(41, 42); }
                   break;
                case 46:
-                  if ((0x5400000054L & l) != 0L && kind > 85)
-                     kind = 85;
+                  if ((0x5400000054L & l) != 0L && kind > 87)
+                     kind = 87;
                   break;
                case 52:
                   if ((0x2000000020L & l) != 0L)
-                     jjAddStates(43, 44);
+                     { jjAddStates(43, 44); }
                   break;
                case 58:
                   if ((0x100000001000000L & l) != 0L)
-                     jjCheckNAdd(59);
+                     { jjCheckNAdd(59); }
                   break;
                case 59:
                   if ((0x7e0000007eL & l) == 0L)
                      break;
-                  if (kind > 84)
-                     kind = 84;
-                  jjCheckNAddTwoStates(59, 40);
+                  if (kind > 86)
+                     kind = 86;
+                  { jjCheckNAddTwoStates(59, 40); }
                   break;
                case 60:
                   if (curChar == 110)
-                     jjAddStates(39, 40);
+                     { jjAddStates(39, 40); }
                   break;
                case 61:
                   if (curChar == 108 && kind > 33)
@@ -2115,55 +2101,55 @@
                case 66:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(32, 38);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(32, 38); }
                   break;
                case 67:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(3, 6);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(3, 6); }
                   break;
                case 69:
                case 70:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(22, 24);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(22, 24); }
                   break;
                case 73:
                case 74:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(25, 28);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(25, 28); }
                   break;
                case 76:
                case 77:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 72)
-                     kind = 72;
-                  jjCheckNAddStates(29, 31);
+                  if (kind > 73)
+                     kind = 73;
+                  { jjCheckNAddStates(29, 31); }
                   break;
                case 78:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 73)
-                     kind = 73;
-                  jjCheckNAddStates(0, 2);
+                  if (kind > 74)
+                     kind = 74;
+                  { jjCheckNAddStates(0, 2); }
                   break;
                case 80:
                case 81:
                   if ((0x7fffffe87fffffeL & l) == 0L)
                      break;
-                  if (kind > 73)
-                     kind = 73;
-                  jjCheckNAddTwoStates(81, 82);
+                  if (kind > 74)
+                     kind = 74;
+                  { jjCheckNAddTwoStates(81, 82); }
                   break;
                default : break;
             }
@@ -2171,7 +2157,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -2180,7 +2166,7 @@
          {
             switch(jjstateSet[--i])
             {
-               default : break;
+               default : if (i1 == 0 || l1 == 0 || i2 == 0 ||  l2 == 0) break; else break;
             }
          } while(i != startsAt);
       }
@@ -2197,24 +2183,21 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-private final int jjStopStringLiteralDfa_1(int pos, long active0, long active1)
-{
+private final int jjStopStringLiteralDfa_1(int pos, long active0, long active1){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_1(int pos, long active0, long active1)
-{
+private final int jjStartNfa_1(int pos, long active0, long active1){
    return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0, active1), pos + 1);
 }
-private int jjMoveStringLiteralDfa0_1()
-{
+private int jjMoveStringLiteralDfa0_1(){
    switch(curChar)
    {
       case 39:
-         return jjStopAtPos(0, 80);
+         return jjStopAtPos(0, 82);
       default :
          return jjMoveNfa_1(0, 0);
    }
@@ -2244,12 +2227,12 @@
             switch(jjstateSet[--i])
             {
                case 0:
-                  if ((0xffffff7fffffffffL & l) != 0L && kind > 79)
-                     kind = 79;
+                  if ((0xffffff7fffffffffL & l) != 0L && kind > 81)
+                     kind = 81;
                   break;
                case 1:
-                  if ((0x8400000000L & l) != 0L && kind > 78)
-                     kind = 78;
+                  if ((0x8400000000L & l) != 0L && kind > 80)
+                     kind = 80;
                   break;
                case 2:
                   if ((0xf000000000000L & l) != 0L)
@@ -2258,13 +2241,13 @@
                case 3:
                   if ((0xff000000000000L & l) == 0L)
                      break;
-                  if (kind > 78)
-                     kind = 78;
+                  if (kind > 80)
+                     kind = 80;
                   jjstateSet[jjnewStateCnt++] = 4;
                   break;
                case 4:
-                  if ((0xff000000000000L & l) != 0L && kind > 78)
-                     kind = 78;
+                  if ((0xff000000000000L & l) != 0L && kind > 80)
+                     kind = 80;
                   break;
                default : break;
             }
@@ -2280,19 +2263,19 @@
                case 0:
                   if ((0xffffffffefffffffL & l) != 0L)
                   {
-                     if (kind > 79)
-                        kind = 79;
+                     if (kind > 81)
+                        kind = 81;
                   }
                   else if (curChar == 92)
-                     jjAddStates(45, 47);
+                     { jjAddStates(45, 47); }
                   break;
                case 1:
-                  if ((0x14404510000000L & l) != 0L && kind > 78)
-                     kind = 78;
+                  if ((0x14404510000000L & l) != 0L && kind > 80)
+                     kind = 80;
                   break;
                case 5:
-                  if ((0xffffffffefffffffL & l) != 0L && kind > 79)
-                     kind = 79;
+                  if ((0xffffffffefffffffL & l) != 0L && kind > 81)
+                     kind = 81;
                   break;
                default : break;
             }
@@ -2300,7 +2283,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -2310,10 +2293,10 @@
             switch(jjstateSet[--i])
             {
                case 0:
-                  if (jjCanMove_0(hiByte, i1, i2, l1, l2) && kind > 79)
-                     kind = 79;
+                  if (jjCanMove_0(hiByte, i1, i2, l1, l2) && kind > 81)
+                     kind = 81;
                   break;
-               default : break;
+               default : if (i1 == 0 || l1 == 0 || i2 == 0 ||  l2 == 0) break; else break;
             }
          } while(i != startsAt);
       }
@@ -2330,24 +2313,21 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-private final int jjStopStringLiteralDfa_2(int pos, long active0, long active1)
-{
+private final int jjStopStringLiteralDfa_2(int pos, long active0, long active1){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_2(int pos, long active0, long active1)
-{
+private final int jjStartNfa_2(int pos, long active0, long active1){
    return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0, active1), pos + 1);
 }
-private int jjMoveStringLiteralDfa0_2()
-{
+private int jjMoveStringLiteralDfa0_2(){
    switch(curChar)
    {
       case 34:
-         return jjStopAtPos(0, 83);
+         return jjStopAtPos(0, 85);
       default :
          return jjMoveNfa_2(0, 0);
    }
@@ -2371,12 +2351,12 @@
             switch(jjstateSet[--i])
             {
                case 0:
-                  if ((0xfffffffbffffffffL & l) != 0L && kind > 82)
-                     kind = 82;
+                  if ((0xfffffffbffffffffL & l) != 0L && kind > 84)
+                     kind = 84;
                   break;
                case 1:
-                  if ((0x8400000000L & l) != 0L && kind > 81)
-                     kind = 81;
+                  if ((0x8400000000L & l) != 0L && kind > 83)
+                     kind = 83;
                   break;
                case 2:
                   if ((0xf000000000000L & l) != 0L)
@@ -2385,13 +2365,13 @@
                case 3:
                   if ((0xff000000000000L & l) == 0L)
                      break;
-                  if (kind > 81)
-                     kind = 81;
+                  if (kind > 83)
+                     kind = 83;
                   jjstateSet[jjnewStateCnt++] = 4;
                   break;
                case 4:
-                  if ((0xff000000000000L & l) != 0L && kind > 81)
-                     kind = 81;
+                  if ((0xff000000000000L & l) != 0L && kind > 83)
+                     kind = 83;
                   break;
                default : break;
             }
@@ -2407,19 +2387,19 @@
                case 0:
                   if ((0xffffffffefffffffL & l) != 0L)
                   {
-                     if (kind > 82)
-                        kind = 82;
+                     if (kind > 84)
+                        kind = 84;
                   }
                   else if (curChar == 92)
-                     jjAddStates(45, 47);
+                     { jjAddStates(45, 47); }
                   break;
                case 1:
-                  if ((0x14404510000000L & l) != 0L && kind > 81)
-                     kind = 81;
+                  if ((0x14404510000000L & l) != 0L && kind > 83)
+                     kind = 83;
                   break;
                case 5:
-                  if ((0xffffffffefffffffL & l) != 0L && kind > 82)
-                     kind = 82;
+                  if ((0xffffffffefffffffL & l) != 0L && kind > 84)
+                     kind = 84;
                   break;
                default : break;
             }
@@ -2427,7 +2407,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -2437,10 +2417,10 @@
             switch(jjstateSet[--i])
             {
                case 0:
-                  if (jjCanMove_0(hiByte, i1, i2, l1, l2) && kind > 82)
-                     kind = 82;
+                  if (jjCanMove_0(hiByte, i1, i2, l1, l2) && kind > 84)
+                     kind = 84;
                   break;
-               default : break;
+               default : if (i1 == 0 || l1 == 0 || i2 == 0 ||  l2 == 0) break; else break;
             }
          } while(i != startsAt);
       }
@@ -2457,23 +2437,6 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-static final int[] jjnextStates = {
-   78, 79, 82, 67, 68, 71, 72, 48, 49, 51, 52, 55, 46, 57, 58, 40, 
-   42, 43, 46, 50, 43, 46, 70, 71, 72, 72, 74, 75, 71, 72, 77, 71, 
-   67, 68, 71, 72, 78, 79, 82, 63, 65, 44, 45, 53, 54, 1, 2, 3, 
-};
-private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2)
-{
-   switch(hiByte)
-   {
-      case 0:
-         return ((jjbitVec2[i2] & l2) != 0L);
-      default :
-         if ((jjbitVec0[i1] & l1) != 0L)
-            return true;
-         return false;
-   }
-}
 
 /** Token literal values. */
 public static final String[] jjstrLiteralImages = {
@@ -2482,93 +2445,16 @@
 "\154\151\153\145\111\147\156\157\162\145\103\141\163\145", "\151\156", "\50", "\51", "\142\145\164\167\145\145\156", "\54", "\174", 
 "\136", "\46", "\74\74", "\76\76", "\53", "\55", "\57", "\176", null, null, null, null, 
 null, null, null, "\141\166\147", "\155\151\156", "\155\141\170", "\163\165\155", 
-"\143\157\165\156\164", "\144\151\163\164\151\156\143\164", "\146\156", "\143\157\156\143\141\164", 
+"\143\157\165\156\164", "\144\151\163\164\151\156\143\164", "\143\157\156\143\141\164", 
 "\163\165\142\163\164\162\151\156\147", "\164\162\151\155", "\154\157\167\145\162", "\165\160\160\145\162", 
 "\154\145\156\147\164\150", "\154\157\143\141\164\145", "\141\142\163", "\163\161\162\164", 
 "\155\157\144", "\143\165\162\162\145\156\164\104\141\164\145", 
 "\143\165\162\162\145\156\164\124\151\155\145", null, "\171\145\141\162", "\155\157\156\164\150", "\167\145\145\153", 
 "\144\141\171\117\146\131\145\141\162", "\144\141\171", "\144\141\171\117\146\115\157\156\164\150", 
 "\144\141\171\117\146\127\145\145\153", "\150\157\165\162", "\155\151\156\165\164\145", "\163\145\143\157\156\144", 
-"\44", "\157\142\152\72", "\144\142\72", "\145\156\165\155\72", 
+"\146\156", "\157\160", "\44", "\157\142\152\72", "\144\142\72", "\145\156\165\155\72", 
 "\144\142\151\144\72", "\52", null, null, null, null, null, null, null, null, null, null, null, null, 
-null, null, null, null, null, null, };
-
-/** Lexer state names. */
-public static final String[] lexStateNames = {
-   "DEFAULT",
-   "WithinSingleQuoteLiteral",
-   "WithinDoubleQuoteLiteral",
-};
-
-/** Lex State array. */
-public static final int[] jjnewLexState = {
-   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
-   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
-   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
-   -1, 1, 2, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, -1, -1, 
-};
-static final long[] jjtoToken = {
-   0xfffffffe1fffffffL, 0x3903ffL, 
-};
-static final long[] jjtoSkip = {
-   0x1e0000000L, 0x0L, 
-};
-static final long[] jjtoMore = {
-   0x0L, 0x6f000L, 
-};
-protected JavaCharStream input_stream;
-private final int[] jjrounds = new int[83];
-private final int[] jjstateSet = new int[166];
-private final StringBuilder jjimage = new StringBuilder();
-private StringBuilder image = jjimage;
-private int jjimageLen;
-private int lengthOfMatch;
-protected char curChar;
-/** Constructor. */
-public ExpressionParserTokenManager(JavaCharStream stream){
-   if (JavaCharStream.staticFlag)
-      throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
-   input_stream = stream;
-}
-
-/** Constructor. */
-public ExpressionParserTokenManager(JavaCharStream stream, int lexState){
-   this(stream);
-   SwitchTo(lexState);
-}
-
-/** Reinitialise parser. */
-public void ReInit(JavaCharStream stream)
-{
-   jjmatchedPos = jjnewStateCnt = 0;
-   curLexState = defaultLexState;
-   input_stream = stream;
-   ReInitRounds();
-}
-private void ReInitRounds()
-{
-   int i;
-   jjround = 0x80000001;
-   for (i = 83; i-- > 0;)
-      jjrounds[i] = 0x80000000;
-}
-
-/** Reinitialise parser. */
-public void ReInit(JavaCharStream stream, int lexState)
-{
-   ReInit(stream);
-   SwitchTo(lexState);
-}
-
-/** Switch to specified lex state. */
-public void SwitchTo(int lexState)
-{
-   if (lexState >= 3 || lexState < 0)
-      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
-   else
-      curLexState = lexState;
-}
-
+null, null, null, null, null, null, null, };
 protected Token jjFillToken()
 {
    final Token t;
@@ -2592,6 +2478,23 @@
 
    return t;
 }
+static final int[] jjnextStates = {
+   78, 79, 82, 67, 68, 71, 72, 48, 49, 51, 52, 55, 46, 57, 58, 40, 
+   42, 43, 46, 50, 43, 46, 70, 71, 72, 72, 74, 75, 71, 72, 77, 71, 
+   67, 68, 71, 72, 78, 79, 82, 63, 65, 44, 45, 53, 54, 1, 2, 3, 
+};
+private static final boolean jjCanMove_0(int hiByte, int i1, int i2, long l1, long l2)
+{
+   switch(hiByte)
+   {
+      case 0:
+         return ((jjbitVec2[i2] & l2) != 0L);
+      default :
+         if ((jjbitVec0[i1] & l1) != 0L)
+            return true;
+         return false;
+   }
+}
 
 int curLexState = 0;
 int defaultLexState = 0;
@@ -2613,9 +2516,10 @@
    {
       curChar = input_stream.BeginToken();
    }
-   catch(java.io.IOException e)
+   catch(Exception e)
    {
       jjmatchedKind = 0;
+      jjmatchedPos = -1;
       matchedToken = jjFillToken();
       return matchedToken;
    }
@@ -2701,37 +2605,45 @@
   }
 }
 
+void SkipLexicalActions(Token matchedToken)
+{
+   switch(jjmatchedKind)
+   {
+      default :
+         break;
+   }
+}
 void MoreLexicalActions()
 {
    jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
    switch(jjmatchedKind)
    {
-      case 76 :
+      case 78 :
          image.append(input_stream.GetSuffix(jjimageLen));
          jjimageLen = 0;
            stringBuffer = new StringBuffer();
          break;
-      case 77 :
+      case 79 :
          image.append(input_stream.GetSuffix(jjimageLen));
          jjimageLen = 0;
             stringBuffer = new StringBuffer();
          break;
-      case 78 :
+      case 80 :
          image.append(input_stream.GetSuffix(jjimageLen));
          jjimageLen = 0;
           stringBuffer.append( escapeChar() );
          break;
-      case 79 :
-         image.append(input_stream.GetSuffix(jjimageLen));
-         jjimageLen = 0;
-          stringBuffer.append( image.charAt(image.length()-1) );
-         break;
       case 81 :
          image.append(input_stream.GetSuffix(jjimageLen));
          jjimageLen = 0;
+          stringBuffer.append( image.charAt(image.length()-1) );
+         break;
+      case 83 :
+         image.append(input_stream.GetSuffix(jjimageLen));
+         jjimageLen = 0;
           stringBuffer.append( escapeChar() );
          break;
-      case 82 :
+      case 84 :
          image.append(input_stream.GetSuffix(jjimageLen));
          jjimageLen = 0;
           stringBuffer.append( image.charAt(image.length()-1) );
@@ -2744,19 +2656,19 @@
 {
    switch(jjmatchedKind)
    {
-      case 80 :
+      case 82 :
         image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
           literalValue = stringBuffer.toString();
          break;
-      case 83 :
+      case 85 :
         image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
           literalValue = stringBuffer.toString();
          break;
-      case 84 :
+      case 86 :
         image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
           literalValue = makeInt();
          break;
-      case 85 :
+      case 87 :
         image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1)));
           literalValue = makeFloat();
          break;
@@ -2791,4 +2703,94 @@
    } while (start++ != end);
 }
 
+    /** Constructor. */
+    public ExpressionParserTokenManager(JavaCharStream stream){
+
+      if (JavaCharStream.staticFlag)
+            throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
+
+    input_stream = stream;
+  }
+
+  /** Constructor. */
+  public ExpressionParserTokenManager (JavaCharStream stream, int lexState){
+    ReInit(stream);
+    SwitchTo(lexState);
+  }
+
+  /** Reinitialise parser. */
+  
+  public void ReInit(JavaCharStream stream)
+  {
+
+
+    jjmatchedPos =
+    jjnewStateCnt =
+    0;
+    curLexState = defaultLexState;
+    input_stream = stream;
+    ReInitRounds();
+  }
+
+  private void ReInitRounds()
+  {
+    int i;
+    jjround = 0x80000001;
+    for (i = 83; i-- > 0;)
+      jjrounds[i] = 0x80000000;
+  }
+
+  /** Reinitialise parser. */
+  public void ReInit(JavaCharStream stream, int lexState)
+  
+  {
+    ReInit(stream);
+    SwitchTo(lexState);
+  }
+
+  /** Switch to specified lex state. */
+  public void SwitchTo(int lexState)
+  {
+    if (lexState >= 3 || lexState < 0)
+      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
+    else
+      curLexState = lexState;
+  }
+
+
+/** Lexer state names. */
+public static final String[] lexStateNames = {
+   "DEFAULT",
+   "WithinSingleQuoteLiteral",
+   "WithinDoubleQuoteLiteral",
+};
+
+/** Lex State array. */
+public static final int[] jjnewLexState = {
+   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
+   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
+   -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 
+   -1, -1, -1, 1, 2, -1, -1, 0, -1, -1, 0, -1, -1, -1, -1, -1, -1, 
+};
+static final long[] jjtoToken = {
+   0xfffffffe1fffffffL, 0xe407ffL, 
+};
+static final long[] jjtoSkip = {
+   0x1e0000000L, 0x0L, 
+};
+static final long[] jjtoSpecial = {
+   0x0L, 0x0L, 
+};
+static final long[] jjtoMore = {
+   0x0L, 0x1bc000L, 
+};
+    protected JavaCharStream  input_stream;
+
+    private final int[] jjrounds = new int[83];
+    private final int[] jjstateSet = new int[2 * 83];
+    private final StringBuilder jjimage = new StringBuilder();
+    private StringBuilder image = jjimage;
+    private int jjimageLen;
+    private int lengthOfMatch;
+    protected char curChar;
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTreeConstants.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTreeConstants.java
index b23f38b..c3b3539 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTreeConstants.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/ExpressionParserTreeConstants.java
@@ -1,4 +1,4 @@
-/* Generated By:JavaCC: Do not edit this line. ExpressionParserTreeConstants.java Version 5.0 */
+/* Generated By:JavaCC: Do not edit this line. ExpressionParserTreeConstants.java Version 7.0.5 */
 package org.apache.cayenne.exp.parser;
 
 public interface ExpressionParserTreeConstants
@@ -37,32 +37,33 @@
   public int JJTBITWISENOT = 31;
   public int JJTNEGATE = 32;
   public int JJTCUSTOMFUNCTION = 33;
-  public int JJTCONCAT = 34;
-  public int JJTSUBSTRING = 35;
-  public int JJTTRIM = 36;
-  public int JJTLOWER = 37;
-  public int JJTUPPER = 38;
-  public int JJTLENGTH = 39;
-  public int JJTLOCATE = 40;
-  public int JJTABS = 41;
-  public int JJTSQRT = 42;
-  public int JJTMOD = 43;
-  public int JJTASTERISK = 44;
-  public int JJTCOUNT = 45;
-  public int JJTAVG = 46;
-  public int JJTMAX = 47;
-  public int JJTMIN = 48;
-  public int JJTSUM = 49;
-  public int JJTCURRENTDATE = 50;
-  public int JJTCURRENTTIME = 51;
-  public int JJTCURRENTTIMESTAMP = 52;
-  public int JJTEXTRACT = 53;
-  public int JJTDISTINCT = 54;
-  public int JJTNAMEDPARAMETER = 55;
-  public int JJTOBJPATH = 56;
-  public int JJTDBPATH = 57;
-  public int JJTENUM = 58;
-  public int JJTDBIDPATH = 59;
+  public int JJTCUSTOMOPERATOR = 34;
+  public int JJTCONCAT = 35;
+  public int JJTSUBSTRING = 36;
+  public int JJTTRIM = 37;
+  public int JJTLOWER = 38;
+  public int JJTUPPER = 39;
+  public int JJTLENGTH = 40;
+  public int JJTLOCATE = 41;
+  public int JJTABS = 42;
+  public int JJTSQRT = 43;
+  public int JJTMOD = 44;
+  public int JJTASTERISK = 45;
+  public int JJTCOUNT = 46;
+  public int JJTAVG = 47;
+  public int JJTMAX = 48;
+  public int JJTMIN = 49;
+  public int JJTSUM = 50;
+  public int JJTCURRENTDATE = 51;
+  public int JJTCURRENTTIME = 52;
+  public int JJTCURRENTTIMESTAMP = 53;
+  public int JJTEXTRACT = 54;
+  public int JJTDISTINCT = 55;
+  public int JJTNAMEDPARAMETER = 56;
+  public int JJTOBJPATH = 57;
+  public int JJTDBPATH = 58;
+  public int JJTENUM = 59;
+  public int JJTDBIDPATH = 60;
 
 
   public String[] jjtNodeName = {
@@ -100,6 +101,7 @@
     "BitwiseNot",
     "Negate",
     "CustomFunction",
+    "CustomOperator",
     "Concat",
     "Substring",
     "Trim",
@@ -128,4 +130,4 @@
     "DbIdPath",
   };
 }
-/* JavaCC - OriginalChecksum=d21da7d665d0ef7c43630d13a09f2c1d (do not edit this line) */
+/* JavaCC - OriginalChecksum=cd72c6d845f6bcd460bdcd71d3700282 (do not edit this line) */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/JJTExpressionParserState.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/JJTExpressionParserState.java
index 349ac96..063fef2 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/JJTExpressionParserState.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/parser/JJTExpressionParserState.java
@@ -1,4 +1,4 @@
-/* Generated By:JavaCC: Do not edit this line. JJTExpressionParserState.java Version 5.0 */
+/* Generated By:JavaCC: Do not edit this line. JJTExpressionParserState.java Version 7.0.5 */
 package org.apache.cayenne.exp.parser;
 
 public class JJTExpressionParserState {
@@ -120,4 +120,4 @@
     }
   }
 }
-/* JavaCC - OriginalChecksum=b1c8a6064ef7a507929d26284938a24b (do not edit this line) */
+/* JavaCC - OriginalChecksum=4600ff3b66322d8f3f50176b034a1b48 (do not edit this line) */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
index af46649..2e1a05f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/BaseProperty.java
@@ -428,7 +428,7 @@
         Object[] expressions = new Expression[arguments.length + 1];
         expressions[0] = getExpression();
         for(int i=1; i<=arguments.length; i++) {
-            expressions[i] = arguments[i].getExpression();
+            expressions[i] = arguments[i-1].getExpression();
         }
         return PropertyFactory.createBase(FunctionExpressionFactory.functionCall(functionName, expressions), returnType);
     }
@@ -443,4 +443,29 @@
         System.arraycopy(arguments, 0, expressions, 1, arguments.length);
         return PropertyFactory.createBase(FunctionExpressionFactory.functionCall(functionName, expressions), returnType);
     }
+
+    /**
+     * @return An expression for using operator with first argument equals to <b>this</b> property
+     *      and provided additional arguments
+     */
+    public <T> BaseProperty<T> operator(String operator, Class<T> returnType, BaseProperty<?>... arguments) {
+        Object[] expressions = new Expression[arguments.length + 1];
+        expressions[0] = getExpression();
+        for(int i=1; i<=arguments.length; i++) {
+            expressions[i] = arguments[i-1].getExpression();
+        }
+        return PropertyFactory.createBase(FunctionExpressionFactory.operator(operator, expressions), returnType);
+    }
+
+    /**
+     * @return An expression for using operator with first argument equals to <b>this</b> property
+     *      and provided additional arguments
+     */
+    public <T> BaseProperty<T> operator(String operator, Class<T> returnType, Object... arguments) {
+        Object[] expressions = new Object[arguments.length + 1];
+        expressions[0] = getExpression();
+        System.arraycopy(arguments, 0, expressions, 1, arguments.length);
+        return PropertyFactory.createBase(FunctionExpressionFactory.operator(operator, expressions), returnType);
+    }
+
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
index ee5ba2a..c5a61af 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/EntityProperty.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.exp.property;
 
+import java.util.Collection;
+
 import org.apache.cayenne.Persistent;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
@@ -53,6 +55,37 @@
         super(name, expression, type);
     }
 
+    public Expression eqId(Object id) {
+        return ExpressionFactory.matchExp(getExpression(), id);
+    }
+
+    public Expression inId(Collection<Object> ids) {
+        return ExpressionFactory.inExp(getExpression(), ids);
+    }
+
+    public Expression inId(Object firstId, Object... moreIds) {
+        Object[] ids = new Object[moreIds.length + 1];
+        ids[0] = firstId;
+        System.arraycopy(moreIds, 0, ids, 1, moreIds.length);
+        return ExpressionFactory.inExp(getExpression(), ids);
+    }
+
+    public Expression neqId(Object id) {
+        return ExpressionFactory.noMatchExp(getExpression(), id);
+    }
+
+    public Expression ninId(Collection<Object> ids) {
+        return ExpressionFactory.notInExp(getExpression(), ids);
+    }
+
+    public Expression ninId(Object firstId, Object... moreIds) {
+        Object[] ids = new Object[moreIds.length + 1];
+        ids[0] = firstId;
+        System.arraycopy(moreIds, 0, ids, 1, moreIds.length);
+        return ExpressionFactory.notInExp(getExpression(), ids);
+    }
+
+
     /**
      * {@inheritDoc}
      */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/package-info.java b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/package-info.java
index 5b23d5f..211ee1f 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/exp/property/package-info.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/exp/property/package-info.java
@@ -39,7 +39,7 @@
  * Currently supported Property types:
  * <ul>
  *     <li>{@link org.apache.cayenne.exp.property.NumericProperty} for all data types inherited from {@link java.lang.Number}.<br>
- *     Supports comparision and math functions (like {@link org.apache.cayenne.exp.property.NumericProperty#sqrt() sqrt()}).
+ *     Supports comparison and math functions (like {@link org.apache.cayenne.exp.property.NumericProperty#sqrt() sqrt()}).
  *     <br>
  *     <li>{@link org.apache.cayenne.exp.property.StringProperty} for all data types inherited from {@link java.lang.CharSequence}.<br>
  *     Supports multiple string functions ({@link org.apache.cayenne.exp.property.StringProperty#like(java.lang.String) like()},
@@ -54,7 +54,7 @@
  *     <br>
  *     <li>{@link org.apache.cayenne.exp.property.ListProperty}, {@link org.apache.cayenne.exp.property.SetProperty}
  *     and {@link org.apache.cayenne.exp.property.MapProperty} are for to-many relationships.<br>
- *     In addition to to-one related methods these properties support collection comparision methods
+ *     In addition to to-one related methods these properties support collection comparison methods
  *     like {@link org.apache.cayenne.exp.property.ListProperty#contains(org.apache.cayenne.Persistent) contains()}.
  *     <br>
  *     <li>{@link org.apache.cayenne.exp.property.EmbeddableProperty} for embeddable objects
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/EntityResolver.java b/cayenne-server/src/main/java/org/apache/cayenne/map/EntityResolver.java
index 088326c..8b945da 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/EntityResolver.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/EntityResolver.java
@@ -34,6 +34,7 @@
 import org.apache.cayenne.reflect.FaultFactory;
 import org.apache.cayenne.reflect.LifecycleCallbackRegistry;
 import org.apache.cayenne.reflect.SingletonFaultFactory;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
 import org.apache.cayenne.reflect.generic.DataObjectDescriptorFactory;
 import org.apache.cayenne.reflect.valueholder.ValueHolderDescriptorFactory;
 import org.slf4j.Logger;
@@ -68,10 +69,16 @@
     protected transient ValueObjectTypeRegistry valueObjectTypeRegistry;
 
     /**
+     * @since 4.2
+     */
+    protected transient ValueComparisonStrategyFactory valueComparisonStrategyFactory;
+
+
+    /**
      * Creates new empty EntityResolver.
      */
     public EntityResolver() {
-        this(Collections.<DataMap> emptyList());
+        this(Collections.emptyList());
     }
 
     /**
@@ -96,10 +103,8 @@
 
             for (DbEntity entity : map.getDbEntities()) {
 
-                // iterate by copy to avoid concurrency modification errors on
-                // reflexive relationships
-                DbRelationship[] relationships = entity.getRelationships().toArray(
-                        new DbRelationship[entity.getRelationships().size()]);
+                // iterate by copy to avoid concurrency modification errors on reflexive relationships
+                DbRelationship[] relationships = entity.getRelationships().toArray(new DbRelationship[0]);
 
                 for (DbRelationship relationship : relationships) {
                     if (relationship.getReverseRelationship() == null) {
@@ -555,11 +560,9 @@
 
                     // add factories in reverse of the desired chain order
                     classDescriptorMap.addFactory(new ValueHolderDescriptorFactory(classDescriptorMap));
-                    classDescriptorMap.addFactory(new DataObjectDescriptorFactory(classDescriptorMap, faultFactory));
+                    classDescriptorMap.addFactory(new DataObjectDescriptorFactory(classDescriptorMap, faultFactory, valueComparisonStrategyFactory));
 
-                    // since ClassDescriptorMap is not synchronized, we need to
-                    // prefill
-                    // it with entity proxies here.
+                    // since ClassDescriptorMap is not synchronized, we need to prefill it with entity proxies here.
                     for (DataMap map : maps) {
                         for (String entityName : map.getObjEntityMap().keySet()) {
                             classDescriptorMap.getDescriptor(entityName);
@@ -590,4 +593,11 @@
     public void setValueObjectTypeRegistry(ValueObjectTypeRegistry valueObjectTypeRegistry) {
         this.valueObjectTypeRegistry = valueObjectTypeRegistry;
     }
+
+    /**
+     * @since 4.2
+     */
+    public void setValueComparisionStrategyFactory(ValueComparisonStrategyFactory valueComparisonStrategyFactory) {
+        this.valueComparisonStrategyFactory = valueComparisonStrategyFactory;
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/ObjAttribute.java b/cayenne-server/src/main/java/org/apache/cayenne/map/ObjAttribute.java
index 9b5b929..4bc0778 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/ObjAttribute.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/ObjAttribute.java
@@ -36,6 +36,10 @@
 
     protected String type;
     protected boolean usedForLocking;
+    /**
+     * @since 4.2
+     */
+    protected boolean lazy;
     protected String dbAttributePath;
 
     public ObjAttribute() {
@@ -62,6 +66,7 @@
         setEntity(attribute.getEntity());
         setDbAttributePath(attribute.getDbAttributePath());
         setUsedForLocking(attribute.isUsedForLocking());
+        setLazy(attribute.isLazy());
     }
 
     @Override
@@ -104,6 +109,7 @@
                 .attribute("name", getName())
                 .attribute("type", getType())
                 .attribute("lock", isUsedForLocking())
+                .attribute("lazy", isLazy())
                 .attribute("db-attribute-path", getDbAttributePath());
 
         delegate.visitObjAttribute(this);
@@ -157,6 +163,22 @@
     }
 
     /**
+     * @return whether this attribute should be loaded lazily.
+     * @since 4.2
+     */
+    public boolean isLazy() {
+        return lazy;
+    }
+
+    /**
+     * Sets whether this attribute should be loaded lazily.
+     * @since 4.2
+     */
+    public void setLazy(boolean lazy) {
+        this.lazy = lazy;
+    }
+
+    /**
      * Returns a DbAttribute mapped by this ObjAttribute.
      */
     public DbAttribute getDbAttribute() {
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java
index f7b5a88..5ca45ef 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/map/SelectQueryDescriptor.java
@@ -23,22 +23,24 @@
 import java.util.List;
 import java.util.Map;
 
+import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.configuration.ConfigurationNodeVisitor;
 import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.Ordering;
 import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.util.XMLEncoder;
 
 /**
  * @since 4.0
- * @deprecated since 4.2
  */
-@Deprecated
 public class SelectQueryDescriptor extends QueryDescriptor {
 
 	private static final long serialVersionUID = -8798258795351950215L;
 
+    public static final String DISTINCT_PROPERTY = "cayenne.SelectQuery.distinct";
+    public static final boolean DISTINCT_DEFAULT = false;
+
 	protected Expression qualifier;
 
     protected List<Ordering> orderings = new ArrayList<>();
@@ -49,13 +51,12 @@
     }
 
     public void setDistinct(boolean value) {
-        setProperty(SelectQuery.DISTINCT_PROPERTY, String.valueOf(value));
+        setProperty(DISTINCT_PROPERTY, String.valueOf(value));
     }
 
     public boolean isDistinct() {
-        String distinct = getProperty(SelectQuery.DISTINCT_PROPERTY);
-
-        return distinct != null ? Boolean.valueOf(distinct) : false;
+        String distinct = getProperty(DISTINCT_PROPERTY);
+        return distinct != null ? Boolean.parseBoolean(distinct) : DISTINCT_DEFAULT;
     }
 
     /**
@@ -167,27 +168,34 @@
     }
 
     @Override
-    public SelectQuery<?> buildQuery() {
-        SelectQuery<Object> selectQuery = new SelectQuery<>();
-        selectQuery.setRoot(this.getRoot());
-        selectQuery.setQualifier(this.getQualifier());
+    public ObjectSelect<?> buildQuery() {
+        // resolve root
+        Object root = getRoot();
+        String rootEntityName;
+        if(root instanceof ObjEntity) {
+            rootEntityName = ((ObjEntity) root).getName();
+        } else if(root instanceof String) {
+            rootEntityName = (String)root;
+        } else {
+            throw new CayenneRuntimeException("Unexpected root for the SelectQueryDescriptor '%s'.", root);
+        }
+
+        ObjectSelect<?> query = ObjectSelect.query(Object.class, getQualifier());
+        query.entityName(rootEntityName);
+        query.setRoot(root);
 
         List<Ordering> orderings = this.getOrderings();
-
         if (orderings != null && !orderings.isEmpty()) {
-            selectQuery.addOrderings(orderings);
+            query.orderBy(orderings);
         }
 
         if (prefetchesMap != null) {
-            for (Map.Entry<String, Integer> entry : prefetchesMap.entrySet()) {
-                selectQuery.addPrefetch(PrefetchTreeNode.withPath(entry.getKey(), entry.getValue()));
-            }
+            prefetchesMap.forEach(query::prefetch);
         }
 
-        // init properties
-        selectQuery.initWithProperties(this.getProperties());
-
-        return selectQuery;
+        // TODO: apply DISTINCT property
+        query.initWithProperties(this.getProperties());
+        return query;
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/BatchQueryRow.java b/cayenne-server/src/main/java/org/apache/cayenne/query/BatchQueryRow.java
index 67136c4..0bab779 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/BatchQueryRow.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/BatchQueryRow.java
@@ -32,6 +32,8 @@
  */
 public abstract class BatchQueryRow {
 
+    private static final int MAX_NESTED_SUPPLIER_LEVEL = 1000;
+
     protected ObjectId objectId;
     protected Map<String, Object> qualifier;
 
@@ -57,11 +59,24 @@
     protected Object getValue(Map<String, Object> valueMap, DbAttribute attribute) {
 
         Object value = valueMap.get(attribute.getName());
+        boolean isSupplier = false;
+        int safeguard = 0;
 
-        // if a value is a Factory, resolve it here...
-        // slight chance that a normal value will implement Factory interface???
-        if (value instanceof Supplier) {
+        // Supplier can be nested, resolve all the way down
+        while(value instanceof Supplier && safeguard < MAX_NESTED_SUPPLIER_LEVEL) {
             value = ((Supplier) value).get();
+            isSupplier = true;
+            safeguard++;
+        }
+
+        // simple guard from recursive Suppliers
+        if(safeguard == MAX_NESTED_SUPPLIER_LEVEL) {
+            throw new CayenneRuntimeException("Possible recursive supplier chain for batch row value, object %s, attribute %s"
+                    , objectId, attribute.getName());
+        }
+
+        // if a value is a Supplier, resolve it here...
+        if (isSupplier) {
             valueMap.put(attribute.getName(), value);
 
             // update replacement id
@@ -74,8 +89,7 @@
 
                 ObjectId id = getObjectId();
                 if (id != null) {
-                    // always override with fresh value as this is what's in the
-                    // DB
+                    // always override with fresh value as this is what's in the DB
                     id.getReplacementIdMap().put(attribute.getName(), value);
                 }
             }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
index d11c491..f886732 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/FluentSelect.java
@@ -21,6 +21,7 @@
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectContext;
@@ -214,4 +215,11 @@
     public boolean isDistinct() {
         return false;
     }
+
+    /**
+     * @since 4.2
+     */
+    public void initWithProperties(Map<String, String> properties) {
+        getBaseMetaData().initWithProperties(properties);
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/MappedSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/MappedSelect.java
index be7a378..18e5f66 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/MappedSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/MappedSelect.java
@@ -199,18 +199,18 @@
 
         switch (descriptor.getType()) {
             case QueryDescriptor.SELECT_QUERY:
-                SelectQuery selectQuery = (SelectQuery) query;
+                ObjectSelect<?> selectQuery = (ObjectSelect<?>) query;
                 if (fetchLimit != null) {
-                    selectQuery.setFetchLimit(fetchLimit);
+                    selectQuery.limit(fetchLimit);
                 }
                 if (fetchOffset != null) {
-                    selectQuery.setFetchOffset(fetchOffset);
+                    selectQuery.offset(fetchOffset);
                 }
                 if (statementFetchSize != null) {
-                    selectQuery.setStatementFetchSize(statementFetchSize);
+                    selectQuery.statementFetchSize(statementFetchSize);
                 }
                 if (pageSize != null) {
-                    selectQuery.setPageSize(pageSize);
+                    selectQuery.pageSize(pageSize);
                 }
                 if (cacheStrategyOverride != null) {
                     selectQuery.setCacheStrategy(cacheStrategyOverride);
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
index d20b5d8..6c728a1 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/query/ObjectSelect.java
@@ -24,6 +24,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 
 import org.apache.cayenne.DataRow;
 import org.apache.cayenne.ObjectContext;
@@ -53,7 +54,7 @@
  *
  * @since 4.0
  */
-public class ObjectSelect<T> extends FluentSelect<T> {
+public class ObjectSelect<T> extends FluentSelect<T> implements ParameterizedQuery {
 
     private static final long serialVersionUID = -156124021150949227L;
 
@@ -692,4 +693,21 @@
     protected BaseQueryMetadata getBaseMetaData() {
         return metaData;
     }
+
+    /**
+     * This method is intended for internal use in a {@link MappedSelect}.
+     *
+     * @param parameters to apply
+     * @return this query with parameters applied to the <b>where</b> qualifier
+     *
+     * @since 4.2
+     */
+    @Override
+    public Query createQuery(Map<String, ?> parameters) {
+        if(where == null) {
+            return this;
+        }
+        where = where.params(parameters, true);
+        return this;
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java
index 24d83ae..f7b9a32 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PersistentDescriptor.java
@@ -44,9 +44,9 @@
  */
 public class PersistentDescriptor implements ClassDescriptor {
 
-	static final Integer TRANSIENT_STATE = Integer.valueOf(PersistenceState.TRANSIENT);
-	static final Integer HOLLOW_STATE = Integer.valueOf(PersistenceState.HOLLOW);
-	static final Integer COMMITTED_STATE = Integer.valueOf(PersistenceState.COMMITTED);
+	static final Integer TRANSIENT_STATE = PersistenceState.TRANSIENT;
+	static final Integer HOLLOW_STATE = PersistenceState.HOLLOW;
+	static final Integer COMMITTED_STATE = PersistenceState.COMMITTED;
 
 	protected ClassDescriptor superclassDescriptor;
 
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PropertyDescriptor.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PropertyDescriptor.java
index b9c9f35..90799a7 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/PropertyDescriptor.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/PropertyDescriptor.java
@@ -19,6 +19,8 @@
 
 package org.apache.cayenne.reflect;
 
+import org.apache.cayenne.util.Util;
+
 /**
  * Defines bean property API used by Cayenne to access object data, do faulting
  * and graph maintenance tasks.
@@ -75,4 +77,19 @@
      */
     void injectValueHolder(Object object) throws PropertyException;
 
+    /**
+     * Allows to use special logic to compare values for equality
+     * as in rare cases it is not sufficient to use the default equals() method.
+     * Default implementation uses {@link Util#nullSafeEquals(Object, Object)} method.
+     *
+     * @param value1 to compare
+     * @param value2 to compare
+     * @return true if given values are equal
+     *
+     * @since 4.2
+     */
+    default boolean equals(Object value1, Object value2) {
+        return Util.nullSafeEquals(value1, value2);
+    }
+
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectAttributeProperty.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectAttributeProperty.java
index e5d85ae..1ee9468 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectAttributeProperty.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectAttributeProperty.java
@@ -26,10 +26,16 @@
 class DataObjectAttributeProperty extends DataObjectBaseProperty implements
         AttributeProperty {
 
-    protected ObjAttribute attribute;
+    protected final ObjAttribute attribute;
 
-    public DataObjectAttributeProperty(ObjAttribute attribute) {
+    /**
+     * @since 4.2
+     */
+    protected final ValueComparisonStrategy<Object> valueComparisonStrategy;
+
+    public DataObjectAttributeProperty(ObjAttribute attribute, ValueComparisonStrategy<Object> valueComparisonStrategy) {
         this.attribute = attribute;
+        this.valueComparisonStrategy = valueComparisonStrategy;
     }
 
     @Override
@@ -49,4 +55,9 @@
     public boolean visit(PropertyVisitor visitor) {
         return visitor.visitAttribute(this);
     }
+
+    @Override
+    public boolean equals(Object value1, Object value2) {
+        return valueComparisonStrategy.equals(value1, value2);
+    }
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory.java
index d86f6a2..ea86196 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory.java
@@ -41,10 +41,14 @@
 
     protected FaultFactory faultFactory;
 
+    protected ValueComparisonStrategyFactory valueComparisonStrategyFactory;
+
     public DataObjectDescriptorFactory(ClassDescriptorMap descriptorMap,
-            FaultFactory faultFactory) {
+                                       FaultFactory faultFactory,
+                                       ValueComparisonStrategyFactory valueComparisonStrategyFactory) {
         super(descriptorMap);
         this.faultFactory = faultFactory;
+        this.valueComparisonStrategyFactory = valueComparisonStrategyFactory;
     }
 
     @Override
@@ -65,7 +69,9 @@
     protected void createAttributeProperty(
             PersistentDescriptor descriptor,
             ObjAttribute attribute) {
-        descriptor.addDeclaredProperty(new DataObjectAttributeProperty(attribute));
+        DataObjectAttributeProperty property
+                = new DataObjectAttributeProperty(attribute, valueComparisonStrategyFactory.getStrategy(attribute));
+        descriptor.addDeclaredProperty(property);
     }
 
     @Override
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DefaultValueComparisonStrategyFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DefaultValueComparisonStrategyFactory.java
new file mode 100644
index 0000000..0aadffc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/DefaultValueComparisonStrategyFactory.java
@@ -0,0 +1,76 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.reflect.generic;
+
+import org.apache.cayenne.access.types.ValueObjectType;
+import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.ObjAttribute;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * @since 4.2
+ */
+public class DefaultValueComparisonStrategyFactory implements ValueComparisonStrategyFactory {
+
+    private static final ValueComparisonStrategy<Object> DEFAULT_STRATEGY = new DefaultValueComparisonStrategy();
+
+    private final ValueObjectTypeRegistry valueObjectTypeRegistry;
+
+    public DefaultValueComparisonStrategyFactory(@Inject ValueObjectTypeRegistry valueObjectTypeRegistry) {
+        this.valueObjectTypeRegistry = valueObjectTypeRegistry;
+    }
+
+    @Override
+    public ValueComparisonStrategy<Object> getStrategy(ObjAttribute attribute) {
+        ValueObjectType<?, ?> valueObjectType = valueObjectTypeRegistry.getValueType(attribute.getJavaClass());
+        if(valueObjectType == null) {
+            return DEFAULT_STRATEGY;
+        } else {
+            return new ValueObjectTypeComparisonStrategy(valueObjectType);
+        }
+    }
+
+    // Using classes instead of lambdas to allow serialization
+
+    @SuppressWarnings({"rawtypes"})
+    static class ValueObjectTypeComparisonStrategy implements ValueComparisonStrategy<Object>, Serializable {
+        private final ValueObjectType valueObjectType;
+
+        public ValueObjectTypeComparisonStrategy(ValueObjectType<?, ?> valueObjectType) {
+            this.valueObjectType = valueObjectType;
+        }
+
+        @SuppressWarnings("unchecked")
+        @Override
+        public boolean equals(Object value1, Object value2) {
+            return valueObjectType.equals(value1, value2);
+        }
+    }
+
+    static class DefaultValueComparisonStrategy implements ValueComparisonStrategy<Object>, Serializable {
+        @Override
+        public boolean equals(Object a, Object b) {
+            return Objects.equals(a, b);
+        }
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/ValueComparisonStrategy.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/ValueComparisonStrategy.java
new file mode 100644
index 0000000..5a09859
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/ValueComparisonStrategy.java
@@ -0,0 +1,30 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.reflect.generic;
+
+/**
+ * @since 4.2
+ * @param <V> type of values to compare
+ */
+public interface ValueComparisonStrategy<V> {
+
+    boolean equals(V value1, V value2);
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/ValueComparisonStrategyFactory.java b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/ValueComparisonStrategyFactory.java
new file mode 100644
index 0000000..2054dfc
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/reflect/generic/ValueComparisonStrategyFactory.java
@@ -0,0 +1,31 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.reflect.generic;
+
+import org.apache.cayenne.map.ObjAttribute;
+
+/**
+ * @since 4.2
+ */
+public interface ValueComparisonStrategyFactory {
+
+    ValueComparisonStrategy<Object> getStrategy(ObjAttribute attribute);
+
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/JJTSQLTemplateParserState.java b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/JJTSQLTemplateParserState.java
index 747abd3..d180f13 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/JJTSQLTemplateParserState.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/JJTSQLTemplateParserState.java
@@ -26,115 +26,119 @@
  * @since 4.1
  */
 public class JJTSQLTemplateParserState {
-    private List<Node> nodes;
-    private List<Integer> marks;
+  private List<Node> nodes;
+  private List<Integer> marks;
 
-    private int sp;        // number of nodes on stack
-    private int mk;        // current mark
-    private boolean node_created;
+  private int sp;        // number of nodes on stack
+  private int mk;        // current mark
+  private boolean node_created;
 
-    public JJTSQLTemplateParserState() {
-        nodes = new ArrayList<>();
-        marks = new ArrayList<>();
-        sp = 0;
-        mk = 0;
+  public JJTSQLTemplateParserState() {
+    nodes = new ArrayList<>();
+    marks = new ArrayList<>();
+    sp = 0;
+    mk = 0;
+  }
+
+  /* Determines whether the current node was actually closed and
+     pushed.  This should only be called in the final user action of a
+     node scope.  */
+  public boolean nodeCreated() {
+    return node_created;
+  }
+
+  /* Call this to reinitialize the node stack.  It is called
+     automatically by the parser's ReInit() method. */
+  public void reset() {
+    nodes.clear();
+    marks.clear();
+    sp = 0;
+    mk = 0;
+  }
+
+  /* Returns the root node of the AST.  It only makes sense to call
+     this after a successful parse. */
+  public Node rootNode() {
+    return nodes.get(0);
+  }
+
+  /* Pushes a node on to the stack. */
+  public void pushNode(Node n) {
+    nodes.add(n);
+    ++sp;
+  }
+
+  /* Returns the node on the top of the stack, and remove it from the
+     stack.  */
+  public Node popNode() {
+    if (--sp < mk) {
+      mk = marks.remove(marks.size()-1);
     }
+    return nodes.remove(nodes.size()-1);
+  }
 
-    /* Determines whether the current node was actually closed and
-       pushed.  This should only be called in the final user action of a
-       node scope.  */
-    public boolean nodeCreated() {
-        return node_created;
+  /* Returns the node currently on the top of the stack. */
+  public Node peekNode() {
+    return nodes.get(nodes.size()-1);
+  }
+
+  /* Returns the number of children on the stack in the current node
+     scope. */
+  public int nodeArity() {
+    return sp - mk;
+  }
+
+
+  public void clearNodeScope(Node n) {
+    while (sp > mk) {
+      popNode();
     }
+    mk = marks.remove(marks.size()-1);
+  }
 
-    /* Call this to reinitialize the node stack.  It is called
-       automatically by the parser's ReInit() method. */
-    public void reset() {
-        nodes.clear();
-        marks.clear();
-        sp = 0;
-        mk = 0;
+
+  public void openNodeScope(Node n) {
+    marks.add(mk);
+    mk = sp;
+  }
+
+
+  /* A definite node is constructed from a specified number of
+     children.  That number of nodes are popped from the stack and
+     made the children of the definite node.  Then the definite node
+     is pushed on to the stack. */
+  public void closeNodeScope(Node n, int num) {
+    mk = marks.remove(marks.size()-1);
+    while (num-- > 0) {
+      Node c = popNode();
+      c.jjtSetParent(n);
+      n.jjtAddChild(c, num);
     }
+    pushNode(n);
+    node_created = true;
+  }
 
-    /* Returns the root node of the AST.  It only makes sense to call
-       this after a successful parse. */
-    public Node rootNode() {
-        return nodes.get(0);
+
+  /* A conditional node is constructed if its condition is true.  All
+     the nodes that have been pushed since the node was opened are
+     made children of the conditional node, which is then pushed
+     on to the stack.  If the condition is false the node is not
+     constructed and they are left on the stack. */
+  public void closeNodeScope(Node n, boolean condition) {
+    if (condition) {
+      int a = nodeArity();
+      mk = marks.remove(marks.size()-1);
+      while (a-- > 0) {
+        Node c = popNode();
+        c.jjtSetParent(n);
+        n.jjtAddChild(c, a);
+      }
+      pushNode(n);
+      node_created = true;
+    } else {
+      mk = marks.remove(marks.size()-1);
+      node_created = false;
     }
-
-    /* Pushes a node on to the stack. */
-    public void pushNode(Node n) {
-        nodes.add(n);
-        ++sp;
-    }
-
-    /* Returns the node on the top of the stack, and remove it from the
-       stack.  */
-    public Node popNode() {
-        if (--sp < mk) {
-            mk = marks.remove(marks.size() - 1);
-        }
-        return nodes.remove(nodes.size() - 1);
-    }
-
-    /* Returns the node currently on the top of the stack. */
-    public Node peekNode() {
-        return nodes.get(nodes.size() - 1);
-    }
-
-    /* Returns the number of children on the stack in the current node
-       scope. */
-    public int nodeArity() {
-        return sp - mk;
-    }
-
-    public void clearNodeScope(Node n) {
-        while (sp > mk) {
-            popNode();
-        }
-        mk = marks.remove(marks.size() - 1);
-    }
-
-    public void openNodeScope(Node n) {
-        marks.add(mk);
-        mk = sp;
-    }
-
-    /* A definite node is constructed from a specified number of
-       children.  That number of nodes are popped from the stack and
-       made the children of the definite node.  Then the definite node
-       is pushed on to the stack. */
-    public void closeNodeScope(Node n, int num) {
-        mk = marks.remove(marks.size() - 1);
-        while (num-- > 0) {
-            Node c = popNode();
-            c.jjtSetParent(n);
-            n.jjtAddChild(c, num);
-        }
-        pushNode(n);
-        node_created = true;
-    }
-
-
-    /* A conditional node is constructed if its condition is true.  All
-       the nodes that have been pushed since the node was opened are
-       made children of the conditional node, which is then pushed
-       on to the stack.  If the condition is false the node is not
-       constructed and they are left on the stack. */
-    public void closeNodeScope(Node n, boolean condition) {
-        if (condition) {
-            int a = nodeArity();
-            mk = marks.remove(marks.size() - 1);
-            while (a-- > 0) {
-                Node c = popNode();
-                c.jjtSetParent(n);
-                n.jjtAddChild(c, a);
-            }
-            pushNode(n);
-            node_created = true;
-        } else {
-            mk = marks.remove(marks.size() - 1);
-            node_created = false;
-        }
-    }
+  }
 }
+/* JavaCC - OriginalChecksum=ab07603ac74783740fe93ab7d678d910 (do not edit this line) */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParser.java b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParser.java
index cfbefe3..24a12c9 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParser.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParser.java
@@ -34,47 +34,51 @@
   final public Node template() throws ParseException {
     block();
     jj_consume_token(0);
-        {if (true) return (ASTBlock) jjtree.rootNode();}
+{if ("" != null) return (ASTBlock) jjtree.rootNode();}
     throw new Error("Missing return statement in function");
-  }
+}
 
 /*
     Top component of parsing tree
 */
-  final public void block() throws ParseException {
-                       /*@bgen(jjtree) Block */
+  final public void block() throws ParseException {/*@bgen(jjtree) Block */
   ASTBlock jjtn000 = new ASTBlock(JJTBLOCK);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
     try {
       label_1:
       while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
         case IF:
         case SHARP:
         case DOLLAR:
         case TEXT:
-        case TEXT_OTHER:
+        case TEXT_OTHER:{
           ;
           break;
+          }
         default:
           jj_la1[0] = jj_gen;
           break label_1;
         }
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case IF:
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case IF:{
           ifElse();
           break;
-        case SHARP:
+          }
+        case SHARP:{
           directive();
           break;
-        case DOLLAR:
+          }
+        case DOLLAR:{
           variable();
           break;
+          }
         case TEXT:
-        case TEXT_OTHER:
+        case TEXT_OTHER:{
           text();
           break;
+          }
         default:
           jj_la1[1] = jj_gen;
           jj_consume_token(-1);
@@ -82,7 +86,7 @@
         }
       }
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -96,48 +100,48 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     Plain text that is not processed in any way by render
 */
-  final public void text() throws ParseException {
-                     /*@bgen(jjtree) Text */
+  final public void text() throws ParseException {/*@bgen(jjtree) Text */
     ASTText jjtn000 = new ASTText(JJTTEXT);
     boolean jjtc000 = true;
     jjtree.openNodeScope(jjtn000);Token t;
     try {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case TEXT:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case TEXT:{
         t = jj_consume_token(TEXT);
         break;
-      case TEXT_OTHER:
+        }
+      case TEXT_OTHER:{
         t = jj_consume_token(TEXT_OTHER);
         break;
+        }
       default:
         jj_la1[2] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
-        jjtree.closeNodeScope(jjtn000, true);
+jjtree.closeNodeScope(jjtn000, true);
         jjtc000 = false;
-        jjtn000.setValue(t.image);
+jjtn000.setValue(t.image);
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     Condition directive: #if(condition) ...  #else ... #end
 */
-  final public void ifElse() throws ParseException {
-                         /*@bgen(jjtree) IfElse */
+  final public void ifElse() throws ParseException {/*@bgen(jjtree) IfElse */
   ASTIfElse jjtn000 = new ASTIfElse(JJTIFELSE);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
@@ -147,18 +151,19 @@
       expression();
       jj_consume_token(RBRACKET);
       block();
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-      case ELSE:
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case ELSE:{
         jj_consume_token(ELSE);
         block();
         break;
+        }
       default:
         jj_la1[3] = jj_gen;
         ;
       }
       jj_consume_token(END);
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -172,26 +177,25 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     Directive in form of #directiveName(args list)
 */
-  final public void directive() throws ParseException {
-                               /*@bgen(jjtree) Directive */
+  final public void directive() throws ParseException {/*@bgen(jjtree) Directive */
     ASTDirective jjtn000 = new ASTDirective(JJTDIRECTIVE);
     boolean jjtc000 = true;
     jjtree.openNodeScope(jjtn000);Token t;
     try {
       jj_consume_token(SHARP);
       t = jj_consume_token(IDENTIFIER);
-        jjtn000.setIdentifier(t.image);
+jjtn000.setIdentifier(t.image);
       jj_consume_token(LBRACKET);
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case DOLLAR:
       case TRUE:
       case FALSE:
@@ -199,11 +203,11 @@
       case SINGLE_QUOTED_STRING:
       case DOUBLE_QUOTED_STRING:
       case INT_LITERAL:
-      case FLOAT_LITERAL:
+      case FLOAT_LITERAL:{
         expression();
         label_2:
         while (true) {
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
           case DOLLAR:
           case TRUE:
           case FALSE:
@@ -212,17 +216,19 @@
           case SINGLE_QUOTED_STRING:
           case DOUBLE_QUOTED_STRING:
           case INT_LITERAL:
-          case FLOAT_LITERAL:
+          case FLOAT_LITERAL:{
             ;
             break;
+            }
           default:
             jj_la1[4] = jj_gen;
             break label_2;
           }
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case COMMA:
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+          case COMMA:{
             jj_consume_token(COMMA);
             break;
+            }
           default:
             jj_la1[5] = jj_gen;
             ;
@@ -230,13 +236,14 @@
           expression();
         }
         break;
+        }
       default:
         jj_la1[6] = jj_gen;
         ;
       }
       jj_consume_token(RBRACKET);
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -250,44 +257,46 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     valid expression in parameters of method or directive
     can be scalar, variable (with methods calls) or array
 */
-  final public void expression() throws ParseException {
-                                 /*@bgen(jjtree) Expression */
+  final public void expression() throws ParseException {/*@bgen(jjtree) Expression */
   ASTExpression jjtn000 = new ASTExpression(JJTEXPRESSION);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
     try {
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case TRUE:
       case FALSE:
       case SINGLE_QUOTED_STRING:
       case DOUBLE_QUOTED_STRING:
       case INT_LITERAL:
-      case FLOAT_LITERAL:
+      case FLOAT_LITERAL:{
         scalar();
         break;
-      case DOLLAR:
+        }
+      case DOLLAR:{
         variable();
         break;
-      case LSBRACKET:
+        }
+      case LSBRACKET:{
         array();
         break;
+        }
       default:
         jj_la1[7] = jj_gen;
         jj_consume_token(-1);
         throw new ParseException();
       }
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -301,11 +310,11 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     Single scalar value: String, long, double, boolean
@@ -314,131 +323,150 @@
     double: simple and exponential form
 */
   final public void scalar() throws ParseException {
-    switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-    case SINGLE_QUOTED_STRING:
+    switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+    case SINGLE_QUOTED_STRING:{
       jj_consume_token(SINGLE_QUOTED_STRING);
-                             ASTStringScalar jjtn001 = new ASTStringScalar(JJTSTRINGSCALAR);
+ASTStringScalar jjtn001 = new ASTStringScalar(JJTSTRINGSCALAR);
                              boolean jjtc001 = true;
                              jjtree.openNodeScope(jjtn001);
       try {
-                             jjtree.closeNodeScope(jjtn001,  0);
+jjtree.closeNodeScope(jjtn001,  0);
                              jjtc001 = false;
-                             jjtn001.setValue((String)token_source.literalValue);
+jjtn001.setValue((String)token_source.literalValue);
       } finally {
-                             if (jjtc001) {
+if (jjtc001) {
                                jjtree.closeNodeScope(jjtn001,  0);
                              }
       }
       break;
-    case DOUBLE_QUOTED_STRING:
+      }
+    case DOUBLE_QUOTED_STRING:{
       jj_consume_token(DOUBLE_QUOTED_STRING);
-                                 ASTStringScalar jjtn002 = new ASTStringScalar(JJTSTRINGSCALAR);
+ASTStringScalar jjtn002 = new ASTStringScalar(JJTSTRINGSCALAR);
                                  boolean jjtc002 = true;
                                  jjtree.openNodeScope(jjtn002);
       try {
-                                 jjtree.closeNodeScope(jjtn002,  0);
+jjtree.closeNodeScope(jjtn002,  0);
                                  jjtc002 = false;
-                                 jjtn002.setValue((String)token_source.literalValue);
+jjtn002.setValue((String)token_source.literalValue);
       } finally {
-                                 if (jjtc002) {
+if (jjtc002) {
                                    jjtree.closeNodeScope(jjtn002,  0);
                                  }
       }
       break;
-    case INT_LITERAL:
+      }
+    case INT_LITERAL:{
       jj_consume_token(INT_LITERAL);
-                          ASTIntScalar jjtn003 = new ASTIntScalar(JJTINTSCALAR);
+ASTIntScalar jjtn003 = new ASTIntScalar(JJTINTSCALAR);
                           boolean jjtc003 = true;
                           jjtree.openNodeScope(jjtn003);
       try {
-                          jjtree.closeNodeScope(jjtn003,  0);
+jjtree.closeNodeScope(jjtn003,  0);
                           jjtc003 = false;
-                          jjtn003.setValue((Long)token_source.literalValue);
+jjtn003.setValue((Long)token_source.literalValue);
       } finally {
-                          if (jjtc003) {
+if (jjtc003) {
                             jjtree.closeNodeScope(jjtn003,  0);
                           }
       }
       break;
-    case FLOAT_LITERAL:
+      }
+    case FLOAT_LITERAL:{
       jj_consume_token(FLOAT_LITERAL);
-                          ASTFloatScalar jjtn004 = new ASTFloatScalar(JJTFLOATSCALAR);
+ASTFloatScalar jjtn004 = new ASTFloatScalar(JJTFLOATSCALAR);
                           boolean jjtc004 = true;
                           jjtree.openNodeScope(jjtn004);
       try {
-                          jjtree.closeNodeScope(jjtn004,  0);
+jjtree.closeNodeScope(jjtn004,  0);
                           jjtc004 = false;
-                          jjtn004.setValue((Double)token_source.literalValue);
+jjtn004.setValue((Double)token_source.literalValue);
       } finally {
-                          if (jjtc004) {
+if (jjtc004) {
                             jjtree.closeNodeScope(jjtn004,  0);
                           }
       }
       break;
-    case TRUE:
+      }
+    case TRUE:{
       jj_consume_token(TRUE);
-                  ASTBoolScalar jjtn005 = new ASTBoolScalar(JJTBOOLSCALAR);
+ASTBoolScalar jjtn005 = new ASTBoolScalar(JJTBOOLSCALAR);
                   boolean jjtc005 = true;
                   jjtree.openNodeScope(jjtn005);
       try {
-                  jjtree.closeNodeScope(jjtn005,  0);
+jjtree.closeNodeScope(jjtn005,  0);
                   jjtc005 = false;
-                  jjtn005.setValue(true);
+jjtn005.setValue(true);
       } finally {
-                  if (jjtc005) {
+if (jjtc005) {
                     jjtree.closeNodeScope(jjtn005,  0);
                   }
       }
       break;
-    case FALSE:
+      }
+    case FALSE:{
       jj_consume_token(FALSE);
-                  ASTBoolScalar jjtn006 = new ASTBoolScalar(JJTBOOLSCALAR);
+ASTBoolScalar jjtn006 = new ASTBoolScalar(JJTBOOLSCALAR);
                   boolean jjtc006 = true;
                   jjtree.openNodeScope(jjtn006);
       try {
-                  jjtree.closeNodeScope(jjtn006,  0);
+jjtree.closeNodeScope(jjtn006,  0);
                   jjtc006 = false;
-                  jjtn006.setValue(false);
+jjtn006.setValue(false);
       } finally {
-                  if (jjtc006) {
+if (jjtc006) {
                     jjtree.closeNodeScope(jjtn006,  0);
                   }
       }
       break;
+      }
     default:
       jj_la1[8] = jj_gen;
       jj_consume_token(-1);
       throw new ParseException();
     }
-  }
+}
 
 /*
     Variable, optionally with some methods calls
     $a or $a.method() or $a.method1().method2()
 */
-  final public void variable() throws ParseException {
-                             /*@bgen(jjtree) Variable */
+  final public void variable() throws ParseException {/*@bgen(jjtree) Variable */
     ASTVariable jjtn000 = new ASTVariable(JJTVARIABLE);
     boolean jjtc000 = true;
     jjtree.openNodeScope(jjtn000);Token t;
     try {
       jj_consume_token(DOLLAR);
-      t = jj_consume_token(IDENTIFIER);
-        jjtn000.setIdentifier(t.image);
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+      case IDENTIFIER:{
+        t = jj_consume_token(IDENTIFIER);
+        break;
+        }
+      case TEXT_OTHER:{
+        t = jj_consume_token(TEXT_OTHER);
+        break;
+        }
+      default:
+        jj_la1[9] = jj_gen;
+        jj_consume_token(-1);
+        throw new ParseException();
+      }
+jjtn000.setIdentifier(t.image);
       label_3:
       while (true) {
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-        case DOT:
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+        case DOT:{
           ;
           break;
+          }
         default:
-          jj_la1[9] = jj_gen;
+          jj_la1[10] = jj_gen;
           break label_3;
         }
         method();
       }
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -452,27 +480,26 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     Method call, valid only as part of variable, can be chain of methods
     $a.method1($var).method2().method3('val')
 */
-  final public void method() throws ParseException {
-                         /*@bgen(jjtree) Method */
+  final public void method() throws ParseException {/*@bgen(jjtree) Method */
     ASTMethod jjtn000 = new ASTMethod(JJTMETHOD);
     boolean jjtc000 = true;
     jjtree.openNodeScope(jjtn000);Token t;
     try {
       jj_consume_token(DOT);
       t = jj_consume_token(IDENTIFIER);
-        jjtn000.setIdentifier(t.image);
+jjtn000.setIdentifier(t.image);
       jj_consume_token(LBRACKET);
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case DOLLAR:
       case TRUE:
       case FALSE:
@@ -480,11 +507,11 @@
       case SINGLE_QUOTED_STRING:
       case DOUBLE_QUOTED_STRING:
       case INT_LITERAL:
-      case FLOAT_LITERAL:
+      case FLOAT_LITERAL:{
         expression();
         label_4:
         while (true) {
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
           case DOLLAR:
           case TRUE:
           case FALSE:
@@ -493,31 +520,34 @@
           case SINGLE_QUOTED_STRING:
           case DOUBLE_QUOTED_STRING:
           case INT_LITERAL:
-          case FLOAT_LITERAL:
+          case FLOAT_LITERAL:{
             ;
             break;
-          default:
-            jj_la1[10] = jj_gen;
-            break label_4;
-          }
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case COMMA:
-            jj_consume_token(COMMA);
-            break;
+            }
           default:
             jj_la1[11] = jj_gen;
+            break label_4;
+          }
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+          case COMMA:{
+            jj_consume_token(COMMA);
+            break;
+            }
+          default:
+            jj_la1[12] = jj_gen;
             ;
           }
           expression();
         }
         break;
+        }
       default:
-        jj_la1[12] = jj_gen;
+        jj_la1[13] = jj_gen;
         ;
       }
       jj_consume_token(RBRACKET);
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -531,51 +561,52 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
 /*
     Comma or space separated array of scalars and/or variables
     valid values: [], ['a' 5], [$a, 'b', 5]
 */
-  final public void array() throws ParseException {
-                       /*@bgen(jjtree) Array */
+  final public void array() throws ParseException {/*@bgen(jjtree) Array */
   ASTArray jjtn000 = new ASTArray(JJTARRAY);
   boolean jjtc000 = true;
   jjtree.openNodeScope(jjtn000);
     try {
       jj_consume_token(LSBRACKET);
-      switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
       case DOLLAR:
       case TRUE:
       case FALSE:
       case SINGLE_QUOTED_STRING:
       case DOUBLE_QUOTED_STRING:
       case INT_LITERAL:
-      case FLOAT_LITERAL:
-        switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+      case FLOAT_LITERAL:{
+        switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
         case TRUE:
         case FALSE:
         case SINGLE_QUOTED_STRING:
         case DOUBLE_QUOTED_STRING:
         case INT_LITERAL:
-        case FLOAT_LITERAL:
+        case FLOAT_LITERAL:{
           scalar();
           break;
-        case DOLLAR:
+          }
+        case DOLLAR:{
           variable();
           break;
+          }
         default:
-          jj_la1[13] = jj_gen;
+          jj_la1[14] = jj_gen;
           jj_consume_token(-1);
           throw new ParseException();
         }
         label_5:
         while (true) {
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
           case DOLLAR:
           case TRUE:
           case FALSE:
@@ -583,47 +614,52 @@
           case SINGLE_QUOTED_STRING:
           case DOUBLE_QUOTED_STRING:
           case INT_LITERAL:
-          case FLOAT_LITERAL:
+          case FLOAT_LITERAL:{
             ;
             break;
-          default:
-            jj_la1[14] = jj_gen;
-            break label_5;
-          }
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
-          case COMMA:
-            jj_consume_token(COMMA);
-            break;
+            }
           default:
             jj_la1[15] = jj_gen;
+            break label_5;
+          }
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
+          case COMMA:{
+            jj_consume_token(COMMA);
+            break;
+            }
+          default:
+            jj_la1[16] = jj_gen;
             ;
           }
-          switch ((jj_ntk==-1)?jj_ntk():jj_ntk) {
+          switch ((jj_ntk==-1)?jj_ntk_f():jj_ntk) {
           case TRUE:
           case FALSE:
           case SINGLE_QUOTED_STRING:
           case DOUBLE_QUOTED_STRING:
           case INT_LITERAL:
-          case FLOAT_LITERAL:
+          case FLOAT_LITERAL:{
             scalar();
             break;
-          case DOLLAR:
+            }
+          case DOLLAR:{
             variable();
             break;
+            }
           default:
-            jj_la1[16] = jj_gen;
+            jj_la1[17] = jj_gen;
             jj_consume_token(-1);
             throw new ParseException();
           }
         }
         break;
+        }
       default:
-        jj_la1[17] = jj_gen;
+        jj_la1[18] = jj_gen;
         ;
       }
       jj_consume_token(RSBRACKET);
     } catch (Throwable jjte000) {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.clearNodeScope(jjtn000);
         jjtc000 = false;
       } else {
@@ -637,11 +673,11 @@
       }
       {if (true) throw (Error)jjte000;}
     } finally {
-      if (jjtc000) {
+if (jjtc000) {
         jjtree.closeNodeScope(jjtn000, true);
       }
     }
-  }
+}
 
   /** Generated Token Manager. */
   public SQLTemplateParserTokenManager token_source;
@@ -652,128 +688,136 @@
   public Token jj_nt;
   private int jj_ntk;
   private int jj_gen;
-  final private int[] jj_la1 = new int[18];
+  final private int[] jj_la1 = new int[19];
   static private int[] jj_la1_0;
   static private int[] jj_la1_1;
   static {
-      jj_la1_init_0();
-      jj_la1_init_1();
-   }
-   private static void jj_la1_init_0() {
-      jj_la1_0 = new int[] {0x320,0x320,0x0,0x40,0x90006e00,0x2000,0x90004e00,0x90004e00,0x90000c00,0x20000,0x90006e00,0x2000,0x90004e00,0x90000e00,0x90002e00,0x2000,0x90000e00,0x90000e00,};
-   }
-   private static void jj_la1_init_1() {
-      jj_la1_1 = new int[] {0xc0,0xc0,0xc0,0x0,0x3,0x0,0x3,0x3,0x3,0x0,0x3,0x0,0x3,0x3,0x3,0x0,0x3,0x3,};
-   }
+	   jj_la1_init_0();
+	   jj_la1_init_1();
+	}
+	private static void jj_la1_init_0() {
+	   jj_la1_0 = new int[] {0x320,0x320,0x0,0x40,0x90006e00,0x2000,0x90004e00,0x90004e00,0x90000c00,0x40000,0x20000,0x90006e00,0x2000,0x90004e00,0x90000e00,0x90002e00,0x2000,0x90000e00,0x90000e00,};
+	}
+	private static void jj_la1_init_1() {
+	   jj_la1_1 = new int[] {0xc0,0xc0,0xc0,0x0,0x3,0x0,0x3,0x3,0x3,0x80,0x0,0x3,0x0,0x3,0x3,0x3,0x0,0x3,0x3,};
+	}
 
   /** Constructor with InputStream. */
   public SQLTemplateParser(java.io.InputStream stream) {
-     this(stream, null);
+	  this(stream, null);
   }
   /** Constructor with InputStream and supplied encoding */
   public SQLTemplateParser(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream = new JavaCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source = new SQLTemplateParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 18; i++) jj_la1[i] = -1;
+	 try { jj_input_stream = new JavaCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+	 token_source = new SQLTemplateParserTokenManager(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jj_gen = 0;
+	 for (int i = 0; i < 19; i++) jj_la1[i] = -1;
   }
 
   /** Reinitialise. */
   public void ReInit(java.io.InputStream stream) {
-     ReInit(stream, null);
+	  ReInit(stream, null);
   }
   /** Reinitialise. */
   public void ReInit(java.io.InputStream stream, String encoding) {
-    try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 18; i++) jj_la1[i] = -1;
+	 try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); }
+	 token_source.ReInit(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jjtree.reset();
+	 jj_gen = 0;
+	 for (int i = 0; i < 19; i++) jj_la1[i] = -1;
   }
 
   /** Constructor. */
   public SQLTemplateParser(java.io.Reader stream) {
-    jj_input_stream = new JavaCharStream(stream, 1, 1);
-    token_source = new SQLTemplateParserTokenManager(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 18; i++) jj_la1[i] = -1;
+	 jj_input_stream = new JavaCharStream(stream, 1, 1);
+	 token_source = new SQLTemplateParserTokenManager(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jj_gen = 0;
+	 for (int i = 0; i < 19; i++) jj_la1[i] = -1;
   }
 
   /** Reinitialise. */
   public void ReInit(java.io.Reader stream) {
-    jj_input_stream.ReInit(stream, 1, 1);
-    token_source.ReInit(jj_input_stream);
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 18; i++) jj_la1[i] = -1;
+	if (jj_input_stream == null) {
+	   jj_input_stream = new JavaCharStream(stream, 1, 1);
+	} else {
+	   jj_input_stream.ReInit(stream, 1, 1);
+	}
+	if (token_source == null) {
+ token_source = new SQLTemplateParserTokenManager(jj_input_stream);
+	}
+
+	 token_source.ReInit(jj_input_stream);
+	 token = new Token();
+	 jj_ntk = -1;
+	 jjtree.reset();
+	 jj_gen = 0;
+	 for (int i = 0; i < 19; i++) jj_la1[i] = -1;
   }
 
   /** Constructor with generated Token Manager. */
   public SQLTemplateParser(SQLTemplateParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jj_gen = 0;
-    for (int i = 0; i < 18; i++) jj_la1[i] = -1;
+	 token_source = tm;
+	 token = new Token();
+	 jj_ntk = -1;
+	 jj_gen = 0;
+	 for (int i = 0; i < 19; i++) jj_la1[i] = -1;
   }
 
   /** Reinitialise. */
   public void ReInit(SQLTemplateParserTokenManager tm) {
-    token_source = tm;
-    token = new Token();
-    jj_ntk = -1;
-    jjtree.reset();
-    jj_gen = 0;
-    for (int i = 0; i < 18; i++) jj_la1[i] = -1;
+	 token_source = tm;
+	 token = new Token();
+	 jj_ntk = -1;
+	 jjtree.reset();
+	 jj_gen = 0;
+	 for (int i = 0; i < 19; i++) jj_la1[i] = -1;
   }
 
   private Token jj_consume_token(int kind) throws ParseException {
-    Token oldToken;
-    if ((oldToken = token).next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    if (token.kind == kind) {
-      jj_gen++;
-      return token;
-    }
-    token = oldToken;
-    jj_kind = kind;
-    throw generateParseException();
+	 Token oldToken;
+	 if ((oldToken = token).next != null) token = token.next;
+	 else token = token.next = token_source.getNextToken();
+	 jj_ntk = -1;
+	 if (token.kind == kind) {
+	   jj_gen++;
+	   return token;
+	 }
+	 token = oldToken;
+	 jj_kind = kind;
+	 throw generateParseException();
   }
 
 
 /** Get the next Token. */
   final public Token getNextToken() {
-    if (token.next != null) token = token.next;
-    else token = token.next = token_source.getNextToken();
-    jj_ntk = -1;
-    jj_gen++;
-    return token;
+	 if (token.next != null) token = token.next;
+	 else token = token.next = token_source.getNextToken();
+	 jj_ntk = -1;
+	 jj_gen++;
+	 return token;
   }
 
 /** Get the specific Token. */
   final public Token getToken(int index) {
-    Token t = token;
-    for (int i = 0; i < index; i++) {
-      if (t.next != null) t = t.next;
-      else t = t.next = token_source.getNextToken();
-    }
-    return t;
+	 Token t = token;
+	 for (int i = 0; i < index; i++) {
+	   if (t.next != null) t = t.next;
+	   else t = t.next = token_source.getNextToken();
+	 }
+	 return t;
   }
 
-  private int jj_ntk() {
-    if ((jj_nt=token.next) == null)
-      return (jj_ntk = (token.next=token_source.getNextToken()).kind);
-    else
-      return (jj_ntk = jj_nt.kind);
+  private int jj_ntk_f() {
+	 if ((jj_nt=token.next) == null)
+	   return (jj_ntk = (token.next=token_source.getNextToken()).kind);
+	 else
+	   return (jj_ntk = jj_nt.kind);
   }
 
   private java.util.List<int[]> jj_expentries = new java.util.ArrayList<int[]>();
@@ -782,36 +826,43 @@
 
   /** Generate ParseException. */
   public ParseException generateParseException() {
-    jj_expentries.clear();
-    boolean[] la1tokens = new boolean[40];
-    if (jj_kind >= 0) {
-      la1tokens[jj_kind] = true;
-      jj_kind = -1;
-    }
-    for (int i = 0; i < 18; i++) {
-      if (jj_la1[i] == jj_gen) {
-        for (int j = 0; j < 32; j++) {
-          if ((jj_la1_0[i] & (1<<j)) != 0) {
-            la1tokens[j] = true;
-          }
-          if ((jj_la1_1[i] & (1<<j)) != 0) {
-            la1tokens[32+j] = true;
-          }
-        }
-      }
-    }
-    for (int i = 0; i < 40; i++) {
-      if (la1tokens[i]) {
-        jj_expentry = new int[1];
-        jj_expentry[0] = i;
-        jj_expentries.add(jj_expentry);
-      }
-    }
-    int[][] exptokseq = new int[jj_expentries.size()][];
-    for (int i = 0; i < jj_expentries.size(); i++) {
-      exptokseq[i] = jj_expentries.get(i);
-    }
-    return new ParseException(token, exptokseq, tokenImage);
+	 jj_expentries.clear();
+	 boolean[] la1tokens = new boolean[40];
+	 if (jj_kind >= 0) {
+	   la1tokens[jj_kind] = true;
+	   jj_kind = -1;
+	 }
+	 for (int i = 0; i < 19; i++) {
+	   if (jj_la1[i] == jj_gen) {
+		 for (int j = 0; j < 32; j++) {
+		   if ((jj_la1_0[i] & (1<<j)) != 0) {
+			 la1tokens[j] = true;
+		   }
+		   if ((jj_la1_1[i] & (1<<j)) != 0) {
+			 la1tokens[32+j] = true;
+		   }
+		 }
+	   }
+	 }
+	 for (int i = 0; i < 40; i++) {
+	   if (la1tokens[i]) {
+		 jj_expentry = new int[1];
+		 jj_expentry[0] = i;
+		 jj_expentries.add(jj_expentry);
+	   }
+	 }
+	 int[][] exptokseq = new int[jj_expentries.size()][];
+	 for (int i = 0; i < jj_expentries.size(); i++) {
+	   exptokseq[i] = jj_expentries.get(i);
+	 }
+	 return new ParseException(token, exptokseq, tokenImage);
+  }
+
+  private boolean trace_enabled;
+
+/** Trace enabled. */
+  final public boolean trace_enabled() {
+	 return trace_enabled;
   }
 
   /** Enable tracing. */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTokenManager.java b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTokenManager.java
index 79fdb7b..7e3de5e 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTokenManager.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTokenManager.java
@@ -21,8 +21,7 @@
 package org.apache.cayenne.template.parser;
 
 /** Token Manager. */
-public class SQLTemplateParserTokenManager implements SQLTemplateParserConstants
-{
+public class SQLTemplateParserTokenManager implements SQLTemplateParserConstants {
     /** Holds the last value computed by a constant token. */
     Object literalValue;
 
@@ -55,18 +54,18 @@
     private char escapeChar() {
         int ofs = image.length() - 1;
         switch ( image.charAt(ofs) ) {
-            case 'n':   return '\u005cn';
-            case 'r':   return '\u005cr';
-            case 't':   return '\u005ct';
-            case 'b':   return '\u005cb';
-            case 'f':   return '\u005cf';
-            case '\u005c\u005c':  return '\u005c\u005c';
-            case '\u005c'':  return '\u005c'';
-            case '\u005c"':  return '\u005c"';
+           case 'n':   return '\n';
+           case 'r':   return '\r';
+           case 't':   return '\t';
+           case 'b':   return '\b';
+           case 'f':   return '\f';
+           case '\\':  return '\\';
+           case '\'':  return '\'';
+           case '\"':  return '\"';
         }
 
           // Otherwise, it's an octal number.  Find the backslash and convert.
-        while ( image.charAt(--ofs) != '\u005c\u005c' ){
+        while ( image.charAt(--ofs) != '\\' ){
         }
 
         int value = 0;
@@ -124,16 +123,14 @@
   public  java.io.PrintStream debugStream = System.out;
   /** Set debug output. */
   public  void setDebugStream(java.io.PrintStream ds) { debugStream = ds; }
-private final int jjStopStringLiteralDfa_0(int pos, long active0)
-{
+private final int jjStopStringLiteralDfa_0(int pos, long active0){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_0(int pos, long active0)
-{
+private final int jjStartNfa_0(int pos, long active0){
    return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1);
 }
 private int jjStopAtPos(int pos, int kind)
@@ -142,8 +139,7 @@
    jjmatchedPos = pos;
    return pos + 1;
 }
-private int jjMoveStringLiteralDfa0_0()
-{
+private int jjMoveStringLiteralDfa0_0(){
    switch(curChar)
    {
       case 35:
@@ -155,8 +151,7 @@
          return jjMoveNfa_0(0, 0);
    }
 }
-private int jjMoveStringLiteralDfa1_0(long active0)
-{
+private int jjMoveStringLiteralDfa1_0(long active0){
    try { curChar = input_stream.readChar(); }
    catch(java.io.IOException e) {
       jjStopStringLiteralDfa_0(0, active0);
@@ -177,8 +172,7 @@
    }
    return jjStartNfa_0(0, active0);
 }
-private int jjMoveStringLiteralDfa2_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa2_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(0, old0);
    try { curChar = input_stream.readChar(); }
@@ -201,8 +195,7 @@
    }
    return jjStartNfa_0(1, active0);
 }
-private int jjMoveStringLiteralDfa3_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa3_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(1, old0);
    try { curChar = input_stream.readChar(); }
@@ -223,8 +216,7 @@
    }
    return jjStartNfa_0(2, active0);
 }
-private int jjMoveStringLiteralDfa4_0(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa4_0(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_0(2, old0);
    try { curChar = input_stream.readChar(); }
@@ -294,7 +286,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -327,20 +319,17 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-private final int jjStopStringLiteralDfa_4(int pos, long active0)
-{
+private final int jjStopStringLiteralDfa_4(int pos, long active0){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_4(int pos, long active0)
-{
+private final int jjStartNfa_4(int pos, long active0){
    return jjMoveNfa_4(jjStopStringLiteralDfa_4(pos, active0), pos + 1);
 }
-private int jjMoveStringLiteralDfa0_4()
-{
+private int jjMoveStringLiteralDfa0_4(){
    switch(curChar)
    {
       case 39:
@@ -408,7 +397,7 @@
                         kind = 27;
                   }
                   else if (curChar == 92)
-                     jjAddStates(0, 2);
+                     { jjAddStates(0, 2); }
                   break;
                case 1:
                   if ((0x14404510000000L & l) != 0L && kind > 26)
@@ -424,7 +413,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -510,7 +499,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -536,20 +525,17 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-private final int jjStopStringLiteralDfa_2(int pos, long active0)
-{
+private final int jjStopStringLiteralDfa_2(int pos, long active0){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_2(int pos, long active0)
-{
+private final int jjStartNfa_2(int pos, long active0){
    return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1);
 }
-private int jjMoveStringLiteralDfa0_2()
-{
+private int jjMoveStringLiteralDfa0_2(){
    switch(curChar)
    {
       case 35:
@@ -565,8 +551,7 @@
          return jjMoveNfa_2(0, 0);
    }
 }
-private int jjMoveStringLiteralDfa1_2(long active0)
-{
+private int jjMoveStringLiteralDfa1_2(long active0){
    try { curChar = input_stream.readChar(); }
    catch(java.io.IOException e) {
       jjStopStringLiteralDfa_2(0, active0);
@@ -583,8 +568,7 @@
    }
    return jjStartNfa_2(0, active0);
 }
-private int jjMoveStringLiteralDfa2_2(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa2_2(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_2(0, old0);
    try { curChar = input_stream.readChar(); }
@@ -607,8 +591,7 @@
    }
    return jjStartNfa_2(1, active0);
 }
-private int jjMoveStringLiteralDfa3_2(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa3_2(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_2(1, old0);
    try { curChar = input_stream.readChar(); }
@@ -629,8 +612,7 @@
    }
    return jjStartNfa_2(2, active0);
 }
-private int jjMoveStringLiteralDfa4_2(long old0, long active0)
-{
+private int jjMoveStringLiteralDfa4_2(long old0, long active0){
    if (((active0 &= old0)) == 0L)
       return jjStartNfa_2(2, old0);
    try { curChar = input_stream.readChar(); }
@@ -672,7 +654,7 @@
                   if ((0xfc00bee7ffffffffL & l) == 0L)
                      break;
                   kind = 39;
-                  jjCheckNAdd(2);
+                  { jjCheckNAdd(2); }
                   break;
                case 1:
                   if ((0x3ff000000000000L & l) == 0L)
@@ -696,13 +678,13 @@
                   {
                      if (kind > 18)
                         kind = 18;
-                     jjCheckNAdd(1);
+                     { jjCheckNAdd(1); }
                   }
                   else if ((0xf800000178000001L & l) != 0L)
                   {
                      if (kind > 39)
                         kind = 39;
-                     jjCheckNAdd(2);
+                     { jjCheckNAdd(2); }
                   }
                   break;
                case 1:
@@ -710,13 +692,13 @@
                      break;
                   if (kind > 18)
                      kind = 18;
-                  jjCheckNAdd(1);
+                  { jjCheckNAdd(1); }
                   break;
                case 2:
                   if ((0xf800000178000001L & l) == 0L)
                      break;
                   kind = 39;
-                  jjCheckNAdd(2);
+                  { jjCheckNAdd(2); }
                   break;
                default : break;
             }
@@ -724,7 +706,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -739,7 +721,7 @@
                      break;
                   if (kind > 39)
                      kind = 39;
-                  jjCheckNAdd(2);
+                  { jjCheckNAdd(2); }
                   break;
                default : break;
             }
@@ -758,20 +740,17 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-private final int jjStopStringLiteralDfa_5(int pos, long active0)
-{
+private final int jjStopStringLiteralDfa_5(int pos, long active0){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_5(int pos, long active0)
-{
+private final int jjStartNfa_5(int pos, long active0){
    return jjMoveNfa_5(jjStopStringLiteralDfa_5(pos, active0), pos + 1);
 }
-private int jjMoveStringLiteralDfa0_5()
-{
+private int jjMoveStringLiteralDfa0_5(){
    switch(curChar)
    {
       case 34:
@@ -839,7 +818,7 @@
                         kind = 30;
                   }
                   else if (curChar == 92)
-                     jjAddStates(0, 2);
+                     { jjAddStates(0, 2); }
                   break;
                case 1:
                   if ((0x14404510000000L & l) != 0L && kind > 29)
@@ -855,7 +834,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -885,20 +864,17 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
-private final int jjStopStringLiteralDfa_1(int pos, long active0)
-{
+private final int jjStopStringLiteralDfa_1(int pos, long active0){
    switch (pos)
    {
       default :
          return -1;
    }
 }
-private final int jjStartNfa_1(int pos, long active0)
-{
+private final int jjStartNfa_1(int pos, long active0){
    return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1);
 }
-private int jjMoveStringLiteralDfa0_1()
-{
+private int jjMoveStringLiteralDfa0_1(){
    switch(curChar)
    {
       case 34:
@@ -953,22 +929,22 @@
             {
                case 3:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddStates(3, 8);
+                     { jjCheckNAddStates(3, 8); }
                   else if (curChar == 46)
-                     jjCheckNAdd(29);
+                     { jjCheckNAdd(29); }
                   else if (curChar == 45)
-                     jjAddStates(9, 10);
+                     { jjAddStates(9, 10); }
                   if ((0x3fe000000000000L & l) != 0L)
                   {
                      if (kind > 32)
                         kind = 32;
-                     jjCheckNAddTwoStates(22, 23);
+                     { jjCheckNAddTwoStates(22, 23); }
                   }
                   else if (curChar == 48)
                   {
                      if (kind > 32)
                         kind = 32;
-                     jjCheckNAddStates(11, 13);
+                     { jjCheckNAddStates(11, 13); }
                   }
                   break;
                case 19:
@@ -980,105 +956,105 @@
                   break;
                case 20:
                   if (curChar == 45)
-                     jjAddStates(9, 10);
+                     { jjAddStates(9, 10); }
                   break;
                case 21:
                   if ((0x3fe000000000000L & l) == 0L)
                      break;
                   if (kind > 32)
                      kind = 32;
-                  jjCheckNAddTwoStates(22, 23);
+                  { jjCheckNAddTwoStates(22, 23); }
                   break;
                case 22:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
                   if (kind > 32)
                      kind = 32;
-                  jjCheckNAddTwoStates(22, 23);
+                  { jjCheckNAddTwoStates(22, 23); }
                   break;
                case 24:
                   if (curChar != 48)
                      break;
                   if (kind > 32)
                      kind = 32;
-                  jjCheckNAddStates(11, 13);
+                  { jjCheckNAddStates(11, 13); }
                   break;
                case 25:
                   if ((0xff000000000000L & l) == 0L)
                      break;
                   if (kind > 32)
                      kind = 32;
-                  jjCheckNAddTwoStates(25, 23);
+                  { jjCheckNAddTwoStates(25, 23); }
                   break;
                case 27:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
                   if (kind > 32)
                      kind = 32;
-                  jjCheckNAddTwoStates(27, 23);
+                  { jjCheckNAddTwoStates(27, 23); }
                   break;
                case 28:
                   if (curChar == 46)
-                     jjCheckNAdd(29);
+                     { jjCheckNAdd(29); }
                   break;
                case 29:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
                   if (kind > 33)
                      kind = 33;
-                  jjCheckNAddStates(14, 16);
+                  { jjCheckNAddStates(14, 16); }
                   break;
                case 31:
                   if ((0x280000000000L & l) != 0L)
-                     jjCheckNAdd(32);
+                     { jjCheckNAdd(32); }
                   break;
                case 32:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
                   if (kind > 33)
                      kind = 33;
-                  jjCheckNAddTwoStates(32, 33);
+                  { jjCheckNAddTwoStates(32, 33); }
                   break;
                case 34:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddStates(3, 8);
+                     { jjCheckNAddStates(3, 8); }
                   break;
                case 35:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddTwoStates(35, 36);
+                     { jjCheckNAddTwoStates(35, 36); }
                   break;
                case 36:
                   if (curChar != 46)
                      break;
                   if (kind > 33)
                      kind = 33;
-                  jjCheckNAddStates(17, 19);
+                  { jjCheckNAddStates(17, 19); }
                   break;
                case 37:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
                   if (kind > 33)
                      kind = 33;
-                  jjCheckNAddStates(17, 19);
+                  { jjCheckNAddStates(17, 19); }
                   break;
                case 38:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddTwoStates(38, 39);
+                     { jjCheckNAddTwoStates(38, 39); }
                   break;
                case 40:
                   if ((0x280000000000L & l) != 0L)
-                     jjCheckNAdd(41);
+                     { jjCheckNAdd(41); }
                   break;
                case 41:
                   if ((0x3ff000000000000L & l) == 0L)
                      break;
                   if (kind > 33)
                      kind = 33;
-                  jjCheckNAddTwoStates(41, 33);
+                  { jjCheckNAddTwoStates(41, 33); }
                   break;
                case 42:
                   if ((0x3ff000000000000L & l) != 0L)
-                     jjCheckNAddTwoStates(42, 33);
+                     { jjCheckNAddTwoStates(42, 33); }
                   break;
                default : break;
             }
@@ -1096,7 +1072,7 @@
                   {
                      if (kind > 18)
                         kind = 18;
-                     jjCheckNAdd(19);
+                     { jjCheckNAdd(19); }
                   }
                   if (curChar == 70)
                      jjstateSet[jjnewStateCnt++] = 16;
@@ -1181,7 +1157,7 @@
                      break;
                   if (kind > 18)
                      kind = 18;
-                  jjCheckNAdd(19);
+                  { jjCheckNAdd(19); }
                   break;
                case 23:
                   if ((0x110000001100L & l) != 0L && kind > 32)
@@ -1189,18 +1165,18 @@
                   break;
                case 26:
                   if ((0x100000001000000L & l) != 0L)
-                     jjCheckNAdd(27);
+                     { jjCheckNAdd(27); }
                   break;
                case 27:
                   if ((0x7e0000007eL & l) == 0L)
                      break;
                   if (kind > 32)
                      kind = 32;
-                  jjCheckNAddTwoStates(27, 23);
+                  { jjCheckNAddTwoStates(27, 23); }
                   break;
                case 30:
                   if ((0x2000000020L & l) != 0L)
-                     jjAddStates(20, 21);
+                     { jjAddStates(20, 21); }
                   break;
                case 33:
                   if ((0x5400000054L & l) != 0L && kind > 33)
@@ -1208,7 +1184,7 @@
                   break;
                case 39:
                   if ((0x2000000020L & l) != 0L)
-                     jjAddStates(22, 23);
+                     { jjAddStates(22, 23); }
                   break;
                default : break;
             }
@@ -1216,7 +1192,7 @@
       }
       else
       {
-         int hiByte = (int)(curChar >> 8);
+         int hiByte = (curChar >> 8);
          int i1 = hiByte >> 6;
          long l1 = 1L << (hiByte & 077);
          int i2 = (curChar & 0xff) >> 6;
@@ -1242,6 +1218,38 @@
       catch(java.io.IOException e) { return curPos; }
    }
 }
+
+/** Token literal values. */
+public static final String[] jjstrLiteralImages = {
+"", null, null, null, null, "\43\151\146", "\43\145\154\163\145", 
+"\43\145\156\144", "\43", "\44", null, null, "\51", "\54", "\133", "\135", "\50", "\56", null, 
+null, null, "\43\43", null, null, null, null, null, null, null, null, null, null, 
+null, null, null, null, null, null, null, null, };
+protected Token jjFillToken()
+{
+   final Token t;
+   final String curTokenImage;
+   final int beginLine;
+   final int endLine;
+   final int beginColumn;
+   final int endColumn;
+   String im = jjstrLiteralImages[jjmatchedKind];
+   curTokenImage = (im == null) ? input_stream.GetImage() : im;
+   beginLine = input_stream.getBeginLine();
+   beginColumn = input_stream.getBeginColumn();
+   endLine = input_stream.getEndLine();
+   endColumn = input_stream.getEndColumn();
+   t = Token.newToken(jjmatchedKind);
+   t.kind = jjmatchedKind;
+   t.image = curTokenImage;
+
+   t.beginLine = beginLine;
+   t.endLine = endLine;
+   t.beginColumn = beginColumn;
+   t.endColumn = endColumn;
+
+   return t;
+}
 static final int[] jjnextStates = {
    1, 2, 3, 35, 36, 38, 39, 42, 33, 21, 24, 25, 26, 23, 29, 30, 
    33, 37, 30, 33, 31, 32, 40, 41, 
@@ -1259,114 +1267,6 @@
    }
 }
 
-/** Token literal values. */
-public static final String[] jjstrLiteralImages = {
-"", null, null, null, null, "\43\151\146", "\43\145\154\163\145", 
-"\43\145\156\144", "\43", "\44", null, null, "\51", "\54", "\133", "\135", "\50", "\56", null, 
-null, null, "\43\43", null, null, null, null, null, null, null, null, null, null, 
-null, null, null, null, null, null, null, null, };
-
-/** Lexer state names. */
-public static final String[] lexStateNames = {
-   "DEFAULT",
-   "ARGS",
-   "NOT_TEXT",
-   "IN_SINGLE_LINE_COMMENT",
-   "WithinSingleQuoteLiteral",
-   "WithinDoubleQuoteLiteral",
-};
-
-/** Lex State array. */
-public static final int[] jjnewLexState = {
-   -1, -1, -1, -1, -1, 2, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, 3, 0, -1, 4, 
-   5, -1, -1, 1, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, 0, 
-};
-static final long[] jjtoToken = {
-   0xc39067ffe1L, 
-};
-static final long[] jjtoSkip = {
-   0x80001eL, 
-};
-static final long[] jjtoMore = {
-   0x6f000000L, 
-};
-protected JavaCharStream input_stream;
-private final int[] jjrounds = new int[43];
-private final int[] jjstateSet = new int[86];
-private final StringBuilder jjimage = new StringBuilder();
-private StringBuilder image = jjimage;
-private int jjimageLen;
-private int lengthOfMatch;
-protected char curChar;
-/** Constructor. */
-public SQLTemplateParserTokenManager(JavaCharStream stream){
-   if (JavaCharStream.staticFlag)
-      throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
-   input_stream = stream;
-}
-
-/** Constructor. */
-public SQLTemplateParserTokenManager(JavaCharStream stream, int lexState){
-   this(stream);
-   SwitchTo(lexState);
-}
-
-/** Reinitialise parser. */
-public void ReInit(JavaCharStream stream)
-{
-   jjmatchedPos = jjnewStateCnt = 0;
-   curLexState = defaultLexState;
-   input_stream = stream;
-   ReInitRounds();
-}
-private void ReInitRounds()
-{
-   int i;
-   jjround = 0x80000001;
-   for (i = 43; i-- > 0;)
-      jjrounds[i] = 0x80000000;
-}
-
-/** Reinitialise parser. */
-public void ReInit(JavaCharStream stream, int lexState)
-{
-   ReInit(stream);
-   SwitchTo(lexState);
-}
-
-/** Switch to specified lex state. */
-public void SwitchTo(int lexState)
-{
-   if (lexState >= 6 || lexState < 0)
-      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
-   else
-      curLexState = lexState;
-}
-
-protected Token jjFillToken()
-{
-   final Token t;
-   final String curTokenImage;
-   final int beginLine;
-   final int endLine;
-   final int beginColumn;
-   final int endColumn;
-   String im = jjstrLiteralImages[jjmatchedKind];
-   curTokenImage = (im == null) ? input_stream.GetImage() : im;
-   beginLine = input_stream.getBeginLine();
-   beginColumn = input_stream.getBeginColumn();
-   endLine = input_stream.getEndLine();
-   endColumn = input_stream.getEndColumn();
-   t = Token.newToken(jjmatchedKind, curTokenImage);
-
-   t.beginLine = beginLine;
-   t.endLine = endLine;
-   t.beginColumn = beginColumn;
-   t.endColumn = endColumn;
-
-   return t;
-}
-
 int curLexState = 0;
 int defaultLexState = 0;
 int jjnewStateCnt;
@@ -1387,9 +1287,10 @@
    {
       curChar = input_stream.BeginToken();
    }
-   catch(java.io.IOException e)
+   catch(Exception e)
    {
       jjmatchedKind = 0;
+      jjmatchedPos = -1;
       matchedToken = jjFillToken();
       return matchedToken;
    }
@@ -1494,6 +1395,14 @@
   }
 }
 
+void SkipLexicalActions(Token matchedToken)
+{
+   switch(jjmatchedKind)
+   {
+      default :
+         break;
+   }
+}
 void MoreLexicalActions()
 {
    jjimageLen += (lengthOfMatch = jjmatchedPos + 1);
@@ -1604,4 +1513,95 @@
    } while (start++ != end);
 }
 
+    /** Constructor. */
+    public SQLTemplateParserTokenManager(JavaCharStream stream){
+
+      if (JavaCharStream.staticFlag)
+            throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer.");
+
+    input_stream = stream;
+  }
+
+  /** Constructor. */
+  public SQLTemplateParserTokenManager (JavaCharStream stream, int lexState){
+    ReInit(stream);
+    SwitchTo(lexState);
+  }
+
+  /** Reinitialise parser. */
+  
+  public void ReInit(JavaCharStream stream)
+  {
+
+
+    jjmatchedPos =
+    jjnewStateCnt =
+    0;
+    curLexState = defaultLexState;
+    input_stream = stream;
+    ReInitRounds();
+  }
+
+  private void ReInitRounds()
+  {
+    int i;
+    jjround = 0x80000001;
+    for (i = 43; i-- > 0;)
+      jjrounds[i] = 0x80000000;
+  }
+
+  /** Reinitialise parser. */
+  public void ReInit(JavaCharStream stream, int lexState)
+  
+  {
+    ReInit(stream);
+    SwitchTo(lexState);
+  }
+
+  /** Switch to specified lex state. */
+  public void SwitchTo(int lexState)
+  {
+    if (lexState >= 6 || lexState < 0)
+      throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE);
+    else
+      curLexState = lexState;
+  }
+
+
+/** Lexer state names. */
+public static final String[] lexStateNames = {
+   "DEFAULT",
+   "ARGS",
+   "NOT_TEXT",
+   "IN_SINGLE_LINE_COMMENT",
+   "WithinSingleQuoteLiteral",
+   "WithinDoubleQuoteLiteral",
+};
+
+/** Lex State array. */
+public static final int[] jjnewLexState = {
+   -1, -1, -1, -1, -1, 2, 0, 0, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, -1, -1, 3, 0, -1, 4, 
+   5, -1, -1, 1, -1, -1, 1, -1, -1, -1, -1, -1, -1, -1, 0, 
+};
+static final long[] jjtoToken = {
+   0xc39067ffe1L, 
+};
+static final long[] jjtoSkip = {
+   0x80001eL, 
+};
+static final long[] jjtoSpecial = {
+   0x0L, 
+};
+static final long[] jjtoMore = {
+   0x6f000000L, 
+};
+    protected JavaCharStream  input_stream;
+
+    private final int[] jjrounds = new int[43];
+    private final int[] jjstateSet = new int[2 * 43];
+    private final StringBuilder jjimage = new StringBuilder();
+    private StringBuilder image = jjimage;
+    private int jjimageLen;
+    private int lengthOfMatch;
+    protected char curChar;
 }
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTreeConstants.java b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTreeConstants.java
index 0549e18..455bdef 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTreeConstants.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/template/parser/SQLTemplateParserTreeConstants.java
@@ -1,4 +1,4 @@
-/* Generated By:JavaCC: Do not edit this line. SQLTemplateParserTreeConstants.java Version 5.0 */
+/* Generated By:JavaCC: Do not edit this line. SQLTemplateParserTreeConstants.java Version 7.0.5 */
 package org.apache.cayenne.template.parser;
 
 public interface SQLTemplateParserTreeConstants
@@ -34,4 +34,4 @@
     "Array",
   };
 }
-/* JavaCC - OriginalChecksum=4e04f6ed8da48f129794e9555444f8df (do not edit this line) */
+/* JavaCC - OriginalChecksum=47a0bbea07c6f5e8188c26aba4a9b62e (do not edit this line) */
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java
index f5a7064..8884e04 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/ObjectDetachOperation.java
@@ -22,16 +22,10 @@
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.ObjectId;
 import org.apache.cayenne.Persistent;
+import org.apache.cayenne.access.AttributeFault;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.query.PrefetchTreeNode;
-import org.apache.cayenne.reflect.ArcProperty;
-import org.apache.cayenne.reflect.AttributeProperty;
-import org.apache.cayenne.reflect.ClassDescriptor;
-import org.apache.cayenne.reflect.PropertyDescriptor;
-import org.apache.cayenne.reflect.PropertyVisitor;
-import org.apache.cayenne.reflect.ToManyMapProperty;
-import org.apache.cayenne.reflect.ToManyProperty;
-import org.apache.cayenne.reflect.ToOneProperty;
+import org.apache.cayenne.reflect.*;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -197,7 +191,11 @@
             public boolean visitAttribute(AttributeProperty property) {
                 PropertyDescriptor targetProperty = targetDescriptor
                         .getProperty(property.getName());
-                targetProperty.writeProperty(target, null, property.readProperty(source));
+                if (!property.getAttribute().isLazy()) {
+                    targetProperty.writeProperty(target, null, property.readProperty(source));
+                } else {
+                    targetProperty.writeProperty(target, null, new AttributeFault(property));
+                }
                 return true;
             }
         });
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/Util.java b/cayenne-server/src/main/java/org/apache/cayenne/util/Util.java
index c86c279..fe40a8b 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/Util.java
+++ b/cayenne-server/src/main/java/org/apache/cayenne/util/Util.java
@@ -53,6 +53,7 @@
 import java.io.Serializable;
 import java.lang.reflect.Member;
 import java.lang.reflect.Modifier;
+import java.math.BigDecimal;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
@@ -209,7 +210,7 @@
 			return builder.isEquals();
 		} else { // It is NOT an array, so use regular equals()
 			return o1.equals(o2);
-		}
+        }
 	}
 
 	/**
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/GeoJson.java b/cayenne-server/src/main/java/org/apache/cayenne/value/GeoJson.java
new file mode 100644
index 0000000..a8c37c9
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/GeoJson.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.value;
+
+import java.util.Objects;
+
+/**
+ * A Cayenne-supported values object that holds GeoJson string.
+ *
+ * @since 4.2
+ */
+public class GeoJson {
+
+    private final String geometry;
+
+    public GeoJson(String geometry) {
+        this.geometry = geometry;
+    }
+
+    public String getGeometry() {
+        return geometry;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        GeoJson other = (GeoJson) o;
+
+        // TODO: and ideal comparison would ignore spaces around JSON tokens, but that may be too expensive
+        return Objects.equals(geometry, other.geometry);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(geometry);
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java b/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java
new file mode 100644
index 0000000..6eeb049
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/Json.java
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.value;
+
+import java.util.Objects;
+
+/**
+ * A Cayenne-supported values object that holds Json string.
+ *
+ * @since 4.2
+ */
+public class Json {
+
+    private final String json;
+
+    public Json(String json) {
+        this.json = json;
+    }
+
+    public String getRawJson() {
+        return json;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Json other = (Json) o;
+        return Objects.equals(json, other.json);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(json);
+    }
+}
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/value/Wkt.java b/cayenne-server/src/main/java/org/apache/cayenne/value/Wkt.java
new file mode 100644
index 0000000..6e2251f
--- /dev/null
+++ b/cayenne-server/src/main/java/org/apache/cayenne/value/Wkt.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ *    or more contributor license agreements.  See the NOTICE file
+ *    distributed with this work for additional information
+ *    regarding copyright ownership.  The ASF licenses this file
+ *    to you under the Apache License, Version 2.0 (the
+ *    "License"); you may not use this file except in compliance
+ *    with the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *    Unless required by applicable law or agreed to in writing,
+ *    software distributed under the License is distributed on an
+ *    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *    KIND, either express or implied.  See the License for the
+ *    specific language governing permissions and limitations
+ *    under the License.
+ */
+
+package org.apache.cayenne.value;
+
+import java.util.Objects;
+
+/**
+ * A Cayenne-supported value object holding a WKT geometry String. By itself it does not provide a WKT parser or
+ * geometry functions. Its goal is to instruct Cayenne to read and write geometries as WKT Strings.
+ *
+ * @since 4.2
+ */
+public class Wkt {
+
+    private final String wkt;
+
+    public Wkt(String wkt) {
+        this.wkt = wkt;
+    }
+
+    public String getWkt() {
+        return wkt;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        Wkt wkt1 = (Wkt) o;
+        return Objects.equals(wkt, wkt1.wkt);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(wkt);
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/main/jjtree/org/apache/cayenne/exp/parser/ExpressionParser.jjt b/cayenne-server/src/main/jjtree/org/apache/cayenne/exp/parser/ExpressionParser.jjt
index 38afc39..90fbea2 100644
--- a/cayenne-server/src/main/jjtree/org/apache/cayenne/exp/parser/ExpressionParser.jjt
+++ b/cayenne-server/src/main/jjtree/org/apache/cayenne/exp/parser/ExpressionParser.jjt
@@ -287,6 +287,8 @@
         pathExpression()
     |
         customFunction()
+    |
+        customOperator()
 }
 
 void functionsReturningStrings() : { }
@@ -299,6 +301,11 @@
     <FUNCTION> "(" stringLiteral() ( "," ( stringExpression() | numericExpression() ) )* ")"
 }
 
+void customOperator() #CustomOperator : { }
+{
+    <OPERATOR> "(" stringLiteral() ( "," ( stringExpression() | numericExpression() ) )* ")"
+}
+
 void concat() #Concat : { }
 {
 	<CONCAT> "(" stringParameter() ( "," stringParameter() )* ")"
@@ -533,7 +540,6 @@
 	|	<SUM: "sum" >
 	|	<COUNT: "count" >
 	|	<DISTINCT: "distinct">
-	|   <FUNCTION: "fn" >
 }
 
 TOKEN : /* functions returning strings */
@@ -579,6 +585,16 @@
     | <SECOND: "second">
 }
 
+/*
+Special operations
+*/
+TOKEN :
+{
+    <FUNCTION: "fn" >
+	|
+	<OPERATOR: "op" >
+}
+
 void namedParameter() :{
 	Token t;
 }
@@ -613,11 +629,13 @@
 
 TOKEN :
 {
-  < IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>)* (["#"]<LETTER> (<LETTER>|<DIGIT>)*)? (["+"])? >
+  < IDENTIFIER: <LETTER> (<LETTER>|<DIGIT>|<DOLLAR_SIGN>)* (["#"]<LETTER> (<LETTER>|<DIGIT>|<DOLLAR_SIGN>)*)? (["+"])? >
 |
   < #LETTER: ["_","a"-"z","A"-"Z"] >
 |
   < #DIGIT: ["0"-"9"] >
+|
+  < #DOLLAR_SIGN: ["$"] >
 }
 
 /**
diff --git a/cayenne-server/src/main/jjtree/org/apache/cayenne/template/parser/SQLTemplateParser.jjt b/cayenne-server/src/main/jjtree/org/apache/cayenne/template/parser/SQLTemplateParser.jjt
index 0106fe2..c64d882 100644
--- a/cayenne-server/src/main/jjtree/org/apache/cayenne/template/parser/SQLTemplateParser.jjt
+++ b/cayenne-server/src/main/jjtree/org/apache/cayenne/template/parser/SQLTemplateParser.jjt
@@ -186,7 +186,7 @@
     Token t;
 }
 {
-    <DOLLAR> ( t = <IDENTIFIER> ) {
+    <DOLLAR> ( t = <IDENTIFIER> | t = <TEXT_OTHER> ) {
         jjtThis.setIdentifier(t.image);
     }
     ( method() )*
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/LazyAttributesIT.java b/cayenne-server/src/test/java/org/apache/cayenne/LazyAttributesIT.java
new file mode 100644
index 0000000..22fe429
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/LazyAttributesIT.java
@@ -0,0 +1,119 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne;
+
+import java.sql.Types;
+
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.lazy.Lazyblob;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.LAZY_ATTRIBUTES_PROJECT)
+public class LazyAttributesIT extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Before
+    public void setup() throws Exception {
+        TableHelper th = new TableHelper(dbHelper, "LAZYBLOB")
+                .setColumns("ID", "NAME", "LAZY_DATA")
+                .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.VARBINARY);
+        th.insert(1, "test", new byte[]{1, 2, 3, 4, 5});
+    }
+
+    @Test
+    public void testRead() {
+        Lazyblob lazyblob = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        byte[] expected = {1, 2, 3, 4, 5};
+
+        assertThat(lazyblob.readPropertyDirectly("lazyData"), instanceOf(Fault.class));
+        assertArrayEquals(expected, (byte[])lazyblob.readProperty("lazyData"));
+        assertArrayEquals(expected, lazyblob.getLazyData());
+    }
+
+    @Test
+    public void testReadColumn() {
+        byte[] lazyData = ObjectSelect.columnQuery(Lazyblob.class, Lazyblob.LAZY_DATA).selectOne(context);
+        byte[] expected = {1, 2, 3, 4, 5};
+        assertArrayEquals(expected, lazyData);
+    }
+
+    @Test
+    public void testWrite() {
+        Lazyblob lazyblob = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        byte[] expected = {5, 4, 3, 2, 1};
+
+        // this cause resolve of the fault
+        lazyblob.setLazyData(expected);
+
+        context.commitChanges();
+
+        assertThat(lazyblob.readPropertyDirectly("lazyData"), instanceOf(byte[].class));
+        assertArrayEquals(expected, (byte[])lazyblob.readProperty("lazyData"));
+        assertArrayEquals(expected, lazyblob.getLazyData());
+
+        Lazyblob lazyblob2 = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        assertArrayEquals(expected, lazyblob2.getLazyData());
+    }
+
+    @Test
+    public void testUpdateNoFetch() {
+        Lazyblob lazyblob = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        lazyblob.setName("updated_name");
+
+        context.commitChanges();
+
+        Lazyblob lazyblob2 = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        assertEquals("updated_name", lazyblob2.getName());
+        assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, lazyblob2.getLazyData());
+    }
+
+    @Test
+    public void testUpdateFetch() {
+        Lazyblob lazyblob = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        lazyblob.setName("updated_name");
+        lazyblob.getLazyData();
+
+        context.commitChanges();
+
+        Lazyblob lazyblob2 = ObjectSelect.query(Lazyblob.class).selectOne(context);
+        assertEquals("updated_name", lazyblob2.getName());
+        assertArrayEquals(new byte[]{1, 2, 3, 4, 5}, lazyblob2.getLazyData());
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641.java b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641.java
new file mode 100644
index 0000000..e10be4b
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2641.java
@@ -0,0 +1,174 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.access.translator.select.DefaultSelectTranslator;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ColumnSelect;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.cay_2641.ArtistLazy;
+import org.apache.cayenne.testdo.cay_2641.DatamapLazy;
+import org.apache.cayenne.testdo.cay_2641.PaintingLazy;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.sql.Types;
+import java.util.List;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.CAY_2641)
+public class Cay2641 extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Inject
+    private DbAdapter adapter;
+
+    @Before
+    public void setup() throws Exception {
+        TableHelper th = new TableHelper(dbHelper, "ArtistLazy")
+                .setColumns("ID", "NAME", "SURNAME")
+                .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.VARCHAR);
+        th.insert(1, "artist1", "artist2");
+
+        th = new TableHelper(dbHelper, "PaintingLazy")
+                .setColumns("ID", "NAME", "ARTIST_ID")
+                .setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.INTEGER);
+        th.insert(1, "painting1", 1);
+    }
+
+    @Test
+    public void testTranslatorSql() {
+        ObjectSelect<ArtistLazy> artists = ObjectSelect.query(ArtistLazy.class);
+
+        DefaultSelectTranslator translator = new DefaultSelectTranslator(artists, adapter, context.getEntityResolver());
+
+        String sql = translator.getSql();
+        assertFalse(sql.contains("t0.NAME"));
+
+        String string = "SELECT t0.SURNAME, t0.ID FROM ArtistLazy t0";
+        assertTrue(sql.equals(string));
+
+        ColumnSelect<String> select = ObjectSelect.columnQuery(ArtistLazy.class, ArtistLazy.NAME);
+        translator = new DefaultSelectTranslator(select, adapter, context.getEntityResolver());
+        sql = translator.getSql();
+
+        assertTrue(sql.contains("t0.NAME"));
+    }
+
+    @Test
+    public void testTypeAttributes() {
+        List<ArtistLazy> artists = ObjectSelect.query(ArtistLazy.class).select(context);
+
+        Object object = artists.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = artists.get(0).readPropertyDirectly("surname");
+        assertTrue(object.equals("artist2"));
+    }
+
+    @Test
+    public void testTypeLazyAttribute() {
+        ArtistLazy artist = ObjectSelect.query(ArtistLazy.class).selectFirst(context);
+
+        Object object = artist.readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        artist.getName();
+        object = artist.readPropertyDirectly("name");
+        assertTrue(object.equals("artist1"));
+    }
+
+    @Test
+    public void testPrefetchLazyTranslatorSql() {
+        ObjectSelect<PaintingLazy> paintingLazyObjectSelect = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.joint());
+        DefaultSelectTranslator translator = new DefaultSelectTranslator(paintingLazyObjectSelect, adapter, context.getEntityResolver());
+        String sql = translator.getSql();
+        assertFalse(sql.contains("t0.NAME"));
+
+        String string = "SELECT DISTINCT t0.ARTIST_ID, t0.ID, t1.ID, t1.SURNAME FROM PaintingLazy t0 LEFT JOIN ArtistLazy t1 ON t0.ARTIST_ID = t1.ID";
+        assertTrue(sql.equals(string));
+    }
+
+    @Test
+    public void testPrefetchLazyTypeAttributes() {
+        List<PaintingLazy> paintingLazyList = ObjectSelect.query(PaintingLazy.class).prefetch(PaintingLazy.ARTIST.joint()).select(context);
+
+        Object object = paintingLazyList.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = paintingLazyList.get(0).getName();
+        assertTrue(object instanceof String);
+        assertTrue(object.equals("painting1"));
+
+        ArtistLazy artist = (ArtistLazy) paintingLazyList.get(0).readPropertyDirectly("artist");
+        object = artist.readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = artist.readPropertyDirectly("surname");
+        assertTrue(object.equals("artist2"));
+
+        object = artist.getName();
+        assertTrue(object instanceof String);
+        assertTrue(object.equals("artist1"));
+    }
+
+    @Test
+    public void testsSimpleSelectCustomer() {
+        DatamapLazy optimistic = DatamapLazy.getInstance();
+        List<ArtistLazy> artistLazies = optimistic.performSimpleSelect(context);
+
+        Object object = artistLazies.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        object = artistLazies.get(0).readPropertyDirectly("surname");
+        assertTrue(object instanceof String);
+        assertTrue(object.equals("artist2"));
+    }
+
+    @Test
+    public void testsPrefetchSelectCustomer() {
+        DatamapLazy optimistic = DatamapLazy.getInstance();
+        List<PaintingLazy> paintingLazies = optimistic.performPrefetchSelect(context);
+
+        Object object = paintingLazies.get(0).readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+
+        ArtistLazy artist = (ArtistLazy) paintingLazies.get(0).readPropertyDirectly("artist");
+        object = artist.readPropertyDirectly("name");
+        assertTrue(object instanceof Fault);
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2666IT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2666IT.java
new file mode 100644
index 0000000..6ea85f4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/Cay2666IT.java
@@ -0,0 +1,158 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access;
+
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.parser.ASTDbPath;
+import org.apache.cayenne.exp.parser.ASTEqual;
+import org.apache.cayenne.exp.parser.ASTObjPath;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.cay_2666.CAY2666;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+/**
+ * @since 4.2
+ */
+@UseServerRuntime(CayenneProjects.CAY_2666)
+public class Cay2666IT extends ServerCase {
+
+    @Inject
+    private DataContext context;
+
+    @Inject
+    private DBHelper dbHelper;
+
+    private TableHelper tTest;
+
+    @Test
+    public void testExp_Path() {
+        Expression exp1 = ExpressionFactory.exp("object$.path");
+        assertEquals(Expression.OBJ_PATH, exp1.getType());
+
+        Expression exp2 = ExpressionFactory.exp("db:object.path$");
+        assertEquals(Expression.DB_PATH, exp2.getType());
+    }
+
+    @Test
+    public void testPathExp() {
+        assertEquals("abc$.xyz$", ExpressionFactory.pathExp("abc$.xyz$").toString());
+    }
+
+    @Test
+    public void testDbPathExp() {
+        assertEquals("db:abc.xyz$", ExpressionFactory.dbPathExp("abc.xyz$").toString());
+    }
+
+    @Test
+    public void testExpWithAlias() {
+        Expression expression = ExpressionFactory.exp("paintings#p1.galleries$#p2.name = 'Test'");
+        assertEquals("p1.p2.name", expression.getOperand(0).toString());
+        assertEquals("galleries$", ((ASTObjPath)expression.getOperand(0)).getPathAliases().get("p2"));
+    }
+
+    @Test
+    public void testExpWithAliasAndOuterJoin() {
+        Expression expression = ExpressionFactory.exp("paintings$#p1+.name = 'Test'");
+        assertEquals("p1.name", expression.getOperand(0).toString());
+        assertEquals("paintings$+", ((ASTObjPath)expression.getOperand(0)).getPathAliases().get("p1"));
+    }
+
+    @Test
+    public void testDbPathWithDollarSign() throws IOException {
+        StringBuilder buffer = new StringBuilder();
+        new ASTDbPath("x$").appendAsString(buffer);
+        assertEquals("db:x$", buffer.toString());
+    }
+
+    @Test
+    public void testExpDbPathWithDollarSign() throws IOException {
+        Expression exp = ExpressionFactory.exp("db:x$ = 'A'");
+        Expression expression = new ASTEqual(new ASTDbPath("x$"), "A");
+        assertEquals(exp, expression);
+
+        exp = ExpressionFactory.exp("x$ = 'A'");
+        expression = new ASTEqual(new ASTDbPath("x$"), "A");
+        assertNotEquals(exp, expression);
+
+        exp = ExpressionFactory.exp("db:x$ = $name", "A");
+        expression = new ASTEqual(new ASTDbPath("x$"), "A");
+        assertEquals(exp, expression);
+    }
+
+    @Test
+    public void testObjPathWithDollarSign() throws IOException {
+        StringBuilder buffer = new StringBuilder();
+
+        new ASTObjPath("obj:x$").appendAsString(buffer);
+        assertEquals("obj:x$", buffer.toString());
+
+        assertEquals("y$", new ASTObjPath("y$").toString());
+    }
+
+    @Test
+    public void testExpObjPathWithDollarSign() throws IOException {
+        Expression exp = ExpressionFactory.exp("obj:x$ = 'A'");
+        Expression expression = new ASTEqual(new ASTObjPath("x$"), "A");
+        assertEquals(exp, expression);
+
+        exp = ExpressionFactory.exp("x$ = 'A'");
+        expression = new ASTEqual(new ASTObjPath("x$"), "A");
+        assertEquals(exp, expression);
+
+
+        exp = ExpressionFactory.exp("obj:x$ = $name", "A");
+        expression = new ASTEqual(new ASTObjPath("x$"), "A");
+        assertEquals(exp, expression);
+    }
+
+    @Test
+    public void testExpressionWithDollarSign() throws Exception {
+        tTest = new TableHelper(dbHelper, "Cay2666");
+        tTest.setColumns("ID", "NAME$");
+        tTest.insert(1, "st.One");
+
+        Expression expression = ExpressionFactory.exp("name$ = 'st.One'");
+        List<CAY2666> cay2666List = ObjectSelect.query(CAY2666.class).where(expression).select(context);
+        assertEquals(1, cay2666List.size());
+
+        expression = ExpressionFactory.exp("obj:name$ = 'st.Two'");
+        cay2666List = ObjectSelect.query(CAY2666.class).where(expression).select(context);
+        assertEquals(0, cay2666List.size());
+
+        tTest.insert(2, "st.Two");
+
+        expression = ExpressionFactory.exp("db:NAME$ = 'st.Two'");
+        cay2666List = ObjectSelect.query(CAY2666.class).where(expression).select(context);
+        assertEquals(1, cay2666List.size());
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetchIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetchIT.java
index cede796..d79dfcf 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetchIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextDisjointByIdPrefetchIT.java
@@ -22,6 +22,7 @@
 import org.apache.cayenne.ValueHolder;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SQLSelect;
 import org.apache.cayenne.query.SortOrder;
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
@@ -139,6 +140,39 @@
     }
 
     @Test
+    public void testOneToMany_SQLSelect() throws Exception {
+        createArtistWithTwoPaintingsDataSet();
+
+        List<Artist> result = SQLSelect.query(Artist.class, "SELECT "
+                + "#result('ARTIST_NAME' 'String'), "
+                + "#result('DATE_OF_BIRTH' 'java.util.Date'), "
+                + "#result('t0.ARTIST_ID' 'int' '' 'ARTIST_ID') "
+                + "FROM ARTIST t0")
+                .addPrefetch(Artist.PAINTING_ARRAY.disjointById())
+                .select(context);
+
+        queryInterceptor.runWithQueriesBlocked(() -> {
+            assertFalse(result.isEmpty());
+            Artist b1 = result.get(0);
+
+            @SuppressWarnings("unchecked")
+            List<Painting> toMany = (List<Painting>) b1.readPropertyDirectly(Artist.PAINTING_ARRAY.getName());
+            assertNotNull(toMany);
+            assertFalse(((ValueHolder) toMany).isFault());
+            assertEquals(2, toMany.size());
+
+            List<String> names = new ArrayList<>();
+            for (Painting b : toMany) {
+                assertEquals(PersistenceState.COMMITTED, b.getPersistenceState());
+                names.add(b.getPaintingTitle());
+            }
+
+            assertTrue(names.contains("Y1"));
+            assertTrue(names.contains("Y2"));
+        });
+    }
+
+    @Test
     public void testManyToOne() throws Exception {
         createArtistWithTwoPaintingsDataSet();
 
@@ -156,6 +190,32 @@
     }
 
     @Test
+    public void testManyToOne_SQLSelect() throws Exception {
+        createArtistWithTwoPaintingsDataSet();
+
+        List<Painting> result = SQLSelect.query(Painting.class, "SELECT "
+                + "#result('ESTIMATED_PRICE' 'BigDecimal'), "
+                + "#result('PAINTING_TITLE' 'String'), "
+                + "#result('PAINTING_DESCRIPTION' 'String'), "
+                + "#result('GALLERY_ID' 'int'), "
+                + "#result('PAINTING_ID' 'int'), "
+                + "#result('ARTIST_ID' 'int') "
+                + "FROM PAINTING")
+                .addPrefetch(Painting.TO_ARTIST.disjointById())
+                .select(context);
+
+        queryInterceptor.runWithQueriesBlocked(() -> {
+            assertFalse(result.isEmpty());
+            Painting p1 = result.get(0);
+            assertEquals(PersistenceState.COMMITTED, p1.getPersistenceState());
+
+            assertNotNull(p1.getToArtist());
+            assertEquals(PersistenceState.COMMITTED, p1.getToArtist().getPersistenceState());
+            assertEquals("X", p1.getToArtist().getArtistName());
+        });
+    }
+
+    @Test
     public void testFetchLimit() throws Exception {
         createThreeArtistsWithPlentyOfPaintingsDataSet();
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java
index 10db03c..76287b8 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEJBQLNumericalFunctionalIT.java
@@ -64,15 +64,15 @@
     public void testABS() {
 
         BigDecimalEntity o1 = context.newObject(BigDecimalEntity.class);
-        o1.setBigDecimalField(new BigDecimal("4.1"));
+        o1.setBigDecimalNumeric(new BigDecimal("4.1"));
 
         BigDecimalEntity o2 = context.newObject(BigDecimalEntity.class);
-        o2.setBigDecimalField(new BigDecimal("-5.1"));
+        o2.setBigDecimalNumeric(new BigDecimal("-5.1"));
 
         context.commitChanges();
 
         EJBQLQuery query = new EJBQLQuery(
-                "SELECT d FROM BigDecimalEntity d WHERE ABS(d.bigDecimalField) < 5.0");
+                "SELECT d FROM BigDecimalEntity d WHERE ABS(d.bigDecimalNumeric) < 5.0");
         List<?> objects = context.performQuery(query);
         assertEquals(1, objects.size());
         assertTrue(objects.contains(o1));
@@ -82,15 +82,15 @@
     public void testSQRT() {
 
         BigDecimalEntity o1 = context.newObject(BigDecimalEntity.class);
-        o1.setBigDecimalField(new BigDecimal("9"));
+        o1.setBigDecimalNumeric(new BigDecimal("9"));
 
         BigDecimalEntity o2 = context.newObject(BigDecimalEntity.class);
-        o2.setBigDecimalField(new BigDecimal("16"));
+        o2.setBigDecimalNumeric(new BigDecimal("16"));
 
         context.commitChanges();
 
         EJBQLQuery query = new EJBQLQuery(
-                "SELECT d FROM BigDecimalEntity d WHERE SQRT(d.bigDecimalField) > 3.1");
+                "SELECT d FROM BigDecimalEntity d WHERE SQRT(d.bigDecimalNumeric) > 3.1");
         List<?> objects = context.performQuery(query);
         assertEquals(1, objects.size());
         assertTrue(objects.contains(o2));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java
index 95edd8c..e65289d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextEntityWithMeaningfulPKIT.java
@@ -204,4 +204,32 @@
         pkObj2.setPk("123");
         context.commitChanges();
     }
+
+    @Test
+    @Ignore
+    public void test_MeaningfulPkInsertDeleteCascade() {
+        // setup
+        MeaningfulPKTest1 obj = context.newObject(MeaningfulPKTest1.class);
+        obj.setPkAttribute(1000);
+        obj.setDescr("aaa");
+        context.commitChanges();
+
+        // must be able to set reverse relationship
+        MeaningfulPKDep dep = context.newObject(MeaningfulPKDep.class);
+        dep.setToMeaningfulPK(obj);
+        dep.setPk(10);
+        context.commitChanges();
+
+        // test
+        context.deleteObject(obj);
+
+        MeaningfulPKTest1 obj2 = context.newObject(MeaningfulPKTest1.class);
+        obj2.setPkAttribute(1000);
+        obj2.setDescr("bbb");
+
+        MeaningfulPKDep dep2 = context.newObject(MeaningfulPKDep.class);
+        dep2.setToMeaningfulPK(obj2);
+        dep2.setPk(10);
+        context.commitChanges();
+    }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPerformQueryAPIIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPerformQueryAPIIT.java
index 6df993f..b95e229 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPerformQueryAPIIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/DataContextPerformQueryAPIIT.java
@@ -207,12 +207,9 @@
         List<?> artists = context.performQuery("QueryWithLocalCache", true);
         assertEquals(2, artists.size());
 
-        queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
-
-            public void execute() {
-                List<?> artists1 = context.performQuery("QueryWithLocalCache", false);
-                assertEquals(2, artists1.size());
-            }
+        queryInterceptor.runWithQueriesBlocked(() -> {
+            List<?> artists1 = context.performQuery("QueryWithLocalCache", false);
+            assertEquals(2, artists1.size());
         });
     }
 
@@ -223,12 +220,9 @@
         List<?> artists = context.performQuery("QueryWithSharedCache", true);
         assertEquals(2, artists.size());
 
-        queryInterceptor.runWithQueriesBlocked(new UnitTestClosure() {
-
-            public void execute() {
-                List<?> artists1 = context2.performQuery("QueryWithSharedCache", false);
-                assertEquals(2, artists1.size());
-            }
+        queryInterceptor.runWithQueriesBlocked(() -> {
+            List<?> artists1 = context2.performQuery("QueryWithSharedCache", false);
+            assertEquals(2, artists1.size());
         });
     }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java
index 18ad1d8..d422d5a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/JointPrefetchIT.java
@@ -130,7 +130,7 @@
 
         final List<Artist> objects = ObjectSelect.query(Artist.class)
                 .limit(2).offset(0)
-                .orderBy("db:ARTIST_ID", SortOrder.ASCENDING)
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
                 .prefetch(Artist.PAINTING_ARRAY.joint())
                 .select(context);
 
@@ -395,8 +395,10 @@
     public void testJointPrefetchSQLSelectToMany() throws Exception {
         createJointPrefetchDataSet();
 
-        @SuppressWarnings("unchecked")
-        final List<Artist> objects = SQLSelect.query(Artist.class, "SELECT "
+        List<Artist> objects = SQLSelect.query(Artist.class, "SELECT "
+                + "#result('ESTIMATED_PRICE' 'BigDecimal' '' 'paintingArray.ESTIMATED_PRICE'), "
+                + "#result('PAINTING_TITLE' 'String' '' 'paintingArray.PAINTING_TITLE'), "
+                + "#result('GALLERY_ID' 'int' '' 'paintingArray.GALLERY_ID'), "
                 + "#result('PAINTING_ID' 'int' '' 'paintingArray.PAINTING_ID'), "
                 + "#result('ARTIST_NAME' 'String'), "
                 + "#result('DATE_OF_BIRTH' 'java.util.Date'), "
@@ -405,8 +407,8 @@
                 + "WHERE t0.ARTIST_ID = t1.ARTIST_ID")
                 .addPrefetch(Artist.PAINTING_ARRAY.joint())
                 .select(context);
+
         queryInterceptor.runWithQueriesBlocked(() -> {
-            assertNotNull(objects);
             assertEquals(2, objects.size());
 
             for (Artist artist : objects) {
@@ -414,6 +416,7 @@
                 assertTrue(paintings.size() > 0);
                 for (Painting painting : paintings) {
                     assertEquals(PersistenceState.COMMITTED, painting.getPersistenceState());
+                    assertNotNull(painting.getPaintingTitle());
                 }
             }
         });
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
index 7189dfb..3d4398d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/NumericTypesIT.java
@@ -41,18 +41,21 @@
 import org.apache.cayenne.testdo.numeric_types.LongEntity;
 import org.apache.cayenne.testdo.numeric_types.SmallintTestEntity;
 import org.apache.cayenne.testdo.numeric_types.TinyintTestEntity;
+import org.apache.cayenne.unit.di.CommitStats;
+import org.apache.cayenne.unit.di.DataChannelInterceptor;
+import org.apache.cayenne.unit.di.DataChannelSyncStats;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
 import org.junit.Before;
+import org.junit.Rule;
 import org.junit.Test;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNotSame;
-import static org.junit.Assert.assertSame;
+import static org.junit.Assert.*;
 
 /**
+ *
  */
 @UseServerRuntime(CayenneProjects.NUMERIC_TYPES_PROJECT)
 public class NumericTypesIT extends ServerCase {
@@ -69,11 +72,15 @@
     @Inject
     protected DBHelper dbHelper;
 
+    private final CommitStats commitStats = new CommitStats(() -> runtime.getDataDomain());
+
     protected TableHelper tSmallintTest;
     protected TableHelper tTinyintTest;
 
     @Before
-    public void setUp() throws Exception {
+    public void before() {
+        commitStats.before();
+
         tSmallintTest = new TableHelper(dbHelper, "SMALLINT_TEST");
         tSmallintTest.setColumns("ID", "SMALLINT_COL");
 
@@ -81,6 +88,11 @@
         tTinyintTest.setColumns("ID", "TINYINT_COL");
     }
 
+    @After
+    public void after() {
+        commitStats.after();
+    }
+
     protected void createShortDataSet() throws Exception {
         tSmallintTest.insert(1, 9999);
         tSmallintTest.insert(2, 3333);
@@ -128,21 +140,70 @@
     }
 
     @Test
-    public void testBigDecimal() throws Exception {
+    public void testBigDecimal_Decimal() {
 
-        BigDecimalEntity test = context.newObject(BigDecimalEntity.class);
+        // this matches the column scale exactly
+        String v1 = "7890.123456";
+        // this has lower scale than the column
+        String v2 = "7890.1";
+        String v2_padded = "7890.100000";
 
-        BigDecimal i = new BigDecimal("1234567890.44");
-        test.setBigDecimalField(i);
-        context.commitChanges();
+        BigDecimalEntity o = context.newObject(BigDecimalEntity.class);
+        o.setBigDecimalDecimal(new BigDecimal(v1));
+        o.getObjectContext().commitChanges();
+        assertEquals(1, commitStats.getCommitCount());
+        BigDecimalEntity o1 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertEquals(v1, o1.getBigDecimalDecimal().toString());
 
-        BigDecimalEntity testRead = ObjectSelect.query(BigDecimalEntity.class)
-                .selectFirst(context);
-        assertNotNull(testRead.getBigDecimalField());
-        assertEquals(i, testRead.getBigDecimalField());
+        o.setBigDecimalDecimal(new BigDecimal(v2));
+        o.getObjectContext().commitChanges();
+        BigDecimalEntity o2 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertEquals(v2_padded, o2.getBigDecimalDecimal().toString());
+        assertEquals(2, commitStats.getCommitCount());
 
-        test.setBigDecimalField(null);
-        context.commitChanges();
+        o2.setBigDecimalDecimal(new BigDecimal(v2));
+        o2.getObjectContext().commitChanges();
+        assertEquals("Commit was not expected. The difference is purely in value padding", 2, commitStats.getCommitCount());
+        BigDecimalEntity o3 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertEquals(v2_padded, o3.getBigDecimalDecimal().toString());
+
+        o3.setBigDecimalDecimal(null);
+        o3.getObjectContext().commitChanges();
+        assertEquals(3, commitStats.getCommitCount());
+        BigDecimalEntity o4 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertNull(o4.getBigDecimalDecimal());
+    }
+
+    @Test
+    public void testBigDecimal_Numeric() {
+
+        String v1 = "1234567890.44";
+        String v2 = "1234567890.4";
+        String v2_padded = "1234567890.40";
+
+        BigDecimalEntity o = context.newObject(BigDecimalEntity.class);
+        o.setBigDecimalNumeric(new BigDecimal(v1));
+        o.getObjectContext().commitChanges();
+        assertEquals(1, commitStats.getCommitCount());
+        BigDecimalEntity o1 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertEquals(v1, o1.getBigDecimalNumeric().toString());
+
+        o1.setBigDecimalNumeric(new BigDecimal(v2));
+        o1.getObjectContext().commitChanges();
+        assertEquals(2, commitStats.getCommitCount());
+        BigDecimalEntity o2 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertEquals(v2_padded, o2.getBigDecimalNumeric().toString());
+
+        o2.setBigDecimalNumeric(new BigDecimal(v2));
+        assertEquals("Commit was not expected. The difference is purely in value padding", 2, commitStats.getCommitCount());
+        BigDecimalEntity o3 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertEquals(v2_padded, o3.getBigDecimalNumeric().toString());
+
+        o3.setBigDecimalNumeric(null);
+        o3.getObjectContext().commitChanges();
+        assertEquals(3, commitStats.getCommitCount());
+        BigDecimalEntity o4 = ObjectSelect.query(BigDecimalEntity.class).selectFirst(runtime.newContext());
+        assertNull(o4.getBigDecimalNumeric());
     }
 
     @Test
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java
new file mode 100644
index 0000000..52ce753
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/flush/EffectiveOpIdTest.java
@@ -0,0 +1,45 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.flush;
+
+import java.util.Collections;
+
+import org.apache.cayenne.ObjectId;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class EffectiveOpIdTest {
+
+    @Test
+    public void testEqualsTempGeneratedId() {
+        ObjectId id1 = ObjectId.of("test");
+        id1.getReplacementIdMap().put("pk", new IdGenerationMarker(id1));
+        EffectiveOpId effectiveOpId1 = new EffectiveOpId("test", Collections.singletonMap("pk", new IdGenerationMarker(id1)));
+
+        EffectiveOpId effectiveOpId2 = new EffectiveOpId("test", Collections.singletonMap("pk", ObjectIdValueSupplier.getFor(id1, "pk")));
+
+        assertEquals(effectiveOpId1, effectiveOpId2);
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/BatchActionLockingIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/BatchActionLockingIT.java
index 7a7bdda..be1314d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/BatchActionLockingIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/jdbc/BatchActionLockingIT.java
@@ -79,13 +79,13 @@
 		Collection<String> nullAttributeNames = Collections.singleton("NAME");
 
 		Map<String, Object> qualifierSnapshot = new HashMap<>();
-		qualifierSnapshot.put("LOCKING_TEST_ID", new Integer(1));
+		qualifierSnapshot.put("LOCKING_TEST_ID", 1);
 
 		DeleteBatchQuery batchQuery = new DeleteBatchQuery(dbEntity, qualifierAttributes, nullAttributeNames, 5);
 		batchQuery.setUsingOptimisticLocking(true);
 		batchQuery.add(qualifierSnapshot);
 
-		DeleteBatchTranslator batchQueryBuilder = new DeleteBatchTranslator(batchQuery, adapter, null);
+		DeleteBatchTranslator batchQueryBuilder = new DeleteBatchTranslator(batchQuery, adapter);
 
 		MockConnection mockConnection = new MockConnection();
 		PreparedStatementResultSetHandler preparedStatementResultSetHandler = mockConnection
@@ -127,7 +127,7 @@
 		batchQuery.setUsingOptimisticLocking(true);
 		batchQuery.add(qualifierSnapshot);
 
-		DeleteBatchTranslator batchQueryBuilder = new DeleteBatchTranslator(batchQuery, adapter, null);
+		DeleteBatchTranslator batchQueryBuilder = new DeleteBatchTranslator(batchQuery, adapter);
 
 		MockConnection mockConnection = new MockConnection();
 		PreparedStatementResultSetHandler preparedStatementResultSetHandler = mockConnection
@@ -145,7 +145,7 @@
 		try {
 			action.runAsIndividualQueries(mockConnection, batchQueryBuilder, new MockOperationObserver(), generatesKeys);
 			fail("No OptimisticLockingFailureException thrown.");
-		} catch (OptimisticLockException e) {
+		} catch (OptimisticLockException ignore) {
 		}
 		assertEquals(0, mockConnection.getNumberCommits());
 		assertEquals(0, mockConnection.getNumberRollbacks());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/DeleteBuilderTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/DeleteBuilderTest.java
new file mode 100644
index 0000000..be6aaba
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/DeleteBuilderTest.java
@@ -0,0 +1,57 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.DeleteNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.junit.Test;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+public class DeleteBuilderTest {
+
+    @Test
+    public void testDelete() {
+        DeleteBuilder builder = new DeleteBuilder("test");
+        Node node = builder.build();
+        assertThat(node, instanceOf(DeleteNode.class));
+        assertSQL("DELETE FROM test", node);
+    }
+
+    @Test
+    public void testDeleteWithQualifier() {
+        DeleteBuilder builder = new DeleteBuilder("test");
+        Node node = builder.where(
+                column("col1").eq(value(1))
+                        .and(column("col2").eq(value("test")))
+                        .and(column("col3").eq(value(null)))
+        ).build();
+        assertThat(node, instanceOf(DeleteNode.class));
+        assertSQL("DELETE FROM test WHERE ( ( col1 = 1 ) AND ( col2 = 'test' ) ) AND ( col3 IS NULL )", node);
+    }
+
+    private void assertSQL(String expected, Node node) {
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new StringBuilderAppendable());
+        node.visit(visitor);
+        assertEquals(expected, visitor.getSQLString());
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/InsertBuilderTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/InsertBuilderTest.java
new file mode 100644
index 0000000..229d6a1
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/InsertBuilderTest.java
@@ -0,0 +1,87 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.InsertNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.junit.Test;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+public class InsertBuilderTest {
+
+    @Test
+    public void testInsert() {
+        InsertBuilder builder = new InsertBuilder("test");
+        Node node = builder.build();
+        assertThat(node, instanceOf(InsertNode.class));
+        assertSQL("INSERT INTO test", node);
+    }
+
+    @Test
+    public void testInsertWithColumns() {
+        InsertBuilder builder = new InsertBuilder("test");
+        builder
+                .column(column("col1"))
+                .column(column("col2"))
+                .column(column("col3"));
+        Node node = builder.build();
+
+        assertThat(node, instanceOf(InsertNode.class));
+        assertSQL("INSERT INTO test( col1, col2, col3)", node);
+    }
+
+    @Test
+    public void testInsertWithValues() {
+        InsertBuilder builder = new InsertBuilder("test");
+        builder
+                .value(value(1))
+                .value(value("test"))
+                .value(value(null));
+        Node node = builder.build();
+
+        assertThat(node, instanceOf(InsertNode.class));
+        assertSQL("INSERT INTO test VALUES( 1, 'test', NULL)", node);
+    }
+
+    @Test
+    public void testInsertWithColumnsAndValues() {
+        InsertBuilder builder = new InsertBuilder("test");
+        builder
+                .column(column("col1"))
+                .value(value(1))
+                .column(column("col2"))
+                .value(value("test"))
+                .column(column("col3"))
+                .value(value(null));
+        Node node = builder.build();
+
+        assertThat(node, instanceOf(InsertNode.class));
+        assertSQL("INSERT INTO test( col1, col2, col3) VALUES( 1, 'test', NULL)", node);
+    }
+
+    private void assertSQL(String expected, Node node) {
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new StringBuilderAppendable());
+        node.visit(visitor);
+        assertEquals(expected, visitor.getSQLString());
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/UpdateBuilderTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/UpdateBuilderTest.java
new file mode 100644
index 0000000..d0c1133
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/sqlbuilder/UpdateBuilderTest.java
@@ -0,0 +1,73 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.sqlbuilder;
+
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.access.sqlbuilder.sqltree.UpdateNode;
+import org.junit.Test;
+
+import static org.apache.cayenne.access.sqlbuilder.SQLBuilder.*;
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class UpdateBuilderTest {
+
+    @Test
+    public void testUpdate() {
+        UpdateBuilder builder = new UpdateBuilder("test");
+        Node node = builder.build();
+        assertThat(node, instanceOf(UpdateNode.class));
+        assertSQL("UPDATE test", node);
+    }
+
+    @Test
+    public void testUpdateWithFields() {
+        UpdateBuilder builder = new UpdateBuilder("test");
+        builder
+                .set(column("col1").eq(value(1)))
+                .set(column("col2").eq(value("test")))
+                .set(column("col3").eq(value(null)));
+        Node node = builder.build();
+
+        assertThat(node, instanceOf(UpdateNode.class));
+        assertSQL("UPDATE test SET col1 = 1, col2 = 'test', col3 = NULL", node);
+    }
+
+    @Test
+    public void testUpdateWithWhere() {
+        UpdateBuilder builder = new UpdateBuilder("test");
+        builder
+                .set(column("col1").eq(value(1)))
+                .where(column("id").eq(value(123L)));
+        Node node = builder.build();
+
+        assertThat(node, instanceOf(UpdateNode.class));
+        assertSQL("UPDATE test SET col1 = 1 WHERE id = 123", node);
+    }
+
+    private void assertSQL(String expected, Node node) {
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new StringBuilderAppendable());
+        node.visit(visitor);
+        assertEquals(expected, visitor.getSQLString());
+    }
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslatorIT.java
index 262c4a6..a921a04 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DeleteBatchTranslatorIT.java
@@ -19,6 +19,11 @@
 
 package org.apache.cayenne.access.translator.batch;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
 import org.apache.cayenne.configuration.server.ServerRuntime;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.JdbcAdapter;
@@ -34,11 +39,6 @@
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertSame;
@@ -60,32 +60,34 @@
     private AdhocObjectFactory objectFactory;
 
     @Test
-    public void testConstructor() throws Exception {
+    public void testConstructor() {
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
 
-        DeleteBatchTranslator builder = new DeleteBatchTranslator(mock(DeleteBatchQuery.class), adapter, null);
+        DeleteBatchQuery query = mock(DeleteBatchQuery.class);
+        DeleteBatchTranslator builder = new DeleteBatchTranslator(query, adapter);
 
-        assertSame(adapter, builder.adapter);
+        assertSame(adapter, builder.context.getAdapter());
+        assertSame(query, builder.context.getQuery());
     }
 
     @Test
-    public void testCreateSqlString() throws Exception {
+    public void testCreateSqlString() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
 
         List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
 
-        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.<String> emptySet(), 1);
+        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.emptySet(), 1);
 
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
-        DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+        DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter);
         String generatedSql = builder.getSql();
         assertNotNull(generatedSql);
         assertEquals("DELETE FROM " + entity.getName() + " WHERE LOCKING_TEST_ID = ?", generatedSql);
     }
 
     @Test
-    public void testCreateSqlStringWithNulls() throws Exception {
+    public void testCreateSqlStringWithNulls() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
 
@@ -97,14 +99,14 @@
         DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, nullAttributes, 1);
 
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
-        DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+        DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter);
         String generatedSql = builder.getSql();
         assertNotNull(generatedSql);
-        assertEquals("DELETE FROM " + entity.getName() + " WHERE LOCKING_TEST_ID = ? AND NAME IS NULL", generatedSql);
+        assertEquals("DELETE FROM " + entity.getName() + " WHERE ( LOCKING_TEST_ID = ? ) AND ( NAME IS NULL )", generatedSql);
     }
 
     @Test
-    public void testCreateSqlStringWithIdentifiersQuote() throws Exception {
+    public void testCreateSqlStringWithIdentifiersQuote() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
         try {
@@ -112,9 +114,9 @@
             entity.getDataMap().setQuotingSQLIdentifiers(true);
             List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
 
-            DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.<String> emptySet(), 1);
+            DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.emptySet(), 1);
             JdbcAdapter adapter = (JdbcAdapter) this.adapter;
-            DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+            DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter);
             String generatedSql = builder.getSql();
 
             String charStart = unitAdapter.getIdentifiersStartQuote();
@@ -130,7 +132,7 @@
     }
 
     @Test
-    public void testCreateSqlStringWithNullsWithIdentifiersQuote() throws Exception {
+    public void testCreateSqlStringWithNullsWithIdentifiersQuote() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
         try {
@@ -146,15 +148,15 @@
 
             JdbcAdapter adapter = (JdbcAdapter) this.adapter;
 
-            DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+            DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter);
             String generatedSql = builder.getSql();
 
             String charStart = unitAdapter.getIdentifiersStartQuote();
             String charEnd = unitAdapter.getIdentifiersEndQuote();
             assertNotNull(generatedSql);
 
-            assertEquals("DELETE FROM " + charStart + entity.getName() + charEnd + " WHERE " + charStart
-                    + "LOCKING_TEST_ID" + charEnd + " = ? AND " + charStart + "NAME" + charEnd + " IS NULL",
+            assertEquals("DELETE FROM " + charStart + entity.getName() + charEnd + " WHERE ( " + charStart
+                    + "LOCKING_TEST_ID" + charEnd + " = ? ) AND ( " + charStart + "NAME" + charEnd + " IS NULL )",
                     generatedSql);
         } finally {
             entity.getDataMap().setQuotingSQLIdentifiers(false);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslatorIT.java
index e130199..bf29cda 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/InsertBatchTranslatorIT.java
@@ -53,17 +53,20 @@
     private AdhocObjectFactory objectFactory;
 
     @Test
-    public void testConstructor() throws Exception {
+    public void testConstructor() {
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
 
-        InsertBatchTranslator builder = new InsertBatchTranslator(mock(InsertBatchQuery.class), adapter);
+        InsertBatchQuery query = mock(InsertBatchQuery.class);
+        InsertBatchTranslator builder = new InsertBatchTranslator(query, adapter);
 
-        assertSame(adapter, builder.adapter);
+        assertSame(adapter, builder.context.getAdapter());
+        assertSame(query, builder.context.getQuery());
     }
 
     @Test
-    public void testCreateSqlString() throws Exception {
-        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+    public void testCreateSqlString() {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver()
+                .getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
 
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
@@ -71,12 +74,13 @@
         InsertBatchTranslator builder = new InsertBatchTranslator(insertQuery, adapter);
         String generatedSql = builder.getSql();
         assertNotNull(generatedSql);
-        assertEquals("INSERT INTO " + entity.getName() + " (DESCRIPTION, INT_COLUMN_NOTNULL, INT_COLUMN_NULL, LOCKING_TEST_ID, NAME) VALUES (?, ?, ?, ?, ?)",
+        assertEquals("INSERT INTO " + entity.getName() + "( DESCRIPTION, INT_COLUMN_NOTNULL, INT_COLUMN_NULL, LOCKING_TEST_ID, NAME) " +
+                        "VALUES( ?, ?, ?, ?, ?)",
                 generatedSql);
     }
 
     @Test
-    public void testCreateSqlStringWithIdentifiersQuote() throws Exception {
+    public void testCreateSqlStringWithIdentifiersQuote() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
         try {
@@ -92,11 +96,11 @@
             String charEnd = unitAdapter.getIdentifiersEndQuote();
             assertNotNull(generatedSql);
             assertEquals("INSERT INTO " + charStart + entity.getName() + charEnd
-                    + " (" + charStart + "DESCRIPTION" + charEnd + ", "
+                    + "( " + charStart + "DESCRIPTION" + charEnd + ", "
                     + charStart + "INT_COLUMN_NOTNULL" + charEnd + ", "
                     + charStart + "INT_COLUMN_NULL" + charEnd + ", "
                     + charStart + "LOCKING_TEST_ID" + charEnd + ", "
-                    + charStart + "NAME" + charEnd + ") VALUES (?, ?, ?, ?, ?)", generatedSql);
+                    + charStart + "NAME" + charEnd + ") VALUES( ?, ?, ?, ?, ?)", generatedSql);
         } finally {
             entity.getDataMap().setQuotingSQLIdentifiers(false);
         }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java
index d32b849..e66fb58 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/SoftDeleteBatchTranslatorIT.java
@@ -18,6 +18,11 @@
  ****************************************************************/
 package org.apache.cayenne.access.translator.batch;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.PersistenceState;
 import org.apache.cayenne.access.DataNode;
@@ -40,11 +45,6 @@
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
@@ -100,7 +100,7 @@
         DeleteBatchTranslator builder = createTranslator(deleteQuery);
         String generatedSql = builder.getSql();
         assertNotNull(generatedSql);
-        assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ID = ? AND NAME IS NULL", generatedSql);
+        assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ( ID = ? ) AND ( NAME IS NULL )", generatedSql);
     }
 
     @Test
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslatorIT.java
index e604c77..03719c5 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/UpdateBatchTranslatorIT.java
@@ -19,11 +19,17 @@
 
 package org.apache.cayenne.access.translator.batch;
 
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
 import org.apache.cayenne.configuration.server.ServerRuntime;
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.dba.JdbcAdapter;
 import org.apache.cayenne.di.AdhocObjectFactory;
 import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.query.UpdateBatchQuery;
 import org.apache.cayenne.testdo.locking.SimpleLockingTestEntity;
@@ -33,11 +39,6 @@
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertSame;
@@ -59,67 +60,70 @@
     private AdhocObjectFactory objectFactory;
 
     @Test
-    public void testConstructor() throws Exception {
+    public void testConstructor() {
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
-        UpdateBatchTranslator builder = new UpdateBatchTranslator(mock(UpdateBatchQuery.class), adapter, null);
-        assertSame(adapter, builder.adapter);
+        UpdateBatchQuery query = mock(UpdateBatchQuery.class);
+        UpdateBatchTranslator builder = new UpdateBatchTranslator(query, adapter);
+
+        assertSame(adapter, builder.context.getAdapter());
+        assertSame(query, builder.context.getQuery());
     }
 
     @Test
-    public void testCreateSqlString() throws Exception {
+    public void testCreateSqlString() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
 
-        List idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
-        List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+        List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
+        List<DbAttribute> updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
 
         UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes,
-                Collections.<String> emptySet(), 1);
+                Collections.emptySet(), 1);
 
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
-        UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+        UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter);
         String generatedSql = builder.getSql();
         assertNotNull(generatedSql);
         assertEquals("UPDATE " + entity.getName() + " SET DESCRIPTION = ? WHERE LOCKING_TEST_ID = ?", generatedSql);
     }
 
     @Test
-    public void testCreateSqlStringWithNulls() throws Exception {
+    public void testCreateSqlStringWithNulls() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
 
-        List idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"), entity.getAttribute("NAME"));
+        List<DbAttribute> idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"), entity.getAttribute("NAME"));
 
-        List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+        List<DbAttribute> updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
 
-        Collection nullAttributes = Collections.singleton("NAME");
+        Collection<String> nullAttributes = Collections.singleton("NAME");
 
         UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes, nullAttributes, 1);
 
         DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
-        UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+        UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter);
         String generatedSql = builder.getSql();
         assertNotNull(generatedSql);
 
-        assertEquals("UPDATE " + entity.getName() + " SET DESCRIPTION = ? WHERE LOCKING_TEST_ID = ? AND NAME IS NULL",
+        assertEquals("UPDATE " + entity.getName() + " SET DESCRIPTION = ? WHERE ( LOCKING_TEST_ID = ? ) AND ( NAME IS NULL )",
                 generatedSql);
     }
 
     @Test
-    public void testCreateSqlStringWithIdentifiersQuote() throws Exception {
+    public void testCreateSqlStringWithIdentifiersQuote() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
         try {
 
             entity.getDataMap().setQuotingSQLIdentifiers(true);
-            List idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
-            List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+            List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
+            List<DbAttribute> updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
 
             UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes,
-                    Collections.<String> emptySet(), 1);
+                    Collections.emptySet(), 1);
             JdbcAdapter adapter = (JdbcAdapter) this.adapter;
 
-            UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+            UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter);
             String generatedSql = builder.getSql();
 
             String charStart = unitAdapter.getIdentifiersStartQuote();
@@ -135,31 +139,31 @@
     }
 
     @Test
-    public void testCreateSqlStringWithNullsWithIdentifiersQuote() throws Exception {
+    public void testCreateSqlStringWithNullsWithIdentifiersQuote() {
         DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
                 .getDbEntity();
         try {
 
             entity.getDataMap().setQuotingSQLIdentifiers(true);
-            List idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"), entity.getAttribute("NAME"));
+            List<DbAttribute> idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"), entity.getAttribute("NAME"));
 
-            List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+            List<DbAttribute> updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
 
-            Collection nullAttributes = Collections.singleton("NAME");
+            Collection<String> nullAttributes = Collections.singleton("NAME");
 
             UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes,
                     nullAttributes, 1);
             JdbcAdapter adapter = (JdbcAdapter) this.adapter;
 
-            UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+            UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter);
             String generatedSql = builder.getSql();
             assertNotNull(generatedSql);
 
             String charStart = unitAdapter.getIdentifiersStartQuote();
             String charEnd = unitAdapter.getIdentifiersEndQuote();
             assertEquals("UPDATE " + charStart + entity.getName() + charEnd + " SET " + charStart + "DESCRIPTION"
-                    + charEnd + " = ? WHERE " + charStart + "LOCKING_TEST_ID" + charEnd + " = ? AND " + charStart
-                    + "NAME" + charEnd + " IS NULL", generatedSql);
+                    + charEnd + " = ? WHERE ( " + charStart + "LOCKING_TEST_ID" + charEnd + " = ? ) AND ( " + charStart
+                    + "NAME" + charEnd + " IS NULL )", generatedSql);
 
         } finally {
             entity.getDataMap().setQuotingSQLIdentifiers(false);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslatorIT.java
similarity index 98%
rename from cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
rename to cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslatorIT.java
index 581ebfc..f200872 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/DefaultBatchTranslatorIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/DefaultBatchTranslatorIT.java
@@ -17,7 +17,7 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.access.translator.batch;
+package org.apache.cayenne.access.translator.batch.legacy;
 
 import org.apache.cayenne.access.translator.DbAttributeBinding;
 import org.apache.cayenne.dba.DbAdapter;
@@ -39,6 +39,7 @@
 import static org.junit.Assert.assertSame;
 import static org.mockito.Mockito.mock;
 
+@Deprecated
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class DefaultBatchTranslatorIT extends ServerCase {
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/DeleteBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/DeleteBatchTranslatorIT.java
new file mode 100644
index 0000000..2af52db
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/DeleteBatchTranslatorIT.java
@@ -0,0 +1,164 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.JdbcAdapter;
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.DeleteBatchQuery;
+import org.apache.cayenne.testdo.locking.SimpleLockingTestEntity;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+@Deprecated
+@UseServerRuntime(CayenneProjects.LOCKING_PROJECT)
+public class DeleteBatchTranslatorIT extends ServerCase {
+
+    @Inject
+    private ServerRuntime runtime;
+
+    @Inject
+    private DbAdapter adapter;
+
+    @Inject
+    private UnitDbAdapter unitAdapter;
+
+    @Inject
+    private AdhocObjectFactory objectFactory;
+
+    @Test
+    public void testConstructor() throws Exception {
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+
+        DeleteBatchTranslator builder = new DeleteBatchTranslator(mock(DeleteBatchQuery.class), adapter, null);
+
+        assertSame(adapter, builder.adapter);
+    }
+
+    @Test
+    public void testCreateSqlString() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+
+        List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
+
+        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.<String> emptySet(), 1);
+
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+        DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+        assertEquals("DELETE FROM " + entity.getName() + " WHERE LOCKING_TEST_ID = ?", generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithNulls() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+
+        List<DbAttribute> idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"),
+                entity.getAttribute("NAME"));
+
+        Collection<String> nullAttributes = Collections.singleton("NAME");
+
+        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, nullAttributes, 1);
+
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+        DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+        assertEquals("DELETE FROM " + entity.getName() + " WHERE LOCKING_TEST_ID = ? AND NAME IS NULL", generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithIdentifiersQuote() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+        try {
+
+            entity.getDataMap().setQuotingSQLIdentifiers(true);
+            List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
+
+            DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.<String> emptySet(), 1);
+            JdbcAdapter adapter = (JdbcAdapter) this.adapter;
+            DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+            String generatedSql = builder.getSql();
+
+            String charStart = unitAdapter.getIdentifiersStartQuote();
+            String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+            assertNotNull(generatedSql);
+            assertEquals("DELETE FROM " + charStart + entity.getName() + charEnd + " WHERE " + charStart
+                    + "LOCKING_TEST_ID" + charEnd + " = ?", generatedSql);
+        } finally {
+            entity.getDataMap().setQuotingSQLIdentifiers(false);
+        }
+
+    }
+
+    @Test
+    public void testCreateSqlStringWithNullsWithIdentifiersQuote() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+        try {
+
+            entity.getDataMap().setQuotingSQLIdentifiers(true);
+
+            List<DbAttribute> idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"),
+                    entity.getAttribute("NAME"));
+
+            Collection<String> nullAttributes = Collections.singleton("NAME");
+
+            DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, nullAttributes, 1);
+
+            JdbcAdapter adapter = (JdbcAdapter) this.adapter;
+
+            DeleteBatchTranslator builder = new DeleteBatchTranslator(deleteQuery, adapter, null);
+            String generatedSql = builder.getSql();
+
+            String charStart = unitAdapter.getIdentifiersStartQuote();
+            String charEnd = unitAdapter.getIdentifiersEndQuote();
+            assertNotNull(generatedSql);
+
+            assertEquals("DELETE FROM " + charStart + entity.getName() + charEnd + " WHERE " + charStart
+                    + "LOCKING_TEST_ID" + charEnd + " = ? AND " + charStart + "NAME" + charEnd + " IS NULL",
+                    generatedSql);
+        } finally {
+            entity.getDataMap().setQuotingSQLIdentifiers(false);
+        }
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/InsertBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/InsertBatchTranslatorIT.java
new file mode 100644
index 0000000..dad5bd4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/InsertBatchTranslatorIT.java
@@ -0,0 +1,105 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.JdbcAdapter;
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.InsertBatchQuery;
+import org.apache.cayenne.testdo.locking.SimpleLockingTestEntity;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+@Deprecated
+@UseServerRuntime(CayenneProjects.LOCKING_PROJECT)
+public class InsertBatchTranslatorIT extends ServerCase {
+
+    @Inject
+    private ServerRuntime runtime;
+
+    @Inject
+    private DbAdapter adapter;
+
+    @Inject
+    private UnitDbAdapter unitAdapter;
+
+    @Inject
+    private AdhocObjectFactory objectFactory;
+
+    @Test
+    public void testConstructor() throws Exception {
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+
+        InsertBatchTranslator builder = new InsertBatchTranslator(mock(InsertBatchQuery.class), adapter);
+
+        assertSame(adapter, builder.adapter);
+    }
+
+    @Test
+    public void testCreateSqlString() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+        InsertBatchQuery insertQuery = new InsertBatchQuery(entity, 1);
+        InsertBatchTranslator builder = new InsertBatchTranslator(insertQuery, adapter);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+        assertEquals("INSERT INTO " + entity.getName() + " (DESCRIPTION, INT_COLUMN_NOTNULL, INT_COLUMN_NULL, LOCKING_TEST_ID, NAME) VALUES (?, ?, ?, ?, ?)",
+                generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithIdentifiersQuote() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+        try {
+
+            entity.getDataMap().setQuotingSQLIdentifiers(true);
+
+            JdbcAdapter adapter = (JdbcAdapter) this.adapter;
+
+            InsertBatchQuery insertQuery = new InsertBatchQuery(entity, 1);
+            InsertBatchTranslator builder = new InsertBatchTranslator(insertQuery, adapter);
+            String generatedSql = builder.getSql();
+            String charStart = unitAdapter.getIdentifiersStartQuote();
+            String charEnd = unitAdapter.getIdentifiersEndQuote();
+            assertNotNull(generatedSql);
+            assertEquals("INSERT INTO " + charStart + entity.getName() + charEnd
+                    + " (" + charStart + "DESCRIPTION" + charEnd + ", "
+                    + charStart + "INT_COLUMN_NOTNULL" + charEnd + ", "
+                    + charStart + "INT_COLUMN_NULL" + charEnd + ", "
+                    + charStart + "LOCKING_TEST_ID" + charEnd + ", "
+                    + charStart + "NAME" + charEnd + ") VALUES (?, ?, ?, ?, ?)", generatedSql);
+        } finally {
+            entity.getDataMap().setQuotingSQLIdentifiers(false);
+        }
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteBatchTranslatorIT.java
new file mode 100644
index 0000000..36efd4c
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/SoftDeleteBatchTranslatorIT.java
@@ -0,0 +1,181 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.access.DataNode;
+import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.JdbcAdapter;
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.map.DbAttribute;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.DeleteBatchQuery;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SQLTemplate;
+import org.apache.cayenne.test.parallel.ParallelTestContainer;
+import org.apache.cayenne.testdo.soft_delete.SoftDelete;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@Deprecated
+@UseServerRuntime(CayenneProjects.SOFT_DELETE_PROJECT)
+public class SoftDeleteBatchTranslatorIT extends ServerCase {
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    protected DbAdapter adapter;
+
+    @Inject
+    private DataNode dataNode;
+
+    @Inject
+    private UnitDbAdapter unitAdapter;
+
+    @Inject
+    private AdhocObjectFactory objectFactory;
+
+    private DeleteBatchTranslator createTranslator(DeleteBatchQuery query) {
+        JdbcAdapter adapter = objectFactory.newInstance(JdbcAdapter.class, JdbcAdapter.class.getName());
+        return createTranslator(query, adapter);
+    }
+
+    private DeleteBatchTranslator createTranslator(DeleteBatchQuery query, JdbcAdapter adapter) {
+        return (DeleteBatchTranslator) new SoftDeleteTranslatorFactory().translator(query, adapter, null);
+    }
+
+    @Test
+    public void testCreateSqlString() {
+        DbEntity entity = context.getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
+
+        List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("ID"));
+
+        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.emptySet(), 1);
+        DeleteBatchTranslator builder = createTranslator(deleteQuery);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+        assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ID = ?", generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithNulls() {
+        DbEntity entity = context.getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
+
+        List<DbAttribute> idAttributes = Arrays.asList(entity.getAttribute("ID"), entity.getAttribute("NAME"));
+
+        Collection<String> nullAttributes = Collections.singleton("NAME");
+
+        DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, nullAttributes, 1);
+        DeleteBatchTranslator builder = createTranslator(deleteQuery);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+        assertEquals("UPDATE " + entity.getName() + " SET DELETED = ? WHERE ID = ? AND NAME IS NULL", generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithIdentifiersQuote() {
+        DbEntity entity = context.getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
+        try {
+
+            entity.getDataMap().setQuotingSQLIdentifiers(true);
+
+            List<DbAttribute> idAttributes = Collections.singletonList(entity.getAttribute("ID"));
+
+            DeleteBatchQuery deleteQuery = new DeleteBatchQuery(entity, idAttributes, Collections.emptySet(), 1);
+            JdbcAdapter adapter = (JdbcAdapter) this.adapter;
+            DeleteBatchTranslator builder = createTranslator(deleteQuery, adapter);
+            String generatedSql = builder.getSql();
+
+            String charStart = unitAdapter.getIdentifiersStartQuote();
+            String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+            assertNotNull(generatedSql);
+            assertEquals("UPDATE " + charStart + entity.getName() + charEnd + " SET " + charStart + "DELETED" + charEnd
+                    + " = ? WHERE " + charStart + "ID" + charEnd + " = ?", generatedSql);
+        } finally {
+            entity.getDataMap().setQuotingSQLIdentifiers(false);
+        }
+
+    }
+
+    @Test
+    public void testUpdate() throws Exception {
+
+        final DbEntity entity = context.getEntityResolver().getObjEntity(SoftDelete.class).getDbEntity();
+
+        BatchTranslatorFactory oldFactory = dataNode.getBatchTranslatorFactory();
+        try {
+            dataNode.setBatchTranslatorFactory(new SoftDeleteTranslatorFactory());
+
+            final SoftDelete test = context.newObject(SoftDelete.class);
+            test.setName("SoftDeleteBatchQueryBuilderTest");
+            context.commitChanges();
+
+            new ParallelTestContainer() {
+
+                @Override
+                protected void assertResult() {
+                    Expression exp = ExpressionFactory.matchExp("name", test.getName());
+                    assertEquals(1, ObjectSelect.query(SoftDelete.class, exp).selectCount(context));
+
+                    exp = ExpressionFactory.matchDbExp("DELETED", true);
+                    assertEquals(0, ObjectSelect.query(SoftDelete.class, exp).selectCount(context));
+                }
+            }.runTest(200);
+
+            context.deleteObjects(test);
+            assertEquals(test.getPersistenceState(), PersistenceState.DELETED);
+            context.commitChanges();
+
+            new ParallelTestContainer() {
+
+                @Override
+                protected void assertResult() {
+                    Expression exp = ExpressionFactory.matchExp("name", test.getName());
+                    assertEquals(0, ObjectSelect.query(SoftDelete.class, exp).selectCount(context));
+
+                    SQLTemplate template = new SQLTemplate(entity, "SELECT * FROM SOFT_DELETE");
+                    template.setFetchingDataRows(true);
+                    assertEquals(1, context.performQuery(template).size());
+                }
+            }.runTest(200);
+        } finally {
+            context.performQuery(new SQLTemplate(entity, "DELETE FROM SOFT_DELETE"));
+            dataNode.setBatchTranslatorFactory(oldFactory);
+        }
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/UpdateBatchTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/UpdateBatchTranslatorIT.java
new file mode 100644
index 0000000..1a10261
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/batch/legacy/UpdateBatchTranslatorIT.java
@@ -0,0 +1,170 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.access.translator.batch.legacy;
+
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.dba.DbAdapter;
+import org.apache.cayenne.dba.JdbcAdapter;
+import org.apache.cayenne.di.AdhocObjectFactory;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.query.UpdateBatchQuery;
+import org.apache.cayenne.testdo.locking.SimpleLockingTestEntity;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Test;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.mockito.Mockito.mock;
+
+@Deprecated
+@UseServerRuntime(CayenneProjects.LOCKING_PROJECT)
+public class UpdateBatchTranslatorIT extends ServerCase {
+
+    @Inject
+    private ServerRuntime runtime;
+
+    @Inject
+    private DbAdapter adapter;
+
+    @Inject
+    private UnitDbAdapter unitAdapter;
+
+    @Inject
+    private AdhocObjectFactory objectFactory;
+
+    @Test
+    public void testConstructor() throws Exception {
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+        UpdateBatchTranslator builder = new UpdateBatchTranslator(mock(UpdateBatchQuery.class), adapter, null);
+        assertSame(adapter, builder.adapter);
+    }
+
+    @Test
+    public void testCreateSqlString() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+
+        List idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
+        List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+
+        UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes,
+                Collections.<String> emptySet(), 1);
+
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+        UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+        assertEquals("UPDATE " + entity.getName() + " SET DESCRIPTION = ? WHERE LOCKING_TEST_ID = ?", generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithNulls() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+
+        List idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"), entity.getAttribute("NAME"));
+
+        List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+
+        Collection nullAttributes = Collections.singleton("NAME");
+
+        UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes, nullAttributes, 1);
+
+        DbAdapter adapter = objectFactory.newInstance(DbAdapter.class, JdbcAdapter.class.getName());
+        UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+        String generatedSql = builder.getSql();
+        assertNotNull(generatedSql);
+
+        assertEquals("UPDATE " + entity.getName() + " SET DESCRIPTION = ? WHERE LOCKING_TEST_ID = ? AND NAME IS NULL",
+                generatedSql);
+    }
+
+    @Test
+    public void testCreateSqlStringWithIdentifiersQuote() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+        try {
+
+            entity.getDataMap().setQuotingSQLIdentifiers(true);
+            List idAttributes = Collections.singletonList(entity.getAttribute("LOCKING_TEST_ID"));
+            List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+
+            UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes,
+                    Collections.<String> emptySet(), 1);
+            JdbcAdapter adapter = (JdbcAdapter) this.adapter;
+
+            UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+            String generatedSql = builder.getSql();
+
+            String charStart = unitAdapter.getIdentifiersStartQuote();
+            String charEnd = unitAdapter.getIdentifiersEndQuote();
+
+            assertNotNull(generatedSql);
+            assertEquals("UPDATE " + charStart + entity.getName() + charEnd + " SET " + charStart + "DESCRIPTION"
+                    + charEnd + " = ? WHERE " + charStart + "LOCKING_TEST_ID" + charEnd + " = ?", generatedSql);
+
+        } finally {
+            entity.getDataMap().setQuotingSQLIdentifiers(false);
+        }
+    }
+
+    @Test
+    public void testCreateSqlStringWithNullsWithIdentifiersQuote() throws Exception {
+        DbEntity entity = runtime.getDataDomain().getEntityResolver().getObjEntity(SimpleLockingTestEntity.class)
+                .getDbEntity();
+        try {
+
+            entity.getDataMap().setQuotingSQLIdentifiers(true);
+            List idAttributes = Arrays.asList(entity.getAttribute("LOCKING_TEST_ID"), entity.getAttribute("NAME"));
+
+            List updatedAttributes = Collections.singletonList(entity.getAttribute("DESCRIPTION"));
+
+            Collection nullAttributes = Collections.singleton("NAME");
+
+            UpdateBatchQuery updateQuery = new UpdateBatchQuery(entity, idAttributes, updatedAttributes,
+                    nullAttributes, 1);
+            JdbcAdapter adapter = (JdbcAdapter) this.adapter;
+
+            UpdateBatchTranslator builder = new UpdateBatchTranslator(updateQuery, adapter, null);
+            String generatedSql = builder.getSql();
+            assertNotNull(generatedSql);
+
+            String charStart = unitAdapter.getIdentifiersStartQuote();
+            String charEnd = unitAdapter.getIdentifiersEndQuote();
+            assertEquals("UPDATE " + charStart + entity.getName() + charEnd + " SET " + charStart + "DESCRIPTION"
+                    + charEnd + " = ? WHERE " + charStart + "LOCKING_TEST_ID" + charEnd + " = ? AND " + charStart
+                    + "NAME" + charEnd + " IS NULL", generatedSql);
+
+        } finally {
+            entity.getDataMap().setQuotingSQLIdentifiers(false);
+        }
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java
index 6b438ce..523d1fe 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/DescriptorColumnExtractorTest.java
@@ -22,15 +22,18 @@
 import java.sql.Types;
 
 import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
+import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjAttribute;
 import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.reflect.generic.DefaultValueComparisonStrategyFactory;
 import org.junit.Test;
 
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
 
 /**
  * @since 4.2
@@ -59,13 +62,14 @@
         ObjAttribute attribute = new ObjAttribute();
         attribute.setName("not_name");
         attribute.setDbAttributePath("name");
-        attribute.setType("my.type");
+        attribute.setType("java.lang.Integer");
         entity.addAttribute(attribute);
 
         dataMap.addObjEntity(entity);
 
         EntityResolver resolver = new EntityResolver();
         resolver.addDataMap(dataMap);
+        resolver.setValueComparisionStrategyFactory(new DefaultValueComparisonStrategyFactory(mock(ValueObjectTypeRegistry.class)));
 
         DescriptorColumnExtractor extractor = new DescriptorColumnExtractor(context, resolver.getClassDescriptor("mock"));
         extractor.extract();
@@ -83,7 +87,7 @@
         assertNotNull(descriptor0.getDbAttribute());
         assertEquals("name", descriptor0.getDataRowKey());
         assertEquals(Types.VARBINARY, descriptor0.getJdbcType());
-        assertEquals("my.type", descriptor0.getJavaType());
+        assertEquals("java.lang.Integer", descriptor0.getJavaType());
 
         assertNull(descriptor1.getProperty());
         assertNotNull(descriptor1.getNode());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
new file mode 100644
index 0000000..9073e85
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorIT.java
@@ -0,0 +1,66 @@
+package org.apache.cayenne.access.translator.select;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.access.sqlbuilder.SQLGenerationVisitor;
+import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable;
+import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
+import org.apache.cayenne.configuration.server.ServerRuntime;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.compound.CompoundFkTestEntity;
+import org.apache.cayenne.testdo.compound.CompoundPkTestEntity;
+import org.apache.cayenne.unit.UnitDbAdapter;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+@UseServerRuntime(CayenneProjects.COMPOUND_PROJECT)
+public class QualifierTranslatorIT extends ServerCase {
+
+    @Inject
+    private ServerRuntime runtime;
+
+    @Inject
+    private ObjectContext context;
+
+    @Inject
+    protected DBHelper dbHelper;
+
+    @Before
+    public void setUp() throws Exception {
+        TableHelper tCompoundPKTest = new TableHelper(dbHelper, "COMPOUND_PK_TEST");
+        tCompoundPKTest.setColumns("KEY1", "KEY2", "NAME");
+        tCompoundPKTest.insert("PK1", "PK2", "BBB");
+    }
+
+    @Test
+    public void testCompoundPK() {
+        CompoundPkTestEntity testEntity = ObjectSelect.query(CompoundPkTestEntity.class).selectFirst(context);
+        assertNotNull(testEntity);
+
+        ObjectSelect<CompoundFkTestEntity> query = ObjectSelect.query(CompoundFkTestEntity.class)
+                .where(CompoundFkTestEntity.TO_COMPOUND_PK.eq(testEntity))
+                .and(CompoundFkTestEntity.NAME.like("test%"))
+                .and(CompoundFkTestEntity.NAME.contains("a"));
+
+        DefaultSelectTranslator translator
+                = new DefaultSelectTranslator(query, runtime.getDataDomain().getDefaultNode().getAdapter(), context.getEntityResolver());
+
+        QualifierTranslator qualifierTranslator = translator.getContext().getQualifierTranslator();
+
+        Node node = qualifierTranslator.translate(query.getWhere());
+
+        SQLGenerationVisitor visitor = new SQLGenerationVisitor(new StringBuilderAppendable());
+        node.visit(visitor);
+
+        assertEquals(" ( ( ( t0.F_KEY1 = 'PK1' ) AND ( t0.F_KEY2 = 'PK2' ) ) AND t0.NAME LIKE 'test%' ) AND t0.NAME LIKE '%a%'", visitor.getSQLString());
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java
index 731cc6d..aa8aeb7 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/access/translator/select/QualifierTranslatorTest.java
@@ -19,39 +19,21 @@
 
 package org.apache.cayenne.access.translator.select;
 
-import java.util.Arrays;
-
 import org.apache.cayenne.CayenneRuntimeException;
 import org.apache.cayenne.access.sqlbuilder.SQLGenerationVisitor;
 import org.apache.cayenne.access.sqlbuilder.StringBuilderAppendable;
-import org.apache.cayenne.access.sqlbuilder.sqltree.BetweenNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.BitwiseNotNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.ColumnNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.EqualNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.FunctionNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.InNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.LikeNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.Node;
-import org.apache.cayenne.access.sqlbuilder.sqltree.NotEqualNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.NotNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.OpExpressionNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.SelectNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.TextNode;
-import org.apache.cayenne.access.sqlbuilder.sqltree.ValueNode;
+import org.apache.cayenne.access.sqlbuilder.sqltree.*;
 import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.parser.ASTAsterisk;
 import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.map.DataMap;
-import org.apache.cayenne.map.DbAttribute;
-import org.apache.cayenne.map.DbEntity;
-import org.apache.cayenne.map.EntityResolver;
-import org.apache.cayenne.map.ObjAttribute;
-import org.apache.cayenne.map.ObjEntity;
+import org.apache.cayenne.map.*;
 import org.apache.cayenne.query.ObjectSelect;
 import org.junit.Before;
 import org.junit.Test;
 
+import java.util.Arrays;
+
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.junit.Assert.*;
 
@@ -368,7 +350,7 @@
     }
 
     @Test
-    public void translateComparision() {
+    public void translateComparison() {
         {
             Node op = translate("a < 2");
             assertThat(op, instanceOf(OpExpressionNode.class));
@@ -484,7 +466,7 @@
     }
 
     @Test
-    public void translateNullComparision() {
+    public void translateNullComparison() {
         Node or = translate("a > null");
         assertNotNull(or);
         assertThat(or, instanceOf(OpExpressionNode.class));
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DataDomainProviderTest.java b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DataDomainProviderTest.java
index 907b4ab..37a1261 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DataDomainProviderTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DataDomainProviderTest.java
@@ -113,6 +113,8 @@
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.EntitySorter;
 import org.apache.cayenne.map.LifecycleEvent;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
+import org.apache.cayenne.reflect.generic.DefaultValueComparisonStrategyFactory;
 import org.apache.cayenne.resource.ClassLoaderResourceLocator;
 import org.apache.cayenne.resource.Resource;
 import org.apache.cayenne.resource.ResourceLocator;
@@ -245,6 +247,7 @@
 
             ServerModule.contributeValueObjectTypes(binder);
             binder.bind(ValueObjectTypeRegistry.class).to(DefaultValueObjectTypeRegistry.class);
+            binder.bind(ValueComparisonStrategyFactory.class).to(DefaultValueComparisonStrategyFactory.class);
         };
 
         Injector injector = DIBootstrap.createInjector(testModule);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DefaultDbAdapterFactoryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DefaultDbAdapterFactoryTest.java
index c76e549..90096bd 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DefaultDbAdapterFactoryTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/configuration/server/DefaultDbAdapterFactoryTest.java
@@ -44,6 +44,8 @@
 import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.log.Slf4jJdbcEventLogger;
 import org.apache.cayenne.map.DbEntity;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
+import org.apache.cayenne.reflect.generic.DefaultValueComparisonStrategyFactory;
 import org.apache.cayenne.resource.ClassLoaderResourceLocator;
 import org.apache.cayenne.resource.ResourceLocator;
 import org.junit.Test;
@@ -160,6 +162,7 @@
 
             ServerModule.contributeValueObjectTypes(binder);
             binder.bind(ValueObjectTypeRegistry.class).to(DefaultValueObjectTypeRegistry.class);
+            binder.bind(ValueComparisonStrategyFactory.class).to(DefaultValueComparisonStrategyFactory.class);
         };
 
         Injector injector = DIBootstrap.createInjector(testModule);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/dba/AutoAdapterIT.java b/cayenne-server/src/test/java/org/apache/cayenne/dba/AutoAdapterIT.java
index 7f0e191..f6c0658 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/dba/AutoAdapterIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/dba/AutoAdapterIT.java
@@ -22,8 +22,8 @@
 import org.apache.cayenne.access.DataNode;
 import org.apache.cayenne.access.jdbc.SQLTemplateAction;
 import org.apache.cayenne.di.Inject;
-import org.apache.cayenne.di.Provider;
 import org.apache.cayenne.log.NoopJdbcEventLogger;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.SQLTemplate;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
@@ -31,11 +31,10 @@
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertSame;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
 
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class AutoAdapterIT extends ServerCase {
@@ -44,31 +43,72 @@
     private DataNode dataNode;
 
     @Test
-    public void testGetAdapter_Proxy() throws Exception {
-        Provider<DbAdapter> adapterProvider = mock(Provider.class);
-        when(adapterProvider.get()).thenReturn(dataNode.getAdapter());
-
-        AutoAdapter adapter = new AutoAdapter(adapterProvider, NoopJdbcEventLogger.getInstance());
+    public void testGetAdapter_Proxy() {
+        AutoAdapter adapter = new AutoAdapter(() -> dataNode.getAdapter(), NoopJdbcEventLogger.getInstance());
         DbAdapter detected = adapter.getAdapter();
         assertSame(dataNode.getAdapter(), detected);
     }
 
     @Test
     public void testCreateSQLTemplateAction() {
-
-        Provider<DbAdapter> adapterProvider = mock(Provider.class);
-        when(adapterProvider.get()).thenReturn(dataNode.getAdapter());
-
-        AutoAdapter autoAdapter = new AutoAdapter(adapterProvider, NoopJdbcEventLogger.getInstance());
+        AutoAdapter autoAdapter = new AutoAdapter(() -> dataNode.getAdapter(), NoopJdbcEventLogger.getInstance());
 
         SQLTemplateAction action = (SQLTemplateAction) autoAdapter.getAction(new SQLTemplate(Artist.class,
                 "select * from artist"), dataNode);
 
-        // it is important for SQLTemplateAction to be used with unwrapped
-        // adapter, as the
-        // adapter class name is used as a key to the correct SQL template.
+        // it is important for SQLTemplateAction to be used with unwrapped adapter,
+        // as the adapter class name is used as a key to the correct SQL template.
         assertNotNull(action.getAdapter());
         assertFalse(action.getAdapter() instanceof AutoAdapter);
         assertSame(dataNode.getAdapter(), action.getAdapter());
     }
+
+    @Test
+    public void testCorrectProxyMethods() {
+        DbAdapter adapter = dataNode.getAdapter();
+        AutoAdapter autoAdapter = new AutoAdapter(() -> adapter, NoopJdbcEventLogger.getInstance());
+
+        ObjectSelect<Artist> select = ObjectSelect.query(Artist.class);
+
+        // query related methods
+        assertEquals(adapter.supportsBatchUpdates(),
+                autoAdapter.supportsBatchUpdates());
+        assertEquals(adapter.supportsGeneratedKeys(),
+                autoAdapter.supportsGeneratedKeys());
+        assertEquals(adapter.supportsGeneratedKeysForBatchInserts(),
+                autoAdapter.supportsGeneratedKeysForBatchInserts());
+        assertSame(adapter.getBatchTerminator(),
+                autoAdapter.getBatchTerminator());
+        assertSame(adapter.getPkGenerator(),
+                autoAdapter.getPkGenerator());
+        assertSame(adapter.getQuotingStrategy(),
+                autoAdapter.getQuotingStrategy());
+        // returns a new instance for each call
+        assertSame(adapter.getSqlTreeProcessor().getClass(),
+                autoAdapter.getSqlTreeProcessor().getClass());
+        assertSame(adapter.getExtendedTypes(),
+                autoAdapter.getExtendedTypes());
+        assertSame(adapter.getEjbqlTranslatorFactory(),
+                autoAdapter.getEjbqlTranslatorFactory());
+        // returns a new instance for each call
+        assertSame(adapter.getSelectTranslator(select, dataNode.getEntityResolver()).getClass(),
+                autoAdapter.getSelectTranslator(select, dataNode.getEntityResolver()).getClass());
+
+
+        // reverse engineering related methods
+        assertEquals(adapter.supportsCatalogsOnReverseEngineering(),
+                autoAdapter.supportsCatalogsOnReverseEngineering());
+        assertSame(adapter.getSystemCatalogs(),
+                autoAdapter.getSystemCatalogs());
+        assertSame(adapter.getSystemSchemas(),
+                autoAdapter.getSystemSchemas());
+        assertSame(adapter.tableTypeForTable(),
+                autoAdapter.tableTypeForTable());
+        assertSame(adapter.tableTypeForView(),
+                autoAdapter.tableTypeForView());
+
+        // db generation related methods
+        assertEquals(adapter.supportsUniqueConstraints(),
+                autoAdapter.supportsUniqueConstraints());
+    }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java
new file mode 100644
index 0000000..2fe5d0a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/AggregateExpInMemoryEvaluationIT.java
@@ -0,0 +1,146 @@
+package org.apache.cayenne.exp;
+
+import java.math.BigDecimal;
+import java.sql.Types;
+import java.text.DateFormat;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.test.jdbc.DBHelper;
+import org.apache.cayenne.test.jdbc.TableHelper;
+import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Painting;
+import org.apache.cayenne.unit.di.server.CayenneProjects;
+import org.apache.cayenne.unit.di.server.ServerCase;
+import org.apache.cayenne.unit.di.server.UseServerRuntime;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+@UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
+public class AggregateExpInMemoryEvaluationIT extends ServerCase {
+
+    // Format: d/m/YY
+    private static final DateFormat DATE_FORMAT = DateFormat.getDateInstance(DateFormat.SHORT, Locale.US);
+
+    @Inject
+    private DBHelper dbHelper;
+
+    @Inject
+    private DataContext context;
+
+    @Before
+    public void createArtistsDataSet() throws Exception {
+        TableHelper tArtist = new TableHelper(dbHelper, "ARTIST");
+        tArtist.setColumns("ARTIST_ID", "ARTIST_NAME", "DATE_OF_BIRTH");
+        tArtist.setColumnTypes(Types.INTEGER, Types.VARCHAR, Types.DATE);
+
+        java.sql.Date[] dates = new java.sql.Date[5];
+        for(int i=1; i<=5; i++) {
+            dates[i-1] = new java.sql.Date(DATE_FORMAT.parse("1/" + i + "/17").getTime());
+        }
+        for (int i = 1; i <= 20; i++) {
+            tArtist.insert(i, "artist" + i, dates[i % 5]);
+        }
+
+        TableHelper tGallery = new TableHelper(dbHelper, "GALLERY");
+        tGallery.setColumns("GALLERY_ID", "GALLERY_NAME");
+        tGallery.insert(1, "tate modern");
+
+        TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
+        tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID", "ESTIMATED_PRICE");
+        for (int i = 1; i <= 20; i++) {
+            tPaintings.insert(i, "painting" + i, i % 5 + 1, 1, i * 10);
+        }
+        tPaintings.insert(21, "painting21", 2, 1, 1000);
+    }
+
+    @After
+    public void clearArtistsDataSet() throws Exception {
+        for(String table : Arrays.asList("PAINTING", "ARTIST", "GALLERY")) {
+            TableHelper tHelper = new TableHelper(dbHelper, table);
+            tHelper.deleteAll();
+        }
+    }
+
+    @Test
+    public void testCount() {
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        Expression countExp = Artist.PAINTING_ARRAY.count().getExpression();
+
+        for (Artist artist : artists) {
+            assertEquals(artist.getPaintingArray().size(), countExp.evaluate(artist));
+        }
+    }
+
+    @Test
+    public void testMax() {
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        Expression maxExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).max().getExpression();
+
+        Object max0 = maxExp.evaluate(artists.get(0));
+        BigDecimal expected0 = BigDecimal.valueOf(20000, 2);
+        assertEquals(expected0, max0);
+
+        Object max1 = maxExp.evaluate(artists.get(1));
+        BigDecimal expected1 = BigDecimal.valueOf(100000, 2);
+        assertEquals(expected1, max1);
+
+        Object max4 = maxExp.evaluate(artists.get(4));
+        BigDecimal expected4 = BigDecimal.valueOf(19000, 2);
+        assertEquals(expected4, max4);
+    }
+
+    @Test
+    public void testMin() {
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        Expression minExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).min().getExpression();
+
+        Object min0 = minExp.evaluate(artists.get(0));
+        BigDecimal expected0 = BigDecimal.valueOf(5000, 2);
+        assertEquals(expected0, min0);
+
+        Object min3 = minExp.evaluate(artists.get(3));
+        BigDecimal expected1 = BigDecimal.valueOf(3000, 2);
+        assertEquals(expected1, min3);
+
+        Object min4 = minExp.evaluate(artists.get(4));
+        BigDecimal expected4 = BigDecimal.valueOf(4000, 2);
+        assertEquals(expected4, min4);
+    }
+
+    @Test
+    public void testAvg() {
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .orderBy(Artist.ARTIST_ID_PK_PROPERTY.asc())
+                .select(context);
+
+        Expression avgExp = Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).avg().getExpression();
+
+        Object avg0 = avgExp.evaluate(artists.get(0));
+        assertEquals(125.0, avg0);
+
+        Object avg2 = avgExp.evaluate(artists.get(2));
+        assertEquals(95.0, avg2);
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionIT.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionIT.java
index 58a5da0..cf6ce5d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/ExpressionIT.java
@@ -128,7 +128,7 @@
 		try {
             artists = ObjectSelect.query(Artist.class, Artist.ARTIST_NAME.lt((String) null)).select(context);
         } catch (CayenneRuntimeException ex) {
-		    if(adapter.supportsNullComparision()) {
+		    if(adapter.supportsNullComparison()) {
 		        throw ex;
             } else {
 		        return;
@@ -148,7 +148,7 @@
         try {
             artists = ObjectSelect.query(Artist.class, Artist.ARTIST_NAME.in("Picasso", (String) null)).select(context);
         } catch (CayenneRuntimeException ex) {
-            if(adapter.supportsNullComparision()) {
+            if(adapter.supportsNullComparison()) {
                 throw ex;
             } else {
                 return;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java
index db914ae..529231a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/FunctionExpressionFactoryTest.java
@@ -27,12 +27,14 @@
 import org.apache.cayenne.exp.parser.ASTCurrentDate;
 import org.apache.cayenne.exp.parser.ASTCurrentTime;
 import org.apache.cayenne.exp.parser.ASTCurrentTimestamp;
+import org.apache.cayenne.exp.parser.ASTCustomOperator;
 import org.apache.cayenne.exp.parser.ASTLength;
 import org.apache.cayenne.exp.parser.ASTLocate;
 import org.apache.cayenne.exp.parser.ASTLower;
 import org.apache.cayenne.exp.parser.ASTMax;
 import org.apache.cayenne.exp.parser.ASTMin;
 import org.apache.cayenne.exp.parser.ASTMod;
+import org.apache.cayenne.exp.parser.ASTObjPath;
 import org.apache.cayenne.exp.parser.ASTScalar;
 import org.apache.cayenne.exp.parser.ASTSqrt;
 import org.apache.cayenne.exp.parser.ASTSubstring;
@@ -264,4 +266,16 @@
         Expression exp = FunctionExpressionFactory.currentTimestamp();
         assertTrue(exp instanceof ASTCurrentTimestamp);
     }
+
+    @Test
+    public void customOpTest() {
+        Expression exp = FunctionExpressionFactory.operator("==>", 123, Artist.ARTIST_NAME.getExpression());
+        assertTrue(exp instanceof ASTCustomOperator);
+        ASTCustomOperator operator = (ASTCustomOperator) exp;
+        assertEquals("==>", operator.getOperator());
+        assertEquals(2, operator.jjtGetNumChildren());
+
+        assertEquals(123, operator.getOperand(0));
+        assertEquals(Artist.ARTIST_NAME.getExpression(), operator.getOperand(1));
+    }
 }
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTCustomOperatorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTCustomOperatorTest.java
new file mode 100644
index 0000000..b5b45ab
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/parser/ASTCustomOperatorTest.java
@@ -0,0 +1,48 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.exp.parser;
+
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionException;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * @since 4.2
+ */
+public class ASTCustomOperatorTest {
+
+    @Test
+    public void testParse() {
+        Expression exp = ExpressionFactory.exp("op('~~>', test, 'abc')");
+
+        assertTrue(exp instanceof ASTCustomOperator);
+        assertEquals("~~>", ((ASTCustomOperator) exp).getOperator());
+        assertEquals("op(\"~~>\", test, \"abc\")", exp.toString());
+    }
+
+    @Test(expected = ExpressionException.class)
+    public void testEvaluate() {
+        new ASTCustomOperator("op").evaluate(new Object());
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/property/BasePropertyTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/property/BasePropertyTest.java
index 99ed791..38c003a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/property/BasePropertyTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/property/BasePropertyTest.java
@@ -265,4 +265,38 @@
         assertNotEquals(INT_FIELD.hashCode(), INT_FIELD3.hashCode());
     }
 
+    @Test
+    public void testFunctionProperty() {
+        BaseProperty<Integer> property = new BaseProperty<>("intField", null, Integer.class);
+        BaseProperty<Integer> arg = new BaseProperty<>("intField2", null, Integer.class);
+
+        BaseProperty<Integer> operator = property.function("%", Integer.class, arg);
+        assertEquals(ExpressionFactory.exp("fn('%', intField, intField2)"), operator.getExpression());
+    }
+
+    @Test
+    public void testFunctionScalar() {
+        BaseProperty<Integer> property = new BaseProperty<>("intField", null, Integer.class);
+
+        BaseProperty<Integer> operator = property.function("%", Integer.class, 10);
+        assertEquals(ExpressionFactory.exp("fn('%', intField, 10)"), operator.getExpression());
+    }
+
+    @Test
+    public void testOperatorProperty() {
+        BaseProperty<Integer> property = new BaseProperty<>("intField", null, Integer.class);
+        BaseProperty<Integer> arg = new BaseProperty<>("intField2", null, Integer.class);
+
+        BaseProperty<Integer> operator = property.operator("%", Integer.class, arg);
+        assertEquals(ExpressionFactory.exp("op('%', intField, intField2)"), operator.getExpression());
+    }
+
+    @Test
+    public void testOperatorScalar() {
+        BaseProperty<Integer> property = new BaseProperty<>("intField", null, Integer.class);
+
+        BaseProperty<Integer> operator = property.operator("%", Integer.class, 10);
+        assertEquals(ExpressionFactory.exp("op('%', intField, 10)"), operator.getExpression());
+    }
+
 }
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/exp/property/EntityPropertyTest.java b/cayenne-server/src/test/java/org/apache/cayenne/exp/property/EntityPropertyTest.java
index 78a532d..3d548e4 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/exp/property/EntityPropertyTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/exp/property/EntityPropertyTest.java
@@ -19,6 +19,9 @@
 
 package org.apache.cayenne.exp.property;
 
+import java.util.Arrays;
+
+import org.apache.cayenne.exp.Expression;
 import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.testdo.testmap.Artist;
 import org.junit.Before;
@@ -76,4 +79,40 @@
         assertEquals("path.other", other.getName());
         assertEquals(ExpressionFactory.pathExp("path.other"), other.getExpression());
     }
+
+    @Test
+    public void eqId() {
+        Expression exp = property.eqId(1);
+        assertEquals(ExpressionFactory.exp("path = 1"), exp);
+    }
+
+    @Test
+    public void inIdCollection() {
+        Expression exp = property.inId(Arrays.asList(1, 2, 3));
+        assertEquals(ExpressionFactory.exp("path in (1, 2, 3)"), exp);
+    }
+
+    @Test
+    public void inIdVararg() {
+        Expression exp = property.inId(1, 2, 3);
+        assertEquals(ExpressionFactory.exp("path in (1, 2, 3)"), exp);
+    }
+
+    @Test
+    public void neqId() {
+        Expression exp = property.neqId(1);
+        assertEquals(ExpressionFactory.exp("path <> 1"), exp);
+    }
+
+    @Test
+    public void ninIdCollection() {
+        Expression exp = property.ninId(Arrays.asList(1, 2, 3));
+        assertEquals(ExpressionFactory.exp("path not in (1, 2, 3)"), exp);
+    }
+
+    @Test
+    public void ninIdVararg() {
+        Expression exp = property.ninId(1, 2, 3);
+        assertEquals(ExpressionFactory.exp("path not in (1, 2, 3)"), exp);
+    }
 }
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/map/SelectQueryDescriptorTest.java b/cayenne-server/src/test/java/org/apache/cayenne/map/SelectQueryDescriptorTest.java
index a5f1944..29c37bf 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/map/SelectQueryDescriptorTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/map/SelectQueryDescriptorTest.java
@@ -20,27 +20,25 @@
 package org.apache.cayenne.map;
 
 import org.apache.cayenne.exp.ExpressionFactory;
-import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.query.QueryMetadata;
-import org.apache.cayenne.query.SelectQuery;
 import org.junit.Test;
 
 import static org.junit.Assert.*;
 
 /**
  */
-@Deprecated
 public class SelectQueryDescriptorTest {
 
     @Test
-    public void testGetQueryType() throws Exception {
+    public void testGetQueryType() {
         SelectQueryDescriptor builder = QueryDescriptor.selectQueryDescriptor();
         builder.setRoot("FakeRoot");
-        assertTrue(builder.buildQuery() instanceof SelectQuery);
+        assertTrue(builder.buildQuery() instanceof ObjectSelect);
     }
 
     @Test
-    public void testGetQueryRoot() throws Exception {
+    public void testGetQueryRoot() {
         DataMap map = new DataMap();
         ObjEntity entity = new ObjEntity("A");
         map.addObjEntity(entity);
@@ -48,31 +46,31 @@
         SelectQueryDescriptor builder = QueryDescriptor.selectQueryDescriptor();
         builder.setRoot(entity);
 
-        assertTrue(builder.buildQuery() instanceof SelectQuery);
+        assertTrue(builder.buildQuery() instanceof ObjectSelect);
         assertSame(entity, builder.buildQuery().getRoot());
     }
 
     @Test
-    public void testGetQueryQualifier() throws Exception {
+    public void testGetQueryQualifier() {
         SelectQueryDescriptor builder = QueryDescriptor.selectQueryDescriptor();
         builder.setRoot("FakeRoot");
         builder.setQualifier(ExpressionFactory.exp("abc = 5"));
 
-        SelectQuery query = builder.buildQuery();
+        ObjectSelect<?> query = builder.buildQuery();
 
-        assertEquals(ExpressionFactory.exp("abc = 5"), query.getQualifier());
+        assertEquals(ExpressionFactory.exp("abc = 5"), query.getWhere());
     }
 
     @Test
-    public void testGetQueryProperties() throws Exception {
+    public void testGetQueryProperties() {
         SelectQueryDescriptor builder = QueryDescriptor.selectQueryDescriptor();
         builder.setRoot("FakeRoot");
         builder.setProperty(QueryMetadata.FETCH_LIMIT_PROPERTY, "5");
         builder.setProperty(QueryMetadata.STATEMENT_FETCH_SIZE_PROPERTY, "6");
 
-        SelectQuery<?> query = builder.buildQuery();
-        assertTrue(query instanceof SelectQuery);
-        assertEquals(5, query.getFetchLimit());
+        ObjectSelect<?> query = builder.buildQuery();
+        assertTrue(query instanceof ObjectSelect);
+        assertEquals(5, query.getLimit());
         assertEquals(6, query.getStatementFetchSize());
 
         // TODO: test other properties...
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
index 5220e7b..eb79b6f 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ColumnSelectIT.java
@@ -126,7 +126,6 @@
     public void testSelectSimpleHaving() throws Exception {
         Object[] result = ObjectSelect.query(Artist.class)
                 .columns(Artist.DATE_OF_BIRTH, PropertyFactory.COUNT)
-                .orderBy(Artist.DATE_OF_BIRTH.asc())
                 .having(Artist.DATE_OF_BIRTH.eq(dateFormat.parse("1/2/17")))
                 .selectOne(context);
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_AggregateIT.java b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_AggregateIT.java
index f3dc231..6c0943d 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_AggregateIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/query/ObjectSelect_AggregateIT.java
@@ -34,6 +34,7 @@
 import org.apache.cayenne.test.jdbc.DBHelper;
 import org.apache.cayenne.test.jdbc.TableHelper;
 import org.apache.cayenne.testdo.testmap.Artist;
+import org.apache.cayenne.testdo.testmap.Painting;
 import org.apache.cayenne.unit.di.server.CayenneProjects;
 import org.apache.cayenne.unit.di.server.ServerCase;
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
@@ -79,11 +80,11 @@
         tGallery.insert(1, "tate modern");
 
         TableHelper tPaintings = new TableHelper(dbHelper, "PAINTING");
-        tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID");
+        tPaintings.setColumns("PAINTING_ID", "PAINTING_TITLE", "ARTIST_ID", "GALLERY_ID", "ESTIMATED_PRICE");
         for (int i = 1; i <= 20; i++) {
-            tPaintings.insert(i, "painting" + i, i % 5 + 1, 1);
+            tPaintings.insert(i, "painting" + i, i % 5 + 1, 1, i * 10);
         }
-        tPaintings.insert(21, "painting21", 2, 1);
+        tPaintings.insert(21, "painting21", 2, 1, 1000);
     }
 
     @After
@@ -178,4 +179,40 @@
         assertEquals("artist1", result[0]);
         assertEquals(4L, result[1]);
     }
+
+    @Test
+    public void testOrderByCount() {
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .orderBy(Artist.PAINTING_ARRAY.outer().count().desc())
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        assertEquals(20, artists.size());
+
+        assertEquals(5, artists.get(0).getPaintingArray().size());
+        assertEquals("artist2", artists.get(0).getArtistName());
+        assertEquals(4, artists.get(1).getPaintingArray().size());
+        assertEquals(4, artists.get(2).getPaintingArray().size());
+
+        assertEquals(0, artists.get(17).getPaintingArray().size());
+        assertEquals(0, artists.get(18).getPaintingArray().size());
+        assertEquals(0, artists.get(19).getPaintingArray().size());
+    }
+
+    @Test
+    public void testOrderByAvg() {
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .orderBy(Artist.PAINTING_ARRAY.dot(Painting.ESTIMATED_PRICE).avg().asc())
+                .prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .select(context);
+
+        assertEquals(5, artists.size());
+
+        assertEquals("artist3", artists.get(0).getArtistName());
+        assertEquals("artist4", artists.get(1).getArtistName());
+        assertEquals("artist5", artists.get(2).getArtistName());
+        assertEquals("artist1", artists.get(3).getArtistName());
+        assertEquals("artist2", artists.get(4).getArtistName());
+
+    }
 }
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectAttributePropertyTest.java b/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectAttributePropertyTest.java
index 81477fd..8d4e483 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectAttributePropertyTest.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectAttributePropertyTest.java
@@ -33,7 +33,7 @@
 		ObjEntity e1 = new ObjEntity("objEntityName");
 		ObjAttribute a1 = new ObjAttribute("aName", "aType", e1);
 		
-		DataObjectAttributeProperty p1 = new DataObjectAttributeProperty(a1);
+		DataObjectAttributeProperty p1 = new DataObjectAttributeProperty(a1, new DefaultValueComparisonStrategyFactory.DefaultValueComparisonStrategy());
 		DataObjectAttributeProperty p2 = Util.cloneViaSerialization(p1);
 		
 		assertNotNull(p2);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactoryIT.java b/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactoryIT.java
index 89786bc..ae85c1f 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactoryIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactoryIT.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.reflect.generic;
 
+import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
@@ -36,6 +37,7 @@
 
 import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
+import static org.mockito.Mockito.mock;
 
 @UseServerRuntime(CayenneProjects.TESTMAP_PROJECT)
 public class DataObjectDescriptorFactoryIT extends ServerCase {
@@ -47,7 +49,9 @@
     public void testVisitDeclaredProperties_IterationOrder() {
 
         DataObjectDescriptorFactory factory = new DataObjectDescriptorFactory(
-                resolver.getClassDescriptorMap(), new SingletonFaultFactory());
+                resolver.getClassDescriptorMap(),
+                new SingletonFaultFactory(),
+                new DefaultValueComparisonStrategyFactory(mock(ValueObjectTypeRegistry.class)));
 
         for (ObjEntity e : resolver.getObjEntities()) {
             ClassDescriptor descriptor = factory.getDescriptor(e.getName());
@@ -84,7 +88,9 @@
     public void testVisitProperties_IterationOrder() {
 
         DataObjectDescriptorFactory factory = new DataObjectDescriptorFactory(
-                resolver.getClassDescriptorMap(), new SingletonFaultFactory());
+                resolver.getClassDescriptorMap(),
+                new SingletonFaultFactory(),
+                new DefaultValueComparisonStrategyFactory(mock(ValueObjectTypeRegistry.class)));
 
         for (ObjEntity e : resolver.getObjEntities()) {
             ClassDescriptor descriptor = factory.getDescriptor(e.getName());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory_InheritanceMapsIT.java b/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory_InheritanceMapsIT.java
index 36886bd..27b3fcc 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory_InheritanceMapsIT.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/reflect/generic/DataObjectDescriptorFactory_InheritanceMapsIT.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.cayenne.reflect.generic;
 
+import org.apache.cayenne.access.types.ValueObjectTypeRegistry;
 import org.apache.cayenne.di.Inject;
 import org.apache.cayenne.map.EntityResolver;
 import org.apache.cayenne.map.ObjEntity;
@@ -33,6 +34,8 @@
 import org.apache.cayenne.unit.di.server.UseServerRuntime;
 import org.junit.Test;
 
+import static org.mockito.Mockito.mock;
+
 @UseServerRuntime(CayenneProjects.INHERITANCE_SINGLE_TABLE1_PROJECT)
 public class DataObjectDescriptorFactory_InheritanceMapsIT extends ServerCase {
 
@@ -43,7 +46,9 @@
     public void testVisitProperties_IterationOrder() {
 
         DataObjectDescriptorFactory factory = new DataObjectDescriptorFactory(
-                resolver.getClassDescriptorMap(), new SingletonFaultFactory());
+                resolver.getClassDescriptorMap(),
+                new SingletonFaultFactory(),
+                new DefaultValueComparisonStrategyFactory(mock(ValueObjectTypeRegistry.class)));
 
         for (ObjEntity e : resolver.getObjEntities()) {
             ClassDescriptor descriptor = factory.getDescriptor(e.getName());
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/ArtistLazy.java
new file mode 100644
index 0000000..dcd9224
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/ArtistLazy.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.cay_2641;
+
+import org.apache.cayenne.testdo.cay_2641.auto._ArtistLazy;
+
+public class ArtistLazy extends _ArtistLazy {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/DatamapLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/DatamapLazy.java
new file mode 100644
index 0000000..f026e12
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/DatamapLazy.java
@@ -0,0 +1,18 @@
+package org.apache.cayenne.testdo.cay_2641;
+
+import org.apache.cayenne.testdo.cay_2641.auto._DatamapLazy;
+
+public class DatamapLazy extends _DatamapLazy {
+
+    private static DatamapLazy instance;
+
+    private DatamapLazy() {}
+
+    public static DatamapLazy getInstance() {
+        if(instance == null) {
+            instance = new DatamapLazy();
+        }
+
+        return instance;
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/PaintingLazy.java
new file mode 100644
index 0000000..dd6f77a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/PaintingLazy.java
@@ -0,0 +1,9 @@
+package org.apache.cayenne.testdo.cay_2641;
+
+import org.apache.cayenne.testdo.cay_2641.auto._PaintingLazy;
+
+public class PaintingLazy extends _PaintingLazy {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_ArtistLazy.java
new file mode 100644
index 0000000..aedc3e9
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_ArtistLazy.java
@@ -0,0 +1,135 @@
+package org.apache.cayenne.testdo.cay_2641.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.List;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.PaintingLazy;
+
+/**
+ * Class _ArtistLazy was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _ArtistLazy extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final StringProperty<String> SURNAME = PropertyFactory.createString("surname", String.class);
+    public static final ListProperty<PaintingLazy> PAINTINGS = PropertyFactory.createList("paintings", PaintingLazy.class);
+
+    protected Object name;
+    protected String surname;
+
+    protected Object paintings;
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+        return (String)this.name;
+    }
+
+    public void setSurname(String surname) {
+        beforePropertyWrite("surname", this.surname, surname);
+        this.surname = surname;
+    }
+
+    public String getSurname() {
+        beforePropertyRead("surname");
+        return this.surname;
+    }
+
+    public void addToPaintings(PaintingLazy obj) {
+        addToManyTarget("paintings", obj, true);
+    }
+
+    public void removeFromPaintings(PaintingLazy obj) {
+        removeToManyTarget("paintings", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<PaintingLazy> getPaintings() {
+        return (List<PaintingLazy>)readProperty("paintings");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "surname":
+                return this.surname;
+            case "paintings":
+                return this.paintings;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "name":
+                this.name = val;
+                break;
+            case "surname":
+                this.surname = (String)val;
+                break;
+            case "paintings":
+                this.paintings = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.name);
+        out.writeObject(this.surname);
+        out.writeObject(this.paintings);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name = in.readObject();
+        this.surname = (String)in.readObject();
+        this.paintings = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_DatamapLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_DatamapLazy.java
new file mode 100644
index 0000000..756360a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_DatamapLazy.java
@@ -0,0 +1,33 @@
+package org.apache.cayenne.testdo.cay_2641.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.query.MappedSelect;
+import org.apache.cayenne.testdo.cay_2641.ArtistLazy;
+import org.apache.cayenne.testdo.cay_2641.PaintingLazy;
+
+/**
+ * This class was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public class _DatamapLazy {
+
+    public static final String PREFETCH_SELECT_QUERYNAME = "prefetchSelect";
+
+    public static final String SIMPLE_SELECT_QUERYNAME = "simpleSelect";
+
+    public List<PaintingLazy> performPrefetchSelect(ObjectContext context) {
+        MappedSelect<PaintingLazy> query = MappedSelect.query(PREFETCH_SELECT_QUERYNAME, PaintingLazy.class);
+        return query.select(context);
+    }
+
+
+    public List<ArtistLazy> performSimpleSelect(ObjectContext context) {
+        MappedSelect<ArtistLazy> query = MappedSelect.query(SIMPLE_SELECT_QUERYNAME, ArtistLazy.class);
+        return query.select(context);
+    }
+
+}
\ No newline at end of file
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_PaintingLazy.java
new file mode 100644
index 0000000..5386919
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/auto/_PaintingLazy.java
@@ -0,0 +1,110 @@
+package org.apache.cayenne.testdo.cay_2641.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.ArtistLazy;
+
+/**
+ * Class _PaintingLazy was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _PaintingLazy extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final EntityProperty<ArtistLazy> ARTIST = PropertyFactory.createEntity("artist", ArtistLazy.class);
+
+    protected Object name;
+
+    protected Object artist;
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+        return (String)this.name;
+    }
+
+    public void setArtist(ArtistLazy artist) {
+        setToOneTarget("artist", artist, true);
+    }
+
+    public ArtistLazy getArtist() {
+        return (ArtistLazy)readProperty("artist");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "artist":
+                return this.artist;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "name":
+                this.name = val;
+                break;
+            case "artist":
+                this.artist = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.name);
+        out.writeObject(this.artist);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name = in.readObject();
+        this.artist = in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/ArtistLazy.java
new file mode 100644
index 0000000..593a6aa
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/ArtistLazy.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.testdo.cay_2641.client;
+
+import org.apache.cayenne.testdo.cay_2641.client.auto._ArtistLazy;
+
+/**
+ * A persistent class mapped as "ArtistLazy" Cayenne entity.
+ */
+public class ArtistLazy extends _ArtistLazy {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/PaintingLazy.java
new file mode 100644
index 0000000..a290ca4
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/PaintingLazy.java
@@ -0,0 +1,12 @@
+package org.apache.cayenne.testdo.cay_2641.client;
+
+import org.apache.cayenne.testdo.cay_2641.client.auto._PaintingLazy;
+
+/**
+ * A persistent class mapped as "PaintingLazy" Cayenne entity.
+ */
+public class PaintingLazy extends _PaintingLazy {
+
+     private static final long serialVersionUID = 1L; 
+     
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_ArtistLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_ArtistLazy.java
new file mode 100644
index 0000000..ec1111d
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_ArtistLazy.java
@@ -0,0 +1,113 @@
+package org.apache.cayenne.testdo.cay_2641.client.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.client.PaintingLazy;
+import org.apache.cayenne.util.PersistentObjectList;
+
+/**
+ * A generated persistent class mapped as "ArtistLazy" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _ArtistLazy extends PersistentObject {
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final StringProperty<String> SURNAME = PropertyFactory.createString("surname", String.class);
+    public static final ListProperty<PaintingLazy> PAINTINGS = PropertyFactory.createList("paintings", PaintingLazy.class);
+
+    protected Object name;
+    protected String surname;
+    protected List<PaintingLazy> paintings;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+
+        return (String) name;
+    }
+
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+            objectContext.propertyChanged(this, "name", this.name, name);
+        }
+
+        this.name = name;
+    }
+
+    public String getSurname() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "surname", false);
+        }
+
+
+        return surname;
+    }
+
+    public void setSurname(String surname) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "surname", false);
+            objectContext.propertyChanged(this, "surname", this.surname, surname);
+        }
+
+        this.surname = surname;
+    }
+
+    public List<PaintingLazy> getPaintings() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList<>(this, "paintings");
+		}
+
+        return paintings;
+    }
+
+    public void addToPaintings(PaintingLazy object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList<>(this, "paintings");
+		}
+
+        this.paintings.add(object);
+    }
+
+    public void removeFromPaintings(PaintingLazy object) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "paintings", true);
+        } else if (this.paintings == null) {
+        	this.paintings = new PersistentObjectList<>(this, "paintings");
+		}
+
+        this.paintings.remove(object);
+    }
+
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name":
+                return this.name;
+            case "surname":
+                return this.surname;
+            case "paintings":
+                return this.paintings;
+            default:
+                return null;
+        }
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_PaintingLazy.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_PaintingLazy.java
new file mode 100644
index 0000000..29e52dc
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2641/client/auto/_PaintingLazy.java
@@ -0,0 +1,66 @@
+package org.apache.cayenne.testdo.cay_2641.client.auto;
+
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.ValueHolder;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.testdo.cay_2641.client.ArtistLazy;
+import org.apache.cayenne.util.PersistentObjectHolder;
+
+/**
+ * A generated persistent class mapped as "PaintingLazy" Cayenne entity. It is a good idea to
+ * avoid changing this class manually, since it will be overwritten next time code is
+ * regenerated. If you need to make any customizations, put them in a subclass.
+ */
+public abstract class _PaintingLazy extends PersistentObject {
+
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+    public static final EntityProperty<ArtistLazy> ARTIST = PropertyFactory.createEntity("artist", ArtistLazy.class);
+
+    protected Object name;
+    protected ValueHolder<ArtistLazy> artist;
+
+    public String getName() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+        }
+
+        if(this.name instanceof Fault) {
+            this.name = ((Fault) this.name).resolveFault(this, "name");
+        }
+
+        return (String) name;
+    }
+
+    public void setName(String name) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "name", false);
+            objectContext.propertyChanged(this, "name", this.name, name);
+        }
+
+        this.name = name;
+    }
+
+    public ArtistLazy getArtist() {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder<>(this, "artist");
+		}
+
+        return artist.getValue();
+    }
+
+    public void setArtist(ArtistLazy artist) {
+        if(objectContext != null) {
+            objectContext.prepareForAccess(this, "artist", true);
+        } else if (this.artist == null) {
+        	this.artist = new PersistentObjectHolder<>(this, "artist");
+		}
+
+        this.artist.setValue(artist);
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2666/CAY2666.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2666/CAY2666.java
new file mode 100644
index 0000000..049ed09
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2666/CAY2666.java
@@ -0,0 +1,28 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.testdo.cay_2666;
+
+import org.apache.cayenne.testdo.cay_2666.auto._CAY2666;
+
+public class CAY2666 extends _CAY2666 {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2666/auto/_CAY2666.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2666/auto/_CAY2666.java
new file mode 100644
index 0000000..4240d4a
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/cay_2666/auto/_CAY2666.java
@@ -0,0 +1,87 @@
+package org.apache.cayenne.testdo.cay_2666.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _CAY2666 was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _CAY2666 extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final StringProperty<String> NAME$ = PropertyFactory.createString("name$", String.class);
+
+    protected String name$;
+
+
+    public void setName$(String name$) {
+        beforePropertyWrite("name$", this.name$, name$);
+        this.name$ = name$;
+    }
+
+    public String getName$() {
+        beforePropertyRead("name$");
+        return this.name$;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "name$":
+                return this.name$;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "name$":
+                this.name$ = (String)val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.name$);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.name$ = (String)in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/lazy/Lazyblob.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lazy/Lazyblob.java
new file mode 100644
index 0000000..50da883
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lazy/Lazyblob.java
@@ -0,0 +1,27 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+package org.apache.cayenne.testdo.lazy;
+
+import org.apache.cayenne.testdo.lazy.auto._Lazyblob;
+
+public class Lazyblob extends _Lazyblob {
+
+    private static final long serialVersionUID = 1L; 
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/lazy/auto/_Lazyblob.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lazy/auto/_Lazyblob.java
new file mode 100644
index 0000000..5d5b550
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/lazy/auto/_Lazyblob.java
@@ -0,0 +1,111 @@
+package org.apache.cayenne.testdo.lazy.auto;
+
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+
+import org.apache.cayenne.BaseDataObject;
+import org.apache.cayenne.Fault;
+import org.apache.cayenne.exp.property.BaseProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _Lazyblob was generated by Cayenne.
+ * It is probably a good idea to avoid changing this class manually,
+ * since it may be overwritten next time code is regenerated.
+ * If you need to make any customizations, please use subclass.
+ */
+public abstract class _Lazyblob extends BaseDataObject {
+
+    private static final long serialVersionUID = 1L; 
+
+    public static final String ID_PK_COLUMN = "ID";
+
+    public static final BaseProperty<byte[]> LAZY_DATA = PropertyFactory.createBase("lazyData", byte[].class);
+    public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
+
+    protected Object lazyData;
+    protected String name;
+
+
+    public void setLazyData(byte[] lazyData) {
+        beforePropertyWrite("lazyData", this.lazyData, lazyData);
+        this.lazyData = lazyData;
+    }
+
+    public byte[] getLazyData() {
+        beforePropertyRead("lazyData");
+        if(this.lazyData instanceof Fault) {
+            this.lazyData = ((Fault) this.lazyData).resolveFault(this, "lazyData");
+        }
+        return (byte[])this.lazyData;
+    }
+
+    public void setName(String name) {
+        beforePropertyWrite("name", this.name, name);
+        this.name = name;
+    }
+
+    public String getName() {
+        beforePropertyRead("name");
+        return this.name;
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "lazyData":
+                return this.lazyData;
+            case "name":
+                return this.name;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "lazyData":
+                this.lazyData = val;
+                break;
+            case "name":
+                this.name = (String)val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+    private void writeObject(ObjectOutputStream out) throws IOException {
+        writeSerialized(out);
+    }
+
+    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        readSerialized(in);
+    }
+
+    @Override
+    protected void writeState(ObjectOutputStream out) throws IOException {
+        super.writeState(out);
+        out.writeObject(this.lazyData);
+        out.writeObject(this.name);
+    }
+
+    @Override
+    protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
+        super.readState(in);
+        this.lazyData = in.readObject();
+        this.name = (String)in.readObject();
+    }
+
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKDep.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKDep.java
index 9e3757f..d3de2b7 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKDep.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKDep.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
@@ -22,13 +21,14 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> PK_ATTRIBUTE_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("PK_ATTRIBUTE"), Integer.class);
     public static final String PK_ATTRIBUTE_PK_COLUMN = "PK_ATTRIBUTE";
 
     public static final StringProperty<String> DESCR = PropertyFactory.createString("descr", String.class);
+    public static final NumericProperty<Integer> PK = PropertyFactory.createNumeric("pk", Integer.class);
     public static final EntityProperty<MeaningfulPKTest1> TO_MEANINGFUL_PK = PropertyFactory.createEntity("toMeaningfulPK", MeaningfulPKTest1.class);
 
     protected String descr;
+    protected int pk;
 
     protected Object toMeaningfulPK;
 
@@ -42,6 +42,16 @@
         return this.descr;
     }
 
+    public void setPk(int pk) {
+        beforePropertyWrite("pk", this.pk, pk);
+        this.pk = pk;
+    }
+
+    public int getPk() {
+        beforePropertyRead("pk");
+        return this.pk;
+    }
+
     public void setToMeaningfulPK(MeaningfulPKTest1 toMeaningfulPK) {
         setToOneTarget("toMeaningfulPK", toMeaningfulPK, true);
     }
@@ -59,6 +69,8 @@
         switch(propName) {
             case "descr":
                 return this.descr;
+            case "pk":
+                return this.pk;
             case "toMeaningfulPK":
                 return this.toMeaningfulPK;
             default:
@@ -76,6 +88,9 @@
             case "descr":
                 this.descr = (String)val;
                 break;
+            case "pk":
+                this.pk = val == null ? 0 : (int)val;
+                break;
             case "toMeaningfulPK":
                 this.toMeaningfulPK = val;
                 break;
@@ -96,6 +111,7 @@
     protected void writeState(ObjectOutputStream out) throws IOException {
         super.writeState(out);
         out.writeObject(this.descr);
+        out.writeInt(this.pk);
         out.writeObject(this.toMeaningfulPK);
     }
 
@@ -103,6 +119,7 @@
     protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
         super.readState(in);
         this.descr = (String)in.readObject();
+        this.pk = in.readInt();
         this.toMeaningfulPK = in.readObject();
     }
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKTest1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKTest1.java
index d0f8e9f..80a2e26 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKTest1.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPKTest1.java
@@ -6,7 +6,6 @@
 import java.util.List;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPk.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPk.java
index c131fc4..780a3d3 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPk.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPk.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPkTest2.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPkTest2.java
index 16281fb..566f556 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPkTest2.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/meaningful_pk/auto/_MeaningfulPkTest2.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigDecimalEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigDecimalEntity.java
index 8dbe3a7..20b6cce 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigDecimalEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigDecimalEntity.java
@@ -6,7 +6,6 @@
 import java.math.BigDecimal;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
@@ -20,22 +19,33 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
-    public static final NumericProperty<BigDecimal> BIG_DECIMAL_FIELD = PropertyFactory.createNumeric("bigDecimalField", BigDecimal.class);
+    public static final NumericProperty<BigDecimal> BIG_DECIMAL_DECIMAL = PropertyFactory.createNumeric("bigDecimalDecimal", BigDecimal.class);
+    public static final NumericProperty<BigDecimal> BIG_DECIMAL_NUMERIC = PropertyFactory.createNumeric("bigDecimalNumeric", BigDecimal.class);
 
-    protected BigDecimal bigDecimalField;
+    protected BigDecimal bigDecimalDecimal;
+    protected BigDecimal bigDecimalNumeric;
 
 
-    public void setBigDecimalField(BigDecimal bigDecimalField) {
-        beforePropertyWrite("bigDecimalField", this.bigDecimalField, bigDecimalField);
-        this.bigDecimalField = bigDecimalField;
+    public void setBigDecimalDecimal(BigDecimal bigDecimalDecimal) {
+        beforePropertyWrite("bigDecimalDecimal", this.bigDecimalDecimal, bigDecimalDecimal);
+        this.bigDecimalDecimal = bigDecimalDecimal;
     }
 
-    public BigDecimal getBigDecimalField() {
-        beforePropertyRead("bigDecimalField");
-        return this.bigDecimalField;
+    public BigDecimal getBigDecimalDecimal() {
+        beforePropertyRead("bigDecimalDecimal");
+        return this.bigDecimalDecimal;
+    }
+
+    public void setBigDecimalNumeric(BigDecimal bigDecimalNumeric) {
+        beforePropertyWrite("bigDecimalNumeric", this.bigDecimalNumeric, bigDecimalNumeric);
+        this.bigDecimalNumeric = bigDecimalNumeric;
+    }
+
+    public BigDecimal getBigDecimalNumeric() {
+        beforePropertyRead("bigDecimalNumeric");
+        return this.bigDecimalNumeric;
     }
 
     @Override
@@ -45,8 +55,10 @@
         }
 
         switch(propName) {
-            case "bigDecimalField":
-                return this.bigDecimalField;
+            case "bigDecimalDecimal":
+                return this.bigDecimalDecimal;
+            case "bigDecimalNumeric":
+                return this.bigDecimalNumeric;
             default:
                 return super.readPropertyDirectly(propName);
         }
@@ -59,8 +71,11 @@
         }
 
         switch (propName) {
-            case "bigDecimalField":
-                this.bigDecimalField = (BigDecimal)val;
+            case "bigDecimalDecimal":
+                this.bigDecimalDecimal = (BigDecimal)val;
+                break;
+            case "bigDecimalNumeric":
+                this.bigDecimalNumeric = (BigDecimal)val;
                 break;
             default:
                 super.writePropertyDirectly(propName, val);
@@ -78,13 +93,15 @@
     @Override
     protected void writeState(ObjectOutputStream out) throws IOException {
         super.writeState(out);
-        out.writeObject(this.bigDecimalField);
+        out.writeObject(this.bigDecimalDecimal);
+        out.writeObject(this.bigDecimalNumeric);
     }
 
     @Override
     protected void readState(ObjectInputStream in) throws IOException, ClassNotFoundException {
         super.readState(in);
-        this.bigDecimalField = (BigDecimal)in.readObject();
+        this.bigDecimalDecimal = (BigDecimal)in.readObject();
+        this.bigDecimalNumeric = (BigDecimal)in.readObject();
     }
 
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigIntegerEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigIntegerEntity.java
index fb2d3b7..416dce5 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigIntegerEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BigIntegerEntity.java
@@ -6,7 +6,6 @@
 import java.math.BigInteger;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
@@ -20,7 +19,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final NumericProperty<BigInteger> BIG_INTEGER_FIELD = PropertyFactory.createNumeric("bigIntegerField", BigInteger.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitNumberTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitNumberTestEntity.java
index 4e8ab84..03d0d7b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitNumberTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitNumberTestEntity.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
@@ -19,7 +18,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final NumericProperty<Integer> BIT_COLUMN = PropertyFactory.createNumeric("bitColumn", Integer.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitTestEntity.java
index 588c136..d3f001a 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BitTestEntity.java
@@ -5,9 +5,7 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
 /**
@@ -20,7 +18,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final BaseProperty<Boolean> BIT_COLUMN = PropertyFactory.createBase("bitColumn", Boolean.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BooleanTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BooleanTestEntity.java
index f2bc004..a728694 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BooleanTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_BooleanTestEntity.java
@@ -5,9 +5,7 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.BaseProperty;
-import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
 /**
@@ -20,7 +18,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final BaseProperty<Boolean> BOOLEAN_COLUMN = PropertyFactory.createBase("booleanColumn", Boolean.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTest1.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTest1.java
index cdb2c1c..11a8dcc 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTest1.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTest1.java
@@ -3,10 +3,8 @@
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
-import java.math.BigDecimal;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTestEntity.java
index 9f12719..b5841ed 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_DecimalPKTestEntity.java
@@ -6,7 +6,6 @@
 import java.math.BigDecimal;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 import org.apache.cayenne.exp.property.StringProperty;
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_LongEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_LongEntity.java
index fbc9a6c..e6dd037 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_LongEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_LongEntity.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
@@ -19,7 +18,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final NumericProperty<Long> LONG_FIELD = PropertyFactory.createNumeric("longField", Long.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_SmallintTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_SmallintTestEntity.java
index 5c1edf8..cbec90c 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_SmallintTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_SmallintTestEntity.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
@@ -19,7 +18,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final NumericProperty<Short> SMALLINT_COL = PropertyFactory.createNumeric("smallintCol", Short.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_TinyintTestEntity.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_TinyintTestEntity.java
index 4051477..a16080c 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_TinyintTestEntity.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/numeric_types/auto/_TinyintTestEntity.java
@@ -5,7 +5,6 @@
 import java.io.ObjectOutputStream;
 
 import org.apache.cayenne.BaseDataObject;
-import org.apache.cayenne.exp.ExpressionFactory;
 import org.apache.cayenne.exp.property.NumericProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
 
@@ -19,7 +18,6 @@
 
     private static final long serialVersionUID = 1L; 
 
-    public static final NumericProperty<Integer> ID_PK_PROPERTY = PropertyFactory.createNumeric(ExpressionFactory.dbPathExp("ID"), Integer.class);
     public static final String ID_PK_COLUMN = "ID";
 
     public static final NumericProperty<Byte> TINYINT_COL = PropertyFactory.createNumeric("tinyintCol", Byte.class);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/testdo/testmap/auto/_Artist.java b/cayenne-server/src/test/java/org/apache/cayenne/testdo/testmap/auto/_Artist.java
index fa1545d..8474a22 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/testdo/testmap/auto/_Artist.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/testdo/testmap/auto/_Artist.java
@@ -21,6 +21,9 @@
  * It is probably a good idea to avoid changing this class manually,
  * since it may be overwritten next time code is regenerated.
  * If you need to make any customizations, please use subclass.
+ *
+ * Example of a comment
+ *
  */
 public abstract class _Artist extends BaseDataObject {
 
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/DerbyUnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/DerbyUnitDbAdapter.java
index 2a09aaa..dcc0872 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/DerbyUnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/DerbyUnitDbAdapter.java
@@ -74,7 +74,7 @@
     }
 
     @Override
-    public boolean supportsNullComparision() {
+    public boolean supportsNullComparison() {
         return false;
     }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
index 4351dab..87fa7bd 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/UnitDbAdapter.java
@@ -412,7 +412,7 @@
         return true;
     }
 
-    public boolean supportsNullComparision() {
+    public boolean supportsNullComparison() {
         return true;
     }
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/CommitStats.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/CommitStats.java
new file mode 100644
index 0000000..4679463
--- /dev/null
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/CommitStats.java
@@ -0,0 +1,50 @@
+package org.apache.cayenne.unit.di;
+
+import org.apache.cayenne.DataChannel;
+import org.apache.cayenne.DataChannelSyncFilter;
+import org.apache.cayenne.DataChannelSyncFilterChain;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.access.DataDomain;
+import org.apache.cayenne.graph.GraphDiff;
+import org.junit.rules.ExternalResource;
+
+import java.util.function.Supplier;
+
+public class CommitStats implements DataChannelSyncFilter {
+
+    private int commitCount;
+    private Supplier<DataDomain> dataDomain;
+
+    public CommitStats(Supplier<DataDomain> dataDomain) {
+        this.dataDomain = dataDomain;
+    }
+
+    public void before() {
+        dataDomain.get().addSyncFilter(this);
+        commitCount = 0;
+    }
+
+    public void after() {
+        dataDomain.get().removeSyncFilter(this);
+    }
+
+    @Override
+    public GraphDiff onSync(
+            ObjectContext originatingContext,
+            GraphDiff changes,
+            int syncType,
+            DataChannelSyncFilterChain filterChain) {
+
+        switch (syncType) {
+            case DataChannel.FLUSH_CASCADE_SYNC:
+                commitCount++;
+                break;
+        }
+
+        return filterChain.onSync(originatingContext, changes, syncType);
+    }
+
+    public int getCommitCount() {
+        return commitCount;
+    }
+}
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
index ed67478..3d7d1e5 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/CayenneProjects.java
@@ -84,4 +84,7 @@
     public static final String HYBRID_DATA_OBJECT_PROJECT = "cayenne-hybrid-data-object.xml";
     public static final String JAVA8 = "cayenne-java8.xml";
     public static final String INHERITANCE_WITH_ENUM_PROJECT = "cayenne-inheritance-with-enum.xml";
+    public static final String LAZY_ATTRIBUTES_PROJECT = "cayenne-lazy-attributes.xml";
+    public static final String CAY_2666 = "cay2666/cayenne-cay-2666.xml";
+    public static final String CAY_2641 = "cay2641/cayenne-cay-2641.xml";
 }
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
index 75a79ee..8067005 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/SchemaBuilder.java
@@ -82,7 +82,7 @@
 			"qualified.map.xml", "quoted-identifiers.map.xml", "inheritance-single-table1.map.xml",
 			"inheritance-vertical.map.xml", "oneway-rels.map.xml", "unsupported-distinct-types.map.xml",
 			"array-type.map.xml", "cay-2032.map.xml", "weighted-sort.map.xml", "hybrid-data-object.map.xml",
-			"java8.map.xml", "inheritance-with-enum.map.xml" };
+			"java8.map.xml", "inheritance-with-enum.map.xml", "lazy-attributes.map.xml", "cay2666/datamap.map.xml", "cay2641/datamapLazy.map.xml" };
 
 	// hardcoded dependent entities that should be excluded
 	// if LOBs are not supported
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceInfoProvider.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceInfoProvider.java
index fff3e73..edc7a6b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceInfoProvider.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseDataSourceInfoProvider.java
@@ -73,7 +73,7 @@
         hsqldb.setAdapterClassName(HSQLDBAdapter.class.getName());
         hsqldb.setUserName("sa");
         hsqldb.setPassword("");
-        hsqldb.setDataSourceUrl("jdbc:hsqldb:mem:aname");
+        hsqldb.setDataSourceUrl("jdbc:hsqldb:mem:aname;sql.regular_names=false");
         hsqldb.setJdbcDriver("org.hsqldb.jdbcDriver");
         hsqldb.setMinConnections(ConnectionProperties.MIN_CONNECTIONS);
         hsqldb.setMaxConnections(ConnectionProperties.MAX_CONNECTIONS);
@@ -83,7 +83,7 @@
         h2.setAdapterClassName(H2Adapter.class.getName());
         h2.setUserName("sa");
         h2.setPassword("");
-        h2.setDataSourceUrl("jdbc:h2:mem:aname;MVCC=TRUE;DB_CLOSE_DELAY=-1");
+        h2.setDataSourceUrl("jdbc:h2:mem:aname;DB_CLOSE_DELAY=-1");
         h2.setJdbcDriver("org.h2.Driver");
         h2.setMinConnections(ConnectionProperties.MIN_CONNECTIONS);
         h2.setMaxConnections(ConnectionProperties.MAX_CONNECTIONS);
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseModule.java b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseModule.java
index 8bc170f..83ce2b6 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseModule.java
+++ b/cayenne-server/src/test/java/org/apache/cayenne/unit/di/server/ServerCaseModule.java
@@ -28,6 +28,7 @@
 import org.apache.cayenne.access.ObjectMapRetainStrategy;
 import org.apache.cayenne.access.translator.batch.BatchTranslatorFactory;
 import org.apache.cayenne.access.types.BigDecimalType;
+import org.apache.cayenne.access.types.BigDecimalValueType;
 import org.apache.cayenne.access.types.BigIntegerValueType;
 import org.apache.cayenne.access.types.BooleanType;
 import org.apache.cayenne.access.types.ByteArrayType;
@@ -112,6 +113,8 @@
 import org.apache.cayenne.log.JdbcEventLogger;
 import org.apache.cayenne.log.Slf4jJdbcEventLogger;
 import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.reflect.generic.ValueComparisonStrategyFactory;
+import org.apache.cayenne.reflect.generic.DefaultValueComparisonStrategyFactory;
 import org.apache.cayenne.resource.ClassLoaderResourceLocator;
 import org.apache.cayenne.resource.ResourceLocator;
 import org.apache.cayenne.test.jdbc.DBHelper;
@@ -216,6 +219,7 @@
         ServerModule.contributeTypeFactories(binder);
         ServerModule.contributeValueObjectTypes(binder)
                 .add(BigIntegerValueType.class)
+                .add(BigDecimalValueType.class)
                 .add(UUIDValueType.class)
                 .add(LocalDateValueType.class)
                 .add(LocalTimeValueType.class)
@@ -223,6 +227,7 @@
                 .add(PeriodValueType.class)
                 .add(CharacterValueType.class);
         binder.bind(ValueObjectTypeRegistry.class).to(DefaultValueObjectTypeRegistry.class);
+        binder.bind(ValueComparisonStrategyFactory.class).to(DefaultValueComparisonStrategyFactory.class);
 
         binder.bind(SchemaBuilder.class).to(SchemaBuilder.class);
         binder.bind(JdbcEventLogger.class).to(Slf4jJdbcEventLogger.class);
diff --git a/cayenne-server/src/test/resources/cay2641/cayenne-cay-2641.xml b/cayenne-server/src/test/resources/cay2641/cayenne-cay-2641.xml
new file mode 100644
index 0000000..3990e67
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay2641/cayenne-cay-2641.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/10/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
+	 project-version="10">
+	<map name="datamapLazy"/>
+</domain>
diff --git a/cayenne-server/src/test/resources/cay2641/datamapLazy.map.xml b/cayenne-server/src/test/resources/cay2641/datamapLazy.map.xml
new file mode 100644
index 0000000..884f8c2
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay2641/datamapLazy.map.xml
@@ -0,0 +1,52 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
+	 project-version="10">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.cay_2641"/>
+	<property name="clientSupported" value="true"/>
+	<property name="defaultClientPackage" value="org.apache.cayenne.testdo.cay_2641.client"/>
+	<db-entity name="ArtistLazy">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true" length="5"/>
+		<db-attribute name="NAME" type="VARCHAR" length="10"/>
+		<db-attribute name="SURNAME" type="VARCHAR" length="10"/>
+	</db-entity>
+	<db-entity name="PaintingLazy">
+		<db-attribute name="ARTIST_ID" type="INTEGER" length="10"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true" length="10"/>
+		<db-attribute name="NAME" type="VARCHAR" length="10"/>
+	</db-entity>
+	<obj-entity name="ArtistLazy" className="org.apache.cayenne.testdo.cay_2641.ArtistLazy" clientClassName="org.apache.cayenne.testdo.cay_2641.client.ArtistLazy" dbEntityName="ArtistLazy">
+		<obj-attribute name="name" type="java.lang.String" lazy="true" db-attribute-path="NAME"/>
+		<obj-attribute name="surname" type="java.lang.String" db-attribute-path="SURNAME"/>
+	</obj-entity>
+	<obj-entity name="PaintingLazy" className="org.apache.cayenne.testdo.cay_2641.PaintingLazy" clientClassName="org.apache.cayenne.testdo.cay_2641.client.PaintingLazy" lock-type="optimistic" dbEntityName="PaintingLazy">
+		<obj-attribute name="name" type="java.lang.String" lazy="true" db-attribute-path="NAME"/>
+	</obj-entity>
+	<db-relationship name="paintings" source="ArtistLazy" target="PaintingLazy" toMany="true">
+		<db-attribute-pair source="ID" target="ARTIST_ID"/>
+	</db-relationship>
+	<db-relationship name="artist" source="PaintingLazy" target="ArtistLazy">
+		<db-attribute-pair source="ARTIST_ID" target="ID"/>
+	</db-relationship>
+	<obj-relationship name="paintings" source="ArtistLazy" target="PaintingLazy" deleteRule="Deny" db-relationship-path="paintings"/>
+	<obj-relationship name="artist" source="PaintingLazy" target="ArtistLazy" deleteRule="Nullify" db-relationship-path="artist"/>
+	<query name="prefetchSelect" type="SelectQuery" root="obj-entity" root-name="PaintingLazy">
+		<prefetch type="disjoint"><![CDATA[artist]]></prefetch>
+	</query>
+	<query name="simpleSelect" type="SelectQuery" root="obj-entity" root-name="ArtistLazy"/>
+	<cgen xmlns="http://cayenne.apache.org/schema/10/cgen">
+		<excludeEntities>ArtistLazy,PaintingLazy</excludeEntities>
+		<destDir>../../java</destDir>
+		<mode>all</mode>
+		<template>templates/v4_1/subclass.vm</template>
+		<superTemplate>templates/v4_1/superclass.vm</superTemplate>
+		<outputPattern>*.java</outputPattern>
+		<makePairs>true</makePairs>
+		<usePkgPath>true</usePkgPath>
+		<overwrite>false</overwrite>
+		<createPropertyNames>false</createPropertyNames>
+		<createPKProperties>false</createPKProperties>
+		<client>false</client>
+	</cgen>
+</data-map>
diff --git a/cayenne-server/src/test/resources/cay2666/cayenne-cay-2666.xml b/cayenne-server/src/test/resources/cay2666/cayenne-cay-2666.xml
new file mode 100644
index 0000000..9f629d3
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay2666/cayenne-cay-2666.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/10/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
+	 project-version="10">
+	<map name="datamap"/>
+</domain>
diff --git a/cayenne-server/src/test/resources/cay2666/datamap.map.xml b/cayenne-server/src/test/resources/cay2666/datamap.map.xml
new file mode 100644
index 0000000..ad70f79
--- /dev/null
+++ b/cayenne-server/src/test/resources/cay2666/datamap.map.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
+	 project-version="10">
+	<property name="quoteSqlIdentifiers" value="true"/>
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.cay_2666"/>
+	<db-entity name="Cay2666">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true" length="10"/>
+		<db-attribute name="NAME$" type="VARCHAR" length="10"/>
+	</db-entity>
+	<obj-entity name="Cay2666" className="org.apache.cayenne.testdo.cay_2666.CAY2666" dbEntityName="Cay2666">
+		<obj-attribute name="name$" type="java.lang.String" db-attribute-path="NAME$"/>
+	</obj-entity>
+	<dbImport xmlns="http://cayenne.apache.org/schema/10/dbimport">
+		<tableTypes>
+			<tableType>TABLE</tableType>
+			<tableType>VIEW</tableType>
+		</tableTypes>
+		<forceDataMapCatalog>false</forceDataMapCatalog>
+		<forceDataMapSchema>false</forceDataMapSchema>
+		<namingStrategy>org.apache.cayenne.dbsync.naming.DefaultObjectNameGenerator</namingStrategy>
+		<skipPrimaryKeyLoading>false</skipPrimaryKeyLoading>
+		<skipRelationshipsLoading>false</skipRelationshipsLoading>
+		<useJava7Types>false</useJava7Types>
+		<usePrimitives>true</usePrimitives>
+	</dbImport>
+	<cgen xmlns="http://cayenne.apache.org/schema/10/cgen">
+		<destDir>../../java</destDir>
+		<mode>entity</mode>
+		<template>templates/v4_1/subclass.vm</template>
+		<superTemplate>templates/v4_1/superclass.vm</superTemplate>
+		<outputPattern>*.java</outputPattern>
+		<makePairs>true</makePairs>
+		<usePkgPath>true</usePkgPath>
+		<overwrite>false</overwrite>
+		<createPropertyNames>false</createPropertyNames>
+		<createPKProperties>false</createPKProperties>
+		<client>false</client>
+	</cgen>
+</data-map>
diff --git a/cayenne-server/src/test/resources/cayenne-lazy-attributes.xml b/cayenne-server/src/test/resources/cayenne-lazy-attributes.xml
new file mode 100644
index 0000000..c1a414e
--- /dev/null
+++ b/cayenne-server/src/test/resources/cayenne-lazy-attributes.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/10/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
+	 project-version="10">
+	<map name="lazy-attributes"/>
+</domain>
diff --git a/cayenne-server/src/test/resources/cayenne-meaningful-pk.xml b/cayenne-server/src/test/resources/cayenne-meaningful-pk.xml
index 7980383..06e854d 100644
--- a/cayenne-server/src/test/resources/cayenne-meaningful-pk.xml
+++ b/cayenne-server/src/test/resources/cayenne-meaningful-pk.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <domain xmlns="http://cayenne.apache.org/schema/10/domain"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain http://cayenne.apache.org/schema/10/domain.xsd"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
 	 project-version="10">
 	<map name="meaningful-pk"/>
 </domain>
diff --git a/cayenne-server/src/test/resources/cayenne-numeric-types.xml b/cayenne-server/src/test/resources/cayenne-numeric-types.xml
index cb44482..d9c33bf 100644
--- a/cayenne-server/src/test/resources/cayenne-numeric-types.xml
+++ b/cayenne-server/src/test/resources/cayenne-numeric-types.xml
@@ -1,5 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <domain xmlns="http://cayenne.apache.org/schema/10/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/domain https://cayenne.apache.org/schema/10/domain.xsd"
 	 project-version="10">
 	<map name="numeric-types"/>
 </domain>
diff --git a/cayenne-server/src/test/resources/compound.map.xml b/cayenne-server/src/test/resources/compound.map.xml
index da8a373..cc37380 100644
--- a/cayenne-server/src/test/resources/compound.map.xml
+++ b/cayenne-server/src/test/resources/compound.map.xml
@@ -89,7 +89,7 @@
 		<db-attribute-pair source="KEY1" target="F_KEY1"/>
 		<db-attribute-pair source="KEY2" target="F_KEY2"/>
 	</db-relationship>
-	<db-relationship name="lines" source="compound_order" target="compound_order_line" toMany="true">
+	<db-relationship name="lines" source="compound_order" target="compound_order_line" toDependentPK="true" toMany="true">
 		<db-attribute-pair source="order_number" target="order_number"/>
 	</db-relationship>
 	<db-relationship name="order" source="compound_order_line" target="compound_order">
diff --git a/cayenne-server/src/test/resources/lazy-attributes.map.xml b/cayenne-server/src/test/resources/lazy-attributes.map.xml
new file mode 100644
index 0000000..d67b1b0
--- /dev/null
+++ b/cayenne-server/src/test/resources/lazy-attributes.map.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
+	 project-version="10">
+	<property name="defaultPackage" value="org.apache.cayenne.testdo.lazy"/>
+	<db-entity name="LAZYBLOB">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="LAZY_DATA" type="VARBINARY" length="10"/>
+		<db-attribute name="NAME" type="VARCHAR" isMandatory="true" length="255"/>
+	</db-entity>
+	<obj-entity name="Lazyblob" className="org.apache.cayenne.testdo.lazy.Lazyblob" dbEntityName="LAZYBLOB">
+		<obj-attribute name="lazyData" type="byte[]" lazy="true" db-attribute-path="LAZY_DATA"/>
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+</data-map>
diff --git a/cayenne-server/src/test/resources/meaningful-pk.map.xml b/cayenne-server/src/test/resources/meaningful-pk.map.xml
index a52d26e..d774b7f 100644
--- a/cayenne-server/src/test/resources/meaningful-pk.map.xml
+++ b/cayenne-server/src/test/resources/meaningful-pk.map.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
 	 project-version="10">
 	<property name="defaultPackage" value="org.apache.cayenne.testdo.meaningful_pk"/>
 	<property name="clientSupported" value="true"/>
@@ -27,6 +27,7 @@
 	</db-entity>
 	<obj-entity name="MeaningfulPKDep" className="org.apache.cayenne.testdo.meaningful_pk.MeaningfulPKDep" dbEntityName="MEANINGFUL_PK_DEP">
 		<obj-attribute name="descr" type="java.lang.String" db-attribute-path="DESCR"/>
+		<obj-attribute name="pk" type="int" db-attribute-path="PK_ATTRIBUTE"/>
 	</obj-entity>
 	<obj-entity name="MeaningfulPKTest1" className="org.apache.cayenne.testdo.meaningful_pk.MeaningfulPKTest1" dbEntityName="MEANINGFUL_PK_TEST1">
 		<obj-attribute name="descr" type="java.lang.String" db-attribute-path="DESCR"/>
@@ -49,5 +50,5 @@
 		<db-attribute-pair source="PK_ATTRIBUTE" target="MASTER_PK"/>
 	</db-relationship>
 	<obj-relationship name="toMeaningfulPK" source="MeaningfulPKDep" target="MeaningfulPKTest1" db-relationship-path="toMeaningfulPK"/>
-	<obj-relationship name="meaningfulPKDepArray" source="MeaningfulPKTest1" target="MeaningfulPKDep" db-relationship-path="meaningfulPKDepArray"/>
+	<obj-relationship name="meaningfulPKDepArray" source="MeaningfulPKTest1" target="MeaningfulPKDep" deleteRule="Cascade" db-relationship-path="meaningfulPKDepArray"/>
 </data-map>
diff --git a/cayenne-server/src/test/resources/numeric-types.map.xml b/cayenne-server/src/test/resources/numeric-types.map.xml
index 65d4000..d53a680 100644
--- a/cayenne-server/src/test/resources/numeric-types.map.xml
+++ b/cayenne-server/src/test/resources/numeric-types.map.xml
@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="utf-8"?>
 <data-map xmlns="http://cayenne.apache.org/schema/10/modelMap"
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap http://cayenne.apache.org/schema/10/modelMap.xsd"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/10/modelMap https://cayenne.apache.org/schema/10/modelMap.xsd"
 	 project-version="10">
 	<property name="defaultPackage" value="org.apache.cayenne.testdo.numeric_types"/>
 	<property name="defaultSuperclass" value="org.apache.cayenne.CayenneDataObject"/>
@@ -9,7 +9,8 @@
 	<property name="defaultClientPackage" value="test.client"/>
 	<property name="defaultClientSuperclass" value="org.apache.cayenne.PersistentObject"/>
 	<db-entity name="BIGDECIMAL_ENTITY">
-		<db-attribute name="BIGDECIMAL_FIELD" type="NUMERIC" length="12" scale="2"/>
+		<db-attribute name="BIG_DECIMAL_DECIMAL" type="DECIMAL" length="12" scale="6"/>
+		<db-attribute name="BIG_DECIMAL_NUMERIC" type="NUMERIC" length="12" scale="2"/>
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 	</db-entity>
 	<db-entity name="BIGINTEGER_ENTITY">
@@ -45,7 +46,8 @@
 		<db-attribute name="TINYINT_COL" type="TINYINT"/>
 	</db-entity>
 	<obj-entity name="BigDecimalEntity" className="org.apache.cayenne.testdo.numeric_types.BigDecimalEntity" dbEntityName="BIGDECIMAL_ENTITY">
-		<obj-attribute name="bigDecimalField" type="java.math.BigDecimal" db-attribute-path="BIGDECIMAL_FIELD"/>
+		<obj-attribute name="bigDecimalDecimal" type="java.math.BigDecimal" db-attribute-path="BIG_DECIMAL_DECIMAL"/>
+		<obj-attribute name="bigDecimalNumeric" type="java.math.BigDecimal" db-attribute-path="BIG_DECIMAL_NUMERIC"/>
 	</obj-entity>
 	<obj-entity name="BigIntegerEntity" className="org.apache.cayenne.testdo.numeric_types.BigIntegerEntity" dbEntityName="BIGINTEGER_ENTITY">
 		<obj-attribute name="bigIntegerField" type="java.math.BigInteger" db-attribute-path="BIG_INTEGER_FIELD"/>
diff --git a/cayenne-server/src/test/resources/testmap.map.xml b/cayenne-server/src/test/resources/testmap.map.xml
index 5ec09e0..e7727c8 100644
--- a/cayenne-server/src/test/resources/testmap.map.xml
+++ b/cayenne-server/src/test/resources/testmap.map.xml
@@ -87,6 +87,7 @@
 	<obj-entity name="Artist" className="org.apache.cayenne.testdo.testmap.Artist" dbEntityName="ARTIST">
 		<obj-attribute name="artistName" type="java.lang.String" db-attribute-path="ARTIST_NAME"/>
 		<obj-attribute name="dateOfBirth" type="java.util.Date" db-attribute-path="DATE_OF_BIRTH"/>
+		<info:property xmlns:info="http://cayenne.apache.org/schema/10/info" name="comment" value="Example of a comment"/>
 	</obj-entity>
 	<obj-entity name="ArtistCallback" className="org.apache.cayenne.testdo.testmap.ArtistCallback" dbEntityName="ARTIST_CT">
 		<obj-attribute name="artistName" type="java.lang.String"/>
diff --git a/cayenne-velocity/pom.xml b/cayenne-velocity/pom.xml
index 53549e5..38cf67b 100644
--- a/cayenne-velocity/pom.xml
+++ b/cayenne-velocity/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-web/pom.xml b/cayenne-web/pom.xml
index cb3ec3f..81fe322 100644
--- a/cayenne-web/pom.xml
+++ b/cayenne-web/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-xmpp/pom.xml b/cayenne-xmpp/pom.xml
index e0996e1..f4e69e8 100644
--- a/cayenne-xmpp/pom.xml
+++ b/cayenne-xmpp/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-parent</artifactId>
         <groupId>org.apache.cayenne</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/cayenne-xmpp/src/main/java/org/apache/cayenne/event/XMPPBridge.java b/cayenne-xmpp/src/main/java/org/apache/cayenne/event/XMPPBridge.java
index 0563ffd..103baaa 100644
--- a/cayenne-xmpp/src/main/java/org/apache/cayenne/event/XMPPBridge.java
+++ b/cayenne-xmpp/src/main/java/org/apache/cayenne/event/XMPPBridge.java
@@ -20,7 +20,7 @@
 package org.apache.cayenne.event;
 
 import org.apache.cayenne.CayenneRuntimeException;
-import org.apache.cayenne.util.Base64Codec;
+import org.apache.cayenne.event.util.Base64Codec;
 import org.apache.cayenne.util.Util;
 import org.jivesoftware.smack.GroupChat;
 import org.jivesoftware.smack.PacketListener;
diff --git a/cayenne-server/src/main/java/org/apache/cayenne/util/Base64Codec.java b/cayenne-xmpp/src/main/java/org/apache/cayenne/event/util/Base64Codec.java
similarity index 95%
rename from cayenne-server/src/main/java/org/apache/cayenne/util/Base64Codec.java
rename to cayenne-xmpp/src/main/java/org/apache/cayenne/event/util/Base64Codec.java
index 5d4ba9f..b82abf6 100644
--- a/cayenne-server/src/main/java/org/apache/cayenne/util/Base64Codec.java
+++ b/cayenne-xmpp/src/main/java/org/apache/cayenne/event/util/Base64Codec.java
@@ -17,7 +17,7 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.util;
+package org.apache.cayenne.event.util;
 
 /**
  * Provides Base64 encoding and decoding as defined by RFC 2045.
@@ -25,9 +25,11 @@
  * <i>This codec is based on Apache commons.codec implementation, copyright The Apache
  * Software Foundation.</i>
  * </p>
- * 
+ *
  * @since 1.2
+ * @deprecated since 4.2. Java 8 has a built-in Base64 class.
  */
+@Deprecated
 public class Base64Codec {
 
     /**
@@ -36,14 +38,14 @@
      * The {@value} character limit does not count the trailing CRLF, but counts all other
      * characters, including any equal signs.
      * </p>
-     * 
+     *
      * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
      */
     static final int CHUNK_SIZE = 76;
 
     /**
      * Chunk separator per RFC 2045 section 2.1.
-     * 
+     *
      * @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
      */
     static final byte[] CHUNK_SEPARATOR = "\r\n".getBytes();
@@ -130,11 +132,9 @@
     private static boolean isBase64(byte octect) {
         if (octect == PAD) {
             return true;
-        }
-        else if (base64Alphabet[octect] == -1) {
+        } else if (base64Alphabet[octect] == -1) {
             return false;
-        }
-        else {
+        } else {
             return true;
         }
     }
@@ -142,10 +142,10 @@
     /**
      * Tests a given byte array to see if it contains only valid characters within the
      * Base64 alphabet.
-     * 
+     *
      * @param arrayOctect byte array to test
      * @return true if all bytes are valid characters in the Base64 alphabet or if the
-     *         byte array is empty; false, otherwise
+     * byte array is empty; false, otherwise
      */
     public static boolean isArrayByteBase64(byte[] arrayOctect) {
 
@@ -167,7 +167,7 @@
 
     /**
      * Encodes binary data using the base64 algorithm but does not chunk the output.
-     * 
+     *
      * @param binaryData binary data to encode
      * @return Base64 characters
      */
@@ -178,7 +178,7 @@
     /**
      * Encodes binary data using the base64 algorithm and chunks the encoded output into
      * 76 character blocks
-     * 
+     *
      * @param binaryData binary data to encode
      * @return Base64 characters chunked in 76 character blocks
      */
@@ -189,10 +189,10 @@
     /**
      * Encodes binary data using the base64 algorithm, optionally chunking the output into
      * 76 character blocks.
-     * 
+     *
      * @param binaryData Array containing binary data to encode.
-     * @param isChunked if isChunked is true this encoder will chunk the base64 output
-     *            into 76 character blocks
+     * @param isChunked  if isChunked is true this encoder will chunk the base64 output
+     *                   into 76 character blocks
      * @return Base64-encoded data.
      */
     public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
@@ -206,8 +206,7 @@
         if (fewerThan24bits != 0) {
             // data not divisible by 24 bit
             encodedDataLength = (numberTriplets + 1) * 4;
-        }
-        else {
+        } else {
             // 16 or 8 bit
             encodedDataLength = numberTriplets * 4;
         }
@@ -289,8 +288,7 @@
             encodedData[encodedIndex + 1] = lookUpBase64Alphabet[k << 4];
             encodedData[encodedIndex + 2] = PAD;
             encodedData[encodedIndex + 3] = PAD;
-        }
-        else if (fewerThan24bits == SIXTEENBIT) {
+        } else if (fewerThan24bits == SIXTEENBIT) {
 
             b1 = binaryData[dataIndex];
             b2 = binaryData[dataIndex + 1];
@@ -319,7 +317,7 @@
 
     /**
      * Decodes Base64 data into octects
-     * 
+     *
      * @param base64Data Byte array containing Base64 data
      * @return Array containing decoded data.
      */
@@ -368,12 +366,10 @@
                 decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
                 decodedData[encodedIndex + 1] = (byte) (((b2 & 0xf) << 4) | ((b3 >> 2) & 0xf));
                 decodedData[encodedIndex + 2] = (byte) (b3 << 6 | b4);
-            }
-            else if (marker0 == PAD) {
+            } else if (marker0 == PAD) {
                 // Two PAD e.g. 3c[Pad][Pad]
                 decodedData[encodedIndex] = (byte) (b1 << 2 | b2 >> 4);
-            }
-            else if (marker1 == PAD) {
+            } else if (marker1 == PAD) {
                 // One PAD e.g. 3cQ[Pad]
                 b3 = base64Alphabet[marker0];
 
@@ -387,7 +383,7 @@
 
     /**
      * Discards any whitespace from a base-64 encoded block.
-     * 
+     *
      * @param data The base-64 encoded data to discard the whitespace from.
      * @return The data, less whitespace (see RFC 2045).
      */
@@ -418,7 +414,7 @@
      * Discards any characters outside of the base64 alphabet, per the requirements on
      * page 25 of RFC 2045 - "Any characters outside of the base64 alphabet are to be
      * ignored in base64 encoded data."
-     * 
+     *
      * @param data The base-64 encoded data to groom
      * @return The data, less non-base64 characters (see RFC 2045).
      */
diff --git a/cayenne-server/src/test/java/org/apache/cayenne/util/Base64CodecTest.java b/cayenne-xmpp/src/test/java/org/apache/cayenne/event/util/Base64CodecTest.java
similarity index 99%
rename from cayenne-server/src/test/java/org/apache/cayenne/util/Base64CodecTest.java
rename to cayenne-xmpp/src/test/java/org/apache/cayenne/event/util/Base64CodecTest.java
index 0ae1a82..e3ffc2b 100644
--- a/cayenne-server/src/test/java/org/apache/cayenne/util/Base64CodecTest.java
+++ b/cayenne-xmpp/src/test/java/org/apache/cayenne/event/util/Base64CodecTest.java
@@ -17,7 +17,7 @@
  *  under the License.
  ****************************************************************/
 
-package org.apache.cayenne.util;
+package org.apache.cayenne.event.util;
 
 import org.junit.Test;
 
diff --git a/docs/asciidoc/cayenne-asciidoc-extension/pom.xml b/docs/asciidoc/cayenne-asciidoc-extension/pom.xml
index 60a6801..08a0a18 100644
--- a/docs/asciidoc/cayenne-asciidoc-extension/pom.xml
+++ b/docs/asciidoc/cayenne-asciidoc-extension/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-asciidoc-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <artifactId>cayenne-asciidoc-extension</artifactId>
diff --git a/docs/asciidoc/cayenne-guide/pom.xml b/docs/asciidoc/cayenne-guide/pom.xml
index f0f29c4..ce16b67 100644
--- a/docs/asciidoc/cayenne-guide/pom.xml
+++ b/docs/asciidoc/cayenne-guide/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-asciidoc-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc
index d3419bf..732c983 100644
--- a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc
+++ b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc
@@ -11,6 +11,8 @@
 // CONDITIONS OF ANY KIND, either express or implied. See the License for
 // the specific language governing permissions and limitations under the
 // License.
+
+[[sqlselect]]
 ==== SQLSelect and SQLExec
 
 SQL is very powerful and allows to manipulate data in ways that can not always be described as a graph of related entities.
diff --git a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc
index bbcdeb5..7349e05 100644
--- a/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc
+++ b/docs/asciidoc/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc
@@ -25,20 +25,16 @@
 
 [source, Java]
 ----
-ObjectSelect<Artist> query = ObjectSelect.query(Artist.class);
-
-// instructs Cayenne to prefetch one of Artist's relationships
-query.prefetch(Artist.PAINTINGS.disjoint());
-
-// the above line is equivalent to the following:
-// query.prefetch("paintings", PrefetchTreeNode.DISJOINT_PREFETCH_SEMANTICS);
-
-// query is expecuted as usual, but the resulting Artists will have
-// their paintings "inflated"
-List<Artist> artists = query.select(context);
+List<Artist> artists = ObjectSelect
+    .query(Artist.class)
+    .prefetch(Artist.PAINTINGS.disjoint()) // <1>
+    .select(context); // <2>
 ----
 
-All types of relationships can be preftetched - to-one, to-many, flattened. A prefetch can span multiple relationships:
+<1> Instructs Cayenne to prefetch one of Artist's relationships.
+<2> Query is executed as usual, but the resulting Artists will have their paintings "inflated"
+
+All types of relationships can be prefetched - to-one, to-many, flattened. A prefetch can span multiple relationships:
 
 [source, Java]
 ----
@@ -49,18 +45,17 @@
 
 [source, Java]
 ----
-query.prefetch(Artist.PAINTINGS.disjoint());
-query.prefetch(Artist.PAINTINGS.dot(Painting.GALLERY).disjoint());
+query.prefetch(Artist.PAINTINGS.disjoint())
+   .prefetch(Artist.PAINTINGS.dot(Painting.GALLERY).disjoint());
 ----
 
 If a query is fetching DataRows, all "disjoint" prefetches are ignored, only "joint" prefetches are executed
 (see prefetching semantics discussion below for what disjoint and joint prefetches mean).
 
-===== Prefetching Semantics
-
-Prefetching semantics defines a strategy to prefetch relationships. Depending on it, Cayenne would generate different types of queries.
-The end result is the same - query root objects with related objects fully resolved. However semantics can affect performance,
-in some cases significantly. There are 3 types of prefetch semantics, all defined as constants in `org.apache.cayenne.query.PrefetchTreeNode`:
+A strategy to prefetch relationships is defined by prefetch "semantics". Depending on semantics, Cayenne would generate
+different types of queries. The end result is the same - query root objects with related objects fully resolved.
+However semantics can affect performance, in some cases significantly. There are 3 types of prefetch semantics
+defined as constants in `org.apache.cayenne.query.PrefetchTreeNode`:
 
 [source]
 ----
@@ -69,13 +64,7 @@
 PrefetchTreeNode.DISJOINT_BY_ID_PREFETCH_SEMANTICS
 ----
 
-There's no limitation on mixing different types of semantics in the same query. Each prefetch can have its own semantics.
-`SelectQuery` uses `DISJOINT_PREFETCH_SEMANTICS` by default. `ObjectSelect` requires explicit semantics as we've seen above.
- `SQLTemplate` and `ProcedureQuery` are both using `JOINT_PREFETCH_SEMANTICS` and it can not be changed due to the nature of those two queries.
-
-===== Disjoint Prefetching Semantics
-
-This semantics results in Cayenne generatiing one SQL statement for the main objects, and a separate statement for
+*Disjoint prefetch semantics* results in Cayenne generating one SQL statement for the main objects, and a separate statement for
 each prefetch path (hence "disjoint" - related objects are not fetched with the main query).
 Each additional SQL statement uses a qualifier of the main query plus a set of joins traversing the
 prefetch path between the main and related entity.
@@ -83,12 +72,10 @@
 This strategy has an advantage of efficient JVM memory use, and faster overall result processing by Cayenne,
 but it requires (1+N) SQL statements to be executed, where N is the number of prefetched relationships.
 
-===== Disjoint-by-ID Prefetching Semantics
-
-This is a variation of disjoint prefetch where related objects are matched against a set of IDs derived from the fetched
-main objects (or intermediate objects in a multi-step prefetch). Cayenne limits the size of the generated WHERE clause,
-as most DBs can't parse arbitrary large SQL. So prefetch queries are broken into smaller queries.
-The size of is controlled by the DI property `Constants.SERVER_MAX_ID_QUALIFIER_SIZE_PROPERTY`
+*Disjoint-by-ID prefetch semantics* is a variation of disjoint prefetch where related objects are matched against a set
+of IDs derived from the fetched main objects (or intermediate objects in a multi-step prefetch). Cayenne limits the
+size of the generated WHERE clause, as most DBs can't parse arbitrary large SQL. So prefetch queries are broken into
+smaller queries. The size of is controlled by the DI property `Constants.SERVER_MAX_ID_QUALIFIER_SIZE_PROPERTY`
 (the default number of conditions in the generated WHERE clause is 10000).
 Cayenne will generate (1 + N * M) SQL statements for each query using disjoint-by-ID prefetches,
 where N is the number of relationships to prefetch, and M is the number of queries for a given prefetch
@@ -100,9 +87,7 @@
 
 The disadvantage is that query SQL can get unwieldy for large result sets, as each object will have to have its own condition in the WHERE clause of the generated SQL.
 
-===== Joint Prefetching Semantics
-
-Joint semantics results in a single SQL statement for root objects and any number of jointly prefetched paths.
+*Joint prefetch semantics* results in a single SQL statement for root objects and any number of jointly prefetched paths.
 Cayenne processes in memory a cartesian product of the entities involved, converting it to an object tree.
 It uses OUTER joins to connect prefetched entities.
 
@@ -110,18 +95,43 @@
 Its downsides are the potentially increased amount of data that needs to get across the network between the application server and the database,
 and more data processing that needs to be done on the Cayenne side.
 
-===== Similar Behaviours Using EJBQL
+`<<select,ObjectSelect>>` query supports all three types of semantics. You can mix and match them in the same query for
+different prefetches.
 
-It is possible to achieve similar behaviours with <<EJBQLQuery>> queries by employing the "FETCH" keyword.
+`<<sqlselect,SQLSelect>>` query supports "JOINT" and "DISJOINT_BY_ID". It does not work with "DISJOINT", as the query does not provide
+enough information to Cayenne to build dependent prefetch queries. So "DISJOINT" will result in exception. "JOINT"
+prefetching requires a bit of effort shaping the SQL to include the right columns in the result and label them properly
+to be convertable into object properties. The main rules to follow are:
+
+* Include _all_ columns from the root entity and every prefetched entity.
+* Label each prefetched entity columns as "dbRelationship.column".
+
+E.g.:
+
+[source, Java]
+----
+List<Artist> objects = SQLSelect.query(Artist.class, "SELECT "
+    + "#result('ESTIMATED_PRICE' 'BigDecimal' '' 'paintingArray.ESTIMATED_PRICE'), "
+    + "#result('PAINTING_TITLE' 'String' '' 'paintingArray.PAINTING_TITLE'), "
+    + "#result('GALLERY_ID' 'int' '' 'paintingArray.GALLERY_ID'), "
+    + "#result('PAINTING_ID' 'int' '' 'paintingArray.PAINTING_ID'), "
+    + "#result('t1.ARTIST_ID' 'int' '' 'paintingArray.ARTIST_ID'), "
+    + "#result('ARTIST_NAME' 'String'), "
+    + "#result('DATE_OF_BIRTH' 'java.util.Date'), "
+    + "#result('t0.ARTIST_ID' 'int' '' 'ARTIST_ID') "
+    + "FROM ARTIST t0, PAINTING t1 "
+    + "WHERE t0.ARTIST_ID = t1.ARTIST_ID")
+    .addPrefetch(Artist.PAINTING_ARRAY.joint())
+    .select(context);
+----
+
+`<<ejbql,EJBQLQuery>>` uses the "FETCH" keyword to enable prefetching:
 
 [source, SQL]
 ----
 SELECT a FROM Artist a LEFT JOIN FETCH a.paintings
 ----
 
-In this case, the Paintings that exist for the Artist will be obtained at the same time as the Artists are fetched.
-Refer to third-party query language documentation for further detail on this mechanism.
-
 ==== Data Rows
 
 Converting result set data to Persistent objects and registering these objects in the ObjectContext can be an expensive
diff --git a/docs/asciidoc/getting-started-db-first/pom.xml b/docs/asciidoc/getting-started-db-first/pom.xml
index 47d2503..6eb6db3 100644
--- a/docs/asciidoc/getting-started-db-first/pom.xml
+++ b/docs/asciidoc/getting-started-db-first/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-asciidoc-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/docs/asciidoc/getting-started-guide/pom.xml b/docs/asciidoc/getting-started-guide/pom.xml
index 35004e6..619972e 100644
--- a/docs/asciidoc/getting-started-guide/pom.xml
+++ b/docs/asciidoc/getting-started-guide/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-asciidoc-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/docs/asciidoc/getting-started-rop/pom.xml b/docs/asciidoc/getting-started-rop/pom.xml
index ea3d58e..f030522 100644
--- a/docs/asciidoc/getting-started-rop/pom.xml
+++ b/docs/asciidoc/getting-started-rop/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>cayenne-asciidoc-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
     <modelVersion>4.0.0</modelVersion>
 
diff --git a/docs/asciidoc/pom.xml b/docs/asciidoc/pom.xml
index 62826b3..74f0ef6 100644
--- a/docs/asciidoc/pom.xml
+++ b/docs/asciidoc/pom.xml
@@ -23,7 +23,7 @@
     <parent>
         <artifactId>cayenne-docs-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <artifactId>cayenne-asciidoc-parent</artifactId>
@@ -125,7 +125,7 @@
                 <executions>
                     <execution>
                         <id>copy docs for site</id>
-                        <phase>install</phase>
+                        <phase>package</phase>
                         <goals>
                             <goal>copy-resources</goal>
                         </goals>
@@ -146,7 +146,7 @@
 
                     <execution>
                         <id>copy images for site</id>
-                        <phase>install</phase>
+                        <phase>package</phase>
                         <goals>
                             <goal>copy-resources</goal>
                         </goals>
diff --git a/docs/asciidoc/upgrade-guide/pom.xml b/docs/asciidoc/upgrade-guide/pom.xml
index d2c5302..de4017b 100644
--- a/docs/asciidoc/upgrade-guide/pom.xml
+++ b/docs/asciidoc/upgrade-guide/pom.xml
@@ -22,7 +22,7 @@
     <parent>
         <artifactId>cayenne-asciidoc-parent</artifactId>
         <groupId>org.apache.cayenne.docs</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <modelVersion>4.0.0</modelVersion>
diff --git a/docs/doc/pom.xml b/docs/doc/pom.xml
index a9496e5..745ba06 100644
--- a/docs/doc/pom.xml
+++ b/docs/doc/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.docs</groupId>
 		<artifactId>cayenne-docs-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-doc</artifactId>
diff --git a/docs/pom.xml b/docs/pom.xml
index 6c0c652..233eb88 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -25,7 +25,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<groupId>org.apache.cayenne.docs</groupId>
diff --git a/maven-plugins/cayenne-maven-plugin/pom.xml b/maven-plugins/cayenne-maven-plugin/pom.xml
index d7b7cd8..84ec42d 100644
--- a/maven-plugins/cayenne-maven-plugin/pom.xml
+++ b/maven-plugins/cayenne-maven-plugin/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<artifactId>cayenne-maven-plugins-parent</artifactId>
 		<groupId>org.apache.cayenne.plugins</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<modelVersion>4.0.0</modelVersion>
diff --git a/maven-plugins/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java b/maven-plugins/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
index 84ddbdd..9ac9329 100644
--- a/maven-plugins/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
+++ b/maven-plugins/cayenne-maven-plugin/src/main/java/org/apache/cayenne/tools/CayenneGeneratorMojo.java
@@ -224,6 +224,13 @@
     @Parameter
     private Boolean createPKProperties;
 
+    /**
+     * Optional path (classpath or filesystem) to external velocity tool configuration file
+     * @since 4.2 
+     */
+    @Parameter
+    private String externalToolConfig;
+    
     private transient Injector injector;
 
     private static final Logger logger = LoggerFactory.getLogger(CayenneGeneratorMojo.class);
@@ -297,7 +304,7 @@
 				makePairs != null || mode != null || outputPattern != null || overwrite != null || superPkg != null ||
 				superTemplate != null || template != null || embeddableTemplate != null || embeddableSuperTemplate != null ||
 				usePkgPath != null || createPropertyNames != null || force || queryTemplate != null ||
-				querySuperTemplate != null || createPKProperties != null;
+				querySuperTemplate != null || createPKProperties != null || externalToolConfig != null;
 	}
 
 	/**
@@ -349,6 +356,7 @@
 		cgenConfiguration.setQueryTemplate(queryTemplate != null ? queryTemplate : cgenConfiguration.getQueryTemplate());
 		cgenConfiguration.setQuerySuperTemplate(querySuperTemplate != null ? querySuperTemplate : cgenConfiguration.getQuerySuperTemplate());
 		cgenConfiguration.setCreatePKProperties(createPKProperties != null ? createPKProperties : cgenConfiguration.isCreatePKProperties());
+		cgenConfiguration.setExternalToolConfig(externalToolConfig != null ? externalToolConfig : cgenConfiguration.getExternalToolConfig());
 		if(!cgenConfiguration.isMakePairs()) {
 			if(template == null) {
 				cgenConfiguration.setTemplate(cgenConfiguration.isClient() ? ClientClassGenerationAction.SINGLE_CLASS_TEMPLATE : ClassGenerationAction.SINGLE_CLASS_TEMPLATE);
diff --git a/maven-plugins/cayenne-modeler-maven-plugin/pom.xml b/maven-plugins/cayenne-modeler-maven-plugin/pom.xml
index ecb1a00..976da59 100644
--- a/maven-plugins/cayenne-modeler-maven-plugin/pom.xml
+++ b/maven-plugins/cayenne-modeler-maven-plugin/pom.xml
@@ -22,7 +22,7 @@
 	<parent>
 		<artifactId>cayenne-maven-plugins-parent</artifactId>
 		<groupId>org.apache.cayenne.plugins</groupId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<modelVersion>4.0.0</modelVersion>
diff --git a/maven-plugins/cayenne-tools-itest/pom.xml b/maven-plugins/cayenne-tools-itest/pom.xml
index b6a7db6..9560881 100644
--- a/maven-plugins/cayenne-tools-itest/pom.xml
+++ b/maven-plugins/cayenne-tools-itest/pom.xml
@@ -21,7 +21,7 @@
     <parent>
         <artifactId>cayenne-maven-plugins-parent</artifactId>
         <groupId>org.apache.cayenne.plugins</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
 	<description>Integration Tests - Cayenne Tools</description>
diff --git a/maven-plugins/pom.xml b/maven-plugins/pom.xml
index 7123582..25f1e7e 100644
--- a/maven-plugins/pom.xml
+++ b/maven-plugins/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	<groupId>org.apache.cayenne.plugins</groupId>
 	<artifactId>cayenne-maven-plugins-parent</artifactId>
diff --git a/modeler/cayenne-modeler-generic-ext/pom.xml b/modeler/cayenne-modeler-generic-ext/pom.xml
index 4fc3796..5afe16c 100644
--- a/modeler/cayenne-modeler-generic-ext/pom.xml
+++ b/modeler/cayenne-modeler-generic-ext/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler-generic-ext</artifactId>
diff --git a/modeler/cayenne-modeler-generic/pom.xml b/modeler/cayenne-modeler-generic/pom.xml
index 1b434eb..824e567 100644
--- a/modeler/cayenne-modeler-generic/pom.xml
+++ b/modeler/cayenne-modeler-generic/pom.xml
@@ -16,7 +16,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler-generic</artifactId>
diff --git a/modeler/cayenne-modeler-mac-ext/pom.xml b/modeler/cayenne-modeler-mac-ext/pom.xml
index 0498608..667fdb3 100644
--- a/modeler/cayenne-modeler-mac-ext/pom.xml
+++ b/modeler/cayenne-modeler-mac-ext/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler-mac-ext</artifactId>
diff --git a/modeler/cayenne-modeler-mac/pom.xml b/modeler/cayenne-modeler-mac/pom.xml
index 3fa1819..0b6409d 100644
--- a/modeler/cayenne-modeler-mac/pom.xml
+++ b/modeler/cayenne-modeler-mac/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler-mac</artifactId>
diff --git a/modeler/cayenne-modeler-win-ext/pom.xml b/modeler/cayenne-modeler-win-ext/pom.xml
index 630a56f..c0887c3 100644
--- a/modeler/cayenne-modeler-win-ext/pom.xml
+++ b/modeler/cayenne-modeler-win-ext/pom.xml
@@ -24,7 +24,7 @@
     <parent>
         <artifactId>cayenne-modeler-parent</artifactId>
         <groupId>org.apache.cayenne.modeler</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <name>cayenne-modeler-win-ext: Modeler Win Extensions</name>
diff --git a/modeler/cayenne-modeler-win/pom.xml b/modeler/cayenne-modeler-win/pom.xml
index 41e472c..6480183 100644
--- a/modeler/cayenne-modeler-win/pom.xml
+++ b/modeler/cayenne-modeler-win/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler-win</artifactId>
diff --git a/modeler/cayenne-modeler/pom.xml b/modeler/cayenne-modeler/pom.xml
index 1db2fa7..0945cfd 100644
--- a/modeler/cayenne-modeler/pom.xml
+++ b/modeler/cayenne-modeler/pom.xml
@@ -24,7 +24,7 @@
     <parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler</artifactId>
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DBConnectionAwareAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DBConnectionAwareAction.java
new file mode 100644
index 0000000..d006889
--- /dev/null
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DBConnectionAwareAction.java
@@ -0,0 +1,109 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.modeler.action;
+
+import java.util.prefs.Preferences;
+
+import org.apache.cayenne.modeler.Application;
+import org.apache.cayenne.modeler.dialog.db.DataSourceWizard;
+import org.apache.cayenne.modeler.pref.DBConnectionInfo;
+import org.apache.cayenne.modeler.pref.DataMapDefaults;
+import org.apache.cayenne.modeler.util.CayenneAction;
+
+import static org.apache.cayenne.modeler.pref.DBConnectionInfo.*;
+
+/**
+ * Base action that provides DBConnectionInfo for the current DataMap or calls {@link DataSourceWizard} dialog to
+ * create one.
+ *
+ * @since 4.2
+ */
+public abstract class DBConnectionAwareAction extends CayenneAction {
+
+    public DBConnectionAwareAction(String name, Application application) {
+        super(name, application);
+    }
+
+    protected DBConnectionInfo getConnectionInfo(String title) {
+        DBConnectionInfo connectionInfo;
+        if (datamapPrefNotExist()) {
+            DataSourceWizard connectWizard = getDataSourceWizard(title);
+            if (connectWizard == null) {
+                return null;
+            }
+            connectionInfo = connectWizard.getConnectionInfo();
+            saveConnectionInfo(connectWizard);
+        } else {
+            connectionInfo = getConnectionInfoFromPreferences();
+        }
+        return connectionInfo;
+    }
+
+    protected DataSourceWizard getDataSourceWizard(String title, String[] buttons) {
+        DataSourceWizard connectWizard = new DataSourceWizard(getProjectController(), title, buttons);
+        if (!connectWizard.startupAction()) {
+            return null;
+        }
+        return connectWizard;
+    }
+
+    protected DataSourceWizard getDataSourceWizard(String title) {
+        DataSourceWizard connectWizard = new DataSourceWizard(getProjectController(), title);
+        if (!connectWizard.startupAction()) {
+            return null;
+        }
+        return connectWizard;
+    }
+
+    protected boolean datamapPrefNotExist() {
+        Preferences dataMapPreference = getProjectController().
+                getDataMapPreferences(getProjectController().getCurrentDataMap())
+                .getCurrentPreference();
+        return dataMapPreference == null || dataMapPreference.get(URL_PROPERTY, null) == null;
+    }
+
+    protected DBConnectionInfo getConnectionInfoFromPreferences() {
+        DBConnectionInfo connectionInfo = new DBConnectionInfo();
+        DataMapDefaults dataMapDefaults = getProjectController()
+                .getDataMapPreferences(getProjectController().getCurrentDataMap());
+        connectionInfo.setDbAdapter(dataMapDefaults.getCurrentPreference().get(DB_ADAPTER_PROPERTY, null));
+        connectionInfo.setUrl(dataMapDefaults.getCurrentPreference().get(URL_PROPERTY, null));
+        connectionInfo.setUserName(dataMapDefaults.getCurrentPreference().get(USER_NAME_PROPERTY, null));
+        connectionInfo.setPassword(dataMapDefaults.getCurrentPreference().get(PASSWORD_PROPERTY, null));
+        connectionInfo.setJdbcDriver(dataMapDefaults.getCurrentPreference().get(JDBC_DRIVER_PROPERTY, null));
+        return connectionInfo;
+    }
+
+    protected void saveConnectionInfo(DataSourceWizard connectWizard) {
+        DataMapDefaults dataMapDefaults = getProjectController().
+                getDataMapPreferences(getProjectController().getCurrentDataMap());
+
+        String dbAdapter = connectWizard.getConnectionInfo().getDbAdapter();
+        if(dbAdapter != null) {
+            dataMapDefaults.getCurrentPreference().put(DB_ADAPTER_PROPERTY, dbAdapter);
+        } else {
+            dataMapDefaults.getCurrentPreference().remove(DB_ADAPTER_PROPERTY);
+        }
+        dataMapDefaults.getCurrentPreference().put(URL_PROPERTY, connectWizard.getConnectionInfo().getUrl());
+        dataMapDefaults.getCurrentPreference().put(USER_NAME_PROPERTY, connectWizard.getConnectionInfo().getUserName());
+        dataMapDefaults.getCurrentPreference().put(PASSWORD_PROPERTY, connectWizard.getConnectionInfo().getPassword());
+        dataMapDefaults.getCurrentPreference().put(JDBC_DRIVER_PROPERTY, connectWizard.getConnectionInfo().getJdbcDriver());
+    }
+}
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DBWizardAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DBWizardAction.java
deleted file mode 100644
index a08b34d..0000000
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DBWizardAction.java
+++ /dev/null
@@ -1,113 +0,0 @@
-/*****************************************************************
- *   Licensed to the Apache Software Foundation (ASF) under one
- *  or more contributor license agreements.  See the NOTICE file
- *  distributed with this work for additional information
- *  regarding copyright ownership.  The ASF licenses this file
- *  to you under the Apache License, Version 2.0 (the
- *  "License"); you may not use this file except in compliance
- *  with the License.  You may obtain a copy of the License at
- *
- *    https://www.apache.org/licenses/LICENSE-2.0
- *
- *  Unless required by applicable law or agreed to in writing,
- *  software distributed under the License is distributed on an
- *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- *  KIND, either express or implied.  See the License for the
- *  specific language governing permissions and limitations
- *  under the License.
- ****************************************************************/
-
-package org.apache.cayenne.modeler.action;
-
-import org.apache.cayenne.dbsync.reverse.dbload.DbLoader;
-import org.apache.cayenne.modeler.Application;
-import org.apache.cayenne.modeler.dialog.db.DataSourceWizard;
-import org.apache.cayenne.modeler.dialog.db.DbActionOptionsDialog;
-import org.apache.cayenne.modeler.util.CayenneAction;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.swing.*;
-import java.sql.Connection;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-
-public abstract class DBWizardAction<T extends DbActionOptionsDialog> extends CayenneAction {
-	private static Logger LOGGER = LoggerFactory.getLogger(DBWizardAction.class);
-	
-    public DBWizardAction(String name, Application application) {
-        super(name, application);
-    }
-
-    protected DataSourceWizard dataSourceWizardDialog(String title) {
-        // connect
-        DataSourceWizard connectWizard = new DataSourceWizard(getProjectController(), title);
-        if (!connectWizard.startupAction()) {
-            return null;
-        }
-
-        return connectWizard;
-    }
-
-    protected abstract T createDialog(Collection<String> catalogs, Collection<String> schemas, String currentCatalog, String currentSchema, int command);
-
-    protected T loaderOptionDialog(DataSourceWizard connectWizard) {
-
-        // use this catalog as the default...
-        List<String> catalogs;
-        List<String> schemas;
-        String currentCatalog;
-        String currentSchema = null;
-        try(Connection connection = connectWizard.getDataSource().getConnection()) {
-            catalogs = getCatalogs(connectWizard, connection);
-            schemas = getSchemas(connection);
-            if (catalogs.isEmpty() && schemas.isEmpty()) {
-                return null;
-            }
-            currentCatalog = connection.getCatalog();
-			
-			try {
-	            currentSchema = connection.getSchema();
-			} catch (Throwable th) {
-                LOGGER.warn("Error getting schema.", th);
-			}
-        } catch (Exception ex) {
-            JOptionPane.showMessageDialog(
-                    Application.getFrame(),
-                    ex.getMessage(),
-                    "Error loading schemas dialog",
-                    JOptionPane.ERROR_MESSAGE);
-            return null;
-        }
-        T optionsDialog = getStartDialog(catalogs, schemas, currentCatalog, currentSchema);
-        optionsDialog.setVisible(true);
-        while ((optionsDialog.getChoice() != DbActionOptionsDialog.CANCEL)) {
-            if (optionsDialog.getChoice() == DbActionOptionsDialog.SELECT) {
-                return optionsDialog;
-            }
-            optionsDialog = createDialog(catalogs, schemas, currentCatalog, currentSchema, optionsDialog.getChoice());
-            optionsDialog.setVisible(true);
-        }
-
-        return null;
-    }
-
-    private T getStartDialog(List<String> catalogs, List<String> schemas, String currentCatalog, String currentSchema) {
-        int command = DbActionOptionsDialog.SELECT;
-        return createDialog(catalogs, schemas, currentCatalog, currentSchema, command);
-    }
-
-    @SuppressWarnings("unchecked")
-    private List<String> getCatalogs(DataSourceWizard connectWizard, Connection connection) throws Exception {
-        if(!connectWizard.getAdapter().supportsCatalogsOnReverseEngineering()) {
-            return (List<String>) Collections.EMPTY_LIST;
-        }
-
-        return DbLoader.loadCatalogs(connection);
-    }
-
-    private List<String> getSchemas(Connection connection) throws Exception {
-        return DbLoader.loadSchemas(connection);
-    }
-}
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DocumentationAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DocumentationAction.java
index 5403d9c..7c01a3e 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DocumentationAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/DocumentationAction.java
@@ -24,10 +24,11 @@
 import org.apache.cayenne.modeler.Application;
 import org.apache.cayenne.modeler.util.BrowserControl;
 import org.apache.cayenne.modeler.util.CayenneAction;
+import org.apache.cayenne.util.LocalizedStringsHandler;
 
 public class DocumentationAction extends CayenneAction {
 
-    public final static String getActionName() {
+    public static String getActionName() {
         return "Documentation";
     }
 
@@ -35,7 +36,14 @@
         super(getActionName(), application);
     }
 
+    @Override
     public void performAction(ActionEvent e) {
-        BrowserControl.displayURL("http://cayenne.apache.org");
+        String version = LocalizedStringsHandler.getString("cayenne.version");
+        String url = "https://cayenne.apache.org";
+        if(!"".equals(version)) {
+            String majorVersion = version.substring(0, version.lastIndexOf('.'));
+            url = url + "/docs/" + majorVersion + "/cayenne-guide/";
+        }
+        BrowserControl.displayURL(url);
     }
 }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/FindAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/FindAction.java
index d3518bc..1185fc5 100755
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/FindAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/FindAction.java
@@ -25,7 +25,7 @@
 import org.apache.cayenne.map.DbAttribute;
 import org.apache.cayenne.map.DbEntity;
 import org.apache.cayenne.map.DbRelationship;
-import org.apache.cayenne.map.DetectedDbEntity;
+import org.apache.cayenne.dbsync.model.DetectedDbEntity;
 import org.apache.cayenne.map.EJBQLQueryDescriptor;
 import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.EmbeddableAttribute;
@@ -55,7 +55,6 @@
 import org.apache.cayenne.modeler.event.RelationshipDisplayEvent;
 import org.apache.cayenne.modeler.util.CayenneAction;
 import org.apache.cayenne.map.QueryDescriptor;
-import org.apache.cayenne.query.SQLTemplate;
 
 import javax.swing.JTextField;
 import javax.swing.tree.DefaultMutableTreeNode;
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/GetDbConnectionAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/GetDbConnectionAction.java
index 2e83a27..d870fea 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/GetDbConnectionAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/GetDbConnectionAction.java
@@ -20,23 +20,14 @@
 package org.apache.cayenne.modeler.action;
 
 import java.awt.event.ActionEvent;
-import java.util.Collection;
 
 import org.apache.cayenne.modeler.Application;
 import org.apache.cayenne.modeler.dialog.db.DataSourceWizard;
-import org.apache.cayenne.modeler.dialog.db.DbActionOptionsDialog;
-import org.apache.cayenne.modeler.pref.DataMapDefaults;
-
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.DB_ADAPTER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.JDBC_DRIVER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.PASSWORD_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.URL_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.USER_NAME_PROPERTY;
 
 /**
  * @since 4.1
  */
-public class GetDbConnectionAction extends DBWizardAction<DbActionOptionsDialog> {
+public class GetDbConnectionAction extends DBConnectionAwareAction {
 
     public static final String DIALOG_TITLE = "Configure Connection to Database";
     private static final String ACTION_NAME = "Configure Connection";
@@ -51,30 +42,11 @@
     }
 
     @Override
-    protected DbActionOptionsDialog createDialog(final Collection<String> catalogs, final Collection<String> schemas,
-                                                 final String currentCatalog, final String currentSchema, final int command) {
-        // NOOP
-        return null;
-    }
-
-    @Override
     public void performAction(final ActionEvent e) {
-        final DataSourceWizard connectWizard = dataSourceWizardDialog(DIALOG_TITLE);
+        DataSourceWizard connectWizard = getDataSourceWizard(DIALOG_TITLE, new String[]{"Continue", "Cancel"});
         if (connectWizard == null) {
             return;
         }
-
-        final DataMapDefaults dataMapDefaults = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap());
-
-        if (connectWizard.getConnectionInfo().getDbAdapter() != null) {
-            dataMapDefaults.getCurrentPreference().put(DB_ADAPTER_PROPERTY, connectWizard.getConnectionInfo().getDbAdapter());
-        } else {
-            dataMapDefaults.getCurrentPreference().remove(DB_ADAPTER_PROPERTY);
-        }
-        dataMapDefaults.getCurrentPreference().put(URL_PROPERTY, connectWizard.getConnectionInfo().getUrl());
-        dataMapDefaults.getCurrentPreference().put(USER_NAME_PROPERTY, connectWizard.getConnectionInfo().getUserName());
-        dataMapDefaults.getCurrentPreference().put(PASSWORD_PROPERTY, connectWizard.getConnectionInfo().getPassword());
-        dataMapDefaults.getCurrentPreference().put(JDBC_DRIVER_PROPERTY, connectWizard.getConnectionInfo().getJdbcDriver());
+        saveConnectionInfo(connectWizard);
     }
 }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/LoadDbSchemaAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/LoadDbSchemaAction.java
index 1d3975c..8d46749 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/LoadDbSchemaAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/LoadDbSchemaAction.java
@@ -23,14 +23,12 @@
 import javax.swing.tree.TreePath;
 import java.awt.event.ActionEvent;
 import java.sql.SQLException;
-import java.util.prefs.Preferences;
 
 import org.apache.cayenne.dbsync.reverse.dbimport.Catalog;
 import org.apache.cayenne.dbsync.reverse.dbimport.IncludeTable;
 import org.apache.cayenne.dbsync.reverse.dbimport.ReverseEngineering;
 import org.apache.cayenne.dbsync.reverse.dbimport.Schema;
 import org.apache.cayenne.modeler.Application;
-import org.apache.cayenne.modeler.dialog.db.DataSourceWizard;
 import org.apache.cayenne.modeler.dialog.db.load.DbImportTreeNode;
 import org.apache.cayenne.modeler.editor.dbimport.DatabaseSchemaLoader;
 import org.apache.cayenne.modeler.editor.dbimport.DbImportModel;
@@ -39,21 +37,13 @@
 import org.apache.cayenne.modeler.editor.dbimport.PrintColumnsBiFunction;
 import org.apache.cayenne.modeler.editor.dbimport.PrintTablesBiFunction;
 import org.apache.cayenne.modeler.pref.DBConnectionInfo;
-import org.apache.cayenne.modeler.pref.DataMapDefaults;
-import org.apache.cayenne.modeler.util.CayenneAction;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.DB_ADAPTER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.JDBC_DRIVER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.PASSWORD_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.URL_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.USER_NAME_PROPERTY;
-
 /**
  * @since 4.1
  */
-public class LoadDbSchemaAction extends CayenneAction {
+public class LoadDbSchemaAction extends DBConnectionAwareAction {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(LoadDbSchemaAction.class);
 
@@ -84,16 +74,10 @@
             draggableTreePanel.getMoveButton().setEnabled(false);
             draggableTreePanel.getMoveInvertButton().setEnabled(false);
             try {
-                DBConnectionInfo connectionInfo;
-                if (datamapPrefNotExist()) {
-                    final DataSourceWizard connectWizard = new DataSourceWizard(getProjectController(), "Load Db Schema");
-                    if (!connectWizard.startupAction()) {
-                        return;
-                    }
-                    connectionInfo = connectWizard.getConnectionInfo();
-                    saveConnectionInfo(connectWizard);
-                } else {
-                    connectionInfo = getConnectionInfoFromPreferences();
+
+                DBConnectionInfo connectionInfo = getConnectionInfo("Load Db Schema");
+                if(connectionInfo == null) {
+                    return;
                 }
 
                 if (tablePath != null) {
@@ -161,40 +145,6 @@
                         new PrintColumnsBiFunction(draggableTreePanel.getSourceTree()));
     }
 
-    private boolean datamapPrefNotExist() {
-        Preferences dataMapPreference = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap())
-                .getCurrentPreference();
-        return dataMapPreference == null || dataMapPreference.get(URL_PROPERTY, null) == null;
-    }
-
-    private DBConnectionInfo getConnectionInfoFromPreferences() {
-        DBConnectionInfo connectionInfo = new DBConnectionInfo();
-        DataMapDefaults dataMapDefaults = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap());
-        connectionInfo.setDbAdapter(dataMapDefaults.getCurrentPreference().get(DB_ADAPTER_PROPERTY, null));
-        connectionInfo.setUrl(dataMapDefaults.getCurrentPreference().get(URL_PROPERTY, null));
-        connectionInfo.setUserName(dataMapDefaults.getCurrentPreference().get(USER_NAME_PROPERTY, null));
-        connectionInfo.setPassword(dataMapDefaults.getCurrentPreference().get(PASSWORD_PROPERTY, null));
-        connectionInfo.setJdbcDriver(dataMapDefaults.getCurrentPreference().get(JDBC_DRIVER_PROPERTY, null));
-        return connectionInfo;
-    }
-
-    private void saveConnectionInfo(DataSourceWizard connectWizard) {
-        DataMapDefaults dataMapDefaults = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap());
-        String dbAdapter = connectWizard.getConnectionInfo().getDbAdapter();
-        if(dbAdapter != null) {
-            dataMapDefaults.getCurrentPreference().put(DB_ADAPTER_PROPERTY, connectWizard.getConnectionInfo().getDbAdapter());
-        } else {
-            dataMapDefaults.getCurrentPreference().remove(DB_ADAPTER_PROPERTY);
-        }
-        dataMapDefaults.getCurrentPreference().put(URL_PROPERTY, connectWizard.getConnectionInfo().getUrl());
-        dataMapDefaults.getCurrentPreference().put(USER_NAME_PROPERTY, connectWizard.getConnectionInfo().getUserName());
-        dataMapDefaults.getCurrentPreference().put(PASSWORD_PROPERTY, connectWizard.getConnectionInfo().getPassword());
-        dataMapDefaults.getCurrentPreference().put(JDBC_DRIVER_PROPERTY, connectWizard.getConnectionInfo().getJdbcDriver());
-    }
-
     public void setDraggableTreePanel(DraggableTreePanel draggableTreePanel) {
         this.draggableTreePanel = draggableTreePanel;
     }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/MigrateAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/MigrateAction.java
index 5c41b31..61fcd29 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/MigrateAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/MigrateAction.java
@@ -20,19 +20,28 @@
 package org.apache.cayenne.modeler.action;
 
 import org.apache.cayenne.dbsync.merge.factory.MergerTokenFactoryProvider;
+import org.apache.cayenne.dbsync.reverse.dbload.DbLoader;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.modeler.Application;
 import org.apache.cayenne.modeler.dialog.db.DataSourceWizard;
 import org.apache.cayenne.modeler.dialog.db.DbActionOptionsDialog;
 import org.apache.cayenne.modeler.dialog.db.merge.MergerOptions;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.awt.event.ActionEvent;
+import java.sql.Connection;
 import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import javax.swing.JOptionPane;
 
 /**
  * Action that alter database schema to match a DataMap.
  */
-public class MigrateAction extends DBWizardAction<DbActionOptionsDialog> {
+public class MigrateAction extends DBConnectionAwareAction {
+
+    private static Logger LOGGER = LoggerFactory.getLogger(MigrateAction.class);
 
     private boolean dialogShown;
 
@@ -46,7 +55,7 @@
 
     public void performAction(ActionEvent e) {
 
-        DataSourceWizard connectWizard = dataSourceWizardDialog("Migrate DB Schema: Connect to Database");
+        DataSourceWizard connectWizard = getDataSourceWizard("Migrate DB Schema: Connect to Database");
         if(connectWizard == null) {
             return;
         }
@@ -76,16 +85,72 @@
                 map, selectedCatalog, selectedSchema, mergerTokenFactoryProvider).startupAction();
     }
 
-    @Override
     protected DbActionOptionsDialog createDialog(Collection<String> catalogs, Collection<String> schemas,
                                                  String currentCatalog, String currentSchema, int command) {
         dialogShown = true;
-        switch (command) {
-            case DbActionOptionsDialog.SELECT:
-                return new DbActionOptionsDialog(Application.getFrame(), "Migrate DB Schema: Select Catalog and Schema",
+        if (command == DbActionOptionsDialog.SELECT) {
+            return new DbActionOptionsDialog(Application.getFrame(), "Migrate DB Schema: Select Catalog and Schema",
                     catalogs, schemas, currentCatalog, currentSchema);
-            default:
-                return null;
         }
+        return null;
+    }
+
+    protected DbActionOptionsDialog loaderOptionDialog(DataSourceWizard connectWizard) {
+
+        // use this catalog as the default...
+        List<String> catalogs;
+        List<String> schemas;
+        String currentCatalog;
+        String currentSchema = null;
+        try(Connection connection = connectWizard.getDataSource().getConnection()) {
+            catalogs = getCatalogs(connectWizard, connection);
+            schemas = getSchemas(connection);
+            if (catalogs.isEmpty() && schemas.isEmpty()) {
+                return null;
+            }
+            currentCatalog = connection.getCatalog();
+
+            try {
+                currentSchema = connection.getSchema();
+            } catch (Throwable th) {
+                LOGGER.warn("Error getting schema.", th);
+            }
+        } catch (Exception ex) {
+            JOptionPane.showMessageDialog(
+                    Application.getFrame(),
+                    ex.getMessage(),
+                    "Error loading schemas dialog",
+                    JOptionPane.ERROR_MESSAGE);
+            return null;
+        }
+        DbActionOptionsDialog optionsDialog = getStartDialog(catalogs, schemas, currentCatalog, currentSchema);
+        optionsDialog.setVisible(true);
+        while ((optionsDialog.getChoice() != DbActionOptionsDialog.CANCEL)) {
+            if (optionsDialog.getChoice() == DbActionOptionsDialog.SELECT) {
+                return optionsDialog;
+            }
+            optionsDialog = createDialog(catalogs, schemas, currentCatalog, currentSchema, optionsDialog.getChoice());
+            optionsDialog.setVisible(true);
+        }
+
+        return null;
+    }
+
+    private DbActionOptionsDialog getStartDialog(List<String> catalogs, List<String> schemas, String currentCatalog, String currentSchema) {
+        int command = DbActionOptionsDialog.SELECT;
+        return createDialog(catalogs, schemas, currentCatalog, currentSchema, command);
+    }
+
+    @SuppressWarnings("unchecked")
+    private List<String> getCatalogs(DataSourceWizard connectWizard, Connection connection) throws Exception {
+        if(!connectWizard.getAdapter().supportsCatalogsOnReverseEngineering()) {
+            return (List<String>) Collections.EMPTY_LIST;
+        }
+
+        return DbLoader.loadCatalogs(connection);
+    }
+
+    private List<String> getSchemas(Connection connection) throws Exception {
+        return DbLoader.loadSchemas(connection);
     }
 }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ReverseEngineeringAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ReverseEngineeringAction.java
index 400d2e6..7930d39 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ReverseEngineeringAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/ReverseEngineeringAction.java
@@ -23,35 +23,24 @@
 import javax.swing.SwingUtilities;
 import java.awt.event.ActionEvent;
 import java.sql.SQLException;
-import java.util.Collection;
 import java.util.HashSet;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
-import java.util.prefs.Preferences;
 
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.modeler.Application;
 import org.apache.cayenne.modeler.ProjectController;
-import org.apache.cayenne.modeler.dialog.db.DataSourceWizard;
-import org.apache.cayenne.modeler.dialog.db.DbActionOptionsDialog;
 import org.apache.cayenne.modeler.dialog.db.load.DbLoadResultDialog;
 import org.apache.cayenne.modeler.dialog.db.load.DbLoaderContext;
 import org.apache.cayenne.modeler.dialog.db.load.LoadDataMapTask;
 import org.apache.cayenne.modeler.editor.DbImportController;
 import org.apache.cayenne.modeler.editor.dbimport.DbImportView;
 import org.apache.cayenne.modeler.pref.DBConnectionInfo;
-import org.apache.cayenne.modeler.pref.DataMapDefaults;
-
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.DB_ADAPTER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.JDBC_DRIVER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.PASSWORD_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.URL_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.USER_NAME_PROPERTY;
 
 /**
  * Action that imports database structure into a DataMap.
  */
-public class ReverseEngineeringAction extends DBWizardAction<DbActionOptionsDialog> {
+public class ReverseEngineeringAction extends DBConnectionAwareAction {
 
     private static final String ACTION_NAME = "Reengineer Database Schema";
     private static final String ICON_NAME = "icon-dbi-runImport.png";
@@ -86,17 +75,12 @@
 
     private void startImport(){
         final DbLoaderContext context = new DbLoaderContext(application.getMetaData());
-        DBConnectionInfo connectionInfo;
-        if (datamapPrefNotExist()) {
-            final DataSourceWizard connectWizard = dataSourceWizardDialog(DIALOG_TITLE);
-            if (connectWizard == null) {
-                return;
-            }
-            connectionInfo = connectWizard.getConnectionInfo();
-            saveConnectionInfo(connectWizard);
-        } else {
-            connectionInfo = getConnectionInfoFromPreferences();
+
+        DBConnectionInfo connectionInfo = getConnectionInfo(DIALOG_TITLE);
+        if(connectionInfo == null) {
+            return;
         }
+
         context.setProjectController(getProjectController());
         try {
             context.setConnection(connectionInfo.makeDataSource(getApplication().getClassLoadingService()).getConnection());
@@ -148,40 +132,6 @@
         this.dataMaps = new HashSet<>();
     }
 
-    private DBConnectionInfo getConnectionInfoFromPreferences() {
-        DBConnectionInfo connectionInfo = new DBConnectionInfo();
-        DataMapDefaults dataMapDefaults = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap());
-        connectionInfo.setDbAdapter(dataMapDefaults.getCurrentPreference().get(DB_ADAPTER_PROPERTY, null));
-        connectionInfo.setUrl(dataMapDefaults.getCurrentPreference().get(URL_PROPERTY, null));
-        connectionInfo.setUserName(dataMapDefaults.getCurrentPreference().get(USER_NAME_PROPERTY, null));
-        connectionInfo.setPassword(dataMapDefaults.getCurrentPreference().get(PASSWORD_PROPERTY, null));
-        connectionInfo.setJdbcDriver(dataMapDefaults.getCurrentPreference().get(JDBC_DRIVER_PROPERTY, null));
-        return connectionInfo;
-    }
-
-    private void saveConnectionInfo(DataSourceWizard connectWizard) {
-        DataMapDefaults dataMapDefaults = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap());
-        String dbAdapter = connectWizard.getConnectionInfo().getDbAdapter();
-        if(dbAdapter != null) {
-            dataMapDefaults.getCurrentPreference().put(DB_ADAPTER_PROPERTY, dbAdapter);
-        } else {
-            dataMapDefaults.getCurrentPreference().remove(DB_ADAPTER_PROPERTY);
-        }
-        dataMapDefaults.getCurrentPreference().put(URL_PROPERTY, connectWizard.getConnectionInfo().getUrl());
-        dataMapDefaults.getCurrentPreference().put(USER_NAME_PROPERTY, connectWizard.getConnectionInfo().getUserName());
-        dataMapDefaults.getCurrentPreference().put(PASSWORD_PROPERTY, connectWizard.getConnectionInfo().getPassword());
-        dataMapDefaults.getCurrentPreference().put(JDBC_DRIVER_PROPERTY, connectWizard.getConnectionInfo().getJdbcDriver());
-    }
-
-    private boolean datamapPrefNotExist() {
-        Preferences dataMapPreference = getProjectController().
-                getDataMapPreferences(getProjectController().getCurrentDataMap())
-                .getCurrentPreference();
-        return dataMapPreference == null || dataMapPreference.get(URL_PROPERTY, null) == null;
-    }
-
     private void runLoaderInThread(final DbLoaderContext context, final Runnable callback) {
         Thread th = new Thread(() -> {
             LoadDataMapTask task = new LoadDataMapTask(Application.getFrame(), "Reengineering DB", context);
@@ -191,13 +141,6 @@
         th.start();
     }
 
-    @Override
-    protected DbActionOptionsDialog createDialog(Collection<String> catalogs, Collection<String> schemas,
-                                                 String currentCatalog, String currentSchema, int command) {
-        // NOOP
-        return null;
-    }
-
     public void setView(DbImportView view) {
         this.view = view;
     }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/dbimport/TreeManipulationAction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/dbimport/TreeManipulationAction.java
index 5b6c7d0..fbbc478 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/dbimport/TreeManipulationAction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/action/dbimport/TreeManipulationAction.java
@@ -34,6 +34,8 @@
 import org.apache.cayenne.modeler.editor.dbimport.DbImportTree;
 import org.apache.cayenne.modeler.undo.DbImportTreeUndoableEdit;
 import org.apache.cayenne.modeler.util.CayenneAction;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import javax.swing.JTree;
 import javax.swing.tree.TreePath;
@@ -49,6 +51,8 @@
  */
 public abstract class TreeManipulationAction extends CayenneAction {
 
+    private static final Logger LOGGER = LoggerFactory.getLogger(TreeManipulationAction.class);
+
     static final String EMPTY_NAME = "";
 
     protected DbImportTree tree;
@@ -154,7 +158,12 @@
             return false;
         }
         Class<?> selectedObjectClass = node.getUserObject().getClass();
-        return levels.get(selectedObjectClass).contains(insertableNodeClass);
+        List<Class<?>> classes = levels.get(selectedObjectClass);
+        if(classes == null) {
+            LOGGER.warn("Trying to insert node of the unknown class '" + selectedObjectClass.getName() + "' to the dbimport tree.");
+            return false;
+        }
+        return classes.contains(insertableNodeClass);
     }
 
     boolean canInsert() {
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/AboutDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/AboutDialog.java
index a811a74..097a2a0 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/AboutDialog.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/AboutDialog.java
@@ -30,6 +30,7 @@
 import java.awt.event.KeyListener;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseListener;
+import java.time.LocalDate;
 
 import javax.swing.ImageIcon;
 import javax.swing.JFrame;
@@ -49,7 +50,6 @@
 // triad, though it might be beneficial to use strings file
 public class AboutDialog extends JFrame implements FocusListener, KeyListener, MouseListener {
 
-    private JLabel license, info;
     private static String infoString;
     private static ImageIcon logoImage;
 
@@ -70,20 +70,18 @@
             double totalMemory = (double) Runtime.getRuntime().totalMemory() / 1024 / 1024;
             double freeMemory = (double) Runtime.getRuntime().freeMemory() / 1024 / 1024;
 
-            StringBuffer buffer = new StringBuffer();
+            StringBuilder buffer = new StringBuilder();
             buffer.append("<html>");
             buffer.append("<font size='-1' face='Arial,Helvetica'>");
-            buffer.append(ModelerUtil.getProperty("cayenne.modeler.about.info"));
+            buffer.append(String.format(ModelerUtil.getProperty("cayenne.modeler.about.info"), LocalDate.now().getYear()));
             buffer.append("</font>");
 
             buffer.append("<font size='-2' face='Arial,Helvetica'>");
-            buffer.append("<br>JVM: " + System.getProperty("java.vm.name") + " " + System.getProperty("java.version"));
+            buffer.append("<br>JVM: ").append(System.getProperty("java.vm.name")).append(" ").append(System.getProperty("java.version"));
             buffer.append(String.format("<br>Memory: used %.2f MB, max %.2f MB", totalMemory - freeMemory, maxMemory));
 
             String version = LocalizedStringsHandler.getString("cayenne.version");
-            if (version != null) {
-                buffer.append("<br>Version: ").append(version);
-            }
+            buffer.append("<br>Version: ").append(version);
 
             String buildDate = LocalizedStringsHandler.getString("cayenne.build.date");
             if (!Util.isEmptyString(buildDate)) {
@@ -119,7 +117,7 @@
         JLabel image = new JLabel(getLogoImage());
         panel.add(image, new GridBagConstraints());
 
-        license = new JLabel();
+        JLabel license = new JLabel();
         final GridBagConstraints gridBagConstraints_1 = new GridBagConstraints();
         gridBagConstraints_1.fill = GridBagConstraints.HORIZONTAL;
         gridBagConstraints_1.anchor = GridBagConstraints.NORTHWEST;
@@ -129,7 +127,7 @@
         panel.add(license, gridBagConstraints_1);
         license.setText("<html><font size='-1' face='Arial,Helvetica'>Available under the Apache license.</font></html>");
 
-        info = new JLabel();
+        JLabel info = new JLabel();
         final GridBagConstraints gridBagConstraints_2 = new GridBagConstraints();
         gridBagConstraints_2.fill = GridBagConstraints.HORIZONTAL;
         gridBagConstraints_2.anchor = GridBagConstraints.NORTHWEST;
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/DataSourceWizard.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/DataSourceWizard.java
index 4070b5a..9ad45a4 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/DataSourceWizard.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/DataSourceWizard.java
@@ -33,7 +33,6 @@
 import org.apache.cayenne.dba.DbAdapter;
 import org.apache.cayenne.modeler.ClassLoadingService;
 import org.apache.cayenne.modeler.ProjectController;
-import org.apache.cayenne.modeler.action.GetDbConnectionAction;
 import org.apache.cayenne.modeler.dialog.pref.GeneralPreferences;
 import org.apache.cayenne.modeler.dialog.pref.PreferenceDialog;
 import org.apache.cayenne.modeler.event.DataSourceModificationEvent;
@@ -44,11 +43,7 @@
 import org.apache.cayenne.swing.BindingBuilder;
 import org.apache.cayenne.swing.ObjectBinding;
 
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.DB_ADAPTER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.JDBC_DRIVER_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.PASSWORD_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.URL_PROPERTY;
-import static org.apache.cayenne.modeler.pref.DBConnectionInfo.USER_NAME_PROPERTY;
+import static org.apache.cayenne.modeler.pref.DBConnectionInfo.*;
 
 /**
  * A subclass of ConnectionWizard that tests configured DataSource, but does not
@@ -57,52 +52,43 @@
  */
 public class DataSourceWizard extends CayenneController {
 
-	private DataSourceWizardView view;
+	private final ProjectController projectController;
+	private final DataSourceWizardView view;
+	private final String[] buttons;
+
 	private ObjectBinding dataSourceBinding;
 	private Map<String, DBConnectionInfo> dataSources;
 	private String dataSourceKey;
-	private ProjectController projectController;
-
-	// this object is a clone of an object selected from the dropdown, as we
-	// need to allow
-	// local temporary modifications
+	// this object is a clone of an object selected from the dropdown, as we need to allow local temporary modifications
 	private DBConnectionInfo connectionInfo;
-
-	private boolean canceled;
-
-	private DataSourceModificationListener dataSourceListener;
-
 	private DbAdapter adapter;
 	private DataSource dataSource;
+	private boolean canceled;
+	private DataSourceModificationListener dataSourceListener;
 
-	public DataSourceWizard(final CayenneController parent, final String title) {
+	public DataSourceWizard(ProjectController parent, String title) {
+		this(parent, title, new String[]{"Continue", "Cancel"});
+	}
+
+	public DataSourceWizard(ProjectController parent, String title, String[] buttons) {
 		super(parent);
 
-		this.view = createView(title);
-		this.view.setTitle(title);
+		this.buttons = buttons;
 		this.connectionInfo = new DBConnectionInfo();
-		this.projectController = (ProjectController) parent;
+		this.projectController = parent;
+
+		this.view = createView();
+		this.view.setTitle(title);
 
 		initBindings();
 		initDataSourceListener();
 	}
 
-	private String[] getLabelsForDialog(final String viewTitle) {
-		switch (viewTitle) {
-			case GetDbConnectionAction.DIALOG_TITLE: {
-				return new String[]{"Save", "Cancel"};
-			}
-			default:
-				return new String[]{"Continue", "Cancel"};
-		}
-	}
-
 	/**
 	 * Creates swing dialog for this wizard
 	 */
-	private DataSourceWizardView createView(final String viewTitle) {
-		final String[] labels = getLabelsForDialog(viewTitle);
-		return new DataSourceWizardView(this, labels);
+	private DataSourceWizardView createView() {
+		return new DataSourceWizardView(this, buttons);
 	}
 
 	protected void initBindings() {
@@ -145,9 +131,9 @@
 	}
 
 	private DBConnectionInfo getConnectionInfoFromPreferences() {
-		final DBConnectionInfo connectionInfo = new DBConnectionInfo();
-		final DataMapDefaults dataMapDefaults = projectController.
-				getDataMapPreferences(projectController.getCurrentDataMap());
+		DBConnectionInfo connectionInfo = new DBConnectionInfo();
+		DataMapDefaults dataMapDefaults = getProjectController()
+				.getDataMapPreferences(getProjectController().getCurrentDataMap());
 		connectionInfo.setDbAdapter(dataMapDefaults.getCurrentPreference().get(DB_ADAPTER_PROPERTY, null));
 		connectionInfo.setUrl(dataMapDefaults.getCurrentPreference().get(URL_PROPERTY, null));
 		connectionInfo.setUserName(dataMapDefaults.getCurrentPreference().get(USER_NAME_PROPERTY, null));
@@ -156,6 +142,10 @@
 		return connectionInfo;
 	}
 
+	private ProjectController getProjectController() {
+		return projectController;
+	}
+
 	public String getDataSourceKey() {
 		return dataSourceKey;
 	}
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/gen/DBGeneratorOptions.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/gen/DBGeneratorOptions.java
index 4d05655..0261d23 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/gen/DBGeneratorOptions.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/db/gen/DBGeneratorOptions.java
@@ -249,8 +249,7 @@
      */
     public void generateSchemaAction() {
 
-        DataSourceWizard connectWizard = new DataSourceWizard(
-                this.getParent(),
+        DataSourceWizard connectWizard = new DataSourceWizard((ProjectController) this.getParent(),
                 "Generate DB Schema: Connect to Database");
         if (!connectWizard.startupAction()) {
             return;
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialog.java
index 060c923..e8e027e 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialog.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialog.java
@@ -155,6 +155,7 @@
 		view.getSourceEntityLabel().setText(attribute.getEntity().getName());
 		view.getTypeComboBox().setSelectedItem(attribute.getType());
 		view.getUsedForLockingCheckBox().setSelected(attribute.isUsedForLocking());
+		view.getLazyCheckBox().setSelected(attribute.isLazy());
 		view.getCommentField().setText(ObjectInfo
 				.getFromMetaData(mediator.getApplication().getMetaData(),
 						attr,
@@ -375,6 +376,7 @@
 			}
 			attributeSaved.setName(view.getAttributeName().getText());
 			attributeSaved.setUsedForLocking(view.getUsedForLockingCheckBox().isSelected());
+			attributeSaved.setLazy(view.getLazyCheckBox().isSelected());
 			ObjectInfo.putToMetaData(mediator.getApplication().getMetaData(),
 					attributeSaved,
 					ObjectInfo.COMMENT,
@@ -451,6 +453,7 @@
 				|| (attribute.getType() == null && view.getTypeComboBox().getSelectedItem() != null)
 				|| !Objects.equals(attribute.getType(), view.getTypeComboBox().getSelectedItem())
 				|| attribute.isUsedForLocking() != view.getUsedForLockingCheckBox().isSelected()
+				|| attribute.isLazy() != view.getLazyCheckBox().isSelected()
 				|| !ObjectInfo.getFromMetaData(
 						mediator.getApplication().getMetaData(), attribute, ObjectInfo.COMMENT)
 				.equals(view.getCommentField().getText());
@@ -460,10 +463,11 @@
 		model.setUpdatedValueAt(attributeSaved.getName(), row, 0);
 		model.setUpdatedValueAt(attributeSaved.getType(), row, 1);
 		model.setUpdatedValueAt(attributeSaved.isUsedForLocking(), row, 4);
+		model.setUpdatedValueAt(attributeSaved.isLazy(), row, 5);
 		model.setUpdatedValueAt(ObjectInfo
 				.getFromMetaData(mediator.getApplication().getMetaData(),
 						attributeSaved,
-						ObjectInfo.COMMENT), row, 5);
+						ObjectInfo.COMMENT), row, 6);
 	}
 
 	public void saveMapping() {
@@ -649,6 +653,7 @@
 		attributeSaved.setParent(attribute.getParent());
 		attributeSaved.setType(attribute.getType());
 		attributeSaved.setUsedForLocking(attribute.isUsedForLocking());
+		attributeSaved.setLazy(attribute.isLazy());
 		String comment = ObjectInfo
 				.getFromMetaData(mediator.getApplication().getMetaData(),
 						attribute,
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialogView.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialogView.java
index 78b4bb3..0678047 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialogView.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/objentity/ObjAttributeInfoDialogView.java
@@ -70,6 +70,7 @@
     private TableColumnPreferences tablePreferences;
 
     private JCheckBox usedForLockingCheckBox;
+    private JCheckBox lazyCheckBox;
     private JTextField commentField;
 
     private static final Dimension BROWSER_CELL_DIM = new Dimension(130, 200);
@@ -89,6 +90,7 @@
         typeComboBox.getRenderer();
 
         this.usedForLockingCheckBox = new JCheckBox();
+        this.lazyCheckBox = new JCheckBox();
         this.commentField = new JTextField();
 
         overrideAttributeTable = new CayenneTable();
@@ -125,8 +127,11 @@
         builder.addLabel("Used for locking:", cc.xy(1, 11));
         builder.add(usedForLockingCheckBox, cc.xywh(3, 11, 1, 1));
 
-        builder.addLabel("Comment:", cc.xy(1, 13));
-        builder.add(commentField, cc.xywh(3, 13, 1, 1));
+        builder.addLabel("Lazy loading:", cc.xy(1, 13));
+        builder.add(lazyCheckBox, cc.xywh(3, 13, 1, 1));
+
+        builder.addLabel("Comment:", cc.xy(1, 15));
+        builder.add(commentField, cc.xywh(3, 15, 1, 1));
 
         builder.addSeparator("Mapping to DbAttributes", cc.xywh(1, 15, 7, 1));
 
@@ -242,6 +247,10 @@
         return usedForLockingCheckBox;
     }
 
+    public JCheckBox getLazyCheckBox() {
+        return lazyCheckBox;
+    }
+
     public JTextField getCommentField() {
         return commentField;
     }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferences.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferences.java
index e3cf205..052cdf1 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferences.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferences.java
@@ -43,12 +43,13 @@
 
     private static final Logger logger = LoggerFactory.getLogger(ClasspathPreferences.class);
 
-    protected ClasspathPreferencesView view;
-    protected List<String> classPathEntries;
-    protected ClasspathTableModel tableModel;
-    protected CayennePreferenceEditor editor;
-    protected List<String> classPathKeys;
-    private Preferences preferences;
+    private final ClasspathPreferencesView view;
+    private final List<String> classPathEntries;
+    private final List<String> classPathKeys;
+    private final ClasspathTableModel tableModel;
+    private final CayennePreferenceEditor editor;
+    private final Preferences preferences;
+
     private int counter;
 
     public ClasspathPreferences(PreferenceDialog parentController) {
@@ -56,29 +57,24 @@
 
         this.view = new ClasspathPreferencesView();
 
+        PreferenceEditor editor = parentController.getEditor();
+        this.editor = editor instanceof CayennePreferenceEditor
+                ? (CayennePreferenceEditor) editor
+                : null;
+
         // this prefs node is shared with other dialog panels... be aware of
         // that when accessing the keys
         this.preferences = getApplication().getPreferencesNode(this.getClass(), "");
 
-        PreferenceEditor editor = parentController.getEditor();
-        if (editor instanceof CayennePreferenceEditor) {
-            this.editor = (CayennePreferenceEditor) editor;
-        }
-
-        List<String> classPathEntries = new ArrayList<String>();
-        List<String> classPathKeys = new ArrayList<String>();
-
-        this.counter = loadPreferences(classPathEntries, classPathKeys);
-
-        this.classPathEntries = classPathEntries;
-        this.classPathKeys = classPathKeys;
-
-        this.tableModel = new ClasspathTableModel();
+        this.classPathEntries = new ArrayList<>();
+        this.classPathKeys = new ArrayList<>();
+        this.counter = loadPreferences();
+        this.tableModel = new ClasspathTableModel(classPathEntries);
 
         initBindings();
     }
 
-    private int loadPreferences(List<String> classPathEntries, List<String> classPathKeys) {
+    private synchronized int loadPreferences() {
 
         String[] cpKeys;
         try {
@@ -89,13 +85,12 @@
         }
 
         int max = 0;
-
         for (String cpKey : cpKeys) {
-
-            int c;
-
             try {
-                c = Integer.parseInt(cpKey);
+                int c = Integer.parseInt(cpKey);
+                if (c > max) {
+                    max = c;
+                }
             } catch (NumberFormatException e) {
                 // we are sharing the 'preferences' node with other dialogs, and
                 // this is a rather poor way of telling our preference keys from
@@ -106,10 +101,6 @@
                 continue;
             }
 
-            if (c > max) {
-                max = c;
-            }
-
             String tempValue = preferences.get(cpKey, "");
             if (!"".equals(tempValue)) {
                 classPathEntries.add(tempValue);
@@ -127,8 +118,9 @@
     protected void initBindings() {
         view.getTable().setModel(tableModel);
         view.getAddDirButton().addActionListener(e -> addClassDirectoryAction());
-        view.getRemoveEntryButton().addActionListener(e -> removeEntryAction());
         view.getAddJarButton().addActionListener(e -> addJarOrZipAction());
+        view.getAddMvnButton().addActionListener(e -> addMvnDependencyAction());
+        view.getRemoveEntryButton().addActionListener(e -> removeEntryAction());
     }
 
     protected void addJarOrZipAction() {
@@ -139,13 +131,18 @@
         chooseClassEntry(null, "Select Java Class Directory.", JFileChooser.DIRECTORIES_ONLY);
     }
 
-    protected void removeEntryAction() {
+    protected void addMvnDependencyAction() {
+        MavenDependencyDialog dialog = new MavenDependencyDialog(this);
+        dialog.getView().setVisible(true);
+    }
+
+    protected synchronized void removeEntryAction() {
         int selected = view.getTable().getSelectedRow();
         if (selected < 0) {
             return;
         }
 
-        addRemovedPreferences(classPathKeys.get(selected));
+        updatePreferences(classPathKeys.get(selected), "");
         classPathEntries.remove(selected);
         classPathKeys.remove(selected);
 
@@ -157,13 +154,10 @@
         chooser.setFileSelectionMode(selectionMode);
         chooser.setDialogType(JFileChooser.OPEN_DIALOG);
         chooser.setAcceptAllFileFilterUsed(true);
-
         getLastDirectory().updateChooser(chooser);
-
         if (filter != null) {
             chooser.addChoosableFileFilter(filter);
         }
-
         chooser.setDialogTitle(title);
 
         File selected = null;
@@ -172,25 +166,29 @@
             selected = chooser.getSelectedFile();
         }
 
-        if (selected != null) {
-            if (!classPathEntries.contains(selected.getAbsolutePath())) {
-                // store last dir in preferences
-                getLastDirectory().updateFromChooser(chooser);
-
-                int len = classPathEntries.size();
-                int key = ++counter;
-
-                String value = selected.getAbsolutePath();
-                addChangedPreferences(Integer.toString(key), value);
-                classPathEntries.add(value);
-                classPathKeys.add(Integer.toString(key));
-
-                tableModel.fireTableRowsInserted(len, len);
-            }
-        }
+        // store last dir in preferences
+        getLastDirectory().updateFromChooser(chooser);
+        // add to classpath list
+        addClasspathEntry(selected);
     }
 
-    public void addChangedPreferences(String key, String value) {
+    public synchronized void addClasspathEntry(File selected) {
+        if (selected == null || classPathEntries.contains(selected.getAbsolutePath())) {
+            return;
+        }
+
+        int len = classPathEntries.size();
+        int key = ++counter;
+
+        String value = selected.getAbsolutePath();
+        updatePreferences(Integer.toString(key), value);
+        classPathEntries.add(value);
+        classPathKeys.add(Integer.toString(key));
+
+        tableModel.fireTableRowsInserted(len, len);
+    }
+
+    public void updatePreferences(String key, String value) {
         Map<String, String> map = editor.getChangedPreferences().get(preferences);
         if (map == null) {
             map = new HashMap<>();
@@ -199,16 +197,13 @@
         editor.getChangedPreferences().put(preferences, map);
     }
 
-    public void addRemovedPreferences(String key) {
-        Map<String, String> map = editor.getRemovedPreferences().get(preferences);
-        if (map == null) {
-            map = new HashMap<>();
-        }
-        map.put(key, "");
-        editor.getRemovedPreferences().put(preferences, map);
-    }
+    static class ClasspathTableModel extends AbstractTableModel {
 
-    class ClasspathTableModel extends AbstractTableModel {
+        private final List<String> classPathEntries;
+
+        ClasspathTableModel(List<String> classPathEntries) {
+            this.classPathEntries = classPathEntries;
+        }
 
         public int getColumnCount() {
             return 1;
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferencesView.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferencesView.java
index 292d362..6b0574c 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferencesView.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/ClasspathPreferencesView.java
@@ -37,6 +37,7 @@
 
     protected JButton addJarButton;
     protected JButton addDirButton;
+    protected JButton addMvnButton;
     protected JButton removeEntryButton;
     protected JTable table;
 
@@ -45,6 +46,7 @@
         // create widgets
         addJarButton = new JButton("Add Jar/Zip");
         addDirButton = new JButton("Add Class Folder");
+        addMvnButton = new JButton("Get From Maven Central");
         removeEntryButton = new JButton("Remove");
 
         table = new CayenneTable();
@@ -59,6 +61,7 @@
 
         builder.append(addJarButton);
         builder.append(addDirButton);
+        builder.append(addMvnButton);
         builder.append(removeEntryButton);
 
         setLayout(new BorderLayout());
@@ -76,6 +79,10 @@
         return addJarButton;
     }
 
+    public JButton getAddMvnButton() {
+        return addMvnButton;
+    }
+
     public JButton getRemoveEntryButton() {
         return removeEntryButton;
     }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/MavenDependencyDialog.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/MavenDependencyDialog.java
new file mode 100644
index 0000000..ab8e066
--- /dev/null
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/MavenDependencyDialog.java
@@ -0,0 +1,167 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.modeler.dialog.pref;
+
+import java.awt.Component;
+import java.awt.Dialog;
+import java.awt.Frame;
+import java.awt.Window;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Objects;
+import javax.swing.JOptionPane;
+import javax.swing.SwingUtilities;
+
+import org.apache.cayenne.modeler.Application;
+import org.apache.cayenne.modeler.util.CayenneController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MavenDependencyDialog extends CayenneController {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(MavenDependencyDialog.class);
+    private static final int DEFAULT_BUFFER_SIZE = 8192;
+
+    private final ClasspathPreferences preferencesController;
+    private final MavenDependencyDialogView view;
+
+    private volatile boolean closing;
+
+    public MavenDependencyDialog(ClasspathPreferences preferencesController) {
+        this.preferencesController = preferencesController;
+        Window parentView = preferencesController.getView() instanceof Window
+                ? (Window) preferencesController.getView()
+                : SwingUtilities.getWindowAncestor(preferencesController.getView());
+        if(parentView instanceof Dialog) {
+            view = new MavenDependencyDialogView((Dialog) parentView);
+        } else {
+            view = new MavenDependencyDialogView((Frame) parentView);
+        }
+        initBindings();
+    }
+
+    private void initBindings() {
+        view.getDownloadButton().addActionListener(e -> loadArtifact());
+        view.getCancelButton().addActionListener(e -> close());
+    }
+
+    private void loadArtifact() {
+        // url template: https://repo1.maven.org/maven2/org/apache/cayenne/cayenne-server/4.2.M1/cayenne-server-4.2.M1.jar
+        String groupPath = view.getGroupId().getText().replace('.', '/').trim();
+        String artifactIdText = view.getArtifactId().getText().trim();
+        String versionText = view.getVersion().getText().trim();
+
+        if("".equals(groupPath)) {
+            JOptionPane.showMessageDialog(view, "Empty group Id", "Warning", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+
+        if("".equals(artifactIdText)) {
+            JOptionPane.showMessageDialog(view, "Empty artifact Id", "Warning", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+
+        if("".equals(versionText)) {
+            JOptionPane.showMessageDialog(view, "Empty version", "Warning", JOptionPane.WARNING_MESSAGE);
+            return;
+        }
+
+        String urlText = "https://repo1.maven.org/maven2/" + groupPath + "/"
+                + artifactIdText + "/" + versionText + "/"
+                + artifactIdText + "-" + versionText + ".jar";
+
+        Application.getInstance().getFrameController().updateStatus("Loading " + urlText);
+
+        String localPath = System.getProperty( "user.home" ) + "/.cayenne/modeler/"
+                + groupPath + "/" + artifactIdText + "-" + versionText + ".jar";
+        File targetFile = new File(localPath);
+
+        view.getDownloadButton().setEnabled(false);
+        new Thread(() -> download(urlText, targetFile)).start();
+    }
+
+    private void close() {
+        this.closing = true;
+        view.close();
+    }
+
+    public void download(String srcUrl, File dstFile) {
+        if(!dstFile.getParentFile().exists()
+                && !dstFile.getParentFile().mkdirs()) {
+            finalizeDownload(dstFile, "Unable to create file " + dstFile, false, false);
+            return;
+        }
+
+        try {
+            BufferedInputStream is = new BufferedInputStream(new URL(srcUrl).openStream());
+            OutputStream os = new FileOutputStream(dstFile);
+            transferTo(is, os);
+        } catch (FileNotFoundException fnf) {
+            finalizeDownload(dstFile, "Url not found: " + srcUrl, false, false);
+            return;
+        } catch (Exception e) {
+            LOGGER.warn("Failed to download Maven dependency " + srcUrl, e);
+            finalizeDownload(dstFile, "Unable to download file " + dstFile, false, true);
+            return;
+        }
+        finalizeDownload(dstFile, "Succesfully downloaded", true, true);
+    }
+
+    private void finalizeDownload(File dstFile, String status, boolean success, boolean shouldClose) {
+        SwingUtilities.invokeLater(() -> {
+            if(success) {
+                preferencesController.addClasspathEntry(dstFile);
+            } else {
+                JOptionPane.showMessageDialog(view, status, "Error", JOptionPane.ERROR_MESSAGE);
+            }
+
+            view.getDownloadButton().setEnabled(true);
+            Application.getInstance().getFrameController().updateStatus(status);
+
+            if(shouldClose) {
+                close();
+            }
+        });
+    }
+
+    private void transferTo(InputStream in, OutputStream out) throws IOException {
+        Objects.requireNonNull(in);
+        Objects.requireNonNull(out);
+        byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
+        int read;
+        while ((read = in.read(buffer, 0, DEFAULT_BUFFER_SIZE)) >= 0) {
+            out.write(buffer, 0, read);
+            if(closing) {
+                break;
+            }
+        }
+    }
+
+    @Override
+    public Component getView() {
+        return view;
+    }
+}
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/MavenDependencyDialogView.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/MavenDependencyDialogView.java
new file mode 100644
index 0000000..6dea331
--- /dev/null
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/dialog/pref/MavenDependencyDialogView.java
@@ -0,0 +1,120 @@
+/*****************************************************************
+ *   Licensed to the Apache Software Foundation (ASF) under one
+ *  or more contributor license agreements.  See the NOTICE file
+ *  distributed with this work for additional information
+ *  regarding copyright ownership.  The ASF licenses this file
+ *  to you under the Apache License, Version 2.0 (the
+ *  "License"); you may not use this file except in compliance
+ *  with the License.  You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing,
+ *  software distributed under the License is distributed on an
+ *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ *  KIND, either express or implied.  See the License for the
+ *  specific language governing permissions and limitations
+ *  under the License.
+ ****************************************************************/
+
+package org.apache.cayenne.modeler.dialog.pref;
+
+import java.awt.BorderLayout;
+import java.awt.Dialog;
+import java.awt.Frame;
+
+import javax.swing.JButton;
+import javax.swing.JTextField;
+
+import com.jgoodies.forms.builder.PanelBuilder;
+import com.jgoodies.forms.layout.CellConstraints;
+import com.jgoodies.forms.layout.FormLayout;
+import org.apache.cayenne.modeler.util.CayenneDialog;
+import org.apache.cayenne.modeler.util.ModelerUtil;
+import org.apache.cayenne.modeler.util.PanelFactory;
+
+public class MavenDependencyDialogView extends CayenneDialog {
+
+    private JButton downloadButton;
+    private JButton cancelButton;
+    private JTextField groupId;
+    private JTextField artifactId;
+    private JTextField version;
+
+    public MavenDependencyDialogView(Dialog parentDialog) {
+        super(parentDialog, "Download artifact", true);
+        this.initView();
+        this.pack();
+        ModelerUtil.centerWindow(parentDialog, this);
+    }
+
+    public MavenDependencyDialogView(Frame parentFrame) {
+        super(parentFrame, "Download artifact", true);
+        this.initView();
+        this.pack();
+        ModelerUtil.centerWindow(parentFrame, this);
+    }
+
+    private void initView() {
+        getContentPane().setLayout(new BorderLayout());
+
+        {
+            groupId = new JTextField(25);
+            artifactId = new JTextField(25);
+            version = new JTextField(25);
+
+            CellConstraints cc = new CellConstraints();
+            PanelBuilder builder = new PanelBuilder(
+                    new FormLayout(
+                            "right:max(50dlu;pref), 3dlu, fill:min(100dlu;pref)",
+                            "p, 3dlu, p, 3dlu, p, 3dlu"
+                    ));
+            builder.setDefaultDialogBorder();
+
+            builder.addLabel("group id:", cc.xy(1, 1));
+            builder.add(groupId, cc.xy(3, 1));
+
+            builder.addLabel("artifact id:", cc.xy(1, 3));
+            builder.add(artifactId, cc.xy(3, 3));
+
+            builder.addLabel("version:", cc.xy(1, 5));
+            builder.add(version, cc.xy(3, 5));
+
+            getContentPane().add(builder.getPanel(), BorderLayout.NORTH);
+        }
+
+        {
+            downloadButton = new JButton("Download");
+            cancelButton = new JButton("Cancel");
+            getRootPane().setDefaultButton(downloadButton);
+
+            JButton[] buttons = {cancelButton, downloadButton};
+            getContentPane().add(PanelFactory.createButtonPanel(buttons), BorderLayout.SOUTH);
+        }
+    }
+
+    public void close() {
+        setVisible(false);
+        dispose();
+    }
+
+    public JButton getCancelButton() {
+        return cancelButton;
+    }
+
+    public JButton getDownloadButton() {
+        return downloadButton;
+    }
+
+    public JTextField getArtifactId() {
+        return artifactId;
+    }
+
+    public JTextField getGroupId() {
+        return groupId;
+    }
+
+    public JTextField getVersion() {
+        return version;
+    }
+}
\ No newline at end of file
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java
index 6068f34..319a186 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/ObjAttributeTableModel.java
@@ -64,8 +64,9 @@
     public static final int DB_ATTRIBUTE = 2;
     public static final int DB_ATTRIBUTE_TYPE = 3;
     public static final int LOCKING = 4;
-    public static final int COMMENT = 5;
-    public static final int COLUMN_COUNT = 6;
+    public static final int LAZY = 5;
+    public static final int COMMENT = 6;
+    public static final int COLUMN_COUNT = 7;
 
     private ObjEntity entity;
     private DbEntity dbEntity;
@@ -98,9 +99,10 @@
         return table;
     }
 
-    public Class getColumnClass(int col) {
+    public Class<?> getColumnClass(int col) {
         switch (col) {
             case LOCKING:
+            case LAZY:
                 return Boolean.class;
             default:
                 return String.class;
@@ -162,6 +164,8 @@
                 return "DB Type";
             case LOCKING:
                 return "Used for Locking";
+            case LAZY:
+                return "Lazy loading";
             case COMMENT:
                 return "Comment";
             default:
@@ -178,12 +182,14 @@
                 return attribute.getName();
             case OBJ_ATTRIBUTE_TYPE:
                 return attribute.getType();
-            case LOCKING:
-                return attribute.isUsedForLocking() ? Boolean.TRUE : Boolean.FALSE;
             case DB_ATTRIBUTE:
                 return getDBAttribute(attribute, dbAttribute);
             case DB_ATTRIBUTE_TYPE:
                 return getDBAttributeType(attribute, dbAttribute);
+            case LOCKING:
+                return attribute.isUsedForLocking();
+            case LAZY:
+                return attribute.isLazy();
             case COMMENT:
                 return getComment(attribute.getValue());
             default:
@@ -308,6 +314,7 @@
         attributeNew.setParent(attribute.getParent());
         attributeNew.setType(attribute.getType());
         attributeNew.setUsedForLocking(attribute.isUsedForLocking());
+        attributeNew.setLazy(attribute.isLazy());
 
         entity.updateAttribute(attributeNew);
 
@@ -337,6 +344,10 @@
         attribute.setUsedForLocking((value instanceof Boolean) && (Boolean) value);
     }
 
+    private void setColumnLazy(ObjAttributeWrapper attribute, Object value) {
+        attribute.setLazy((value instanceof Boolean) && (Boolean) value);
+    }
+
     private void setDbAttribute(ObjAttributeWrapper attribute, Object value) {
 
         // If db attribute exist, associate it with obj attribute
@@ -370,13 +381,17 @@
                 setObjAttributeType(attribute, value);
                 fireTableCellUpdated(row, column);
                 break;
+            case DB_ATTRIBUTE:
+                setDbAttribute(attribute, value);
+                fireTableRowsUpdated(row, row);
+                break;
             case LOCKING:
                 setColumnLocking(attribute, value);
                 fireTableCellUpdated(row, column);
                 break;
-            case DB_ATTRIBUTE:
-                setDbAttribute(attribute, value);
-                fireTableRowsUpdated(row, row);
+            case LAZY:
+                setColumnLazy(attribute, value);
+                fireTableCellUpdated(row, column);
                 break;
             case COMMENT:
                 setComment((String)value, attribute.getValue());
@@ -429,6 +444,9 @@
             case LOCKING:
                 sortByElementProperty("usedForLocking", isAscent);
                 break;
+            case LAZY:
+                sortByElementProperty("lazy", isAscent);
+                break;
             case DB_ATTRIBUTE:
             case DB_ATTRIBUTE_TYPE:
                 Collections.sort(objectList, new ObjAttributeTableComparator(sortCol));
@@ -436,8 +454,6 @@
                     Collections.reverse(objectList);
                 }
                 break;
-            default:
-                return;
         }
     }
 
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
index 4049bc8..e63b40b 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/SelectQueryMainTab.java
@@ -36,7 +36,6 @@
 import org.apache.cayenne.map.ObjEntity;
 import org.apache.cayenne.map.QueryDescriptor;
 import org.apache.cayenne.map.SelectQueryDescriptor;
-import org.apache.cayenne.query.SelectQuery;
 import org.apache.cayenne.swing.components.JCayenneCheckBox;
 import org.apache.cayenne.modeler.ProjectController;
 import org.apache.cayenne.modeler.util.Comparators;
@@ -134,7 +133,7 @@
         distinct.addItemListener(e -> {
             QueryDescriptor query = getQuery();
             if (query != null) {
-                query.setProperty(SelectQuery.DISTINCT_PROPERTY, Boolean.toString(distinct.isSelected()));
+                query.setProperty(SelectQueryDescriptor.DISTINCT_PROPERTY, Boolean.toString(distinct.isSelected()));
                 mediator.fireQueryEvent(new QueryEvent(this, query));
             }
         });
@@ -155,7 +154,7 @@
         SelectQueryDescriptor query = (SelectQueryDescriptor) descriptor;
 
         name.setText(query.getName());
-        distinct.setSelected(Boolean.valueOf(query.getProperties().get(SelectQuery.DISTINCT_PROPERTY)));
+        distinct.setSelected(Boolean.parseBoolean(query.getProperties().get(SelectQueryDescriptor.DISTINCT_PROPERTY)));
         qualifier.setText(query.getQualifier() != null ? query
                 .getQualifier()
                 .toString() : null);
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
index 1c863f0..d292ac2 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CodeGeneratorController.java
@@ -35,12 +35,13 @@
 
 import org.apache.cayenne.configuration.event.DataMapEvent;
 import org.apache.cayenne.configuration.event.DataMapListener;
+import org.apache.cayenne.configuration.xml.DataChannelMetaData;
 import org.apache.cayenne.di.DIBootstrap;
+import org.apache.cayenne.di.Module;
 import org.apache.cayenne.di.spi.ModuleLoader;
 import org.apache.cayenne.gen.CgenConfiguration;
 import org.apache.cayenne.gen.ClassGenerationAction;
 import org.apache.cayenne.gen.ClassGenerationActionFactory;
-import org.apache.cayenne.gen.ClientClassGenerationAction;
 import org.apache.cayenne.map.DataMap;
 import org.apache.cayenne.map.Embeddable;
 import org.apache.cayenne.map.Entity;
@@ -56,7 +57,7 @@
 import org.apache.cayenne.modeler.util.CayenneController;
 import org.apache.cayenne.modeler.util.ModelerUtil;
 import org.apache.cayenne.swing.BindingBuilder;
-import org.apache.cayenne.tools.CayenneToolsModuleProvider;
+import org.apache.cayenne.tools.ToolsInjectorBuilder;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -67,21 +68,17 @@
 public class CodeGeneratorController extends CayenneController implements ObjEntityListener, EmbeddableListener, DataMapListener {
     private static final Logger LOGGER = LoggerFactory.getLogger(ErrorDebugDialog.class);
 
-    public static final String SELECTED_PROPERTY = "selected";
+    protected final ProjectController projectController;
+    protected final List<Object> classes;
+    protected final SelectionModel selectionModel;
+    protected final CodeGeneratorPane view;
+    protected final ClassesTabController classesSelector;
+    protected final GeneratorTabController generatorSelector;
+    protected final ConcurrentMap<DataMap, GeneratorController> prevGeneratorController;
 
-    protected ProjectController projectController;
-
-    protected List<Object> classes;
-    protected SelectionModel selectionModel;
-    protected Object currentClass;
-
-    protected boolean initFromModel;
-
-    protected CodeGeneratorPane view;
-
-    protected ClassesTabController classesSelector;
-    protected GeneratorTabController generatorSelector;
-    private ConcurrentMap<DataMap, GeneratorController> prevGeneratorController;
+    private Object currentClass;
+    private CgenConfiguration cgenConfiguration;
+    private boolean initFromModel;
 
     public CodeGeneratorController(ProjectController projectController) {
         super(projectController);
@@ -98,17 +95,21 @@
 
     public void initFromModel() {
         initFromModel = true;
-        prepareClasses(projectController.getCurrentDataMap());
         DataMap dataMap = projectController.getCurrentDataMap();
-        classesSelectedAction();
-        CgenConfiguration cgenConfiguration = createConfiguration();
-        GeneratorController modeController = prevGeneratorController.get(dataMap) != null
-                        ? prevGeneratorController.get(dataMap)
-                        : isDefaultConfig(cgenConfiguration)
-                            ? cgenConfiguration.isClient()
-                                ? generatorSelector.getClientGeneratorController()
-                                : generatorSelector.getStandartController()
-                            : generatorSelector.getCustomModeController();
+
+        prepareClasses(dataMap);
+        createConfiguration(dataMap);
+
+        GeneratorController modeController = prevGeneratorController.get(dataMap);
+        if(modeController == null) {
+            if(cgenConfiguration.isDefault()) {
+                modeController = cgenConfiguration.isClient()
+                        ? generatorSelector.getClientGeneratorController()
+                        : generatorSelector.getStandartController();
+            } else {
+                modeController = generatorSelector.getCustomModeController();
+            }
+        }
 
         prevGeneratorController.put(dataMap, modeController);
         generatorSelector.setSelectedController(modeController);
@@ -117,18 +118,6 @@
         classesSelector.validate(classes);
     }
 
-    private boolean isDefaultConfig(CgenConfiguration cgenConfiguration) {
-        return cgenConfiguration.isMakePairs() && cgenConfiguration.isUsePkgPath() &&
-                !cgenConfiguration.isOverwrite() && !cgenConfiguration.isCreatePKProperties() &&
-                !cgenConfiguration.isCreatePropertyNames() && cgenConfiguration.getOutputPattern().equals("*.java") &&
-                (cgenConfiguration.getTemplate().equals(ClassGenerationAction.SUBCLASS_TEMPLATE) ||
-                        cgenConfiguration.getTemplate().equals(ClientClassGenerationAction.SUBCLASS_TEMPLATE)) &&
-                (cgenConfiguration.getSuperTemplate().equals(ClassGenerationAction.SUPERCLASS_TEMPLATE) ||
-                        cgenConfiguration.getSuperTemplate().equals(ClientClassGenerationAction.SUPERCLASS_TEMPLATE)) &&
-                (cgenConfiguration.getSuperPkg() == null || cgenConfiguration.getSuperPkg().isEmpty());
-
-    }
-
     private void initListeners(){
         projectController.addObjEntityListener(this);
         projectController.addEmbeddableListener(this);
@@ -144,7 +133,6 @@
         BindingBuilder builder = new BindingBuilder(getApplication().getBindingFactory(), this);
 
         builder.bindToAction(view.getGenerateButton(), "generateAction()");
-        builder.bindToAction(this, "classesSelectedAction()", SELECTED_PROPERTY);
         builder.bindToAction(generatorSelector, "generatorSelectedAction()",
                 GeneratorTabController.GENERATOR_PROPERTY);
 
@@ -174,17 +162,13 @@
         classesSelector.classSelectedAction();
     }
 
-    public void classesSelectedAction() {
-        if(!isInitFromModel()) {
-            getProjectController().setDirty(true);
-        }
-    }
-
+    @SuppressWarnings("unused")
     public void generateAction() {
-        CgenConfiguration cgenConfiguration = createConfiguration();
-        ClassGenerationAction generator = DIBootstrap
-                .createInjector(new ModuleLoader()
-                        .load(CayenneToolsModuleProvider.class))
+        ClassGenerationAction generator = new ToolsInjectorBuilder()
+                .addModule(binder
+                        -> binder.bind(DataChannelMetaData.class)
+                        .toInstance(projectController.getApplication().getMetaData()))
+                .create()
                 .getInstance(ClassGenerationActionFactory.class)
                 .createAction(cgenConfiguration);
 
@@ -221,14 +205,13 @@
     /**
      * Creates a class generator for provided selections.
      */
-    public CgenConfiguration createConfiguration() {
-        DataMap map = projectController.getCurrentDataMap();
-        CgenConfiguration cgenConfiguration = projectController.getApplication().getMetaData().get(map, CgenConfiguration.class);
+    public void createConfiguration(DataMap map) {
+        cgenConfiguration = projectController.getApplication().getMetaData().get(map, CgenConfiguration.class);
         if(cgenConfiguration != null){
             addToSelectedEntities(cgenConfiguration.getEntities());
             addToSelectedEmbeddables(cgenConfiguration.getEmbeddables());
             cgenConfiguration.setForce(true);
-            return cgenConfiguration;
+            return;
         }
 
         cgenConfiguration = new CgenConfiguration(false);
@@ -244,13 +227,13 @@
                 Files.createDirectories(basePath);
             } catch (IOException e) {
                 JOptionPane.showMessageDialog(getView(), "Can't create directory. Select a different one.");
-                return null;
+                return;
             }
         }
         // not a directory
         if (!Files.isDirectory(basePath)) {
             JOptionPane.showMessageDialog(this.getView(), basePath + " is not a valid directory.");
-            return null;
+            return;
         }
 
         cgenConfiguration.setRootPath(basePath);
@@ -267,10 +250,6 @@
                 .stream()
                 .map(Embeddable::getClassName)
                 .collect(Collectors.toList()));
-        getApplication().getMetaData().add(map, cgenConfiguration);
-        projectController.setDirty(true);
-
-        return cgenConfiguration;
     }
 
     public List<Object> getClasses() {
@@ -282,26 +261,21 @@
 
         for (Object classObj : classes) {
             if(classObj instanceof DataMap) {
-                boolean select = predicate.test(classObj);
-                updateArtifactGenerationMode(classObj, select);
+                boolean selected = predicate.test(classObj);
+                updateArtifactGenerationMode(selected);
             }
         }
 
-        if (modified) {
-            firePropertyChange(SELECTED_PROPERTY, null, null);
-        }
-
         return modified;
     }
 
-    private void updateArtifactGenerationMode(Object classObj, boolean selected) {
-        DataMap dataMap = (DataMap) classObj;
-        CgenConfiguration cgenConfiguration = projectController.getApplication().getMetaData().get(dataMap, CgenConfiguration.class);
+    private void updateArtifactGenerationMode(boolean selected) {
         if(selected) {
             cgenConfiguration.setArtifactsGenerationMode("all");
         } else {
             cgenConfiguration.setArtifactsGenerationMode("entity");
         }
+        checkCgenConfigDirty();
     }
 
     public boolean isSelected() {
@@ -310,15 +284,9 @@
 
     public void setSelected(boolean selectedFlag) {
         if (currentClass instanceof DataMap) {
-            updateArtifactGenerationMode(currentClass, selectedFlag);
+            updateArtifactGenerationMode(selectedFlag);
         }
-        if (selectionModel.setSelected(currentClass, selectedFlag)) {
-            firePropertyChange(SELECTED_PROPERTY, null, null);
-        }
-    }
-
-    public Object getCurrentClass() {
-        return currentClass;
+        selectionModel.setSelected(currentClass, selectedFlag);
     }
 
     public void setCurrentClass(Object currentClass) {
@@ -330,29 +298,38 @@
         updateEmbeddables();
     }
 
-    CgenConfiguration getCurrentConfiguration() {
-        DataMap map = getProjectController().getCurrentDataMap();
-        return projectController.getApplication().getMetaData().get(map, CgenConfiguration.class);
+    public void checkCgenConfigDirty() {
+        if(initFromModel || cgenConfiguration == null) {
+            return;
+        }
+
+        DataMap map = projectController.getCurrentDataMap();
+        CgenConfiguration existingConfig = projectController.getApplication().getMetaData().get(map, CgenConfiguration.class);
+        if(existingConfig == null) {
+            getApplication().getMetaData().add(map, cgenConfiguration);
+        }
+
+        projectController.setDirty(true);
     }
 
     private void updateEntities() {
-        CgenConfiguration cgenConfiguration = getCurrentConfiguration();
         if(cgenConfiguration != null) {
             cgenConfiguration.getEntities().clear();
             for(ObjEntity entity: selectionModel.getSelectedEntities(classes)) {
                 cgenConfiguration.loadEntity(entity);
             }
         }
+        checkCgenConfigDirty();
     }
 
     private void updateEmbeddables() {
-        CgenConfiguration cgenConfiguration = getCurrentConfiguration();
         if(cgenConfiguration != null) {
             cgenConfiguration.getEmbeddables().clear();
             for(Embeddable embeddable : selectionModel.getSelectedEmbeddables(classes)) {
                 cgenConfiguration.loadEmbeddable(embeddable.getClassName());
             }
         }
+        checkCgenConfigDirty();
     }
 
     private void addToSelectedEntities(Collection<String> entities) {
@@ -363,10 +340,10 @@
     void addEntity(DataMap dataMap, ObjEntity objEntity) {
         prepareClasses(dataMap);
         selectionModel.addSelectedEntity(objEntity.getName());
-        CgenConfiguration cgenConfiguration = getCurrentConfiguration();
         if(cgenConfiguration != null) {
             cgenConfiguration.loadEntity(objEntity);
         }
+        checkCgenConfigDirty();
     }
 
     private void addToSelectedEmbeddables(Collection<String> embeddables) {
@@ -374,7 +351,6 @@
         updateEmbeddables();
     }
 
-
     public int getSelectedEntitiesSize() {
         return selectionModel.getSelectedEntitiesCount();
     }
@@ -410,11 +386,10 @@
     @Override
     public void objEntityRemoved(EntityEvent e) {
         selectionModel.removeFromSelectedEntities((ObjEntity) e.getEntity());
-        DataMap map = e.getEntity().getDataMap();
-        CgenConfiguration cgenConfiguration = projectController.getApplication().getMetaData().get(map, CgenConfiguration.class);
         if(cgenConfiguration != null) {
             cgenConfiguration.getEntities().remove(e.getEntity().getName());
         }
+        checkCgenConfigDirty();
     }
 
     @Override
@@ -425,32 +400,32 @@
         prepareClasses(map);
         String embeddableClassName = e.getEmbeddable().getClassName();
         selectionModel.addSelectedEmbeddable(embeddableClassName);
-        CgenConfiguration cgenConfiguration = getCurrentConfiguration();
         if(cgenConfiguration != null) {
             cgenConfiguration.loadEmbeddable(embeddableClassName);
         }
+        checkCgenConfigDirty();
     }
 
     @Override
     public void embeddableRemoved(EmbeddableEvent e, DataMap map) {
         selectionModel.removeFromSelectedEmbeddables(e.getEmbeddable());
-        CgenConfiguration cgenConfiguration = projectController.getApplication().getMetaData().get(map, CgenConfiguration.class);
         if(cgenConfiguration != null) {
             cgenConfiguration.getEmbeddables().remove(e.getEmbeddable().getClassName());
         }
+        checkCgenConfigDirty();
     }
 
     @Override
     public void dataMapChanged(DataMapEvent e) {
         if(e.getSource() instanceof DbImportController) {
-            CgenConfiguration cgenConfiguration = getCurrentConfiguration();
             if(cgenConfiguration != null) {
-                for(ObjEntity objEntity : e.getDataMap().getObjEntities()) {
-                    if(!cgenConfiguration.getExcludeEntityArtifacts().contains(objEntity.getName())) {
+                for (ObjEntity objEntity : e.getDataMap().getObjEntities()) {
+                    if (!cgenConfiguration.getExcludeEntityArtifacts().contains(objEntity.getName())) {
                         addEntity(cgenConfiguration.getDataMap(), objEntity);
                     }
                 }
             }
+            checkCgenConfigDirty();
         }
     }
 
@@ -459,4 +434,8 @@
 
     @Override
     public void dataMapRemoved(DataMapEvent e) {}
+
+    public CgenConfiguration getCgenConfiguration() {
+        return cgenConfiguration;
+    }
 }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CustomModeController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CustomModeController.java
index 6d5be73..d051015 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CustomModeController.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/CustomModeController.java
@@ -139,37 +139,27 @@
                 cgenConfiguration.setQueryTemplate(ClassGenerationAction.DATAMAP_SUBCLASS_TEMPLATE);
             }
             initForm(cgenConfiguration);
-            if(!getParentController().isInitFromModel()) {
-                getParentController().getProjectController().setDirty(true);
-            }
+            getParentController().checkCgenConfigDirty();
         });
 
         view.getOverwrite().addActionListener(val -> {
             cgenConfiguration.setOverwrite(view.getOverwrite().isSelected());
-            if(!getParentController().isInitFromModel()) {
-                getParentController().getProjectController().setDirty(true);
-            }
+            getParentController().checkCgenConfigDirty();
         });
 
         view.getCreatePropertyNames().addActionListener(val -> {
             cgenConfiguration.setCreatePropertyNames(view.getCreatePropertyNames().isSelected());
-            if(!getParentController().isInitFromModel()) {
-                getParentController().getProjectController().setDirty(true);
-            }
+            getParentController().checkCgenConfigDirty();
         });
 
         view.getUsePackagePath().addActionListener(val -> {
             cgenConfiguration.setUsePkgPath(view.getUsePackagePath().isSelected());
-            if(!getParentController().isInitFromModel()) {
-                getParentController().getProjectController().setDirty(true);
-            }
+            getParentController().checkCgenConfigDirty();
         });
 
         view.getPkProperties().addActionListener(val -> {
             cgenConfiguration.setCreatePKProperties(view.getPkProperties().isSelected());
-            if(!getParentController().isInitFromModel()) {
-                getParentController().getProjectController().setDirty(true);
-            }
+            getParentController().checkCgenConfigDirty();
         });
 
         view.getClientMode().addActionListener(val -> {
@@ -195,9 +185,7 @@
                     cgenConfiguration.getRootPath());
             view.getSubclassTemplate().setItem(templateName);
             view.getSuperclassTemplate().setItem(superTemplateName);
-            if(!getParentController().isInitFromModel()) {
-                getParentController().getProjectController().setDirty(true);
-            }
+            getParentController().checkCgenConfigDirty();
         });
     }
 
@@ -213,7 +201,6 @@
         view.getPkProperties().setSelected(cgenConfiguration.isCreatePKProperties());
         view.getSuperPkg().setText(cgenConfiguration.getSuperPkg());
         updateComboBoxes();
-        getParentController().setInitFromModel(false);
     }
 
     @Override
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorTabController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorTabController.java
index d7a6121..259cb2c 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorTabController.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/GeneratorTabController.java
@@ -78,7 +78,7 @@
         view.getGenerationMode().addActionListener(action -> {
             String name = (String)view.getGenerationMode().getSelectedItem();
             GeneratorController modeController = getGeneratorController();
-            CgenConfiguration cgenConfiguration = getParentController().createConfiguration();
+            CgenConfiguration cgenConfiguration = getParentController().getCgenConfiguration();
             modeController.updateConfiguration(cgenConfiguration);
             controllers.get(name).initForm(cgenConfiguration);
             getParentController().getPrevGeneratorController().put(cgenConfiguration.getDataMap(), modeController);
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/StandardModeController.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/StandardModeController.java
index 90368ab..b3156ed 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/StandardModeController.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/cgen/StandardModeController.java
@@ -44,12 +44,6 @@
     }
 
     @Override
-    protected void initForm(CgenConfiguration cgenConfiguration) {
-        super.initForm(cgenConfiguration);
-        getParentController().setInitFromModel(false);
-    }
-
-    @Override
     public void updateConfiguration(CgenConfiguration cgenConfiguration) {
         cgenConfiguration.setClient(false);
         cgenConfiguration.setTemplate(ClassGenerationAction.SUBCLASS_TEMPLATE);
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/DraggableTreePanel.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/DraggableTreePanel.java
index aa0a3a9..320ac40 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/DraggableTreePanel.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/DraggableTreePanel.java
@@ -64,7 +64,7 @@
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
 import java.io.IOException;
-import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -83,16 +83,16 @@
     private static final String MOVE_BUTTON_LABEL = "Include";
     private static final String MOVE_INV_BUTTON_LABEL = "Exclude";
 
-    private DbImportTree sourceTree;
-    private DbImportTree targetTree;
+    private final ProjectController projectController;
+    private final DbImportTree sourceTree;
+    private final DbImportTree targetTree;
+    private final Map<DataMap, ReverseEngineering> databaseStructures;
+    private final Map<Class<?>, Integer> levels;
+    private final Map<Class<?>, List<Class<?>>> insertableLevels;
+    private final Map<Class<?>, Class<? extends TreeManipulationAction>> actions;
+
     private CayenneAction.CayenneToolbarButton moveButton;
     private CayenneAction.CayenneToolbarButton moveInvertButton;
-    private Map<DataMap, ReverseEngineering> databaseStructures;
-
-    private ProjectController projectController;
-    private Map<Class, Integer> levels;
-    private Map<Class, List<Class>> insertableLevels;
-    private Map<Class, Class> actions;
 
     public DraggableTreePanel(ProjectController projectController, DbImportTree sourceTree, DbImportTree targetTree) {
         super(sourceTree);
@@ -100,6 +100,10 @@
         this.sourceTree = sourceTree;
         this.projectController = projectController;
         this.databaseStructures = new HashMap<>();
+        this.levels = new HashMap<>();
+        this.insertableLevels = new HashMap<>();
+        this.actions = new HashMap<>();
+
         initLevels();
         initElement();
         initActions();
@@ -107,7 +111,6 @@
     }
 
     private void initActions() {
-        actions = new HashMap<>();
         actions.put(Catalog.class, AddCatalogAction.class);
         actions.put(Schema.class, AddSchemaAction.class);
         actions.put(IncludeTable.class, AddIncludeTableAction.class);
@@ -132,26 +135,22 @@
 
     private void initListeners() {
         sourceTree.addKeyListener(new SourceTreeKeyListener());
-        targetTree.addKeyListener(new TargetTreeKeyListener());
-        targetTree.addTreeSelectionListener(new TargetTreeSelectionListener());
-        targetTree.setTransferHandler(new TargetTreeTransferHandler());
         sourceTree.setTransferHandler(new SourceTreeTransferHandler());
         sourceTree.addTreeSelectionListener(new SourceTreeSelectionListener());
         sourceTree.addMouseListener(new ResetFocusMouseAdapter());
+
+        targetTree.addKeyListener(new TargetTreeKeyListener());
+        targetTree.setTransferHandler(new TargetTreeTransferHandler());
+        targetTree.addTreeSelectionListener(new TargetTreeSelectionListener());
         targetTree.setDragEnabled(true);
     }
 
     private boolean canBeInverted() {
-        if (sourceTree.getSelectionPath() != null) {
-            DbImportTreeNode selectedElement = sourceTree.getSelectedNode();
-            if (selectedElement == null) {
-                return false;
-            }
-            if (levels.get(selectedElement.getUserObject().getClass()) < SECOND_LEVEL) {
-                return true;
-            }
+        DbImportTreeNode selectedElement = sourceTree.getSelectedNode();
+        if (selectedElement == null) {
+            return false;
         }
-        return false;
+        return levels.get(selectedElement.getUserObject().getClass()) < SECOND_LEVEL;
     }
 
     private void initElement() {
@@ -159,16 +158,16 @@
         sourceTree.setCellRenderer(new ColorTreeRenderer());
         sourceTree.setDropMode(DropMode.INSERT);
 
-        MoveImportNodeAction action = projectController.getApplication().
-                getActionManager().getAction(MoveImportNodeAction.class);
+        MoveImportNodeAction action = projectController.getApplication().getActionManager()
+                .getAction(MoveImportNodeAction.class);
         action.setPanel(this);
         action.setSourceTree(sourceTree);
         action.setTargetTree(targetTree);
         moveButton = (CayenneAction.CayenneToolbarButton) action.buildButton();
         moveButton.setShowingText(true);
         moveButton.setText(MOVE_BUTTON_LABEL);
-        MoveInvertNodeAction actionInv = projectController.getApplication().
-                getActionManager().getAction(MoveInvertNodeAction.class);
+        MoveInvertNodeAction actionInv = projectController.getApplication().getActionManager()
+                .getAction(MoveInvertNodeAction.class);
         actionInv.setPanel(this);
         actionInv.setSourceTree(sourceTree);
         actionInv.setTargetTree(targetTree);
@@ -184,7 +183,6 @@
     }
 
     private void initLevels() {
-        levels = new HashMap<>();
         levels.put(ReverseEngineering.class, ROOT_LEVEL);
         levels.put(Catalog.class, FIRST_LEVEL);
         levels.put(Schema.class, SECOND_LEVEL);
@@ -195,73 +193,60 @@
         levels.put(IncludeProcedure.class, FIFTH_LEVEL);
         levels.put(ExcludeProcedure.class, FIFTH_LEVEL);
 
-        insertableLevels = new HashMap<>();
-        List<Class> rootLevelClasses = new ArrayList<>();
-        rootLevelClasses.add(Catalog.class);
-        rootLevelClasses.add(Schema.class);
-        rootLevelClasses.add(IncludeTable.class);
-        rootLevelClasses.add(ExcludeTable.class);
-        rootLevelClasses.add(IncludeColumn.class);
-        rootLevelClasses.add(ExcludeColumn.class);
-        rootLevelClasses.add(IncludeProcedure.class);
-        rootLevelClasses.add(ExcludeProcedure.class);
+        insertableLevels.put(ReverseEngineering.class, Arrays.asList(
+                Catalog.class, Schema.class,
+                IncludeTable.class, ExcludeTable.class,
+                IncludeColumn.class, ExcludeColumn.class,
+                IncludeProcedure.class, ExcludeProcedure.class
+        ));
 
-        List<Class> catalogLevelClasses = new ArrayList<>();
-        catalogLevelClasses.add(Schema.class);
-        catalogLevelClasses.add(IncludeTable.class);
-        catalogLevelClasses.add(ExcludeTable.class);
-        catalogLevelClasses.add(IncludeColumn.class);
-        catalogLevelClasses.add(ExcludeColumn.class);
-        catalogLevelClasses.add(IncludeProcedure.class);
-        catalogLevelClasses.add(ExcludeProcedure.class);
+        insertableLevels.put(Catalog.class, Arrays.asList(
+                Schema.class,
+                IncludeTable.class, ExcludeTable.class,
+                IncludeColumn.class, ExcludeColumn.class,
+                IncludeProcedure.class, ExcludeProcedure.class
+        ));
 
-        List<Class> schemaLevelClasses = new ArrayList<>();
-        schemaLevelClasses.add(IncludeTable.class);
-        schemaLevelClasses.add(ExcludeTable.class);
-        schemaLevelClasses.add(IncludeColumn.class);
-        schemaLevelClasses.add(ExcludeColumn.class);
-        schemaLevelClasses.add(IncludeProcedure.class);
-        schemaLevelClasses.add(ExcludeProcedure.class);
+        insertableLevels.put(Schema.class, Arrays.asList(
+                IncludeTable.class, ExcludeTable.class,
+                IncludeColumn.class, ExcludeColumn.class,
+                IncludeProcedure.class, ExcludeProcedure.class
+        ));
 
-        List<Class> includeTableLevelClasses = new ArrayList<>();
-        includeTableLevelClasses.add(IncludeColumn.class);
-        includeTableLevelClasses.add(ExcludeColumn.class);
-
-        insertableLevels.put(ReverseEngineering.class, rootLevelClasses);
-        insertableLevels.put(Catalog.class, catalogLevelClasses);
-        insertableLevels.put(Schema.class, schemaLevelClasses);
-        insertableLevels.put(IncludeTable.class, includeTableLevelClasses);
+        insertableLevels.put(IncludeTable.class, Arrays.asList(
+                IncludeColumn.class, ExcludeColumn.class
+        ));
     }
 
     private boolean canBeMoved() {
-        if (sourceTree.getSelectionPath() != null) {
-            DbImportTreeNode selectedElement = sourceTree.getSelectedNode();
-            if (selectedElement == null) {
+        DbImportTreeNode selectedElement = sourceTree.getSelectedNode();
+        if (selectedElement == null) {
+            return false;
+        }
+
+        if (selectedElement.isIncludeColumn() || selectedElement.isExcludeColumn()) {
+            DbImportTreeNode node = targetTree.findNode(targetTree.getRootNode(), selectedElement.getParent(), 0);
+            if(node != null && node.isExcludeTable()) {
                 return false;
             }
-            if (selectedElement.isIncludeColumn() || selectedElement.isExcludeColumn()) {
-                DbImportTreeNode node = targetTree.findNode(targetTree.getRootNode(), (DbImportTreeNode) selectedElement.getParent(), 0);
-                if(node != null && node.isExcludeTable()) {
-                    return false;
-                }
-            }
-            Class draggableElementClass = selectedElement.getUserObject().getClass();
-            Class reverseEngineeringElementClass;
-            if (targetTree.getSelectionPath() != null) {
-                selectedElement = targetTree.getSelectedNode();
-                DbImportTreeNode parent = (DbImportTreeNode) selectedElement.getParent();
-                if (parent != null) {
-                    reverseEngineeringElementClass = parent.getUserObject().getClass();
-                } else {
-                    reverseEngineeringElementClass = selectedElement.getUserObject().getClass();
-                }
-            } else {
-                reverseEngineeringElementClass = ReverseEngineering.class;
-            }
-            List<Class> containsList = insertableLevels.get(reverseEngineeringElementClass);
-            return containsList.contains(draggableElementClass);
         }
-        return false;
+
+        Class<?> draggableElementClass = selectedElement.getUserObject().getClass();
+        Class<?> reverseEngineeringElementClass;
+        if (targetTree.getSelectionPath() != null) {
+            selectedElement = targetTree.getSelectedNode();
+            DbImportTreeNode parent = selectedElement.getParent();
+            if (parent != null) {
+                reverseEngineeringElementClass = parent.getUserObject().getClass();
+            } else {
+                reverseEngineeringElementClass = selectedElement.getUserObject().getClass();
+            }
+        } else {
+            reverseEngineeringElementClass = ReverseEngineering.class;
+        }
+
+        List<Class<?>> containsList = insertableLevels.get(reverseEngineeringElementClass);
+        return containsList.contains(draggableElementClass);
     }
 
     public JButton getMoveButton() {
@@ -272,14 +257,12 @@
         return moveInvertButton;
     }
 
-    public TreeManipulationAction getActionByNodeType(Class nodeType) {
-        Class actionClass = actions.get(nodeType);
-        if (actionClass != null) {
-            TreeManipulationAction action = (TreeManipulationAction) projectController.getApplication().
-                    getActionManager().getAction(actionClass);
-            return action;
+    public TreeManipulationAction getActionByNodeType(Class<?> nodeType) {
+        Class<? extends TreeManipulationAction> actionClass = actions.get(nodeType);
+        if (actionClass == null) {
+            return null;
         }
-        return null;
+        return projectController.getApplication().getActionManager().getAction(actionClass);
     }
 
     public void bindReverseEngineeringToDatamap(DataMap dataMap, ReverseEngineering reverseEngineering) {
@@ -301,8 +284,9 @@
         public Transferable createTransferable(JComponent c) {
             JTree tree = (JTree) c;
             TreePath[] paths = tree.getSelectionPaths();
-            DbImportTreeNode[] nodes = new DbImportTreeNode[paths.length];
-            for (int i = 0; i < paths.length; i++) {
+            int pathLength = paths == null ? 0 : paths.length;
+            DbImportTreeNode[] nodes = new DbImportTreeNode[pathLength];
+            for (int i = 0; i < pathLength; i++) {
                 nodes[i] = (DbImportTreeNode) paths[i].getLastPathComponent();
             }
             return new Transferable() {
@@ -317,7 +301,7 @@
                 }
 
                 @Override
-                public Object getTransferData(DataFlavor flavor) throws UnsupportedFlavorException, IOException {
+                public Object getTransferData(DataFlavor flavor) {
                     return nodes;
                 }
             };
@@ -365,17 +349,9 @@
             if (root.getChildCount() > 0) {
                 model.nodesChanged(root, new int[]{root.getChildCount() - 1});
             }
-            if (canBeMoved()) {
-                moveButton.setEnabled(true);
-                if (canBeInverted()) {
-                    moveInvertButton.setEnabled(true);
-                } else {
-                    moveInvertButton.setEnabled(false);
-                }
-            } else {
-                moveButton.setEnabled(false);
-                moveInvertButton.setEnabled(false);
-            }
+            boolean canBeMoved = canBeMoved();
+            moveButton.setEnabled(canBeMoved);
+            moveInvertButton.setEnabled(canBeMoved && canBeInverted());
         }
     }
 
@@ -388,10 +364,7 @@
 
         @Override
         public boolean canImport(TransferSupport support) {
-            if (!support.isDrop()) {
-                return false;
-            }
-            return true;
+            return support.isDrop();
         }
 
         @Override
@@ -412,8 +385,8 @@
                 return false;
             }
             if (transferData != null) {
-                MoveImportNodeAction action = projectController.getApplication().
-                        getActionManager().getAction(MoveImportNodeAction.class);
+                MoveImportNodeAction action = projectController.getApplication().getActionManager()
+                        .getAction(MoveImportNodeAction.class);
                 action.setSourceTree(sourceTree);
                 action.setTargetTree(targetTree);
                 action.setPanel(DraggableTreePanel.this);
@@ -428,17 +401,9 @@
         @Override
         public void valueChanged(TreeSelectionEvent e) {
             if (sourceTree.getLastSelectedPathComponent() != null) {
-                if (canBeMoved()) {
-                    moveButton.setEnabled(true);
-                    if (canBeInverted()) {
-                        moveInvertButton.setEnabled(true);
-                    } else {
-                        moveInvertButton.setEnabled(false);
-                    }
-                } else {
-                    moveInvertButton.setEnabled(false);
-                    moveButton.setEnabled(false);
-                }
+                boolean canBeMoved = canBeMoved();
+                moveButton.setEnabled(canBeMoved);
+                moveInvertButton.setEnabled(canBeMoved && canBeInverted());
             }
         }
     }
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/PrintColumnsBiFunction.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/PrintColumnsBiFunction.java
index b7003fe..1458bec 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/PrintColumnsBiFunction.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/dbimport/PrintColumnsBiFunction.java
@@ -26,7 +26,7 @@
 
 public class PrintColumnsBiFunction implements BiFunction<FilterContainer, DbImportTreeNode, Void> {
 
-    private DbImportTree dbImportTree;
+    private final DbImportTree dbImportTree;
 
     public PrintColumnsBiFunction(DbImportTree dbImportTree) {
         this.dbImportTree = dbImportTree;
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java
index fd88e66..f6f64db 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/editor/wrapper/ObjAttributeWrapper.java
@@ -139,6 +139,14 @@
         objAttribute.setUsedForLocking(usedForLocking);
     }
 
+    public boolean isLazy() {
+        return objAttribute.isLazy();
+    }
+
+    public void setLazy(boolean lazy) {
+        objAttribute.setLazy(lazy);
+    }
+
     public DbAttribute getDbAttribute() {
         try {
             return objAttribute.getDbAttribute();
diff --git a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/graph/DbGraphBuilder.java b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/graph/DbGraphBuilder.java
index a6cf59a..b2261d6 100644
--- a/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/graph/DbGraphBuilder.java
+++ b/modeler/cayenne-modeler/src/main/java/org/apache/cayenne/modeler/graph/DbGraphBuilder.java
@@ -22,7 +22,7 @@
 import java.util.Collection;

 

 import org.apache.cayenne.map.DataMap;

-import org.apache.cayenne.map.DetectedDbEntity;

+import org.apache.cayenne.dbsync.model.DetectedDbEntity;

 import org.apache.cayenne.map.Entity;

 import org.apache.cayenne.map.Relationship;

 import org.apache.cayenne.map.event.AttributeEvent;

diff --git a/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/cayennemodeler-strings.properties b/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/cayennemodeler-strings.properties
index 5e683bb..541b9ba 100644
--- a/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/cayennemodeler-strings.properties
+++ b/modeler/cayenne-modeler/src/main/resources/org/apache/cayenne/modeler/cayennemodeler-strings.properties
@@ -17,8 +17,8 @@
 
 cayenne.bugreport.url = http://issues.apache.org/jira/browse/CAY
 
-cayenne.modeler.about.info = (c) 2001-2019 Apache Software Foundation and individual authors.\
-    <br><br>http://cayenne.apache.org/<br>
+cayenne.modeler.about.info = (c) 2001-%d Apache Software Foundation and individual authors.\
+    <br><br>https://cayenne.apache.org/<br>
 
 
 # "New Project" dialog.
diff --git a/modeler/cayenne-wocompat/pom.xml b/modeler/cayenne-wocompat/pom.xml
index 2f2c0ef..42eb72b 100644
--- a/modeler/cayenne-wocompat/pom.xml
+++ b/modeler/cayenne-wocompat/pom.xml
@@ -21,7 +21,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.modeler</groupId>
 		<artifactId>cayenne-modeler-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-wocompat</artifactId>
diff --git a/modeler/pom.xml b/modeler/pom.xml
index 254b74f..03a9d34 100644
--- a/modeler/pom.xml
+++ b/modeler/pom.xml
@@ -24,7 +24,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>cayenne-modeler-parent</artifactId>
diff --git a/pom.xml b/pom.xml
index 7f45930..fd39c74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
 	<groupId>org.apache.cayenne</groupId>
 	<name>cayenne-parent: Top-level parent of Cayenne modules</name>
 
-	<version>4.2.M1-SNAPSHOT</version>
+	<version>4.2.M2-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<description>
@@ -1738,7 +1738,7 @@
 			</activation>
 			<properties>
 				<javadoc.doclint>none</javadoc.doclint>
-				<javadoc.additionalOptions />
+				<javadoc.additionalOptions>--no-module-directories</javadoc.additionalOptions>
 			</properties>
 		</profile>
 		<profile>
@@ -1747,7 +1747,7 @@
 				<jdk>[9,)</jdk>
 			</activation>
 			<properties>
-				<javadoc.additionalOptions>-html5</javadoc.additionalOptions>
+				<javadoc.additionalOptions>-html5 --no-module-directories</javadoc.additionalOptions>
 			</properties>
 		</profile>
 	</profiles>
diff --git a/tutorials/pom.xml b/tutorials/pom.xml
index 4cbd065..334e2fc 100644
--- a/tutorials/pom.xml
+++ b/tutorials/pom.xml
@@ -22,7 +22,7 @@
 	<parent>
 		<groupId>org.apache.cayenne</groupId>
 		<artifactId>cayenne-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 	
 	<groupId>org.apache.cayenne.tutorials</groupId>
diff --git a/tutorials/tutorial-rop-client-http2/pom.xml b/tutorials/tutorial-rop-client-http2/pom.xml
index 204f870..f9b64b8 100644
--- a/tutorials/tutorial-rop-client-http2/pom.xml
+++ b/tutorials/tutorial-rop-client-http2/pom.xml
@@ -18,7 +18,7 @@
     <parent>
         <artifactId>cayenne-tutorials-parent</artifactId>
         <groupId>org.apache.cayenne.tutorials</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <artifactId>tutorial-rop-client-http2</artifactId>
diff --git a/tutorials/tutorial-rop-client/pom.xml b/tutorials/tutorial-rop-client/pom.xml
index 1466726..a7f1626 100644
--- a/tutorials/tutorial-rop-client/pom.xml
+++ b/tutorials/tutorial-rop-client/pom.xml
@@ -18,7 +18,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.tutorials</groupId>
 		<artifactId>cayenne-tutorials-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>tutorial-rop-client</artifactId>
diff --git a/tutorials/tutorial-rop-server-http2/pom.xml b/tutorials/tutorial-rop-server-http2/pom.xml
index 67bb2a2..e15d62b 100644
--- a/tutorials/tutorial-rop-server-http2/pom.xml
+++ b/tutorials/tutorial-rop-server-http2/pom.xml
@@ -18,7 +18,7 @@
     <parent>
         <artifactId>cayenne-tutorials-parent</artifactId>
         <groupId>org.apache.cayenne.tutorials</groupId>
-        <version>4.2.M1-SNAPSHOT</version>
+        <version>4.2.M2-SNAPSHOT</version>
     </parent>
 
     <artifactId>tutorial-rop-server-http2</artifactId>
diff --git a/tutorials/tutorial-rop-server/pom.xml b/tutorials/tutorial-rop-server/pom.xml
index 48d51ed..7d30984 100644
--- a/tutorials/tutorial-rop-server/pom.xml
+++ b/tutorials/tutorial-rop-server/pom.xml
@@ -18,7 +18,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.tutorials</groupId>
 		<artifactId>cayenne-tutorials-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>tutorial-rop-server</artifactId>
diff --git a/tutorials/tutorial/pom.xml b/tutorials/tutorial/pom.xml
index cfa9fcb..8574e3f 100644
--- a/tutorials/tutorial/pom.xml
+++ b/tutorials/tutorial/pom.xml
@@ -18,7 +18,7 @@
 	<parent>
 		<groupId>org.apache.cayenne.tutorials</groupId>
 		<artifactId>cayenne-tutorials-parent</artifactId>
-		<version>4.2.M1-SNAPSHOT</version>
+		<version>4.2.M2-SNAPSHOT</version>
 	</parent>
 
 	<artifactId>tutorial</artifactId>