cindel 0.6.5 copy "cindel: ^0.6.5" to clipboard
cindel: ^0.6.5 copied to clipboard

Generated typed local database for Flutter and Dart apps with MDBX, SQLite, and SQLite Web/OPFS backends.

Cindel

A Flutter-first local database with generated typed Dart APIs, a Rust native core, MDBX by default, SQLite native, and SQLite Web/OPFS.

Typed collections · Generated queries · Transactions · Watchers · Flutter native and Web

Quickstart · Models · Opening · CRUD · Queries · Transactions · Watchers · Web · Testing

Status #

Cindel is pre-1.0. The public direction is a generated typed API: application code works with typed collections such as db.users, and each backend adapts internally to that same app code.

Features #

  • Generated typed collections from Dart model classes.
  • MDBX as the default native backend.
  • SQLite as an explicit native backend.
  • Experimental SQLite Web/OPFS backend for Flutter Web.
  • Auto-increment ids.
  • Typed CRUD and bulk operations.
  • Generated where() and filter() query helpers.
  • Sorting, pagination, distinct, projections, and aggregates.
  • Read and write transactions.
  • Typed object, collection, query, and lazy watchers.
  • Embedded objects and embedded object lists.
  • Freezed classic class and primary factory support.

Quickstart #

1. Add Dependencies #

Flutter apps should depend on cindel and cindel_flutter_libs:

dependencies:
  cindel: ^0.6.4
  cindel_flutter_libs: ^0.6.4

dev_dependencies:
  build_runner: ^2.15.0
  cindel_generator: ^0.6.4

Pure Dart tools can depend on cindel directly and provide a native library with CINDEL_NATIVE_LIBRARY when needed.

2. Define A Model #

import 'package:cindel/cindel.dart';

part 'user.g.dart';

@Collection(name: 'users')
class User {
  Id dbId = autoIncrement;

  @Index(unique: true)
  late String email;

  @Index()
  late String name;

  bool active = true;

  DateTime createdAt = DateTime.now().toUtc();
}

3. Generate Code #

dart run build_runner build --delete-conflicting-outputs

The generator creates the schema, serializers, typed collection getter, and query helpers.

4. Open And Use #

final db = await Cindel.open(
  directory: directory.path,
  schemas: [UserSchema],
);

final user = User()
  ..email = 'ada@example.com'
  ..name = 'Ada Lovelace';

await db.users.put(user);

final saved = await db.users.get(user.dbId);
final activeUsers = await db.users.filter().activeEqualTo(true).findAll();

await db.close();

Models #

Collection Names #

Use @Collection for root persisted objects:

@Collection(name: 'accounts')
class Account {
  Id dbId = autoIncrement;

  @Index(unique: true, replace: true)
  late String username;

  String? displayName;
}

Use @Name when the stored collection or field name should differ from the Dart name:

@Name('products')
@collection
class Product {
  Id dbId = autoIncrement;

  @Name('sku_code')
  @Index(unique: true)
  late String sku;
}

Supported Field Shapes #

Cindel persists:

  • bool, int, double, and String
  • DateTime and Duration
  • enums
  • nullable supported values
  • embedded objects
  • lists of supported non-list values
  • lists of embedded objects

Ignore transient fields with @ignore:

@ignore
String runtimeOnlyLabel = '';

Enums #

Enums can be stored by name, ordinal, or a value field:

enum Plan {
  free('free'),
  pro('pro');

  const Plan(this.code);

  final String code;
}

@collection
class Subscription {
  Id dbId = autoIncrement;

  @Enumerated(CindelEnumType.value, valueField: 'code')
  Plan plan = Plan.free;
}

Freezed #

Cindel supports Freezed classic classes and single primary factories:

import 'package:cindel/cindel.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'product.freezed.dart';
part 'product.g.dart';

