Skip to content

feat: Add micronaut-data-nitrite embedded document store module - #3960

Open
sfkamath wants to merge 118 commits into
micronaut-projects:5.2.xfrom
sfkamath:5.1.x-nitrite
Open

feat: Add micronaut-data-nitrite embedded document store module#3960
sfkamath wants to merge 118 commits into
micronaut-projects:5.2.xfrom
sfkamath:5.1.x-nitrite

Conversation

@sfkamath

@sfkamath sfkamath commented Jul 14, 2026

Copy link
Copy Markdown

Adds micronaut-data-nitrite, an embedded document store module for Micronaut Data, filling the gap that SQLite fills for relational use cases. It targets embedded apps, CLI tools, desktop apps, and integration tests where running MongoDB is too heavy.

New Module: data-nitrite

  • Follows data-mongodb conventions, including QueryBuilder, AbstractConnectionOperations, and document processor integration.
  • Criteria API focused, aligned with the direction taken in Micronaut Data 5.x. The deprecated QueryModel API is present for compatibility but is not the primary implementation path.
  • Association mapping support for @Join across ONE_TO_MANY, MANY_TO_MANY, and MANY_TO_ONE.
  • Supports optimistic locking, transactions, composite IDs, DTO projections, sorting, pagination, cursored pages, unique single-result enforcement, and Jakarta expression predicates.
  • Multi-mode storage with in-memory and file-backed options using MVStore / RocksDB.
  • Full-text and geospatial/spatial index support
  • Query caching and a JMH benchmark are included.
  • Passes the document TCK.
  • Expanded Jakarta Data TCK coverage, including common Jakarta query paths, static metamodel sorting, ignore-case queries, id function projections, expression predicates, projection aliases, DTO projection context, and field reference normalization.
  • NullAway clean and public API Javadoc coverage added.
  • Multi-language documentation examples for Java, Kotlin, and Groovy, with refreshed query support boundaries.

Non-Nitrite Changes

  • data-runtime / RuntimePersistentPropertyPathImpl: adds implicit association join support. Navigating an association property in a Criteria query now resolves through a join rather than failing, enabling MANY_TO_ONE and association-path traversal across all backends.
  • data-model / AbstractExpression and InPredicate: enables in(...) predicates on computed Criteria expressions and supports builder-less expression trees. This lets backend implementations receive and process Jakarta Data expression predicates instead of failing while constructing the shared Criteria tree.

@sfkamath

sfkamath commented Jul 17, 2026

Copy link
Copy Markdown
Author

@radovanradic thanks for your previous GitHub workflow approvals this past week.

This is now ready for release, pending CI checks.

The latest round of work focused mainly on getting the Nitrite module into much better Jakarta Data TCK shape. The most important part is the Jakarta TCK compliance work: enabling the common TCK paths, adding coverage for static metamodel sorting, ignore-case queries, id function projection queries, Jakarta expression predicates, and the remaining restricted-query paths.

The last set of commits also includes fixes for DTO projection context, projection aliases, expression field reference normalization, sorting and pagination behavior, cursored pages, unique single-result enforcement, and some cleanup around query operator handling. NullAway is now satisfied, the public Nitrite API has the missing Javadocs, and the docs have been refreshed to clarify the supported query boundaries.

Assuming CI stays green, I think this branch is in a good state for release

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@sfkamath

Copy link
Copy Markdown
Author

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

Is there any way to constrain the Copilot review to 'data-nitrite/src/main/java'

@graemerocher

Copy link
Copy Markdown
Contributor

@copilot review only data-nitrite/src/main/java

sfkamath and others added 18 commits July 22, 2026 16:43
…ture

- Replace NitriteEntityMapper with PropertyStrategy-based serialization
  using Serde for boundary conversion and entity metadata caching
- Replace NitriteOperationsFactory to use dedicated JacksonMapperModule
  and inject Serde ObjectMapper for entity↔Map conversion
- Add helper classes: ValueConverter, ObjectRepositoryMapper,
  ObjectRepositoryWriter, CollectionWriter, NitriteOperationsHelper,
  NitriteOperationContext
- Adapt DefaultNitriteRepositoryOperations, stored/prepared query
  wrappers, and NitriteStoredQuery interface to new signatures
