Typed Query DSL
Build compile-time-safe InstaQL queries with Col, Filter, Order and TypedQuery
The typed query DSL (Phase 6a) lets you build queries against typed column handles.
Every clause is type-checked at compile time, and TypedQuery.toQuery() compiles to
the same InstaQL map the untyped engine consumes.
Defining a table
A table is a class extending InstantTable<Self> that declares a Col<T> per field.
Pass the namespace (entity type) to the superclass constructor.
class Todos extends InstantTable<Todos> {
Todos() : super('todos');
final title = Col<String>('title');
final priority = Col<int>('priority');
final createdAt = Col<int>('createdAt');
}You can write tables by hand like this, or generate them from an annotated model — see Code Generation.
Columns and operators
Col<T> exposes operators whose value types are bound to the column's T:
final title = Col<String>('title');
final priority = Col<int>('priority');
title.eq('Run'); // {'title': 'Run'}
title.ne('Run'); // {'title': {'$ne': 'Run'}}
title.isNull(true); // {'title': {'$isNull': true}}
priority.inList([1, 2]); // {'priority': {'$in': [1, 2]}}Comparisons (Comparable only)
gt / gte / lt / lte are available only on Col<T> where T is Comparable:
priority.gt(5); // {'priority': {'$gt': 5}}
priority.gte(5); // {'priority': {'$gte': 5}}
priority.lt(5); // {'priority': {'$lt': 5}}
priority.lte(5); // {'priority': {'$lte': 5}}String match (Col<String> only)
like (case-sensitive) and ilike (case-insensitive) are available only on
Col<String> and accept SQL % / _ wildcards:
title.like('%x%'); // {'title': {'$like': '%x%'}}
title.ilike('%x%'); // {'title': {'$ilike': '%x%'}}Calling priority.like(...) or title.gt(...) is a compile error.
Combining filters
Combine leaf filters with & (and) and | (or):
// AND
final f = priority.gte(8) & title.ilike('%x%');
// {'and': [{'priority': {'$gte': 8}}, {'title': {'$ilike': '%x%'}}]}
// OR
final g = title.eq('A') | title.eq('B');
// {'or': [{'title': 'A'}, {'title': 'B'}]}Ordering
Col.asc() / Col.desc() produce an Order:
Col<int>('createdAt').asc(); // {'createdAt': 'asc'}
Col<int>('createdAt').desc(); // {'createdAt': 'desc'}Building a query
Start a query with table.query(), then chain fluent, immutable methods — each
returns a new TypedQuery; the source query is never mutated.
final q = Todos()
.query()
.where((t) => t.priority.gte(8) & t.title.ilike('%x%'))
.order((t) => t.createdAt.desc())
.first(20)
.select((t) => [t.title, t.priority]);
q.toQuery();
// {
// 'todos': {
// '$': {
// 'where': {'and': [{'priority': {'$gte': 8}}, {'title': {'$ilike': '%x%'}}]},
// 'order': {'createdAt': 'desc'},
// 'first': 20,
// 'fields': ['title', 'priority'],
// },
// },
// }Pagination and limits
first / last / after / before (cursor pagination), afterInclusive /
beforeInclusive, and limit / offset are all available:
final q = Todos()
.query()
.order((t) => t.createdAt.asc())
.first(2)
.after('cursor1')
.afterInclusive(true);Projection
select restricts the returned attributes (id is always included):
Todos().query().select((t) => [t.title, t.priority]);Running typed queries
db.queryTyped returns a reactive Signal<QueryResult>; db.queryOnceTyped runs once.
final signal = db.queryTyped(Todos().query().where((t) => t.priority.gte(8)));
Watch((context) {
final result = signal.value;
if (result.isLoading) return const CircularProgressIndicator();
final todos = result.data!['todos'] as List;
return Text('${todos.length} todos');
});final result = await db.queryOnceTyped(
Todos().query().where((t) => t.title.ilike('%urgent%')),
);
final todos = result.data!['todos'] as List;Both return the same QueryResult as the untyped API, so result.pageInfo and
result.documents behave identically. To map result documents into typed model
objects, see Code Generation (the generated getAll/watchAll helpers).