TurtleSQL is a modular Java ORM that separates entity mapping, the SQL AST, dialect rendering, and database drivers.
The repository ships an executable reflective persistence path for SQLite through JDBC, plus initial PostgreSQL SQL
rendering, with schema policies, deterministic migrations, observability, generated metadata, and explicit extension
points for additional dialects.
The supported surface and intentionally deferred capabilities are documented in SPECS.md and tracked in
ROADMAP.md.
The generated API documentation is published from the main branch through the repository's GitHub Pages workflow.
The companion documentation site covers setup, migrations, dialects, and driver authoring.
The current API stability policy documents the pre-1.0 compatibility boundary and release gates.
Every Java module checks its reviewed public API snapshot from api-baseline/; intentional API changes require an API
review followed by ./gradlew updateApiBaseline.
The development history is tracked in CHANGELOG.md.
The tested path includes:
- Mapping annotations and reflective metadata validation.
- Immutable table, identifier, and logical-to-physical column mapping values are available from resolved metadata, including physical column expansion for multi-column codecs.
@Table(schema = ..., catalog = ...)qualified mappings are used by repository SQL, foreign-key DDL, table creation, and index creation; schema introspection and migration comparison remain dialect-specific.@ManyToOnemappings store related identifiers and materialize the related entity on reads; reflective@OneToMany(mappedBy = "...")fields provide deferred inverse collections, and@ManyToManyfields load through generated or explicitly named join tables.@OneToOnemappings reuse the foreign-key relation path and enforce a unique join column during schema creation.DRef<T>provides an explicit cached loader withisLoaded,load,get,set, andclear; it integrates with@ManyToOneand@OneToOnerelation fields in the reflective mapper.DCollection<T>provides the matching cached collection wrapper withisLoaded,load,get,add,remove,clear, and localqueryoperations; reflective@OneToManyand@ManyToManyfields load their target rows on demand.FetchPlan<T>resolves dot-separated relation paths after an entity query, including nestedDRefandDCollectionpaths; root and nested collection paths batch by owner key, while relation hops that still load once per owner emit a structured N+1 warning.ManyToOneschema creation emits portable foreign-key clauses for SQLite, PostgreSQL, MySQL, and MariaDB; all four introspectors read foreign keys and their referential actions into the schema snapshot.@ManyToOneand@OneToOneexposeonDeleteandonUpdatereferential actions, which flow into portable DDL and migration rendering.@Embeddedflattens value objects into the owning table, including accumulated prefixes, nested values, explicit@Columnnames, nullable propagation, and reflective CRUD materialization.@EmbeddedIdmaps composite identifiers to flattened primary-key columns and supports reflective CRUD lookups;@CompositeIdis provided as a compatibility alias; many-to-many relations can target them with explicitinverseJoinColumns, and to-one relations can use explicitjoinColumns/referencedColumns.@ManyToManyschema creation emits a join table with two foreign keys and a unique pair index; composite targets use one inverse join column per identifier field; loaded collections synchronize link rows during owner insert/save, while new target entities still require explicit cascade settings.DField<T>with unset, explicit-null, database-default, loaded, dirty, and pending-operation states.- A dialect-neutral query AST and fluent
SelectQuerybuilder. - Query operators for comparisons, inclusive ranges,
IN/NOT IN, null checks, and prefix/suffix/substring matching. - PostgreSQL's case-insensitive
ilike(...)andnotIlike(...)operators are capability-gated; SQLite and MySQL-family dialects reject them during rendering instead of silently changing semantics. - Literal prefix, suffix, and substring operators escape SQL
LIKEwildcards and emit a portableESCAPEclause;like(...)remains available for explicit SQL patterns. - SQLite SQL rendering, capability checks, conflict handling, joins, returning, limits, and schema creation.
- Initial PostgreSQL SQL rendering with PostgreSQL quoting,
$1parameters, native scalar types, identity columns,RETURNING,ON CONFLICT, and supported joins/locking reads. - Initial MySQL 8 SQL rendering with backtick quoting, JDBC parameters, auto-increment columns, JSON, duplicate-key upserts, supported joins and locking reads.
- MySQL JDBC integration smoke tests cover driver conformance, current-database schema introspection, generated keys,
repository reads, and atomic updates with
./gradlew :turtle-dialect-mysql:test :turtle-core:test -Pdatabases=mysql. - MariaDB 11.4 has a distinct dialect reusing the MySQL-family renderer, maps logical JSON to MariaDB's
LONGTEXTstorage, preserves the connected server version throughatVersion(...), and has an opt-in JDBC driver conformance test:./gradlew :turtle-dialect-mariadb:test -Pdatabases=mariadb. PostgreSqlSchemaIntrospectorreads the current PostgreSQL schema into the portable snapshot model; its real-driver smoke test runs with./gradlew :turtle-dialect-postgresql:test -Pdatabases=postgresql.- The reflective ORM path has a PostgreSQL JDBC smoke test covering schema creation, identity keys, entity reads, and
atomic
DFieldupdates:./gradlew :turtle-core:test -Pdatabases=postgresql. - Schema initialization for mapped columns plus field and type-level SQLite indexes.
- Schema diffs replace an index deterministically when its name remains stable but columns or uniqueness change; the replacement is destructive and therefore policy-controlled.
- Table-level
@Checkexpressions are emitted in SQLite DDL. - Loaded plain fields are snapshot-tracked, so changing them and calling
save()emits an update; unchanged entities issue no SQL. - The optional processor generates
<Entity>Meta,<Entity>Table, and<Entity>Fieldsclasses with table references and typed query fields (StringField,NumericField,BooleanField, temporal, enum, and JSON fields). - Generated
<Entity>Tableclasses exposeinnerJoinRelation()andleftJoinRelation()shortcuts for scalar@ManyToOneand@OneToOnemappings, preserving@Tableschema/catalog qualification; arbitrarySelectQueryjoins remain available. - The processor rejects missing identifiers and duplicate mapped columns at compile time with source-attached diagnostics.
@UseCodecselects and caches a field-specificScalarCodec, taking precedence over the database-wide registry.@UseCodecmay also select aMultiColumnCodec<T>; its orderedCodecColumndescriptors expand one mapped field into physical<base>_<suffix>columns and participate in schema creation, reads, inserts, updates, snapshots, and dirty tracking. Multi-column codecs are intentionally not valid for identifiers, relations, indexes, versions, or atomicDFieldexpressions.Database.Builder.metadataMode(...)selects reflective metadata, required generated metadata, or generated metadata with reflective fallback.- Generated metadata provides direct accessors for visible fields and
DFieldfactories; private or otherwise inaccessible members retain the explicit reflective fallback. - When an entity has an accessible no-argument constructor, its generated accessor also creates instances, avoiding reflective constructor lookup in the generated path.
database.openSession()provides an identity map so repeated lookups of the same entity id return the same instance.Session.flush()persists changes made to all entities currently managed by that session. Sessions supportFlushMode.MANUAL,AUTO(before reads), andCOMMIT(before the surrounding transaction commits), withCOMMITas the default.QueryOptionsprovides immutable entity-query filters, ordering, distinct reads, bounds, and locking-read requests.Repository.findPage(...)provides offset pagination with a separate total-count query throughPageRequestandPage.Repository.findKeysetPage(...)provides bidirectional cursor pagination over mapped column orderings without offset scans and appends identifier tie-breakers when the requested ordering is not unique.- Repositories can return scalar values, raw tuples, and reflective constructor/record projections with
findValues(...),findTuples(...), andselectInto(...). - Complete
SelectQueryASTs can execute as tuple rows or constructor/record projections while preserving joins, grouping, ordering, bounds, distinctness, and locking flags. Repository.stream(...)exposes a forward-only cursor-backed stream that closes its driver resources when exhausted or closed.AsyncDatabaseDriverandAsyncDriverConnectionexpose completion-stage preparation, execution, cursors, and transaction operations;AsyncDriverAdapters.blocking(...)bridges synchronous drivers through an executor.AsyncTransactions.inTransaction(...)composes asynchronous begin/commit/rollback with deterministic failure cleanup; native async sessions remain a separate integration concern.- Optional
turtle-driver-r2dbcadapts an application-owned R2DBCConnectionFactoryto the async driver SPI, including positional binding, updates, generated keys, transactions, savepoints, and demand-driven async cursors. - Optional
turtle-driver-remoteprovides a reference Java-serialization socket driver/server pair for synchronous queries, updates, batches, transactions, savepoints, and generated keys. It is intended for trusted private networks; authentication, TLS, and wire-compatible streaming are not included. AsyncRepositoryandAsyncQueryexpose the repository and fluent select surface throughCompletionStage, with executor-backed blocking adaptation.Repository.insertAll(...)batches compatible reflective inserts while preserving lifecycle hooks, generated identifiers, entity state, and a portable sequential fallback.Repository.attach(...)anddetach(...)manage clean snapshots and entity lifecycle state without forcing a reload; the async facade exposes both operations as completion stages.Repository.refresh(...)reloads an entity in place through the synchronous, session-bound, or completion-stage API.Repository.saveAll(...)preserves insert-or-update semantics, batching all-new collections and safeALL_COLUMNSupdates for existing entities without versioning or atomic expressions.Repository.update(...)performs update-only persistence and rejects new entities instead of silently inserting them;AsyncRepository.update(...)exposes the same contract throughCompletionStage.DriverConnection.executeBatch(...)provides portable batch fallback; the JDBC adapter uses native JDBC batching for compatible SQL and preserves per-command update results and aligned generated keys.- Repositories expose filtered
count,sum,average,minimum,maximum, anddistinctCountaggregate operations. Repository.findOne()returns empty for no rows, the unique entity for one row, and raisesNonUniqueResultExceptionwhen multiple rows match.BulkUpdateandBulkDeleteexecute parameterized set, default, column-copy, arithmetic, string, and filtered mutations; sessions clear their identity map after bulk changes.Database.eventBus()publishes structuredENTITY_INSERTED,ENTITY_UPDATED, andENTITY_DELETEDevents for repository mutations, including bulk operations.- Session bulk mutations also publish
IDENTITY_MAP_INVALIDATEDwith the affected table, row count, and reason after clearing managed instances. - The connection wrapper also publishes prepared/executed/failed query events with duration and row metrics, transaction lifecycle events, and connection acquisition/release events; listeners never receive bound parameter values.
- The same event bus publishes
MIGRATION_PLANNEDandMIGRATION_EXECUTEDevents with operation count, destructive flag, version when available, and duration metrics. database.rawQuery(...)anddatabase.rawExecute(...)support explicitly non-portable SQL while keeping values separately bound.RawPredicateandSelectQuery.whereRaw(...)/havingRaw(...)provide the same separately bound escape hatch inside query predicates; fragments use?placeholders and remain outside portability guarantees.SqliteSchemaIntrospectorreads SQLite tables, columns, indexes, and foreign keys into immutableSchemaSnapshotvalues;Database.planSchemaMigration(...)produces a deterministic migration plan for supported differences. Check definitions are preserved in desired metadata and rebuilt when they change.- Type-level
@Index(where = "...")declares partial indexes; PostgreSQL and SQLite render and introspect their predicates, while MySQL-family dialects reject them explicitly. @RenamedFrompreserves table and column data during SQLite schema evolution by producing explicit rename operations instead of drop-and-add changes.Database.validateSchema(...)reports all supported table, column, type, nullability, key, and default mismatches with destructive flags.SqliteTableRebuildercan replace a table definition while copying shared data and adding nullable columns; callers own the surrounding transaction.SqliteSchemaPlanneris discovered through the schema-planner SPI and automatically turns incompatible SQLite column, check, and foreign-key changes into a deterministic rebuild migration sequence.SchemaPolicycontrolsinitializeSchema()withNONE,VALIDATE,CREATE,CREATE_IF_MISSING,MIGRATE,CREATE_OR_MIGRATE, andDROP_AND_CREATE; validating policies accept a dialect-specific schema introspector through the builder.DestructivePolicysupportsDENY,WARN,ALLOW, andALLOW_WITH_BACKUP; warning events are emitted before destructive execution, and backup mode requires an explicitMigrationBackupcallback.@Versionenables optimistic locking: stale saves fail withOptimisticLockExceptioninstead of overwriting newer data.@SoftDeleteturns repository deletes into marker updates; boolean markers and non-null timestamp markers are supported, while normal reads hide marked rows andwithDeleted(),onlyDeleted(),restoreById(), andhardDeleteById()provide explicit lifecycle control.@Discriminatorsupports reflective single-table subtype repositories: registered subtypes share their inherited table, persist a discriminator value automatically, and reads/mutations remain scoped to the subtype. A repository requested for a registered base type aggregates its discriminator subtypes for common CRUD and aggregate operations.@CreatedAtand@UpdatedAtpopulate supported Java timestamp fields during repository inserts and updates.- Registered global or repository-specific
LifecycleHooklisteners,LifecycleAwareentities, and@OnLifecyclemethods receive the eleven persistence callbacks with database, optional active session, repository, entity, operation, changed-field, and transaction context throughHookContext. - Java-side validation supports
@NotNull,@Length,@Range,@Matches, and custom@ValidateWithrules before SQL execution; schema constraints remain authoritative and independent. - Optional
turtle-validation-jakartaadapts a JakartaValidatorthroughJakartaValidation.hook(validator), preserving the core's dependency-free validation boundary and translating bean violations toValidationException. - Entities may extend
Entity<ID>for lifecycle-awaresave(),delete(),refresh(), dirty-field inspection, and explicit detachment; invalid operations on detached or removed instances raiseDetachedEntityException. - Repository saves default to
DIRTY_ONLYand also supportALL_COLUMNS,NON_NULL, andEXPLICIT_FIELDSthroughSaveMode. - Atomic
DFieldexpressions useAtomicRefresh.RETURNING_WHEN_SUPPORTEDby default, withNONE,ALWAYS_REFRESH, andESTIMATE_FROM_LOCAL_VALUEavailable through the database builder; unknown results remain unloaded instead of being reported asnull. turtle-migrationsprovides immutable migration plans, versioned definitions, deterministic descriptions, and destructive-operation policies.- Optional
turtle-log-slf4jprovidesSlf4jLogSink, mapping TurtleSQL categories and levels to SLF4J without adding a logging implementation to the core. turtle-driverprovides explicit andServiceLoader-based driver registration; the JDBC adapter is discoverable asjdbc.ConnectionPoolprovides bounded, blocking reuse with configurable acquisition timeout and transaction-state reset.ConnectionSource.url(...)handles JDBC-style credentials, whileConnectionSource.supplier(...)lets custom drivers consume preconfigured connections.- The JDBC adapter reuses prepared statements per connection with a bounded
StatementCacheOptionscache and exposes hit/miss metrics throughDriverConnection.statementCacheMetrics(). DriverConformanceSuiteexercises DDL, CRUD, cursors, transactions, and cleanup against a real driver/dialect pair.DialectConformanceSuitecovers the SQLite, PostgreSQL, MySQL, and MariaDB renderer contracts;SchemaRoundTripSuiteprovides reusable create/introspect/drop verification, with SQLite enabled by default and container dialects opt-in.- The core portability test runs the same generated-key, insert, read, update, and delete entity flow on all four
dialects; SQLite runs by default and the other backends use
-Pdatabases=postgresql,mysql,mariadbwhen containers are available. - Generated schema constraint names are deterministic, readable when short, and truncated with a stable SHA-256 suffix when needed.
database.applyMigration(plan, policy)renders and applies plan statements atomically through the configured dialect and driver.database.dryRunMigration(plan)renders migration SQL and parameters without opening a transaction or executing commands;plan.describe()provides the deterministic human-readable summary.- Versioned migrations are checksum-checked and idempotent through
_turtle_migrations; applying the same version with changed content fails. database.rollbackMigration(migration, policy)executes the versioneddownplan and removes its history entry after success.- Versioned migration history records checksum, applied timestamp, execution duration, dialect, and successful application status.
- Existing
_turtle_migrationstables are evolved incrementally when the driver exposes JDBC table-column metadata, so older history tables can adopt the current metadata columns without being dropped. - JDBC connections report the server version at connect time; SQLite adjusts its
RETURNINGcapability below 3.35 and PostgreSQL below 8.2. - Dialects expose normalized reserved-word sets through
SqlDialect.isReservedWord(...); identifiers remain quoted by each dialect's own rules. - Dialects expose immutable
TypeRegistryandFunctionRegistryinstances throughSqlDialect; schema renderers use the registered native type mappings and function aliases while unknown custom functions remain available. - The generated dialect capability matrix records feature negotiation directly from the
shipped dialect implementations; regenerate it with
./gradlew :turtle-testkit:generateCapabilityMatrix. - SQLite migration plans include portable
CreateTable,AddColumn,DropColumn,AddCheck,DropCheck,AddForeignKey,DropForeignKey,RenameTable,RenameColumn,CreateIndex,DropIndex, andDropTableoperations; constraint alterations are applied through table rebuilds. - A driver SPI, JDBC adapter, transactions, savepoints, generated keys, and portable error translation.
- Reflective repositories for insert, save, find, count, existence checks, and delete.
- Built-in scalar codecs for JDK scalar and time types, UUID, decimal, and byte arrays.
- Codec operator capabilities reject unsupported
LIKE, ordering, equality, and membership predicates before SQL execution; custom codecs may override the capability policy. - Codec storage fingerprints are recorded in
_turtle_codec_versionsduring schema initialization; changing a persisted codec contract fails before application data is accessed.
Current release boundaries: SQLite is the supplied executable dialect; PostgreSQL and MySQL have renderer, catalog
introspection, and opt-in JDBC smoke-test baselines;
repositories remain reflective at runtime, collection relations are supported through reflective inverse and join-table
loaders, generated projection metadata, advanced bulk helpers, inaccessible nested accessors, and Active Record entities
remain roadmap work. A Database uses one connection at a time unless configured with ConnectionPool.
The project is not published yet. Build it with Java 17 or newer:
./gradlew checkAn application using the current SQLite path needs turtle-core, turtle-dialect-sqlite, turtle-driver-jdbc, and an
SQLite JDBC driver. PostgreSQL, MySQL, and MariaDB applications additionally use their dialect module and vendor JDBC driver.
Applications using Jakarta Bean Validation may additionally depend on turtle-validation-jakarta and register its hook
with the database builder.
Applications using R2DBC may additionally depend on turtle-driver-r2dbc; the R2DBC ConnectionFactory owns URL and
credential configuration.
Applications using the reference remote driver may additionally depend on turtle-driver-remote; start a
RemoteDriverServer over an application-owned backend and connect with ConnectionSource.url("remote://host:port").
Integration URLs and credentials accept matching Gradle property overrides, such as
-Pturtle.mysql.url=jdbc:mysql://localhost:53306/testpilot, so a complete matrix can run on non-default ports.
The repository keeps vendor drivers as application choices; its tests use SQLite JDBC 3.46.1.0, PostgreSQL JDBC
42.7.5, MySQL Connector/J 9.0.0, and MariaDB Connector/J 3.5.3 only for test/runtime verification. PostgreSQL,
MySQL, and MariaDB integration tests expect a local server or container and are opt-in through -Pdatabases=postgresql,
-Pdatabases=mysql, or -Pdatabases=mariadb.
@Table("users")
public class User {
@Id
@GeneratedValue
public Long id;
@Column(nullable = false)
public String name;
public DField<Integer> age;
}try (Database database = Database.builder()
.dialect(new SqliteDialect())
.driver(new JdbcDriver())
.connection(ConnectionSource.url("jdbc:sqlite:app.db"))
.registerEntities(User.class)
.build()) {
database.initializeSchema();
Repository<User, Long> users = database.getRepository(User.class);
User user = users.newEntity();
user.name = "Grace";
user.age.set(37);
users.insert(user);
user.age.add(1);
users.save(user);
User loaded = users.findById(user.id).orElseThrow();
}Database owns one driver connection, so concurrent access must be serialized by the application. Use
database.transaction(db -> { ... }) when multiple repository operations must commit or roll back together.
Nested calls to transaction use driver savepoints and can be caught without aborting the outer transaction.
TransactionOptions.timeoutSeconds applies to JDBC statements in the transaction. Opt-in
TransactionRetryPolicy retries configured transient SQLSTATE failures on outer transactions; nested savepoints are
never retried automatically.
The reflective reader recognizes @Table, @Column, @Id, @GeneratedValue, @Transient, @Unique, @Index,
@Check, @Version, and @UnsavedValue. It walks the class hierarchy, applies snake-case naming by default, ignores
static/transient fields, rejects duplicate columns, and requires exactly one identifier.
Plain Java fields are supported. A DField<T> is useful when the distinction between “not assigned”, explicit null, a
value, and a database-side operation matters:
user.age.set(20);
user.age.setNull();
user.age.setDefault();
user.age.unset();
user.age.add(1);
user.age.multiply(2);The repository turns arithmetic and string operations into SQL expressions, preserving atomic updates. Inserted and read
values pass through the CodecRegistry; applications can provide a custom registry with
Database.builder().codecRegistry(registry).
The implemented Repository<T, ID> methods are:
newEntity, insert, save, findById, findOne, exists, findAll, findAll(SelectQuery), findPage, findKeysetPage, findValues,
findTuples, selectInto, bulkUpdate, bulkDelete, count, stream, delete, deleteById
findAll(SelectQuery) currently expects a projection containing every mapped field in declaration order. The fluent
query builder supports from, select, distinct, where, andWhere, joins, grouping, having, ordering, limit,
offset, and forUpdate (the latter is rejected by SQLite because the dialect does not support it).
| Layer | Responsibility | Current implementation |
|---|---|---|
turtle-annotations |
Mapping declarations | Implemented |
turtle-core |
Metadata, DField, database, repositories |
Reflective SQLite path implemented |
turtle-validation-jakarta |
Optional Jakarta Bean Validation bridge | Lifecycle hook adapter implemented |
turtle-query |
Dialect-neutral AST and rendering SPI | Implemented core AST |
turtle-dialect-sqlite |
SQLite SQL rendering | Implemented |
turtle-dialect-postgresql |
PostgreSQL SQL rendering and catalog introspection | Renderer and current-schema introspection implemented; advanced DDL and full driver integration pending |
turtle-dialect-mysql |
MySQL SQL rendering and catalog introspection | MySQL 8 renderer, introspection, and opt-in JDBC smoke path implemented; generated columns and engine/collation options pending |
turtle-dialect-mariadb |
MariaDB SQL rendering and catalog introspection | Distinct renderer, MariaDB LONGTEXT JSON mapping, connected-version adjustment, and opt-in JDBC conformance implemented; returning, sequences, and generated-column syntax pending |
turtle-driver |
Driver and connection SPI | Implemented |
turtle-driver-jdbc |
JDBC adapter | Implemented |
turtle-driver-r2dbc |
Optional R2DBC adapter | SPI adapter and mock conformance path implemented; real vendor driver integration remains opt-in |
turtle-driver-remote |
Optional reference socket driver | Java-serialization loopback reference with queries, updates, batches, transactions, savepoints, and generated keys |
turtle-log-slf4j |
Optional SLF4J logging sink | Implemented |
turtle-codecs |
Scalar conversion registry | Implemented built-ins |
turtle-schema / turtle-migrations |
Schema and migration models | Immutable snapshots, introspection, deterministic diffs including alterable columns, FK operations, policies, dry runs, and execution implemented; advanced operation AST remains |
turtle-processor |
Generated metadata | EntityMeta classes, typed query fields, scalar to-one join shortcuts, and accessible direct/nested runtime accessors implemented; inaccessible members retain reflection |
turtle-testkit |
Shared conformance tests | Baseline dialect conformance suite implemented |
turtle-benchmarks |
Optional JMH performance tooling | Metadata, AST, insert and select/rendering benchmarks; not an application runtime dependency |
turtle-native-smoke |
Non-published GraalVM verification | Generated-metadata native-image smoke executable |
Core modules do not import java.sql or vendor APIs. The JDBC dependency boundary is enforced by build checks; dialect
modules are the only place where vendor SQL syntax is allowed.
./gradlew check
./gradlew spotlessApplyTests include AST and SQL golden tests, driver SPI tests, codec tests, metadata tests, DField tests, fake-driver
repository tests, a real SQLite JDBC integration test, and a loopback test for the reference remote driver.
Deterministic property-style tests also exercise randomized SQL rendering, schema-diff stability, and built-in codec
round-trips.
The generated-metadata path has a GraalVM native-image smoke test; run it with bash scripts/native-image-smoke.sh
when Docker is available.
The optional turtle-benchmarks module contains JMH benchmarks for metadata resolution, SQL AST construction, and
SQLite rendering of select and insert statements. Run the complete suite with
./gradlew :turtle-benchmarks:jmh; narrow it with -PjmhInclude='.*renderSelect'. CI runs a short rendering smoke
benchmark, while recorded performance baselines remain environment-specific.
See CODESTYLE.md for contribution conventions. Commits use prefix: description format.
MIT. See LICENSE.