@freezed
@Collection(name: 'products')
abstract class Product with _$Product {
  const factory Product({
    required Id dbId,
    @Index(unique: true) required String sku,
    @Index() required String name,
    @Default(true) bool active,
  }) = _Product;
}

Freezed union/sealed multi-constructor models are not supported.

Opening A Database #

MDBX is the default backend on native platforms:

final db = await Cindel.open(
  directory: directory.path,
  schemas: [UserSchema, AccountSchema],
);

Select SQLite explicitly when you want the native SQLite backend:

final db = await Cindel.open(
  directory: directory.path,
  schemas: [UserSchema],
  backend: CindelStorageBackend.sqlite,
);

Use an in-memory database for tests:

final db = await Cindel.openInMemory(schemas: [UserSchema]);

CRUD #

Generated collections are available from the database handle:

final ada = User()
  ..email = 'ada@example.com'
  ..name = 'Ada Lovelace';

await db.users.put(ada);

final saved = await db.users.get(ada.dbId);

await db.users.delete(ada.dbId);

Bulk operations preserve input order and return null for missing ids:

await db.users.putAll([ada, grace, linus]);

final users = await db.users.getAll([ada.dbId, 404, grace.dbId]);

await db.users.deleteAll([ada.dbId, grace.dbId]);

Unique indexes with replace: true generate natural-key upsert helpers:

@collection
class Account {
  Id dbId = autoIncrement;

  @Index(unique: true, replace: true)
  late String username;
}
final account = Account()..username = 'ada';

await db.accounts.putByUsername(account);

If another row already has username == 'ada', the generated helper reuses that id instead of inserting a duplicate.

Queries #

Use generated where() helpers for indexed lookups:

final ada = await db.users
    .where()
    .emailEqualTo('ada@example.com')
    .findFirst();

final recentUsers = await db.users
    .where()
    .createdAtBetween(start, end)
    .findAll();

Use generated filter() helpers for general filtering:

final activeUsers = await db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .findAll();

String fields support contains, prefix, and suffix filters:

final matches = await db.users
    .filter()
    .nameContains('Ada')
    .findAll();

List fields expose element and length helpers:

final flutterUsers = await db.users
    .filter()
    .tagsElementEqualTo('flutter')
    .findAll();

final taggedUsers = await db.users
    .filter()
    .tagsLengthGreaterThan(0)
    .findAll();

Compose dynamic filters with optional, anyOf, and allOf:

final bySearch = await db.users
    .filter()
    .optional(search.isNotEmpty, (q) => q.nameContains(search))
    .findAll();

final byTags = await db.users
    .filter()
    .anyOf(selectedTags, (q, tag) => q.tagsElementEqualTo(tag))
    .findAll();

Queries can count, delete, update, sort, paginate, deduplicate, project, and aggregate:

final count = await db.users.filter().activeEqualTo(true).count();

final names = await db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .offset(20)
    .limit(10)
    .nameProperty()
    .findAll();

final maxId = await db.users.all().dbIdProperty().max();

final updated = await db.users
    .filter()
    .activeEqualTo(false)
    .updateAll({'active': true});

final deleted = await db.users
    .filter()
    .activeEqualTo(false)
    .deleteAll();

Indexes #

Add @Index to fields that should be queryable through generated where() helpers or optimized by the backend:

@collection
class Article {
  Id dbId = autoIncrement;

  @Index()
  late String title;

  @Index(caseSensitive: false)
  late String normalizedTitle;

  @Index(type: CindelIndexType.words, caseSensitive: false)
  late String body;

  @Index(type: CindelIndexType.multiEntry, caseSensitive: false)
  List<String> tags = const [];
}

Composite indexes are declared on the collection:

@Collection(
  name: 'events',
  indexes: [
    CompositeIndex(['accountId', 'createdAt']),
  ],
)
class Event {
  Id dbId = autoIncrement;

  @Index()
  late int accountId;

  @Index()
  late DateTime createdAt;
}

Transactions #

