Skip to content

Generated code

For each schema the generator writes a *.gisila.g.dart file that imports package:gisila_orm/gisila.dart. Don’t edit it; regenerate after every schema change. The examples on this page use this schema:

Author:
columns:
name: { type: varchar, is_null: false }
email: { type: varchar, is_null: false, is_unique: true, allow_blank: false }
Post:
columns:
title: { type: varchar, is_null: false }
body: { type: text }
published_at: { type: timestamp }
author:
type: Author
is_null: false
reverse_name: posts

Per model you get four things:

Generated Example Purpose
Model class class Post with Preloadable An immutable row value.
Table class class PostTable Typed ColumnRefs and the TableMeta.
Query accessor Query<Post> posts() Shortcut for Query<Post>(PostTable.metadata).
Relations Post.author, Author.posts Static Relation values for preload.

Schema enums also become Dart enums.

class Post with Preloadable {
final int? id;
final String title;
final String? body;
final DateTime? publishedAt;
final int authorId;
Post({this.id, required this.title, this.body, this.publishedAt, required this.authorId});
factory Post.fromRow(Map<String, dynamic> row);
Map<String, dynamic> toRow();
factory Post.fromJson(Map<String, dynamic> json);
Map<String, dynamic> toJson({List<String> exclude = const [], List<String> only = const []});
Post copyWith({int? id, String? title, String? body, DateTime? publishedAt, int? authorId});
List<String> validate();
static final Relation<Post, Author> author = BelongsToRelation<Post, Author>(...);
Author? get authorLoaded;
}

When to use: the model is what queries return. Build one yourself when you need a value object (for example in tests); to write rows, pass a map to insert instead.

Rule Detail
Field names camelCase of the column name; foreign keys use the physical column (author → authorId).
Nullability Nullable when is_null: true. The primary key is always nullable (int? id), because the database assigns it on insert.
required Parameters for is_null: false columns, except the primary key.
Many-to-many columns Produce no field.
const The constructor isn’t const, because Preloadable holds a mutable map.
Member Description
fromRow(row) Builds a model from a column-name map, coercing driver values: DateTime.parse for date/timestamp strings, Vector.parse / Vector.fromList for vectors, Point.fromString and friends for geometry, <Enum>Gisila.parse for enums, toString() for decimals. Keys are physical (snake_case) column names.
toRow() The reverse: physical column name → value ready to bind. Enums become .sqlValue, geometry becomes .toSqlLiteral(), JSON becomes encodeJsonColumn(value). Includes id, even when null.
fromJson(json) Same as fromRow.
toJson({exclude, only}) toRow() with DateTime values as ISO-8601 UTC strings and JSON columns as their decoded value, so jsonEncode(post.toJson()) works. only keeps just the listed keys; otherwise exclude removes keys. Keys are snake_case.
copyWith(...) Copy with some fields replaced. Passing null keeps the current value, so you can’t clear a field with it.
validate() Returns error strings such as Author.email must not be blank for allow_blank: false string columns. Empty when valid. Nothing calls it for you.
preloaded<T>(name), isPreloaded(name), $preloaded From Preloadable.
Schema Static relation Accessor after preload
author: { type: Author } on Post Post.author (BelongsToRelation<Post, Author>) Author? get authorLoaded
reverse_name: posts on that column Author.posts (HasManyRelation<Author, Post>) List<Post> get postsList
Same, with is_unique: true on the FK Author.posts (HasOneRelation<Author, Post>) Post? get postsLoaded
tags: { type: Tag, many_to_many: true, reverse_name: posts } Post.tags, Tag.posts (ManyToManyRelation) List<Tag> get tagsList, List<Post> get postsList

Relations are static final, not const. Collection accessors return an empty list when the relation wasn’t preloaded; single accessors return null.

