Class SimpleJsonDataOperator<T extends BaseDataEntity<String>>

java.lang.Object
com.ultikits.ultitools.interfaces.impl.data.json.SimpleJsonDataOperator<T>
Type Parameters:
T - Data type inherited from BaseDataEntity
All Implemented Interfaces:
Cached, DataOperator<T>, RowCountingDelete

public class SimpleJsonDataOperator<T extends BaseDataEntity<String>> extends Object implements DataOperator<T>, Cached, RowCountingDelete
Simple Json data operator.
Version:
1.0.0
Author:
wisdomme
  • Nested Class Summary

    Nested classes/interfaces inherited from interface com.ultikits.ultitools.interfaces.DataOperator

    DataOperator.LikeType
  • Field Summary

    Fields
    Modifier and Type
    Field
    Description
    private final Map<Object,T>
     
    private final Map<String,String>
    SQL column name (lowercase) -> Java field name.
    private final Map<Object,Long>
    Keys whose committed value (or absence) has changed since the last checkpoint that captured them, each with the generation of its latest change.
    private static final com.google.gson.Gson
    Default Gson has no bundled adapter for java.time.LocalDateTime: its reflective fallback tries to reach LocalDateTime's private fields, which JDK 9+'s module system refuses without --add-opens java.base/java.time, throwing JsonIOException the first time this operator serializes an entity carrying a non-null LocalDateTime-typed @Column (02-08 -- AuditableDataEntity#createdAt/updatedAt were always null before this plan, so the whole-entity GSON.toJson(...) calls below never reached this type until the lifecycle hooks started populating it).
    private final String
    This table's name in journal records: its directory's path relative to the identity root.
    private final JsonLockTable
    The table write locks this operator's writes take: JsonStore's shared lock table, or, for an operator constructed directly, a lock table of its own.
    private static final Logger
     
    private final String
     
    The JsonTransactionManager every read and write of this operator goes through: the identity's manager, which JsonStore passes in, or, for an operator constructed directly, the manager of a standalone identity of its own (see SimpleJsonDataOperator(String, Class)).
    private final Class<T>
     
  • Constructor Summary

    Constructors
    Constructor
    Description
    SimpleJsonDataOperator(String storeLocation, Class<T> type)
    An operator over the table directory storeLocation, with an identity of its own: its transactions journal in <storeLocation>.ultitools-journal, a sibling of the table directory, and it has its own table locks.
    SimpleJsonDataOperator(String storeLocation, Class<T> type, JsonTransactionManager transactionManager, JsonLockTable lockTable)
    An operator of an identity JsonStore manages: it uses that identity's manager (and so its journal, commit gate and visibility lock) and the store's lock table, and registers with the identity so the identity's checkpoints capture its changes.
  • Method Summary

    Modifier and Type
    Method
    Description
    (package private) void
    Applies one transaction's staged writes for this operator to the committed entries and marks each key dirty.
    private static Map<String,String>
    Builds the "SQL column name -> Java field name" mapping from the entity class's @Column annotations.
    (package private) void
    Adds every dirty key of this operator, with its generation and its committed value (or null if the entry is gone), to captured.
    (package private) JsonJournal.Entry
    The journal entry that brings key's file to value: the entity's JSON (the form this operator has always written to its files) for a value, a delete for null.
    (package private) void
    clearDirty(Object key, long generation)
    Clears key's dirty mark if it still carries generation: a key written again after a checkpoint's capture keeps its newer mark for the next checkpoint.
    private boolean
    conditionCal(String data, String value, WhereCondition condition)
     
    void
    del(WhereCondition... whereConditions)
    Deletes by predicate, without loading matched entries into entities first.
    void
    Deletes by id, fetching the entity first so onDelete() can fire on it -- unlike del(WhereCondition...), which deletes by predicate without materializing entries and therefore does not fire the hook.
    int
    delById(Object), returning how many cache entries it removed (#521), so Query#delete() can return rows removed instead of rows matched.
    private void
    delLocked(JsonTransaction transaction, WhereCondition... whereConditions)
    del(WhereCondition...)'s body after the refusal, run holding this table's write lock.
    private T
    detach(T entity)
    Returns a detached copy of entity, produced by the same Gson form this operator persists to disk (#522).
    private List<T>
    detachAll(List<T> entities)
     
    The non-empty conditions of an updateIf(T, com.ultikits.ultitools.entities.WhereCondition...) call, each checked before anything is read: a null condition, a column the entity does not map with @Column, a null value under any comparison other than Comparison.EQUAL (under EQUAL it means "absent or JSON null", #640), and a non-serialisable value are refused with a DataAccessException, as the relational operators refuse them.
    boolean
    exist(WhereCondition... whereConditions)
    Check if the data record exists.
    boolean
    exist(T object)
    Looks the entry up by the entity's id, as the relational operators do (#522).
    void
    Persist cache object.
    void
    gc()
    Compare local with cache and delete local persistent files.
    Get all data record.
    getAll(WhereCondition... whereConditions)
    Fires onLoad() once per entity actually returned to the caller.
    private List<T>
    getAllRaw(WhereCondition... whereConditions)
     
    Get data record by ID.
    getLike(String column, String value, DataOperator.LikeType likeType)
    Fuzzy Query
    void
    insert(T obj)
    Insert data record.
    void
    insertAll(List<T> entities)
    Insert multiple entities atomically: on every shipped backend either all of them are stored or, if one fails, none is.
    private void
    insertLocked(JsonTransaction transaction, T obj)
    insert(T)'s body, run holding this table's write lock.
    (package private) void
    Adds the journal entries of one transaction's staged writes for this operator to record, in staging order: the entity's JSON for a put (the form a checkpoint writes to its file), a delete for a staged delete.
    (package private) JsonLockTable
    The lock table this operator's writes lock in.
    private void
    Marks key changed since the last checkpoint, with a fresh generation.
    private boolean
    matches(Map<String,Object> map, WhereCondition condition, String valueJson)
    Whether one serialised entry satisfies one condition -- the single matching rule shared by getAll(WhereCondition...), del(WhereCondition...) and updateIf(T, com.ultikits.ultitools.entities.WhereCondition...).
    Every entry transaction sees (every committed entry when it is null): the committed entries whose key has no staged write, then the staged puts.
    private T
    mergedFind(JsonTransaction transaction, Object id)
    mergedGet(com.ultikits.ultitools.interfaces.impl.data.json.JsonTransaction, java.lang.Object) by an entity id, trying its string form when the key type differs.
    private T
    mergedGet(JsonTransaction transaction, Object key)
    The entry for key as transaction sees it: its staged write when it has one (null for a staged delete), otherwise the committed entry.
    page(int page, int size, WhereCondition... whereConditions)
    Get data record by page.
    private <R> R
    Applies a write made outside any transaction: with the identity's commit gate held (read side), then the visibility write side, so a checkpoint's capture sees it whole or not at all and every reader sees it at one instant.
    private <R> R
    Runs read with the calling thread's transaction (or null outside one) under the visibility read side.
    private void
    replaceEntry(T stored, T source)
    Replaces stored with a copy carrying every field of source except id, in one step -- staged in the calling thread's transaction, or put into the committed entries under the visibility write side -- so a concurrent read never sees a half-copied entry.
    private void
    Refuses a new id whose file <id>.json the insert must not create (question 12 A of the Phase 20.4 decisions): a Windows device name with or without an extension, a trailing dot or space, . or .., a control character, a path separator, a file name longer than 255 UTF-8 bytes, or a name this server's file system rejects or does not resolve to <table directory>/<id>.json (on Windows, < > : " | ? *, including a leading drive prefix such as c:).
    private void
    requireId(Object id, String operation)
    Refuses an update or delete addressed by a null id (#546), which no entry can have as its key; the cache is a ConcurrentHashMap, so it used to surface as a raw NullPointerException.
    private void
    Refuses an id that cannot name this table's entity file <id>.json: an empty id, or one containing a slash, a backslash or NUL.
    private String
    Resolves the caller-supplied column name into the key used in the Gson-serialized map.
    private void
    store(Object key, T detachedValue)
    Stores detachedValue under key: staged in the calling thread's transaction, or, outside one, put into the committed entries in one step (see publishPlainWrite(Supplier)).
    private String
    Remove JSON quotes from a serialized string value.
    (package private) String
    The name of this operator's table for messages: its @Table value, else the entity's simple name.
    void
    Execute operations within a transaction (void variant), with the same guarantees as DataOperator.transaction(Callable) on every shipped backend, including the JSON backend's table lock at the first read of a transaction this method begins.
    <R> R
    transaction(Callable<R> action)
    Execute operations within a transaction.
    (package private) JsonTransactionManager
    The manager this operator uses, fixed at construction.
    void
    update(String column, Object value, Object id)
    Update one field of one record.
    void
    update(T obj)
    Update data record by object.
    void
    updateAll(List<T> entities)
    Update multiple entities atomically: on every shipped backend either all of them are updated or, if one fails, none is.
    int
    updateCounted(T entity)
    Counted update: DataOperator.update(BaseDataEntity), returning how many rows it wrote, so the caller can tell that nothing was written (#558).
    private int
    update(BaseDataEntity), returning how many entries it changed: an id no entry has writes nothing and logs one WARNING, where it used to throw a raw NullPointerException (#558).
    boolean
    updateIf(T entity, WhereCondition... expected)
    Conditional update: writes entity over the stored row with the same id only if that row still matches every one of expected, and reports whether it did (#543).
    private boolean
    updateIfLocked(T entity, Object id, List<WhereCondition> conditions)
    updateIf(T, com.ultikits.ultitools.entities.WhereCondition...)'s check and write, run holding this table's write lock.
    private List<T>
    The values the calling thread sees, collected under the visibility read side.
    private void
    Logs one WARNING for an update by a non-null id that matches no entry (#558, maintainer decision 2026-09-29), naming the store by its @Table value -- the directory it is kept in -- as the relational backends name the table.
    private <R> R
    Runs one write while holding this table's write lock.

    Methods inherited from class java.lang.Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

    Methods inherited from interface com.ultikits.ultitools.interfaces.DataOperator

    query
  • Field Details

    • LOGGER

      private static final Logger LOGGER
    • GSON

      private static final com.google.gson.Gson GSON
      Default Gson has no bundled adapter for java.time.LocalDateTime: its reflective fallback tries to reach LocalDateTime's private fields, which JDK 9+'s module system refuses without --add-opens java.base/java.time, throwing JsonIOException the first time this operator serializes an entity carrying a non-null LocalDateTime-typed @Column (02-08 -- AuditableDataEntity#createdAt/updatedAt were always null before this plan, so the whole-entity GSON.toJson(...) calls below never reached this type until the lifecycle hooks started populating it). Mirrors AbstractRelationalDataOperator's identical adapter (02-08), serializing to/from LocalDateTime.toString()'s ISO-8601 form.
    • storeLocation

      private final String storeLocation
    • type

      private final Class<T extends BaseDataEntity<String>> type
    • cache

      private final Map<Object,T extends BaseDataEntity<String>> cache
    • columnToFieldName

      private final Map<String,String> columnToFieldName
      SQL column name (lowercase) -> Java field name.

      Callers always query using the SQL column name declared on @Column (52 call sites across 17 Modules), but the key Gson serializes into the map is the Java field name -- the two don't line up on snake_case vs. camelCase, and the query would silently return zero hits. This map translates the former into the latter, matching the read path AbstractRelationalDataOperator#getListHandler() already uses. See issue #176.

    • transactionManager

      private final JsonTransactionManager transactionManager
      The JsonTransactionManager every read and write of this operator goes through: the identity's manager, which JsonStore passes in, or, for an operator constructed directly, the manager of a standalone identity of its own (see SimpleJsonDataOperator(String, Class)). Fixed at construction, so this operator's readers and committers always use the same transaction contexts, the same visibility lock and the same journal.
    • lockTable

      private final JsonLockTable lockTable
      The table write locks this operator's writes take: JsonStore's shared lock table, or, for an operator constructed directly, a lock table of its own.
    • journalTable

      private final String journalTable
      This table's name in journal records: its directory's path relative to the identity root.
    • dirty

      private final Map<Object,Long> dirty
      Keys whose committed value (or absence) has changed since the last checkpoint that captured them, each with the generation of its latest change. Written under the commit gate's read side and the visibility write side; a checkpoint captures it under the gate's write side.
  • Constructor Details

    • SimpleJsonDataOperator

      public SimpleJsonDataOperator(String storeLocation, Class<T> type)
      An operator over the table directory storeLocation, with an identity of its own: its transactions journal in <storeLocation>.ultitools-journal, a sibling of the table directory, and it has its own table locks. Before it reads the table directory it recovers that journal, so the entries it loads include every transaction committed before a crash. Servers reach JSON data through JsonStore, whose operators share their plugin's identity; this constructor serves code that builds an operator directly.
      Parameters:
      storeLocation - the table directory, one <key>.json file per entity
      type - the entity type
      Throws:
      DataAccessException - naming the file if a committed journal record is damaged; nothing is changed, and constructing the operator again succeeds once the file has been moved out of the journal directory
    • SimpleJsonDataOperator

      SimpleJsonDataOperator(String storeLocation, Class<T> type, JsonTransactionManager transactionManager, JsonLockTable lockTable)
      An operator of an identity JsonStore manages: it uses that identity's manager (and so its journal, commit gate and visibility lock) and the store's lock table, and registers with the identity so the identity's checkpoints capture its changes. Recovers the identity's journal before reading the table directory (for JsonStore it already has, and this is a no-op), so the table files read here include every committed record.
      Parameters:
      storeLocation - the table directory, a direct child of the identity root
      type - the entity type
      transactionManager - the identity's manager
      lockTable - the store's lock table
  • Method Details

    • transactionManager

      JsonTransactionManager transactionManager()
      The manager this operator uses, fixed at construction.
      Returns:
      this operator's transaction manager, never null
    • lockTable

      JsonLockTable lockTable()
      The lock table this operator's writes lock in.
    • tableName

      String tableName()
      The name of this operator's table for messages: its @Table value, else the entity's simple name.
    • write

      private <R> R write(Function<JsonTransaction,R> body)
      Runs one write while holding this table's write lock. Inside a transaction the transaction first rejects the write if it is read-only or past its deadline, then takes the lock, before body evaluates or stages anything, and holds it until the transaction ends; outside one the lock is held for the duration of the call. Every visibility-lock step body takes comes after the table lock, so no thread waits for a table lock while holding the visibility lock.
      Type Parameters:
      R - the write's result
      Parameters:
      body - the write, given the calling thread's transaction or null outside one
      Returns:
      what body returned
      Throws:
      DataAccessException - if the transaction is read-only or past its deadline, or the lock wait fails (deadlock or lock-wait timeout)
    • readVisible

      private <R> R readVisible(Function<JsonTransaction,R> read)
      Runs read with the calling thread's transaction (or null outside one) under the visibility read side. read only collects references; conditions, detaching and hooks run after this returns. Inside a transaction the transaction first checks its deadline and, at REPEATABLE_READ or SERIALIZABLE, takes this table's lock -- before the read side is taken, never while holding it.
    • mergedGet

      private T mergedGet(JsonTransaction transaction, Object key)
      The entry for key as transaction sees it: its staged write when it has one (null for a staged delete), otherwise the committed entry. Outside a transaction, the committed entry. Called under the visibility read side.
    • mergedFind

      private T mergedFind(JsonTransaction transaction, Object id)
      mergedGet(com.ultikits.ultitools.interfaces.impl.data.json.JsonTransaction, java.lang.Object) by an entity id, trying its string form when the key type differs.
    • mergedEntries

      private List<Map.Entry<Object,T>> mergedEntries(JsonTransaction transaction)
      Every entry transaction sees (every committed entry when it is null): the committed entries whose key has no staged write, then the staged puts. Called under the visibility read side; collects references only. Committed values are never changed in place, so the references stay valid after the lock is released.
    • visibleValues

      private List<T> visibleValues()
      The values the calling thread sees, collected under the visibility read side.
    • store

      private void store(Object key, T detachedValue)
      Stores detachedValue under key: staged in the calling thread's transaction, or, outside one, put into the committed entries in one step (see publishPlainWrite(Supplier)).
    • publishPlainWrite

      private <R> R publishPlainWrite(Supplier<R> apply)
      Applies a write made outside any transaction: with the identity's commit gate held (read side), then the visibility write side, so a checkpoint's capture sees it whole or not at all and every reader sees it at one instant. The caller holds this table's write lock. Plain writes are not journaled: they reach disk at the next checkpoint, as before 6.3.0. apply updates in-memory maps only and marks every key it changes dirty.
    • markDirty

      private void markDirty(Object key)
      Marks key changed since the last checkpoint, with a fresh generation.
    • applyStaged

      void applyStaged(JsonTransaction.Overlay<T> overlay)
      Applies one transaction's staged writes for this operator to the committed entries and marks each key dirty. Called only by JsonTransaction.commit(), after the transaction's journal record is committed, holding this table's write lock, the commit gate's read side and the visibility write side. Takes no lock of its own, so a commit holding the write side never waits for anything.
      Parameters:
      overlay - the staged writes; a null value is a staged delete
    • journalEntries

      void journalEntries(JsonTransaction.Overlay<T> overlay, List<JsonJournal.Entry> record)
      Adds the journal entries of one transaction's staged writes for this operator to record, in staging order: the entity's JSON for a put (the form a checkpoint writes to its file), a delete for a staged delete.
      Throws:
      IllegalArgumentException - if a key is not one path segment
    • checkpointEntry

      JsonJournal.Entry checkpointEntry(Object key, Object value)
      The journal entry that brings key's file to value: the entity's JSON (the form this operator has always written to its files) for a value, a delete for null.
      Throws:
      IllegalArgumentException - if key is not one path segment
    • captureDirty

      void captureDirty(List<JsonIdentity.Captured> captured)
      Adds every dirty key of this operator, with its generation and its committed value (or null if the entry is gone), to captured. Called by JsonIdentity.checkpoint() under the commit gate's write side, when no commit or plain write is applying. Collects references only; committed values are never changed in place.
    • clearDirty

      void clearDirty(Object key, long generation)
      Clears key's dirty mark if it still carries generation: a key written again after a checkpoint's capture keeps its newer mark for the next checkpoint.
    • requireStorableKey

      private void requireStorableKey(Object key)
      Refuses an id that cannot name this table's entity file <id>.json: an empty id, or one containing a slash, a backslash or NUL. Such an entry could never be journaled or written by a checkpoint; before 6.3.0 it was flushed into a sub-path and read back as a corrupt entry.
    • requireCreatableKey

      private void requireCreatableKey(Object key)
      Refuses a new id whose file <id>.json the insert must not create (question 12 A of the Phase 20.4 decisions): a Windows device name with or without an extension, a trailing dot or space, . or .., a control character, a path separator, a file name longer than 255 UTF-8 bytes, or a name this server's file system rejects or does not resolve to <table directory>/<id>.json (on Windows, < > : " | ? *, including a leading drive prefix such as c:). See JsonFileNames. Called only for a key the insert creates, so an existing file whose name these rules would refuse keeps working.
    • buildColumnToFieldName

      private static Map<String,String> buildColumnToFieldName(Class<?> type)
      Builds the "SQL column name -> Java field name" mapping from the entity class's @Column annotations. Collected along the inheritance chain, so the @Column("id") inherited from BaseDataEntity is included too.
      Parameters:
      type - the entity type
      Returns:
      an unmodifiable map keyed by the lowercased SQL column name
    • resolveColumn

      private String resolveColumn(String column)
      Resolves the caller-supplied column name into the key used in the Gson-serialized map.
      Parameters:
      column - the caller-supplied column name, which may be a SQL column name, a Java field name, or a nested path
      Returns:
      a key or path that can be passed directly to JsonPathUtil
    • detach

      private T detach(T entity)
      Returns a detached copy of entity, produced by the same Gson form this operator persists to disk (#522).

      Every read path hands out one of these rather than the instance held in cache, and insert(T) stores one rather than the caller's instance. Before 6.3.0 the cached instances themselves were returned, so changing a loaded entity without calling update(...) changed the store and was written at the next flush -- on datasource.type: json only; the relational backends materialise every row afresh, so there a change without update(...) never persisted. The copy is what makes the same module code behave the same on every backend.

      Parameters:
      entity - the cached (or caller-supplied) instance
      Returns:
      a new instance equal in every persisted field
    • requireId

      private void requireId(Object id, String operation)
      Refuses an update or delete addressed by a null id (#546), which no entry can have as its key; the cache is a ConcurrentHashMap, so it used to surface as a raw NullPointerException. Mirrors AbstractRelationalDataOperator's refusal.
    • detachAll

      private List<T> detachAll(List<T> entities)
    • exist

      public boolean exist(T object)
      Looks the entry up by the entity's id, as the relational operators do (#522). Comparing the caller's instance with the cached one through equals() stopped matching once reads and inserts were detached: an entity whose equals() covers a field it changed without calling update(...) no longer equals the stored copy.
      Specified by:
      exist in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      object - Data record entity
      Returns:
      Whether the record exists
    • exist

      public boolean exist(WhereCondition... whereConditions)
      Description copied from interface: DataOperator
      Check if the data record exists.
      Specified by:
      exist in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      whereConditions - Conditions
      Returns:
      Whether the record exists
    • getById

      public T getById(Object id)
      Description copied from interface: DataOperator
      Get data record by ID.
      Specified by:
      getById in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      id - Record ID
      Returns:
      Data record
    • getAll

      public List<T> getAll()
      Description copied from interface: DataOperator
      Get all data record.
      Specified by:
      getAll in interface DataOperator<T extends BaseDataEntity<String>>
      Returns:
      Data record list
    • getAll

      public List<T> getAll(WhereCondition... whereConditions)
      Fires onLoad() once per entity actually returned to the caller. Delegates the matching logic to getAllRaw(com.ultikits.ultitools.entities.WhereCondition...), which exist(WhereCondition...) and page(int, int, com.ultikits.ultitools.entities.WhereCondition...) also call directly -- exist because a boolean check should not fire a load hook, and page because firing on every matched row (rather than only the page's slice) would fire onLoad() for entities the caller never actually receives.
      Specified by:
      getAll in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      whereConditions - Conditions
      Returns:
      Data record list
    • getAllRaw

      private List<T> getAllRaw(WhereCondition... whereConditions)
    • getLike

      public List<T> getLike(String column, String value, DataOperator.LikeType likeType)
      Description copied from interface: DataOperator
      Fuzzy Query
      Specified by:
      getLike in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      column - Column name
      value - Query value
      likeType - Like type
      Returns:
      Data record list
    • page

      public List<T> page(int page, int size, WhereCondition... whereConditions)
      Description copied from interface: DataOperator
      Get data record by page.
      Specified by:
      page in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      page - Page number
      size - Page size
      whereConditions - Conditions
      Returns:
      Data record list
    • insert

      public void insert(T obj)
      Insert data record.

      The entity is stored as <id>.json. An insert of an id no entry has yet refuses, with a DataAccessException (DATA_ENTITY_INVALID) naming the id, an id that cannot name a portable file: a Windows device name with or without an extension, a trailing dot or space, . or .., a control character, a path separator, a file name longer than 255 UTF-8 bytes, or a name this server's file system rejects or does not resolve to <table directory>/<id>.json (on Windows, < > : " | ? *, including a leading drive prefix such as c:); see JsonFileNames. An insert of an id that already has an entry changes nothing and is never refused, so a file whose name these rules would refuse keeps working. A refusal does not mark the transaction rollback-only.

      Specified by:
      insert in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      obj - Data record
    • insertLocked

      private void insertLocked(JsonTransaction transaction, T obj)
      insert(T)'s body, run holding this table's write lock.
    • del

      public void del(WhereCondition... whereConditions)
      Deletes by predicate, without loading matched entries into entities first. Consequently this overload does not fire onDelete() -- there is no entity to fire it on without fabricating one, and this class does not fabricate entities to satisfy a hook. delById(Object) fetches the entity first and does fire onDelete() on it (02-08-PLAN.md's delete-hook-path split, mirrored from AbstractRelationalDataOperator).

      A null or zero-length whereConditions is refused with a DataAccessException, matching AbstractRelationalDataOperator.del()'s guard and the contract DataOperator.del(com.ultikits.ultitools.entities.WhereCondition...)'s interface javadoc promises (CR-02, 02-13). The refusal runs before anything is read or staged, so a refused call inside a transaction stages nothing.

      Inside a transaction, every entry the transaction sees that matches is staged as a delete; outside one, the matches are removed in one step under the visibility write side. Either way the matches are found while holding this table's write lock.

      Specified by:
      del in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      whereConditions - Conditions
    • delLocked

      private void delLocked(JsonTransaction transaction, WhereCondition... whereConditions)
      del(WhereCondition...)'s body after the refusal, run holding this table's write lock.
    • delById

      public void delById(Object id)
      Deletes by id, fetching the entity first so onDelete() can fire on it -- unlike del(WhereCondition...), which deletes by predicate without materializing entries and therefore does not fire the hook. If no entry matches id, nothing fires. Reads the stored entry directly (not getById(java.lang.Object)) so this does not also fire onLoad() -- deleting is not loading.

      onDelete() fires on a detached copy of the entry being deleted (inside a transaction, of the entry as that transaction sees it), so the hook sees every persisted field and nothing it changes on the entity reaches a committed value -- not even through a delete that is then rolled back.

      Specified by:
      delById in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      id - Record ID
    • deleteByIdCounted

      public int deleteByIdCounted(Object id)
      delById(Object), returning how many cache entries it removed (#521), so Query#delete() can return rows removed instead of rows matched.
      Specified by:
      deleteByIdCounted in interface RowCountingDelete
      Parameters:
      id - the entry id
      Returns:
      1 if an entry with this id was removed, 0 if there was none
    • update

      public void update(String column, Object value, Object id)
      Description copied from interface: DataOperator
      Update one field of one record.
      Specified by:
      update in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      column - Column name
      value - New value
      id - Record ID
    • update

      public void update(T obj)
      Description copied from interface: DataOperator
      Update data record by object. It will not update the fields that the incoming entity does not have.
      Specified by:
      update in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      obj - Data record
    • updateCounted

      public int updateCounted(T entity)
      Counted update: DataOperator.update(BaseDataEntity), returning how many rows it wrote, so the caller can tell that nothing was written (#558).

      An update by a non-null id that matches no row writes nothing on every backend: SQLite, MySQL and JSON each log one WARNING naming the table and the id, and return normally. update(T) gives the caller no way to see that; this method returns 0 instead of 1. Use it wherever "the row was gone" must be treated as a failure -- for example a write that credits an account that another server has just removed. For a write that should apply only while the stored row still has the values you read, use DataOperator.updateIf(T, com.ultikits.ultitools.entities.WhereCondition...) instead.

      The framework's operators return the backend's own affected-row count. This default, which a third-party implementation inherits unless it overrides it, checks with DataOperator.exist(WhereCondition...) whether a row with the entity's id exists, returns 0 without writing if it does not, and otherwise calls update(T) and returns 1. It cannot see what that update wrote, so one case remains in which it reports 1 although nothing was written: another writer deleting the row during that one call.

      Returns the number of entries replaced, decided while holding this table's write lock.

      Specified by:
      updateCounted in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      entity - the new state of the row, carrying the id of the row to write
      Returns:
      the number of rows written: 1, or 0 when no row has that id
    • updateEntry

      private int updateEntry(T obj)
      update(BaseDataEntity), returning how many entries it changed: an id no entry has writes nothing and logs one WARNING, where it used to throw a raw NullPointerException (#558).
    • warnNoEntry

      private void warnNoEntry(Object id)
      Logs one WARNING for an update by a non-null id that matches no entry (#558, maintainer decision 2026-09-29), naming the store by its @Table value -- the directory it is kept in -- as the relational backends name the table.
    • replaceEntry

      private void replaceEntry(T stored, T source)
      Replaces stored with a copy carrying every field of source except id, in one step -- staged in the calling thread's transaction, or put into the committed entries under the visibility write side -- so a concurrent read never sees a half-copied entry. Shared by update(BaseDataEntity) and updateIf(T, com.ultikits.ultitools.entities.WhereCondition...).

      BeanCopyUtil copies field references, so the merged copy still shares the caller's List, Map and other mutable field values; a detached copy of it is what gets stored, so no object reachable from the caller's entity is reachable from a committed or staged value (C11). A change the caller makes to its entity afterwards changes nothing stored.

    • updateIf

      public boolean updateIf(T entity, WhereCondition... expected)
      Conditional update: writes entity over the stored row with the same id only if that row still matches every one of expected, and reports whether it did (#543).

      The check and the write are one step: on SQLite and MySQL a single UPDATE ... WHERE id = ? AND <expected> whose affected-row count decides the result, so it holds across servers sharing one database; on the JSON backend the check and the write run under the operator's own lock (a JSON store is local to one server). The conditions mean exactly what they mean in DataOperator.getAll(WhereCondition...) on the same backend, values are bound as parameters, and a condition whose isEmpty() is true is ignored.

      Typical use is compare-and-set: read a row, compute the new state, and write it conditioned on the value that was read; if another writer changed the row in between, nothing is written and false comes back, so the caller re-reads and decides again rather than writing on top. UltiEconomy's one-time wallet merge uses it this way, conditioned on the balance it read:

      
       Account read = accounts.getById(id);
       double seen = read.getBalance();
       read.setBalance(seen + amount);
       if (!accounts.updateIf(read, WhereCondition.builder().column("balance").value(seen).build())) {
           // someone else wrote first: re-read and decide again
       }
       
      Like DataOperator.update(BaseDataEntity), every mapped field of entity is written, and onUpdate() fires on entity before the fields are read, whether or not the write then applies.

      A null expected value means IS NULL under the default Comparison.EQUAL comparison: the condition holds only while the stored column is unset -- on SQLite and MySQL <column> IS NULL inside the same single statement, on JSON an entry whose field is absent or JSON null. A compare-and-set against a column that was unset when it was read therefore misses, and keeps the value, when another writer filled the column in between. This meaning belongs to updateIf alone: DataOperator.getAll(WhereCondition...), DataOperator.exist(WhereCondition...), DataOperator.del(WhereCondition...) and DataOperator.page(int, int, WhereCondition...) treat a null condition value as they always have. (6.3.0 snapshots published before this change refused a null expected value; no release did.)

      The entry the caller sees -- inside a transaction, the transaction's staged version of it -- is checked against expected with the same matching getAll(WhereCondition...) uses, and replaced, while this table's write lock is held, so no other write -- a plain write or another transaction's -- can come between the check and the write. Inside a transaction the lock is held until the transaction ends, so a second transaction's updateIf on the same table is checked only after the first has committed or rolled back: of two calls expecting the same value, at most one succeeds. A JSON store belongs to one server; nothing here coordinates two servers.

      A null expected value under the default Comparison.EQUAL is satisfied by an entry whose field is absent or JSON null, and by no present value (#640). This meaning belongs to this method alone: getAll(WhereCondition...) and del(WhereCondition...) are unchanged.

      Specified by:
      updateIf in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      entity - the new state of the row, carrying the id of the row to write
      expected - the conditions the stored row must still meet
      Returns:
      true if the row matched and was written; false if no row with that id matched every condition, in which case nothing was written
    • updateIfLocked

      private boolean updateIfLocked(T entity, Object id, List<WhereCondition> conditions)
      updateIf(T, com.ultikits.ultitools.entities.WhereCondition...)'s check and write, run holding this table's write lock.
    • evaluableConditions

      private List<WhereCondition> evaluableConditions(WhereCondition... expected)
      The non-empty conditions of an updateIf(T, com.ultikits.ultitools.entities.WhereCondition...) call, each checked before anything is read: a null condition, a column the entity does not map with @Column, a null value under any comparison other than Comparison.EQUAL (under EQUAL it means "absent or JSON null", #640), and a non-serialisable value are refused with a DataAccessException, as the relational operators refuse them. getAll(WhereCondition...) would simply match nothing for an unknown column, but a conditional write that can never apply would make the caller's re-read-and-retry loop spin forever.
    • matches

      private boolean matches(Map<String,Object> map, WhereCondition condition, String valueJson)
      Whether one serialised entry satisfies one condition -- the single matching rule shared by getAll(WhereCondition...), del(WhereCondition...) and updateIf(T, com.ultikits.ultitools.entities.WhereCondition...). An entry that lacks the condition's column never matches. valueJson is the condition's value already serialised, once per condition rather than once per entry; each caller has already checked that the value is serialisable.
    • flush

      public void flush()
      Persist cache object.

      Runs a checkpoint of this operator's identity (JsonIdentity.checkpoint()): every entry changed since the last checkpoint, of every table of the plugin, is written through one checkpoint record, so a crash during the flush leaves either the state before it or the state it captured, never a half-written table and never an older value. Entries that did not change are not rewritten. Waits for a checkpoint of the same plugin already running, and for a commit that is between its record and its apply; takes no table lock. Never writes staged data of an open transaction.

      Specified by:
      flush in interface Cached
      Throws:
      DataAccessException - (DATA_PERSISTENCE_FAILED) if the data could not be written; the changed entries stay pending for the next flush
    • gc

      public void gc()
      Compare local with cache and delete local persistent files.

      Deletes every *.json file of this table that has no committed entry, except the file of a key with a pending dirty mark: a deletion that has not been checkpointed yet reaches disk only through the next checkpoint's record, which is what keeps plain writes crash atomic per flush. Holds the identity's checkpoint mutex, so no checkpoint writes files meanwhile.

      Specified by:
      gc in interface Cached
    • transaction

      public <R> R transaction(Callable<R> action) throws Exception
      Execute operations within a transaction. All operations commit together or roll back on exception.

      Every backend this framework ships (SQLite, MySQL, JSON) runs the action in a transaction: it joins the transaction already active on the calling thread, if any, and any Throwable the action throws rolls it back and is rethrown. SQLite and MySQL use a database transaction. On JSON, the action's writes are staged until commit and are invisible to other threads until then; the commit applies them together, so other threads see all of them at once; a rollback discards them.

      On JSON, a transaction this method begins locks each table at its first read or write and holds the lock until it commits or rolls back, so a read-modify-write inside the action cannot lose an update, as in 6.2.5, where each JSON transaction(...) callback ran alone per operator. A read inside the action can therefore wait for another transaction that holds the table: at most 5 seconds, then it fails with a DataAccessException and the transaction rolls back. A call that joins an active transaction keeps that transaction's isolation: inside a @Transactional method at the default isolation, JSON reads take no table lock.

      When the commit itself fails: on SQLite and MySQL, the exception does not tell whether the database stored the transaction (the outcome is unknown; see TransactionManager.commit()), and nothing is retried, so code that must not apply a write twice re-reads the data, or uses DataOperator.updateIf(T, com.ultikits.ultitools.entities.WhereCondition...), before trying again. On JSON a failed commit commits nothing. An UnexpectedRollbackException from the commit means the transaction was rolled back, none of its writes stored, on every backend.

      This default implementation, used only by a third-party operator that does not override it, runs the action directly without a transaction.

      Runs action in a transaction of this operator's JsonTransactionManager: begins one, or joins the one already active on this thread, runs the action, and commits. Writes the action makes through any operator of the same plugin are staged until the outermost commit, invisible to other threads until then, and applied together at that commit, so other threads see all of them at once. Any Throwable the action throws -- an Error included (#634) -- rolls the transaction back, which discards the staged writes and never touches committed data, and is rethrown as the same instance; a rollback failure is attached to it as suppressed. A commit that throws (a record that cannot be written, or UnexpectedRollbackException for a transaction marked rollback-only) has already ended the transaction and is not followed by a rollback. Mirrors AbstractRelationalDataOperator#transaction(Callable).

      Isolation (question 11 A of the Phase 20.4 decisions). A transaction this method begins locks each table at its first read or write, the REPEATABLE_READ behaviour of JsonTransactionManager: once the action has read a table, no other transaction or plain write can change it until this transaction ends, so a read-modify-write inside the action cannot lose an update. This keeps the guarantee 6.2.5 gave by running each transaction(...) callback alone per operator. The cost is the same as there: a read inside the action can wait for another transaction that holds the table, at most 5 seconds, after which it fails with a DataAccessException and the transaction rolls back; a wait that would close a cycle of transactions waiting for each other fails at once. The table lock is requested before the read takes the visibility lock, never while holding it. A call that joins a transaction already active on this thread (a @Transactional method, or an outer transaction(...)) keeps that transaction's isolation, as a participating scope does in Spring and as a JDBC transaction keeps the isolation it began with: inside a default @Transactional method, reads stay at read committed and take no table lock.

      Specified by:
      transaction in interface DataOperator<T extends BaseDataEntity<String>>
      Type Parameters:
      R - the return type
      Parameters:
      action - the operations to execute
      Returns:
      the result of the action
      Throws:
      Exception - if the action fails
    • transaction

      public void transaction(Runnable action)
      Description copied from interface: DataOperator
      Execute operations within a transaction (void variant), with the same guarantees as DataOperator.transaction(Callable) on every shipped backend, including the JSON backend's table lock at the first read of a transaction this method begins. The same commit outcomes apply: see DataOperator.transaction(Callable).

      This default implementation, used only by a third-party operator that does not override it, runs the action directly without a transaction.

      Specified by:
      transaction in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      action - the operations to execute
    • insertAll

      public void insertAll(List<T> entities)
      Insert multiple entities atomically: on every shipped backend either all of them are stored or, if one fails, none is. Uses JDBC batch for SQL backends; on JSON the batch is one transaction. Inside an active transaction the batch joins it.

      Atomic: runs as one transaction (joining the one active on this thread, if any), so a failure part-way stores none of the entities. The batch goes through transaction(Runnable), so a batch transaction it begins locks a table at its first read; every insert takes the table's lock before its own read, so this changes nothing here.

      Specified by:
      insertAll in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      entities - the entities to insert
    • updateAll

      public void updateAll(List<T> entities) throws IllegalAccessException
      Update multiple entities atomically: on every shipped backend either all of them are updated or, if one fails, none is. Uses JDBC batch for SQL backends; on JSON the batch is one transaction. Inside an active transaction the batch joins it.

      This default implementation, used only by a third-party operator that does not override it, updates the entities one by one without a transaction.

      Atomic: runs as one transaction (joining the one active on this thread, if any), so a failure part-way changes none of the entries. The batch goes through transaction(Callable), so a batch transaction it begins locks a table at its first read; every update takes the table's lock before its own read, so this changes nothing here.

      Specified by:
      updateAll in interface DataOperator<T extends BaseDataEntity<String>>
      Parameters:
      entities - the entities to update
      Throws:
      IllegalAccessException - if field access fails
    • conditionCal

      private boolean conditionCal(String data, String value, WhereCondition condition)
    • stripJsonQuotes

      private String stripJsonQuotes(String jsonValue)
      Remove JSON quotes from a serialized string value. e.g., "\"hello\"" becomes "hello"