Schema-first design
What it is
Section titled “What it is”In gisila_orm the source of truth for your tables is a YAML file, not Dart
classes and not the live database. You describe models, columns, relations,
indexes and constraints in *.gisila.yaml; the generator derives everything
else from it:
blog.gisila.yaml ──▶ Dart models, <Model>Table columns, relations ──▶ full CREATE / DROP SQL ──▶ incremental migrations (diff against the last snapshot)Because the Dart code and the SQL come from the same file, they can’t drift
apart: a column that exists in PostTable exists in the migration that creates
the table, with a matching type and nullability.
When to use it
Section titled “When to use it”- A service that owns its PostgreSQL database and evolves its schema with the code.
- You want compile-time checked column names and value types in queries (
PostTable.title.eq(42)doesn’t compile). - You want migrations written for you from schema edits, and refused when they would drop data.
- Several packages or apps share one data model: the YAML is easy to review in pull requests.
When not to use it
Section titled “When not to use it”- The database is owned by someone else (a legacy system, a reporting replica). Generating migrations makes no sense there. Write
TableMetaby hand for the tables you read, or use raw SQL. - Heavy use of features the schema can’t describe: partial or expression indexes, composite primary keys, triggers, views, partitioning, decimal precision and scale, PostGIS. You can still use gisila_orm, but put that DDL in hand-written migrations next to the generated ones.
- A database other than PostgreSQL. Only PostgreSQL is supported.
How it works
Section titled “How it works”- Parse and validate. All schema files merge into one
SchemaDefinition. The validator reports every mistake at once, with file, line and a “did you mean” hint, before anything is written. - Apply conventions. Table names default to plural snake_case (
BlogPost→blog_posts), a missing primary key becomesid BIGSERIAL, a foreign keyauthoris stored asauthor_idand typed like its target’s primary key, and relations get an inverse when you name it withreverse_name. - Emit. Dart models and typed columns, a full-schema SQL script, and an incremental migration from the diff with the last committed snapshot.
The schema describes shape; it doesn’t describe behaviour. Business rules
beyond NOT NULL, UNIQUE, CHECK and foreign keys live in your Dart code.
allow_blank: false is the one exception: it generates a validate() check in
Dart, not a database constraint.
Trade-offs
Section titled “Trade-offs”| You get | You give up |
|---|---|
| One reviewed file for the whole data model | Some Postgres features need hand-written SQL |
| Typed queries that track the schema | Regenerating after every schema edit |
| Safe, generated migrations | Control over migration SQL unless you edit or add files |
| Models free of annotations and runtime reflection | Custom methods on models (generated files are overwritten; use extensions) |
To add behaviour to a generated model, write an extension in your own file:
import 'models/blog.gisila.g.dart';
extension PostX on Post { bool get isPublished => publishedAt != null;}Example
Section titled “Example”Author: columns: name: { type: varchar, is_null: false } email: { type: varchar, is_null: false, is_unique: true }
Post: columns: title: { type: varchar, is_null: false, max_length: 200 } published_at: { type: timestamp } author: type: Author is_null: false on_delete: CASCADE reverse_name: postsFrom this you get Author, Post, AuthorTable, PostTable, Post.author,
Author.posts, authors(), posts(), the CREATE TABLE statements with a
foreign key posts.author_id → authors.id ON DELETE CASCADE, and a migration.