basalt 0.1.0 copy "basalt: ^0.1.0" to clipboard
basalt: ^0.1.0 copied to clipboard

A type-safe, migration-first ORM for Dart: a compile-time-checked query builder, code-generated row mappers, and pluggable SQL backends.

basalt #

Dart Driver Part of

The dialect-agnostic core of basalt_dart — the typed schema, expression/query/write builders, the SQL serializer, and the Connection / SqlDialect seams. This package has no driver dependency; concrete backends (basalt_sqlite, basalt_postgres) plug into it.

Part of the basalt_dart workspace. New here? Start with the root README and the getting-started guide.

Contents #

What's inside #

Area Types Source
Types & codecs SqlType<T>integer, text, real, boolean, blob, dateTime, *OrNull types/sql_type.dart
Schema sealed TableColumn<T, Tbl> (ValueColumn / PrimaryKey / Ref), TableRef, QuerySource, TableAlias, Selection, Aggregate schema/table.dart
Expressions Expression<T, Tbl> + & / | / .and / .or combinators expression/expression.dart
Query builder from(...)where/filter/order/limit/offset/select/groupBy/having/join.map(...)MappedQuery<R>; RowMapper<R> query/query.dart
Writes insertInto / update / deleteFrom, TableColumn.set, RETURNING, ON CONFLICT query/write.dart
AST + serializer untyped SqlNode tree → QueryBuilder(sql, params) ast/, serialize/
Execution seam Connection (async-first), SqlDialect, IntrospectedTable connection.dart, serialize/sql_dialect.dart
Annotations @Queryable / @Insertable / @AsChangeset / @Column / @Relation annotations/

Install #

basalt alone builds and serializes queries; add a backend to run them.

dependencies:
  basalt:
  basalt_sqlite:   # or basalt_postgres

The typed schema #

A table is a final class extending TableRef<Self>; static const table is its singleton instance and columns are static const objects holding a typed reference to it, so the same object serves the query builder (Users.age.gt(18)) and the codegen annotations (@Column(Users.name)) — annotation arguments must be constants. The default projection lives in the columns getter override (a getter, not a const field, which is what keeps table ⇄ columns free of const-initializer cycles). In real projects this file is generated by basalt generate-schema.

import 'package:basalt/basalt.dart';

final class Users extends TableRef<Users> {
  const Users._() : super('users');

  static const table = Users._();

  static const id = PrimaryKey<int, Users>(table, 'id', IntSqlType());
  static const name = ValueColumn<String, Users>(table, 'name', StringSqlType());
  static const age = ValueColumn<int, Users>(table, 'age', IntSqlType());
  static const managerId = Ref<int?, Users, Users>(table, 'manager_id',
      NullableSqlType(IntSqlType()), references: Users.id);

  @override
  List<TableColumn<Object?, Object?>> get columns => const [id, name, age, managerId];
}

TableColumn<T, Tbl> is sealed — every column is a ValueColumn, a PrimaryKey, or a Ref (foreign key) — which is what gives the join API and codegen FK-awareness. (Note: Column is the field annotation; the column type is TableColumn.)

Building queries #

Expression<T, Tbl> carries a phantom Tbl scope, so a predicate can only reference its own table until you join:

final q = from(Users.table)
    .where((Users.age > 18) & Users.name.like('A%'))   // & / | combine; chaining .where() REPLACES
    .orderBy(Users.name.asc())
    .limit(20)
    .map((r) => (r.get(Users.id), r.get(Users.name)));  // -> MappedQuery<(int, String)>

Predicates: eq ne gt ge lt le (and > < >= <= sugar), isIn/eqAny, between, like (text columns), isNull/isNotNull, eqColumn (for joins). Aggregates (count/sum/avg/min/max, countAll()) plug into select + groupBy + having. See packages/basalt/doc/queries.md.

Gotcha: chained .where().where() replaces the predicate (last wins). Combine with &/|, or use .filter() — the basalt-style method that ANDs repeated calls.

Reading rows #

A RowReader reads values by selection key (columns by table.name, alias-aware; aggregates by alias) — never by position — so reads are order-independent and join-safe. RowMapper<R> wraps a reader for reuse (this is what codegen emits):

const userMapper = RowMapper<User>(_userFromRow);
final users = await db.fetch(from(Users.table).map(userMapper.read));

Writes #

insertInto(Users.table).value(Users.name.set('Bob'));           // INSERT
update(Users.table).set(Users.age.set(31)).where(Users.id.eq(1)); // UPDATE ... SET ... WHERE
deleteFrom(Posts.table).where(Posts.views.lt(10));                // DELETE

// RETURNING, batch VALUES, and upsert (ON CONFLICT ... DO UPDATE / DO NOTHING) are supported —
// see packages/basalt/doc/queries.md.

TableColumn.set(value) is type-checked: TableColumn<int>.set('x') is a compile error.

Serializer & dialect seam #

The heart of the package is pure and driver-free: QueryBuilder(dialect).buildSelect(query) / .buildWrite(stmt) walk the AST into (String sql, List<Object?> params). A SqlDialect supplies the three things that differ per backend — quoteIdentifier, placeholder, and encodeParam. This is the unit-testable core (test/serializer_test.dart), and it's why adding a backend is a drop-in.

The Connection interface #

Connection is async-first: every method returns a Future; FutureOr appears only on the transaction callback. SQLite runs synchronously and returns already-completed futures, which is exactly what lets an async backend (Postgres) implement the same interface unchanged.

abstract interface class Connection {
  Future<List<R>> fetch<R>(SelectQuery<R> statement);
  Future<int> execute(WriteStatement statement);
  Future<List<R>> executeReturning<R>(ReturningQuery<R> statement);
  Future<void> executeSql(String sql, [List<Object?> params]);
  Future<List<Map<String, Object?>>> queryRaw(String sql, [List<Object?> params]);
  Future<List<IntrospectedTable>> introspect();
  Future<T> transaction<T>(FutureOr<T> Function(Connection tx) action);
  Future<void> close();
}

Annotations #

Metadata only — no runtime behaviour; basalt_codegen consumes them.

Annotation Purpose
@Queryable(table) derive a row reader + mapper + query getter
@Insertable(table) derive toInsert()
@AsChangeset(table) derive toUpdate()
@Column(col, {readOnly, writeOnly}) map a field to a column / tune its read/write direction
@Relation(fk, {depth}) fill a field by joining and nesting a related @Queryable

DevTools inspector #

package:basalt/devtools.dart is a separate entrypoint (not exported by package:basalt/basalt.dart, so plain ORM users don't pull it in) that powers a DevTools "basalt" tab: pick the active instance, browse / filter / sort tables, view and edit rows, and run raw SQL. Backend-agnostic (SQLite + Postgres).

import 'package:basalt/devtools.dart';
BasaltDevTools.register(conn, name: 'main'); // dev-only; the ext.basalt.* hooks are absent from release builds

The Flutter UI lives in packages/basalt_devtools_extension and is compiled into this package's extension/devtools/. Opening the tab from the CLI needs a Dart Tooling Daemon, so use the launcher:

dart run example/tool/inspect.dart   # starts a DTD + app + DevTools; then enable "basalt" in the Extensions menu

Reference #

1
likes
160
points
109
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

A type-safe, migration-first ORM for Dart: a compile-time-checked query builder, code-generated row mappers, and pluggable SQL backends.

Repository (GitHub)
View/report issues
Contributing

Topics

#orm #database #sql #query-builder

License

MIT (license)

More

Packages that depend on basalt