typed_sql 0.1.3 copy "typed_sql: ^0.1.3" to clipboard
typed_sql: ^0.1.3 copied to clipboard

Package for doing SQL with some type safety.

Type-safe SQL in Dart #

This package aims to offer a slightly opinionated type-safe API for interacting with an SQL database from Dart. Thus, offering users the power of SQL, with the type-safety of Dart (including null-safety!).

Features:

  • Type safety: If your Dart code is type-safe you will not get SQL syntax errors at runtime.
  • Null-safety: If you return a field that has a NOT NULL constraint, the deserialized Dart type will be non-nullable!
  • Fluent API: Queries and expressions are build with chainable extension methods making it easy to discover available operators using auto-completion!
  • Expressiveness: Support for a wide range of SQL features, including:
    • Powerful arbitrary SQL expressions, including subqueries.
    • Insert, update and delete with
      • Returning clause for projections of affected rows.
      • Update/Delete filtering with SQL expressions.
      • Update with SQL expression on existing rows.
    • Select queries with
      • Projections with SQL expressions,
      • Aggregations (sum, count, avg, min, max) and GROUP BY,
      • Joins
      • Subqueries (including convenient lookups of foreign keys)
      • Ordering, limiting, and offsetting
      • Transactions (including nested transactions)
    • Schema definition with
      • Auto increment
      • Composite primary keys
      • Unique constraints
      • Not null constraints (linked to nullability in Dart!)
      • Default values
      • Foreign keys
  • Database agnostic: This package already supports
    • Sqlite, and,
    • Postgres!

Example #

This packages requires that you define a schema for code-generation with build_runner. This is usually defined in a model.dart, with the generated code ending up in a model.g.dart part-file.

abstract final class Bookstore extends Schema {
  Table<Author> get authors;
  Table<Book> get books;
}

@PrimaryKey(['authorId'])
abstract final class Author extends Row {
  @AutoIncrement()
  int get authorId;

  @Unique()
  String get name;
}

@PrimaryKey(['bookId'])
abstract final class Book extends Row {
  @AutoIncrement()
  int get bookId;

  String? get title;

  @References(table: 'authors', field: 'authorId', name: 'author', as: 'books')
  int get authorId;

  @DefaultValue(0)
  int get stock;
}
copied to clipboard

To use this you must first install build_runner as dev-dependency, and run it:

dart pub add typed_sql
dart pub add dev:build_runner
dart run build_runner build
copied to clipboard

Once the model.g.dart file has been generated, you can create tables and insert/update/delete rows as follows:

// Connect to database
final db = Database<Bookstore>(
  DatabaseAdapter.sqlite3(Uri.parse(file)),
  SqlDialect.sqlite(),
);

// Create tables
await db.createTables();

// Insert an author and return the authorId!
final authorId = await db.authors
    .insert(
      name: toExpr('Bucks Bunny'),
    )
    .returning((author) => (author.authorId,))
    .executeAndFetch();

// Insert a book, omitting stock since it has a default value!
await db.books
    .insert(
      title: toExpr('Vegan Dining'),
      authorId: toExpr(authorId), // by Bucks Bunny
      stock: toExpr(3),
    )
    .execute();

// Decrease stock for 'Vegan Dining', return update stock
final updatedStock = await db.books
    .where((b) => b.title.equals(toExpr('Vegan Dining')))
    .update((b, set) => set(
          stock: b.stock - toExpr(1),
        ))
    .returning((b) => (b.stock,))
    .executeAndFetch();
check(updatedStock).deepEquals([2]);

// Delete all books by Bucks Bunny
await db.books
    .where((b) => b.authorId.equals(toExpr(authorId)))
    .delete()
    .execute();
copied to clipboard

We can also query the database in complex manners as follows:

// Lookup author by id
final author = await db.authors.byKey(authorId).fetch();
if (author == null) {
  throw Exception('Author not found!');
}
check(author.name).equals('Bucks Bunny');

// Lookup book and associated author in one query
final (book, authorOfBook) = await db.books
    // Filtering using a .where clause with a typed expression
    .where((b) => b.title.equals(toExpr('Vegan Dining')))
    // Projection to select Expr<book> and Expr<Author> using a subquery
    .select((b) => (b, b.author))
    .first // only get the first result
    .fetchOrNulls();
if (book == null || authorOfBook == null) {
  throw Exception('Book or author not found');
}
check(book.title).equals('Vegan Dining');
check(authorOfBook.name).equals('Bucks Bunny');

// We can also query for books with more than 5 in stock and get the title
// and stock of each book.
final titleAndStock = await db.books
    .where((Expr<Book> b) => b.stock > toExpr(5))
    .select((b) => (b.title, b.stock))
    .fetch();
check(titleAndStock).unorderedEquals([
  // title, stock
  ('Are Bunnies Unhealthy?', 10),
  ('Hiding Eggs for dummies', 12),
  ('Vegetarian Dining', 42),
]);

// We can also join books and authors, group by author sum how many books we
// have in stock by author.
final stockByAuthor = await db.books
    .join(db.authors)
    .usingAuthor()
    .groupBy((b, a) => (a,))
    .aggregate((agg) => agg.sum((b, a) => b.stock))
    .select((a, totalStock) => (a.name, totalStock))
    .fetch();
check(stockByAuthor).unorderedEquals([
  // name, totalStock
  ('Easter Bunny', 22),
  ('Bucks Bunny', 45),
]);

// We can also compute this with subqueries using the @Reference annotation
final stockByAuthorUsingSubquery = await db.authors
    .select((a) => (
          a.name,
          a.books.select((b) => (b.stock,)).sum(),
        ))
    .fetch();
check(stockByAuthorUsingSubquery).unorderedEquals([
  // name, totalStock
  ('Easter Bunny', 22),
  ('Bucks Bunny', 45),
]);
copied to clipboard

In general .where can be used to filter results, you can build complex expressions using differnet extension methods for Expr<String>, Expr<int>, Expr<DateTime>, etc. making it easy to compare values correctly, and hard (if not impossible) to make syntax errors in SQL!

The .select can be used to create custom projections, by returning a positional record of Expr objects. Where the type of the objects are carried into the .fetch() method ensuring that Expr<String> becomes a String when fetched. Here nullability is preserved, so you don't have to do null checks for columns (or expressions) that can't be null!

package:typed_sql has many more features and tutorial style documentation that demonstrates most of the features by example. In practice, you'll hopefully find that most features are discoverable through auto-completion.

Status #

package:typed_sql is still under active development, breaking changes will occur going forward as features are both added and removed.

13
likes
130
points
67
downloads

Publisher

verified publishergoogle.dev

Weekly Downloads

2024.10.07 - 2025.09.01

Package for doing SQL with some type safety.

Homepage
Repository (GitHub)
View/report issues
Contributing

Documentation

API reference

License

Apache-2.0 (license)

Dependencies

analyzer, build, code_builder, collection, convert, glob, meta, postgres, source_gen, sqlite3

More

Packages that depend on typed_sql