You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/09/20 19:00:35 UTC

(cayenne) branch master updated (c20f29168 -> c5d1d5409)

This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git


    from c20f29168 CAY-3026 Parseable ObjectSelect / ColumnSelect
     new f803bff73 Moving docs code snippets into compiled tests
     new c5d1d5409 Moving docs code snippets into compiled tests

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CLAUDE.md                                          |  35 +++
 .../org/apache/cayenne/query/ProcedureCall.java    |   9 +-
 .../org/apache/cayenne/query/ProcedureCallIT.java  |  23 ++
 docs/cayenne-guide/pom.xml                         |  64 ++++
 .../_cayenne-guide/configurationProperties.adoc    |  24 +-
 .../asciidoc/_cayenne-guide/part1/mapping.adoc     |   2 +-
 .../asciidoc/_cayenne-guide/part2/commitLog.adoc   |  69 +---
 .../asciidoc/_cayenne-guide/part2/customize.adoc   | 277 +++-------------
 .../asciidoc/_cayenne-guide/part2/expressions.adoc | 152 +++------
 .../asciidoc/_cayenne-guide/part2/lifecycle.adoc   | 164 ++--------
 .../_cayenne-guide/part2/objectContext.adoc        | 145 +++------
 .../asciidoc/_cayenne-guide/part2/orderings.adoc   |  10 +-
 .../_cayenne-guide/part2/queries/custom.adoc       |  37 +--
 .../_cayenne-guide/part2/queries/ejbql.adoc        |  51 ++-
 .../_cayenne-guide/part2/queries/mapped.adoc       |  16 +-
 .../part2/queries/objectselect-parsed.adoc         | 148 +++------
 .../_cayenne-guide/part2/queries/objectselect.adoc |  68 ++--
 .../_cayenne-guide/part2/queries/procedure.adoc    |  27 +-
 .../_cayenne-guide/part2/queries/selectbyid.adoc   |  31 +-
 .../asciidoc/_cayenne-guide/part2/queries/sql.adoc |  31 +-
 .../_cayenne-guide/part2/queries/sqlscripting.adoc |  22 +-
 .../asciidoc/_cayenne-guide/part2/starting.adoc    |  92 +++---
 .../docs/asciidoc/_cayenne-guide/part2/tuning.adoc | 116 ++-----
 .../_cayenne-guide/part5/cacheInvalidation.adoc    |  24 +-
 .../docs/asciidoc/_cayenne-guide/part5/crypto.adoc |  25 +-
 .../docs/asciidoc/_cayenne-guide/part5/jCache.adoc |  18 +-
 .../src/docs/asciidoc/cayenne-guide.adoc           |   3 +
 .../java/org/apache/cayenne/docs/BaseTest.java     | 155 +++++++++
 .../apache/cayenne/docs/CacheInvalidationTest.java |  31 +-
 .../org/apache/cayenne/docs/CommitLogTest.java     | 138 ++++++++
 .../org/apache/cayenne/docs/CryptoExamples.java    |  71 +++++
 .../org/apache/cayenne/docs/CustomQueryTest.java   |  34 +-
 .../org/apache/cayenne/docs/CustomizeTest.java     | 175 +++++++++++
 .../org/apache/cayenne/docs/EJBQLQueryTest.java    | 149 +++++++++
 .../org/apache/cayenne/docs/ExpressionsTest.java   | 271 ++++++++++++++++
 .../org/apache/cayenne/docs/JCacheExamples.java    |  60 ++++
 .../org/apache/cayenne/docs/LifecycleTest.java     |  67 ++++
 .../org/apache/cayenne/docs/MappedQueryTest.java   |  52 +--
 .../org/apache/cayenne/docs/ObjectContextTest.java | 330 +++++++++++++++++++
 .../cayenne/docs/ObjectSelectParsedTest.java       | 349 +++++++++++++++++++++
 .../org/apache/cayenne/docs/ObjectSelectTest.java  | 159 ++++++++++
 .../org/apache/cayenne/docs/OrderingsTest.java     |  46 +--
 .../apache/cayenne/docs/ProcedureCallExamples.java |  71 +++++
 .../org/apache/cayenne/docs/SQLScriptingTest.java  |  76 +++++
 .../org/apache/cayenne/docs/SQLSelectTest.java     |  81 +++++
 .../org/apache/cayenne/docs/SelectByIdTest.java    | 106 +++++++
 .../java/org/apache/cayenne/docs/StartingTest.java | 139 ++++++++
 .../java/org/apache/cayenne/docs/TuningTest.java   | 242 ++++++++++++++
 .../CustomInvalidationHandler.java                 |  24 +-
 .../cayenne/docs/cacheinvalidation/MyEntity.java   |  12 +-
 .../cayenne/docs/commitlog/AuditListener.java      |  24 +-
 .../cayenne/docs/commitlog/MyEntityFactory.java    |  42 ++-
 .../org/apache/cayenne/docs/commitlog/Order.java   |  10 +-
 .../cayenne/docs/commitlog/attributes/Order.java   |  17 +-
 .../org/apache/cayenne/docs/crypto/MyClass.java    |   9 +-
 .../cayenne/docs/crypto/MyClassBytesConverter.java |  19 +-
 .../apache/cayenne/docs/crypto/MyColumnMapper.java |  16 +-
 .../cayenne/docs/customize/DoubleArrayType.java    |  60 ++--
 .../org/apache/cayenne/docs/customize/Money.java   |  22 +-
 .../docs/customize/MoneyValueObjectType.java       |  32 +-
 .../docs/customize/MyDbAdapterDetector.java        |  32 +-
 .../cayenne/docs/customize/MyExtensionsModule.java |  15 +-
 .../docs/customquery/MyDelegatingQuery.java        |  37 +--
 .../apache/cayenne/docs/customquery/MyQuery.java   |  53 ++--
 .../java/org/apache/cayenne/docs/di/Module1.java   |  15 +-
 .../java/org/apache/cayenne/docs/di/Service1.java  |   9 +-
 .../org/apache/cayenne/docs/di/Service1Impl.java   |  14 +-
 .../apache/cayenne/docs/di/Service1Provider.java   |   9 +-
 .../java/org/apache/cayenne/docs/di/Service2.java  |   9 +-
 .../org/apache/cayenne/docs/di/Service2Impl.java   |  13 +-
 .../cayenne/docs/di/constructor/Service1Impl.java  |  22 +-
 .../cayenne/docs/lifecycle/AnnotationExamples.java |  37 ++-
 .../docs/lifecycle/CommittedObjectCounter.java     |  59 ++++
 .../apache/cayenne/docs/lifecycle/Listener1.java   |  13 +-
 .../apache/cayenne/docs/lifecycle/Listener2.java   |  22 +-
 .../apache/cayenne/docs/lifecycle/Listener3.java   |  13 +-
 .../apache/cayenne/docs/lifecycle/MyEntity.java    |  10 +-
 .../apache/cayenne/docs/lifecycle/MyEntity1.java   |   9 +-
 .../apache/cayenne/docs/lifecycle/MyEntity2.java   |   9 +-
 .../org/apache/cayenne/docs/lifecycle/Order.java   |  16 +-
 .../cayenne/docs/lifecycle/OrderListener.java      |  16 +-
 .../org/apache/cayenne/docs/lifecycle/Tag.java     |   6 +-
 .../apache/cayenne/docs/lifecycle/_MyEntity.java   |  12 +-
 .../apache/cayenne/docs/lifecycle/_MyEntity1.java  |   8 +-
 .../apache/cayenne/docs/lifecycle/_MyEntity2.java  |   8 +-
 .../org/apache/cayenne/docs/lifecycle/_Order.java  |  21 +-
 .../cayenne/docs/lifecycle/modeler/Order.java      |  10 +-
 .../cayenne/docs/lifecycle/modeler/_Order.java     |  14 +-
 .../org/apache/cayenne/docs/persistent/Artist.java |   7 +-
 .../apache/cayenne/docs/persistent/CompoundPk.java |   7 +-
 .../apache/cayenne/docs}/persistent/Gallery.java   |   4 +-
 .../apache/cayenne/docs}/persistent/Painting.java  |   4 +-
 .../cayenne/docs/persistent/auto/_Artist.java      |  62 +++-
 .../cayenne/docs/persistent/auto/_CompoundPk.java  |  50 +--
 .../cayenne/docs/persistent}/auto/_Gallery.java    |  47 ++-
 .../cayenne/docs/persistent}/auto/_Painting.java   |  65 ++--
 .../org/apache/cayenne/docs/tuning/MyModule.java   |  16 +-
 .../src/test/resources/cayenne-project.xml         |   2 +-
 .../resources/com/example/cayenne-mydomain.xml     |   1 -
 .../test/resources/com/example/cayenne-project.xml |   1 -
 docs/cayenne-guide/src/test/resources/docs.map.xml |  71 +++++
 .../org/apache/cayenne/docs/expressions.txt        |  63 ++++
 .../apache/cayenne/docs/objectselect-parsed.txt    |  85 +++++
 .../test/resources/org/foo/cayenne-library1.xml    |   1 -
 .../test/resources/org/foo/cayenne-library2.xml    |   1 -
 .../_getting-started-db-first/part4-java-code.adoc |  34 +-
 .../docs/asciidoc/getting-started-db-first.adoc    |   2 +
 .../java/org/apache/cayenne/tutorial/Main.java     |  59 ++++
 .../cayenne/tutorial/persistent/Artist.java}       |   4 +-
 .../cayenne/tutorial/persistent/Gallery.java       |   0
 .../cayenne/tutorial/persistent/Painting.java      |   0
 .../cayenne/tutorial/persistent/auto/_Artist.java  |   4 +
 .../cayenne/tutorial/persistent/auto/_Gallery.java |   4 +
 .../tutorial/persistent/auto/_Painting.java        |   8 +-
 .../src/main/resources/cayenne-project.xml         |   0
 .../src/main/resources/datamap.map.xml             |  26 +-
 docs/getting-started-guide/pom.xml                 |  15 +
 .../asciidoc/_getting-started-guide/delete.adoc    |  12 +-
 .../_getting-started-guide/java-classes.adoc       |   7 +-
 .../_getting-started-guide/object-context.adoc     |  36 +--
 .../_getting-started-guide/select-query.adoc       |  15 +-
 .../_getting-started-guide/starting-project.adoc   |  33 +-
 .../src/docs/asciidoc/getting-started-guide.adoc   |   2 +-
 .../src/docs/asciidoc/images/base-datanode.png     | Bin 92592 -> 0 bytes
 .../src/docs/asciidoc/images/icon-node.png         | Bin 384 -> 0 bytes
 .../cayenne/tutorial => example/cayenne}/Main.java |  51 ++-
 .../cayenne}/persistent/Artist.java                |   4 +-
 .../cayenne}/persistent/Datamap.java               |   4 +-
 .../cayenne}/persistent/Gallery.java               |   4 +-
 .../cayenne}/persistent/Painting.java              |   4 +-
 .../cayenne}/persistent/auto/_Artist.java          |   8 +-
 .../cayenne}/persistent/auto/_Datamap.java         |   2 +-
 .../cayenne}/persistent/auto/_Gallery.java         |   8 +-
 .../cayenne}/persistent/auto/_Painting.java        |  14 +-
 .../src/main/resources/datamap.map.xml             |   8 +-
 .../test/java/org/example/cayenne/MainTest.java    |  16 +-
 docs/pom.xml                                       |   6 +
 137 files changed, 4289 insertions(+), 1871 deletions(-)
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/BaseTest.java
 copy cayenne/src/test/java/org/apache/cayenne/access/DefaultDataRowStoreFactoryIT.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CacheInvalidationTest.java (60%)
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CommitLogTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CryptoExamples.java
 copy cayenne/src/test/java/org/apache/cayenne/exp/parser/ASTCurrentDateTest.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomQueryTest.java (60%)
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomizeTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/EJBQLQueryTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/JCacheExamples.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/LifecycleTest.java
 copy cayenne/src/test/java/org/apache/cayenne/query/EntityResultSegmentTest.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/MappedQueryTest.java (50%)
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectContextTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectParsedTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectTest.java
 copy cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/filter/NamePatternMatcherTest.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/OrderingsTest.java (53%)
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLScriptingTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLSelectTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SelectByIdTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/TuningTest.java
 copy cayenne-cache-invalidation/src/main/java/org/apache/cayenne/cache/invalidation/InvalidationHandler.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/CustomInvalidationHandler.java (66%)
 copy cayenne-cache-invalidation/src/test/java/org/apache/cayenne/cache/invalidation/db/E1.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/MyEntity.java (82%)
 copy cayenne/src/main/java/org/apache/cayenne/commitlog/CommitLogListener.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/AuditListener.java (58%)
 copy cayenne/src/main/java/org/apache/cayenne/configuration/xml/DefaultHandlerFactory.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/MyEntityFactory.java (50%)
 copy cayenne/src/test/java/org/apache/cayenne/commitlog/db/Auditable1.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/Order.java (85%)
 copy cayenne/src/test/java/org/apache/cayenne/commitlog/db/Auditable4.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/attributes/Order.java (63%)
 copy cayenne-gradle-plugin/src/test/resources/org/apache/cayenne/tools/cgen_default_config.gradle => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClass.java (92%)
 copy cayenne/src/main/java/org/apache/cayenne/access/sqlbuilder/sqltree/BitwiseNotNode.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClassBytesConverter.java (69%)
 copy cayenne-crypto/src/main/java/org/apache/cayenne/crypto/map/ColumnMapper.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyColumnMapper.java (78%)
 copy cayenne/src/main/java/org/apache/cayenne/access/types/ExtendedType.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/DoubleArrayType.java (56%)
 copy cayenne-di/src/main/java/org/apache/cayenne/di/spi/InstanceProvider.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/Money.java (76%)
 copy cayenne/src/main/java/org/apache/cayenne/access/types/BigIntegerValueType.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MoneyValueObjectType.java (61%)
 copy cayenne/src/test/java/org/apache/cayenne/unit/dba/H2TestDbAdapter.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyDbAdapterDetector.java (66%)
 copy cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyExtensionsModule.java (79%)
 copy cayenne/src/main/java/org/apache/cayenne/dba/frontbase/FrontBaseActionBuilder.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyDelegatingQuery.java (61%)
 copy cayenne/src/test/java/org/apache/cayenne/query/MockQuery.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyQuery.java (51%)
 copy cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Module1.java (79%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockInterface1.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1.java (88%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockInterface1_Decorator1.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Impl.java (81%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockInterface1Provider.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Provider.java (84%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockInterface1.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2.java (88%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockImplementation1Alt.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2Impl.java (83%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockImplementation2_Constructor.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/constructor/Service1Impl.java (70%)
 copy cayenne-cache-invalidation/src/main/java/org/apache/cayenne/cache/invalidation/InvalidationHandler.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/AnnotationExamples.java (54%)
 create mode 100644 docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/CommittedObjectCounter.java
 copy cayenne/src/main/java/org/apache/cayenne/access/NoopParentAttachmentStrategy.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener1.java (80%)
 copy cayenne/src/main/java/org/apache/cayenne/access/ToManySetFault.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener2.java (69%)
 copy cayenne/src/main/java/org/apache/cayenne/access/NoopParentAttachmentStrategy.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener3.java (79%)
 copy cayenne-gradle-plugin/src/test/resources/org/apache/cayenne/tools/cgen_default_config.gradle => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity.java (91%)
 copy cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/Table2.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity1.java (88%)
 copy cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/Table2.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity2.java (88%)
 copy cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_HMAC_IT.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Order.java (79%)
 copy cayenne-crypto/src/test/java/org/apache/cayenne/crypto/Runtime_AES128_GZIP_HMAC_IT.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/OrderListener.java (76%)
 copy cayenne/src/test/java/org/apache/cayenne/testdo/testmap/annotations/Tag1.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Tag.java (91%)
 copy cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/value/BytesConverter.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity.java (79%)
 copy cayenne/src/test/java/org/apache/cayenne/testdo/testmap/ArtPersistentObject.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity1.java (79%)
 copy cayenne/src/test/java/org/apache/cayenne/testdo/testmap/ArtPersistentObject.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity2.java (79%)
 copy cayenne/src/test/java/org/apache/cayenne/testdo/testmap/ArtPersistentObject.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_Order.java (63%)
 copy cayenne-di/src/test/java/org/apache/cayenne/di/mock/MockImplementation5.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/Order.java (83%)
 copy cayenne-crypto/src/main/java/org/apache/cayenne/crypto/transformer/MapTransformer.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/_Order.java (80%)
 copy cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/Table4.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Artist.java (87%)
 copy cayenne-crypto/src/test/java/org/apache/cayenne/crypto/db/Table4.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/CompoundPk.java (86%)
 copy docs/{getting-started-guide/src/main/java/org/apache/cayenne/tutorial => cayenne-guide/src/test/java/org/apache/cayenne/docs}/persistent/Gallery.java (90%)
 copy docs/{getting-started-guide/src/main/java/org/apache/cayenne/tutorial => cayenne-guide/src/test/java/org/apache/cayenne/docs}/persistent/Painting.java (90%)
 copy cayenne/src/test/java/org/apache/cayenne/testdo/testmap/auto/_ROArtist.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Artist.java (52%)
 copy cayenne/src/test/java/org/apache/cayenne/testdo/compound/auto/_CharFkTestEntity.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_CompoundPk.java (52%)
 copy {cayenne/src/test/java/org/apache/cayenne/testdo/testmap => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent}/auto/_Gallery.java (70%)
 copy {cayenne/src/test/java/org/apache/cayenne/testdo/testmap => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent}/auto/_Painting.java (65%)
 copy cayenne-velocity/src/main/java/org/apache/cayenne/velocity/VelocityModule.java => docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/tuning/MyModule.java (76%)
 copy cayenne/src/test/resources/cayenne-lob.xml => docs/cayenne-guide/src/test/resources/cayenne-project.xml (93%)
 copy cayenne/src/test/resources/cayenne-lob.xml => docs/cayenne-guide/src/test/resources/com/example/cayenne-mydomain.xml (94%)
 copy cayenne/src/test/resources/cayenne-lob.xml => docs/cayenne-guide/src/test/resources/com/example/cayenne-project.xml (94%)
 create mode 100644 docs/cayenne-guide/src/test/resources/docs.map.xml
 create mode 100644 docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/expressions.txt
 create mode 100644 docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/objectselect-parsed.txt
 copy cayenne/src/test/resources/cayenne-lob.xml => docs/cayenne-guide/src/test/resources/org/foo/cayenne-library1.xml (94%)
 copy cayenne/src/test/resources/cayenne-lob.xml => docs/cayenne-guide/src/test/resources/org/foo/cayenne-library2.xml (94%)
 create mode 100644 docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/Main.java
 copy docs/{getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java => getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java} (90%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java (100%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java (100%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java (94%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java (92%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java (85%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/resources/cayenne-project.xml (100%)
 copy docs/{getting-started-guide => getting-started-db-first}/src/main/resources/datamap.map.xml (70%)
 delete mode 100644 docs/getting-started-guide/src/docs/asciidoc/images/base-datanode.png
 delete mode 100755 docs/getting-started-guide/src/docs/asciidoc/images/icon-node.png
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/Main.java (75%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/Artist.java (94%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/Datamap.java (91%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/Gallery.java (90%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/Painting.java (90%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/auto/_Artist.java (91%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/auto/_Datamap.java (83%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/auto/_Gallery.java (89%)
 rename docs/getting-started-guide/src/main/java/org/{apache/cayenne/tutorial => example/cayenne}/persistent/auto/_Painting.java (81%)
 copy cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/reverse/dbimport/ExcludeRelationship.java => docs/getting-started-guide/src/test/java/org/example/cayenne/MainTest.java (79%)


(cayenne) 01/02: Moving docs code snippets into compiled tests

Posted by aa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit f803bff73fc920894bb209432c6614586dcd4471
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Sun Sep 20 14:44:14 2026 -0400

    Moving docs code snippets into compiled tests
---
 CLAUDE.md                                          |  33 ++
 docs/cayenne-guide/pom.xml                         |  64 ++++
 .../asciidoc/_cayenne-guide/part2/commitLog.adoc   |  69 +---
 .../asciidoc/_cayenne-guide/part2/customize.adoc   | 275 +++-------------
 .../asciidoc/_cayenne-guide/part2/expressions.adoc | 123 ++------
 .../asciidoc/_cayenne-guide/part2/lifecycle.adoc   | 164 ++--------
 .../_cayenne-guide/part2/objectContext.adoc        | 145 +++------
 .../asciidoc/_cayenne-guide/part2/orderings.adoc   |  10 +-
 .../_cayenne-guide/part2/queries/custom.adoc       |  37 +--
 .../_cayenne-guide/part2/queries/ejbql.adoc        |  47 ++-
 .../_cayenne-guide/part2/queries/mapped.adoc       |  14 +-
 .../part2/queries/objectselect-parsed.adoc         | 148 +++------
 .../_cayenne-guide/part2/queries/objectselect.adoc |  68 ++--
 .../_cayenne-guide/part2/queries/procedure.adoc    |  27 +-
 .../_cayenne-guide/part2/queries/selectbyid.adoc   |  31 +-
 .../asciidoc/_cayenne-guide/part2/queries/sql.adoc |  31 +-
 .../_cayenne-guide/part2/queries/sqlscripting.adoc |  22 +-
 .../asciidoc/_cayenne-guide/part2/starting.adoc    |  61 +---
 .../docs/asciidoc/_cayenne-guide/part2/tuning.adoc | 116 ++-----
 .../_cayenne-guide/part5/cacheInvalidation.adoc    |  24 +-
 .../docs/asciidoc/_cayenne-guide/part5/crypto.adoc |  25 +-
 .../docs/asciidoc/_cayenne-guide/part5/jCache.adoc |  18 +-
 .../src/docs/asciidoc/cayenne-guide.adoc           |   3 +
 .../java/org/apache/cayenne/docs/BaseTest.java     | 155 +++++++++
 .../apache/cayenne/docs/CacheInvalidationTest.java |  44 +++
 .../org/apache/cayenne/docs/CommitLogTest.java     | 138 ++++++++
 .../org/apache/cayenne/docs/CryptoExamples.java    |  71 +++++
 .../org/apache/cayenne/docs/CustomQueryTest.java   |  46 +++
 .../org/apache/cayenne/docs/CustomizeTest.java     | 175 +++++++++++
 .../org/apache/cayenne/docs/EJBQLQueryTest.java    | 149 +++++++++
 .../org/apache/cayenne/docs/ExpressionsTest.java   | 236 ++++++++++++++
 .../org/apache/cayenne/docs/JCacheExamples.java    |  60 ++++
 .../org/apache/cayenne/docs/LifecycleTest.java     |  67 ++++
 .../org/apache/cayenne/docs/MappedQueryTest.java   |  60 ++++
 .../org/apache/cayenne/docs/ObjectContextTest.java | 330 +++++++++++++++++++
 .../cayenne/docs/ObjectSelectParsedTest.java       | 349 +++++++++++++++++++++
 .../org/apache/cayenne/docs/ObjectSelectTest.java  | 159 ++++++++++
 .../org/apache/cayenne/docs/OrderingsTest.java     |  57 ++++
 .../apache/cayenne/docs/ProcedureCallExamples.java |  72 +++++
 .../org/apache/cayenne/docs/SQLScriptingTest.java  |  76 +++++
 .../org/apache/cayenne/docs/SQLSelectTest.java     |  81 +++++
 .../org/apache/cayenne/docs/SelectByIdTest.java    | 106 +++++++
 .../java/org/apache/cayenne/docs/StartingTest.java |  75 +++++
 .../java/org/apache/cayenne/docs/TuningTest.java   | 242 ++++++++++++++
 .../CustomInvalidationHandler.java                 |  36 +++
 .../cayenne/docs/cacheinvalidation/MyEntity.java   |  29 ++
 .../cayenne/docs/commitlog/AuditListener.java      |  40 +++
 .../cayenne/docs/commitlog/MyEntityFactory.java    |  54 ++++
 .../org/apache/cayenne/docs/commitlog/Order.java   |  29 ++
 .../cayenne/docs/commitlog/attributes/Order.java   |  34 ++
 .../org/apache/cayenne/docs/crypto/MyClass.java    |  22 ++
 .../cayenne/docs/crypto/MyClassBytesConverter.java |  36 +++
 .../apache/cayenne/docs/crypto/MyColumnMapper.java |  30 ++
 .../cayenne/docs/customize/DoubleArrayType.java    |  92 ++++++
 .../org/apache/cayenne/docs/customize/Money.java   |  37 +++
 .../docs/customize/MoneyValueObjectType.java       |  53 ++++
 .../docs/customize/MyDbAdapterDetector.java        |  35 +++
 .../cayenne/docs/customize/MyExtensionsModule.java |  30 ++
 .../docs/customquery/MyDelegatingQuery.java        |  42 +++
 .../apache/cayenne/docs/customquery/MyQuery.java   |  55 ++++
 .../java/org/apache/cayenne/docs/di/Module1.java   |  32 ++
 .../java/org/apache/cayenne/docs/di/Service1.java  |  25 ++
 .../org/apache/cayenne/docs/di/Service1Impl.java   |  32 ++
 .../apache/cayenne/docs/di/Service1Provider.java   |  29 ++
 .../java/org/apache/cayenne/docs/di/Service2.java  |  25 ++
 .../org/apache/cayenne/docs/di/Service2Impl.java   |  29 ++
 .../cayenne/docs/di/constructor/Service1Impl.java  |  38 +++
 .../cayenne/docs/lifecycle/AnnotationExamples.java |  47 +++
 .../docs/lifecycle/CommittedObjectCounter.java     |  59 ++++
 .../apache/cayenne/docs/lifecycle/Listener1.java   |  32 ++
 .../apache/cayenne/docs/lifecycle/Listener2.java   |  38 +++
 .../apache/cayenne/docs/lifecycle/Listener3.java   |  32 ++
 .../apache/cayenne/docs/lifecycle/MyEntity.java    |  23 ++
 .../apache/cayenne/docs/lifecycle/MyEntity1.java   |  26 ++
 .../apache/cayenne/docs/lifecycle/MyEntity2.java   |  26 ++
 .../org/apache/cayenne/docs/lifecycle/Order.java   |  31 ++
 .../cayenne/docs/lifecycle/OrderListener.java      |  33 ++
 .../org/apache/cayenne/docs/lifecycle/Tag.java     |  32 ++
 .../apache/cayenne/docs/lifecycle/_MyEntity.java   |  27 ++
 .../apache/cayenne/docs/lifecycle/_MyEntity1.java  |  27 ++
 .../apache/cayenne/docs/lifecycle/_MyEntity2.java  |  27 ++
 .../org/apache/cayenne/docs/lifecycle/_Order.java  |  42 +++
 .../cayenne/docs/lifecycle/modeler/Order.java      |  29 ++
 .../cayenne/docs/lifecycle/modeler/_Order.java     |  27 ++
 .../org/apache/cayenne/docs/persistent/Artist.java |  25 ++
 .../apache/cayenne/docs/persistent/CompoundPk.java |  25 ++
 .../apache/cayenne/docs/persistent/Gallery.java    |  25 ++
 .../apache/cayenne/docs/persistent/Painting.java   |  25 ++
 .../cayenne/docs/persistent/auto/_Artist.java      | 126 ++++++++
 .../cayenne/docs/persistent/auto/_CompoundPk.java  |  84 +++++
 .../cayenne/docs/persistent/auto/_Gallery.java     | 107 +++++++
 .../cayenne/docs/persistent/auto/_Painting.java    | 136 ++++++++
 .../org/apache/cayenne/docs/tuning/MyModule.java   |  35 +++
 .../src/test/resources/cayenne-project.xml         |   7 +
 .../resources/com/example/cayenne-mydomain.xml     |   6 +
 .../test/resources/com/example/cayenne-project.xml |   6 +
 docs/cayenne-guide/src/test/resources/docs.map.xml |  71 +++++
 .../org/apache/cayenne/docs/expressions.txt        |  63 ++++
 .../apache/cayenne/docs/objectselect-parsed.txt    |  85 +++++
 .../test/resources/org/foo/cayenne-library1.xml    |   6 +
 .../test/resources/org/foo/cayenne-library2.xml    |   6 +
 .../_getting-started-db-first/part4-java-code.adoc |   1 +
 docs/getting-started-guide/pom.xml                 |  15 +
 .../asciidoc/_getting-started-guide/delete.adoc    |  12 +-
 .../_getting-started-guide/select-query.adoc       |  15 +-
 .../java/org/apache/cayenne/tutorial/Main.java     |  18 +-
 .../java/org/apache/cayenne/tutorial/MainTest.java |  32 ++
 docs/pom.xml                                       |   6 +
 108 files changed, 5541 insertions(+), 1121 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 6ed2ae128..6c9e4fde8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -101,6 +101,39 @@ Test naming: `*Test.java` = unit tests (Surefire), `*IT.java` = integration test
 All new tests must use JUnit 5. Test classes and methods must be `public`. Method names must not use the `test` prefix (e.g. `someFeature()` not `testSomeFeature()`).
 Do not add `@since` tags to test classes — version tags are only meaningful on public API.
 
+## Documentation Code Examples
+
+The docs live in `docs/*` as Asciidoc. Java examples must not be typed inline in the `.adoc` files. They are tagged
+regions of compiled (and, where possible, run) code, included in the docs by tag, so that they can not get out of sync
+with the Cayenne API:
+
+- `docs/cayenne-guide` — examples are in the JUnit tests under `src/test/java/org/apache/cayenne/docs` (`BaseTest`
+  provides a runtime over an in-memory DB and the Artist / Painting / Gallery model). Whole-class examples are separate
+  files in the subpackages (`lifecycle`, `customize`, etc.). Non-Java examples that can be checked (QL queries,
+  expression Strings) are tagged in `src/test/resources/org/apache/cayenne/docs/*.txt` and read by the tests via
+  `BaseTest.example(..)`.
+- `docs/getting-started-guide` — examples are in the tutorial app under `src/main/java`, that is run by `MainTest`.
+
+```java
+// tag::where[]
+List<Artist> objects = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.like("Pablo%")).select(context);
+// end::where[]
+```
+
+```asciidoc
+[source,java,indent=0]
+----
+include::{java-examples}/ObjectSelectTest.java[tags=where]
+----
+```
+
+The docs build fails if an included tag is missing, and the guide examples fail to compile if they use deprecated API.
+
+```bash
+# Build the guide and run its example tests only
+mvn verify -pl docs/cayenne-guide -am -Dtest='org/apache/cayenne/docs/**' -Dsurefire.failIfNoSpecifiedTests=false -DskipITs
+```
+
 ## `pom.xml` Style
 
 All POM plugins from submodules must be delcared in the parent module `<pluginManagement>`. All plugin versions should be 
diff --git a/docs/cayenne-guide/pom.xml b/docs/cayenne-guide/pom.xml
index 785585e04..0e2a2bae4 100644
--- a/docs/cayenne-guide/pom.xml
+++ b/docs/cayenne-guide/pom.xml
@@ -31,8 +31,72 @@
     <packaging>jar</packaging>
     <name>cayenne-guide: AsciiDoc - Cayenne Framework Guide</name>
 
+    <dependencies>
+
+        <!-- Test dependencies. Java code examples in the docs are included from the compiled tests -->
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-cache-invalidation</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-crypto</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.cayenne</groupId>
+            <artifactId>cayenne-jcache</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ehcache</groupId>
+            <artifactId>ehcache</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.xml.bind</groupId>
+            <artifactId>jaxb-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.sun.xml.bind</groupId>
+            <artifactId>jaxb-impl</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
     <build>
         <plugins>
+            <!-- The tests are the source of the code examples in the docs. Do not allow deprecated API there -->
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>default-testCompile</id>
+                        <configuration>
+                            <showWarnings>true</showWarnings>
+                            <compilerArgs>
+                                <arg>-Xlint:deprecation,removal</arg>
+                                <arg>-Werror</arg>
+                            </compilerArgs>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
             <plugin>
                 <groupId>org.asciidoctor</groupId>
                 <artifactId>asciidoctor-maven-plugin</artifactId>
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/commitLog.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/commitLog.adoc
index c47fda904..d5be9d547 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/commitLog.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/commitLog.adoc
@@ -29,48 +29,27 @@ To receive commit notifications, follow three steps.
 
 Mark every entity whose changes you want to capture with `@CommitLog`:
 
-[source,java]
+[source,java,indent=0]
 ----
-@CommitLog
-public class Order extends _Order {
-    // ...
-}
+include::{java-examples}/commitlog/Order.java[tags=content]
 ----
 
 By default every committed entity is included. To limit auditing to annotated entities only, call `commitLogAnnotationEntitiesOnly()` during configuration (see below).
 
 The annotation accepts optional attributes:
 
-[source,java]
+[source,java,indent=0]
 ----
-@CommitLog(
-    ignoredProperties = {"internalState"},         // exclude from change records
-    ignoreAttributes = false,                       // set true to skip all attributes
-    ignoreToOneRelationships = false,               // set true to skip all to-one relationships
-    ignoreToManyRelationships = false               // set true to skip all to-many relationships
-)
-public class Order extends _Order { ... }
+include::{java-examples}/commitlog/attributes/Order.java[tags=content]
 ----
 
 For properties containing sensitive data, annotate the getter with `@Confidential` instead of `ignoredProperties`. The value will appear in the `ChangeMap` as `*******` rather than the real value.
 
 ===== 2. Implement CommitLogListener
 
-[source,java]
+[source,java,indent=0]
 ----
-public class AuditListener implements CommitLogListener {
-
-    @Override
-    public void onPostCommit(ObjectContext originatingContext, ChangeMap changes) {
-        for (ObjectChange change : changes.getUniqueChanges()) {
-            switch (change.getType()) {
-                case INSERT -> System.out.println("Inserted: " + change.getPostCommitId());
-                case UPDATE -> System.out.println("Updated:  " + change.getPostCommitId());
-                case DELETE -> System.out.println("Deleted:  " + change.getPreCommitId());
-            }
-        }
-    }
-}
+include::{java-examples}/commitlog/AuditListener.java[tags=content]
 ----
 
 `ChangeMap` gives access to the full set of changes in one commit. Each `ObjectChange` carries:
@@ -84,35 +63,27 @@ Multiple listeners may be registered; all receive identical change data for the
 
 ===== 3. Register the listener
 
-[source,java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addConfig("cayenne-project.xml")
-        .addModule(binder -> CoreModule.extend(binder)
-                .addCommitLogListener(AuditListener.class))
-        .build();
+include::{java-examples}/CommitLogTest.java[tags=register]
 ----
 
 ==== Restricting to Annotated Entities
 
 To audit only entities marked with `@CommitLog`:
 
-[source,java]
+[source,java,indent=0]
 ----
-CoreModule.extend(binder)
-        .commitLogAnnotationEntitiesOnly()
-        .addCommitLogListener(AuditListener.class)
+include::{java-examples}/CommitLogTest.java[tags=annotationEntitiesOnly]
 ----
 
 ==== Dispatching Events Outside the Transaction
 
 By default, listeners are called within the database transaction, so a listener can write additional rows as part of the same commit. If your listener performs its own transaction (for example writing to a different database), call `excludeCommitLogFromTransaction()` to move the notification outside the main transaction:
 
-[source,java]
+[source,java,indent=0]
 ----
-CoreModule.extend(binder)
-        .excludeCommitLogFromTransaction()
-        .addCommitLogListener(AuditListener.class)
+include::{java-examples}/CommitLogTest.java[tags=excludeFromTransaction]
 ----
 
 IMPORTANT: `excludeCommitLogFromTransaction()` must be called *before* `addCommitLogListener()`.
@@ -121,20 +92,12 @@ IMPORTANT: `excludeCommitLogFromTransaction()` must be called *before* `addCommi
 
 For full control over which entities are included and how their metadata is computed, provide a custom `CommitLogEntityFactory`:
 
-[source,java]
+[source,java,indent=0]
 ----
-public class MyEntityFactory implements CommitLogEntityFactory {
-    @Override
-    public CommitLogEntity getEntity(ObjEntity entity) {
-        // return null to exclude the entity, or a CommitLogEntity instance to include it
-        ...
-    }
-}
+include::{java-examples}/commitlog/MyEntityFactory.java[tags=content]
 ----
 
-[source,java]
+[source,java,indent=0]
 ----
-CoreModule.extend(binder)
-        .commitLogEntityFactory(MyEntityFactory.class)
-        .addCommitLogListener(AuditListener.class)
+include::{java-examples}/CommitLogTest.java[tags=entityFactory]
 ----
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc
index 385aef99e..9398d52aa 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc
@@ -28,125 +28,72 @@ Cayenne DI probably has ~80% of the features expected in a DI container and has
 
 To have a working DI container, we need three things: service interfaces and classes, a module that describes service bindings, a container that loads the module, and resolves the dependencies. Let's start with service interfaces and classes:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public interface Service1 {
-    String getString();
-}
+include::{java-examples}/di/Service1.java[tags=content]
 ----
 
-[source, Java]
+[source,java,indent=0]
 ----
-public interface Service2 {
-    int getInt();
-}
+include::{java-examples}/di/Service2.java[tags=content]
 ----
 
 A service implementation using instance variable injection:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Service1Impl implements Service1 {
-    @Inject
-    private Service2 service2;
-
-    public String getString() {
-        return service2.getInt() + "_Service1Impl";
-    }
-}
+include::{java-examples}/di/Service1Impl.java[tags=content]
 ----
 
 Same thing, but using constructor injection:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Service1Impl implements Service1 {
-
-    private Service2 service2;
-
-    public Service1Impl(@Inject Service2 service2) {
-        this.service2 = service2;
-    }
-
-    public String getString() {
-        return service2.getInt() + "_Service1Impl";
-    }
-}
+include::{java-examples}/di/constructor/Service1Impl.java[tags=content]
 ----
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Service2Impl implements Service2 {
-    private int i;
-
-    public int getInt() {
-        return i++;
-    }
-}
+include::{java-examples}/di/Service2Impl.java[tags=content]
 ----
 
 Now let's create a module implementing `org.apache.cayenne.tutorial.di.Module` interface that will contain DI configuration. A module binds service objects to keys that are reference. Binder provided by container implements fluent API to connect the key to implementation, and to configure various binding options (the options, such as scope, are demonstrated later in this chapter). The simplest form of a key is a Java Class object representing service interface. Here is a module that bind [...]
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Module1 implements Module {
-
-    public void configure(Binder binder) {
-        binder.bind(Service1.class).to(Service1Impl.class);
-        binder.bind(Service2.class).to(Service2Impl.class);
-    }
-}
+include::{java-examples}/di/Module1.java[tags=content]
 ----
 
 Once we have at least one module, we can create a DI container. `org.apache.cayenne.di.Injector` is the container class in Cayenne:
 
-[source, Java]
+[source,java,indent=0]
 ----
-Injector injector = DIBootstrap.createInjector(new Module1());
+include::{java-examples}/CustomizeTest.java[tags=injector]
 ----
 
 Now that we have created the container, we can obtain services from it and call their methods:
 
-[source, Java]
+[source,java,indent=0]
 ----
-Service1 s1 = injector.getInstance(Service1.class);
-for (int i = 0; i < 5; i++) {
-    System.out.println("S1 String: " + s1.getString());
-}
+include::{java-examples}/CustomizeTest.java[tags=getInstance]
 ----
 
 This outputs the following lines, demonstrating that s1 was Service1Impl and Service2 injected into it was Service2Impl:
 
 [source]
 ----
-0_Service1Impl
-1_Service1Impl
-2_Service1Impl
-3_Service1Impl
-4_Service1Impl
+S1 String: 0_Service1Impl
+S1 String: 1_Service1Impl
+S1 String: 2_Service1Impl
+S1 String: 3_Service1Impl
+S1 String: 4_Service1Impl
 ----
 
 There are more flavors of bindings:
 
-[source, Java]
+[source,java,indent=0]
 ----
-// binding to instance - allowing user to create and configure instance
-// inside the module class
-binder.bind(Service2.class).toInstance(new Service2Impl());
-
-// binding to provider - delegating instance creation to a special
-// provider class
-binder.bind(Service1.class).toProvider(Service1Provider.class);
-
-// binding to provider instance
-binder.bind(Service1.class).toProviderInstance(new Service1Provider());
-
-// multiple bindings of the same type using Key
-// injection can reference the key name in annotation:
-// @Inject("i1")
-// private Service2 service2;
-binder.bind(Key.get(Service2.class, "i1")).to(Service2Impl.class);
-binder.bind(Key.get(Service2.class, "i2")).to(Service2Impl.class);
+include::{java-examples}/CustomizeTest.java[tags=bindings]
 ----
 
 
@@ -165,9 +112,9 @@ Such method should be implemented if a service needs to clean up some resources,
 
 Another useful scope is "no scope", meaning that every time a container is asked to provide a service instance for a given key, a new instance will be created and returned:
 
-[source, Java]
+[source,java,indent=0]
 ----
-binder.bind(Service2.class).to(Service2Impl.class).withoutScope();
+include::{java-examples}/CustomizeTest.java[tags=withoutScope]
 ----
 
 Users can also create their own scopes, e.g. a web application request scope or a session scope. Most often than not custom scopes
@@ -191,22 +138,14 @@ DI module (or multiple modules) that configures service overrides. This section
 including changing properties of the existing services, contributing services to standard service lists and maps, and overriding service implementations.
 All the code examples later in this section are assumed to be placed in an application module "configure" method:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class MyExtensionsModule implements Module {
-    public void configure(Binder binder) {
-        // customizations go here...
-    }
-}
+include::{java-examples}/customize/MyExtensionsModule.java[tags=content]
 ----
 
-[source, Java]
+[source,java,indent=0]
 ----
-Module extensions = new MyExtensionsModule();
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addConfig("com/example/cayenne-mydomain.xml")
-        .addModule(extensions)
-        .build();
+include::{java-examples}/CustomizeTest.java[tags=extensionsModule]
 ----
 
 ===== Changing Properties of Existing Services
@@ -238,20 +177,14 @@ A service collection allows things like appending a custom strategy to a list of
 E.g. an application that needs to install a custom DbAdapter for some database type may contribute an instance of custom
 DbAdapterDetector to a `o.a.c.configuration.runtime.DefaultDbAdapterFactory.detectors` list:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class MyDbAdapterDetector implements DbAdapterDetector {
-    public DbAdapter createAdapter(DatabaseMetaData md) throws SQLException {
-        // check if we support this database and return custom adapter
-        ...
-    }
-}
+include::{java-examples}/customize/MyDbAdapterDetector.java[tags=content]
 ----
 
-[source, Java]
+[source,java,indent=0]
 ----
-CoreModule.extend(binder)
-    .addAdapterDetector(MyDbAdapterDetector.class);
+include::{java-examples}/CustomizeTest.java[tags=adapterDetector]
 ----
 
 The names of built-in collections are listed in "Appendix B".
@@ -273,66 +206,23 @@ namely look in `org.apache.cayenne.configuration.runtime.CoreModule` - the main
 
 In order to use your custom data type you should implement `ValueObjectType` describing it in terms of some type already known to Cayenne (e.g. backed by system or user ExtendedType). Let's assume we want to support some data type called `Money`:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Money {
-    private BigDecimal value;
-
-    public Money(BigDecimal value) {
-        this.value = value;
-    }
-
-    public BigDecimal getValue() {
-        return value;
-    }
-
-    // .. some other business logic ..
-}
+include::{java-examples}/customize/Money.java[tags=content]
 ----
 
 Here is how `ValueObjectType` that will allow to store our `Money` class as `BigDecimal` can be implemented:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class MoneyValueObjectType implements ValueObjectType<Money, BigDecimal> {
-
-    @Override
-    public Class<BigDecimal> getTargetType() {
-        return BigDecimal.class;
-    }
-
-    @Override
-    public Class<Money> getValueType() {
-        return Money.class;
-    }
-
-    @Override
-    public Money toJavaObject(BigDecimal value) {
-        return new Money(value);
-    }
-
-    @Override
-    public BigDecimal fromJavaObject(Money object) {
-        return object.getValue();
-    }
-
-    @Override
-    public String toCacheKey(Money object) {
-        return object.getValue().toString();
-    }
-}
+include::{java-examples}/customize/MoneyValueObjectType.java[tags=content]
 ----
 
 Last step is to register this new type in `CayenneRuntime`:
 
-[source, Java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-    .addConfig("cayenne-project.xml")
-    .addModule(binder ->
-        CoreModule.extend(binder)
-            .addValueObjectType(MoneyValueObjectType.class))
-    .build();
+include::{java-examples}/CustomizeTest.java[tags=valueObjectType]
 ----
 
 More examples of implementation you can find in
@@ -346,85 +236,14 @@ JDBC specification defines a set of "standard" database column types (defined in
 
 For supporting non-standard type you should define it via an interface `org.apache.cayenne.access.types.ExtendedType`. An implementation must provide `ExtendedType.getClassName()` method that returns a fully qualified Java class name for the supported custom type, and a number of methods that convert data between JDBC and custom type. The following example demonstrates how to add a custom DoubleArrayType to store `java.lang.Double[]` as a custom string in a database:
 
-[source, Java]
-----
-/**
-* Defines methods to read Java objects from JDBC ResultSets and write as parameters of
-* PreparedStatements.
-*/
-public class DoubleArrayType implements ExtendedType {
-
-    private final String SEPARATOR = ",";
-
-    /**
-    * Returns a full name of Java class that this ExtendedType supports.
-    */
-    @Override
-    public String getClassName() {
-        return Double[].class.getCanonicalName();
-    }
-
-    /**
-    * Initializes a single parameter of a PreparedStatement with object value.
-    */
-    @Override
-    public void setJdbcObject(PreparedStatement statement, Object value,
-            int pos, int type, int scale) throws Exception {
-
-        String str = StringUtils.join((Double[]) value, SEPARATOR);
-        statement.setString(pos, str);
-    }
-
-
-    /**
-    * Reads an object from JDBC ResultSet column, converting it to class returned by
-    * 'getClassName' method.
-    *
-    * @throws Exception if read error occurred, or an object can't be converted to a
-    *             target Java class.
-    */
-    @Override
-    public Object materializeObject(ResultSet rs, int index, int type) throws Exception {
-        String[] str = rs.getString(index).split(SEPARATOR);
-        Double[] res = new Double[str.length];
-
-        for (int i = 0; i < str.length; i++) {
-            res[i] = Double.valueOf(str[i]);
-        }
-
-        return res;
-    }
-
-    /**
-    * Reads an object from a stored procedure OUT parameter, converting it to class
-    * returned by 'getClassName' method.
-    *
-    * @throws Exception if read error ocurred, or an object can't be converted to a
-    *             target Java class.
-    */
-    @Override
-    public Object materializeObject(CallableStatement rs, int index, int type) throws Exception {
-        String[] str = rs.getString(index).split(SEPARATOR);
-        Double[] res = new Double[str.length];
-
-        for (int i = 0; i < str.length; i++) {
-            res[i] = Double.valueOf(str[i]);
-        }
-
-        return res;
-    }
-}
-----
-
-[source, Java]
-----
-// add DoubleArrayType to list of user types
-CayenneRuntime runtime = CayenneRuntime.of()
-                .addConfig("cayenne-project.xml")
-                .addModule(binder ->
-                    CoreModule.contributeUserTypes(binder)
-                        .add(new DoubleArrayType()))
-                .build();
+[source,java,indent=0]
+----
+include::{java-examples}/customize/DoubleArrayType.java[tags=content]
+----
+
+[source,java,indent=0]
+----
+include::{java-examples}/CustomizeTest.java[tags=extendedType]
 ----
 
 ====== DbAdapters and Extended Types
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc
index ed44fd225..d60e922f2 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc
@@ -64,10 +64,9 @@ otherwise, an `Exception` will be thrown. The main use of aliases is to allow us
 While in most cases users are likely to rely on the API from the following section for expression creation, we'll start by showing String expressions, as this will help you understand the semantics. A Cayenne expression can be represented as a String, which can be converted to an expression object using the `ExpressionFactory.exp` static method. Here is an example:
 
 
-[source, java]
+[source,java,indent=0]
 ----
-String expString = "name like 'A%' and price < 1000";
-Expression exp = ExpressionFactory.exp(expString);
+include::{java-examples}/ExpressionsTest.java[tags=exp]
 ----
 
 This particular expression may be used to match Paintings whose names start with "A" and whose price is less than $1000. While this example is pretty self-explanatory, there are a few points worth mentioning. "name" and "price" here are object paths discussed earlier. As always, paths themselves are not attached to a specific root entity and can be applied to any entity that has similarly named attributes or relationships. So, when we say that this expression "may be used to match Painti [...]
@@ -84,101 +83,69 @@ name = \"ABC\"
 
 Case sensitivity. Expression operators are case sensitive and are usually lowercase. Complex words follow the Java camel-case style:
 
-[source, java]
+[source]
 ----
-// valid
-name likeIgnoreCase 'A%'
-
-// invalid - will throw a parse exception
-name LIKEIGNORECASE 'A%'
+include::{resource-examples}/expressions.txt[tags=case]
 ----
 
 Grouping with parenthesis:
 
 
-[source, java]
+[source]
 ----
-value = (price + 250.00) * 3
+include::{resource-examples}/expressions.txt[tags=grouping]
 ----
 
 Path prefixes. Object expressions are unquoted strings, optionally prefixed by `obj:` (usually they are not prefixed at all).
 Database expressions are always prefixed with `db:`. A special kind of prefix, not discussed yet, is `enum:` that prefixes an enumeration constant:
 
-[source, java]
+[source]
 ----
-// object path
-name = 'Salvador Dali'
-
-// same object path - a rarely used form
-obj:name = 'Salvador Dali'
-
-// multi-segment object path
-artist.name = 'Salvador Dali'
-
-// db path
-db:NAME = 'Salvador Dali'
-
-// enumeration constant
-name = enum:org.foo.EnumClass.VALUE1
+include::{resource-examples}/expressions.txt[tags=paths]
 ----
 
 Binary conditions are expressions that contain a path on the left, a value on the right, and some operation between them, such as equals like, etc. They can be used as qualifiers in SelectQueries:
 
-[source, java]
+[source]
 ----
-name like 'A%'
+include::{resource-examples}/expressions.txt[tags=like]
 ----
 
 
 Parameters. Expressions can contain named parameters (names that start with "$") that can be substituted with values either by name or by position. Parameterized expressions let you create reusable expression templates. Also, if an expression contains a complex object that doesn’t have a simple String representation (e.g. a `Date`, a `Persistent`, an `ObjectId`), parameterizing the expression is the only way to represent it as String. Here are examples of both positional and named parame [...]
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression template = ExpressionFactory.exp("name = $name");
-...
-// name binding
-Map p1 = Collections.singletonMap("name", "Salvador Dali");
-Expression qualifier1 = template.params(p1);
-...
-// positional binding
-Expression qualifier2 = template.paramsArray("Monet");
+include::{java-examples}/ExpressionsTest.java[tags=params]
 ----
 
 Positional binding is usually shorter. You can pass positional bindings to the `exp(..)` factory method (its second argument is a params vararg):
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression qualifier = ExpressionFactory.exp("name = $name", "Monet");
+include::{java-examples}/ExpressionsTest.java[tags=expWithParams]
 ----
 
 In parameterized expressions with a LIKE clause, SQL wildcards must be part of the values in the Map and not the expression string itself:
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression qualifier = ExpressionFactory.exp("name like $name", "Salvador%");
+include::{java-examples}/ExpressionsTest.java[tags=likeParam]
 ----
 
 When matching on a relationship, the value parameter must be either a Persistent object, an `org.apache.cayenne.ObjectId`, or a numeric ID value (for single column IDs). E.g.:
 
-[source, java]
+[source,java,indent=0]
 ----
-Artist dali = ... // assume we fetched this one already
-Expression qualifier = ExpressionFactory.exp("artist = $artist", dali);
+include::{java-examples}/ExpressionsTest.java[tags=objectParam]
 ----
 
 When you use positional binding, Cayenne expects values for all parameters to be present. Binding by name offers extra flexibility: sub-expressions with uninitialized parameters are automatically pruned from the expression. So, e.g., if certain parts of the expression criteria are not provided to the application, you can still build a valid expression
 
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression template = ExpressionFactory.exp("name like $name and dateOfBirth > $date");
-...
-Map p1 = Collections.singletonMap("name", "Salvador%");
-Expression qualifier1 = template.params(p1);
-
-// "qualifier1" is now "name like 'Salvador%'".
-// 'dateOfBirth > $date' condition was pruned, as no value was specified for
-// the $date parameter
+include::{java-examples}/ExpressionsTest.java[tags=pruning]
 ----
 
 Null handling. Handling of Java nulls as operands is no different handling from normal values. Instead of using special conditional operators, like SQL does (`IS NULL`, `IS NOT NULL`), "=" and "!=" expressions are used directly with null values. It is up to Cayenne to translate expressions with nulls to the valid SQL.
@@ -189,12 +156,9 @@ Null handling. Handling of Java nulls as operands is no different handling from
 
 Creating expressions from Strings is a powerful and dynamic approach, however a safer alternative is to use the Java API. It provides compile-time checking of the expression's validity. The API in question is provided by the `ExpressionFactory` class (that we've seen already), the Property class and the Expression class itself. `ExpressionFactory` contains a number of self-explanatory static methods that can be used to build expressions. E.g.:
 
-[source, java]
+[source,java,indent=0]
 ----
-// String expression: name like 'A%' and price < 1000
-Expression e1 = ExpressionFactory.likeExp("name", "A%");
-Expression e2 = ExpressionFactory.lessExp("price", 1000);
-Expression finalExp = e1.andExp(e2);
+include::{java-examples}/ExpressionsTest.java[tags=expressionFactory]
 ----
 
 NOTE: The last line in the example above shows how to create a new expression by "chaining" two other expressions. A common error when chaining expressions is to assume that "andExp" and "orExp" append another expression to the current expression. In fact, a new expression is created. Expression API treats existing expressions as immutable.
@@ -212,14 +176,9 @@ The "Path" argument to both of these methods can use a split character (a pipe s
 While ExpressionFactory is pretty powerful, there's an even easier way to create an expression using static Property objects generated by Cayenne for each persistent class.
 Some examples:
 
-[source, java]
+[source,java,indent=0]
 ----
-// Artist.NAME is generated by Cayenne and has a type of Property<String>
-Expression e1 = Artist.NAME.eq("Pablo");
-
-// Chaining multiple properties into a path.
-// Painting.ARTIST is generated by Cayenne and has a type of Property<Artist>
-Expression e2 = Painting.ARTIST.dot(Artist.NAME).eq("Pablo");
+include::{java-examples}/ExpressionsTest.java[tags=properties]
 ----
 
 Property objects provide the API mostly analogous to ExpressionFactory, though it is significantly shorter and is aware of the value types.
@@ -233,30 +192,24 @@ When used in a query, an expression is converted to a SQL WHERE or ORDER BY clau
 
 Checking whether an object satisfies an expression:
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression e = Artist.NAME.in("John", "Bob");
-Artist artist = ...
-if(e.match(artist)) {
-   ...
-}
+include::{java-examples}/ExpressionsTest.java[tags=match]
 ----
 
 Reading property value:
 
 
-[source, java]
+[source,java,indent=0]
 ----
-String name = Artist.NAME.path().evaluate(artist);
+include::{java-examples}/ExpressionsTest.java[tags=evaluate]
 ----
 
 Filtering a list of objects:
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression e = Artist.NAME.in("John", "Bob");
-List<Artist> unfiltered = ...
-List<Artist> filtered = e.filterObjects(unfiltered);
+include::{java-examples}/ExpressionsTest.java[tags=filterObjects]
 ----
 
 
@@ -267,23 +220,15 @@ NOTE: Current limitation of in-memory expressions is that no collections are per
 <<ejbql,EJBQL>> is a textual query language that can be used with Cayenne. In some situations,
 it is convenient to be able to convert Expression instances into EJBQL. Expressions support this conversion. An example is shown below.
 
-[source, java]
+[source,java,indent=0]
 ----
-String serial = ...
-Expression e = Pkg.SERIAL.eq(serial);
-List<Object> params = new ArrayList<Object>();
-EJBQLQuery query = new EJBQLQuery("SELECT p FROM Pkg p WHERE " + e.toEJBQL(params,"p");
-
-for(int i=0;i<params.size();i++) {
-  query.setParameter(i+1, params.get(i));
-}
+include::{java-examples}/ExpressionsTest.java[tags=toEJBQL]
 ----
 
 This would be equivalent to the following purely EJBQL querying logic;
 
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery query = new EJBQLQuery("SELECT p FROM Pkg p WHERE p.serial = ?1");
-query.setParameter(1,serial);
+include::{java-examples}/ExpressionsTest.java[tags=ejbql]
 ----
 
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/lifecycle.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/lifecycle.adoc
index a61e53287..65c5a8622 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/lifecycle.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/lifecycle.adoc
@@ -69,34 +69,20 @@ NOTE: Validation and callbacks: There is a clear overlap in functionality betwee
 
 You can define callback methods directly in your Persistent object class using annotations. These methods are invoked automatically by Cayenne during the lifecycle of the object.
 
-[source,java]
+[source,java,indent=0]
 ----
-public class Order extends _Order {
-
-    @PostAdd
-    public void onNewOrder() {
-        // Custom logic before inserting
-    }
-}
+include::{java-examples}/lifecycle/Order.java[tags=content]
 ----
 
 ===== Cayenne Modeler Callbacks Configuration
 
 Alternatively, you can configure callbacks using the Cayenne Modeler for each ObjEntity. Empty callback methods are automatically created as a part of class generation (either with Maven, Ant or the Modeler) and are later filled with appropriate logic by the programmer. E.g. assuming we mapped a 'post-add' callback called 'onNewOrder' in ObjEntity 'Order', the following code will be generated:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public abstract class _Order extends PersistentObject {
-    protected abstract void onNewOrder();
-}
-
-public class Order extends _Order {
+include::{java-examples}/lifecycle/modeler/_Order.java[tags=content]
 
-    @Override
-    protected void onNewOrder() {
-        //TODO: implement onNewOrder
-    }
-}
+include::{java-examples}/lifecycle/modeler/Order.java[tags=content]
 ----
 
 As `onNewOrder()` is already declared in the mapping, it does not need to be registered explicitly. Implementing the method in subclass to do something meaningful is all that is required at this point.
@@ -105,58 +91,27 @@ As `onNewOrder()` is already declared in the mapping, it does not need to be reg
 
 A listener is simply some application class that has one or more annotated callback methods. A callback method signature should be `void someMethod(SomePersistentType object)`. It can be public, private, protected or use default access:
 
-[source, Java]
+[source,java,indent=0]
 ----
- public class OrderListener {
-
-   @PostAdd(Order.class)
-   public void setDefaultsForNewOrder(Order o) {
-      o.setCreatedOn(new Date());
-   }
-}
+include::{java-examples}/lifecycle/OrderListener.java[tags=content]
 ----
 
 Notice that the example above contains an annotation on the callback method that defines the type of the event this method should be called for. Before we go into annotation details, we'll show how to create and register a listener with Cayenne. It is always a user responsibility to register desired application listeners, usually right after CayenneRuntime is started. Here is an example:
 
 First let's define 2 simple listeners.
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Listener1 {
-
-    @PostAdd(MyEntity.class)
-    void postAdd(Persistent object) {
-        // do something
-    }
-}
-
-public class Listener2 {
-
-    @PostRemove({ MyEntity1.class, MyEntity2.class })
-    void postRemove(Persistent object) {
-        // do something
-    }
-
-    @PostUpdate({ MyEntity1.class, MyEntity2.class })
-    void postUpdate(Persistent object) {
-        // do something
-    }
-}
+include::{java-examples}/lifecycle/Listener1.java[tags=content]
+
+include::{java-examples}/lifecycle/Listener2.java[tags=content]
 ----
 
 Ignore the annotations for a minute. The important point here is that the listeners are arbitrary classes unmapped and unknown to Cayenne, that contain some callback methods. Now let's register them with runtime:
 
-[source,Java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-       // ..
-       .addModule(binder ->
-            CoreModule.extend(bidner)
-                .addListener(Listener1.class)
-                .addListener(new Listener2())
-       )
-       // ..
-       .build();
+include::{java-examples}/LifecycleTest.java[tags=listeners]
 ----
 
 
@@ -164,65 +119,37 @@ Listeners in this example are very simple. However they don't have to be. Unlike
 
 Now let's discuss the annotations. There are eight annotations exactly matching the names of eight lifecycle events. A callback method in a listener should be annotated with at least one, but possibly with more than one of them. Annotation itself defines what event the callback should react to. Annotation parameters are essentially an entity filter, defining a subset of ObjEntities whose events we are interested in:
 
-[source, Java]
+[source,java,indent=0]
 ----
-// this callback will be invoked on PostRemove event of any object
-// belonging to MyEntity1, MyEntity2 or their subclasses
-@PostRemove({ MyEntity1.class, MyEntity2.class })
-void postRemove(Persistent object) {
-    // ...
-}
+include::{java-examples}/lifecycle/AnnotationExamples.java[tags=multipleEntities]
 ----
 
-[source, Java]
+[source,java,indent=0]
 ----
-// similar example with multiple annotations on a single method
-// each matching just one entity
-@PostPersist(MyEntity1.class)
-@PostRemove(MyEntity1.class)
-@PostUpdate(MyEntity1.class)
-void postCommit(MyEntity1 object) {
-    // ...
-}
+include::{java-examples}/lifecycle/AnnotationExamples.java[tags=multipleAnnotations]
 ----
 
 As shown above, "value" (the implicit annotation parameter) can contain one or more entity classes. Only these entities' events will result in callback invocation. There's also another way to match entities - via custom annotations. This allows to match any number of entities without even knowing what they are. Here is an example. We'll first define a custom annotation:
 
-[source, Java]
+[source,java,indent=0]
 ----
-@Target(ElementType.TYPE)
-@Retention(RetentionPolicy.RUNTIME)
-public @interface Tag {
-
-}
+include::{java-examples}/lifecycle/Tag.java[tags=content]
 ----
 
 Now we can define a listener that will react to events from ObjEntities annotated with this annotation:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class Listener3 {
-
-    @PostAdd(entityAnnotations = Tag.class)
-    void postAdd(Persistent object) {
-        // do something
-    }
-}
+include::{java-examples}/lifecycle/Listener3.java[tags=content]
 ----
 
 As you see we don't have any entities yet, still we can define a listener that does something useful. Now let's annotate some entities:
 
-[source, Java]
+[source,java,indent=0]
 ----
-@Tag
-public class MyEntity1 extends _MyEntity1 {
-
-}
-
-@Tag
-public class MyEntity2 extends _MyEntity2 {
+include::{java-examples}/lifecycle/MyEntity1.java[tags=content]
 
-}
+include::{java-examples}/lifecycle/MyEntity2.java[tags=content]
 ----
 
 
@@ -235,51 +162,16 @@ They will store their state in a `ThreadLocal` variable of the filter.
 Here is an example filter that does something pretty meaningless - counts how many total objects were committed.
 However it demonstrates the important pattern of aggregating multiple events and presenting a combined result:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class CommittedObjectCounter implements DataChannelSyncFilter {
-
-    private ThreadLocal<int[]> counter = new ThreadLocal<int[]>();
-
-    @Override
-    public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType,
-            DataChannelSyncFilterChain filterChain) {
-
-        // init the counter for the current commit
-        counter.set(new int[1]);
-
-        try {
-            return filterChain.onSync(originatingContext, changes, syncType);
-        } finally {
-
-            // process aggregated result and release the counter
-            System.out.println("Committed " + counter.get()[0] + " object(s)");
-            counter.set(null);
-        }
-    }
-
-    @PostPersist(entityAnnotations = Tag.class)
-    @PostUpdate(entityAnnotations = Tag.class)
-    @PostRemove(entityAnnotations = Tag.class)
-    void afterCommit(Persistent object) {
-        counter.get()[0]++;
-    }
-}
+include::{java-examples}/lifecycle/CommittedObjectCounter.java[tags=content]
 ----
 
 Now since this is both a filter and a listener, it needs to be registered as such:
 
-[source, Java]
+[source,java,indent=0]
 ----
-// this will also add filter as a listener
-CayenneRuntime runtime = CayenneRuntime.of()
-        // ..
-        .addModule(b ->
-                CoreModule.extend(b)
-                        .addSyncFilter(CommittedObjectCounter.class)
-        )
-        // ..
-        .build();
+include::{java-examples}/LifecycleTest.java[tags=syncFilter]
 ----
 
 
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc
index aa6f519d7..4252efd73 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/objectContext.adoc
@@ -19,9 +19,9 @@
 
 ObjectContext is an interface that users normally work with to access the database. It provides the API to execute database operations and to manage persistent objects. A context is obtained from the CayenneRuntime:
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectContext context = runtime.newContext();
+include::{java-examples}/ObjectContextTest.java[tags=newContext]
 ----
 
 The call above creates a new instance of ObjectContext that can access the database via this runtime. ObjectContext is a single "work area" in Cayenne, storing persistent objects. ObjectContext guarantees that, for each database row with a unique ID, it will contain at most one instance of an object, thus ensuring object graph consistency between multiple selects (a feature called "uniquing"). At the same time, different ObjectContexts will have independent copies of objects for each uni [...]
@@ -68,10 +68,9 @@ Each persistent object belongs to a single ObjectContext, and can be in one of t
 
 One of the first things users usually want to do with an `ObjectContext` is to select some objects from a database:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> artists = ObjectSelect.query(Artist.class)
-    .select(context);
+include::{java-examples}/ObjectContextTest.java[tags=select]
 ----
 
 We'll discuss queries in some detail in the <<queries,Queries>> chapter. The example above is self-explanatory -
@@ -82,72 +81,56 @@ Some queries can be quite complex, returning multiple result sets or even updati
 For such queries, ObjectContext provides the `execute()` method.
 While not commonly used, it is nevertheless important in some situations. E.g.:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<QueryResult> result = context.execute(query);
-for (QueryResult item : result) {
-    switch (item) {
-        case QueryResult.Select<?> select -> process(select.objects());
-        case QueryResult.Update update -> process(update.counts());
-        case QueryResult.Iterator<?> iterator -> process(iterator.iterator());
-        case QueryResult.OutParameters out -> process(out.values());
-    }
-}
+include::{java-examples}/ObjectContextTest.java[tags=execute]
 ----
 
 When multiple such queries need to run together, execute them inside an explicit transaction:
 
-[source, java]
+[source,java,indent=0]
 ----
-Collection<Query> queries = ... // multiple queries that need to be run together
-
-runtime.performInTransaction(() -> {
-    queries.forEach(context::execute);
-    return null;
-});
+include::{java-examples}/ObjectContextTest.java[tags=executeInTransaction]
 ----
 
 
 An application might modify selected objects. E.g.:
 
-[source, java]
+[source,java,indent=0]
 ----
-Artist selectedArtist = artists.get(0);
-selectedArtist.setName("Dali");
+include::{java-examples}/ObjectContextTest.java[tags=modify]
 ----
 
 The first time the object property is changed, the object's state is automatically set to *MODIFIED* by Cayenne. Cayenne tracks all in-memory changes until a user calls `commitChanges`:
 
-[source, java]
+[source,java,indent=0]
 ----
-context.commitChanges();
+include::{java-examples}/ObjectContextTest.java[tags=commit]
 ----
 
 At this point, all in-memory changes are analyzed and a minimal set of SQL statements is issued in a single transaction to synchronize the database with the in-memory state. In our example, `commitChanges` commits just one object, but generally it can be any number of objects.
 
 If, instead of commit, we wanted to reset all changed objects to the previously committed state, we'd call `rollbackChanges` instead:
 
-[source, java]
+[source,java,indent=0]
 ----
-context.rollbackChanges();
+include::{java-examples}/ObjectContextTest.java[tags=rollback]
 ----
 
 `newObject` method call creates a persistent object and sets its state to *NEW*:
 
-[source, java]
+[source,java,indent=0]
 ----
-Artist newArtist = context.newObject(Artist.class);
-newArtist.setName("Picasso");
+include::{java-examples}/ObjectContextTest.java[tags=newObject]
 ----
 
 It only exists in memory until `commitChanges` is issued. On commit Cayenne might generate a new primary key (unless a user set it explicitly, or a PK was inferred from a relationship) and issue an `INSERT` SQL statement to permanently store the object.
 
 The `deleteObjects` method takes one or more `Persistent` objects and marks them as *DELETED*:
 
-[source, java]
+[source,java,indent=0]
 ----
-context.deleteObjects(artist1);
-context.deleteObjects(artist2, artist3, artist4);
+include::{java-examples}/ObjectContextTest.java[tags=deleteObjects]
 ----
 
 Additionally, `deleteObjects` processes all delete rules modeled for the affected objects.
@@ -160,18 +143,17 @@ Similarly `rollbackChanges` will undo the effect of `newObject` and `deleteObjec
 Since an application often works with more than one context, `localObject` is a rather common operation. E.g. to improve performance, a user might utilize a single shared context to select and cache data, and then occasionally transfer some selected objects to another context to modify and commit them:
 
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectContext editingContext = runtime.newContext();
-Artist localArtist = editingContext.localObject(artist);
+include::{java-examples}/ObjectContextTest.java[tags=localObject]
 ----
 
 Often an application needs to inspect mapping metadata. This information is stored in the `EntityResolver` object,
 accessible via the `ObjectContext`:
 
-[source, java]
+[source,java,indent=0]
 ----
-EntityResolver resolver = objectContext.getEntityResolver();
+include::{java-examples}/ObjectContextTest.java[tags=entityResolver]
 ----
 
 Here we discussed the most commonly-used subset of the ObjectContext API. There are other useful methods, e.g. those allowing you to inspect registered objects' state in bulk, etc. Check the latest JavaDocs for details.
@@ -180,17 +162,17 @@ Here we discussed the most commonly-used subset of the ObjectContext API. There
 
 There is a useful helper class called `Cayenne` (fully-qualified name `org.apache.cayenne.Cayenne`) that builds on the ObjectContext API to provide a number of very common operations. E.g. get a primary key (most entities do not model PK as an object property) :
 
-[source, java]
+[source,java,indent=0]
 ----
-long pk = Cayenne.longPKForObject(artist);
+include::{java-examples}/ObjectContextTest.java[tags=pkForObject]
 ----
 
 The reverse operation - finding an object given a known PK - is a part of the ObjectContext API. It returns an
 object already registered in the context without a query, and consults the caches before going to the database:
 
-[source, java]
+[source,java,indent=0]
 ----
-Artist artist = context.objectForPK(Artist.class, 34579);
+include::{java-examples}/ObjectContextTest.java[tags=objectForPK]
 ----
 
 For more flexibility, you could <<selectbyid,select by ID>> with `ObjectSelect` instead.
@@ -206,32 +188,21 @@ In theory, Cayenne supports any number of nesting levels; however, applications
 
 To create a nested context, use an instance of CayenneRuntime, passing it the desired parent:
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectContext parent = runtime.newContext();
-ObjectContext nested = runtime.newContext(parent);
+include::{java-examples}/ObjectContextTest.java[tags=nesting]
 ----
 
 From here, a nested context operates just like a regular context (you can perform queries, create and delete objects, etc...). The only difference is that commit and rollback operations can either be limited to synchronization with the parent, or cascade all the way to the database:
 
-[source, java]
+[source,java,indent=0]
 ----
-// merges nested context changes into the parent context
-nested.commitChangesToParent();
-
-// regular 'commitChanges' cascades commit through the chain
-// of parent contexts all the way to the database
-nested.commitChanges();
+include::{java-examples}/ObjectContextTest.java[tags=nestedCommit]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-// unrolls all local changes, getting context in a state identical to parent
-nested.rollbackChangesLocally();
-
-// regular 'rollbackChanges' cascades rollback through the chain of contexts
-// all the way to the topmost parent
-nested.rollbackChanges();
+include::{java-examples}/ObjectContextTest.java[tags=nestedRollback]
 ----
 
 ==== Generic Persistent Objects
@@ -240,40 +211,37 @@ As described in the CayenneModeler chapter, Cayenne supports mapping of complete
 
 Generic objects are first-class citizens in Cayenne, and all common persistent operations apply to them as well. There are some peculiarities, however, described below.
 
-When creating a generic object, either cast your ObjectContext to `DataContext` (that provides `newObject(String)` API), or provide your object with an explicit ObjectId:
+When creating a generic object, either use `ObjectContext.newObject(String)`, that takes an entity name, or provide your object with an explicit ObjectId:
 
-[source, java]
+[source,java,indent=0]
 ----
-Persistent generic = context.newObject("GenericEntity");
+include::{java-examples}/ObjectContextTest.java[tags=genericNewObject]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-Persistent generic = new GenericPersistentObject();
-generic.setObjectId(ObjectId.of("GenericEntity"));
-context.registerNewObject(generic);
+include::{java-examples}/ObjectContextTest.java[tags=genericRegisterNewObject]
 ----
 
 ObjectSelect for a generic object should be created by passing the entity name String, instead of just a Java class:
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect<Persistent> query = ObjectSelect.query(Persistent.class, "GenericEntity");
+include::{java-examples}/ObjectContextTest.java[tags=genericSelect]
 ----
 
 Use Persistent API to access and modify properties of a generic object:
 
-[source, java]
+[source,java,indent=0]
 ----
-String name = (String) generic.readProperty("name");
-generic.writeProperty("name", "New Name");
+include::{java-examples}/ObjectContextTest.java[tags=genericProperties]
 ----
 
 This is how an application can obtain the entity name of a generic object:
 
-[source, java]
+[source,java,indent=0]
 ----
-String entityName = generic.getObjectId().getEntityName();
+include::{java-examples}/ObjectContextTest.java[tags=genericEntityName]
 ----
 
 
@@ -288,35 +256,20 @@ If you are using Spring, EJB or another environment that manages transactions, y
 
 In the second scenario, an application might need to define its own transaction scope that spans more than one Cayenne operation. E.g. two sequential commits that need to be rolled back together in case of failure. This can be done via the `CayenneRuntime.performInTransaction` method:
 
-[source, java]
+[source,java,indent=0]
 ----
-Integer result = runtime.performInTransaction(() -> {
-    // commit one or more contexts
-    context1.commitChanges();
-    context2.commitChanges();
-    ....
-    // after changing some objects in context1, commit again
-    context1.commitChanges();
-    ....
-
-    // return an arbitrary result or null if we don't care about the result
-    return 5;
-});
+include::{java-examples}/ObjectContextTest.java[tags=performInTransaction]
 ----
 
 When inside a transaction, current thread Transaction object can be accessed via a static method:
-[source, java]
+[source,java,indent=0]
 ----
-Transaction tx = BaseTransaction.getThreadTransaction();
+include::{java-examples}/ObjectContextTest.java[tags=threadTransaction]
 ----
 
 You can control transaction isolation level and propagation logic using `TransactionDescriptor`.
 
-[source, java]
+[source,java,indent=0]
 ----
-TransactionDescriptor descriptor = TransactionDescriptor.builder()
-        .isolation(Connection.TRANSACTION_SERIALIZABLE)
-        .propagation(TransactionPropagation.REQUIRES_NEW)
-        .build();
-runtime.performInTransaction(transactionalOperation, descriptor);
+include::{java-examples}/ObjectContextTest.java[tags=transactionDescriptor]
 ----
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/orderings.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/orderings.adoc
index 2b9cb10d1..a1a79a877 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/orderings.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/orderings.adoc
@@ -16,18 +16,16 @@
 
 An Ordering object defines how a list of objects should be ordered. Orderings are essentially path expressions combined with a sorting strategy. Creating an Ordering:
 
-[source, java]
+[source,java,indent=0]
 ----
-Ordering o = new Ordering(Painting.NAME_PROPERTY, SortOrder.ASCENDING);
+include::{java-examples}/OrderingsTest.java[tags=create]
 ----
 
 Like expressions, orderings are translated into SQL as parts of queries (and the sorting occurs in the database). Also like expressions, orderings can be used in memory, naturally - to sort objects:
 
-[source, java]
+[source,java,indent=0]
 ----
-Ordering o = new Ordering(Painting.NAME_PROPERTY, SortOrder.ASCENDING_INSENSITIVE);
-List<Painting> list = ...
-o.orderList(list);
+include::{java-examples}/OrderingsTest.java[tags=orderList]
 ----
 
 Note that unlike filtering with Expressions, ordering is performed in-place. This list object is reordered and no new list is created.
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/custom.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/custom.adoc
index 192a10d49..b13d0f1c2 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/custom.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/custom.adoc
@@ -15,46 +15,19 @@
 
 If a user needs some extra functionality not addressed by the existing set of Cayenne queries, he can write his own.
 The only requirement is to implement `org.apache.cayenne.query.Query` interface.
-The easiest way to go about it is to subclass some of the base queries in Cayenne.
+E.g. to do something directly in the JDBC layer, you might implement `Query` like this:
 
-E.g. to do something directly in the JDBC layer, you might subclass `AbstractQuery`:
-
-[source, java]
+[source,java,indent=0]
 ----
-public class MyQuery extends AbstractQuery {
-
-    @Override
-    public SQLAction createSQLAction(SQLActionVisitor visitor) {
-        return new SQLAction() {
-
-            @Override
-            public void performAction(Connection connection, OperationObserver observer) throws SQLException, Exception {
-                // 1. do some JDBC work using provided connection...
-                // 2. push results back to Cayenne via OperationObserver
-            }
-        };
-    }
-}
+include::{java-examples}/customquery/MyQuery.java[tags=content]
 ----
 
 To delegate the actual query execution to a standard Cayenne query, you may subclass `IndirectQuery`:
 
 
-[source, java]
+[source,java,indent=0]
 ----
-public class MyDelegatingQuery extends IndirectQuery {
-
-    @Override
-    protected Query createReplacementQuery(EntityResolver resolver) {
-        SQLTemplate delegate = new SQLTemplate(SomeClass.class, generateRawSQL());
-        delegate.setFetchingDataRows(true);
-        return delegate;
-    }
-
-    protected String generateRawSQL() {
-        // build some SQL string
-    }
-}
+include::{java-examples}/customquery/MyDelegatingQuery.java[tags=content]
 ----
 
 In fact many internal Cayenne queries are `IndirectQueries`, delegating to `ObjectSelect` or `SQLTemplate`
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc
index b7d05a94c..1847b7fc4 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc
@@ -22,9 +22,9 @@ EJBQLQuery was created as a part of an experiment in adopting some of Java Persi
 It is a parameterized object query that is created from query String. A String used to build EJBQLQuery follows JPQL
 (JPA Query Language) syntax:
 
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery query = new EJBQLQuery("select a FROM Artist a");
+include::{java-examples}/EJBQLQueryTest.java[tags=create]
 ----
 
 JPQL details can be found in any JPA manual. Here we'll focus on how this fits into Cayenne and what are the
@@ -36,24 +36,19 @@ varieties available.
 NOTE: DELETE and UPDATE do not change the state of objects in the ObjectContext. They are run directly against the
 database instead.
 
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery<Artist> select =
-    new EJBQLQuery<>("select a FROM Artist a WHERE a.name = 'Salvador Dali'");
-List<Artist> artists = context.select(select);
+include::{java-examples}/EJBQLQueryTest.java[tags=select]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery delete = new EJBQLQuery("delete from Painting");
-context.execute(delete);
+include::{java-examples}/EJBQLQueryTest.java[tags=delete]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery update =
-    new EJBQLQuery("UPDATE Painting AS p SET p.name = 'P2' WHERE p.name = 'P1'");
-context.execute(update);
+include::{java-examples}/EJBQLQueryTest.java[tags=update]
 ----
 
 In most cases `ObjectSelect` is preferred to `EJBQLQuery`, as it is API-based, and provides you with better compile-time
@@ -63,45 +58,39 @@ such as subqueries.
 
 Just like `ObjectSelect` `EJBQLQuery` can return a `List` of `Object[]` elements, where each entry in an array is either a
 `Persistent` or a scalar, depending on the query SELECT clause.
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery<Object[]> query = new EJBQLQuery<>("select a, COUNT(p) FROM Artist a JOIN a.paintings p GROUP BY a");
-List<Object[]> result = context.select(query);
-for(Object[] artistWithCount : result) {
-    Artist a = (Artist) artistWithCount[0];
-    int hasPaintings = (Integer) artistWithCount[1];
-}
+include::{java-examples}/EJBQLQueryTest.java[tags=objectsAndScalars]
 ----
 
 A result can also be a list of scalars:
-[source, java]
+[source,java,indent=0]
 ----
-EJBQLQuery<String> query = new EJBQLQuery<>("select a.name FROM Artist a");
-List<String> names = context.select(query);
+include::{java-examples}/EJBQLQueryTest.java[tags=scalars]
 ----
 
 EJBQLQuery supports an "IN" clause with three different usage-patterns. The following example would require three
 individual positional parameters (named parameters could also have been used) to be supplied.
 
-[source, java]
+[source,java,indent=0]
 ----
-select p from Painting p where p.paintingTitle in (?1,?2,?3)
+include::{java-examples}/EJBQLQueryTest.java[tags=inPositional]
 ----
 
 The following example requires a single positional parameter to be supplied.
 The parameter can be any concrete implementation of the `java.util.Collection` interface
 such as `java.util.List` or `java.util.Set`.
 
-[source, java]
+[source,java,indent=0]
 ----
-select p from Painting p where p.paintingTitle in ?1
+include::{java-examples}/EJBQLQueryTest.java[tags=inCollection]
 ----
 
 The following example is functionally identical to the one prior.
 
-[source, java]
+[source,java,indent=0]
 ----
-select p from Painting p where p.paintingTitle in (?1)
+include::{java-examples}/EJBQLQueryTest.java[tags=inCollectionInParentheses]
 ----
 
 It is possible to convert an xref:expressions[Expression] object used with a xref:select[ObjectSelect] to EJBQL. Use the
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc
index ce1bc6bc3..19f23fa74 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc
@@ -23,20 +23,14 @@ NOTE: These queries are "fluent" versions of deprecated `NamedQuery` class.
 
 Here is example of how to use `MappedSelect`:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> results = MappedSelect.query("artistsByName", Artist.class)

-    .param("name", "Picasso")

-    .select(context);
+include::{java-examples}/MappedQueryTest.java[tags=mappedSelect]
 ----
 
 And here is example of `MappedExec`:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<QueryResult> result = MappedExec.query("updateQuery")
-    .param("var", "value")
-    .execute(context);
-QueryResult.Update update = (QueryResult.Update) result.getFirst();
-System.out.println("Rows updated: " + update.count());
+include::{java-examples}/MappedQueryTest.java[tags=mappedExec]
 ----
\ No newline at end of file
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect-parsed.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect-parsed.adoc
index 4375448b2..dd40af54c 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect-parsed.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect-parsed.adoc
@@ -17,24 +17,18 @@
 Since 5.0 an `ObjectSelect` or a `ColumnSelect` can be created from a String. This is an alernative to the fluent API.
 E.g.:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> artists = ObjectSelect
-    .parse(Artist.class, "from Artist where artistName like $name order by dateOfBirth desc limit 10", "A%")
-    .select(context);
+include::{java-examples}/ObjectSelectParsedTest.java[tags=parse]
 ----
 
 The parser produces the same query object that the fluent API would, so everything said about
 <<select,ObjectSelect>> above applies. The parsed query can be customized further with the fluent API before it is
 executed:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> artists = ObjectSelect
-    .parse(Artist.class, "from Artist where artistName like $name", "A%")
-    .localCache("artists")
-    .pageSize(50)
-    .select(context);
+include::{java-examples}/ObjectSelectParsedTest.java[tags=parseAndCustomize]
 ----
 
 ===== Query Structure
@@ -76,13 +70,9 @@ and are bound in the order in which the parameters first appear in the String. I
 it takes a single value that is bound in all those places. A value can be anything that is a valid expression operand:
 a scalar, a collection for `in`, a persistent object to match a relationship, etc.:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Painting> paintings = ObjectSelect
-    .parse(Painting.class,
-        "from Painting where toArtist = $artist and estimatedPrice between $low and $high",
-        artist, 10, 500)
-    .select(context);
+include::{java-examples}/ObjectSelectParsedTest.java[tags=params]
 ----
 
 The parameters can be used anywhere in the expressions, including the nested selects. `limit` and `offset` only take
@@ -92,20 +82,9 @@ integer literals.
 
 There are four methods to parse a query, that differ in the type of the result:
 
-[source, java]
+[source,java,indent=0]
 ----
-// any query. Returns either an ObjectSelect or a ColumnSelect
-FluentSelect<?, ?> q1 = ObjectSelect.parse("from Artist");
-
-// a query of the root objects. "DataRow.class" is used to fetch the data rows instead of objects
-ObjectSelect<Artist> q2 = ObjectSelect.parse(Artist.class, "from Artist");
-ObjectSelect<DataRow> q3 = ObjectSelect.parse(DataRow.class, "from Artist");
-
-// a query with a single column
-ColumnSelect<String> q4 = ObjectSelect.parseColumn(String.class, "select artistName from Artist");
-
-// a query with multiple columns
-ColumnSelect<Object[]> q5 = ObjectSelect.parseColumns("select artistName, dateOfBirth from Artist");
+include::{java-examples}/ObjectSelectParsedTest.java[tags=api]
 ----
 
 The last three are there to avoid the type casts. They throw an exception if the query is of a different kind,
@@ -122,16 +101,12 @@ Each example shows a query String and the same query built with the fluent API.
 
 [source]
 ----
-from Artist where artistName like 'A%' order by dateOfBirth desc, artistName asc insensitive limit 10 offset 20
+include::{resource-examples}/objectselect-parsed.txt[tags=filterOrderLimit]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class)
-    .where(Artist.ARTIST_NAME.like("A%"))
-    .orderBy(Artist.DATE_OF_BIRTH.desc(), Artist.ARTIST_NAME.ascInsensitive())
-    .limit(10)
-    .offset(20)
+include::{java-examples}/ObjectSelectParsedTest.java[tags=filterOrderLimit]
 ----
 
 An ordering is an expression, optionally followed by `asc` (the default) or `desc`, and then optionally
@@ -143,78 +118,67 @@ The joins are implied by the paths. Here is an outer join, matching the artists
 
 [source]
 ----
-from Artist where paintingArray+ = null
+include::{resource-examples}/objectselect-parsed.txt[tags=outerJoin]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class).where(Artist.PAINTING_ARRAY.outer().isNull())
+include::{java-examples}/ObjectSelectParsedTest.java[tags=outerJoin]
 ----
 
 And here are two separate joins over the same relationship, matching the artists that have both paintings:
 
 [source]
 ----
-from Artist where paintingArray#p1.paintingTitle = 'P1' and paintingArray#p2.paintingTitle = 'P2'
+include::{resource-examples}/objectselect-parsed.txt[tags=splitJoins]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class)
-    .where(Artist.PAINTING_ARRAY.alias("p1").dot(Painting.PAINTING_TITLE).eq("P1"))
-    .and(Artist.PAINTING_ARRAY.alias("p2").dot(Painting.PAINTING_TITLE).eq("P2"))
+include::{java-examples}/ObjectSelectParsedTest.java[tags=splitJoins]
 ----
 
 ====== Columns
 
 [source]
 ----
-select artistName, dateOfBirth from Artist order by artistName
+include::{resource-examples}/objectselect-parsed.txt[tags=columns]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
-    .orderBy(Artist.ARTIST_NAME.asc())
+include::{java-examples}/ObjectSelectParsedTest.java[tags=columns]
 ----
 
 The result is a `List<Object[]>`. With a single column it is a list of values:
 
 [source]
 ----
-select distinct toGallery.galleryName from Painting
+include::{resource-examples}/objectselect-parsed.txt[tags=column]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.columnQuery(Painting.class, Painting.TO_GALLERY.dot(Gallery.GALLERY_NAME)).distinct()
+include::{java-examples}/ObjectSelectParsedTest.java[tags=column]
 ----
 
 A column can be any expression, and it can have an alias (the same as `BaseProperty.alias(..)`):
 
 [source]
 ----
-select upper(artistName) as name, length(trim(artistName)) as size from Artist
+include::{resource-examples}/objectselect-parsed.txt[tags=columnExpressions]
 ----
 
 ====== Aggregates
 
 [source]
 ----
-select toArtist.artistName, sum(estimatedPrice), count(*)
-from Painting
-having sum(estimatedPrice) > $min
-order by toArtist.artistName
+include::{resource-examples}/objectselect-parsed.txt[tags=aggregates]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.columnQuery(Painting.class,
-        Painting.TO_ARTIST.dot(Artist.ARTIST_NAME),
-        Painting.ESTIMATED_PRICE.sum(),
-        PropertyFactory.COUNT)
-    .having(Painting.ESTIMATED_PRICE.sum().gt(min))
-    .orderBy(Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).asc())
+include::{java-examples}/ObjectSelectParsedTest.java[tags=aggregates]
 ----
 
 `toArtist.artistName` becomes a grouping column here, as the other columns are aggregates.
@@ -226,37 +190,36 @@ and a `Long`:
 
 [source]
 ----
-select self, count(paintingArray+) from Artist order by count(paintingArray+) desc
+include::{resource-examples}/objectselect-parsed.txt[tags=self]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.columnQuery(Artist.class, Artist.SELF, Artist.PAINTING_ARRAY.outer().count())
-    .orderBy(Artist.PAINTING_ARRAY.outer().count().desc())
+include::{java-examples}/ObjectSelectParsedTest.java[tags=self]
 ----
 
 A to-one relationship gives a related object in each row:
 
 [source]
 ----
-select paintingTitle, toArtist, toGallery from Painting
+include::{resource-examples}/objectselect-parsed.txt[tags=toOne]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.columnQuery(Painting.class, Painting.PAINTING_TITLE, Painting.TO_ARTIST, Painting.TO_GALLERY)
+include::{java-examples}/ObjectSelectParsedTest.java[tags=toOne]
 ----
 
 With a to-many relationship a row is repeated for each related object. This is the same as `flat()` in the fluent API:
 
 [source]
 ----
-select artistName, paintingArray from Artist
+include::{resource-examples}/objectselect-parsed.txt[tags=toMany]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.flat())
+include::{java-examples}/ObjectSelectParsedTest.java[tags=toMany]
 ----
 
 In both cases a row with no related object is still present in the result, with a `null` in place of the object. To
@@ -270,16 +233,12 @@ the same structure as the main query, except for `prefetch`. Its paths are relat
 
 [source]
 ----
-from Artist where exists (
-    from Painting where toArtist = enclosing(self) and estimatedPrice > $price)
+include::{resource-examples}/objectselect-parsed.txt[tags=exists]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class)
-    .where(ExpressionFactory.exists(ObjectSelect.query(Painting.class)
-        .where(Painting.TO_ARTIST.eq(Artist.SELF.enclosing()))
-        .and(Painting.ESTIMATED_PRICE.gt(price))))
+include::{java-examples}/ObjectSelectParsedTest.java[tags=exists]
 ----
 
 `enclosing(self)` is the root object of the enclosing query, `enclosing(artistName)` is its property, and
@@ -289,18 +248,12 @@ ObjectSelect.query(Artist.class)
 
 [source]
 ----
-from Painting where toArtist.artistName in (select artistName from Artist where dateOfBirth < $date)
-
-from Painting where estimatedPrice > all (select estimatedPrice from Painting where paintingTitle = 'P2')
-
-from Painting where estimatedPrice = any (select max(estimatedPrice) from Painting)
+include::{resource-examples}/objectselect-parsed.txt[tags=nestedSelects]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Painting.class)
-    .where(Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).in(
-        ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME).where(Artist.DATE_OF_BIRTH.lt(date))))
+include::{java-examples}/ObjectSelectParsedTest.java[tags=in]
 ----
 
 NOTE: When there is no need for a nested select with its own conditions,`exists` over a path is shorter:
@@ -313,14 +266,12 @@ paths, with no need for a `db:` prefix:
 
 [source]
 ----
-from db:ARTIST where ARTIST_NAME like 'A%' order by ARTIST_ID desc
+include::{resource-examples}/objectselect-parsed.txt[tags=dbRoot]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.dbQuery("ARTIST")
-    .where(ExpressionFactory.likeDbExp("ARTIST_NAME", "A%"))
-    .orderBy(new Ordering(ExpressionFactory.dbPathExp("ARTIST_ID"), SortOrder.DESCENDING))
+include::{java-examples}/ObjectSelectParsedTest.java[tags=dbRoot]
 ----
 
 ====== Prefetching
@@ -331,13 +282,10 @@ ObjectSelect.dbQuery("ARTIST")
 
 [source]
 ----
-from Artist where artistName = $name prefetch paintingArray joint, paintingArray.toGallery
+include::{resource-examples}/objectselect-parsed.txt[tags=prefetch]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class)
-    .where(Artist.ARTIST_NAME.eq(name))
-    .prefetch(Artist.PAINTING_ARRAY.joint())
-    .prefetch("paintingArray.toGallery", PrefetchTreeNode.UNDEFINED_SEMANTICS)
+include::{java-examples}/ObjectSelectParsedTest.java[tags=prefetch]
 ----
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect.adoc
index 73514c339..66f1bca36 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/objectselect.adoc
@@ -21,16 +21,16 @@ NOTE: `ObjectSelect` supersedes older `SelectQuery`. `SelectQuery` is deprecated
 `ObjectSelect` is the most commonly used query in Cayenne applications. This may be the only query you will ever need.
 It returns a list of persistent objects (or data rows) of a certain type specified in the query:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> objects = ObjectSelect.query(Artist.class).select(context);
+include::{java-examples}/ObjectSelectTest.java[tags=selectAll]
 ----
 
 This returned all rows in the _ARTIST_ table. If the logs were turned on, you might see the following SQL printed:
 
 [source]
 ----
-INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0
+INFO: SELECT t0.ARTIST_NAME, t0.DATE_OF_BIRTH, t0.ARTIST_ID FROM ARTIST t0
 INFO: === returned 5 row. - took 5 ms.
 ----
 
@@ -40,40 +40,32 @@ Qualifier is simply an Expression (Expressions where discussed in the <<Expressi
 appended to the query using "where" method. If you only want artists whose name begins with 'Pablo',
 you might use the following qualifier expression:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> objects = ObjectSelect.query(Artist.class)
-    .where(Artist.NAME.like("Pablo%"))
-    .select(context);
+include::{java-examples}/ObjectSelectTest.java[tags=where]
 ----
 
 The SQL will look different this time:
 
 [source]
 ----
-INFO: SELECT t0.DATE_OF_BIRTH, t0.NAME, t0.ID FROM ARTIST t0 WHERE t0.NAME LIKE ?
-[bind: 1->NAME:'Pablo%']
+INFO: SELECT t0.ARTIST_NAME, t0.DATE_OF_BIRTH, t0.ARTIST_ID FROM ARTIST t0 WHERE t0.ARTIST_NAME LIKE ?
+[bind: 1->ARTIST_NAME:'Pablo%']
 INFO: === returned 1 row. - took 6 ms.
 ----
 
 `ObjectSelect` allows to assemble qualifier from parts, using "and" and "or" method to chain them together:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> objects = ObjectSelect.query(Artist.class)
-    .where(Artist.NAME.like("A%"))
-    .and(Artist.DATE_OF_BIRTH.gt(someDate)
-    .select(context);
+include::{java-examples}/ObjectSelectTest.java[tags=and]
 ----
 
 To order the results of `ObjectSelect`, one or more orderings can be applied:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> objects = ObjectSelect.query(Artist.class)
-    .orderBy(Artist.DATE_OF_BIRTH.desc())
-    .orderBy(Artist.NAME.asc())
-    .select(context);
+include::{java-examples}/ObjectSelectTest.java[tags=orderBy]
 ----
 
 There's a number of other useful methods in `ObjectSelect` that define what to select
@@ -85,19 +77,16 @@ Others are fairly self-explanatory. Please check the API docs for the full exten
 
 `ObjectSelect` query can be used to fetch individual properties of objects via type-safe API:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<String> names = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME)
-    .select(context);
+include::{java-examples}/ObjectSelectTest.java[tags=column]
 ----
 
 And here is an example of selecting several properties. The result is a list of `Object[]`:
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Object[]> nameAndDate = ObjectSelect
-    .columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
-    .select(context);
+include::{java-examples}/ObjectSelectTest.java[tags=columns]
 ----
 
 ===== Selecting using aggregate functions
@@ -105,29 +94,16 @@ List<Object[]> nameAndDate = ObjectSelect
 ObjectSelect query supports usage of aggregate functions.
 Most common variant of aggregation is selecting count of records, this can be done really easy:
 
-[source, java]
+[source,java,indent=0]
 ----
-long count = ObjectSelect.query(Artist.class).selectCount(context);
+include::{java-examples}/ObjectSelectTest.java[tags=count]
 ----
 
 But you can use aggregates in more cases, even combine selecting individual properties and aggregates:
 
-[source, java]
+[source,java,indent=0]
 ----
-// Artist.SELF - is a special property that denotes a full object in this case
-List<Object[]> artistAndPaintingCount = ObjectSelect.columnQuery(Artist.class,
-        Artist.SELF,
-        Artist.PAINTING_ARRAY.count())
-    .where(Artist.ARTIST_NAME.like("a%"))
-    .having(Artist.PAINTING_ARRAY.count().lt(5L))
-    .orderBy(Artist.PAINTING_ARRAY.count().desc(), Artist.ARTIST_NAME.asc())
-    .select(context);
-
-for(Object[] next : artistAndPaintingCount) {
-    Artist artist = (Artist)next[0];
-    long paintings = (Long)next[1];
-    System.out.println(artist.getArtistName() + " have " + paintings + " paintings");
-}
+include::{java-examples}/ObjectSelectTest.java[tags=aggregates]
 ----
 
 Here is generated `SQL` for this query:
@@ -147,11 +123,9 @@ ORDER BY COUNT(t1.PAINTING_ID) DESC, t0.ARTIST_NAME
 Since Cayenne 4.2 `ObjectSelect` supports subqueries, and since 5.0 subqueries API is greatly improved.
 Here is an example of a basic `NOT EXISTS` subquery:
 
-[source, java]
+[source,java,indent=0]
 ----
-long count = ObjectSelect.query(Artist.class)
-        .where(Artist.PAINTING_ARRAY.notExists())
-        .selectCount(context);
+include::{java-examples}/ObjectSelectTest.java[tags=notExists]
 ----
 
 And here's a generated `SQL`:
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/procedure.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/procedure.adoc
index c0d0e4685..4731d2e2d 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/procedure.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/procedure.adoc
@@ -21,22 +21,14 @@ some data modification (returned as an update count), or a combination of these.
 So use root class to get a single result set, and use only procedure name for anything else:
 
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> result = ProcedureCall.query("my_procedure", Artist.class)
-    .param("p1", "abc")
-    .param("p2", 3000)
-    .select(context);
+include::{java-examples}/ProcedureCallExamples.java[tags=select]
 ----
 
-[source, java]
+[source,java,indent=0]
 ----
-// here we do not bother with root class.
-// Procedure name gives us needed routing information
-List<QueryResult> result = ProcedureCall.query("my_procedure")
-    .param("p1", "abc")
-    .param("p2", 3000)
-    .call(context);
+include::{java-examples}/ProcedureCallExamples.java[tags=call]
 ----
 
 `QueryResult` is a sealed interface with `Select`, `Update`, `Iterator` and `OutParameters` variants, listed in
@@ -45,16 +37,9 @@ switch. A stored procedure can return data back to the application as result set
 The values of OUT and INOUT parameters are reported as a separate `QueryResult.OutParameters` item of the
 result, a map keyed by parameter name:
 
-[source, java]
+[source,java,indent=0]
 ----
-for (QueryResult item : ProcedureCall.query("my_procedure").call(context)) {
-    switch (item) {
-        case QueryResult.Select<?> select -> process(select.objects());
-        case QueryResult.Update update -> process(update.counts());
-        case QueryResult.OutParameters out -> process(out.values().get("out_param"));
-        default -> {}
-    }
-}
+include::{java-examples}/ProcedureCallExamples.java[tags=outParameters]
 ----
 
 There maybe a situation when a stored procedure handles its own transactions,
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/selectbyid.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/selectbyid.adoc
index f9348f042..b3fd81d5d 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/selectbyid.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/selectbyid.adoc
@@ -17,15 +17,9 @@
 
 `ObjectSelect` can match objects of the root entity by their primary key with `byId(..)` and `byIds(..)`:
 
-[source, java]
+[source,java,indent=0]
 ----
-Artist artistWithId1 = ObjectSelect.query(Artist.class)
-    .byId(1)
-    .selectOne(context);
-
-List<Artist> artists = ObjectSelect.query(Artist.class)
-    .byIds(1, 2, 3)
-    .select(context);
+include::{java-examples}/SelectByIdTest.java[tags=byId]
 ----
 
 Both methods accept an id in any of three forms, and `byIds(..)` may mix them:
@@ -34,15 +28,9 @@ Both methods accept an id in any of three forms, and `byIds(..)` may mix them:
 * a `Map` of DB attribute names to values, which is the form to use for a compound primary key;
 * an `ObjectId`.
 
-[source, java]
+[source,java,indent=0]
 ----
-CompoundPk object = ObjectSelect.query(CompoundPk.class)
-    .byId(Map.of("KEY1", "x", "KEY2", "y"))
-    .selectOne(context);
-
-Artist artist = ObjectSelect.query(Artist.class)
-    .byId(objectId)
-    .selectOne(context);
+include::{java-examples}/SelectByIdTest.java[tags=byIdForms]
 ----
 
 Being an ordinary qualifier, an id match combines with everything else `ObjectSelect` offers - prefetching,
@@ -50,16 +38,9 @@ caching, ordering, pagination and further conditions. Under the hood these are s
 the entity `SELF` property. So here is a somewhat more verbose equivalent that is also more flexible (e.g. allowing to
 chain id qualifier with "or") :
 
-[source, java]
+[source,java,indent=0]
 ----
-List<Artist> artists = ObjectSelect.query(Artist.class)
-    .where(Artist.ARTIST_NAME.eq("Dali"))
-    .or(Artist.SELF.idsIn(1, 2, 3))
-    .select(context);
-
-List<Painting> paintings = ObjectSelect.query(Painting.class)
-    .where(Painting.TO_ARTIST.eqId(1))
-    .select(context);
+include::{java-examples}/SelectByIdTest.java[tags=idExpressions]
 ----
 
 Yet another way to find a single object by PK per <<persistent-objects-objectcontext,the ObjectContext chapter>> is
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc
index 732c98300..b19708144 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sql.adoc
@@ -24,37 +24,14 @@ used (as the name suggests) to select custom data in form of entities, separate
 Both queries support advanced SQL templating, with variable substitution and special directives as described
 <<sqlscripting,in the next chapter>>. Here we'll just provide a few simple examples:
 
-[source, java]
+[source,java,indent=0]
 ----
-// Selecting objects
-List<Painting> paintings = SQLSelect
-    .query(Painting.class, "SELECT * FROM PAINTING WHERE PAINTING_TITLE LIKE 'A%'")
-    .upperColumnNames()
-    .localCache()
-    .limit(100)
-    .select(context);
-
-// Selecting scalar values
-List<String> paintingNames = SQLSelect
-    .scalarQuery(String.class, "SELECT PAINTING_TITLE FROM PAINTING WHERE ESTIMATED_PRICE > 100000")
-    .select(context);
-
-// Selecting DataRow with predefined types
-List<DataRow> result = SQLSelect
-    .dataRowQuery("SELECT * FROM ARTIST", Integer.class, String.class, LocalDate.class)
-    .select(context);
-
-// Selecting Object[] with predefined types
-List<Object[]> result = SQLSelect
-    .scalarQuery("SELECT * FROM ARTIST", Integer.class, String.class, LocalDate.class)
-    .select(context);
+include::{java-examples}/SQLSelectTest.java[tags=select]
 ----
 
 And here is an example of how to use `SQLExec`:
 
-[source, java]
+[source,java,indent=0]
 ----
-int inserted = SQLExec
-    .query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (55, 'Picasso')")
-    .update(context);
+include::{java-examples}/SQLSelectTest.java[tags=exec]
 ----
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sqlscripting.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sqlscripting.adoc
index f2bb1bb64..5f7216df6 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sqlscripting.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/sqlscripting.adoc
@@ -23,11 +23,9 @@ below we'll use both selecting and updating examples, as scripting works the sam
 
 All variables in the template string are replaced from query parameters:
 
-[source, java]
+[source,java,indent=0]
 ----
-// this will generate SQL like this: "delete from mydb.PAINTING"
-SQLExec query = SQLExec.query("delete from $tableName")
-    .params("mydb.PAINTING");
+include::{java-examples}/SQLScriptingTest.java[tags=variable]
 ----
 
 Variable substitution within the text uses `object.toString()` method to replace the variable value.
@@ -185,13 +183,9 @@ Usage:
 
 Full example:
 
-[source, java]
+[source,java,indent=0]
 ----
-String sql = "SELECT * FROM PAINTING t0 WHERE #bindObjectEqual($a 't0.ARTIST_ID' 'ARTIST_ID' )";
-Artist artistParam = ...;
-
-SQLSelect select = SQLSelect.query(Painting.class, sql)
-    .params("a", artistParam);
+include::{java-examples}/SQLScriptingTest.java[tags=bindObjectEqual]
 ----
 
 ====== #bindObjectNotEqual
@@ -217,13 +211,9 @@ Usage:
 
 Full example:
 
-[source, java]
+[source,java,indent=0]
 ----
-String sql = "SELECT * FROM PAINTING t0 WHERE #bindObjectNotEqual($a 't0.ARTIST_ID' 'ARTIST_ID' )";
-Artist artistParam = ...;
-
-SQLSelect select = SQLSelect.query(Painting.class, sql)
-    .params("a", artistParam);
+include::{java-examples}/SQLScriptingTest.java[tags=bindObjectNotEqual]
 ----
 
 ====== #result
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc
index 7e5adf7e7..9cc0c01a7 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc
@@ -18,33 +18,25 @@
 
 In runtime Cayenne is accessed via `org.apache.cayenne.runtime.CayenneRuntime`. CayenneRuntime is created by calling a convenient builder:
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-                .addConfig("com/example/cayenne-project.xml")
-                .build();
+include::{java-examples}/StartingTest.java[tags=start]
 ----
 
 The parameter you pass to the builder is a location of the main project file. Location is a '/'-separated path (same path separator is used on UNIX and Windows) that is resolved relative to the application classpath. The project file can be placed in the root package or in a subpackage (e.g. in the code above it is in "com/example" subpackage).
 
 CayenneRuntime encapsulates a single Cayenne stack. Most applications will just have one CayenneRuntime using it to create as many ObjectContexts as needed, access the Dependency Injection (DI) container and work with other Cayenne features. Internally CayenneRuntime is just a thin wrapper around the DI container. Detailed features of the container are discussed in <<Customizing Cayenne Runtime>> chapter. Here we'll just show an example of how an application might turn on external transactions:
 
-[source, java]
+[source,java,indent=0]
 ----
-Module extensions = binder ->
-      CoreModule.extend(binder).setProperty(Constants.EXTERNAL_TX_PROPERTY, "true");
-
-CayenneRuntime runtime = CayenneRuntime.of()
-      .addConfig("com/example/cayenne-project.xml")
-      .addModule(extensions)
-      .build();
+include::{java-examples}/StartingTest.java[tags=externalTransactions]
 ----
 
 It is a good idea to shut down the runtime when it is no longer needed, usually before the application itself is shutdown:
 
-[source, java]
+[source,java,indent=0]
 ----
-runtime.shutdown();
+include::{java-examples}/StartingTest.java[tags=stop]
 ----
 
 When a runtime object has the same scope as the application, this may not be always necessary, however in some cases it is essential, and is generally considered a good practice. E.g. in a web container hot redeploy of a webapp will cause resource leaks and eventual OutOfMemoryError if the application fails to shutdown CayenneRuntime.
@@ -53,13 +45,9 @@ When a runtime object has the same scope as the application, this may not be alw
 
 CayenneRuntime requires at least one mapping project to run. But it can also take multiple projects and merge them together in a single configuration. This way different parts of a database can be mapped independently from each other (even by different software providers), and combined in runtime when assembling an application. Doing it is as easy as passing multiple project locations to CayenneRuntime builder:
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addConfig("com/example/cayenne-project.xml")
-        .addConfig("org/foo/cayenne-library1.xml")
-        .addConfig("org/foo/cayenne-library2.xml")
-        .build();
+include::{java-examples}/StartingTest.java[tags=multipleProjects]
 ----
 
 When the projects are merged, the following rules are applied:
@@ -79,36 +67,3 @@ When the projects are merged, the following rules are applied:
 ==== Web Applications
 
 Web applications can use a variety of mechanisms to configure and start the "services" they need, Cayenne being one of such services. Configuration can be done within standard servlet specification objects like Servlets, Filters, or ServletContextListeners, or can use Spring, JEE CDI, etc. This is a user's architectural choice and Cayenne is agnostic to it and will happily work in any environment. As described above, all that is needed is to create an instance of CayenneRuntime somewhere [...]
-
-Still Cayenne includes a piece of web app configuration code that can assist in quickly setting up simple Cayenne-enabled web applications. We are talking about CayenneFilter. It is declared in web.xml:
-
-[source, XML]
-----
-<web-app>
-    ...
-    <filter>
-        <filter-name>cayenne-project</filter-name>
-        <filter-class>org.apache.cayenne.configuration.web.CayenneFilter</filter-class>
-    </filter>
-     <filter-mapping>
-        <filter-name>cayenne-project</filter-name>
-        <url-pattern>/*</url-pattern>
-     </filter-mapping>
-    ...
- </web-app>
-----
-
-When started by the web container, it creates a instance of CayenneRuntime and stores it in the ServletContext. Note that the name of a Cayenne XML project file is derived from the "filter-name". In the example above, CayenneFilter will look for an XML file "cayenne-project.xml". This can be overridden with the "configuration-location" init parameter.
-
-When the application runs, all HTTP requests matching the filter url-pattern have access to a session-scoped ObjectContext like this:
-
-[source, java]
-----
- ObjectContext context = DataContext.getThreadObjectContext();
-----
-
-Of course, the ObjectContext scope and other behavior of the Cayenne runtime can be customized via dependency injection. For this, another filter init parameter called "extra-modules" is used. "extra-modules" is a comma- or space-separated list of class names, with each class implementing Module interface. These optional custom modules are loaded after the the standard ones, which allows users to override all standard definitions.
-
-For those interested in the DI container contents of the runtime created by CayenneFilter, it is the same CayenneRuntime as would have been created by other means, but with an extra <code>org.apache.cayenne.configuration.web.WebModule</code> module that provides the <code>org.apache.cayenne.configuration.web.RequestHandler</code> service. This is the service to override in the custom modules if you need to provide a different ObjectContext scope, etc.
-
-NOTE: You should not think of CayenneFilter as the only way to start and use Cayenne in a web application. In fact, CayenneFilter is entirely optional. Use it if you don’t have any special design for application service management. If you do, simply integrate Cayenne into that design.
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc
index aa6a40bc8..48b0c4754 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/tuning.adoc
@@ -23,12 +23,9 @@ object relationships, so more generally it can be viewed as a technique for mana
 
 Prefetching example:
 
-[source, Java]
+[source,java,indent=0]
 ----
-List<Artist> artists = ObjectSelect
-    .query(Artist.class)
-    .prefetch(Artist.PAINTINGS.disjoint()) // <1>
-    .select(context); // <2>
+include::{java-examples}/TuningTest.java[tags=prefetch]
 ----
 
 <1> Instructs Cayenne to prefetch one of Artist's relationships.
@@ -36,17 +33,16 @@ List<Artist> artists = ObjectSelect
 
 All types of relationships can be prefetched - to-one, to-many, flattened. A prefetch can span multiple relationships:
 
-[source, Java]
+[source,java,indent=0]
 ----
-query.prefetch(Artist.PAINTINGS.dot(Painting.GALLERY).disjoint());
+include::{java-examples}/TuningTest.java[tags=prefetchPath]
 ----
 
 A query can have multiple prefetches:
 
-[source, Java]
+[source,java,indent=0]
 ----
-query.prefetch(Artist.PAINTINGS.disjoint())
-   .prefetch(Artist.PAINTINGS.dot(Painting.GALLERY).disjoint());
+include::{java-examples}/TuningTest.java[tags=prefetchMultiple]
 ----
 
 If a query is fetching DataRows, all "disjoint" prefetches are ignored, only "joint" prefetches are executed
@@ -108,28 +104,16 @@ to be convertible into object properties. The main rules to follow are:
 
 E.g.:
 
-[source, Java]
+[source,java,indent=0]
 ----
-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);
+include::{java-examples}/TuningTest.java[tags=sqlSelectPrefetch]
 ----
 
 `<<ejbql,EJBQLQuery>>` uses the "FETCH" keyword to enable prefetching:
 
 [source, SQL]
 ----
-SELECT a FROM Artist a LEFT JOIN FETCH a.paintings
+SELECT a FROM Artist a LEFT JOIN FETCH a.paintingArray
 ----
 
 ==== Data Rows
@@ -142,34 +126,22 @@ DataRow is a simply a map of values keyed by their DB column name. It is a ubiqu
 And it can be quite usable as is in the application in many cases. So performance sensitive selects should consider
 DataRows - it saves memory and CPU cycles. All selecting queries support DataRows option, e.g.:
 
-[source, Java]
+[source,java,indent=0]
 ----
-ObjectSelect<DataRow> query = ObjectSelect.dataRowQuery(Artist.class);
-
-List<DataRow> rows = query.select(context);
+include::{java-examples}/TuningTest.java[tags=dataRows]
 ----
 
-[source, Java]
+[source,java,indent=0]
 ----
-SQLSelect<DataRow> query = SQLSelect.dataRowQuery("SELECT * FROM ARTIST");
-List<DataRow> rows = query.select(context);
+include::{java-examples}/TuningTest.java[tags=sqlDataRows]
 ----
 
 Individual DataRows may be converted to Persistent objects as needed. So e.g. you may implement some in-memory filtering, only converting a subset of fetched objects:
 
 
-[source, Java]
+[source,java,indent=0]
 ----
-// you need to cast ObjectContext to DataContext to get access to 'objectFromDataRow'
-DataContext dataContext = (DataContext) context;
-
-for(DataRow row : rows) {
-    if(row.get("DATE_OF_BIRTH") != null) {
-        Artist artist = dataContext.objectFromDataRow(Artist.class, row);
-        // do something with Artist...
-        ...
-    }
-}
+include::{java-examples}/TuningTest.java[tags=objectFromDataRow]
 ----
 
 ==== Specific Attributes and Relationships with EJBQL
@@ -208,40 +180,25 @@ Cayenne handle resource management. In both cases iteration can be performed usi
 Direct access. Here common sense tells us that ResultIterators instances should be processed and closed as soon as possible to release the DB connection.
 E.g. storing open iterators between HTTP requests for unpredictable length of time would quickly exhaust the connection pool.
 
-[source, Java]
+[source,java,indent=0]
 ----
-try(ResultIterator<Artist> it = ObjectSelect.query(Artist.class).iterator(context)) {
-    for(Artist a : it) {
-       // do something with the object...
-       ...
-    }
-}
+include::{java-examples}/TuningTest.java[tags=iterator]
 ----
 
 Same thing with a callback:
 
-[source, Java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class).iterate(context, (Artist a) -> {
-    // do something with the object...
-    ...
-});
+include::{java-examples}/TuningTest.java[tags=iterate]
 ----
 
 Another example is a batch iterator that allows to process more than one object in each iteration.
 This is a common scenario in various data processing jobs - read a batch of objects, process them, commit the results,
 and then repeat. This allows to further optimize processing (e.g. by avoiding frequent commits).
 
-[source, Java]
+[source,java,indent=0]
 ----
-try(ResultBatchIterator<Artist> it = ObjectSelect.query(Artist.class).batchIterator(context, 100)) {
-    for(List<Artist> list : it) {
-       // do something with each list
-       ...
-       // possibly commit your changes
-       context.commitChanges();
-    }
-}
+include::{java-examples}/TuningTest.java[tags=batchIterator]
 ----
 
 ==== Paginated Queries
@@ -250,20 +207,17 @@ Enabling query pagination allows to load very large result sets in a Java app wi
 (much smaller than even the DataRows option discussed above). Moreover it is completely transparent to the application -
 a user gets what appears to be a list of Persistent objects - there's no iterator to close or DataRows to convert to objects:
 
-[source, Java]
+[source,java,indent=0]
 ----
-// the fact that result is paginated is transparent
-List<Artist> artists =
-    ObjectSelect.query(Artist.class).pageSize(50).select(context);
+include::{java-examples}/TuningTest.java[tags=pagination]
 ----
 
 Having said that, DataRows option can be combined with pagination, providing the best of both worlds:
 
 
-[source, Java]
+[source,java,indent=0]
 ----
-List<DataRow> rows =
-    ObjectSelect.dataRowQuery(Artist.class).pageSize(50).select(context);
+include::{java-examples}/TuningTest.java[tags=dataRowsPagination]
 ----
 
 The way pagination works internally, it first fetches a list of IDs for the root entity of the query. This is very
@@ -290,9 +244,9 @@ Local cache is much faster then the shared one, but is limited to a single conte
 To take advantage of query result caching, the first step is to mark your queries appropriately.
 Here is an example for ObjectSelect query. Other types of queries have similar API:
 
-[source, Java]
+[source,java,indent=0]
 ----
-ObjectSelect.query(Artist.class).localCache("artists");
+include::{java-examples}/TuningTest.java[tags=localCache]
 ----
 
 This tells Cayenne that the query created here would like to use local cache of the context it is executed against.
@@ -306,10 +260,9 @@ Use can use <<ext-jcache,JCache integration module>> to enable any of JCache API
 Often "passive" cache expiration policies used by caching providers are not sufficient, and the users want real-time cache invalidation when the data changes.
 So in addition to those policies, the app can invalidate individual cache groups explicitly via `QueryCache`:
 
-[source, Java]
+[source,java,indent=0]
 ----
-QueryCache cache = runtime.getDataDomain().getQueryCache();
-cache.removeGroup("artist");
+include::{java-examples}/TuningTest.java[tags=removeGroup]
 ----
 
 The above can be used e.g. to build UI for manual cache invalidation.
@@ -350,16 +303,9 @@ $ java -Dcayenne.contexts_sync_strategy=false
 
 Or by changing the standard properties Map in a custom extensions module:
 
-[source, Java]
+[source,java,indent=0]
 ----
-public class MyModule implements Module {
-
-    @Override
-    public void configure(Binder binder) {
-        CoreModule.contributeProperties(binder)
-            .put(Constants.CONTEXTS_SYNC_PROPERTY, "false");
-    }
-}
+include::{java-examples}/tuning/MyModule.java[tags=content]
 ----
 
 
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/cacheInvalidation.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/cacheInvalidation.adoc
index 35c4216b7..4708fbae5 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/cacheInvalidation.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/cacheInvalidation.adoc
@@ -42,12 +42,9 @@ compile 'org.apache.cayenne:cayenne-cache-invalidation:{version}'
 Module supports autoloading mechanism, so no other actions required to enable it. Just mark your entities with @CacheGroups
 annotation and you are ready to use it:
 
-[source, java]
+[source,java,indent=0]
 ----
-@CacheGroups("some-group")
-public class MyEntity extends _MyEntity {
-    // ...
-}
+include::{java-examples}/cacheinvalidation/MyEntity.java[tags=content]
 ----
 
 After any modification of `MyEntity` objects cache group `"some-group"` will be dropped from cache automatically.
@@ -60,25 +57,16 @@ To do so you need to implement `o.a.c.cache.invalidation.InvalidationHandler` in
 Invalidation module to use it. Let's use implementation class called `CustomInvalidationHandler` that will simply match
 all entities' types with `"custom-group"` cache group regardless of any annotations:
 
-[source, java]
+[source,java,indent=0]
 ----
-public class CustomInvalidationHandler implements InvalidationHandler {
-    @Override
-    public InvalidationFunction canHandle(Class<? extends Persistent> type) {
-        return p -> Collections.singleton(new CacheGroupDescriptor("custom-group"));
-    }
-}
+include::{java-examples}/cacheinvalidation/CustomInvalidationHandler.java[tags=content]
 ----
 
 Now we'll set up it's usage by `CayenneRuntime`:
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime.of()
-        .addModule(binder -> CacheInvalidationModule.extend(binder)
-                // optionally you can disable @CacheGroups annotation processing
-                .noCacheGroupsHandler()
-                .addHandler(CustomInvalidationHandler.class))
+include::{java-examples}/CacheInvalidationTest.java[tags=customHandler]
 ----
 
 NOTE: You can combine as many invalidation handlers as you need.
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/crypto.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/crypto.adoc
index 169e09e16..a444f27da 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/crypto.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/crypto.adoc
@@ -54,11 +54,9 @@ NOTE: Not all data types may be supported by your database.
 Default naming strategy that doesn't require additional setup suggests using "CRYPTO_" prefix. You can change this default
 strategy by injecting you own implementation of `o.a.c.crypto.map.ColumnMapper` interface.
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addModule(binder -> CryptoModule.extend(binder)
-                .columnMapper(MyColumnMapper.class));
+include::{java-examples}/CryptoExamples.java[tags=columnMapper]
 ----
 
 Here is an example of how `ObjEntity` with two encrypted and two unencrypted properties can look like:
@@ -70,11 +68,9 @@ image::../../images/ext-crypto-obj-entity.png[align="left"]
 To perform encryption you must provide `KEYSTORE_URL` and `KEY_PASSWORD`. Currently crypto module supports only Java "jceks"
 KeyStore.
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addModule(binder -> CryptoModule.extend(binder)
-                .keyStore(this.getClass().getResource("keystore.jcek"), "my-password".toCharArray(), "my-key-alias"));
+include::{java-examples}/CryptoExamples.java[tags=keyStore]
 ----
 
 ===== Additional settings
@@ -82,22 +78,17 @@ CayenneRuntime runtime = CayenneRuntime.of()
 In addition to `ColumnMapper` mentioned above you can customize other parts of `crypto module`. You can enable `gzip`
 compression and `HMAC` usage (later will ensure integrity of data).
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addModule(binder -> CryptoModule.extend(binder)
-                .compress()
-                .useHMAC());
+include::{java-examples}/CryptoExamples.java[tags=compressAndHMAC]
 ----
 
 Another useful extension point is support for custom Java value types. To add support for your data type you need to
 implement `o.a.c.crypto.transformer.value.BytesConverter` interface that will convert required type to and from `byte[]`.
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addModule(binder -> CryptoModule.extend(binder)
-                .objectToBytesConverter(MyClass.class, new MyClassBytesConverter()));
+include::{java-examples}/CryptoExamples.java[tags=bytesConverter]
 ----
 
 NOTE: In addition to Java primitive types (and their object counterparts), `crypto module` supports encryption only
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/jCache.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/jCache.adoc
index cb7229ab8..812f9360e 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/jCache.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part5/jCache.adoc
@@ -44,20 +44,16 @@ To use JCache provider in your app you need to include this module and caching p
 For advanced configuration and management please use provider specific options and tools.
 
 JCache module supports custom configuration files for cache managers.
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addModule(binder -> JCacheModule.extend(binder)
-            .setJCacheProviderConfig("cache-config.xml"));
+include::{java-examples}/JCacheExamples.java[tags=providerConfig]
 ----
 
 Also JCache module supports contribution of pre-configured cache manager.
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime.of()
-        .addModule(binder ->
-                binder.bind(CacheManager.class).toInstance(customCacheManager));
+include::{java-examples}/JCacheExamples.java[tags=cacheManager]
 ----
 
 NOTE: You can read about using cache in Cayenne in xref:caching[this] chapter.
@@ -79,11 +75,9 @@ First you need to include `ehcache` dependency:
 
 If you need custom configuration you can contribute configuration file to JCache module:
 
-[source, java]
+[source,java,indent=0]
 ----
-CayenneRuntime runtime = CayenneRuntime.of()
-        .addModule(binder -> JCacheModule.extend(binder)
-            .setJCacheProviderConfig("file:/ehcache.xml"));
+include::{java-examples}/JCacheExamples.java[tags=ehcacheConfig]
 ----
 
 As a result you will have `ehcache` manager as your default cache manager.
\ No newline at end of file
diff --git a/docs/cayenne-guide/src/docs/asciidoc/cayenne-guide.adoc b/docs/cayenne-guide/src/docs/asciidoc/cayenne-guide.adoc
index 658849308..de35f48d0 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/cayenne-guide.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/cayenne-guide.adoc
@@ -27,6 +27,9 @@
 //:linkcss:
 // base path to java code include
 :cayenne-root: {basedir}/../../..
+// Java examples are included from the tests, so that they are compiled and run against the current Cayenne code
+:java-examples: {basedir}/src/test/java/org/apache/cayenne/docs
+:resource-examples: {basedir}/src/test/resources/org/apache/cayenne/docs
 
 [small]#Copyright © 2011-{docyear} Apache Software Foundation and individual authors#
 
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/BaseTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/BaseTest.java
new file mode 100644
index 000000000..31f19a4d3
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/BaseTest.java
@@ -0,0 +1,155 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.configuration.DataNodeDescriptor;
+import org.apache.cayenne.datasource.CayenneDataSource;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Gallery;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.SQLExec;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * A superclass of the tests that hold the code examples included in the documentation. The examples are the parts of
+ * the test code between the "tag::xyz[]" and "end::xyz[]" comments. As they are compiled and run with the rest of the
+ * tests, they can not get out of sync with the Cayenne API.
+ */
+public abstract class BaseTest {
+
+    protected static CayenneRuntime runtime;
+
+    protected ObjectContext context;
+
+    @BeforeAll
+    public static void startRuntime() {
+        DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:docs").pool(1, 3).build();
+
+        DataNodeDescriptor dataNode = DataNodeDescriptor.of("docs")
+                .dataSource(dataSource)
+                .createSchemaIfNeeded()
+                .build();
+
+        // optional modules (crypto, jcache, etc.) are on the classpath for their own examples to compile, but should
+        // not be a part of this runtime
+        runtime = CayenneRuntime.of()
+                .disableModulesAutoLoading()
+                .addConfig("cayenne-project.xml")
+                .defaultDataNode(dataNode)
+                .build();
+    }
+
+    @AfterAll
+    public static void stopRuntime() {
+        runtime.shutdown();
+        runtime = null;
+    }
+
+    @BeforeEach
+    public void resetData() {
+        context = runtime.newContext();
+
+        for (String table : List.of("PAINTING", "ARTIST", "GALLERY", "COMPOUND_PK", "GENERIC_ENTITY")) {
+            SQLExec.query("DELETE FROM " + table).update(context);
+        }
+    }
+
+    protected Artist createArtist(String name, LocalDate dateOfBirth) {
+        Artist artist = context.newObject(Artist.class);
+        artist.setArtistName(name);
+        artist.setDateOfBirth(dateOfBirth);
+        return artist;
+    }
+
+    protected Gallery createGallery(String name) {
+        Gallery gallery = context.newObject(Gallery.class);
+        gallery.setGalleryName(name);
+        return gallery;
+    }
+
+    protected Painting createPainting(String title, int price, Artist artist, Gallery gallery) {
+        Painting painting = context.newObject(Painting.class);
+        painting.setPaintingTitle(title);
+        painting.setEstimatedPrice(BigDecimal.valueOf(price));
+        painting.setToArtist(artist);
+        painting.setToGallery(gallery);
+        return painting;
+    }
+
+    /**
+     * Creates and commits 3 artists, 2 galleries and 4 paintings.
+     */
+    protected void createArtistsDataSet() {
+        Artist dali = createArtist("Dali", LocalDate.of(1904, 5, 11));
+        Artist picasso = createArtist("Pablo Picasso", LocalDate.of(1881, 10, 25));
+        createArtist("Aivazovsky", LocalDate.of(1817, 7, 29));
+
+        Gallery moma = createGallery("MoMA");
+        Gallery prado = createGallery("Prado");
+
+        createPainting("P1", 100, dali, moma);
+        createPainting("P2", 2000, dali, prado);
+        createPainting("Guernica", 300000, picasso, prado);
+        createPainting("Gertrude Stein", 5000, picasso, null);
+
+        context.commitChanges();
+    }
+
+    /**
+     * Returns a non-Java example (such as a query String) from a test resource file. The example is the text between
+     * the "tag::xyz[]" and "end::xyz[]" lines. This allows the tests to check the same text that goes in the docs.
+     */
+    protected static String example(String resource, String tag) {
+
+        List<String> lines;
+        try (InputStream in = BaseTest.class.getResourceAsStream(resource)) {
+            lines = new String(in.readAllBytes(), StandardCharsets.UTF_8).lines().collect(Collectors.toList());
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+
+        int from = indexOfLineWith(lines, "tag::" + tag + "[]");
+        int to = indexOfLineWith(lines, "end::" + tag + "[]");
+        return String.join("\n", lines.subList(from + 1, to));
+    }
+
+    private static int indexOfLineWith(List<String> lines, String marker) {
+        for (int i = 0; i < lines.size(); i++) {
+            if (lines.get(i).contains(marker)) {
+                return i;
+            }
+        }
+
+        throw new IllegalArgumentException("No such marker: " + marker);
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CacheInvalidationTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CacheInvalidationTest.java
new file mode 100644
index 000000000..3a0628c29
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CacheInvalidationTest.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.docs;
+
+import org.apache.cayenne.cache.invalidation.CacheInvalidationModule;
+import org.apache.cayenne.docs.cacheinvalidation.CustomInvalidationHandler;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class CacheInvalidationTest {
+
+    @Test
+    public void customHandler() {
+        // tag::customHandler[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> CacheInvalidationModule.extend(binder)
+                        // optionally you can disable @CacheGroups annotation processing
+                        .noCacheGroupsHandler()
+                        .addHandler(CustomInvalidationHandler.class))
+                .build();
+        // end::customHandler[]
+
+        assertNotNull(runtime);
+        runtime.shutdown();
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CommitLogTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CommitLogTest.java
new file mode 100644
index 000000000..44fbebefd
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CommitLogTest.java
@@ -0,0 +1,138 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.commitlog.CommitLogListener;
+import org.apache.cayenne.configuration.DataNodeDescriptor;
+import org.apache.cayenne.configuration.runtime.CoreModule;
+import org.apache.cayenne.datasource.CayenneDataSource;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.docs.commitlog.AuditListener;
+import org.apache.cayenne.docs.commitlog.MyEntityFactory;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class CommitLogTest {
+
+    /**
+     * Commits an Artist and a Painting, and returns the names of the entities that were reported to the listener.
+     */
+    private List<String> commitAndCollectChanges(Module commitLogModule) {
+
+        List<String> changed = new ArrayList<>();
+        CommitLogListener collector = (c, changes) -> changes.getUniqueChanges()
+                .forEach(ch -> changed.add(ch.getPostCommitId().getEntityName()));
+
+        DataNodeDescriptor dataNode = DataNodeDescriptor.of("commitlog")
+                .dataSource(CayenneDataSource.of("jdbc:hsqldb:mem:commitlog").build())
+                .createSchemaIfNeeded()
+                .build();
+
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .defaultDataNode(dataNode)
+                .addModule(commitLogModule)
+                .addModule(b -> CoreModule.extend(b).addCommitLogListener(collector))
+                .build();
+
+        try {
+            ObjectContext context = runtime.newContext();
+            context.newObject(Artist.class).setArtistName("a1");
+            context.newObject(Painting.class).setPaintingTitle("p1");
+            context.commitChanges();
+        } finally {
+            runtime.shutdown();
+        }
+
+        changed.sort(null);
+        return changed;
+    }
+
+    @Test
+    public void register() {
+        // tag::register[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .addModule(binder -> CoreModule.extend(binder)
+                        .addCommitLogListener(AuditListener.class))
+                .build();
+        // end::register[]
+
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+
+    @Test
+    public void listener() {
+        List<String> changed = commitAndCollectChanges(b -> CoreModule.extend(b)
+                .addCommitLogListener(AuditListener.class));
+
+        assertEquals(List.of("Artist", "Painting"), changed);
+    }
+
+    @Test
+    public void annotationEntitiesOnly() {
+        // no entities in the docs model are annotated with @CommitLog
+        assertEquals(List.of(), commitAndCollectChanges(this::annotationEntitiesOnly));
+    }
+
+    private void annotationEntitiesOnly(Binder binder) {
+        // tag::annotationEntitiesOnly[]
+        CoreModule.extend(binder)
+                .commitLogAnnotationEntitiesOnly()
+                .addCommitLogListener(AuditListener.class);
+        // end::annotationEntitiesOnly[]
+    }
+
+    @Test
+    public void excludeFromTransaction() {
+        assertEquals(List.of("Artist", "Painting"), commitAndCollectChanges(this::excludeFromTransaction));
+    }
+
+    private void excludeFromTransaction(Binder binder) {
+        // tag::excludeFromTransaction[]
+        CoreModule.extend(binder)
+                .excludeCommitLogFromTransaction()
+                .addCommitLogListener(AuditListener.class);
+        // end::excludeFromTransaction[]
+    }
+
+    @Test
+    public void entityFactory() {
+        assertEquals(List.of("Artist"), commitAndCollectChanges(this::entityFactory));
+    }
+
+    private void entityFactory(Binder binder) {
+        // tag::entityFactory[]
+        CoreModule.extend(binder)
+                .commitLogEntityFactory(MyEntityFactory.class)
+                .addCommitLogListener(AuditListener.class);
+        // end::entityFactory[]
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CryptoExamples.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CryptoExamples.java
new file mode 100644
index 000000000..7fecc9023
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CryptoExamples.java
@@ -0,0 +1,71 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.crypto.CryptoModule;
+import org.apache.cayenne.docs.crypto.MyClass;
+import org.apache.cayenne.docs.crypto.MyClassBytesConverter;
+import org.apache.cayenne.docs.crypto.MyColumnMapper;
+import org.apache.cayenne.runtime.CayenneRuntime;
+
+/**
+ * Examples that are only compiled, but not run, as there is no keystore or encrypted columns in the docs test project.
+ */
+public class CryptoExamples {
+
+    public void columnMapper() {
+        // tag::columnMapper[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> CryptoModule.extend(binder)
+                        .columnMapper(MyColumnMapper.class))
+                .build();
+        // end::columnMapper[]
+    }
+
+    public void keyStore() {
+        // tag::keyStore[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> CryptoModule.extend(binder)
+                        .keyStore(
+                                this.getClass().getResource("keystore.jcek"),
+                                "my-password".toCharArray(),
+                                "my-key-alias"))
+                .build();
+        // end::keyStore[]
+    }
+
+    public void compressAndHMAC() {
+        // tag::compressAndHMAC[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> CryptoModule.extend(binder)
+                        .compress()
+                        .useHMAC())
+                .build();
+        // end::compressAndHMAC[]
+    }
+
+    public void bytesConverter() {
+        // tag::bytesConverter[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> CryptoModule.extend(binder)
+                        .objectToBytesConverter(MyClass.class, new MyClassBytesConverter()))
+                .build();
+        // end::bytesConverter[]
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomQueryTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomQueryTest.java
new file mode 100644
index 000000000..40ac3c423
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomQueryTest.java
@@ -0,0 +1,46 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.QueryResult;
+import org.apache.cayenne.docs.customquery.MyDelegatingQuery;
+import org.apache.cayenne.docs.customquery.MyQuery;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class CustomQueryTest extends BaseTest {
+
+    @Test
+    public void myQuery() {
+        assertEquals(0, context.execute(new MyQuery()).size());
+    }
+
+    @Test
+    public void myDelegatingQuery() {
+        createArtistsDataSet();
+
+        List<QueryResult> result = context.execute(new MyDelegatingQuery());
+        QueryResult.Select<?> select = assertInstanceOf(QueryResult.Select.class, result.getFirst());
+        assertEquals(3, select.objects().size());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomizeTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomizeTest.java
new file mode 100644
index 000000000..fb6434b13
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/CustomizeTest.java
@@ -0,0 +1,175 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.configuration.runtime.CoreModule;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.DIBootstrap;
+import org.apache.cayenne.di.Injector;
+import org.apache.cayenne.di.Key;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.docs.customize.DoubleArrayType;
+import org.apache.cayenne.docs.customize.MoneyValueObjectType;
+import org.apache.cayenne.docs.customize.MyDbAdapterDetector;
+import org.apache.cayenne.docs.customize.MyExtensionsModule;
+import org.apache.cayenne.docs.di.Module1;
+import org.apache.cayenne.docs.di.Service1;
+import org.apache.cayenne.docs.di.Service1Provider;
+import org.apache.cayenne.docs.di.Service2;
+import org.apache.cayenne.docs.di.Service2Impl;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+
+public class CustomizeTest {
+
+    @Test
+    public void injector() {
+        // tag::injector[]
+        Injector injector = DIBootstrap.createInjector(new Module1());
+        // end::injector[]
+
+        // tag::getInstance[]
+        Service1 s1 = injector.getInstance(Service1.class);
+        for (int i = 0; i < 5; i++) {
+            System.out.println("S1 String: " + s1.getString());
+        }
+        // end::getInstance[]
+
+        // the 5 calls above must have incremented the counter in the injected Service2
+        assertEquals("5_Service1Impl", s1.getString());
+    }
+
+    @Test
+    public void constructorInjection() {
+        Injector injector = DIBootstrap.createInjector(b -> {
+            b.bind(Service1.class).to(org.apache.cayenne.docs.di.constructor.Service1Impl.class);
+            b.bind(Service2.class).to(Service2Impl.class);
+        });
+
+        assertEquals("0_Service1Impl", injector.getInstance(Service1.class).getString());
+    }
+
+    @Test
+    public void bindings() {
+        Module providers = binder -> {
+            // tag::bindings[]
+            // binding to instance - allowing user to create and configure instance
+            // inside the module class
+            binder.bind(Service2.class).toInstance(new Service2Impl());
+
+            // binding to provider - delegating instance creation to a special
+            // provider class
+            binder.bind(Service1.class).toProvider(Service1Provider.class);
+
+            // binding to provider instance
+            binder.bind(Service1.class).toProviderInstance(new Service1Provider());
+
+            // multiple bindings of the same type using Key
+            // injection can reference the key name in annotation:
+            // @Inject("i1")
+            // private Service2 service2;
+            binder.bind(Key.get(Service2.class, "i1")).to(Service2Impl.class);
+            binder.bind(Key.get(Service2.class, "i2")).to(Service2Impl.class);
+            // end::bindings[]
+        };
+
+        Injector injector = DIBootstrap.createInjector(providers);
+        assertEquals("Service1Provider", injector.getInstance(Service1.class).getString());
+        assertNotSame(
+                injector.getInstance(Key.get(Service2.class, "i1")),
+                injector.getInstance(Key.get(Service2.class, "i2")));
+    }
+
+    @Test
+    public void withoutScope() {
+        Injector injector = DIBootstrap.createInjector(binder -> {
+            // tag::withoutScope[]
+            binder.bind(Service2.class).to(Service2Impl.class).withoutScope();
+            // end::withoutScope[]
+        });
+
+        assertNotSame(injector.getInstance(Service2.class), injector.getInstance(Service2.class));
+    }
+
+    @Test
+    public void extensionsModule() {
+        // tag::extensionsModule[]
+        Module extensions = new MyExtensionsModule();
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("com/example/cayenne-mydomain.xml")
+                .addModule(extensions)
+                .build();
+        // end::extensionsModule[]
+
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+
+    @Test
+    public void adapterDetector() {
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .addModule(this::adapterDetector)
+                .build();
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+
+    private void adapterDetector(Binder binder) {
+        // tag::adapterDetector[]
+        CoreModule.extend(binder)
+                .addAdapterDetector(MyDbAdapterDetector.class);
+        // end::adapterDetector[]
+    }
+
+    @Test
+    public void valueObjectType() {
+        // tag::valueObjectType[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .addModule(binder ->
+                        CoreModule.extend(binder)
+                                .addValueObjectType(MoneyValueObjectType.class))
+                .build();
+        // end::valueObjectType[]
+
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+
+    @Test
+    public void extendedType() {
+        // tag::extendedType[]
+        // add DoubleArrayType to list of user types
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .addModule(binder ->
+                        CoreModule.extend(binder)
+                                .addUserExtendedType(new DoubleArrayType()))
+                .build();
+        // end::extendedType[]
+
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/EJBQLQueryTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/EJBQLQueryTest.java
new file mode 100644
index 000000000..6a40c2e39
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/EJBQLQueryTest.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.docs;
+
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.EJBQLQuery;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class EJBQLQueryTest extends BaseTest {
+
+    @Test
+    public void create() {
+        // tag::create[]
+        EJBQLQuery<Artist> query = new EJBQLQuery<>("select a FROM Artist a");
+        // end::create[]
+
+        assertNotNull(query);
+    }
+
+    @Test
+    public void select() {
+        createArtistsDataSet();
+
+        // tag::select[]
+        EJBQLQuery<Artist> select =
+                new EJBQLQuery<>("select a FROM Artist a WHERE a.artistName = 'Dali'");
+        List<Artist> artists = context.select(select);
+        // end::select[]
+
+        assertEquals(1, artists.size());
+    }
+
+    @Test
+    public void delete() {
+        createArtistsDataSet();
+
+        // tag::delete[]
+        EJBQLQuery<?> delete = new EJBQLQuery<>("delete from Painting");
+        context.execute(delete);
+        // end::delete[]
+
+        assertEquals(0, context.select(new EJBQLQuery<Painting>("select p FROM Painting p")).size());
+    }
+
+    @Test
+    public void update() {
+        createArtistsDataSet();
+
+        // tag::update[]
+        EJBQLQuery<?> update =
+                new EJBQLQuery<>("UPDATE Painting AS p SET p.paintingTitle = 'P3' WHERE p.paintingTitle = 'P1'");
+        context.execute(update);
+        // end::update[]
+
+        assertEquals(1, context
+                .select(new EJBQLQuery<Painting>("select p FROM Painting p WHERE p.paintingTitle = 'P3'"))
+                .size());
+    }
+
+    @Test
+    public void objectsAndScalars() {
+        createArtistsDataSet();
+
+        // tag::objectsAndScalars[]
+        EJBQLQuery<Object[]> query =
+                new EJBQLQuery<>("select a, COUNT(p) FROM Artist a JOIN a.paintingArray p GROUP BY a");
+        List<Object[]> result = context.select(query);
+        for (Object[] artistWithCount : result) {
+            Artist a = (Artist) artistWithCount[0];
+            long hasPaintings = (Long) artistWithCount[1];
+        }
+        // end::objectsAndScalars[]
+
+        assertEquals(2, result.size());
+    }
+
+    @Test
+    public void scalars() {
+        createArtistsDataSet();
+
+        // tag::scalars[]
+        EJBQLQuery<String> query = new EJBQLQuery<>("select a.artistName FROM Artist a");
+        List<String> names = context.select(query);
+        // end::scalars[]
+
+        assertEquals(3, names.size());
+    }
+
+    @Test
+    public void inPositional() {
+        createArtistsDataSet();
+
+        // tag::inPositional[]
+        EJBQLQuery<Painting> query =
+                new EJBQLQuery<>("select p from Painting p where p.paintingTitle in (?1,?2,?3)");
+        query.setParameter(1, "P1");
+        query.setParameter(2, "P2");
+        query.setParameter(3, "P3");
+        // end::inPositional[]
+
+        assertEquals(2, context.select(query).size());
+    }
+
+    @Test
+    public void inCollection() {
+        createArtistsDataSet();
+
+        // tag::inCollection[]
+        EJBQLQuery<Painting> query = new EJBQLQuery<>("select p from Painting p where p.paintingTitle in ?1");
+        query.setParameter(1, List.of("P1", "P2", "P3"));
+        // end::inCollection[]
+
+        assertEquals(2, context.select(query).size());
+    }
+
+    @Test
+    public void inCollectionInParentheses() {
+        createArtistsDataSet();
+
+        // tag::inCollectionInParentheses[]
+        EJBQLQuery<Painting> query = new EJBQLQuery<>("select p from Painting p where p.paintingTitle in (?1)");
+        query.setParameter(1, List.of("P1", "P2", "P3"));
+        // end::inCollectionInParentheses[]
+
+        assertEquals(2, context.select(query).size());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java
new file mode 100644
index 000000000..7926ec2a6
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java
@@ -0,0 +1,236 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.exp.Expression;
+import org.apache.cayenne.exp.ExpressionException;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.query.EJBQLQuery;
+import org.apache.cayenne.query.ObjectSelect;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class ExpressionsTest extends BaseTest {
+
+    private Artist dali() {
+        return ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("Dali")).selectOne(context);
+    }
+
+    @Test
+    public void exp() {
+        // tag::exp[]
+        String expString = "name like 'A%' and price < 1000";
+        Expression exp = ExpressionFactory.exp(expString);
+        // end::exp[]
+
+        assertEquals("(name like \"A%\") and (price < 1000)", exp.toString());
+    }
+
+    /**
+     * Checks the expression Strings from "expressions.txt", that is included in the docs.
+     */
+    @Test
+    public void expStrings() {
+        List<String> valid = List.of("caseValid", "grouping", "objPath", "objPrefixPath", "multiSegmentPath", "dbPath", "like");
+        for (String tag : valid) {
+            assertNotNull(ExpressionFactory.exp(example("expressions.txt", tag)), tag);
+        }
+
+        assertEquals(ExpressionFactory.exp(example("expressions.txt", "objPath")),
+                ExpressionFactory.exp(example("expressions.txt", "objPrefixPath")));
+
+        assertThrows(ExpressionException.class, () -> ExpressionFactory.exp(example("expressions.txt", "caseInvalid")));
+    }
+
+    @Test
+    public void params() {
+        // tag::params[]
+        Expression template = ExpressionFactory.exp("name = $name");
+
+        // name binding
+        Map<String, Object> p1 = Map.of("name", "Salvador Dali");
+        Expression qualifier1 = template.params(p1);
+
+        // positional binding
+        Expression qualifier2 = template.paramsArray("Monet");
+        // end::params[]
+
+        assertEquals("name = \"Salvador Dali\"", qualifier1.toString());
+        assertEquals("name = \"Monet\"", qualifier2.toString());
+    }
+
+    @Test
+    public void expWithParams() {
+        // tag::expWithParams[]
+        Expression qualifier = ExpressionFactory.exp("name = $name", "Monet");
+        // end::expWithParams[]
+
+        assertEquals("name = \"Monet\"", qualifier.toString());
+    }
+
+    @Test
+    public void likeParam() {
+        // tag::likeParam[]
+        Expression qualifier = ExpressionFactory.exp("name like $name", "Salvador%");
+        // end::likeParam[]
+
+        assertEquals("name like \"Salvador%\"", qualifier.toString());
+    }
+
+    @Test
+    public void objectParam() {
+        createArtistsDataSet();
+
+        // tag::objectParam[]
+        Artist dali = dali(); // assume we fetched this one already
+        Expression qualifier = ExpressionFactory.exp("toArtist = $artist", dali);
+        // end::objectParam[]
+
+        assertEquals(2, ObjectSelect.query(Painting.class).where(qualifier).select(context).size());
+    }
+
+    @Test
+    public void pruning() {
+        // tag::pruning[]
+        Expression template = ExpressionFactory.exp("name like $name and dateOfBirth > $date");
+
+        Map<String, Object> p1 = Map.of("name", "Salvador%");
+        Expression qualifier1 = template.params(p1);
+
+        // "qualifier1" is now "name like 'Salvador%'".
+        // 'dateOfBirth > $date' condition was pruned, as no value was specified for
+        // the $date parameter
+        // end::pruning[]
+
+        assertEquals("(name like \"Salvador%\")", qualifier1.toString());
+    }
+
+    @Test
+    public void expressionFactory() {
+        // tag::expressionFactory[]
+        // String expression: name like 'A%' and price < 1000
+        Expression e1 = ExpressionFactory.likeExp("name", "A%");
+        Expression e2 = ExpressionFactory.lessExp("price", 1000);
+        Expression finalExp = e1.andExp(e2);
+        // end::expressionFactory[]
+
+        assertEquals(ExpressionFactory.exp("name like 'A%' and price < 1000"), finalExp);
+    }
+
+    @Test
+    public void properties() {
+        // tag::properties[]
+        // Artist.ARTIST_NAME is generated by Cayenne and has a type of StringProperty<String>
+        Expression e1 = Artist.ARTIST_NAME.eq("Pablo");
+
+        // Chaining multiple properties into a path.
+        // Painting.TO_ARTIST is generated by Cayenne and has a type of EntityProperty<Artist>
+        Expression e2 = Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).eq("Pablo");
+        // end::properties[]
+
+        assertEquals(ExpressionFactory.exp("artistName = 'Pablo'"), e1);
+        assertEquals(ExpressionFactory.exp("toArtist.artistName = 'Pablo'"), e2);
+    }
+
+    @Test
+    public void match() {
+        createArtistsDataSet();
+        boolean[] matched = new boolean[1];
+
+        // tag::match[]
+        Expression e = Artist.ARTIST_NAME.in("Dali", "Monet");
+        Artist artist = dali();
+        if (e.match(artist)) {
+            // do something with the matching object...
+            // end::match[]
+            matched[0] = true;
+            // tag::match[]
+        }
+        // end::match[]
+
+        assertTrue(matched[0]);
+    }
+
+    @Test
+    public void evaluate() {
+        createArtistsDataSet();
+        Artist artist = dali();
+
+        // tag::evaluate[]
+        String name = (String) Artist.ARTIST_NAME.getExpression().evaluate(artist);
+        // end::evaluate[]
+
+        assertEquals("Dali", name);
+    }
+
+    @Test
+    public void filterObjects() {
+        createArtistsDataSet();
+
+        // tag::filterObjects[]
+        Expression e = Artist.ARTIST_NAME.in("Dali", "Monet");
+        List<Artist> unfiltered = ObjectSelect.query(Artist.class).select(context);
+        List<Artist> filtered = e.filterObjects(unfiltered);
+        // end::filterObjects[]
+
+        assertEquals(3, unfiltered.size());
+        assertEquals(1, filtered.size());
+    }
+
+    @Test
+    public void toEJBQL() {
+        createArtistsDataSet();
+
+        // tag::toEJBQL[]
+        String title = "Guernica";
+        Expression e = Painting.PAINTING_TITLE.eq(title);
+        List<Object> params = new ArrayList<>();
+        EJBQLQuery<Painting> query = new EJBQLQuery<>("SELECT p FROM Painting p WHERE " + e.toEJBQL(params, "p"));
+
+        for (int i = 0; i < params.size(); i++) {
+            query.setParameter(i + 1, params.get(i));
+        }
+        // end::toEJBQL[]
+
+        assertEquals(1, context.select(query).size());
+    }
+
+    @Test
+    public void ejbql() {
+        createArtistsDataSet();
+        String title = "Guernica";
+
+        // tag::ejbql[]
+        EJBQLQuery<Painting> query = new EJBQLQuery<>("SELECT p FROM Painting p WHERE p.paintingTitle = ?1");
+        query.setParameter(1, title);
+        // end::ejbql[]
+
+        assertEquals(1, context.select(query).size());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/JCacheExamples.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/JCacheExamples.java
new file mode 100644
index 000000000..533b4192b
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/JCacheExamples.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.jcache.JCacheModule;
+import org.apache.cayenne.runtime.CayenneRuntime;
+
+import javax.cache.CacheManager;
+
+/**
+ * Examples that are only compiled, but not run, as they refer to the cache configuration files that are not a part of
+ * the docs test project.
+ */
+public class JCacheExamples {
+
+    private CacheManager customCacheManager;
+
+    public void providerConfig() {
+        // tag::providerConfig[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> JCacheModule.extend(binder)
+                        .setJCacheProviderConfig("cache-config.xml"))
+                .build();
+        // end::providerConfig[]
+    }
+
+    public void cacheManager() {
+        // tag::cacheManager[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder ->
+                        binder.bind(CacheManager.class).toInstance(customCacheManager))
+                .build();
+        // end::cacheManager[]
+    }
+
+    public void ehcacheConfig() {
+        // tag::ehcacheConfig[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addModule(binder -> JCacheModule.extend(binder)
+                        .setJCacheProviderConfig("file:/ehcache.xml"))
+                .build();
+        // end::ehcacheConfig[]
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/LifecycleTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/LifecycleTest.java
new file mode 100644
index 000000000..3ff23aae7
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/LifecycleTest.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.docs;
+
+import org.apache.cayenne.configuration.runtime.CoreModule;
+import org.apache.cayenne.docs.lifecycle.CommittedObjectCounter;
+import org.apache.cayenne.docs.lifecycle.Listener1;
+import org.apache.cayenne.docs.lifecycle.Listener2;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class LifecycleTest {
+
+    @Test
+    public void listeners() {
+        // tag::listeners[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                // ..
+                .addModule(binder ->
+                        CoreModule.extend(binder)
+                                .addListenerType(Listener1.class)
+                                .addListener(new Listener2())
+                )
+                // ..
+                .build();
+        // end::listeners[]
+
+        assertNotNull(runtime);
+        runtime.shutdown();
+    }
+
+    @Test
+    public void syncFilter() {
+        // tag::syncFilter[]
+        // this will also add filter as a listener
+        CayenneRuntime runtime = CayenneRuntime.of()
+                // ..
+                .addModule(b ->
+                        CoreModule.extend(b)
+                                .addSyncFilter(CommittedObjectCounter.class)
+                )
+                // ..
+                .build();
+        // end::syncFilter[]
+
+        assertNotNull(runtime);
+        runtime.shutdown();
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/MappedQueryTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/MappedQueryTest.java
new file mode 100644
index 000000000..a3b90625f
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/MappedQueryTest.java
@@ -0,0 +1,60 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.QueryResult;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.query.MappedExec;
+import org.apache.cayenne.query.MappedSelect;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class MappedQueryTest extends BaseTest {
+
+    @Test
+    public void mappedSelect() {
+        createArtistsDataSet();
+
+        // tag::mappedSelect[]
+        List<Artist> results = MappedSelect.query("artistsByName", Artist.class)
+                .param("name", "Dali")
+                .select(context);
+        // end::mappedSelect[]
+
+        assertEquals(1, results.size());
+    }
+
+    @Test
+    public void mappedExec() {
+        createArtistsDataSet();
+
+        // tag::mappedExec[]
+        List<QueryResult> result = MappedExec.query("updatePaintingTitle")
+                .param("title", "Untitled")
+                .execute(context);
+        QueryResult.Update update = (QueryResult.Update) result.getFirst();
+        System.out.println("Rows updated: " + update.count());
+        // end::mappedExec[]
+
+        assertEquals(4, update.count());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectContextTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectContextTest.java
new file mode 100644
index 000000000..9e9a580fa
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectContextTest.java
@@ -0,0 +1,330 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.Cayenne;
+import org.apache.cayenne.GenericPersistentObject;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.PersistenceState;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.QueryResult;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SQLExec;
+import org.apache.cayenne.tx.BaseTransaction;
+import org.apache.cayenne.tx.Transaction;
+import org.apache.cayenne.tx.TransactionDescriptor;
+import org.apache.cayenne.tx.TransactionPropagation;
+import org.apache.cayenne.tx.TransactionalOperation;
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.util.Collection;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+public class ObjectContextTest extends BaseTest {
+
+    private long count() {
+        return ObjectSelect.query(Artist.class).selectCount(runtime.newContext());
+    }
+
+    @Test
+    public void newContext() {
+        // tag::newContext[]
+        ObjectContext context = runtime.newContext();
+        // end::newContext[]
+
+        assertNotNull(context);
+    }
+
+    @Test
+    public void selectModifyCommit() {
+        createArtistsDataSet();
+
+        // tag::select[]
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .select(context);
+        // end::select[]
+
+        // tag::modify[]
+        Artist selectedArtist = artists.get(0);
+        selectedArtist.setArtistName("Dali");
+        // end::modify[]
+
+        // tag::commit[]
+        context.commitChanges();
+        // end::commit[]
+
+        assertEquals(PersistenceState.COMMITTED, selectedArtist.getPersistenceState());
+    }
+
+    @Test
+    public void rollback() {
+        createArtist("Dali", null);
+
+        // tag::rollback[]
+        context.rollbackChanges();
+        // end::rollback[]
+
+        assertEquals(0, count());
+    }
+
+    @Test
+    public void execute() {
+        createArtistsDataSet();
+        Query query = SQLExec.query("UPDATE ARTIST SET ARTIST_NAME = 'Unknown'");
+
+        // tag::execute[]
+        List<QueryResult> result = context.execute(query);
+        for (QueryResult item : result) {
+            switch (item) {
+                case QueryResult.Select<?> select -> process(select.objects());
+                case QueryResult.Update update -> process(update.counts());
+                case QueryResult.Iterator<?> iterator -> process(iterator.iterator());
+                case QueryResult.OutParameters out -> process(out.values());
+            }
+        }
+        // end::execute[]
+
+        assertEquals(1, result.size());
+    }
+
+    private void process(Object result) {
+    }
+
+    @Test
+    public void executeInTransaction() {
+        createArtistsDataSet();
+
+        // tag::executeInTransaction[]
+        Collection<Query> queries = List.of( // multiple queries that need to be run together
+                SQLExec.query("DELETE FROM PAINTING"),
+                SQLExec.query("DELETE FROM ARTIST"));
+
+        runtime.performInTransaction(() -> {
+            queries.forEach(context::execute);
+            return null;
+        });
+        // end::executeInTransaction[]
+
+        assertEquals(0, count());
+    }
+
+    @Test
+    public void newObject() {
+        // tag::newObject[]
+        Artist newArtist = context.newObject(Artist.class);
+        newArtist.setArtistName("Picasso");
+        // end::newObject[]
+
+        assertEquals(PersistenceState.NEW, newArtist.getPersistenceState());
+    }
+
+    @Test
+    public void deleteObjects() {
+        Artist artist1 = createArtist("a1", null);
+        Artist artist2 = createArtist("a2", null);
+        Artist artist3 = createArtist("a3", null);
+        Artist artist4 = createArtist("a4", null);
+        context.commitChanges();
+
+        // tag::deleteObjects[]
+        context.deleteObjects(artist1);
+        context.deleteObjects(artist2, artist3, artist4);
+        // end::deleteObjects[]
+
+        context.commitChanges();
+        assertEquals(0, count());
+    }
+
+    @Test
+    public void localObject() {
+        Artist artist = createArtist("a1", null);
+        context.commitChanges();
+
+        // tag::localObject[]
+        ObjectContext editingContext = runtime.newContext();
+        Artist localArtist = editingContext.localObject(artist);
+        // end::localObject[]
+
+        assertNotSame(artist, localArtist);
+        assertEquals(artist.getObjectId(), localArtist.getObjectId());
+    }
+
+    @Test
+    public void entityResolver() {
+        // tag::entityResolver[]
+        EntityResolver resolver = context.getEntityResolver();
+        // end::entityResolver[]
+
+        assertNotNull(resolver.getObjEntity("Artist"));
+    }
+
+    @Test
+    public void pk() {
+        SQLExec.query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (34579, 'Dali')").update(context);
+
+        // tag::objectForPK[]
+        Artist artist = context.objectForPK(Artist.class, 34579);
+        // end::objectForPK[]
+
+        // tag::pkForObject[]
+        long pk = Cayenne.longPKForObject(artist);
+        // end::pkForObject[]
+
+        assertEquals(34579L, pk);
+    }
+
+    @Test
+    public void nesting() {
+        // tag::nesting[]
+        ObjectContext parent = runtime.newContext();
+        ObjectContext nested = runtime.newContext(parent);
+        // end::nesting[]
+
+        Artist artist = nested.newObject(Artist.class);
+        artist.setArtistName("a1");
+
+        // tag::nestedCommit[]
+        // merges nested context changes into the parent context
+        nested.commitChangesToParent();
+
+        // end::nestedCommit[]
+        assertEquals(0, count());
+        artist.setArtistName("a1.1");
+        // tag::nestedCommit[]
+        // regular 'commitChanges' cascades commit through the chain
+        // of parent contexts all the way to the database
+        nested.commitChanges();
+        // end::nestedCommit[]
+
+        assertEquals(1, count());
+        artist.setArtistName("a2");
+
+        // tag::nestedRollback[]
+        // unrolls all local changes, getting context in a state identical to parent
+        nested.rollbackChangesLocally();
+
+        // end::nestedRollback[]
+        assertEquals("a1.1", artist.getArtistName());
+        artist.setArtistName("a3");
+        // tag::nestedRollback[]
+        // regular 'rollbackChanges' cascades rollback through the chain of contexts
+        // all the way to the topmost parent
+        nested.rollbackChanges();
+        // end::nestedRollback[]
+
+        assertEquals("a1.1", artist.getArtistName());
+    }
+
+    @Test
+    public void genericNewObject() {
+        // tag::genericNewObject[]
+        Persistent generic = context.newObject("GenericEntity");
+        // end::genericNewObject[]
+
+        // tag::genericProperties[]
+        String name = (String) generic.readProperty("name");
+        generic.writeProperty("name", "New Name");
+        // end::genericProperties[]
+
+        // tag::genericEntityName[]
+        String entityName = generic.getObjectId().getEntityName();
+        // end::genericEntityName[]
+
+        context.commitChanges();
+        assertEquals("GenericEntity", entityName);
+
+        // tag::genericSelect[]
+        ObjectSelect<Persistent> query = ObjectSelect.query(Persistent.class, "GenericEntity");
+        // end::genericSelect[]
+
+        assertSame(generic, query.selectOne(context));
+    }
+
+    @Test
+    public void genericRegisterNewObject() {
+        // tag::genericRegisterNewObject[]
+        Persistent generic = new GenericPersistentObject();
+        generic.setObjectId(ObjectId.of("GenericEntity"));
+        context.registerNewObject(generic);
+        // end::genericRegisterNewObject[]
+
+        assertEquals(PersistenceState.NEW, generic.getPersistenceState());
+    }
+
+    @Test
+    public void performInTransaction() {
+        ObjectContext context1 = runtime.newContext();
+        ObjectContext context2 = runtime.newContext();
+        context1.newObject(Artist.class).setArtistName("a1");
+        context2.newObject(Artist.class).setArtistName("a2");
+
+        // tag::performInTransaction[]
+        Integer result = runtime.performInTransaction(() -> {
+            // commit one or more contexts
+            context1.commitChanges();
+            context2.commitChanges();
+
+            // after changing some objects in context1, commit again
+            context1.newObject(Artist.class).setArtistName("a3");
+            context1.commitChanges();
+
+            // return an arbitrary result or null if we don't care about the result
+            return 5;
+        });
+        // end::performInTransaction[]
+
+        assertEquals(5, result);
+        assertEquals(3, count());
+    }
+
+    @Test
+    public void threadTransaction() {
+        Transaction inTx = runtime.performInTransaction(() -> {
+            // tag::threadTransaction[]
+            Transaction tx = BaseTransaction.getThreadTransaction();
+            // end::threadTransaction[]
+            return tx;
+        });
+
+        assertNotNull(inTx);
+    }
+
+    @Test
+    public void transactionDescriptor() {
+        TransactionalOperation<Long> transactionalOperation = this::count;
+
+        // tag::transactionDescriptor[]
+        TransactionDescriptor descriptor = TransactionDescriptor.builder()
+                .isolation(Connection.TRANSACTION_SERIALIZABLE)
+                .propagation(TransactionPropagation.REQUIRES_NEW)
+                .build();
+        runtime.performInTransaction(transactionalOperation, descriptor);
+        // end::transactionDescriptor[]
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectParsedTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectParsedTest.java
new file mode 100644
index 000000000..78f84af20
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectParsedTest.java
@@ -0,0 +1,349 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Gallery;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.exp.ExpressionFactory;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.query.ColumnSelect;
+import org.apache.cayenne.query.FluentSelect;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.Ordering;
+import org.apache.cayenne.query.PrefetchTreeNode;
+import org.apache.cayenne.query.SortOrder;
+import org.junit.jupiter.api.Test;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+/**
+ * The query Strings for these examples are in "objectselect-parsed.txt", that is also included in the docs. Each String
+ * is run together with the fluent API query that the docs show as its equivalent, and the results are compared.
+ */
+public class ObjectSelectParsedTest extends BaseTest {
+
+    private static String ql(String tag) {
+        return example("objectselect-parsed.txt", tag);
+    }
+
+    private void assertSameResult(FluentSelect<?, ?> parsed, FluentSelect<?, ?> fluent) {
+        assertSameResult(parsed, fluent, false);
+    }
+
+    private void assertSameResult(FluentSelect<?, ?> parsed, FluentSelect<?, ?> fluent, boolean emptyExpected) {
+        List<?> parsedResult = parsed.select(context);
+        List<?> fluentResult = fluent.select(context);
+
+        assertEquals(emptyExpected, fluentResult.isEmpty());
+        assertEquals(fluentResult.size(), parsedResult.size());
+
+        for (int i = 0; i < fluentResult.size(); i++) {
+            if (fluentResult.get(i) instanceof Object[] row) {
+                assertArrayEquals(row, (Object[]) parsedResult.get(i));
+            } else {
+                assertEquals(fluentResult.get(i), parsedResult.get(i));
+            }
+        }
+    }
+
+    @Test
+    public void parse() {
+        createArtistsDataSet();
+
+        // tag::parse[]
+        List<Artist> artists = ObjectSelect
+                .parse(Artist.class, "from Artist where artistName like $name order by dateOfBirth desc limit 10", "A%")
+                .select(context);
+        // end::parse[]
+
+        assertEquals(1, artists.size());
+    }
+
+    @Test
+    public void parseAndCustomize() {
+        createArtistsDataSet();
+
+        // tag::parseAndCustomize[]
+        List<Artist> artists = ObjectSelect
+                .parse(Artist.class, "from Artist where artistName like $name", "A%")
+                .localCache("artists")
+                .pageSize(50)
+                .select(context);
+        // end::parseAndCustomize[]
+
+        assertEquals(1, artists.size());
+    }
+
+    @Test
+    public void params() {
+        createArtistsDataSet();
+        Artist artist = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("Dali")).selectOne(context);
+
+        // tag::params[]
+        List<Painting> paintings = ObjectSelect
+                .parse(Painting.class,
+                        "from Painting where toArtist = $artist and estimatedPrice between $low and $high",
+                        artist, 10, 500)
+                .select(context);
+        // end::params[]
+
+        assertEquals(1, paintings.size());
+    }
+
+    @Test
+    public void api() {
+        createArtistsDataSet();
+
+        // tag::api[]
+        // any query. Returns either an ObjectSelect or a ColumnSelect
+        FluentSelect<?, ?> q1 = ObjectSelect.parse("from Artist");
+
+        // a query of the root objects. "DataRow.class" is used to fetch the data rows instead of objects
+        ObjectSelect<Artist> q2 = ObjectSelect.parse(Artist.class, "from Artist");
+        ObjectSelect<DataRow> q3 = ObjectSelect.parse(DataRow.class, "from Artist");
+
+        // a query with a single column
+        ColumnSelect<String> q4 = ObjectSelect.parseColumn(String.class, "select artistName from Artist");
+
+        // a query with multiple columns
+        ColumnSelect<Object[]> q5 = ObjectSelect.parseColumns("select artistName, dateOfBirth from Artist");
+        // end::api[]
+
+        assertInstanceOf(ObjectSelect.class, q1);
+        assertEquals(3, q1.select(context).size());
+        assertEquals(3, q2.select(context).size());
+        assertInstanceOf(DataRow.class, q3.select(context).get(0));
+        assertEquals(3, q4.select(context).size());
+        assertEquals(2, q5.select(context).get(0).length);
+    }
+
+    @Test
+    public void filterOrderLimit() {
+        createArtistsDataSet();
+
+        // tag::filterOrderLimit[]
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class)
+                .where(Artist.ARTIST_NAME.like("A%"))
+                .orderBy(Artist.DATE_OF_BIRTH.desc(), Artist.ARTIST_NAME.ascInsensitive())
+                .limit(10)
+                .offset(20);
+        // end::filterOrderLimit[]
+
+        assertSameResult(ObjectSelect.parse(ql("filterOrderLimit")), query, true);
+        assertSameResult(ObjectSelect.parse(ql("filterOrderLimit")).offset(0), query.offset(0));
+    }
+
+    @Test
+    public void outerJoin() {
+        createArtistsDataSet();
+
+        // tag::outerJoin[]
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class).where(Artist.PAINTING_ARRAY.outer().isNull());
+        // end::outerJoin[]
+
+        assertSameResult(ObjectSelect.parse(ql("outerJoin")), query);
+    }
+
+    @Test
+    public void splitJoins() {
+        createArtistsDataSet();
+
+        // tag::splitJoins[]
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class)
+                .where(Artist.PAINTING_ARRAY.alias("p1").dot(Painting.PAINTING_TITLE).eq("P1"))
+                .and(Artist.PAINTING_ARRAY.alias("p2").dot(Painting.PAINTING_TITLE).eq("P2"));
+        // end::splitJoins[]
+
+        assertSameResult(ObjectSelect.parse(ql("splitJoins")), query);
+    }
+
+    @Test
+    public void columns() {
+        createArtistsDataSet();
+
+        // tag::columns[]
+        ColumnSelect<Object[]> query = ObjectSelect
+                .columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
+                .orderBy(Artist.ARTIST_NAME.asc());
+        // end::columns[]
+
+        assertSameResult(ObjectSelect.parse(ql("columns")), query);
+    }
+
+    @Test
+    public void column() {
+        createArtistsDataSet();
+
+        // tag::column[]
+        ColumnSelect<String> query = ObjectSelect
+                .columnQuery(Painting.class, Painting.TO_GALLERY.dot(Gallery.GALLERY_NAME))
+                .distinct();
+        // end::column[]
+
+        assertEquals(2, query.select(context).size());
+        assertEquals(2, ObjectSelect.parse(ql("column")).select(context).size());
+    }
+
+    @Test
+    public void columnExpressions() {
+        createArtistsDataSet();
+
+        List<Object[]> result = ObjectSelect.parseColumns(ql("columnExpressions")).select(context);
+        assertEquals(3, result.size());
+        assertInstanceOf(String.class, result.get(0)[0]);
+        assertInstanceOf(Number.class, result.get(0)[1]);
+    }
+
+    @Test
+    public void aggregates() {
+        createArtistsDataSet();
+        BigDecimal min = BigDecimal.valueOf(1000);
+
+        // tag::aggregates[]
+        ColumnSelect<Object[]> query = ObjectSelect
+                .columnQuery(Painting.class,
+                        Painting.TO_ARTIST.dot(Artist.ARTIST_NAME),
+                        Painting.ESTIMATED_PRICE.sum(),
+                        PropertyFactory.COUNT)
+                .having(Painting.ESTIMATED_PRICE.sum().gt(min))
+                .orderBy(Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).asc());
+        // end::aggregates[]
+
+        assertSameResult(ObjectSelect.parse(ql("aggregates"), min), query);
+    }
+
+    @Test
+    public void self() {
+        createArtistsDataSet();
+
+        // no two artists should have the same number of paintings for the order to be predictable
+        Artist dali = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("Dali")).selectOne(context);
+        createPainting("P3", 50, dali, null);
+        context.commitChanges();
+
+        // tag::self[]
+        ColumnSelect<Object[]> query = ObjectSelect
+                .columnQuery(Artist.class, Artist.SELF, Artist.PAINTING_ARRAY.outer().count())
+                .orderBy(Artist.PAINTING_ARRAY.outer().count().desc());
+        // end::self[]
+
+        assertSameResult(ObjectSelect.parse(ql("self")), query);
+    }
+
+    @Test
+    public void toOne() {
+        createArtistsDataSet();
+
+        // tag::toOne[]
+        ColumnSelect<Object[]> query = ObjectSelect
+                .columnQuery(Painting.class, Painting.PAINTING_TITLE, Painting.TO_ARTIST, Painting.TO_GALLERY);
+        // end::toOne[]
+
+        assertEquals(4, query.select(context).size());
+        assertEquals(4, ObjectSelect.parse(ql("toOne")).select(context).size());
+    }
+
+    @Test
+    public void toMany() {
+        createArtistsDataSet();
+
+        // tag::toMany[]
+        ColumnSelect<Object[]> query = ObjectSelect
+                .columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.PAINTING_ARRAY.flat());
+        // end::toMany[]
+
+        assertEquals(5, query.select(context).size());
+        assertEquals(5, ObjectSelect.parse(ql("toMany")).select(context).size());
+    }
+
+    @Test
+    public void exists() {
+        createArtistsDataSet();
+        BigDecimal price = BigDecimal.valueOf(100000);
+
+        // tag::exists[]
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class)
+                .where(ExpressionFactory.exists(ObjectSelect.query(Painting.class)
+                        .where(Painting.TO_ARTIST.eq(Artist.SELF.enclosing()))
+                        .and(Painting.ESTIMATED_PRICE.gt(price))));
+        // end::exists[]
+
+        assertSameResult(ObjectSelect.parse(ql("exists"), price), query);
+    }
+
+    @Test
+    public void in() {
+        createArtistsDataSet();
+        LocalDate date = LocalDate.of(1900, 1, 1);
+
+        // tag::in[]
+        ObjectSelect<Painting> query = ObjectSelect.query(Painting.class)
+                .where(Painting.TO_ARTIST.dot(Artist.ARTIST_NAME).in(
+                        ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME)
+                                .where(Artist.DATE_OF_BIRTH.lt(date))));
+        // end::in[]
+
+        assertSameResult(ObjectSelect.parse(ql("in"), date), query);
+    }
+
+    @Test
+    public void allAndAny() {
+        createArtistsDataSet();
+
+        assertFalse(ObjectSelect.parse(ql("all")).select(context).isEmpty());
+        assertFalse(ObjectSelect.parse(ql("any")).select(context).isEmpty());
+    }
+
+    @Test
+    public void dbRoot() {
+        createArtistsDataSet();
+
+        // tag::dbRoot[]
+        ObjectSelect<DataRow> query = ObjectSelect.dbQuery("ARTIST")
+                .where(ExpressionFactory.likeDbExp("ARTIST_NAME", "A%"))
+                .orderBy(new Ordering(ExpressionFactory.dbPathExp("ARTIST_ID"), SortOrder.DESCENDING));
+        // end::dbRoot[]
+
+        assertSameResult(ObjectSelect.parse(ql("dbRoot")), query);
+    }
+
+    @Test
+    public void prefetch() {
+        createArtistsDataSet();
+        String name = "Dali";
+
+        // tag::prefetch[]
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class)
+                .where(Artist.ARTIST_NAME.eq(name))
+                .prefetch(Artist.PAINTING_ARRAY.joint())
+                .prefetch("paintingArray.toGallery", PrefetchTreeNode.UNDEFINED_SEMANTICS);
+        // end::prefetch[]
+
+        assertSameResult(ObjectSelect.parse(ql("prefetch"), name), query);
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectTest.java
new file mode 100644
index 000000000..baed1c7c4
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ObjectSelectTest.java
@@ -0,0 +1,159 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.query.ObjectSelect;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class ObjectSelectTest extends BaseTest {
+
+    @Test
+    public void selectAll() {
+        createArtistsDataSet();
+
+        // tag::selectAll[]
+        List<Artist> objects = ObjectSelect.query(Artist.class).select(context);
+        // end::selectAll[]
+
+        assertEquals(3, objects.size());
+    }
+
+    @Test
+    public void where() {
+        createArtistsDataSet();
+
+        // tag::where[]
+        List<Artist> objects = ObjectSelect.query(Artist.class)
+                .where(Artist.ARTIST_NAME.like("Pablo%"))
+                .select(context);
+        // end::where[]
+
+        assertEquals(1, objects.size());
+    }
+
+    @Test
+    public void and() {
+        createArtistsDataSet();
+        LocalDate someDate = LocalDate.of(1800, 1, 1);
+
+        // tag::and[]
+        List<Artist> objects = ObjectSelect.query(Artist.class)
+                .where(Artist.ARTIST_NAME.like("A%"))
+                .and(Artist.DATE_OF_BIRTH.gt(someDate))
+                .select(context);
+        // end::and[]
+
+        assertEquals(1, objects.size());
+    }
+
+    @Test
+    public void orderBy() {
+        createArtistsDataSet();
+
+        // tag::orderBy[]
+        List<Artist> objects = ObjectSelect.query(Artist.class)
+                .orderBy(Artist.DATE_OF_BIRTH.desc())
+                .orderBy(Artist.ARTIST_NAME.asc())
+                .select(context);
+        // end::orderBy[]
+
+        assertEquals("Dali", objects.get(0).getArtistName());
+    }
+
+    @Test
+    public void column() {
+        createArtistsDataSet();
+
+        // tag::column[]
+        List<String> names = ObjectSelect.columnQuery(Artist.class, Artist.ARTIST_NAME)
+                .select(context);
+        // end::column[]
+
+        assertEquals(3, names.size());
+    }
+
+    @Test
+    public void columns() {
+        createArtistsDataSet();
+
+        // tag::columns[]
+        List<Object[]> nameAndDate = ObjectSelect
+                .columnQuery(Artist.class, Artist.ARTIST_NAME, Artist.DATE_OF_BIRTH)
+                .select(context);
+        // end::columns[]
+
+        assertEquals(3, nameAndDate.size());
+        assertEquals(2, nameAndDate.get(0).length);
+    }
+
+    @Test
+    public void count() {
+        createArtistsDataSet();
+
+        // tag::count[]
+        long count = ObjectSelect.query(Artist.class).selectCount(context);
+        // end::count[]
+
+        assertEquals(3, count);
+    }
+
+    @Test
+    public void aggregates() {
+        createArtistsDataSet();
+
+        // tag::aggregates[]
+        // Artist.SELF - is a special property that denotes a full object in this case
+        List<Object[]> artistAndPaintingCount = ObjectSelect.columnQuery(Artist.class,
+                        Artist.SELF,
+                        Artist.PAINTING_ARRAY.count())
+                .where(Artist.ARTIST_NAME.like("P%"))
+                .having(Artist.PAINTING_ARRAY.count().lt(5L))
+                .orderBy(Artist.PAINTING_ARRAY.count().desc(), Artist.ARTIST_NAME.asc())
+                .select(context);
+
+        for (Object[] next : artistAndPaintingCount) {
+            Artist artist = (Artist) next[0];
+            long paintings = (Long) next[1];
+            System.out.println(artist.getArtistName() + " has " + paintings + " paintings");
+        }
+        // end::aggregates[]
+
+        assertEquals(1, artistAndPaintingCount.size());
+        assertEquals(2L, artistAndPaintingCount.get(0)[1]);
+    }
+
+    @Test
+    public void notExists() {
+        createArtistsDataSet();
+
+        // tag::notExists[]
+        long count = ObjectSelect.query(Artist.class)
+                .where(Artist.PAINTING_ARRAY.notExists())
+                .selectCount(context);
+        // end::notExists[]
+
+        assertEquals(1, count);
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/OrderingsTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/OrderingsTest.java
new file mode 100644
index 000000000..782e952b3
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/OrderingsTest.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.docs;
+
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.Ordering;
+import org.apache.cayenne.query.SortOrder;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class OrderingsTest extends BaseTest {
+
+    @Test
+    public void create() {
+        // tag::create[]
+        Ordering o1 = new Ordering("paintingTitle", SortOrder.ASCENDING);
+
+        // the same ordering created from a property generated by Cayenne
+        Ordering o2 = Painting.PAINTING_TITLE.asc();
+        // end::create[]
+
+        assertEquals(o1, o2);
+    }
+
+    @Test
+    public void orderList() {
+        createArtistsDataSet();
+
+        // tag::orderList[]
+        Ordering o = Painting.PAINTING_TITLE.ascInsensitive();
+        List<Painting> list = ObjectSelect.query(Painting.class).select(context);
+        o.orderList(list);
+        // end::orderList[]
+
+        assertEquals("Gertrude Stein", list.get(0).getPaintingTitle());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java
new file mode 100644
index 000000000..6a9edb11d
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.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.docs;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.QueryResult;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.query.ProcedureCall;
+
+import java.util.List;
+
+/**
+ * Examples that are only compiled, but not run, as there are no stored procedures in the docs test schema.
+ */
+public class ProcedureCallExamples {
+
+    private ObjectContext context;
+
+    public void select() {
+        // tag::select[]
+        List<Artist> result = ProcedureCall.query("my_procedure", Artist.class)
+                .param("p1", "abc")
+                .param("p2", 3000)
+                .select(context);
+        // end::select[]
+    }
+
+    public void call() {
+        // tag::call[]
+        // here we do not bother with root class.
+        // Procedure name gives us needed routing information
+        List<QueryResult> result = ProcedureCall.query("my_procedure")
+                .param("p1", "abc")
+                .param("p2", 3000)
+                .call(context);
+        // end::call[]
+    }
+
+    public void outParameters() {
+        // tag::outParameters[]
+        List<QueryResult> result = ProcedureCall.query("my_procedure").call(context);
+        for (QueryResult item : result) {
+            switch (item) {
+                case QueryResult.Select<?> select -> process(select.objects());
+                case QueryResult.Update update -> process(update.counts());
+                case QueryResult.OutParameters out -> process(out.values().get("out_param"));
+                default -> {
+                }
+            }
+        }
+        // end::outParameters[]
+    }
+
+    private void process(Object result) {
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLScriptingTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLScriptingTest.java
new file mode 100644
index 000000000..8c75a4bbe
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLScriptingTest.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.docs;
+
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SQLExec;
+import org.apache.cayenne.query.SQLSelect;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class SQLScriptingTest extends BaseTest {
+
+    @Test
+    public void variable() {
+        createArtistsDataSet();
+
+        // tag::variable[]
+        // this will generate SQL like this: "delete from PUBLIC.PAINTING"
+        SQLExec query = SQLExec.query("delete from $tableName")
+                .params("tableName", "PUBLIC.PAINTING");
+        // end::variable[]
+
+        assertEquals(4, query.update(context));
+    }
+
+    @Test
+    public void bindObjectEqual() {
+        createArtistsDataSet();
+        Artist dali = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("Dali")).selectOne(context);
+
+        // tag::bindObjectEqual[]
+        String sql = "SELECT * FROM PAINTING t0 WHERE #bindObjectEqual($a 't0.ARTIST_ID' 'ARTIST_ID' )";
+        Artist artistParam = dali;
+
+        SQLSelect<Painting> select = SQLSelect.query(Painting.class, sql)
+                .param("a", artistParam);
+        // end::bindObjectEqual[]
+
+        assertEquals(2, select.select(context).size());
+    }
+
+    @Test
+    public void bindObjectNotEqual() {
+        createArtistsDataSet();
+        Artist dali = ObjectSelect.query(Artist.class).where(Artist.ARTIST_NAME.eq("Dali")).selectOne(context);
+
+        // tag::bindObjectNotEqual[]
+        String sql = "SELECT * FROM PAINTING t0 WHERE #bindObjectNotEqual($a 't0.ARTIST_ID' 'ARTIST_ID' )";
+        Artist artistParam = dali;
+
+        SQLSelect<Painting> select = SQLSelect.query(Painting.class, sql)
+                .param("a", artistParam);
+        // end::bindObjectNotEqual[]
+
+        assertEquals(2, select.select(context).size());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLSelectTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLSelectTest.java
new file mode 100644
index 000000000..9cbe0ab8c
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SQLSelectTest.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.docs;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.SQLExec;
+import org.apache.cayenne.query.SQLSelect;
+import org.junit.jupiter.api.Test;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+public class SQLSelectTest extends BaseTest {
+
+    @Test
+    public void select() {
+        createArtistsDataSet();
+
+        // tag::select[]
+        // Selecting objects
+        List<Painting> paintings = SQLSelect
+                .query(Painting.class, "SELECT * FROM PAINTING WHERE PAINTING_TITLE LIKE 'G%'")
+                .upperColumnNames()
+                .localCache()
+                .limit(100)
+                .select(context);
+
+        // Selecting scalar values
+        List<String> paintingNames = SQLSelect
+                .scalarQuery("SELECT PAINTING_TITLE FROM PAINTING WHERE ESTIMATED_PRICE > 100000", String.class)
+                .select(context);
+
+        // Selecting DataRow with predefined types
+        List<DataRow> rows = SQLSelect
+                .dataRowQuery("SELECT * FROM ARTIST", Integer.class, String.class, LocalDate.class)
+                .select(context);
+
+        // Selecting Object[] with predefined types
+        List<Object[]> arrays = SQLSelect
+                .columnQuery("SELECT * FROM ARTIST", Integer.class, String.class, LocalDate.class)
+                .select(context);
+        // end::select[]
+
+        assertEquals(2, paintings.size());
+        assertEquals(List.of("Guernica"), paintingNames);
+        assertEquals(3, rows.size());
+        assertEquals(3, arrays.size());
+        assertInstanceOf(LocalDate.class, arrays.get(0)[2]);
+    }
+
+    @Test
+    public void exec() {
+        // tag::exec[]
+        int inserted = SQLExec
+                .query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (55, 'Picasso')")
+                .update(context);
+        // end::exec[]
+
+        assertEquals(1, inserted);
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SelectByIdTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SelectByIdTest.java
new file mode 100644
index 000000000..b0a1c5c05
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/SelectByIdTest.java
@@ -0,0 +1,106 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.CompoundPk;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SQLExec;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class SelectByIdTest extends BaseTest {
+
+    private void createArtistsWithIds() {
+        for (int i = 1; i <= 4; i++) {
+            SQLExec.query("INSERT INTO ARTIST (ARTIST_ID, ARTIST_NAME) VALUES (#bind($id), #bind($name))")
+                    .params("id", i)
+                    .params("name", "artist" + i)
+                    .update(context);
+        }
+
+        SQLExec.query("INSERT INTO PAINTING (PAINTING_ID, PAINTING_TITLE, ARTIST_ID) VALUES (1, 'P1', 1)")
+                .update(context);
+    }
+
+    @Test
+    public void byId() {
+        createArtistsWithIds();
+
+        // tag::byId[]
+        Artist artistWithId1 = ObjectSelect.query(Artist.class)
+                .byId(1)
+                .selectOne(context);
+
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .byIds(1, 2, 3)
+                .select(context);
+        // end::byId[]
+
+        assertEquals("artist1", artistWithId1.getArtistName());
+        assertEquals(3, artists.size());
+    }
+
+    @Test
+    public void byIdForms() {
+        createArtistsWithIds();
+        SQLExec.query("INSERT INTO COMPOUND_PK (KEY1, KEY2, NAME) VALUES ('x', 'y', 'xy')").update(context);
+        ObjectId objectId = ObjectId.of("Artist", Artist.ARTIST_ID_PK_COLUMN, 2);
+
+        // tag::byIdForms[]
+        CompoundPk object = ObjectSelect.query(CompoundPk.class)
+                .byId(Map.of("KEY1", "x", "KEY2", "y"))
+                .selectOne(context);
+
+        Artist artist = ObjectSelect.query(Artist.class)
+                .byId(objectId)
+                .selectOne(context);
+        // end::byIdForms[]
+
+        assertEquals("xy", object.getName());
+        assertNotNull(artist);
+        assertEquals("artist2", artist.getArtistName());
+    }
+
+    @Test
+    public void idExpressions() {
+        createArtistsWithIds();
+
+        // tag::idExpressions[]
+        List<Artist> artists = ObjectSelect.query(Artist.class)
+                .where(Artist.ARTIST_NAME.eq("artist4"))
+                .or(Artist.SELF.idsIn(1, 2, 3))
+                .select(context);
+
+        List<Painting> paintings = ObjectSelect.query(Painting.class)
+                .where(Painting.TO_ARTIST.eqId(1))
+                .select(context);
+        // end::idExpressions[]
+
+        assertEquals(4, artists.size());
+        assertEquals(1, paintings.size());
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java
new file mode 100644
index 000000000..e569d1db1
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java
@@ -0,0 +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.docs;
+
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.runtime.CoreModule;
+import org.apache.cayenne.di.Module;
+import org.apache.cayenne.runtime.CayenneRuntime;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+public class StartingTest {
+
+    @Test
+    public void startAndStop() {
+        // tag::start[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("com/example/cayenne-project.xml")
+                .build();
+        // end::start[]
+
+        assertNotNull(runtime.getDataDomain());
+
+        // tag::stop[]
+        runtime.shutdown();
+        // end::stop[]
+    }
+
+    @Test
+    public void externalTransactions() {
+        // tag::externalTransactions[]
+        Module extensions = binder ->
+                CoreModule.extend(binder).setProperty(Constants.EXTERNAL_TX_PROPERTY, "true");
+
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("com/example/cayenne-project.xml")
+                .addModule(extensions)
+                .build();
+        // end::externalTransactions[]
+
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+
+    @Test
+    public void multipleProjects() {
+        // tag::multipleProjects[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("com/example/cayenne-project.xml")
+                .addConfig("org/foo/cayenne-library1.xml")
+                .addConfig("org/foo/cayenne-library2.xml")
+                .build();
+        // end::multipleProjects[]
+
+        assertNotNull(runtime.getDataDomain());
+        runtime.shutdown();
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/TuningTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/TuningTest.java
new file mode 100644
index 000000000..7c57f3081
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/TuningTest.java
@@ -0,0 +1,242 @@
+/*****************************************************************
+ *   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.docs;
+
+import org.apache.cayenne.DataRow;
+import org.apache.cayenne.ResultBatchIterator;
+import org.apache.cayenne.ResultIterator;
+import org.apache.cayenne.access.DataContext;
+import org.apache.cayenne.cache.QueryCache;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.SQLSelect;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class TuningTest extends BaseTest {
+
+    @Test
+    public void prefetch() {
+        createArtistsDataSet();
+
+        // tag::prefetch[]
+        List<Artist> artists = ObjectSelect
+                .query(Artist.class)
+                .prefetch(Artist.PAINTING_ARRAY.disjoint()) // <1>
+                .select(context); // <2>
+        // end::prefetch[]
+
+        assertEquals(3, artists.size());
+    }
+
+    @Test
+    public void prefetchPath() {
+        createArtistsDataSet();
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class);
+
+        // tag::prefetchPath[]
+        query.prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).disjoint());
+        // end::prefetchPath[]
+
+        assertEquals(3, query.select(context).size());
+    }
+
+    @Test
+    public void prefetchMultiple() {
+        createArtistsDataSet();
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class);
+
+        // tag::prefetchMultiple[]
+        query.prefetch(Artist.PAINTING_ARRAY.disjoint())
+                .prefetch(Artist.PAINTING_ARRAY.dot(Painting.TO_GALLERY).disjoint());
+        // end::prefetchMultiple[]
+
+        assertEquals(3, query.select(context).size());
+    }
+
+    @Test
+    public void sqlSelectPrefetch() {
+        createArtistsDataSet();
+
+        // tag::sqlSelectPrefetch[]
+        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.time.LocalDate'), "
+                        + "#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);
+        // end::sqlSelectPrefetch[]
+
+        assertEquals(2, objects.size());
+        assertEquals(2, objects.get(0).getPaintingArray().size());
+    }
+
+    @Test
+    public void dataRows() {
+        createArtistsDataSet();
+
+        // tag::dataRows[]
+        ObjectSelect<DataRow> query = ObjectSelect.dataRowQuery(Artist.class);
+
+        List<DataRow> rows = query.select(context);
+        // end::dataRows[]
+
+        assertEquals(3, rows.size());
+
+        int[] converted = new int[1];
+
+        // tag::objectFromDataRow[]
+        // you need to cast ObjectContext to DataContext to get access to 'objectFromDataRow'
+        DataContext dataContext = (DataContext) context;
+
+        for (DataRow row : rows) {
+            if (row.get("DATE_OF_BIRTH") != null) {
+                Artist artist = dataContext.objectFromDataRow(Artist.class, row);
+                // do something with Artist...
+                // end::objectFromDataRow[]
+                converted[0]++;
+                // tag::objectFromDataRow[]
+            }
+        }
+        // end::objectFromDataRow[]
+
+        assertEquals(3, converted[0]);
+    }
+
+    @Test
+    public void sqlDataRows() {
+        createArtistsDataSet();
+
+        // tag::sqlDataRows[]
+        SQLSelect<DataRow> query = SQLSelect.dataRowQuery("SELECT * FROM ARTIST");
+        List<DataRow> rows = query.select(context);
+        // end::sqlDataRows[]
+
+        assertEquals(3, rows.size());
+    }
+
+    @Test
+    public void iterator() {
+        createArtistsDataSet();
+        int[] iterated = new int[1];
+
+        // tag::iterator[]
+        try (ResultIterator<Artist> it = ObjectSelect.query(Artist.class).iterator(context)) {
+            for (Artist a : it) {
+                // do something with the object...
+                // end::iterator[]
+                iterated[0]++;
+                // tag::iterator[]
+            }
+        }
+        // end::iterator[]
+
+        assertEquals(3, iterated[0]);
+    }
+
+    @Test
+    public void iterate() {
+        createArtistsDataSet();
+        int[] iterated = new int[1];
+
+        // tag::iterate[]
+        ObjectSelect.query(Artist.class).iterate(context, (Artist a) -> {
+            // do something with the object...
+            // end::iterate[]
+            iterated[0]++;
+            // tag::iterate[]
+        });
+        // end::iterate[]
+
+        assertEquals(3, iterated[0]);
+    }
+
+    @Test
+    public void batchIterator() {
+        createArtistsDataSet();
+        int[] iterated = new int[1];
+
+        // tag::batchIterator[]
+        try (ResultBatchIterator<Artist> it = ObjectSelect.query(Artist.class).batchIterator(context, 100)) {
+            for (List<Artist> list : it) {
+                // do something with each list
+                // end::batchIterator[]
+                iterated[0] += list.size();
+                // tag::batchIterator[]
+                // possibly commit your changes
+                context.commitChanges();
+            }
+        }
+        // end::batchIterator[]
+
+        assertEquals(3, iterated[0]);
+    }
+
+    @Test
+    public void pagination() {
+        createArtistsDataSet();
+
+        // tag::pagination[]
+        // the fact that result is paginated is transparent
+        List<Artist> artists =
+                ObjectSelect.query(Artist.class).pageSize(50).select(context);
+        // end::pagination[]
+
+        assertEquals(3, artists.size());
+    }
+
+    @Test
+    public void dataRowsPagination() {
+        createArtistsDataSet();
+
+        // tag::dataRowsPagination[]
+        List<DataRow> rows =
+                ObjectSelect.dataRowQuery(Artist.class).pageSize(50).select(context);
+        // end::dataRowsPagination[]
+
+        assertEquals(3, rows.size());
+    }
+
+    @Test
+    public void localCache() {
+        createArtistsDataSet();
+
+        // tag::localCache[]
+        ObjectSelect<Artist> query = ObjectSelect.query(Artist.class).localCache("artists");
+        // end::localCache[]
+
+        assertEquals(3, query.select(context).size());
+
+        // tag::removeGroup[]
+        QueryCache cache = runtime.getDataDomain().getQueryCache();
+        cache.removeGroup("artists");
+        // end::removeGroup[]
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/CustomInvalidationHandler.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/CustomInvalidationHandler.java
new file mode 100644
index 000000000..6a3995661
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/CustomInvalidationHandler.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.docs.cacheinvalidation;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.cache.invalidation.CacheGroupDescriptor;
+import org.apache.cayenne.cache.invalidation.InvalidationHandler;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.function.Function;
+
+// tag::content[]
+public class CustomInvalidationHandler implements InvalidationHandler {
+    @Override
+    public Function<Persistent, Collection<CacheGroupDescriptor>> canHandle(Class<? extends Persistent> type) {
+        return p -> Collections.singleton(new CacheGroupDescriptor("custom-group"));
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/MyEntity.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/MyEntity.java
new file mode 100644
index 000000000..8ae2d715d
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/cacheinvalidation/MyEntity.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.docs.cacheinvalidation;
+
+import org.apache.cayenne.cache.invalidation.CacheGroups;
+import org.apache.cayenne.docs.lifecycle._MyEntity;
+
+// tag::content[]
+@CacheGroups("some-group")
+public class MyEntity extends _MyEntity {
+    // ...
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/AuditListener.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/AuditListener.java
new file mode 100644
index 000000000..e66957667
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/AuditListener.java
@@ -0,0 +1,40 @@
+/*****************************************************************
+ *   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.docs.commitlog;
+
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.commitlog.CommitLogListener;
+import org.apache.cayenne.commitlog.model.ChangeMap;
+import org.apache.cayenne.commitlog.model.ObjectChange;
+
+// tag::content[]
+public class AuditListener implements CommitLogListener {
+
+    @Override
+    public void onPostCommit(ObjectContext originatingContext, ChangeMap changes) {
+        for (ObjectChange change : changes.getUniqueChanges()) {
+            switch (change.getType()) {
+                case INSERT -> System.out.println("Inserted: " + change.getPostCommitId());
+                case UPDATE -> System.out.println("Updated:  " + change.getPostCommitId());
+                case DELETE -> System.out.println("Deleted:  " + change.getPreCommitId());
+            }
+        }
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/MyEntityFactory.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/MyEntityFactory.java
new file mode 100644
index 000000000..88b126dfd
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/MyEntityFactory.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.docs.commitlog;
+
+import org.apache.cayenne.ObjectId;
+import org.apache.cayenne.commitlog.meta.CommitLogEntity;
+import org.apache.cayenne.commitlog.meta.CommitLogEntityFactory;
+
+// tag::content[]
+public class MyEntityFactory implements CommitLogEntityFactory {
+
+    @Override
+    public CommitLogEntity getEntity(ObjectId id) {
+
+        // the returned CommitLogEntity defines whether the entity and its individual properties are included
+        boolean entityIncluded = !"Painting".equals(id.getEntityName());
+
+        return new CommitLogEntity() {
+
+            @Override
+            public boolean isIncluded() {
+                return entityIncluded;
+            }
+
+            @Override
+            public boolean isIncluded(String property) {
+                // all the properties of the excluded entity must be excluded as well
+                return entityIncluded;
+            }
+
+            @Override
+            public boolean isConfidential(String property) {
+                return false;
+            }
+        };
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/Order.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/Order.java
new file mode 100644
index 000000000..8d49b432e
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/Order.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.docs.commitlog;
+
+import org.apache.cayenne.annotation.CommitLog;
+import org.apache.cayenne.docs.lifecycle._Order;
+
+// tag::content[]
+@CommitLog
+public class Order extends _Order {
+    // ...
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/attributes/Order.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/attributes/Order.java
new file mode 100644
index 000000000..e31509fe1
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/commitlog/attributes/Order.java
@@ -0,0 +1,34 @@
+/*****************************************************************
+ *   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.docs.commitlog.attributes;
+
+import org.apache.cayenne.annotation.CommitLog;
+import org.apache.cayenne.docs.lifecycle._Order;
+
+// tag::content[]
+@CommitLog(
+        ignoredProperties = {"internalState"},         // exclude from change records
+        ignoreAttributes = false,                       // set true to skip all attributes
+        ignoreToOneRelationships = false,               // set true to skip all to-one relationships
+        ignoreToManyRelationships = false               // set true to skip all to-many relationships
+)
+public class Order extends _Order {
+    // ...
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClass.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClass.java
new file mode 100644
index 000000000..29a4d0614
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClass.java
@@ -0,0 +1,22 @@
+/*****************************************************************
+ *   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.docs.crypto;
+
+public record MyClass(String value) {
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClassBytesConverter.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClassBytesConverter.java
new file mode 100644
index 000000000..b2ba635e9
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyClassBytesConverter.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.docs.crypto;
+
+import org.apache.cayenne.crypto.transformer.value.BytesConverter;
+
+import java.nio.charset.StandardCharsets;
+
+public class MyClassBytesConverter implements BytesConverter<MyClass> {
+
+    @Override
+    public MyClass fromBytes(byte[] bytes) {
+        return new MyClass(new String(bytes, StandardCharsets.UTF_8));
+    }
+
+    @Override
+    public byte[] toBytes(MyClass value) {
+        return value.value().getBytes(StandardCharsets.UTF_8);
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyColumnMapper.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyColumnMapper.java
new file mode 100644
index 000000000..9e92d7f6c
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/crypto/MyColumnMapper.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.docs.crypto;
+
+import org.apache.cayenne.crypto.map.ColumnMapper;
+import org.apache.cayenne.map.DbAttribute;
+
+public class MyColumnMapper implements ColumnMapper {
+
+    @Override
+    public boolean isEncrypted(DbAttribute column) {
+        return column.getName().startsWith("SECRET_");
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/DoubleArrayType.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/DoubleArrayType.java
new file mode 100644
index 000000000..99bf59d3b
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/DoubleArrayType.java
@@ -0,0 +1,92 @@
+/*****************************************************************
+ *   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.docs.customize;
+
+import org.apache.cayenne.access.types.ExtendedType;
+
+import java.sql.CallableStatement;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.Arrays;
+import java.util.stream.Collectors;
+
+// tag::content[]
+/**
+ * Defines methods to read Java objects from JDBC ResultSets and write as parameters of
+ * PreparedStatements.
+ */
+public class DoubleArrayType implements ExtendedType<Double[]> {
+
+    private final String SEPARATOR = ",";
+
+    /**
+     * Returns a full name of Java class that this ExtendedType supports.
+     */
+    @Override
+    public String getClassName() {
+        return Double[].class.getCanonicalName();
+    }
+
+    /**
+     * Initializes a single parameter of a PreparedStatement with object value.
+     */
+    @Override
+    public void setJdbcObject(PreparedStatement statement, Double[] value,
+                              int pos, int type, int scale) throws Exception {
+
+        statement.setString(pos, toString(value));
+    }
+
+    /**
+     * Reads an object from JDBC ResultSet column, converting it to class returned by
+     * 'getClassName' method.
+     *
+     * @throws Exception if read error occurred, or an object can't be converted to a
+     *                   target Java class.
+     */
+    @Override
+    public Double[] materializeObject(ResultSet rs, int index, int type) throws Exception {
+        return fromString(rs.getString(index));
+    }
+
+    /**
+     * Reads an object from a stored procedure OUT parameter, converting it to class
+     * returned by 'getClassName' method.
+     *
+     * @throws Exception if read error occurred, or an object can't be converted to a
+     *                   target Java class.
+     */
+    @Override
+    public Double[] materializeObject(CallableStatement rs, int index, int type) throws Exception {
+        return fromString(rs.getString(index));
+    }
+
+    /**
+     * Converts the value to a String. Used for the database write, as well as by Cayenne for logging.
+     */
+    @Override
+    public String toString(Double[] value) {
+        return Arrays.stream(value).map(String::valueOf).collect(Collectors.joining(SEPARATOR));
+    }
+
+    private Double[] fromString(String string) {
+        return Arrays.stream(string.split(SEPARATOR)).map(Double::valueOf).toArray(Double[]::new);
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/Money.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/Money.java
new file mode 100644
index 000000000..8f46c77f0
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/Money.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.docs.customize;
+
+import java.math.BigDecimal;
+
+// tag::content[]
+public class Money {
+    private BigDecimal value;
+
+    public Money(BigDecimal value) {
+        this.value = value;
+    }
+
+    public BigDecimal getValue() {
+        return value;
+    }
+
+    // .. some other business logic ..
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MoneyValueObjectType.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MoneyValueObjectType.java
new file mode 100644
index 000000000..b4a9a8f5d
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MoneyValueObjectType.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.docs.customize;
+
+import org.apache.cayenne.access.types.ValueObjectType;
+
+import java.math.BigDecimal;
+
+// tag::content[]
+public class MoneyValueObjectType implements ValueObjectType<Money, BigDecimal> {
+
+    @Override
+    public Class<BigDecimal> getTargetType() {
+        return BigDecimal.class;
+    }
+
+    @Override
+    public Class<Money> getValueType() {
+        return Money.class;
+    }
+
+    @Override
+    public Money toJavaObject(BigDecimal value) {
+        return new Money(value);
+    }
+
+    @Override
+    public BigDecimal fromJavaObject(Money object) {
+        return object.getValue();
+    }
+
+    @Override
+    public String toCacheKey(Money object) {
+        return object.getValue().toString();
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyDbAdapterDetector.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyDbAdapterDetector.java
new file mode 100644
index 000000000..4ec9194e7
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyDbAdapterDetector.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.docs.customize;
+
+import org.apache.cayenne.configuration.runtime.DbAdapterDetector;
+import org.apache.cayenne.dba.DbAdapter;
+
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+
+// tag::content[]
+public class MyDbAdapterDetector implements DbAdapterDetector {
+    public DbAdapter createAdapter(DatabaseMetaData md) throws SQLException {
+        // check if we support this database and return custom adapter,
+        // or return null to let the other detectors handle it
+        return null;
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyExtensionsModule.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyExtensionsModule.java
new file mode 100644
index 000000000..bd5c38e89
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customize/MyExtensionsModule.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.docs.customize;
+
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+
+// tag::content[]
+public class MyExtensionsModule implements Module {
+    public void configure(Binder binder) {
+        // customizations go here...
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyDelegatingQuery.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyDelegatingQuery.java
new file mode 100644
index 000000000..163f159b0
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyDelegatingQuery.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.docs.customquery;
+
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.IndirectQuery;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.SQLTemplate;
+
+// tag::content[]
+public class MyDelegatingQuery extends IndirectQuery {
+
+    @Override
+    protected Query createReplacementQuery(EntityResolver resolver) {
+        SQLTemplate delegate = new SQLTemplate(Artist.class, generateRawSQL());
+        delegate.setFetchingDataRows(true);
+        return delegate;
+    }
+
+    protected String generateRawSQL() {
+        // build some SQL string
+        return "SELECT * FROM ARTIST";
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyQuery.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyQuery.java
new file mode 100644
index 000000000..08d302723
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/customquery/MyQuery.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.docs.customquery;
+
+import org.apache.cayenne.access.OperationObserver;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.map.EntityResolver;
+import org.apache.cayenne.query.ObjectSelect;
+import org.apache.cayenne.query.Query;
+import org.apache.cayenne.query.QueryMetadata;
+import org.apache.cayenne.query.SQLAction;
+import org.apache.cayenne.query.SQLActionVisitor;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+
+// tag::content[]
+public class MyQuery implements Query {
+
+    @Override
+    public QueryMetadata getMetaData(EntityResolver resolver) {
+        // the metadata tells Cayenne which DataNode to run the query on. Here we simply borrow it from a standard query
+        return ObjectSelect.query(Artist.class).getMetaData(resolver);
+    }
+
+    @Override
+    public SQLAction createSQLAction(SQLActionVisitor visitor) {
+        return new SQLAction() {
+
+            @Override
+            public void performAction(Connection connection, OperationObserver observer)
+                    throws SQLException, Exception {
+                // 1. do some JDBC work using provided connection...
+                // 2. push results back to Cayenne via OperationObserver
+            }
+        };
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Module1.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Module1.java
new file mode 100644
index 000000000..8cb4c9231
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Module1.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   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.docs.di;
+
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+
+// tag::content[]
+public class Module1 implements Module {
+
+    public void configure(Binder binder) {
+        binder.bind(Service1.class).to(Service1Impl.class);
+        binder.bind(Service2.class).to(Service2Impl.class);
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1.java
new file mode 100644
index 000000000..7aa896a02
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1.java
@@ -0,0 +1,25 @@
+/*****************************************************************
+ *   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.docs.di;
+
+// tag::content[]
+public interface Service1 {
+    String getString();
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Impl.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Impl.java
new file mode 100644
index 000000000..eadc383ad
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Impl.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   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.docs.di;
+
+import org.apache.cayenne.di.Inject;
+
+// tag::content[]
+public class Service1Impl implements Service1 {
+    @Inject
+    private Service2 service2;
+
+    public String getString() {
+        return service2.getInt() + "_Service1Impl";
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Provider.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Provider.java
new file mode 100644
index 000000000..2899e9439
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service1Provider.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.docs.di;
+
+import org.apache.cayenne.di.Provider;
+
+public class Service1Provider implements Provider<Service1> {
+
+    @Override
+    public Service1 get() {
+        return () -> "Service1Provider";
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2.java
new file mode 100644
index 000000000..f91bbab3c
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2.java
@@ -0,0 +1,25 @@
+/*****************************************************************
+ *   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.docs.di;
+
+// tag::content[]
+public interface Service2 {
+    int getInt();
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2Impl.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2Impl.java
new file mode 100644
index 000000000..26aeefee1
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/Service2Impl.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.docs.di;
+
+// tag::content[]
+public class Service2Impl implements Service2 {
+    private int i;
+
+    public int getInt() {
+        return i++;
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/constructor/Service1Impl.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/constructor/Service1Impl.java
new file mode 100644
index 000000000..642f04e36
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/di/constructor/Service1Impl.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.docs.di.constructor;
+
+import org.apache.cayenne.di.Inject;
+import org.apache.cayenne.docs.di.Service1;
+import org.apache.cayenne.docs.di.Service2;
+
+// tag::content[]
+public class Service1Impl implements Service1 {
+
+    private Service2 service2;
+
+    public Service1Impl(@Inject Service2 service2) {
+        this.service2 = service2;
+    }
+
+    public String getString() {
+        return service2.getInt() + "_Service1Impl";
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/AnnotationExamples.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/AnnotationExamples.java
new file mode 100644
index 000000000..28d78bee3
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/AnnotationExamples.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.docs.lifecycle;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.annotation.PostPersist;
+import org.apache.cayenne.annotation.PostRemove;
+import org.apache.cayenne.annotation.PostUpdate;
+
+public class AnnotationExamples {
+
+    // tag::multipleEntities[]
+    // this callback will be invoked on PostRemove event of any object
+    // belonging to MyEntity1, MyEntity2 or their subclasses
+    @PostRemove({MyEntity1.class, MyEntity2.class})
+    void postRemove(Persistent object) {
+        // ...
+    }
+    // end::multipleEntities[]
+
+    // tag::multipleAnnotations[]
+    // similar example with multiple annotations on a single method
+    // each matching just one entity
+    @PostPersist(MyEntity1.class)
+    @PostRemove(MyEntity1.class)
+    @PostUpdate(MyEntity1.class)
+    void postCommit(MyEntity1 object) {
+        // ...
+    }
+    // end::multipleAnnotations[]
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/CommittedObjectCounter.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/CommittedObjectCounter.java
new file mode 100644
index 000000000..22085a0e7
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/CommittedObjectCounter.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+import org.apache.cayenne.DataChannelSyncFilter;
+import org.apache.cayenne.DataChannelSyncFilterChain;
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.annotation.PostPersist;
+import org.apache.cayenne.annotation.PostRemove;
+import org.apache.cayenne.annotation.PostUpdate;
+import org.apache.cayenne.graph.GraphDiff;
+
+// tag::content[]
+public class CommittedObjectCounter implements DataChannelSyncFilter {
+
+    private ThreadLocal<int[]> counter = new ThreadLocal<int[]>();
+
+    @Override
+    public GraphDiff onSync(ObjectContext originatingContext, GraphDiff changes, int syncType,
+                            DataChannelSyncFilterChain filterChain) {
+
+        // init the counter for the current commit
+        counter.set(new int[1]);
+
+        try {
+            return filterChain.onSync(originatingContext, changes, syncType);
+        } finally {
+
+            // process aggregated result and release the counter
+            System.out.println("Committed " + counter.get()[0] + " object(s)");
+            counter.set(null);
+        }
+    }
+
+    @PostPersist(entityAnnotations = Tag.class)
+    @PostUpdate(entityAnnotations = Tag.class)
+    @PostRemove(entityAnnotations = Tag.class)
+    void afterCommit(Persistent object) {
+        counter.get()[0]++;
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener1.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener1.java
new file mode 100644
index 000000000..d1225d3ae
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener1.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.annotation.PostAdd;
+
+// tag::content[]
+public class Listener1 {
+
+    @PostAdd(MyEntity.class)
+    void postAdd(Persistent object) {
+        // do something
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener2.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener2.java
new file mode 100644
index 000000000..444f571ca
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener2.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.docs.lifecycle;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.annotation.PostRemove;
+import org.apache.cayenne.annotation.PostUpdate;
+
+// tag::content[]
+public class Listener2 {
+
+    @PostRemove({MyEntity1.class, MyEntity2.class})
+    void postRemove(Persistent object) {
+        // do something
+    }
+
+    @PostUpdate({MyEntity1.class, MyEntity2.class})
+    void postUpdate(Persistent object) {
+        // do something
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener3.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener3.java
new file mode 100644
index 000000000..4e88ef138
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Listener3.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+import org.apache.cayenne.Persistent;
+import org.apache.cayenne.annotation.PostAdd;
+
+// tag::content[]
+public class Listener3 {
+
+    @PostAdd(entityAnnotations = Tag.class)
+    void postAdd(Persistent object) {
+        // do something
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity.java
new file mode 100644
index 000000000..28ddfe45c
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity.java
@@ -0,0 +1,23 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+public class MyEntity extends _MyEntity {
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity1.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity1.java
new file mode 100644
index 000000000..23baebcc5
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity1.java
@@ -0,0 +1,26 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+// tag::content[]
+@Tag
+public class MyEntity1 extends _MyEntity1 {
+
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity2.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity2.java
new file mode 100644
index 000000000..8e78297f2
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/MyEntity2.java
@@ -0,0 +1,26 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+// tag::content[]
+@Tag
+public class MyEntity2 extends _MyEntity2 {
+
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Order.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Order.java
new file mode 100644
index 000000000..7cd62da1b
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Order.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.docs.lifecycle;
+
+import org.apache.cayenne.annotation.PostAdd;
+
+// tag::content[]
+public class Order extends _Order {
+
+    @PostAdd
+    public void onNewOrder() {
+        // Custom logic before inserting
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/OrderListener.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/OrderListener.java
new file mode 100644
index 000000000..a4fe1e4f0
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/OrderListener.java
@@ -0,0 +1,33 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+import org.apache.cayenne.annotation.PostAdd;
+
+import java.util.Date;
+
+// tag::content[]
+public class OrderListener {
+
+    @PostAdd(Order.class)
+    public void setDefaultsForNewOrder(Order o) {
+        o.setCreatedOn(new Date());
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Tag.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Tag.java
new file mode 100644
index 000000000..31478e975
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/Tag.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   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.docs.lifecycle;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+// tag::content[]
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Tag {
+
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity.java
new file mode 100644
index 000000000..def784f1a
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity.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.docs.lifecycle;
+
+import org.apache.cayenne.PersistentObject;
+
+/**
+ * A simplified stand-in for a generated superclass, that is not a part of the docs test model.
+ */
+public abstract class _MyEntity extends PersistentObject {
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity1.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity1.java
new file mode 100644
index 000000000..fe8173812
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity1.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.docs.lifecycle;
+
+import org.apache.cayenne.PersistentObject;
+
+/**
+ * A simplified stand-in for a generated superclass, that is not a part of the docs test model.
+ */
+public abstract class _MyEntity1 extends PersistentObject {
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity2.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity2.java
new file mode 100644
index 000000000..5a4f937f9
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_MyEntity2.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.docs.lifecycle;
+
+import org.apache.cayenne.PersistentObject;
+
+/**
+ * A simplified stand-in for a generated superclass, that is not a part of the docs test model.
+ */
+public abstract class _MyEntity2 extends PersistentObject {
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_Order.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_Order.java
new file mode 100644
index 000000000..5ec1c3c46
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/_Order.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.docs.lifecycle;
+
+import org.apache.cayenne.PersistentObject;
+
+import java.util.Date;
+
+/**
+ * A simplified stand-in for a generated superclass, that is not a part of the docs test model. Allows the examples
+ * that need an "Order" entity to compile.
+ */
+public abstract class _Order extends PersistentObject {
+
+    protected Date createdOn;
+
+    public void setCreatedOn(Date createdOn) {
+        beforePropertyWrite("createdOn", this.createdOn, createdOn);
+        this.createdOn = createdOn;
+    }
+
+    public Date getCreatedOn() {
+        beforePropertyRead("createdOn");
+        return this.createdOn;
+    }
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/Order.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/Order.java
new file mode 100644
index 000000000..c1ba9666b
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/Order.java
@@ -0,0 +1,29 @@
+/*****************************************************************
+ *   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.docs.lifecycle.modeler;
+
+// tag::content[]
+public class Order extends _Order {
+
+    @Override
+    protected void onNewOrder() {
+        //TODO: implement onNewOrder
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/_Order.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/_Order.java
new file mode 100644
index 000000000..16219e6c9
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/lifecycle/modeler/_Order.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.docs.lifecycle.modeler;
+
+import org.apache.cayenne.PersistentObject;
+
+// tag::content[]
+public abstract class _Order extends PersistentObject {
+    protected abstract void onNewOrder();
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Artist.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Artist.java
new file mode 100644
index 000000000..86c12cb45
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Artist.java
@@ -0,0 +1,25 @@
+/*****************************************************************
+ *   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.docs.persistent;
+
+import org.apache.cayenne.docs.persistent.auto._Artist;
+
+public class Artist extends _Artist {
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/CompoundPk.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/CompoundPk.java
new file mode 100644
index 000000000..3ade34f7f
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/CompoundPk.java
@@ -0,0 +1,25 @@
+/*****************************************************************
+ *   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.docs.persistent;
+
+import org.apache.cayenne.docs.persistent.auto._CompoundPk;
+
+public class CompoundPk extends _CompoundPk {
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Gallery.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Gallery.java
new file mode 100644
index 000000000..96d6442a2
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Gallery.java
@@ -0,0 +1,25 @@
+/*****************************************************************
+ *   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.docs.persistent;
+
+import org.apache.cayenne.docs.persistent.auto._Gallery;
+
+public class Gallery extends _Gallery {
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Painting.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Painting.java
new file mode 100644
index 000000000..7c0ae9442
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/Painting.java
@@ -0,0 +1,25 @@
+/*****************************************************************
+ *   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.docs.persistent;
+
+import org.apache.cayenne.docs.persistent.auto._Painting;
+
+public class Painting extends _Painting {
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Artist.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Artist.java
new file mode 100644
index 000000000..73a62238c
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Artist.java
@@ -0,0 +1,126 @@
+/*****************************************************************
+ *   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.docs.persistent.auto;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.exp.property.DateProperty;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _Artist 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 _Artist extends PersistentObject {
+
+    public static final SelfProperty<Artist> SELF = PropertyFactory.createSelf(Artist.class);
+
+    public static final String ARTIST_ID_PK_COLUMN = "ARTIST_ID";
+
+    public static final StringProperty<String> ARTIST_NAME = PropertyFactory.createString("artistName", String.class);
+    public static final DateProperty<LocalDate> DATE_OF_BIRTH = PropertyFactory.createDate("dateOfBirth", LocalDate.class);
+    public static final ListProperty<Painting> PAINTING_ARRAY = PropertyFactory.createList("paintingArray", Painting.class);
+
+    protected String artistName;
+    protected LocalDate dateOfBirth;
+
+    protected Object paintingArray;
+
+    public void setArtistName(String artistName) {
+        beforePropertyWrite("artistName", this.artistName, artistName);
+        this.artistName = artistName;
+    }
+
+    public String getArtistName() {
+        beforePropertyRead("artistName");
+        return this.artistName;
+    }
+
+    public void setDateOfBirth(LocalDate dateOfBirth) {
+        beforePropertyWrite("dateOfBirth", this.dateOfBirth, dateOfBirth);
+        this.dateOfBirth = dateOfBirth;
+    }
+
+    public LocalDate getDateOfBirth() {
+        beforePropertyRead("dateOfBirth");
+        return this.dateOfBirth;
+    }
+
+    public void addToPaintingArray(Painting obj) {
+        addToManyTarget("paintingArray", obj, true);
+    }
+
+    public void removeFromPaintingArray(Painting obj) {
+        removeToManyTarget("paintingArray", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Painting> getPaintingArray() {
+        return (List<Painting>)readProperty("paintingArray");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "artistName":
+                return this.artistName;
+            case "dateOfBirth":
+                return this.dateOfBirth;
+            case "paintingArray":
+                return this.paintingArray;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "artistName":
+                this.artistName = (String)val;
+                break;
+            case "dateOfBirth":
+                this.dateOfBirth = (LocalDate)val;
+                break;
+            case "paintingArray":
+                this.paintingArray = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_CompoundPk.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_CompoundPk.java
new file mode 100644
index 000000000..c699d6de4
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_CompoundPk.java
@@ -0,0 +1,84 @@
+/*****************************************************************
+ *   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.docs.persistent.auto;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.docs.persistent.CompoundPk;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _CompoundPk 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 _CompoundPk extends PersistentObject {
+
+    public static final SelfProperty<CompoundPk> SELF = PropertyFactory.createSelf(CompoundPk.class);
+
+    public static final String KEY1_PK_COLUMN = "KEY1";
+    public static final String KEY2_PK_COLUMN = "KEY2";
+
+    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);
+        }
+    }
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Gallery.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Gallery.java
new file mode 100644
index 000000000..42d2add14
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Gallery.java
@@ -0,0 +1,107 @@
+/*****************************************************************
+ *   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.docs.persistent.auto;
+
+import java.util.List;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.docs.persistent.Gallery;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.exp.property.ListProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _Gallery 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 _Gallery extends PersistentObject {
+
+    public static final SelfProperty<Gallery> SELF = PropertyFactory.createSelf(Gallery.class);
+
+    public static final String GALLERY_ID_PK_COLUMN = "GALLERY_ID";
+
+    public static final StringProperty<String> GALLERY_NAME = PropertyFactory.createString("galleryName", String.class);
+    public static final ListProperty<Painting> PAINTING_ARRAY = PropertyFactory.createList("paintingArray", Painting.class);
+
+    protected String galleryName;
+
+    protected Object paintingArray;
+
+    public void setGalleryName(String galleryName) {
+        beforePropertyWrite("galleryName", this.galleryName, galleryName);
+        this.galleryName = galleryName;
+    }
+
+    public String getGalleryName() {
+        beforePropertyRead("galleryName");
+        return this.galleryName;
+    }
+
+    public void addToPaintingArray(Painting obj) {
+        addToManyTarget("paintingArray", obj, true);
+    }
+
+    public void removeFromPaintingArray(Painting obj) {
+        removeToManyTarget("paintingArray", obj, true);
+    }
+
+    @SuppressWarnings("unchecked")
+    public List<Painting> getPaintingArray() {
+        return (List<Painting>)readProperty("paintingArray");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "galleryName":
+                return this.galleryName;
+            case "paintingArray":
+                return this.paintingArray;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "galleryName":
+                this.galleryName = (String)val;
+                break;
+            case "paintingArray":
+                this.paintingArray = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Painting.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Painting.java
new file mode 100644
index 000000000..665d9069d
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/persistent/auto/_Painting.java
@@ -0,0 +1,136 @@
+/*****************************************************************
+ *   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.docs.persistent.auto;
+
+import java.math.BigDecimal;
+
+import org.apache.cayenne.PersistentObject;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.docs.persistent.Gallery;
+import org.apache.cayenne.docs.persistent.Painting;
+import org.apache.cayenne.exp.property.EntityProperty;
+import org.apache.cayenne.exp.property.NumericProperty;
+import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
+import org.apache.cayenne.exp.property.StringProperty;
+
+/**
+ * Class _Painting 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 _Painting extends PersistentObject {
+
+    public static final SelfProperty<Painting> SELF = PropertyFactory.createSelf(Painting.class);
+
+    public static final String PAINTING_ID_PK_COLUMN = "PAINTING_ID";
+
+    public static final NumericProperty<BigDecimal> ESTIMATED_PRICE = PropertyFactory.createNumeric("estimatedPrice", BigDecimal.class);
+    public static final StringProperty<String> PAINTING_TITLE = PropertyFactory.createString("paintingTitle", String.class);
+    public static final EntityProperty<Artist> TO_ARTIST = PropertyFactory.createEntity("toArtist", Artist.class);
+    public static final EntityProperty<Gallery> TO_GALLERY = PropertyFactory.createEntity("toGallery", Gallery.class);
+
+    protected BigDecimal estimatedPrice;
+    protected String paintingTitle;
+
+    protected Object toArtist;
+    protected Object toGallery;
+
+    public void setEstimatedPrice(BigDecimal estimatedPrice) {
+        beforePropertyWrite("estimatedPrice", this.estimatedPrice, estimatedPrice);
+        this.estimatedPrice = estimatedPrice;
+    }
+
+    public BigDecimal getEstimatedPrice() {
+        beforePropertyRead("estimatedPrice");
+        return this.estimatedPrice;
+    }
+
+    public void setPaintingTitle(String paintingTitle) {
+        beforePropertyWrite("paintingTitle", this.paintingTitle, paintingTitle);
+        this.paintingTitle = paintingTitle;
+    }
+
+    public String getPaintingTitle() {
+        beforePropertyRead("paintingTitle");
+        return this.paintingTitle;
+    }
+
+    public void setToArtist(Artist toArtist) {
+        setToOneTarget("toArtist", toArtist, true);
+    }
+
+    public Artist getToArtist() {
+        return (Artist)readProperty("toArtist");
+    }
+
+    public void setToGallery(Gallery toGallery) {
+        setToOneTarget("toGallery", toGallery, true);
+    }
+
+    public Gallery getToGallery() {
+        return (Gallery)readProperty("toGallery");
+    }
+
+    @Override
+    public Object readPropertyDirectly(String propName) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch(propName) {
+            case "estimatedPrice":
+                return this.estimatedPrice;
+            case "paintingTitle":
+                return this.paintingTitle;
+            case "toArtist":
+                return this.toArtist;
+            case "toGallery":
+                return this.toGallery;
+            default:
+                return super.readPropertyDirectly(propName);
+        }
+    }
+
+    @Override
+    public void writePropertyDirectly(String propName, Object val) {
+        if(propName == null) {
+            throw new IllegalArgumentException();
+        }
+
+        switch (propName) {
+            case "estimatedPrice":
+                this.estimatedPrice = (BigDecimal)val;
+                break;
+            case "paintingTitle":
+                this.paintingTitle = (String)val;
+                break;
+            case "toArtist":
+                this.toArtist = val;
+                break;
+            case "toGallery":
+                this.toGallery = val;
+                break;
+            default:
+                super.writePropertyDirectly(propName, val);
+        }
+    }
+
+}
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/tuning/MyModule.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/tuning/MyModule.java
new file mode 100644
index 000000000..3bcc9d8fa
--- /dev/null
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/tuning/MyModule.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.docs.tuning;
+
+import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.runtime.CoreModule;
+import org.apache.cayenne.di.Binder;
+import org.apache.cayenne.di.Module;
+
+// tag::content[]
+public class MyModule implements Module {
+
+    @Override
+    public void configure(Binder binder) {
+        CoreModule.extend(binder)
+                .setProperty(Constants.CONTEXTS_SYNC_PROPERTY, "false");
+    }
+}
+// end::content[]
diff --git a/docs/cayenne-guide/src/test/resources/cayenne-project.xml b/docs/cayenne-guide/src/test/resources/cayenne-project.xml
new file mode 100644
index 000000000..008cd7012
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/cayenne-project.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/13/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/domain https://cayenne.apache.org/schema/13/domain.xsd"
+	 project-version="13">
+	<map name="docs"/>
+</domain>
diff --git a/docs/cayenne-guide/src/test/resources/com/example/cayenne-mydomain.xml b/docs/cayenne-guide/src/test/resources/com/example/cayenne-mydomain.xml
new file mode 100644
index 000000000..cc225d349
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/com/example/cayenne-mydomain.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/13/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/domain https://cayenne.apache.org/schema/13/domain.xsd"
+	 project-version="13">
+</domain>
diff --git a/docs/cayenne-guide/src/test/resources/com/example/cayenne-project.xml b/docs/cayenne-guide/src/test/resources/com/example/cayenne-project.xml
new file mode 100644
index 000000000..cc225d349
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/com/example/cayenne-project.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/13/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/domain https://cayenne.apache.org/schema/13/domain.xsd"
+	 project-version="13">
+</domain>
diff --git a/docs/cayenne-guide/src/test/resources/docs.map.xml b/docs/cayenne-guide/src/test/resources/docs.map.xml
new file mode 100644
index 000000000..8ebaae7a2
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/docs.map.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<data-map xmlns="http://cayenne.apache.org/schema/13/modelMap"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/modelMap https://cayenne.apache.org/schema/13/modelMap.xsd"
+	 project-version="13">
+	<property name="defaultPackage" value="org.apache.cayenne.docs.persistent"/>
+	<db-entity name="ARTIST">
+		<db-attribute name="ARTIST_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="ARTIST_NAME" type="VARCHAR" length="200"/>
+		<db-attribute name="DATE_OF_BIRTH" type="DATE"/>
+	</db-entity>
+	<db-entity name="COMPOUND_PK">
+		<db-attribute name="KEY1" type="VARCHAR" isPrimaryKey="true" isMandatory="true" length="20"/>
+		<db-attribute name="KEY2" type="VARCHAR" isPrimaryKey="true" isMandatory="true" length="20"/>
+		<db-attribute name="NAME" type="VARCHAR" length="200"/>
+	</db-entity>
+	<db-entity name="GALLERY">
+		<db-attribute name="GALLERY_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="GALLERY_NAME" type="VARCHAR" length="200"/>
+	</db-entity>
+	<db-entity name="GENERIC_ENTITY">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="NAME" type="VARCHAR" length="200"/>
+	</db-entity>
+	<db-entity name="PAINTING">
+		<db-attribute name="ARTIST_ID" type="INTEGER"/>
+		<db-attribute name="ESTIMATED_PRICE" type="DECIMAL" length="10" scale="2"/>
+		<db-attribute name="GALLERY_ID" type="INTEGER"/>
+		<db-attribute name="PAINTING_ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="PAINTING_TITLE" type="VARCHAR" length="200"/>
+	</db-entity>
+	<obj-entity name="Artist" className="org.apache.cayenne.docs.persistent.Artist" dbEntityName="ARTIST">
+		<obj-attribute name="artistName" type="java.lang.String" db-attribute-path="ARTIST_NAME"/>
+		<obj-attribute name="dateOfBirth" type="java.time.LocalDate" db-attribute-path="DATE_OF_BIRTH"/>
+	</obj-entity>
+	<obj-entity name="CompoundPk" className="org.apache.cayenne.docs.persistent.CompoundPk" dbEntityName="COMPOUND_PK">
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+	<obj-entity name="Gallery" className="org.apache.cayenne.docs.persistent.Gallery" dbEntityName="GALLERY">
+		<obj-attribute name="galleryName" type="java.lang.String" db-attribute-path="GALLERY_NAME"/>
+	</obj-entity>
+	<obj-entity name="GenericEntity" dbEntityName="GENERIC_ENTITY">
+		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
+	</obj-entity>
+	<obj-entity name="Painting" className="org.apache.cayenne.docs.persistent.Painting" dbEntityName="PAINTING">
+		<obj-attribute name="estimatedPrice" type="java.math.BigDecimal" db-attribute-path="ESTIMATED_PRICE"/>
+		<obj-attribute name="paintingTitle" type="java.lang.String" db-attribute-path="PAINTING_TITLE"/>
+	</obj-entity>
+	<db-relationship name="paintingArray" source="ARTIST" target="PAINTING" toMany="true">
+		<db-attribute-pair source="ARTIST_ID" target="ARTIST_ID"/>
+	</db-relationship>
+	<db-relationship name="paintingArray" source="GALLERY" target="PAINTING" toMany="true">
+		<db-attribute-pair source="GALLERY_ID" target="GALLERY_ID"/>
+	</db-relationship>
+	<db-relationship name="toArtist" source="PAINTING" target="ARTIST">
+		<db-attribute-pair source="ARTIST_ID" target="ARTIST_ID"/>
+	</db-relationship>
+	<db-relationship name="toGallery" source="PAINTING" target="GALLERY">
+		<db-attribute-pair source="GALLERY_ID" target="GALLERY_ID"/>
+	</db-relationship>
+	<obj-relationship name="paintingArray" source="Artist" target="Painting" deleteRule="Cascade" db-relationship-path="paintingArray"/>
+	<obj-relationship name="paintingArray" source="Gallery" target="Painting" deleteRule="Nullify" db-relationship-path="paintingArray"/>
+	<obj-relationship name="toArtist" source="Painting" target="Artist" deleteRule="Nullify" db-relationship-path="toArtist"/>
+	<obj-relationship name="toGallery" source="Painting" target="Gallery" deleteRule="Nullify" db-relationship-path="toGallery"/>
+	<query name="artistsByName" type="SelectQuery" root="obj-entity" root-name="Artist">
+		<qualifier><![CDATA[artistName = $name]]></qualifier>
+	</query>
+	<query name="updatePaintingTitle" type="SQLTemplate" root="data-map" root-name="docs">
+		<sql><![CDATA[UPDATE PAINTING SET PAINTING_TITLE = #bind($title)]]></sql>
+	</query>
+</data-map>
diff --git a/docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/expressions.txt b/docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/expressions.txt
new file mode 100644
index 000000000..d19751eff
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/expressions.txt
@@ -0,0 +1,63 @@
+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.
+
+The query Strings that are included in the docs, and are parsed and run by ObjectSelectParsedTest
+The expression Strings that are included in the docs, and are parsed by ExpressionsTest
+
+tag::case[]
+// valid
+tag::caseValid[]
+name likeIgnoreCase 'A%'
+end::caseValid[]
+
+// invalid - will throw a parse exception
+tag::caseInvalid[]
+name LIKEIGNORECASE 'A%'
+end::caseInvalid[]
+end::case[]
+
+tag::grouping[]
+value = (price + 250.00) * 3
+end::grouping[]
+
+tag::paths[]
+// object path
+tag::objPath[]
+name = 'Salvador Dali'
+end::objPath[]
+
+// same object path - a rarely used form
+tag::objPrefixPath[]
+obj:name = 'Salvador Dali'
+end::objPrefixPath[]
+
+// multi-segment object path
+tag::multiSegmentPath[]
+artist.name = 'Salvador Dali'
+end::multiSegmentPath[]
+
+// db path
+tag::dbPath[]
+db:NAME = 'Salvador Dali'
+end::dbPath[]
+
+// enumeration constant
+tag::enumPath[]
+name = enum:org.foo.EnumClass.VALUE1
+end::enumPath[]
+end::paths[]
+
+tag::like[]
+name like 'A%'
+end::like[]
diff --git a/docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/objectselect-parsed.txt b/docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/objectselect-parsed.txt
new file mode 100644
index 000000000..652647fe4
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/org/apache/cayenne/docs/objectselect-parsed.txt
@@ -0,0 +1,85 @@
+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.
+
+The query Strings that are included in the docs, and are parsed and run by ObjectSelectParsedTest
+
+tag::filterOrderLimit[]
+from Artist where artistName like 'A%' order by dateOfBirth desc, artistName asc insensitive limit 10 offset 20
+end::filterOrderLimit[]
+
+tag::outerJoin[]
+from Artist where paintingArray+ = null
+end::outerJoin[]
+
+tag::splitJoins[]
+from Artist where paintingArray#p1.paintingTitle = 'P1' and paintingArray#p2.paintingTitle = 'P2'
+end::splitJoins[]
+
+tag::columns[]
+select artistName, dateOfBirth from Artist order by artistName
+end::columns[]
+
+tag::column[]
+select distinct toGallery.galleryName from Painting
+end::column[]
+
+tag::columnExpressions[]
+select upper(artistName) as name, length(trim(artistName)) as size from Artist
+end::columnExpressions[]
+
+tag::aggregates[]
+select toArtist.artistName, sum(estimatedPrice), count(*)
+from Painting
+having sum(estimatedPrice) > $min
+order by toArtist.artistName
+end::aggregates[]
+
+tag::self[]
+select self, count(paintingArray+) from Artist order by count(paintingArray+) desc
+end::self[]
+
+tag::toOne[]
+select paintingTitle, toArtist, toGallery from Painting
+end::toOne[]
+
+tag::toMany[]
+select artistName, paintingArray from Artist
+end::toMany[]
+
+tag::exists[]
+from Artist where exists (
+    from Painting where toArtist = enclosing(self) and estimatedPrice > $price)
+end::exists[]
+
+tag::nestedSelects[]
+tag::in[]
+from Painting where toArtist.artistName in (select artistName from Artist where dateOfBirth < $date)
+end::in[]
+
+tag::all[]
+from Painting where estimatedPrice > all (select estimatedPrice from Painting where paintingTitle = 'P2')
+end::all[]
+
+tag::any[]
+from Painting where estimatedPrice = any (select max(estimatedPrice) from Painting)
+end::any[]
+end::nestedSelects[]
+
+tag::dbRoot[]
+from db:ARTIST where ARTIST_NAME like 'A%' order by ARTIST_ID desc
+end::dbRoot[]
+
+tag::prefetch[]
+from Artist where artistName = $name prefetch paintingArray joint, paintingArray.toGallery
+end::prefetch[]
diff --git a/docs/cayenne-guide/src/test/resources/org/foo/cayenne-library1.xml b/docs/cayenne-guide/src/test/resources/org/foo/cayenne-library1.xml
new file mode 100644
index 000000000..cc225d349
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/org/foo/cayenne-library1.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/13/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/domain https://cayenne.apache.org/schema/13/domain.xsd"
+	 project-version="13">
+</domain>
diff --git a/docs/cayenne-guide/src/test/resources/org/foo/cayenne-library2.xml b/docs/cayenne-guide/src/test/resources/org/foo/cayenne-library2.xml
new file mode 100644
index 000000000..cc225d349
--- /dev/null
+++ b/docs/cayenne-guide/src/test/resources/org/foo/cayenne-library2.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/13/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/domain https://cayenne.apache.org/schema/13/domain.xsd"
+	 project-version="13">
+</domain>
diff --git a/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc b/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
index 71f6ec9b2..c867f4c43 100644
--- a/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
+++ b/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
@@ -39,6 +39,7 @@ public class Main {
 package org.apache.cayenne.tutorial;
 
 import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.datasource.CayenneDataSource;
 import org.apache.cayenne.runtime.CayenneRuntime;
 
 public class Main {
diff --git a/docs/getting-started-guide/pom.xml b/docs/getting-started-guide/pom.xml
index 2e2f18d52..2b909b7e1 100644
--- a/docs/getting-started-guide/pom.xml
+++ b/docs/getting-started-guide/pom.xml
@@ -31,6 +31,21 @@
     <packaging>jar</packaging>
     <name>getting-started-guide: AsciiDoc - Getting Started with Cayenne</name>
 
+    <dependencies>
+
+        <!-- Test dependencies. The test runs the tutorial code that is included in the docs -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
     <build>
         <plugins>
             <plugin>
diff --git a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/delete.adoc b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/delete.adoc
index 45ff050c0..7865d80cc 100644
--- a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/delete.adoc
+++ b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/delete.adoc
@@ -38,20 +38,16 @@ more common way in Cayenne (or ORM in general) is to get a hold of the object fi
 then delete it via the context. Let's use utility class Cayenne to find an
 artist:
 
-[source, java]
+[source,java,indent=0]
 ----
-Artist picasso = ObjectSelect.query(Artist.class)
-            .where(Artist.NAME.eq("Pablo Picasso")).selectOne(context);
+include::{java-include-dir}/Main.java[tags=delete-select]
 ----
 
 Now let's delete the artist:
 
-[source, java]
+[source,java,indent=0]
 ----
-if (picasso != null) {
-    context.deleteObject(picasso);
-    context.commitChanges();
-}
+include::{java-include-dir}/Main.java[tags=delete]
 ----
 Since we set up "Cascade" delete rule for the Artist.paintings relationships, Cayenne
 will automatically delete all paintings of this artist. So when your run the app you'll
diff --git a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/select-query.adoc b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/select-query.adoc
index 1914644c2..fb10065ca 100644
--- a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/select-query.adoc
+++ b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/select-query.adoc
@@ -23,9 +23,9 @@ in the database yet, but we can still demonstrate the main principles below.
 
 - Select all paintings (the code, and the log output it generates):
 
-[source,java]
+[source,java,indent=0]
 ----
-List<Painting> paintings1 = ObjectSelect.query(Painting.class).select(context);
+include::{java-include-dir}/Main.java[tags=select-all]
 ----
 
    INFO: SELECT t0.GALLERY_ID, t0.ARTIST_ID, t0.NAME, t0.ID FROM PAINTING t0
@@ -33,10 +33,9 @@ List<Painting> paintings1 = ObjectSelect.query(Painting.class).select(context);
 
 - Select paintings that start with "`gi`", ignoring case:
 
-[source,java]
+[source,java,indent=0]
 ----
-List<Painting> paintings2 = ObjectSelect.query(Painting.class)
-        .where(Painting.NAME.likeIgnoreCase("gi%")).select(context);
+include::{java-include-dir}/Main.java[tags=select-like]
 ----
     INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 WHERE UPPER(t0.NAME) LIKE UPPER(?)
       [bind: 1->NAME:'gi%'] - prepared in 6 ms.
@@ -44,11 +43,9 @@ List<Painting> paintings2 = ObjectSelect.query(Painting.class)
 
 - Select all paintings done by artists who were born more than a 100 years ago:
 
-[source,java]
+[source,java,indent=0]
 ----
-List<Painting> paintings3 = ObjectSelect.query(Painting.class)
-        .where(Painting.ARTIST.dot(Artist.DATE_OF_BIRTH).lt(LocalDate.of(1900,1,1)))
-        .select(context);
+include::{java-include-dir}/Main.java[tags=select-path]
 ----
 
     INFO: SELECT t0.GALLERY_ID, t0.NAME, t0.ARTIST_ID, t0.ID FROM PAINTING t0 JOIN ARTIST t1 ON (t0.ARTIST_ID = t1.ID)
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java b/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java
index 3214780b5..3b462894a 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java
+++ b/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java
@@ -92,25 +92,35 @@ public class Main {
     }
 
     static void selectTutorial(ObjectContext context) {
-        // SelectQuery examples
+        // ObjectSelect examples
+        // tag::select-all[]
         List<Painting> paintings1 = ObjectSelect.query(Painting.class).select(context);
+        // end::select-all[]
 
+        // tag::select-like[]
         List<Painting> paintings2 = ObjectSelect.query(Painting.class)
                 .where(Painting.NAME.likeIgnoreCase("gi%")).select(context);
+        // end::select-like[]
 
+        // tag::select-path[]
         List<Painting> paintings3 = ObjectSelect.query(Painting.class)
-                .where(Painting.ARTIST.dot(Artist.DATE_OF_BIRTH)
-                        .lt(LocalDate.of(1900,1,1))).select(context);
+                .where(Painting.ARTIST.dot(Artist.DATE_OF_BIRTH).lt(LocalDate.of(1900, 1, 1)))
+                .select(context);
+        // end::select-path[]
     }
 
     static void deleteTutorial(ObjectContext context) {
         // Delete object examples
+        // tag::delete-select[]
         Artist picasso = ObjectSelect.query(Artist.class)
                 .where(Artist.NAME.eq("Pablo Picasso")).selectOne(context);
+        // end::delete-select[]
 
+        // tag::delete[]
         if (picasso != null) {
-            context.deleteObjects(picasso);
+            context.deleteObject(picasso);
             context.commitChanges();
         }
+        // end::delete[]
     }
 }
diff --git a/docs/getting-started-guide/src/test/java/org/apache/cayenne/tutorial/MainTest.java b/docs/getting-started-guide/src/test/java/org/apache/cayenne/tutorial/MainTest.java
new file mode 100644
index 000000000..394d7b30b
--- /dev/null
+++ b/docs/getting-started-guide/src/test/java/org/apache/cayenne/tutorial/MainTest.java
@@ -0,0 +1,32 @@
+/*****************************************************************
+ *   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.tutorial;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Runs the tutorial application, parts of which are included in the docs as code examples.
+ */
+public class MainTest {
+
+    @Test
+    public void main() {
+        Main.main(new String[0]);
+    }
+}
diff --git a/docs/pom.xml b/docs/pom.xml
index c794a1dea..001385458 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -107,6 +107,12 @@
 				<configuration>
 					<sourceDirectory>src/docs/asciidoc</sourceDirectory>
 					<doctype>book</doctype>
+					<!-- Fail on warnings, most importantly on the code examples not found in the included Java sources -->
+					<logHandler>
+						<failIf>
+							<severity>WARN</severity>
+						</failIf>
+					</logHandler>
 					<!-- Attributes common to all output formats -->
 					<attributes>
 						<endpoint-url>http://cayenne.apache.org</endpoint-url>


(cayenne) 02/02: Moving docs code snippets into compiled tests

Posted by aa...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git

commit c5d1d54096f8349ee1cdf6a5ee301611841f07da
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Sun Sep 20 14:44:14 2026 -0400

    Moving docs code snippets into compiled tests
    
    ... fixing stale docs
---
 CLAUDE.md                                          |   2 +
 .../org/apache/cayenne/query/ProcedureCall.java    |   9 ++-
 .../org/apache/cayenne/query/ProcedureCallIT.java  |  23 ++++++++
 .../_cayenne-guide/configurationProperties.adoc    |  24 ++++----
 .../asciidoc/_cayenne-guide/part1/mapping.adoc     |   2 +-
 .../asciidoc/_cayenne-guide/part2/customize.adoc   |   2 +-
 .../asciidoc/_cayenne-guide/part2/expressions.adoc |  29 +++++-----
 .../_cayenne-guide/part2/queries/ejbql.adoc        |   4 +-
 .../_cayenne-guide/part2/queries/mapped.adoc       |   2 -
 .../asciidoc/_cayenne-guide/part2/starting.adoc    |  31 +++++++++-
 .../org/apache/cayenne/docs/ExpressionsTest.java   |  37 +++++++++++-
 .../apache/cayenne/docs/ProcedureCallExamples.java |   3 +-
 .../java/org/apache/cayenne/docs/StartingTest.java |  64 +++++++++++++++++++++
 .../_getting-started-db-first/part4-java-code.adoc |  35 ++---------
 .../docs/asciidoc/getting-started-db-first.adoc    |   2 +
 .../java/org/apache/cayenne/tutorial/Main.java     |  59 +++++++++++++++++++
 .../cayenne/tutorial/persistent/Artist.java}       |   4 +-
 .../cayenne/tutorial/persistent/Gallery.java       |   0
 .../cayenne/tutorial/persistent/Painting.java      |   0
 .../cayenne/tutorial/persistent/auto/_Artist.java  |   4 ++
 .../cayenne/tutorial/persistent/auto/_Gallery.java |   4 ++
 .../tutorial/persistent/auto/_Painting.java        |   8 ++-
 .../src/main/resources/cayenne-project.xml         |   7 +++
 .../src/main/resources/datamap.map.xml             |  26 ++++-----
 .../_getting-started-guide/java-classes.adoc       |   7 +--
 .../_getting-started-guide/object-context.adoc     |  36 ++++--------
 .../_getting-started-guide/starting-project.adoc   |  33 +----------
 .../src/docs/asciidoc/getting-started-guide.adoc   |   2 +-
 .../src/docs/asciidoc/images/base-datanode.png     | Bin 92592 -> 0 bytes
 .../src/docs/asciidoc/images/icon-node.png         | Bin 384 -> 0 bytes
 .../cayenne/tutorial => example/cayenne}/Main.java |  33 ++++++++---
 .../cayenne}/persistent/Artist.java                |   4 +-
 .../cayenne}/persistent/Datamap.java               |   4 +-
 .../cayenne}/persistent/Gallery.java               |   4 +-
 .../cayenne}/persistent/Painting.java              |   4 +-
 .../cayenne}/persistent/auto/_Artist.java          |   8 ++-
 .../cayenne}/persistent/auto/_Datamap.java         |   2 +-
 .../cayenne}/persistent/auto/_Gallery.java         |   8 ++-
 .../cayenne}/persistent/auto/_Painting.java        |  14 +++--
 .../src/main/resources/datamap.map.xml             |   8 +--
 .../tutorial => example/cayenne}/MainTest.java     |   2 +-
 41 files changed, 366 insertions(+), 184 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 6c9e4fde8..ebddf15a8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -113,6 +113,8 @@ with the Cayenne API:
   expression Strings) are tagged in `src/test/resources/org/apache/cayenne/docs/*.txt` and read by the tests via
   `BaseTest.example(..)`.
 - `docs/getting-started-guide` — examples are in the tutorial app under `src/main/java`, that is run by `MainTest`.
+- `docs/getting-started-db-first` — examples are in the tutorial app under `src/main/java`. It is only compiled, as it
+  needs a MySQL DB to run.
 
 ```java
 // tag::where[]
diff --git a/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java b/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java
index 562e8e52f..f60e9b4dd 100644
--- a/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java
+++ b/cayenne/src/main/java/org/apache/cayenne/query/ProcedureCall.java
@@ -37,10 +37,13 @@ import java.util.Map;
 public class ProcedureCall<T> extends IndirectQuery {
 
     /**
-     * Creates procedure call using name of stored procedure defined in the mapping file.
+     * Creates procedure call using name of stored procedure defined in the mapping file. With no result class
+     * specified, the result sets are returned as data rows.
      */
-    public static ProcedureCall query(String procedure) {
-        return new ProcedureCall(procedure);
+    public static ProcedureCall<DataRow> query(String procedure) {
+        ProcedureCall<DataRow> procedureCall = new ProcedureCall<>(procedure);
+        procedureCall.fetchingDataRows = true;
+        return procedureCall;
     }
 
     /**
diff --git a/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java b/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java
index 213619f7b..2e1d05355 100644
--- a/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java
+++ b/cayenne/src/test/java/org/apache/cayenne/query/ProcedureCallIT.java
@@ -207,6 +207,29 @@ public class ProcedureCallIT {
         assertEquals(40, price.intValue());
     }
 
+    @Test
+    public void callResultIsTyped() throws Exception {
+        if (!env.testDbAdapter().supportsStoredProcedures()) {
+            return;
+        }
+
+        // iterating over "call(..)" with no intermediate variable only compiles if "query(String)" is not a raw type
+        int outParams = runProcedure(() -> {
+            int count = 0;
+            for (QueryResult item : ProcedureCall.query(OUT_STORED_PROCEDURE)
+                    .param("in_param", 20)
+                    .call(env.context())) {
+                if (item instanceof QueryResult.OutParameters out) {
+                    assertEquals(40, ((Number) out.values().get("out_param")).intValue());
+                    count++;
+                }
+            }
+            return count;
+        });
+
+        assertEquals(1, outParams);
+    }
+
     @Test
     public void selectPersistentObject() throws Exception {
         if (!env.testDbAdapter().supportsStoredProcedures()) {
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/configurationProperties.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/configurationProperties.adoc
index 12bb8ee0a..427634458 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/configurationProperties.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/configurationProperties.adoc
@@ -19,34 +19,34 @@ Note that the property names below are defined as constants in `org.apache.cayen
 [#configProperties]
 * `cayenne.jdbc.driver[.domain_name.node_name]`
    defines a JDBC driver class to use when creating a DataSource. If domain name and optionally - node name are specified,
-   the setting overrides DataSource info just for this domain/node. Otherwise the override is applied to all domains/nodes in the system.
-    ** Default value: none, project DataNode configuration is used
+   the setting applies just to this domain/node. Otherwise it applies to all domains/nodes in the system.
+    ** Default value: none. Only used for the DataNodes that were not given an explicit DataSource
 
 * `cayenne.jdbc.url[.domain_name.node_name]`
     defines a DB URL to use when creating a DataSource. If domain name and optionally - node name are specified,
-    the setting overrides DataSource info just for this domain/node. Otherwise the override is applied to all domains/nodes in the system.
-    ** Default value: none, project DataNode configuration is used
+    the setting applies just to this domain/node. Otherwise it applies to all domains/nodes in the system.
+    ** Default value: none. Only used for the DataNodes that were not given an explicit DataSource
 
 * `cayenne.jdbc.username[.domain_name.node_name]`
    defines a DB user name to use when creating a DataSource. If domain name and optionally - node name are specified,
-   the setting overrides DataSource info just for this domain/node. Otherwise the override is applied to all domains/nodes in the system.
+   the setting applies just to this domain/node. Otherwise it applies to all domains/nodes in the system.
     ** Possible values: any
-    ** Default value: none, project DataNode configuration is used
+    ** Default value: none. Only used for the DataNodes that were not given an explicit DataSource
 
 * `cayenne.jdbc.password[.domain_name.node_name]`
    defines a DB password to use when creating a DataSource. If domain name and optionally - node name are specified,
-   the setting overrides DataSource info just for this domain/node. Otherwise the override is applied to all domains/nodes in the system
-    ** Default value: none, project DataNode configuration is used
+   the setting applies just to this domain/node. Otherwise it applies to all domains/nodes in the system
+    ** Default value: none. Only used for the DataNodes that were not given an explicit DataSource
 
 * `cayenne.jdbc.min_connections[.domain_name.node_name]`
    defines the DB connection pool minimal size. If domain name and optionally - node name are specified, the setting
-   overrides DataSource info just for this domain/node. Otherwise the override is applied to all domains/nodes in the system
-    ** Default value: none, project DataNode configuration is used
+   applies just to this domain/node. Otherwise it applies to all domains/nodes in the system
+    ** Default value: none. Only used for the DataNodes that were not given an explicit DataSource
 
 * `cayenne.jdbc.max_connections[.domain_name.node_name]`
    defines the DB connection pool maximum size. If domain name and optionally - node name are specified, the setting
-   overrides DataSource info just for this domain/node. Otherwise the override is applied to all domains/nodes in the system
-    ** Default value: none, project DataNode configuration is used
+   applies just to this domain/node. Otherwise it applies to all domains/nodes in the system
+    ** Default value: none. Only used for the DataNodes that were not given an explicit DataSource
 
 * `cayenne.jdbc.max_wait`
    defines a maximum time in milliseconds that a connection request could
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part1/mapping.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part1/mapping.adoc
index f8ad37814..0bc323d93 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part1/mapping.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part1/mapping.adoc
@@ -42,7 +42,7 @@ DataMap is a container of persistent entities and other object-relational metada
 
 ==== DataNode
 
-DataNode is model of a database. It is actually pretty simple. It has an arbitrary user-provided name and information needed to create or locate a JDBC DataSource. Most projects only have one DataNode, though there may be any number of nodes if needed.
+DataNode is a model of a database. It is actually pretty simple. It has a name and a JDBC DataSource. Unlike the rest of the objects discussed here, DataNodes are not a part of the XML mapping project. They are defined in the code, when Cayenne runtime is created (see <<Starting Cayenne>>). Most applications only have one DataNode, though there may be any number of nodes if needed.
 
 ==== DbEntity
 
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc
index 9398d52aa..024839f26 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/customize.adoc
@@ -57,7 +57,7 @@ include::{java-examples}/di/constructor/Service1Impl.java[tags=content]
 include::{java-examples}/di/Service2Impl.java[tags=content]
 ----
 
-Now let's create a module implementing `org.apache.cayenne.tutorial.di.Module` interface that will contain DI configuration. A module binds service objects to keys that are reference. Binder provided by container implements fluent API to connect the key to implementation, and to configure various binding options (the options, such as scope, are demonstrated later in this chapter). The simplest form of a key is a Java Class object representing service interface. Here is a module that bind [...]
+Now let's create a module implementing `org.apache.cayenne.di.Module` interface that will contain DI configuration. A module binds service objects to keys that are reference. Binder provided by container implements fluent API to connect the key to implementation, and to configure various binding options (the options, such as scope, are demonstrated later in this chapter). The simplest form of a key is a Java Class object representing service interface. Here is a module that binds Service [...]
 
 [source,java,indent=0]
 ----
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc
index d60e922f2..4ddd76134 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/expressions.adoc
@@ -20,15 +20,17 @@ Cayenne provides a simple, yet powerful, object-based expression language. The m
 
 Before discussing how to build expressions, it is important to understand one group of expressions widely used in Cayenne - path expressions. There are two types of path expressions - object and database, used for navigating graphs of connected objects or joined DB tables, respectively. Object paths are much more commonly used, as, after all, Cayenne is supposed to provide a degree of isolation of the object model from the database. However, database paths are helpful in certain situatio [...]
 
-[source, java]
+[source]
 ----
- [db:]segment[+][.segment[+]...]
+[db:]segment[+][#alias][.segment[+][#alias]...]
 ----
 
 - `db:` is an optional prefix indicating that the following path is a DB path. Otherwise it is an object path.
 
 - `segment` is a name of a property (relationship or attribute in Cayenne terms) in the path. The path must have at least one segment; segments are separated by dot (".").
 
+- `#alias` is an optional alias of a relationship segment, discussed below.
+
 - `+` An "OUTER JOIN" path component. Currently "+" only has effect when translated to SQL as OUTER JOIN. When evaluating expressions in memory, it is ignored.
 
 An object path expression represents a chain of property names rooted in a certain (unspecified during expression creation) object and "navigating" to its related value.
@@ -50,14 +52,13 @@ A database path therefore might look like this - `db:dbrelationshipX.dbrelations
 
 - `db:artist.artistExhibits.exhibit.CLOSING_DATE` - can be used to match a closing date of any of the exhibits of a related artist record.
 
-Cayenne supports "aliases" in path expressions. E.g. the same expression can be written using the explicit path or an alias:
+Cayenne supports "aliases" in path expressions. An alias is appended to a relationship segment after the `#` character:
 
-- `artist.exhibits.closingDate` - full path
+- `artist.exhibits.closingDate` - a path with no aliases
 
-- `e.closingDate` - alias "e" is used for `artist.exhibits`.
+- `artist.exhibits#e.closingDate` - alias "e" is used for `artist.exhibits`.
 
-SelectQuery using the second form of the path expression must be made aware of the alias via `SelectQuery.aliasPathSplits(..)`;
-otherwise, an `Exception` will be thrown. The main use of aliases is to allow users to control how SQL joins are generated if the same path is encountered more than once in any given `Expression`. Each alias for any given path would result in a separate join. Without aliases, a single join will be used for a group of matching paths.
+The main use of aliases is to allow users to control how SQL joins are generated if the same path is encountered more than once in any given `Expression`. Each alias for any given path would result in a separate join. Without aliases, a single join will be used for a group of matching paths.
 
 ==== Creating Expressions from Strings
 
@@ -73,12 +74,9 @@ This particular expression may be used to match Paintings whose names start with
 
 Character constants that are not paths or numeric values should be enclosed in single or double quotes. Two of the expressions below are equivalent:
 
-[source, java]
+[source,java,indent=0]
 ----
-name = 'ABC'
-
-// double quotes are escaped inside Java Strings of course
-name = \"ABC\"
+include::{java-examples}/ExpressionsTest.java[tags=quotes]
 ----
 
 Case sensitivity. Expression operators are case sensitive and are usually lowercase. Complex words follow the Java camel-case style:
@@ -163,12 +161,11 @@ include::{java-examples}/ExpressionsTest.java[tags=expressionFactory]
 
 NOTE: The last line in the example above shows how to create a new expression by "chaining" two other expressions. A common error when chaining expressions is to assume that "andExp" and "orExp" append another expression to the current expression. In fact, a new expression is created. Expression API treats existing expressions as immutable.
 
-As discussed earlier, Cayenne supports aliases in path Expressions, so you can control how SQL joins are generated if the same path is encountered more than once in the same Expression. Two ExpressionFactory methods let you implicitly generate aliases to "split" match paths into individual joins if needed:
+As discussed earlier, Cayenne supports aliases in path Expressions, so you can control how SQL joins are generated if the same path is encountered more than once in the same Expression. Two `ExpressionFactory.matchAllExp(..)` methods (one taking a `Collection` of values, another - a vararg) let you implicitly generate aliases to "split" match paths into individual joins if needed:
 
-[source, java]
+[source,java,indent=0]
 ----
-Expression matchAllExp(String path, Collection values)
-Expression matchAllExp(String path, Object... values)
+include::{java-examples}/ExpressionsTest.java[tags=matchAllExp]
 ----
 
 The "Path" argument to both of these methods can use a split character (a pipe symbol '|') instead of a dot to indicate that the relationship following a path should be split into a separate set of joins, one per collection value. There can only be one split at most in any given path. The split must always precede a relationship. E.g. `"|exhibits.paintings"`, `"exhibits|paintings"`, etc. Internally, Cayenne generates distinct aliases for each of the split expressions, forcing separate joins.
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc
index 1847b7fc4..9be499ad0 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/ejbql.adoc
@@ -99,9 +99,9 @@ Expression#appendAsEJBQL methods for this purpose.
 While Cayenne Expressions discussed previously can be thought of as identical to JPQL WHERE clause, and indeed they are
 very close, there are a few notable differences:
 
-- Null handling: SelectQuery would translate the expressions matching NULL values to the corresponding "X IS NULL" or
+- Null handling: ObjectSelect would translate the expressions matching NULL values to the corresponding "X IS NULL" or
 "X IS NOT NULL" SQL syntax. EJBQLQuery on the other hand requires explicit "IS NULL" (or "IS NOT NULL") syntax to be
 used, otherwise the generated SQL will look like "X = NULL" (or "X <> NULL"), which will evaluate differently.
 
-- Expression Parameters: SelectQuery uses "$" to denote named parameters (e.g. "$myParam"), while EJBQL uses ":"
+- Expression Parameters: ObjectSelect uses "$" to denote named parameters (e.g. "$myParam"), while EJBQL uses ":"
 (e.g. ":myParam"). Also EJBQL supports positional parameters denoted by the question mark: "?3".
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc
index 19f23fa74..e7d9ae4f5 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/queries/mapped.adoc
@@ -19,8 +19,6 @@ The actual stored query can be SelectQuery, SQLTemplate, EJBQLQuery, etc.
 Difference between `MappedSelect` and `MappedExec` is (as reflected in their names) whether underlying query
 intended to select data or just to perform some generic SQL code.
 
-NOTE: These queries are "fluent" versions of deprecated `NamedQuery` class.
-
 Here is example of how to use `MappedSelect`:
 
 [source,java,indent=0]
diff --git a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc
index 9cc0c01a7..00c497809 100644
--- a/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc
+++ b/docs/cayenne-guide/src/docs/asciidoc/_cayenne-guide/part2/starting.adoc
@@ -41,6 +41,33 @@ include::{java-examples}/StartingTest.java[tags=stop]
 
 When a runtime object has the same scope as the application, this may not be always necessary, however in some cases it is essential, and is generally considered a good practice. E.g. in a web container hot redeploy of a webapp will cause resource leaks and eventual OutOfMemoryError if the application fails to shutdown CayenneRuntime.
 
+==== Connecting to a Database
+
+The mapping project does not contain any database connection information. The database is defined in the code, when
+the runtime is created, as a DataNode. In the simplest case a DataNode is created from a `DataSource`. It can be any
+`DataSource`, e.g., the one provided by your application container, or the one built with `CayenneDataSource`:
+
+[source,java,indent=0]
+----
+include::{java-examples}/StartingTest.java[tags=dataSource]
+----
+
+When a DataNode needs to be customized, e.g., to let Cayenne create the DB schema on the first connection, to set
+a specific DbAdapter, or to give the node a name, use a `DataNodeDescriptor`:
+
+[source,java,indent=0]
+----
+include::{java-examples}/StartingTest.java[tags=dataNodeDescriptor]
+----
+
+There can be more than one DataNode. Each one is linked to its DataMaps by name. The "default" DataNode is optional,
+and is used for all the DataMaps that are not linked to a node explicitly:
+
+[source,java,indent=0]
+----
+include::{java-examples}/StartingTest.java[tags=multipleDataNodes]
+----
+
 ==== Merging Multiple Projects
 
 CayenneRuntime requires at least one mapping project to run. But it can also take multiple projects and merge them together in a single configuration. This way different parts of a database can be mapped independently from each other (even by different software providers), and combined in runtime when assembling an application. Doing it is as easy as passing multiple project locations to CayenneRuntime builder:
@@ -60,9 +87,7 @@ When the projects are merged, the following rules are applied:
 
 - If there are two or more DataMaps with the same name, only one DataMap is used in the merged project, the rest are discarded. Same precedence rules apply - DataMap from the project with the highest index in the project list overrides all other DataMaps with the same name.
 
-- If there are two or more DataNodes with the same name, only one DataNode is used in the merged project, the rest are discarded. DataNode coming from project with the highest index in the project list is chosen per precedence rule above.
-
-- There is a notion of "default" DataNode. After the merge if any DataMaps are not explicitly linked to DataNodes, their queries will be executed via a default DataNode. This makes it possible to build mapping "libraries" that are only associated with a specific database in runtime. If there's only one DataNode in the merged project, it will be automatically chosen as default. A possible way to explicitly designate a specific node as default is to override `DataDomainProvider.createAndIn [...]
+- DataNodes are not a part of the projects. After the merge the DataMaps are linked to the DataNodes as described above. If any DataMaps are not explicitly linked to DataNodes, their queries will be executed via the default DataNode. This makes it possible to build mapping "libraries" that are only associated with a specific database in runtime.
 
 ==== Web Applications
 
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java
index 7926ec2a6..76a9e0450 100644
--- a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ExpressionsTest.java
@@ -57,7 +57,8 @@ public class ExpressionsTest extends BaseTest {
      */
     @Test
     public void expStrings() {
-        List<String> valid = List.of("caseValid", "grouping", "objPath", "objPrefixPath", "multiSegmentPath", "dbPath", "like");
+        List<String> valid = List.of(
+                "caseValid", "grouping", "objPath", "objPrefixPath", "multiSegmentPath", "dbPath", "like");
         for (String tag : valid) {
             assertNotNull(ExpressionFactory.exp(example("expressions.txt", tag)), tag);
         }
@@ -68,6 +69,40 @@ public class ExpressionsTest extends BaseTest {
         assertThrows(ExpressionException.class, () -> ExpressionFactory.exp(example("expressions.txt", "caseInvalid")));
     }
 
+    @Test
+    public void quotes() {
+        // tag::quotes[]
+        Expression e1 = ExpressionFactory.exp("name = 'ABC'");
+
+        // double quotes are escaped inside Java Strings of course
+        Expression e2 = ExpressionFactory.exp("name = \"ABC\"");
+        // end::quotes[]
+
+        assertEquals(e1, e2);
+    }
+
+    @Test
+    public void aliases() {
+        createArtistsDataSet();
+
+        // each alias results in a separate join, so this matches the artists that have both paintings
+        Expression e = ExpressionFactory.exp(
+                "paintingArray#p1.paintingTitle = 'P1' and paintingArray#p2.paintingTitle = 'P2'");
+        assertEquals(1, ObjectSelect.query(Artist.class).where(e).select(context).size());
+    }
+
+    @Test
+    public void matchAllExp() {
+        createArtistsDataSet();
+
+        // tag::matchAllExp[]
+        // matches the artists that have both "P1" and "P2" paintings
+        Expression e = ExpressionFactory.matchAllExp("|paintingArray.paintingTitle", "P1", "P2");
+        // end::matchAllExp[]
+
+        assertEquals(1, ObjectSelect.query(Artist.class).where(e).select(context).size());
+    }
+
     @Test
     public void params() {
         // tag::params[]
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java
index 6a9edb11d..9daac9671 100644
--- a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/ProcedureCallExamples.java
@@ -54,8 +54,7 @@ public class ProcedureCallExamples {
 
     public void outParameters() {
         // tag::outParameters[]
-        List<QueryResult> result = ProcedureCall.query("my_procedure").call(context);
-        for (QueryResult item : result) {
+        for (QueryResult item : ProcedureCall.query("my_procedure").call(context)) {
             switch (item) {
                 case QueryResult.Select<?> select -> process(select.objects());
                 case QueryResult.Update update -> process(update.counts());
diff --git a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java
index e569d1db1..68a21e8b2 100644
--- a/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java
+++ b/docs/cayenne-guide/src/test/java/org/apache/cayenne/docs/StartingTest.java
@@ -19,11 +19,18 @@
 package org.apache.cayenne.docs;
 
 import org.apache.cayenne.configuration.Constants;
+import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.configuration.runtime.CoreModule;
+import org.apache.cayenne.datasource.CayenneDataSource;
 import org.apache.cayenne.di.Module;
+import org.apache.cayenne.docs.persistent.Artist;
+import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.runtime.CayenneRuntime;
 import org.junit.jupiter.api.Test;
 
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 
 public class StartingTest {
@@ -72,4 +79,61 @@ public class StartingTest {
         assertNotNull(runtime.getDataDomain());
         runtime.shutdown();
     }
+
+    @Test
+    public void dataSource() {
+        // tag::dataSource[]
+        DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:starting")
+                .userName("sa")
+                .password("")
+                .pool(1, 5)
+                .build();
+
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .defaultDataNode(dataSource)
+                .build();
+        // end::dataSource[]
+
+        assertEquals(1, runtime.getDataDomain().getDataNodes().size());
+        runtime.shutdown();
+    }
+
+    @Test
+    public void dataNodeDescriptor() {
+        DataSource dataSource = CayenneDataSource.of("jdbc:hsqldb:mem:starting").build();
+
+        // tag::dataNodeDescriptor[]
+        DataNodeDescriptor node = DataNodeDescriptor.of("node1")
+                .dataSource(dataSource)
+                .createSchemaIfNeeded()
+                .build();
+
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .defaultDataNode(node)
+                .build();
+        // end::dataNodeDescriptor[]
+
+        assertNotNull(runtime.getDataDomain().getDataNode("node1"));
+        assertEquals(0, ObjectSelect.query(Artist.class).selectCount(runtime.newContext()));
+        runtime.shutdown();
+    }
+
+    @Test
+    public void multipleDataNodes() {
+        DataSource ds1 = CayenneDataSource.of("jdbc:hsqldb:mem:starting1").build();
+        DataSource ds2 = CayenneDataSource.of("jdbc:hsqldb:mem:starting2").build();
+
+        // tag::multipleDataNodes[]
+        CayenneRuntime runtime = CayenneRuntime.of()
+                .addConfig("cayenne-project.xml")
+                .addDataNode(ds1, "docs")
+                .defaultDataNode(ds2)
+                .build();
+        // end::multipleDataNodes[]
+
+        assertEquals(2, runtime.getDataDomain().getDataNodes().size());
+        runtime.shutdown();
+    }
 }
diff --git a/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc b/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
index c867f4c43..b37d490ea 100644
--- a/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
+++ b/docs/getting-started-db-first/src/docs/asciidoc/_getting-started-db-first/part4-java-code.adoc
@@ -22,39 +22,14 @@ In this section we'll write a simple main class to run our application, and get
 +
 [source,java]
 ----
-package org.apache.cayenne.tutorial;
-
-public class Main {
-
-    public static void main(String[] args) {
-
-    }
-}
+include::{java-include-dir}/Main.java[tags=main-empty]
 ----
 * The first thing you need to be able to access the database is to create a `CayenneRuntime` object
 (which is essentially a wrapper around Cayenne stack) and use it to obtain an instance of an `ObjectContext`.
 +
 [source,java]
 ----
-package org.apache.cayenne.tutorial;
-
-import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.datasource.CayenneDataSource;
-import org.apache.cayenne.runtime.CayenneRuntime;
-
-public class Main {
-
-    public static void main(String[] args) {
-        CayenneRuntime cayenneRuntime = CayenneRuntime.of()
-            .defaultDataNode(CayenneDataSource
-                    .of("jdbc:mysql://127.0.0.1:3306/cayenne_demo")
-                    .userName("root") // TODO: change to your actual username and password
-                    .password("your-password").build())
-            .addConfig("cayenne-project.xml")
-            .build();
-        ObjectContext context = cayenneRuntime.newContext();
-    }
-}
+include::{java-include-dir}/Main.java[tags=main-runtime]
 ----
 + 
 `ObjectContext` is an isolated "session" in Cayenne that provides all needed API to work with data.
@@ -64,11 +39,9 @@ When the first ObjectContext is created in the application,
 Cayenne loads XML mapping files and creates a shared access stack that is later reused by other ObjectContexts.
 * Let's now add some code that will create persistent object: 
 +
-[source,java]
+[source,java,indent=0]
 ----
-Artist artist = context.newObject(Artist.class);
-artist.setName("Picasso");
-context.commitChanges();
+include::{java-include-dir}/Main.java[tags=new-artist]
 ----
 
 ==== Running Application
diff --git a/docs/getting-started-db-first/src/docs/asciidoc/getting-started-db-first.adoc b/docs/getting-started-db-first/src/docs/asciidoc/getting-started-db-first.adoc
index f9e527c68..1027a1e91 100644
--- a/docs/getting-started-db-first/src/docs/asciidoc/getting-started-db-first.adoc
+++ b/docs/getting-started-db-first/src/docs/asciidoc/getting-started-db-first.adoc
@@ -16,6 +16,8 @@
 // enable section numbering, limiting depth to 2
 :sectnums:
 :sectnumlevels: 2
+// base path to java code include
+:java-include-dir: {basedir}/src/main/java/org/apache/cayenne/tutorial
 // use custom header
 :hugo-header: _getting-started-db-first/header.html
 
diff --git a/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/Main.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/Main.java
new file mode 100644
index 000000000..c585f976f
--- /dev/null
+++ b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/Main.java
@@ -0,0 +1,59 @@
+/*****************************************************************
+ *   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.
+ ****************************************************************/
+// The parts of this class are included in the tutorial docs. "main-empty" and "main-runtime" tags assemble the versions
+// of this class at the different tutorial steps. The class is only compiled, but not run during the build, as it needs
+// a MySQL database.
+// tag::main-runtime[]
+// tag::main-empty[]
+package org.apache.cayenne.tutorial;
+
+// end::main-empty[]
+import org.apache.cayenne.ObjectContext;
+import org.apache.cayenne.datasource.CayenneDataSource;
+import org.apache.cayenne.runtime.CayenneRuntime;
+// end::main-runtime[]
+import org.apache.cayenne.tutorial.persistent.Artist;
+// tag::main-runtime[]
+
+// tag::main-empty[]
+public class Main {
+
+    public static void main(String[] args) {
+        // end::main-empty[]
+        CayenneRuntime cayenneRuntime = CayenneRuntime.of()
+                .defaultDataNode(CayenneDataSource
+                        .of("jdbc:mysql://127.0.0.1:3306/cayenne_demo")
+                        .userName("root") // TODO: change to your actual username and password
+                        .password("your-password").build())
+                .addConfig("cayenne-project.xml")
+                .build();
+        ObjectContext context = cayenneRuntime.newContext();
+        // end::main-runtime[]
+
+        // tag::new-artist[]
+        Artist artist = context.newObject(Artist.class);
+        artist.setName("Picasso");
+        context.commitChanges();
+        // end::new-artist[]
+        // tag::main-runtime[]
+        // tag::main-empty[]
+    }
+}
+// end::main-empty[]
+// end::main-runtime[]
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
similarity index 90%
copy from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
copy to docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
index 2553b0904..011f48698 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
+++ b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
@@ -18,8 +18,8 @@
  ****************************************************************/
 package org.apache.cayenne.tutorial.persistent;
 
-import org.apache.cayenne.tutorial.persistent.auto._Gallery;
+import org.apache.cayenne.tutorial.persistent.auto._Artist;
 
-public class Gallery extends _Gallery {
+public class Artist extends _Artist {
 
 }
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
similarity index 100%
copy from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
copy to docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
similarity index 100%
copy from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
copy to docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
similarity index 94%
copy from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
copy to docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
index 38f8f3569..9b93cc355 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
+++ b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
@@ -7,7 +7,9 @@ import org.apache.cayenne.PersistentObject;
 import org.apache.cayenne.exp.property.DateProperty;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.tutorial.persistent.Artist;
 import org.apache.cayenne.tutorial.persistent.Painting;
 
 /**
@@ -18,6 +20,8 @@ import org.apache.cayenne.tutorial.persistent.Painting;
  */
 public abstract class _Artist extends PersistentObject {
 
+    public static final SelfProperty<Artist> SELF = PropertyFactory.createSelf(Artist.class);
+
     public static final String ID_PK_COLUMN = "ID";
 
     public static final DateProperty<LocalDate> DATE_OF_BIRTH = PropertyFactory.createDate("dateOfBirth", LocalDate.class);
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
similarity index 92%
copy from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
copy to docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
index 5bd2f7f7a..e5f5f303c 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
+++ b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
@@ -5,7 +5,9 @@ import java.util.List;
 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.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
+import org.apache.cayenne.tutorial.persistent.Gallery;
 import org.apache.cayenne.tutorial.persistent.Painting;
 
 /**
@@ -16,6 +18,8 @@ import org.apache.cayenne.tutorial.persistent.Painting;
  */
 public abstract class _Gallery extends PersistentObject {
 
+    public static final SelfProperty<Gallery> SELF = PropertyFactory.createSelf(Gallery.class);
+
     public static final String ID_PK_COLUMN = "ID";
 
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
similarity index 85%
copy from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
copy to docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
index 73684c46e..744332ab2 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
+++ b/docs/getting-started-db-first/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
@@ -3,9 +3,11 @@ package org.apache.cayenne.tutorial.persistent.auto;
 import org.apache.cayenne.PersistentObject;
 import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
 import org.apache.cayenne.tutorial.persistent.Artist;
 import org.apache.cayenne.tutorial.persistent.Gallery;
+import org.apache.cayenne.tutorial.persistent.Painting;
 
 /**
  * Class _Painting was generated by Cayenne.
@@ -15,11 +17,13 @@ import org.apache.cayenne.tutorial.persistent.Gallery;
  */
 public abstract class _Painting extends PersistentObject {
 
+    public static final SelfProperty<Painting> SELF = PropertyFactory.createSelf(Painting.class);
+
     public static final String ID_PK_COLUMN = "ID";
 
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
-    public static final EntityProperty<org.apache.cayenne.tutorial.persistent.Artist> ARTIST = PropertyFactory.createEntity("artist", org.apache.cayenne.tutorial.persistent.Artist.class);
-    public static final EntityProperty<org.apache.cayenne.tutorial.persistent.Gallery> GALLERY = PropertyFactory.createEntity("gallery", org.apache.cayenne.tutorial.persistent.Gallery.class);
+    public static final EntityProperty<Artist> ARTIST = PropertyFactory.createEntity("artist", Artist.class);
+    public static final EntityProperty<Gallery> GALLERY = PropertyFactory.createEntity("gallery", Gallery.class);
 
     protected String name;
 
diff --git a/docs/getting-started-db-first/src/main/resources/cayenne-project.xml b/docs/getting-started-db-first/src/main/resources/cayenne-project.xml
new file mode 100644
index 000000000..55fbc529c
--- /dev/null
+++ b/docs/getting-started-db-first/src/main/resources/cayenne-project.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<domain xmlns="http://cayenne.apache.org/schema/13/domain"
+	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/domain http://cayenne.apache.org/schema/13/domain.xsd"
+	 project-version="13">
+	<map name="datamap"/>
+</domain>
diff --git a/docs/getting-started-guide/src/main/resources/datamap.map.xml b/docs/getting-started-db-first/src/main/resources/datamap.map.xml
similarity index 70%
copy from docs/getting-started-guide/src/main/resources/datamap.map.xml
copy to docs/getting-started-db-first/src/main/resources/datamap.map.xml
index 953e6f657..82ee77e0a 100644
--- a/docs/getting-started-guide/src/main/resources/datamap.map.xml
+++ b/docs/getting-started-db-first/src/main/resources/datamap.map.xml
@@ -4,41 +4,41 @@
 	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/modelMap http://cayenne.apache.org/schema/13/modelMap.xsd"
 	 project-version="13">
 	<property name="defaultPackage" value="org.apache.cayenne.tutorial.persistent"/>
-	<db-entity name="ARTIST">
+	<db-entity name="artist" catalog="cayenne_demo">
 		<db-attribute name="DATE_OF_BIRTH" type="DATE"/>
-		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isGenerated="true" isMandatory="true"/>
 		<db-attribute name="NAME" type="VARCHAR" length="200"/>
 	</db-entity>
-	<db-entity name="GALLERY">
-		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+	<db-entity name="gallery" catalog="cayenne_demo">
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isGenerated="true" isMandatory="true"/>
 		<db-attribute name="NAME" type="VARCHAR" length="200"/>
 	</db-entity>
-	<db-entity name="PAINTING">
+	<db-entity name="painting" catalog="cayenne_demo">
 		<db-attribute name="ARTIST_ID" type="INTEGER"/>
 		<db-attribute name="GALLERY_ID" type="INTEGER"/>
-		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
+		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isGenerated="true" isMandatory="true"/>
 		<db-attribute name="NAME" type="VARCHAR" length="200"/>
 	</db-entity>
-	<obj-entity name="Artist" className="org.apache.cayenne.tutorial.persistent.Artist" dbEntityName="ARTIST">
+	<obj-entity name="Artist" className="org.apache.cayenne.tutorial.persistent.Artist" dbEntityName="artist">
 		<obj-attribute name="dateOfBirth" type="java.time.LocalDate" db-attribute-path="DATE_OF_BIRTH"/>
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
-	<obj-entity name="Gallery" className="org.apache.cayenne.tutorial.persistent.Gallery" dbEntityName="GALLERY">
+	<obj-entity name="Gallery" className="org.apache.cayenne.tutorial.persistent.Gallery" dbEntityName="gallery">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
-	<obj-entity name="Painting" className="org.apache.cayenne.tutorial.persistent.Painting" dbEntityName="PAINTING">
+	<obj-entity name="Painting" className="org.apache.cayenne.tutorial.persistent.Painting" dbEntityName="painting">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
-	<db-relationship name="paintings" source="ARTIST" target="PAINTING" toMany="true">
+	<db-relationship name="paintings" source="artist" target="painting" toMany="true">
 		<db-attribute-pair source="ID" target="ARTIST_ID"/>
 	</db-relationship>
-	<db-relationship name="paintings" source="GALLERY" target="PAINTING" toMany="true">
+	<db-relationship name="paintings" source="gallery" target="painting" toMany="true">
 		<db-attribute-pair source="ID" target="GALLERY_ID"/>
 	</db-relationship>
-	<db-relationship name="artist" source="PAINTING" target="ARTIST">
+	<db-relationship name="artist" source="painting" target="artist">
 		<db-attribute-pair source="ARTIST_ID" target="ID"/>
 	</db-relationship>
-	<db-relationship name="gallery" source="PAINTING" target="GALLERY">
+	<db-relationship name="gallery" source="painting" target="gallery">
 		<db-attribute-pair source="GALLERY_ID" target="ID"/>
 	</db-relationship>
 	<obj-relationship name="paintings" source="Artist" target="Painting" deleteRule="Cascade" db-relationship-path="paintings"/>
diff --git a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/java-classes.adoc b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/java-classes.adoc
index daf83dd94..626167f60 100644
--- a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/java-classes.adoc
+++ b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/java-classes.adoc
@@ -13,10 +13,9 @@
 //    License.
 === Creating Java Classes
 Here we'll generate the Java classes from the model that was created in the previous
-section. CayenneModeler can be used to also generate the database schema, but since we
-specified "`CreateIfNoSchemaStrategy`" earlier when we created a DataNode, we'll skip the
-database schema step. Still be aware that you can do it if you need to via "Tools >
-Create Database Schema".
+section. CayenneModeler can be used to also generate the database schema, but since later in the
+tutorial we'll tell Cayenne to create the schema on startup, we'll skip this step. Still be aware
+that you can do it if you need to via "Tools > Create Database Schema".
 
 ==== Creating Java Classes
 - Select your datamap in a project tree and open "Class Generation" tab.
diff --git a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/object-context.adoc b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/object-context.adoc
index 4c1f030d9..5289d5f9a 100644
--- a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/object-context.adoc
+++ b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/object-context.adoc
@@ -22,34 +22,21 @@ introduction to Cayenne ObjectContext.
 
 [source,java]
 ----
-package org.example.cayenne;
-
-public class Main {
-
-    public static void main(String[] args) {
-    }
-}
+include::{java-include-dir}/Main.java[tags=main-empty]
 ----
 - The first thing you need to be able to access the database is to create a
 `CayenneRuntime` object (which is essentially a wrapper around Cayenne stack) and
-use it to obtain an instance of an `ObjectContext`.
+use it to obtain an instance of an `ObjectContext`. The runtime needs to know which database to connect to.
+The database connection is not a part of the mapping project. It is defined in the code as a `DataSource`.
+Here we are using an in-memory Derby database, so when you stop the application, all the data will be lost.
+In most real-life cases you'll be connecting to a database that actually persists its data on
+disk, but an in-memory DB will do for the simple tutorial. We are wrapping the `DataSource` in a
+`DataNodeDescriptor` with `createSchemaIfNeeded()`, so that Cayenne creates a new schema on Derby based on
+the ORM mapping when the application starts.
 
 [source,java]
 ----
-package org.example.cayenne;
-
-import org.apache.cayenne.ObjectContext;
-import org.apache.cayenne.runtime.CayenneRuntime;
-
-public class Main {
-
-    public static void main(String[] args) {
-        CayenneRuntime cayenneRuntime = CayenneRuntime.of()
-                        .addConfig("cayenne-project.xml")
-                        .build();
-        ObjectContext context = cayenneRuntime.newContext();
-    }
-}
+include::{java-include-dir}/Main.java[tags=main-runtime]
 ----
 `ObjectContext` is an isolated "session" in Cayenne that provides all needed API
 to work with data. ObjectContext has methods to execute queries and manage
@@ -84,10 +71,7 @@ In the console you'll see output similar to this, indicating that Cayenne stack
 
     INFO: Loading XML configuration resource from file:/.../cayenne-project.xml
     INFO: Loading XML DataMap resource from file:/.../datamap.map.xml
-    INFO: loading user name and password.
-    INFO: Connecting to 'jdbc:derby:memory:testdb;create=true' as 'null'
-    INFO: +++ Connecting: SUCCESS.
-    INFO: setting DataNode 'datanode' as default, used by all unlinked DataMaps</screen>
+    INFO: setting DataNode 'tutorial' as default, used by all unlinked DataMaps
             
 ==== How to Configure Cayenne Logging
 Follow the instructions in the logging chapter to tweak verbosity of the logging output.
\ No newline at end of file
diff --git a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/starting-project.adoc b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/starting-project.adoc
index 8fbd19866..4452aa15b 100644
--- a/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/starting-project.adoc
+++ b/docs/getting-started-guide/src/docs/asciidoc/_getting-started-guide/starting-project.adoc
@@ -16,7 +16,7 @@
 The goal of this chapter is to create a new Java project in IntelliJ IDEA
 containing a basic Cayenne mapping. It presents an introduction to
 CayenneModeler GUI tool, showing how to create the initial mapping
-objects: `DataDomain`, `DataNode`, `DataMap`.
+objects: `DataDomain` and `DataMap`.
 
 ==== Create a new Project in IntelliJ IDEA
 
@@ -53,42 +53,11 @@ that contains a single *DataDomain*. The meaning of a
 DataDomain is explained elsewhere in the User Guide. For now it is sufficient to
 understand that DataDomain is the root of your mapping project.
 
-==== Create a DataNode
-
-The next project object you will create is a *DataNode*.
-DataNode is a descriptor of a single database your application
-will connect to. Cayenne mapping project can use more than one database, but for now,
-we'll only use one. With "project" selected on the left, click on `Create DataNode` button image:icon-node.png[]
-on the toolbar (or select `Project > Create DataNode` from the menu).
-
-A new DataNode is displayed. Now you need to specify JDBC connection parameters. For
-an in-memory Derby database you can enter the following settings:
-
-- JDBC Driver: `org.apache.derby.jdbc.EmbeddedDriver`
-- DB URL: `jdbc:derby:memory:testdb;create=true`
-
-NOTE: We are creating an in-memory database here. So when
-      you stop your application, all the data will be lost. In most real-life
-      cases you'll be connecting to a database that actually persists its data on
-      disk, but an in-memory DB will do for the simple tutorial.
-
-Also you will need to change "Schema Update Strategy". Select
-`org.apache.cayenne.access.dbsync.CreateIfNoSchemaStrategy` from the dropdown, so that
-Cayenne creates a new schema on Derby based on the ORM mapping when the application
-starts.
-
-image::base-datanode.png[align="center"]
-
 ==== Create a DataMap
 
 Now you will create a *DataMap*. DataMap is an object
 that holds all the mapping information. To create it, click on "Create DataMap" button
 image:icon-datamap.png[] (or select a corresponding menu item).
-Note that the newly created DataMap is automatically linked to the DataNode that you created in
-the previous step. If there is more than one DataNode, you may need to link a DataMap
-to the correct node manually. In other words a DataMap within DataDomain must point
-to a database described by the map.
-
 You can leave all the DataMap defaults unchanged except for one - "Java Package".
 Enter `org.example.cayenne.persistent`. This name will later be used for all persistent
 classes.
diff --git a/docs/getting-started-guide/src/docs/asciidoc/getting-started-guide.adoc b/docs/getting-started-guide/src/docs/asciidoc/getting-started-guide.adoc
index cfedaf66f..b0c38e7a7 100644
--- a/docs/getting-started-guide/src/docs/asciidoc/getting-started-guide.adoc
+++ b/docs/getting-started-guide/src/docs/asciidoc/getting-started-guide.adoc
@@ -19,7 +19,7 @@
 // use custom header
 :hugo-header: _getting-started-guide/header.html
 // base path to java code include
-:java-include-dir: {basedir}/src/main/java/org/apache/cayenne/tutorial
+:java-include-dir: {basedir}/src/main/java/org/example/cayenne
 
 [small]#Copyright © 2011-{docyear} Apache Software Foundation and individual authors#
 
diff --git a/docs/getting-started-guide/src/docs/asciidoc/images/base-datanode.png b/docs/getting-started-guide/src/docs/asciidoc/images/base-datanode.png
deleted file mode 100644
index bb83b436c..000000000
Binary files a/docs/getting-started-guide/src/docs/asciidoc/images/base-datanode.png and /dev/null differ
diff --git a/docs/getting-started-guide/src/docs/asciidoc/images/icon-node.png b/docs/getting-started-guide/src/docs/asciidoc/images/icon-node.png
deleted file mode 100755
index fe0f402e3..000000000
Binary files a/docs/getting-started-guide/src/docs/asciidoc/images/icon-node.png and /dev/null differ
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/Main.java
similarity index 83%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/Main.java
index 3b462894a..baeefa469 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/Main.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/Main.java
@@ -16,30 +16,41 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.tutorial;
+// The parts of this class are included in the tutorial docs. "main-empty" and "main-runtime" tags assemble the versions
+// of this class at the different tutorial steps
+// tag::main-runtime[]
+// tag::main-empty[]
+package org.example.cayenne;
 
+// end::main-empty[]
 import org.apache.cayenne.ObjectContext;
 import org.apache.cayenne.configuration.DataNodeDescriptor;
 import org.apache.cayenne.datasource.CayenneDataSource;
-import org.apache.cayenne.query.ObjectSelect;
 import org.apache.cayenne.runtime.CayenneRuntime;
-import org.apache.cayenne.tutorial.persistent.Artist;
-import org.apache.cayenne.tutorial.persistent.Gallery;
-import org.apache.cayenne.tutorial.persistent.Painting;
 
 import javax.sql.DataSource;
+// end::main-runtime[]
+import org.apache.cayenne.query.ObjectSelect;
+import org.example.cayenne.persistent.Artist;
+import org.example.cayenne.persistent.Gallery;
+import org.example.cayenne.persistent.Painting;
+
 import java.time.LocalDate;
 import java.util.List;
+// tag::main-runtime[]
 
+// tag::main-empty[]
 public class Main {
 
     public static void main(String[] args) {
+        // end::main-empty[]
 
-        // starting Cayenne
+        // an in-memory Derby database
         DataSource dataSource = CayenneDataSource.of("jdbc:derby:memory:testdb;create=true")
                 .pool(1, 1)
                 .build();
 
+        // let Cayenne create the DB schema from the mapping, if it is not there yet
         DataNodeDescriptor dataNode = DataNodeDescriptor.of("tutorial")
                 .dataSource(dataSource)
                 .createSchemaIfNeeded()
@@ -50,13 +61,17 @@ public class Main {
                 .defaultDataNode(dataNode)
                 .build();
 
-        // getting a hold of ObjectContext
         ObjectContext context = cayenneRuntime.newContext();
+        // end::main-runtime[]
 
         newObjectsTutorial(context);
         selectTutorial(context);
         deleteTutorial(context);
+        // tag::main-runtime[]
+        // tag::main-empty[]
     }
+    // end::main-empty[]
+    // end::main-runtime[]
 
     static void newObjectsTutorial(ObjectContext context) {
 
@@ -123,4 +138,8 @@ public class Main {
         }
         // end::delete[]
     }
+// tag::main-runtime[]
+// tag::main-empty[]
 }
+// end::main-empty[]
+// end::main-runtime[]
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Artist.java
similarity index 94%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Artist.java
index b6be4cc34..9c04c09eb 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Artist.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Artist.java
@@ -18,13 +18,13 @@
  ****************************************************************/
 // This code used in docs too, so it should be formatted
 // to 80 char line to fit in PDF.
-package org.apache.cayenne.tutorial.persistent;
+package org.example.cayenne.persistent;
 
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
 
-import org.apache.cayenne.tutorial.persistent.auto._Artist;
+import org.example.cayenne.persistent.auto._Artist;
 
 // tag::content[]
 public class Artist extends _Artist {
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Datamap.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Datamap.java
similarity index 91%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Datamap.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Datamap.java
index ddf2ddf45..8fcced4a3 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Datamap.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Datamap.java
@@ -16,9 +16,9 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.tutorial.persistent;
+package org.example.cayenne.persistent;
 
-import org.apache.cayenne.tutorial.persistent.auto._Datamap;
+import org.example.cayenne.persistent.auto._Datamap;
 
 public class Datamap extends _Datamap {
 
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Gallery.java
similarity index 90%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Gallery.java
index 2553b0904..b75e197c0 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Gallery.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Gallery.java
@@ -16,9 +16,9 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.tutorial.persistent;
+package org.example.cayenne.persistent;
 
-import org.apache.cayenne.tutorial.persistent.auto._Gallery;
+import org.example.cayenne.persistent.auto._Gallery;
 
 public class Gallery extends _Gallery {
 
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Painting.java
similarity index 90%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Painting.java
index f1364b0b0..a9e79eaed 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/Painting.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/Painting.java
@@ -16,9 +16,9 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.tutorial.persistent;
+package org.example.cayenne.persistent;
 
-import org.apache.cayenne.tutorial.persistent.auto._Painting;
+import org.example.cayenne.persistent.auto._Painting;
 
 public class Painting extends _Painting {
 
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Artist.java
similarity index 91%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Artist.java
index 38f8f3569..072cd796e 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Artist.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Artist.java
@@ -1,4 +1,4 @@
-package org.apache.cayenne.tutorial.persistent.auto;
+package org.example.cayenne.persistent.auto;
 
 import java.time.LocalDate;
 import java.util.List;
@@ -7,8 +7,10 @@ import org.apache.cayenne.PersistentObject;
 import org.apache.cayenne.exp.property.DateProperty;
 import org.apache.cayenne.exp.property.ListProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
-import org.apache.cayenne.tutorial.persistent.Painting;
+import org.example.cayenne.persistent.Artist;
+import org.example.cayenne.persistent.Painting;
 
 /**
  * Class _Artist was generated by Cayenne.
@@ -18,6 +20,8 @@ import org.apache.cayenne.tutorial.persistent.Painting;
  */
 public abstract class _Artist extends PersistentObject {
 
+    public static final SelfProperty<Artist> SELF = PropertyFactory.createSelf(Artist.class);
+
     public static final String ID_PK_COLUMN = "ID";
 
     public static final DateProperty<LocalDate> DATE_OF_BIRTH = PropertyFactory.createDate("dateOfBirth", LocalDate.class);
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Datamap.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Datamap.java
similarity index 83%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Datamap.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Datamap.java
index f947cc113..9da9c6194 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Datamap.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Datamap.java
@@ -1,4 +1,4 @@
-package org.apache.cayenne.tutorial.persistent.auto;
+package org.example.cayenne.persistent.auto;
 
 /**
  * This class was generated by Cayenne.
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Gallery.java
similarity index 89%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Gallery.java
index 5bd2f7f7a..dd68137cf 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Gallery.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Gallery.java
@@ -1,12 +1,14 @@
-package org.apache.cayenne.tutorial.persistent.auto;
+package org.example.cayenne.persistent.auto;
 
 import java.util.List;
 
 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.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
-import org.apache.cayenne.tutorial.persistent.Painting;
+import org.example.cayenne.persistent.Gallery;
+import org.example.cayenne.persistent.Painting;
 
 /**
  * Class _Gallery was generated by Cayenne.
@@ -16,6 +18,8 @@ import org.apache.cayenne.tutorial.persistent.Painting;
  */
 public abstract class _Gallery extends PersistentObject {
 
+    public static final SelfProperty<Gallery> SELF = PropertyFactory.createSelf(Gallery.class);
+
     public static final String ID_PK_COLUMN = "ID";
 
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
diff --git a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Painting.java
similarity index 81%
rename from docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
rename to docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Painting.java
index 73684c46e..c47db5bc0 100644
--- a/docs/getting-started-guide/src/main/java/org/apache/cayenne/tutorial/persistent/auto/_Painting.java
+++ b/docs/getting-started-guide/src/main/java/org/example/cayenne/persistent/auto/_Painting.java
@@ -1,11 +1,13 @@
-package org.apache.cayenne.tutorial.persistent.auto;
+package org.example.cayenne.persistent.auto;
 
 import org.apache.cayenne.PersistentObject;
 import org.apache.cayenne.exp.property.EntityProperty;
 import org.apache.cayenne.exp.property.PropertyFactory;
+import org.apache.cayenne.exp.property.SelfProperty;
 import org.apache.cayenne.exp.property.StringProperty;
-import org.apache.cayenne.tutorial.persistent.Artist;
-import org.apache.cayenne.tutorial.persistent.Gallery;
+import org.example.cayenne.persistent.Artist;
+import org.example.cayenne.persistent.Gallery;
+import org.example.cayenne.persistent.Painting;
 
 /**
  * Class _Painting was generated by Cayenne.
@@ -15,11 +17,13 @@ import org.apache.cayenne.tutorial.persistent.Gallery;
  */
 public abstract class _Painting extends PersistentObject {
 
+    public static final SelfProperty<Painting> SELF = PropertyFactory.createSelf(Painting.class);
+
     public static final String ID_PK_COLUMN = "ID";
 
     public static final StringProperty<String> NAME = PropertyFactory.createString("name", String.class);
-    public static final EntityProperty<org.apache.cayenne.tutorial.persistent.Artist> ARTIST = PropertyFactory.createEntity("artist", org.apache.cayenne.tutorial.persistent.Artist.class);
-    public static final EntityProperty<org.apache.cayenne.tutorial.persistent.Gallery> GALLERY = PropertyFactory.createEntity("gallery", org.apache.cayenne.tutorial.persistent.Gallery.class);
+    public static final EntityProperty<Artist> ARTIST = PropertyFactory.createEntity("artist", Artist.class);
+    public static final EntityProperty<Gallery> GALLERY = PropertyFactory.createEntity("gallery", Gallery.class);
 
     protected String name;
 
diff --git a/docs/getting-started-guide/src/main/resources/datamap.map.xml b/docs/getting-started-guide/src/main/resources/datamap.map.xml
index 953e6f657..62092c11f 100644
--- a/docs/getting-started-guide/src/main/resources/datamap.map.xml
+++ b/docs/getting-started-guide/src/main/resources/datamap.map.xml
@@ -3,7 +3,7 @@
 	 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 	 xsi:schemaLocation="http://cayenne.apache.org/schema/13/modelMap http://cayenne.apache.org/schema/13/modelMap.xsd"
 	 project-version="13">
-	<property name="defaultPackage" value="org.apache.cayenne.tutorial.persistent"/>
+	<property name="defaultPackage" value="org.example.cayenne.persistent"/>
 	<db-entity name="ARTIST">
 		<db-attribute name="DATE_OF_BIRTH" type="DATE"/>
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
@@ -19,14 +19,14 @@
 		<db-attribute name="ID" type="INTEGER" isPrimaryKey="true" isMandatory="true"/>
 		<db-attribute name="NAME" type="VARCHAR" length="200"/>
 	</db-entity>
-	<obj-entity name="Artist" className="org.apache.cayenne.tutorial.persistent.Artist" dbEntityName="ARTIST">
+	<obj-entity name="Artist" className="org.example.cayenne.persistent.Artist" dbEntityName="ARTIST">
 		<obj-attribute name="dateOfBirth" type="java.time.LocalDate" db-attribute-path="DATE_OF_BIRTH"/>
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
-	<obj-entity name="Gallery" className="org.apache.cayenne.tutorial.persistent.Gallery" dbEntityName="GALLERY">
+	<obj-entity name="Gallery" className="org.example.cayenne.persistent.Gallery" dbEntityName="GALLERY">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
-	<obj-entity name="Painting" className="org.apache.cayenne.tutorial.persistent.Painting" dbEntityName="PAINTING">
+	<obj-entity name="Painting" className="org.example.cayenne.persistent.Painting" dbEntityName="PAINTING">
 		<obj-attribute name="name" type="java.lang.String" db-attribute-path="NAME"/>
 	</obj-entity>
 	<db-relationship name="paintings" source="ARTIST" target="PAINTING" toMany="true">
diff --git a/docs/getting-started-guide/src/test/java/org/apache/cayenne/tutorial/MainTest.java b/docs/getting-started-guide/src/test/java/org/example/cayenne/MainTest.java
similarity index 96%
rename from docs/getting-started-guide/src/test/java/org/apache/cayenne/tutorial/MainTest.java
rename to docs/getting-started-guide/src/test/java/org/example/cayenne/MainTest.java
index 394d7b30b..f3da86db2 100644
--- a/docs/getting-started-guide/src/test/java/org/apache/cayenne/tutorial/MainTest.java
+++ b/docs/getting-started-guide/src/test/java/org/example/cayenne/MainTest.java
@@ -16,7 +16,7 @@
  *  specific language governing permissions and limitations
  *  under the License.
  ****************************************************************/
-package org.apache.cayenne.tutorial;
+package org.example.cayenne;
 
 import org.junit.jupiter.api.Test;