TDB — Lightweight Binary Database for Dart

TDB is a high‑performance, append‑only binary database engine written in pure Dart. It is designed for speed, low memory usage, and easy integration into Flutter or server applications. TDB supports custom data models using adapters, auto‑increment IDs, record querying, event listeners, and automatic compaction.


Version Compatibility

Current Database Version: 1.2.0

Starting from version 1.2.0, the database format has been fully updated. Older database versions are no longer supported and cannot be opened or migrated automatically.

If you attempt to open a database created with a previous version, the system will reject it for compatibility and safety reasons.

Important Notes:

  • Databases created with version 1.2.0 and above are fully compatible with future releases.
  • Databases created with versions below 3.0.0 must be recreated or manually migrated.

✅ Features

  • Pure Dart implementation — No native dependencies
  • Binary storage format — Fast read/write
  • Append‑only engine — Durable and efficient
  • Custom data type support using TDAdapter<T>
  • Auto‑increment IDs for all records
  • Box-based access similar to Hive (e.g., db.getBox<User>())
  • Query and streaming API
  • Event listeners for add/update/delete
  • Automatic compaction to reduce file size
  • Backup support during compaction

🚀 Quick Start

1. Create Model & Adapter

class User {
  final int autoId; //need field for autoId
  final String name;
  User({this.autoId = 0, required this.name});

  Map<String, dynamic> toMap() {
    return <String, dynamic>{'autoId': autoId, 'name': name};
  }

  factory User.fromMap(Map<String, dynamic> map) {
    return User(autoId: map['autoId'] as int, name: map['name'] as String);
  }
  @override
  String toString() {
    return 'ID: $autoId - Name: $name';
  }
}

class UserAdapter extends TDAdapter<User> {
  @override
  User fromMap(Map<String, dynamic> map) {
    return User.fromMap(map);
  }

  @override
  int getUniqueFieldId() {
    return 1; // must be unique for each model
  }

  @override
  Map<String, dynamic> toMap(User value) {
    return value.toMap();
  }
}

2. Open Database

final db = TDB.getInstance();
await db.open('test.db');

db.setAdapter<User>(UserAdapter());

✨ Basic Operations

Add

final id = await db.add<User>(User(name: 'Than'));
printf(id); // auto-increment ID

Get All

final users = await db.getAll<User>();

Query

final result = await db.queryAll<User>((u) => u.name.startsWith('T'));

Update

await db.updateById<User>(1, User(name: 'Updated'));

Delete

await db.deleteById<User>(1);

You can listen for changes

db.stream.listen((event) {
  print(event.type);   // add, delete, update
  print(event.id);      // affected record
  print(event.uniqueFieldId) //uniqueFieldId
});

📦 Box API

TDBox<T> is a typed data container created automatically when you call db.setAdapter<T>(). It provides an easy, safe CRUD interface on top of the TDB core.


Note About autoId Field

Every model class used with TDB must include an autoId field. This field will be automatically populated by the database during insertion.

Example:

class User {
  final int autoId;     // MUST exist — TDB writes newId into this field
  final String name;

  User({ this.autoId = 0, required this.name });
}

If autoId is missing:

  • The database cannot assign a generated ID back into the object
  • Update / Delete operations may not function correctly
  • Querying by ID becomes impossible

TDBox<T> is a typed data container created automatically when you call db.setAdapter<T>(). It provides an easy, safe CRUD interface on top of the TDB core.


🔧 How Box Works Internally

A Box is connected to:

  • the database instance (TDB)
  • the registered adapter for type T

When you call:

final box = db.getBox<User>();

TDB internally maps:

  • adapter → serialization
  • box → CRUD access by type

Each Box only accesses records that match its adapter's unique field ID.


📌 TDBox<T> Class Structure

abstract class TDBoxInterface<T> {
  ///
  /// ### Add Single
  ///
  Future<T?> add(T value);

  Future<void> addAll(List<T> values);