Use writeTxn when several writes must commit or roll back together:

await db.writeTxn(() async {
  await db.users.put(ada);
  await db.accounts.put(account);
});

Use readTxn for a consistent read block:

final users = await db.readTxn(() {
  return db.users.filter().activeEqualTo(true).findAll();
});

If a write transaction throws, Cindel rolls back the pending writes.

Watchers #

Watch one object:

final sub = db.users.watchObject(ada.dbId).listen((user) {
  // user is null when the object does not exist.
});

Watch a collection:

final sub = db.users.watchCollection().listen((users) {
  // Full typed snapshot.
});

Watch a query:

final sub = db.users
    .filter()
    .activeEqualTo(true)
    .sortByName()
    .watch()
    .listen((users) {
      // Matching typed snapshot.
    });

Lazy watchers emit invalidation signals without returning objects:

final sub = db.users.watchCollectionLazy().listen((_) {
  // Refresh cached state.
});

Embedded Objects #

Use @embedded for values stored inside a parent collection object:

@collection
class Email {
  Id dbId = autoIncrement;

  String? subject;

  Recipient? sender;

  List<Recipient>? recipients;
}

@embedded
class Recipient {
  String? name;
  String? address;
  RecipientMetadata? metadata;
}

@embedded
class RecipientMetadata {
  String? label;
}

Generated filters can query inside embedded objects:

final messages = await db.emails
    .filter()
    .sender((recipient) => recipient.addressEqualTo('ada@example.com'))
    .findAll();

final secondary = await db.emails
    .filter()
    .recipientsElement((recipient) {
      return recipient.metadata((metadata) {
        return metadata.labelEqualTo('secondary');
      });
    })
    .findAll();

Embedded classes are not root collections. Add indexes to root collection fields, not inside embedded classes.

Web #

Flutter Web uses the same typed app code:

final db = await Cindel.open(
  directory: 'app.db',
  schemas: [UserSchema],
);

Keep both packages in the app:

dependencies:
  cindel: ^0.6.4
  cindel_flutter_libs: ^0.6.4

Current Web behavior:

  • Web uses SQLite in a Worker with OPFS persistence.
  • Cindel.open(...) loads the packaged Worker and Wasm assets.
  • MDBX is not used in the browser.
  • Generated typed CRUD, queries, transactions, and single-tab watchers are the supported Web path.
  • Web support is experimental and should be validated in the target browser.
  • Multi-tab coordination is not part of the current preview.

Native Binaries #

Flutter apps should include cindel_flutter_libs so native and Web runtime assets are bundled automatically.

For custom local native builds, set CINDEL_NATIVE_LIBRARY before running Dart tests or tools:

$env:CINDEL_NATIVE_LIBRARY = 'D:\path\to\cindel_native.dll'

Testing #

Use openInMemory for fast unit tests:

test('stores users', () async {
  final db = await Cindel.openInMemory(schemas: [UserSchema]);
  addTearDown(db.close);

  final user = User()
    ..email = 'ada@example.com'
    ..name = 'Ada';

  await db.users.put(user);

  expect(await db.users.get(user.dbId), isNotNull);
});

Benchmarks #

Benchmarks are a rough signal rather than a performance guarantee. They are useful for tracking whether changes move Cindel in the right direction.

Small Payloads #

big=false

Cindel benchmark chart for small payloads

Larger Payloads #

big=true

Cindel benchmark chart for larger payloads

License #

Cindel is licensed under the Apache License, Version 2.0. See the repository license file for details.

3
likes
155
points
868
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Generated typed local database for Flutter and Dart apps with MDBX, SQLite, and SQLite Web/OPFS backends.

Repository (GitHub)
View/report issues
Contributing

Topics

#database #nosql #storage #cindel #flutter

License

Apache-2.0 (license)

Dependencies

cindel_annotations, ffi, hooks, native_toolchain_rust, web

More

Packages that depend on cindel