ffastdb 0.2.5
ffastdb: ^0.2.5 copied to clipboard
A high-performance, pure-Dart embedded NoSQL database with B-Tree indexes, WAL crash recovery, and reactive queries.
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