  Future<bool> updateById(int id, T value);
  Future<bool> deleteById(int id);
  Future<void> deleteAll(List<int> idList);
  Future<List<T>> getAll();
  Future<T?> getOne(bool Function(T value) test);
  // query
  Future<List<T>> getQuery(bool Function(T value) test);

  // Stream
  Stream<T> getAllStream();
  Stream<List<T>> getQueryStream(bool Function(T value) test);
  Stream<T?> getOneStream(bool Function(T value) test);
}

(Similar to Hive) TDB provides a simple Box API through TDBox<T>. A Box is automatically created when you call db.setAdapter<T>().

Creating and Using a Box

final userBox = db.getBox<User>();

Box Methods

TDBox<T> provides convenient CRUD and query helpers:

Get All

final users = await userBox.getAll();

Query

final adults = await userBox.queryAll((u) => u.age >= 18);

Stream All

await for (final user in userBox.getAllStream()) {
  print(user);
}

Stream Query

await for (final user in userBox.queryAllStream((u) => u.isActive)) {}

Add

final id = await userBox.add(User(name: "Aung"));

Add Multiple

await userBox.addAll([user1, user2, user3]);

Update

await userBox.updateById(1, User(name: "Updated"));

Delete

await userBox.deleteById(3);

Delete Multiple

await userBox.deleteAll([1, 2, 3]);

🔔 Box Event Listener

TDBox<T> supports reactive data listening. Events: add, update, delete.


(Similar to Hive) When you register an adapter, TDB automatically creates a box:

final userBox = db.getBox<User>();

You can listen for changes

// TDBoxStreamCRUDEvent
// TDBoxStreamErrorEvent
db.boxStream.listen((event) {
    print(event);
});

🔍 Streaming API

Read All Stream

await for (var user in db.getAllStream<User>()) {
  print(user.name);
}

🧹 Auto Compaction

The database grows over time because of append-only writes. Deleted or updated records are cleaned automatically based on configuration:

DBConfig(
  autoCompact: true,
  minDeletedCount: 20,
  minDeletedSize: 4096,
  saveBackupDBCompact: true,
);

You can also run compaction manually:

await db.compact();

🛑 Closing Database

await db.close();

DBConfig

DBConfig defines all configuration options for how the database behaves internally, including versioning, type signature, compaction rules, backups, and locking.


Configuration Fields

dbVersion

  • Type: int
  • Description: Database format version. Must fit into 1 byte (0–255). Used to validate database compatibility.

dbType

  • Type: String
  • Expected Length: 4 bytes
  • Default: TDBT
  • Identifies the file as a valid TDB database.

saveLocalDBLock

  • Type: bool
  • When enabled, the engine creates a local lock file to prevent accidental corruption from concurrent access.

minDeletedCount

  • Type: int
  • Minimum number of deleted entries required before auto-compaction can run.

minDeletedSize

  • Type: int
  • Minimum total deleted data size (in bytes) required before auto-compaction can run.

saveBackupDBCompact

  • Type: bool
  • If enabled, a backup file is created each time a compaction occurs.

autoCompact

  • Type: bool
  • When enabled, the database automatically performs compaction after update or delete operations once thresholds are reached.

Default Configuration

The built-in default settings are:

dbVersion: 1,
dbType: 'TDBT',
saveLocalDBLock: true,
minDeletedCount: 100,
minDeletedSize: 1024 * 1024, // 1MB
saveBackupDBCompact: true,
autoCompact: true,

To get the default config:

final config = DBConfig.getDefault();

Copying With Modifications

You can easily override specific fields using copyWith():

final config = DBConfig.getDefault().copyWith(
  dbVersion: 2,
  autoCompact: false,
);

🧪 Safe to Use In:

  • Flutter mobile apps
  • Desktop apps
  • CLI tools
  • Local server storage

Not suitable for:

  • Multi-process access
  • High-concurrency server DB

📌 Notes

  • Each model must have a unique getUniqueFieldId()
  • Database is append-only, so compaction is necessary

📄 License

MIT

Libraries

t_db
Support for doing something awesome.