class PostTable {
PostTable._();
static const ColumnRef<int?> id = ColumnRef<int?>(table: 'posts', column: 'id');
static const ColumnRef<String> title = ColumnRef<String>(table: 'posts', column: 'title');
static const ColumnRef<String?> body = ColumnRef<String?>(table: 'posts', column: 'body');
static const ColumnRef<DateTime?> publishedAt =
ColumnRef<DateTime?>(table: 'posts', column: 'published_at');
static const ColumnRef<int> authorId = ColumnRef<int>(table: 'posts', column: 'author_id');
static const TableMeta<Post> metadata = TableMeta<Post>(
tableName: 'posts',
primaryKey: 'id',
columnNames: ['id', 'title', 'body', 'published_at', 'author_id'],
fromRow: Post.fromRow,
);
}

When to use: reach for PostTable.<column> whenever you filter or sort (where, orderBy), and pass PostTable.metadata to Query<Post>(...).

Each ColumnRef<T> is typed with the model field’s Dart type, so PostTable.title.eq(42) doesn’t compile. The type decides which extension operators are available: like on ColumnRef<String> and ColumnRef<String?>, array operators on ColumnRef<List<T>>, vector distances on ColumnRef<Vector> and ColumnRef<Vector?>.

Static metadata that a Query<T> carries: which table to read and how to build a T from a row.

class TableMeta<T> {
const TableMeta({
required String tableName,
required List<String> columnNames,
required RowMapper<T> fromRow,
String primaryKey = 'id',
});
}

When to use: generated for you. Write one by hand only to query a table that isn’t in your schema (a view, a legacy table) with the typed builder.

Member Type Default Description
tableName String – Unquoted SQL table name.
columnNames List<String> – Non-relation columns in schema order. Used as the default SELECT list and by the preloader.
fromRow RowMapper<T> – Builds a T from a column map.
primaryKey String 'id' Primary key column name.

Example

import 'package:gisila_orm/gisila.dart';
class ActiveUser {
ActiveUser(this.id, this.email);
final int id;
final String email;
}
const activeUsers = TableMeta<ActiveUser>(
tableName: 'active_users_view',
columnNames: ['id', 'email'],
fromRow: _activeUserFromRow,
);
ActiveUser _activeUserFromRow(Map<String, dynamic> row) =>
ActiveUser(row['id'] as int, row['email'] as String);
Future<List<ActiveUser>> recent(DbContext db) =>
Query<ActiveUser>(activeUsers).limit(10).all(db);
typedef RowMapper<T> = T Function(Map<String, dynamic> row);
class Hydrator<T> {
const Hydrator(RowMapper<T> mapper);
List<T> hydrateAll(Iterable<ResultRow> rows);
T hydrateOne(ResultRow row);
}

Hydrator turns driver ResultRows into models by calling mapper(row.toColumnMap()). Every terminal operation uses it. Use it yourself to hydrate the result of a hand-written query:

final result = await db.execute(
'SELECT * FROM posts WHERE to_tsvector(body) @@ plainto_tsquery(\$1)',
parameters: ['dart'],
);
final posts = Hydrator<Post>(Post.fromRow).hydrateAll(result);
Query<Post> posts() => Query<Post>(PostTable.metadata);

A top-level function named with the plural camelCase of the model (Author → authors(), Category → categories()). Each call returns a new builder.

enum ReviewVerdict { pending, approved, rejected }
extension ReviewVerdictGisila on ReviewVerdict {
String get sqlValue => name;
static ReviewVerdict parse(String raw); // throws FormatException for unknown labels
}

Pass .sqlValue when you write an enum column through a map: {'verdict': ReviewVerdict.approved.sqlValue}.

Schema files Output
One (lib/models/blog.gisila.yaml) blog.gisila.g.dart, blog.gisila.up.sql, blog.gisila.down.sql beside it
Two or more lib/models/schema.gisila.g.dart (+ .up.sql, .down.sql). Without a lib/ directory, the common parent directory of the files.

The .up.sql / .down.sql files are a full create / drop script for the current schema, not an incremental migration. See The codegen pipeline.

See also: Schema YAML format, Query builder, Relations and preloading.