- Add NitriteQueryExecutor and NitriteCriteriaExecutor for
  prepared/criteria query execution with join fetching
- Add CollectionFieldMapper, CollectionProjectionMapper,
  CollectionAggregator for native projections and aggregations
- Extend NitriteQueryParser with parseSelectClause,
  extractProjectionField, and hasProjection for SELECT/$project support
- Fix NitriteCriteriaExecutor to use public PersistentEntityCriteriaQuery
  interfaces instead of removed internal class
- Replace BaseQueryDefinition parameter with PersistentEntity in constructor
- Update Criteria-based buildSelect/buildUpdate/buildDelete calls to pass
  query.persistentEntity() or definition.persistentEntity()
- Implement SyncCascadeOperationsHelper and NitriteOperationsHelper
- Add NitriteEntityOperations, NitriteEntitiesOperations, and
  cascadeOperations field
- Replace persist/persistAll/update/updateAll/delete/deleteAll
  bodies with delegation to new entity operation classes
- Add placeholder implementations for NitriteOperationsHelper methods
  (toFilterValue, updateEntityId, logging) – will be finalised in next commit
- Delegate toFilterValue to NitriteEntityMapper for proper type coercion
- Use ConversionService in updateEntityId for correct ID assignment
- Enable DEBUG-level logging for insert/update/find operations
Import full test suite covering:
- Cascade operations and lifecycle events
- Transaction management
- Upsert operations with version tracking
- Associations (one-to-many, many-to-many)
- Criteria queries and MongoDB-like syntax
- Embedded entities and embedded IDs
- Projections and aggregations
- Snake case conversions
- Custom storage configurations

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit brings over the remaining unported logic from the 4.14.x branch:

DefaultNitriteRepositoryOperations.java (+1330 lines):
- Port remaining insertOne, updateOne, deleteOne logic with proper OperationType handling and cascade operations
- Port persistEntity and computePersistOperations methods for nested entity persistence
- Port executeEntityOperation and entity relationship handling (NestedJoinAlias, etc.)
- Port prepareResult and extractId logic for proper entity transformation
- Port collectAssociations and cascade save operations with CascadeOp
- Port optimistic locking support with LoadedEntity tracking
- Port txContext and inTransaction logic for transaction-aware operations

NitriteCriteriaExecutor.java (new +408 lines):
- Port criteria executor implementation for criteria-based query execution
- Handle association joins and nested property paths in criteria
- Port predicate building with NitritePredicateVisitor
- Port ResolveContext handling for correlated subqueries

Supporting changes:
- NitriteEntityMapper: Add safeGetIdentity() helper to handle RuntimePersistentEntity.getIdentity() throwing IllegalStateException for embedded entities (5.0.x behavior vs 4.14.x returning null)
- NitriteEntitiesOperations: Use updateOptions(true) for upsert semantics
- NitriteUpdateExecutor: Added with proper update operation handling
- NitriteTransactionManager: Add doSuspend()/doResume() overrides
- NitriteCriteriaSpec: Add integration tests for criteria and association joins
- Restore updateOne() to OperationType.UPDATE + op.update() so cascade updates go through the UPDATE branch and call persistNewCascadeChildren, fixing new cascade children not being persisted on re-save
- Use updateOptions(upsert = versionProp == null) in single-entity UPDATE path: versioned entities use strict update (version filter must fail for OL), non-versioned entities use upsert to support pre-assigned IDs
- Fix NitriteEntitiesOperations batch UPDATE to use updateOptions(false): updateAll() must never insert
- Guard associatedEntity.getIdentity() in persistNewCascadeChildren with try-catch ISE — skip entities with no identity (e.g. NitriteComplexValue) rather than throwing
- Batch UPDATE path uses upsert = versionProp == null — non-versioned entities get insert-if-absent, versioned get strict update
- Add LOG.debug to ObjectRepositoryWriter ISE catch to aid diagnosis

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…omposite ID path mapping

- Map embedded @EmbeddedId associations to _id in NitriteDB document paths, traversing
  nested identity associations inline rather than stopping at FK field names
