ffastdb 0.3.5
ffastdb: ^0.3.5 copied to clipboard
A high-performance, pure-Dart embedded NoSQL database with B-Tree indexes, WAL crash recovery, and reactive queries.
Changelog #
0.3.5 #
Fixed — performance #
BufferedStorageStrategycould be unexpectedly slow when many different offsets are written in a singleflush()call. If the pending writes are widely scattered (e.g. each writes to a different index block or document offset), themergeChunkslogic could create many individual_writeChunksub-ranges, resulting in more I/O operations than necessary.
0.3.4 #
Fixed — data loss / correctness #
- FTS and Composite indexes could come back silently empty after closing and reopening the database. Three compounding bugs in the index-persistence path:
SortedIndex.deserialize()corrupted its read cursor on any string-valued entry (off += readInt32()discarded 4 bytes due to Dart's compound-assignment evaluation order — the RHS's side effect onoffisn't visible to the addition), throwing aRangeErroronce entries existed.- That exception was swallowed by
loadIndexes(), silently aborting the load of every index serialized after the failing one — including FTS and Composite. - The fallback path (
reindex(field:), used for any index whose persisted blob failed to load) extracted the wrong document field for FTS (used its_secondaryIndexesmap key, e.g._fts_content, instead of the actual fieldcontent) and didn't handle Composite's multi-field extraction at all — so even the fallback silently produced empty indexes.
loadIndexes()also looked up an index's "already registered" check by its plainfieldName, which for FtsIndex can collide with an unrelated Hash/Sorted index registered on the same field name — could incorrectly discard a valid loaded FTS index as a "type mismatch".compact()(single-file mode) could corrupt live document data: it didn't clear the stale pre-compact index-block pointer in the header, sosaveIndexes()could reuse an offset from the old (larger) file layout that now overlapped freshly rewritten documents in the smaller, compacted file.
Fixed — performance #
insertAll()andcompact()'s rebuild path indexed documents one at a time (SecondaryIndex.add()), which is an O(n) array shift per call forSortedIndex— making indexing a batch O(n²) in its own size. Both now batch field values per index and apply them with oneaddAll()call per index per chunk.WalStorageStrategy(the default native storage) applied every buffered write to the main file individually — one syscall per document. Writes are now coalesced into a handful of large sequential writes per transaction, with correct "last write wins" ordering preserved for overlapping ranges.- For transactions larger than the in-RAM buffering threshold (huge
insertAllcalls), each streamed write hit the WAL file with its own syscall — up to ~1M for a 1M-document batch. These are now accumulated in a rolling buffer and flushed every few MB instead. BufferedStorageStrategy.commit()'s write-coalescing was O(n²) in the number of pending writes (re-copied the whole accumulated buffer on every additional write).reindex()/rebuildSecondaryIndexes()read each document with its ownread()call — mostly Dart-side Future/lock overhead rather than actual I/O. Densely-packed batches now fetch their whole byte span in one read and slice records out of the shared buffer in memory.- Net effect: 1M-document
insertAllwent from not completing in 5+ minutes to ~34s;reindex()on 100k documents went from ~6s to ~2.7s; a 100k-documentinsertAllwith Hash + Sorted + FTS + Composite indexes together went from visibly quadratic per-batch growth to flat.
Notes #
- The two fixes above to
WalStorageStrategy/BufferedStorageStrategywrite-coalescing were themselves iterated on during development after being found to zero out untouched bytes between non-adjacent writes, and to break "last write wins" for writes that overlap out of offset order — both are covered by the final implementation and its tests. - Individual (non-batched)
insert()/update()calls are unaffected by the coalescing changes: transactions with a single write (the common case) take a direct fast path with no extra allocation.
0.3.3 #
Fix - Delete - Clear all the docs in the database works well Fix - Rebuild Indexes - Rebuild all the indexes in the database works well
0.3.2 #
Bugfix release — crash safety and reactive consistency #
Critical:
- FIX -
_readAtsilently returnednullon CRC32 mismatch — corrupted documents were indistinguishable from missing ones. Now throwsStateErrorwith offset and checksum values so corruption is immediately visible instead of surfacing as phantomfindByIdnulls. - FIX -
deleteWheredid not notify reactive watchers —db.watch()streams were never updated after a batch delete, leaving UIs showing stale data._notifyWatchersBatch()is now called after the WAL commit on the success path.
High:
- FIX -
OperationLogwas written before the WAL commit in all 4 CRUD methods — a crash betweenopLog.log()andwal.commit()caused_replayOpLog()to re-apply operations that had never actually completed, potentially duplicating documents or corrupting_nextId. The log entry is now written only after a successful commit. - FIX -
_serializeused a lazyMap.cast<String,dynamic>()view — when a document's map had non-Stringkeys (e.g.Map<dynamic,dynamic>from Firebase or untypedjsonDecode), theCastErrorappeared deep inside the serializer instead of at theinsert()call site. Replaced withMap<String,dynamic>.from(doc)for eager validation.
Medium:
- FIX -
_replayOpLogsilently swallowed all replay errors — a barecatch (_) {}discarded failures with no logging. Now emits a diagnosticprintin debug mode viaassert()(zero overhead in release builds). - FIX -
_applySortusedids.length * 4 < idx.size— on Dart Web (int= JS 53-bit number) this multiplication could overflow with very large collections. Replaced withids.length < idx.size ~/ 4and added aidx.size > 0guard.
0.3.0 #
Durability release. Breaking for custom StorageStrategy implementations only.
Critical — crash atomicity:
compact()destroyed docs on crash/ENOSPC — truncated the file first, rewrote from RAM after. The truncate is now a WAL transaction entry; compact is all-or-nothing.- Rollback after a failed commit-apply deleted a committed transaction — it checkpointed the WAL past the atomic point. Rollback now preserves the WAL and the strategy refuses further operations until reopened.
- Schema migrations applied twice after a crash — version header was saved last, so a kill re-ran everything. Migrations now run inside a WAL transaction.
Uint8Listin documents came back asList<dynamic>—JsonEncodernever callstoEncodableforListsubclasses, so the blob path was dead code (as was the 0.2.8\u0000escaping). Now pre-encoded recursively; round-trips correctly.- Concurrent
find()threwFileSystemExceptionon native storage — concurrentsetPosition/readon oneRandomAccessFile. Handle operations are now serialized inIoStorageStrategy.
High:
- Opening a locked DB hung forever (
FileLock.blockingExclusive) — now fails fast; also retries ~2 s after an owner dies (Windows lock-release lag, errno 33). close()always releases the file lock, even when a flush fails; op log kept for replay on unclean close.StorageStrategy.innerStoragelets capability detection (WAL) see through any wrapper.
Breaking: custom StorageStrategy implementations must add innerStorage (one line, return null); opening a DB held by another process now throws instead of waiting forever.
Tests: physical durability suite — SIGKILL mid-write (insert/batch/update/compact/reindex/blob/encrypted), SIGKILL mid-migration, fault injection on the commit-apply path, cross-version migration test (old package code → new code), concurrent writes/transactions, multi-process lock, ENOSPC, LRU eviction, watcher stress, query fuzzing vs linear oracle, index edge cases, opt-in soak test (FFASTDB_SOAK=1), manual on-device checklist (test/manual/).
0.2.8 #
Bugfix & Performance Release — full audit (see FIX_PLAN.md) #
Data correctness (critical):
- FIX - B-Tree
rangeSearchoff-by-one: the last document was lost when the internal separator equaled the upper bound (e.g. afterinsertAllof 229/458/... docs,getAll()returned N-1). - FIX -
BufferedStorageStrategy: coalescing discarded recent writes contained within another write (silent corruption). - FIX -
PageManager: a stale dirty page could overwrite newer data; batch rollbacks no longer bypass the WAL. - FIX -
EncryptedStorageStrategy(WalStorageStrategy): the WAL was not detected through the encrypted wrapper (no real atomicity and ~2.5× fsyncs). - FIX - Per-instance
QueryCache: no more result contamination between FastDB instances. - FIX - Unindexed conditions: now perform a full scan with in-memory filtering (previously silently dropped, returning incorrect results).
Durability/consistency (high): read-your-writes in WAL transactions (and _findById consults _batchEntries), delete() flushes on web, header saved before flush, IndexedDB fail-closed with dirty chunk retries, cache keys free of type collisions, aliasing removed (immutable query results), int64 serialization in SortedIndex/BitmaskIndex, _writeValue fails fast on unsupported types, CompositeIndex with collision-free keys (requires automatic rebuild), count() consistent with pagination, upsert respects manual ids.
API/indexes (medium): watch() without duplicate emissions and with wildcard for condition-less queries, not() propagated to isNull/isNotNull/alwaysTrue, put() rejects ids < 1, unified numeric comparator (1 == 1.0) without crashes on mixed types, idempotent add() across all indexes, bulkLoad tolerates out-of-order/duplicate input, transaction() rollback reverts secondary indexes, page free-list (file no longer grows monotonically on merges/rebuilds).
Performance: eliminated the fsync storm (double fsync + checkpoint per commit), streaming WAL (large imports without OOM), autoCompactThreshold actually disabled by default (was ~22 ms/delete in benchmarks), amortized operation log checkpoint and cleanup on close, table-driven CRC32 (4-8×), bulk index deserialize (startup without O(n²)), dead Bloom filter removed, _applySort O(k log k), concurrent find(), page free-list.
Serialization/security: DateTime preserves isUtc in TypeAdapters, \u0000 sentinels escaped, aes256KeyFromPassword rejects empty passwords, typeId 256 reserved.
Minor breaking changes: findIds() results are immutable (copy with .toList() if you need to mutate them); put() throws ArgumentError on ids < 1; HashIndex.mightContainValue removed along with the Bloom filter; CompositeIndex blobs persisted with the old format are discarded and automatically rebuilt on open.
0.2.7 #
Usage Update #
- CHANGED — Native Internal ID Migrated to
ffdbID: The database now stores its internal numeric ID in theffdbIDkey instead ofid. This safely fully segregates FastDB IDs from user IDs (like Firebaseid), eliminating the need for_originalIdworkarounds. Legacy documents withidare migrated automatically on read.
0.2.6 #
Reliability and Usage Updates #
- NEW — Sequential operation log for write recovery: Added a
.logsidecar file that recordsinsert,put,update, anddeleteoperations before applying them to the main database. Pending operations are replayed automatically on startup after an interrupted write. - IMPROVED — Native
Uint8Liststorage: Binary payloads are now stored as raw bytes in the main database format instead of being encoded as Base64 strings, reducing storage overhead and improving large-binary performance. - CHANGED — Single-owner native access model: Removed the transparent multi-isolate socket proxy path. On native platforms, a database file is now documented and treated as having one active owner at a time.
- FIX — Safer close/write synchronization: Close now waits behind the write lock so shutdown cannot race with pending exclusive writes.
- FIX — Safer storage opening semantics: Native file opening now avoids truncation while still resetting the cursor for random-access writes.
0.2.5 #
Bug Fixes (B-Tree Corruption & Recovery) #
- FIX — Added cycle detection to B-Tree traversal: B-Tree traversal functions (
searchSync,search,_countLeafEntries,_extractLeafEntries) now detect circular page references (cycles). If a cycle is detected, the function immediately returnsnullor throws aStateErrorwith a helpful message, preventing infinite loops and StackOverflow errors caused by corrupt index pages. - FIX — Improved WAL recovery error messages: Enhanced
_recover()to provide more context when encountering corrupted or truncated WAL entries. Errors now include the offset, size, and type of the problematic entry, aiding in diagnosis. - MODERATE — Updated B-Tree rebuilding logic: Refined
_rebuild()to use iterative traversals instead of recursive ones for counting and extracting leaf entries. This prevents StackOverflow errors on very deep index trees.
0.2.4 #
Bug Fixes (WAL Corruption & Multi-Isolate Stability) #
- CRITICAL — Fixed WAL recovery replaying uncommitted transactions: The
_recover()method used a singlehasCommitflag across the entire WAL file. If TX1 was committed and TX2 was interrupted mid-flight (e.g. app killed), recovery would sethasCommit = trueand replay all entries — including the uncommitted ones from TX2 — silently corrupting the B-Tree. Fixed by trackingpendingEntriesper transaction: a COMMIT marker moves pending → committed; entries still pending at EOF are discarded. - CRITICAL — WAL now checkpoints after every commit: Previously the WAL only truncated when it exceeded 1 MB, leaving dozens of committed transactions accumulated in a single file. Combined with the multi-tx recovery bug above, this was the primary source of database corruption on mobile. The WAL is now truncated after every
commit()call, ensuring it never holds more than one transaction at a time and recovery is instantaneous. - FIX — Stale
.fdb.portfile causes SocketException on reopen: If the Owner isolate crashed or closed the database without callingcoordinator.stop(), the.fdb.portsidecar file was left on disk. The nextopenDatabase()call would find the port, try to connect, and throw aSocketException. Fixed by addingIsolateCoordinator.isPortAlive()— a 200 ms probe that verifies the socket is accepting connections before entering proxy mode. Stale port files are deleted automatically. - FIX —
.fdb.portnot cleaned up in tests:persistence_test.darttearDown/setUp did not include.fdb.portin the file cleanup list, causing cross-test contamination when running the full suite. Added to the cleanup set.
0.1.2 #
Bug Fixes (WAL & B-Tree Stability on Mobile) #
- CRITICAL — Fixed infinite loop in WAL binary recovery on Android/iOS: When a COMMIT marker's trailing 4-byte CRC was truncated (i.e. the file ended between the 25-byte header and its checksum),
_recover()advancedoffsetonly inside theif (offset + 4 <= raw.length)guard, thencontinue-d back to thewhile— re-matching the same magic bytes forever. Fixed by movingoffset += 4outside theifso the parser always advances past the COMMIT entry, even when truncated. - CRITICAL — Fixed Stack Overflow in
BTree._insertNonFullon corrupt databases: The recursive descent could cause a Dart VM stack overflow (~55 000 frames) when B-Tree page pointers formed a cycle due to a previously corrupt WAL recovery. Converted_insertNonFullto an iterative loop with avisitedpage-index set that detects cycles immediately and throws a clearStateErrorinstead of crashing the VM. - FIX —
_WalEntrytruncation check now includes CRC bytes: The_kEntryWritetruncation guard previously checkedoffset + length > raw.length, ignoring the 4 trailing CRC bytes. A write entry whose data fit exactly but left no room for the CRC would throw aRangeErrorinside thecatch (_)silently. Now checksoffset + length + 4 > raw.length. - FIX — Unknown WAL entry types now break the parse loop: Previously an unrecognized
typebyte would leaveoffsetunchanged and spin thewhileloop forever. Added an explicitbreakafter the_kEntryWriteblock for unknown types.
0.1.1 #
Bug Fixes #
- FIX — Fixed cumulative sum stream: Resolved an issue where the reactive stream returned by aggregation watchers was accumulating values across emissions instead of replacing them, causing incorrect cumulative totals on repeated events.
0.1.0 #
Stable Public API Release #
- BREAKING — Public index types: Removed
@internalfromSecondaryIndex,HashIndex,SortedIndex,FtsIndex,BitmaskIndex, andCompositeIndex. These are now part of the stable public API. - BREAKING —
IndexManageris now public: Thedb.indexesgetter previously returned a private_IndexManagertype. It now returns the publicIndexManagerclass, enabling proper static typing in user code. - FIX —
SortedIndex.startsWithis now O(log n): Replaced the O(n)_reversescan with a true binary prefix search using_lowerBound(prefix)/_lowerBound(nextPrefix). Prefix queries onaddSortedIndexfields are now as fast as range queries. - FIX —
HashIndex.startsWithwas broken (returned[]): AddedstartsWithandcontainssupport toHashIndex.search(). O(n) scan as expected for a hash structure — useaddSortedIndexfor prefix-heavy workloads. - CLEANUP — 0
dart analyzewarnings: Removed all unused imports, dead code (_readAtSync), unnecessary null assertions, and redundant type checks.
0.0.27 #
Bug Fixes (B-Tree Stability & Indexing) #
-
CRITICAL — Data corruption in single-file mode fixed: Resolved a critical issue where document data could overwrite B-Tree index pages. Implemented
_syncDataOffset()to ensure the document write pointer is always synchronized with the underlying storage size after every structural index operation. This prevents structural corruption under high load. -
MODERATE — Fixed Indexing Type Strictness: Corrected a bug where documents were silently skipped during indexing because of strict
Map<String, dynamic>runtime type checks. The library now correctly supports allMapvariants (includingMap<String, Object>literals used in common application code). -
MODERATE — Fixed Composite Index Population: Fixed a bug where
CompositeIndexfields were not being correctly extracted and indexed during standardinsert()andput()operations. -
CRITICAL — Fixed Index Persistence & Hot Restart failures: Fixed an issue where
FtsIndexandCompositeIndexinstances were left empty in memory if they were registered manually after opening the database, causing searches to fail after a Flutter Hot Restart._StorageManager.loadIndexeswas updated to properly restore complex indexes and map them correctly to the query engine. -
MODERATE — Missing indexes automatically rebuilt:
FastDB.open()now actively checks if any registered secondary indexes are missing from the loaded storage payload. If missing indexes are detected (e.g. after a schema update), FastDB automatically triggers a background rebuild. -
MODERATE — Fixed
FtsIndexsearch signature: UpdatedFtsIndexto correctly implement theSecondaryIndex.search(String operator, dynamic value)interface, preventing runtimeToo few positional argumentserrors.
New Features & Improvements #
- NEW — Declarative Index Registration: Extended
openDatabaseandFfastDb.initto directly acceptindexes,sortedIndexes,ftsIndexes, andcompositeIndexes. This declarative approach guarantees that indexes are fully initialized before disk loading, resolving schema state issues during app restarts. - IMPROVED — Case-Insensitive Search: The
contains()andstartsWith()operators are now case-insensitive by default. This provides a much more intuitive user experience for string-based filtering. - NEW — Index Diagnostics API: Exposed
db.indexes.allto allow developers to inspect active indexes, check their sizes, and retrieve internal statistics (useful for debugging and performance tuning). - NEW — Public Index Types: Exported
HashIndex,SortedIndex,FtsIndex,BitmaskIndex, andCompositeIndexas public types.
0.0.26 #
Bug Fixes (Index Rebuild Corruption on Startup) #
-
CRITICAL —
reindex()andrebuildSecondaryIndexes()caused duplicate index entries: When a database from pre-0.0.24 versions opened in 0.0.23+, if the header was not marked "clean",open()automatically calledrebuildSecondaryIndexes(). However, this method calledrangeSearch()without deduplication. IfrangeSearch()returned IDs multiple times due to B-Tree structural inconsistencies from old versions, each ID would be indexed multiple times. This corrupted secondary indexes, causing subsequentgetAll()calls to return duplicate documents. Fixed by deduplicatingrangeSearch()results usingLinkedHashSetin bothreindex([field])andrebuildSecondaryIndexes(). -
Test Results: All 94 tests pass. Databases created in 0.0.22 or earlier that were opened in 0.0.23-0.0.25 should now be cleaned up by running
reindex()or closing/reopening in 0.0.26.
0.0.25 #
Bug Fixes (Out-of-Memory on Large Batch Inserts) #
-
CRITICAL —
bulkLoadwith large existing trees caused OOM: When inserting 5000+ documents into a database that already had data, the code extracted all existing entries into memory at once via_extractLeafEntries(), then merged and rebuilt. With 10,000+ total entries in RAM simultaneously, Dart's memory allocator was overwhelmed. Fixed by implementing a threshold strategy: trees with <500 entries use the extract-merge-rebuild approach (memory safe), while trees ≥500 entries fall back to individual inserts (no OOM, and now safe because the median-key bug from 0.0.24 is fixed). This trades some speed for memory safety on large existing datasets. -
Test Results: All 94 tests pass including the critical "large batch insert with write-behind does not OOM" test. Batch operations of 5000+ documents now work reliably.
0.0.24 #
Bug Fixes (B-Tree Duplicate IDs) #
-
CRITICAL —
insertAll/commitBatchafter existing data caused duplicate IDs:bulkLoadwas replacing the B-Tree root with a freshly-built tree containing only the new entries, orphaning all previously-written pages on disk. On subsequentgetAll()/rangeSearch()the old pages could still be reached via stale page references, returning IDs 3–5× duplicated. Fixed by extracting all existing leaf entries, O(N+M) merging them with the new batch, and rebuilding a single correct tree from the full combined set. -
CRITICAL — Median key data loss during leaf splits in individual inserts: When
bulkLoadfell back to individualinsertcalls on a non-empty tree, B-Tree leaf splits moved the median key up to the internal node and discarded its document offset. Every 128th document was silently lost. Resolved by the merge-and-rebuild strategy above which eliminates leaf splits entirely for batch operations. -
MODERATE —
rangeSearchcould return duplicate IDs on structurally inconsistent trees: Added aseenIdsSet<int>guard inside_rangeNodeso each ID is emitted at most once regardless of tree state. -
MODERATE —
getAllImpldid not deduplicate IDs from range scan:getAllImplnow passes IDs through aLinkedHashSetbefore fetching documents, providing a second safety net against duplicates reaching the caller. -
MINOR — WAL recovery was not idempotent:
_recover()now compares the WAL entry bytes against what is already on disk and skips writes where the data already matches, preventing double-application of committed transactions after a crash-and-reopen cycle. -
Test Results: All 94 tests pass; 4 new regression tests added covering all duplicate-ID scenarios.
0.0.23 #
Bug Fixes (Query Cache & Reindex) #
-
CRITICAL — Query Cache Key Bug: Fixed query cache key generation that only included condition field types, not actual values. This caused different queries on the same field (e.g.,
equals('London')andequals('NoCity')) to share the same cache entry, returning stale results. Affected operations:sumWhere(),avgWhere(),maxWhere(): returned wrong aggregated valuesupdateWhere(),deleteWhere(): returned wrong update/delete countsfindStream(): yielded stale cached documents- Fix: Cache key now includes condition values for all condition types (equals, range, contains, startsWith, isIn, isNull, fts)
-
MODERATE — Reindex Cache Invalidation: Fixed
reindex()not clearing query cache after rebuilding indexes, causing queries to return stale cached results. AddedQueryBuilder.clearCache()call after index rebuild. -
MODERATE — Test Cross-Contamination: Fixed global static query cache causing test cross-contamination (one test's cached results affecting another test). Added
QueryBuilder.clearCache()call at start ofFastDB.open(). -
Test Results: All 90 tests now pass (previously 11 were failing due to cache issues)
0.0.22 #
Performance Optimizations (Batch Reads & Cache) #
-
Default LRU cache size increased:
cacheCapacitydefault raised from256(1 MB) to2048(8 MB) inFfastDb.init(). This dramatically improves performance on datasets with working sets larger than 1 MB, reducing disk I/O from repeated page cache evictions. -
New method:
findByIdBatch(List<int> ids, {int concurrency = 50}): Parallelizes document reads up to a controlled concurrency limit. For queries returning many IDs, this is 10–100× faster than sequentialfindByIdcalls. Automatically processes results in batches to prevent memory spikes on large result sets. Example:final ids = await db.query().where('status').equals('active').findIds(); final docs = await db.findByIdBatch(ids); // Parallel reads, 10–100x faster -
New method:
stream<dynamic> stream(): Returns a lazy stream that yields documents one by one without loading the entire result set into memory. Ideal for exporting large datasets, pagination, or processing documents as they arrive. Example:await for (final doc in db.stream()) { print(doc); // Process one doc at a time } -
getAll()optimized: Now usesfindByIdBatch()internally instead of sequential reads, providing automatic 10–100× speedup on large databases with no code changes.
0.0.21 #
Bug Fixes (Index Corruption & Startup Reliability) #
- CRITICAL — Bug #1: Fixed
addSortedIndexandaddBitmaskIndexoverwriting already-loaded indexes with empty instances. Both methods now useputIfAbsentso that a persisted index loaded from disk by_loadIndexes()is preserved. The direct assignment caused every startup after the first to return empty query results and permanently corrupt the on-disk index. - GRAVE — Bug #2: Fixed race condition in
openDatabasewhereFfastDb.instancebecame accessible before secondary indexes were registered. Indexes are now passed intoFfastDb.init()and registered beforeopen()is called, so_loadIndexes()can match serialized blobs to the correct type and the singleton is never exposed in a half-initialized state. - GRAVE — Bug #3: Fixed
_loadIndexes()silently ignoring an index-type change between startups (e.g.HashIndex→SortedIndex). If the on-disk type differs from the pre-registered type the blob is now discarded; the index is rebuilt with the correct type via_rebuildSecondaryIndexes(), preventing silent O(n log n) degradation of range queries. - MODERATE — Bug #4: Fixed a single corrupt document aborting the entire startup.
_rebuildSecondaryIndexes()now wraps eachfindByIdcall in a try/catch; corrupt documents are skipped and will be cleaned up on the nextcompact(). - GRAVE — Bug #5a: Fixed O(n) memory allocation on every
startsWithquery withHashIndex._StartsWithConditionnow calls the newHashIndex.filterKeys()method, which iterates buckets without sorting or copying the full index, reducing per-query allocation from O(n) to O(k) where k is the number of matching documents. - GRAVE — Bug #5b: Changed the default
autoCompactThresholdfrom0(never compact) todouble.minPositive(compact as soon as any dead document exists). This prevents the WAL file from growing unboundedly and eliminates the associated RAM spike on startup caused by replaying months of historical writes.
0.0.20 #
Bug Fixes (Web Memory Crash) #
- CRITICAL (Web/IndexedDB): Rewrote
IndexedDbStorageStrategywith chunked incremental flush. Data is now stored as 64 KB chunks in IndexedDB;flush()only writes the chunks modified since the last flush. Previously everyinsert()/update()/delete()copied the entire database buffer (e.g. 50 MB) from Dart to JavaScript, causing peak memory of ~3× the DB size per flush and OOM crashes on large databases. Peak memory per flush is now O(64 KB) instead of O(DB size). - Web/IndexedDB: Backward-compatible migration — databases stored in the old single-key format (
<name>_buffer) are loaded transparently and migrated to the chunked format on the nextflush(). - Web/IndexedDB:
truncate()now cleans up orphan chunk keys in IndexedDB on the next flush, preventing stale data aftercompact(). - MemoryStorageStrategy:
truncate()now reclaims the backingUint8Listwhen the used size shrinks by more than 512 KB (matching the existing fix inWebStorageStrategyandIndexedDbStorageStrategy). Previously a database that grew to 64 MB and was compacted to 5 MB still held the 64 MB buffer in RAM. - Core: Eliminated redundant
storage.flush()calls in_insertImpl()and_updateImpl()whendataStorageis null (single-file mode). On web this avoided creating an unnecessary second IndexedDB transaction per operation. - Web: Reduced default
cacheCapacityinopenDatabase()from 256 to 64 pages (1 MB → 256 KB). On web the entire database is already in RAM, so a large LRU page cache is redundant overhead.
0.0.19 #
Bug Fixes (Memory) #
- OOM fix -
insertAll: Documents are now serialized and written to storage one at a time instead of accumulating all serializedUint8Listobjects in RAM before writing. For large batches (e.g. 100K × 1KB docs) this eliminates ~100MB of peak heap usage. - B-Tree node cache: Reduced
_nodeCacheCapacityfrom 4096 to 512 deserialized nodes, cutting the in-memory node object overhead from ~16MB to ~2MB. Hot nodes remain fast via the underlying LRU page cache. - BitmaskIndex: Default
maxDocIdreduced from 1,048,576 (128KB per bitset) to 65,536 (8KB per bitset). The index still grows automatically via_grow()when document IDs exceed the initial capacity, so behaviour is unchanged for large datasets. _BatchStateenum: Removed the now-unused state-machine enum that was part of the old two-passinsertAllimplementation.
0.0.18 #
Bug Fixes & Code Quality #
- Web/LocalStorage: Fixed
Uint8Listnot found compile error by adding missingdart:typed_dataimport (caused incomplete package analysis and 0/50 static analysis score on pub.dev). - Static analysis: Resolved all
lib/warnings and infos: removed unuseddart:js_interopimport, fixedreturn nullinvoidmethod, replacedLinkedHashMap()with collection literal, addedlibrary;directive toopen_database.dart, madeFieldConditionpublic (was_FieldCondition), fixed doc-comment angle brackets, and improvedprefer_is_emptyusage.
0.0.17 #
Bug Fixes (Web Memory) #
- Web/IndexedDB:
flush()now skips the IndexedDB put when no data has changed since the last flush (_dirtyflag). Eliminates redundant writes that fired 2-3× perinsert()/update()/delete()whenneedsExplicitFlushis true. - Web/IndexedDB:
flush()no longer creates an intermediate Dartsublist()copy of the buffer. A zero-copy typed-data view (buffer.asUint8List) is used instead, reducing the peak RAM during flush from 3× to 2× the database size. - Web/IndexedDB & WebStorageStrategy:
truncate()now releases the backingUint8Listwhen the used size shrinks by more than 512 KB (e.g. aftercompact()). Previously the oversized buffer was retained in RAM until the page reloaded. - Web/LocalStorage: Added
_dirtyflag (same flush-deduplication as IndexedDB) and overrides for bothwrite()andwriteSync(). - Web/LocalStorage:
flush()now catchesQuotaExceededError(the ~5 MBlocalStoragelimit) and throws a descriptiveStateErrorthat suggests switching touseIndexedDb: true, instead of silently losing data.
0.0.15 #
Bug Fixes #
- Web: Fixed
Function converted via 'toJS' contains invalid typescompiler error inIndexedDbStorageStrategyby removing an invalidasynckeyword from a JS interop closure.
0.0.14 #
Critical Bug Fixes #
- CRITICAL: Fixed
openDatabase()unconditionally callingFfastDb.disposeInstance()at the start of every call. This caused"Bad state: Cannot perform operations on a closed database"errors when multiple code paths (e.g., a BLoC and a repository) calledffastdb.init()concurrently during app startup. The function now reuses the live instance if one is already open. - CRITICAL (Web): Fixed
IndexedDbStorageStrategyusing the hardcoded key'db_buffer'for all database instances. Opening two databases (e.g.,'users'and'products') caused their data to collide in the same IndexedDB slot. Each database name now gets its own isolated key ('${name}_buffer').
New Features #
QueryBuilder.find()— executes a query and returns the full document list directly. No more manualfindByIdloop. Use viadb.query().where('field').equals('value').find().QueryBuilder.findFirst()— returns the first matching document ornull, resolving only one document ID for efficiency.QueryBuilder.count()— returns the count of matching documents with an O(1) hot path for simple equality queries on indexed fields (reads the index bucket size directly).FastDB.isOpengetter — exposes whether the database instance is currently usable.
Improvements #
- Improved error message for closed-database operations: now explains the three most common
causes and how to recover, instead of the previous generic
"Cannot perform operations...". EncryptedStorageStrategydoc comment updated with a clear security warning: it uses a Vigenère-style XOR cipher (obfuscation, not cryptographic-grade encryption). Guidance for using AES-256-GCM viaencrypt/pointycastleis included.- Barrel export (
package:ffastdb/ffastdb.dart) now includesEncryptedStorageStrategyand the platform-appropriate storage strategy (IoStorageStrategyon native,IndexedDbStorageStrategyon web) — no more imports of internalsrc/paths.
0.0.13 #
- solve wasm issues
0.0.12 #
- Fig minors issues
0.0.11 (unreleased) #
Critical Bug fixes #
- CRITICAL: Fixed database corruption on Android/iOS caused by using
FileMode.append. On mobile platforms,FileMode.appendignoressetPosition()calls and forces all writes to the end of the file, corrupting B-tree nodes that need to be updated at specific offsets. Now usesFileMode.writewhich correctly respects random-access writes.
API Changes #
- Restored public
FastDB()constructor for non-singleton use cases (benchmarks, multiple instances). For most applications, continue usingFfastDb.init()with the singleton pattern.
0.0.10 #
- fix package compatibility
0.0.9 #
- add meta
- fix library versions
0.0.8 #
- Fix Garbage collector issue
- Fix Firebase problems
0.0.7 #
- fix unsupported type fallbacks
0.0.6 #
- add serializable
0.0.5 #
- fix firebase bugs
0.0.4 #
- Fix persistence bug
0.0.3 #
- Fixed web bug
0.0.2 #
Bug fixes #
- Fixed corrupted documents being read silently from disk without checksum validation.
- Fixed database getting stuck when a batch insert fails halfway through.
- Fixed
compact()not actually freeing disk space in single-file mode. - Fixed index values greater than 2 billion being corrupted after a restart.
- Fixed memory growing unboundedly after many deletes or updates.
- Fixed nested
transaction()calls silently corrupting rollback state — now throws a clear error. - Fixed calling
beginTransaction()twice discarding pending writes silently — now throws a clear error. - Fixed
watch()streams accumulating in memory after all listeners are gone. - Fixed registering two adapters with the same
typeIdsilently overwriting the first one — now throws an error.
New features #
DateTimeis now supported natively — no more manual conversion needed.
Improvements #
- Queries are noticeably faster: the query planner no longer runs each condition twice to estimate cost.
startsWith()is now much faster on sorted indexes (uses a range scan instead of scanning everything).
0.0.1 #
- Pure Dart DB
- Type Adapters
- B-Tree primary index
- Multiplatform storage
- Index persistence
- Hash Index
- Sorted Index
- Bitmask Index
- CRUD Operations
- WAL crash recovery
- Transactions
- Schema migrations
- Fluent query builder
- Aggregations
- Reactive watchers
- Auto-compact
- First version