- Use $between operator for range queries instead of decomposing to $gte/$lte
- Optimize $not:{$in:[...]} to $nin:[...]
- Implement visitArrayContains using $all operator
- Fix sort query guard to detect unquoted $sort key
- Deduplicate conjunction/disjunction visit logic into visitLogical
- Handle non-IPredicate property path in single-predicate conjunction (treat as isTrue)
- Use pipeline format when both match and sort are present in buildSelect
- Extract pipeline $match stage via extractFilterMap in criteria executor and operations
- Update NitriteCriteriaSpec expectations to match corrected output formats
- NitriteFilterBuilder: recognize _id as identity prefix in dot-path lookup
- parseSortFromJsonQuery: handle pipeline list format
- NitriteCriteriaExecutor.buildFindOptions: scan all pipeline stages for $sort, $skip, $limit (not just the first matching stage)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cent and update spec

Fix integer promotion bug where ternary forced Integer to Double; update
NitriteQueryParserSpec to reflect correct escape-sequence unescaping behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…d break DynamicFieldNode cycle

Extract PatternConverter, ValueResolver, SpatialFilterFactory, and AssociationFilterResolver
from the god-class NitriteFilterBuilder. Replace the operator switch with an OperatorHandler
registry. Eliminate the DynamicFieldNode→buildFieldFilter back-reference by introducing
AssociationFieldNode with an AssociationFieldEvaluator callback and extracting the shared
buildAssociationOrNestedField path. Add PatternConverterSpec and ValueResolverSpec unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…odule

Simplify conditional expressions, remove redundant code, and apply style
improvements suggested by IntelliJ across all Nitrite runtime classes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…and array-contains filter

- NitriteQueryBuilder: generate $lookup/$unwind stages from join paths (ported from MongoQueryBuilder)
- NitriteQueryExecutor: non-joined associations stay null; joined associations explicitly set to empty list when no children found
- NitriteCategory: productList default changed to null so absent fields are never initialised by the mapper
- NitritePredicateVisitor: fix MANY_TO_ONE field path to use join alias; skip empty collections in toJsonString
- NitritePredicateVisitor: add $all filter handler using resolveCollection + per-element eq checks (AND-ed; single-element skips Filter.and wrapper)
- NitriteCriteriaSpec: update join test expected strings to Nitrite entity/field naming conventions

Tests (NitriteDocumentRepositorySpec): 6 new tests covering findByIdIn/NotIn, findByNameIn (List/array),
findByNameLike/NotLike, array-contains (String/List + criteria), Map<String, Owner> round-trip, and criteria IN variants
Tests (NitriteMultiOneToManySpec): 1 new test asserting non-joined collection is null
Support files: NitriteDocument, NitriteDocumentOwner, NitriteDocumentEntityRepository, NitriteCriteriaPersonRepository

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… extract shared utilities

- Add NitriteTypeRegistry: single registration point (TypeEntry<T> with strategy,
  write, fromNumber, fromString) for all concrete temporal and string-serialised
  types; supertype walk handles abstract keys (Charset, ZoneId subclasses)
- Add NitriteTypeRegistrySpec: 37 Spock tests covering write/read round-trips,
  supertype resolution, no-op read cases, strategyFor and hasEntry
- Extract PropertyStrategy to its own file; classifyValueStrategy 8-case type
  list collapsed to single NitriteTypeRegistry.hasEntry check
- ValueConverter.toFilterValueStatic: 10 explicit cases → default registry write
- ValueConverter.convertWithTemporalHandling: Number/String if-chains → registry read
- NitriteEntityMapper.convertToDocumentInternal: per-type strategy arms grouped
  into single NitriteTypeRegistry.write call
- NitriteEntityMapper.convertFromDocumentValue: explicit temporal type list
  replaced with NitriteTypeRegistry.hasEntry guard
- ValueResolver.preConvertForFilter: 4-case switch → NitriteTypeRegistry.write
- Extract NameUtils (camelToSnake/snakeToCamel); remove duplicates from
  NitriteEntityMapper, DefaultNitriteRepositoryOperations, CollectionAggregator,
  AssociationFilterResolver
- Extract NitriteEntityMeta and WritablePropertyMeta records to own files
- Delete CollectionWriter (confirmed dead code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…triteQueryBinder

Deduplicates JSON parameter resolution logic from DNRO and NQE into a
single internal utility class, eliminating copy-paste across the two
query execution paths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…dropped in 5.0.x)

Nitrite has no native SQL support. The SQL parsing layer was ported from
4.14.x where it served Query2-style queries; those were removed in 5.0.x,
leaving ~400 lines of dead code. Deletes NitriteUpdateExecutor, removes
isSql() from the stored/prepared query chain, drops all parseWhereClause/
parseFilter*/parseSortFromSqlQuery/reorderParamsForSql methods from DNRO,
NQE, NitriteOperationsHelper, and NitriteQueryParser. Test SQL @query
annotations replaced with JSON equivalents.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread data-nitrite/build.gradle Outdated
…mantics, and mapper correctness

Review-requested:
- Support named datasources under micronaut.nitrite.<name>, with per-configuration
  repository operations, an implicit default in-memory datasource, and @NitriteRepository
  bound to a named datasource; database bean resolution is now qualified per
  datasource, fixing "Multiple possible bean candidates found: [Nitrite, Nitrite]"
  once a second datasource was configured.
- Detect optimistic locking from entity version metadata/query bindings instead of
  method-name text; reject updates with a null identity instead of upserting a
  transient entity.
- Execute max/min/sum/avg criteria selections instead of treating any numeric
  result as a count; resolve criteria update fields to persisted names with
  distinct parameter indexes per bound assignment.
- Fail startup when the RocksDB adapter or a present-but-uninitializable spatial
  module is missing, instead of silently falling back or logging a warning.

Bugs surfaced fixing the above:
- Nitrite has no arithmetic update operator: increments/multiplies are
  read-modify-write per document, and were unsynchronized — concurrent updates
  lost writes (8 threads x 50 increments landed on 150/400). Now serialized per
  collection via CollectionUpdateLock; overflow raises DataAccessException
  instead of a raw ArithmeticException.
- Dotted property paths only resolved their last segment, so nested @MappedProperty
  paths addressed a field absent from the stored document — broke criteria
  updates and nested sort ordering alike.
- Read-only (constructor-only) properties were dropped from writable metadata,
  so immutable entities (Kotlin non-null constructor params) persisted nothing
  but their identity; required constructor associations deferred to null and
  broke constructors that reject null.
- SQL-shaped generated queries mis-split literal AND inside quoted strings and
  silently dropped unbound assignments; batch delete/deleteAll reported wrong
  counts.
- IntelliJ inspection pass: dangling javadoc, identity lookup throwing instead
  of falling back to persisted-name matching, identical catch branches,
  unguarded nullable QueryResult, raw parameterized-type usage.

Docs corrected to match: config prefix was documented as nitrite.* instead of
micronaut.nitrite.<name>.*, multi-datasource usage undocumented, association
fetching/transaction/limitations text updated to match actual behavior.

Co-Authored-By: OpenAI Codex Sol 5.6 <noreply@openai.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@radovanradic

Copy link
Copy Markdown
Contributor

Checkstyle failed

* @param database the Nitrite database
*/
public NitriteConnectionOperations(Nitrite database) {
this.database = database;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The database is now created per datasource, but transaction operations and NitriteTransactionHolder remain global and inject the primary Nitrite instance. A secondary repository can therefore use the wrong transaction/database.

Document update = repositoryWriter.toDocument(entity);
if (update != null) {
helper.logUpdate(collection.getName(), filter, update);
boolean upsert = meta.versionProp() == null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

update() and updateAll() insert entities that are not already present when no @Version property exists (upsert = true in that case). If this is intentional Nitrite-specific behavior, please document it clearly and add coverage, including the distinction from save() and handling of missing IDs.

}

private void appendIdentitySort(Map<String, Sort.Order> orders, RuntimePersistentEntity<?> entity) {
if (!entity.hasIdentity() || entity.hasCompositeIdentity()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Composite IDs are not used as a secondary sort key. As a result, cursor pagination sorted by a non-unique field can skip records that share the same sort value.

// Try to get values - field might be stored as camelCase or snake_case
List<Object> values = docs.stream()
.map(d -> {
Object val = d.get(fieldName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like that derived aggregates only try the Java property name and snake-case form. A property mapped to a custom name such as total_value will not be found.

return NONE;
}

String idField = entity.getIdentity().getPersistedName();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems it still uses getPersistedName(), while identities are stored under canonical id. Nested reverse-association queries with a mapped ID can return no results.
Also, entity.getIdentity() is not checked with hasIdentity()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants