Schema YAML format
A schema file describes your tables. The generator turns it into Dart models
(see Generated code) and SQL migrations
(see Migrations API). This page lists everything
the parser accepts. Anything else is rejected with an unknown_key diagnostic.
File naming and discovery
Section titled “File naming and discovery”| Rule | Detail |
|---|---|
| File name | Must end in .gisila.yaml or .gisila.yml. |
| Location | dart run gisila_orm:generate scans lib/, test/ and example/ recursively. |
| Skipped | Anything under a .gisila/ directory and any file named project.gisila.yaml (the generator’s own snapshot). |
| Multiple files | All discovered files merge into one project schema, so models and enums can reference each other across files. |
Top-level shape
Section titled “Top-level shape”The top level is a map. Each key is either the reserved key enums or a model name.
enums: # optional TeamRole: [viewer, developer, admin]
TeamMember: # a model: one table db_table: team_members # optional columns: # required role: type: TeamRole default: developer is_null: false indexes: # optional idx_team_member_role: columns: [role] checks: # optional role_not_owner: expression: "role <> 'owner'"Model names
Section titled “Model names”| Rule | Detail |
|---|---|
| Pattern | ^[A-Za-z_][A-Za-z0-9_]*$ (invalid_model_name otherwise). |
| Convention | PascalCase. A name starting with a lowercase letter raises a naming_convention warning. |
| Uniqueness | A model may be declared once per project (duplicate_model). A name can’t be both a model and an enum (enum_model_collision). |
Model keys
Section titled “Model keys”| Key | Type | Required | Default | Description |
|---|---|---|---|---|
columns |
map | yes | – | Column name → column definition. Missing gives missing_columns. |
db_table |
string | no | plural snake_case of the model name | SQL table name. Must match ^[a-z_][a-z0-9_]*$. A PostgreSQL reserved word (for example user, order) gives reserved_table_name. |
indexes |
map | no | – | Named explicit indexes. See Indexes. |
checks |
map | no | – | Named table-level CHECK constraints. See Checks. |
The default table name is the model name in snake_case, pluralised with simple
English rules: User → users, Category → categories, Box → boxes,
BlogPost → blog_posts.
Implicit primary key
Section titled “Implicit primary key”If no column has is_primary: true, the parser inserts a first column:
id: { type: integer, is_primary: true, is_null: false, is_unique: true, is_index: true }which the SQL emitter writes as "id" BIGSERIAL PRIMARY KEY. Any
integer or bigint primary key, implicit or declared, becomes BIGSERIAL.
Column names
Section titled “Column names”Column names must match ^[a-z_][a-z0-9_]*$. camelCase such as createdAt
is rejected with invalid_column_name. Dart fields use the camelCase form
(created_at → createdAt).
Column keys
Section titled “Column keys”| Key | Type | Default | Applies to | Description |
|---|---|---|---|---|
type |
string | – (required) | all | Column type. See Column types. Missing gives missing_type. |
is_null |
bool | true |
all | false emits NOT NULL and makes the Dart field non-nullable and a required constructor parameter. |
is_unique |
bool | false |
all | Emits UNIQUE. On a foreign key it also makes the inverse relation a has-one instead of a has-many. |
is_index |
bool | false |
all | Emits CREATE INDEX "idx_<table>_<column>". Skipped for primary keys and unique columns (already indexed). On a vector column, builds a pgvector index (see Vector options). |
is_primary |
bool | false |
non-relation columns | Marks the primary key and suppresses the implicit id. Must not be combined with is_null: true. Relationship columns can’t be primary keys. |
allow_blank |
bool | true |
varchar, text, uuid |
false adds a check to the generated validate() method that rejects empty or whitespace-only strings. No SQL is emitted. |
default |
string, number, bool or null | none | all | SQL DEFAULT. See Defaults. |
max_length |
positive int | 255 |
varchar, varchar[] |
Emits VARCHAR(n). Any other type gives invalid_max_length. |
check |
string | none | all | Column CHECK constraint, emitted as CONSTRAINT "<table>_<column>_check" CHECK (<expr>). The expression is copied into the SQL verbatim. |
references |
string | the type value |
relations | Target model name. Required with type: foreign_key. |
reverse_name |
identifier | none | relations | Name of the inverse relation generated on the target model. |
many_to_many |
bool | false |
type: <Model> |
Makes the column a many-to-many relation backed by a junction table. |
on_delete |
string | SET NULL |
foreign keys | NO ACTION, RESTRICT, CASCADE, SET NULL or SET DEFAULT (case-insensitive). |
on_update |
string | CASCADE |
foreign keys | Same values as on_delete. |
dimensions |
positive int | – (required for vectors) | vector |
Vector length: VECTOR(n). |
index_method |
string | hnsw |
vector |
hnsw or ivfflat. Used when is_index: true. |
distance |
string | l2 |
vector |
l2, cosine or ip (also innerProduct). Picks the index opclass. |
Changing is_unique or is_index on a column that already exists isn’t
migrated automatically; see what the differ detects.
Booleans must be real YAML booleans (true/false). A quoted "true" gives invalid_value.
Column types
Section titled “Column types”Type names are case-insensitive for built-ins.
YAML type |
PostgreSQL | Dart field | Notes |
|---|---|---|---|
varchar |
VARCHAR(255) or VARCHAR(n) |
String |
max_length sets n. |
text |
TEXT |
String |
|
integer |
INTEGER |
int |
BIGSERIAL when primary key. |
bigint |
BIGINT |
int |
BIGSERIAL when primary key. |
boolean |
BOOLEAN |
bool |
|
date |
DATE |
DateTime |
|
timestamp |
TIMESTAMP WITH TIME ZONE |
DateTime |
|
decimal |
DECIMAL |
String |
Kept as exact text so money isn’t rounded. Parse it with a decimal package. |
json |
JSONB |
Object |
Any JSON value. Written through encodeJsonColumn. |
uuid |
UUID |
String |
|
vector |
VECTOR(n) |
Vector |
Needs the pgvector extension and dimensions. |
point |
POINT |
Point |
|
box |
BOX |
Box |
|
circle |
CIRCLE |
Circle |
|
lseg |
LSEG |
Lseg |
|
<element>[] |
<element type>[] |
List<T> |
See Arrays. |
<EnumName> |
"<enum_snake_name>" |
the generated Dart enum | Must be declared under enums:. |
<ModelName> |
FK column typed like the target’s primary key | the target PK’s Dart type | A belongs-to relation, or many-to-many with many_to_many: true. |
foreign_key (or ForeignKey) |
FK column typed like the target’s primary key | the target PK’s Dart type | Requires references:. Can’t be combined with many_to_many. |
A PascalCase type that isn’t a built-in or an enum is treated as a model
reference. Anything else is unknown_type, with a “did you mean” hint.
Arrays
Section titled “Arrays”tags: type: varchar[] max_length: 64 # element type becomes VARCHAR(64) default: '{}'| Rule | Detail |
|---|---|
| Allowed elements | varchar, text, integer, bigint, boolean, date, timestamp, decimal, uuid |
| Not allowed | json[], vector[], geometric arrays, enum arrays, nested arrays, relations (invalid_array_element / invalid_relationship) |
| Dart element types | String for varchar/text/uuid/decimal, int for integer/bigint, bool, DateTime for date/timestamp |
| Default | A string array literal such as '{}' or '{a,b}', emitted as '{a,b}'::varchar[] |
enums: ReviewVerdict: - pending - approved - rejected
Review: columns: verdict: type: ReviewVerdict default: pending is_null: false| Rule | Detail |
|---|---|
| Enum name | Identifier; PascalCase recommended. Postgres type name is snake_case (ReviewVerdict → review_verdict). |
| Values | Non-empty list of unique identifiers (invalid_enum_values, invalid_enum_value, duplicate_enum_value). |
| Default | Must be one of the values (invalid_enum_default); emitted as 'pending'::review_verdict. |
| Across files | The same enum may appear in several files only with identical values (duplicate_enum). |
Relations
Section titled “Relations”Post: columns: author: # belongs-to; stored as author_id type: Author reverse_name: posts # generates Author.posts on_delete: CASCADE editor_id: # foreign_key style; stored as editor_id type: foreign_key references: User is_null: true tags: # many-to-many through a junction table type: Tag many_to_many: true reverse_name: posts| Behaviour | Detail |
|---|---|
| Physical column | <name>_id, unless the name already ends in _id. |
| Column type | The target’s actual primary key type. An integer primary key gives BIGINT, a uuid primary key gives UUID. |
| Constraint | Added after all tables: ALTER TABLE ... ADD CONSTRAINT "<table>_<column>_fkey" FOREIGN KEY ... ON DELETE <on_delete> ON UPDATE <on_update>. |
| Dart field | camelCase of the physical column (author → authorId). |
| Relation accessor | camelCase of the YAML name without a trailing _id (author → Post.author, editor_id → Post.editor). |
| Inverse | Only generated when reverse_name is set. Has-many by default, has-one when the FK is is_unique: true. |
reverse_name rules |
Must not collide with a column on the target, and two relations can’t use the same reverse_name on the same target (reverse_name_collision). |
| Unknown target | unknown_reference, with a “did you mean” hint. |
Many-to-many columns produce no column on either table. The generator creates
one junction table per pair, named from the two table names sorted and joined
with _ (books + users → books_users):
CREATE TABLE "books_users" ( "books_id" BIGINT NOT NULL, "users_id" BIGINT NOT NULL, "created_at" TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY ("books_id", "users_id"), FOREIGN KEY ("books_id") REFERENCES "books" ("id") ON DELETE CASCADE, FOREIGN KEY ("users_id") REFERENCES "users" ("id") ON DELETE CASCADE);Vector options
Section titled “Vector options”Document: columns: embedding: type: vector dimensions: 1536 is_index: true index_method: hnsw # or ivfflat distance: cosine # l2 | cosine | ipWith is_index: true this emits
CREATE INDEX "idx_documents_embedding" ON "documents" USING hnsw ("embedding" vector_cosine_ops);.
Any schema with a vector column or vector index starts its up migration with
CREATE EXTENSION IF NOT EXISTS vector;. Using dimensions, index_method
or distance on a non-vector column gives invalid_vector_option.
Defaults
Section titled “Defaults”default accepts a string, number, boolean or null. Lists and maps are rejected.
| Column type | Accepted | Emitted SQL |
|---|---|---|
| any | a function call such as gen_random_uuid(), NOW(), nextval('seq'), myschema.next_id(), optionally with a ::type cast |
verbatim |
boolean |
true / false |
TRUE / FALSE |
integer, bigint |
whole number | the number |
date, timestamp |
now, NOW(), CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME (any case) |
the function; now becomes NOW() |
date, timestamp |
any other string | quoted literal |
varchar, text, uuid, decimal, json |
any other value | quoted literal, with ' doubled |
| enum | one of the enum values | 'value'::enum_type |
| array | '{}' or '{a,b}' |
'{a,b}'::varchar[] (cast per element type) |
| geometric | text form such as '(1,2)' |
'(1,2)'::point |
An invalid combination (for example default: abc on an integer) fails SQL
generation with invalid_default, naming the column.
Indexes
Section titled “Indexes”Review: columns: book: { type: Book } reviewer: { type: User } embedding: { type: vector, dimensions: 384 } indexes: idx_review_book_reviewer: columns: [book, reviewer] # logical names; FKs become book_id, reviewer_id unique: true idx_review_embedding: columns: [embedding] using: ivfflat distance: l2| Key | Type | Default | Description |
|---|---|---|---|
columns |
list of column names | – (required) | Logical YAML names. Foreign keys are rewritten to their _id column. Unknown names give unknown_column. |
unique |
bool | false |
CREATE UNIQUE INDEX. Not allowed with using (invalid_vector_index). |
using |
string | none | hnsw or ivfflat: a pgvector index. Needs exactly one column; a multi-column vector index emits no SQL. |
distance |
string | the column’s distance, else l2 |
Opclass for a pgvector index. |
The index key is used as the SQL index name.
Checks
Section titled “Checks”Order: columns: total: { type: integer, is_null: false } discount: { type: integer, is_null: false, default: 0 } checks: discount_below_total: expression: "discount <= total"| Key | Type | Description |
|---|---|---|
expression |
non-empty string | SQL boolean expression, copied verbatim. Emitted as ALTER TABLE ... ADD CONSTRAINT "<check name>" CHECK (<expression>) after the tables. |
Diagnostics
Section titled “Diagnostics”The parser collects every problem before failing and throws a single
SchemaValidationException.
dart run gisila_orm:generate prints them in this form and exits with code 1:
error[unknown_type]: unknown column type "varchars" --> lib/models/blog.gisila.yaml:5:13 | 5 | type: varchars | ^^^^^^^^ did you mean "varchar"?| Code | Cause |
|---|---|
invalid_yaml, duplicate_key, empty_schema, expected_map, expected_list |
File isn’t a valid schema shape. |
invalid_model_name, duplicate_model, naming_convention, enum_model_collision |
Model naming. |
missing_columns, invalid_db_table, reserved_table_name |
Model keys. |
invalid_column_name, duplicate_column, missing_type, invalid_type, unknown_type, unknown_key, invalid_key, invalid_value |
Column shape. |
invalid_primary_key, invalid_max_length, invalid_check, invalid_default |
Column constraints. |
invalid_relationship, missing_references, invalid_references, unknown_reference, invalid_referential_action, invalid_reverse_name, reverse_name_collision |
Relations. |
invalid_enum_name, duplicate_enum, invalid_enum_values, invalid_enum_value, duplicate_enum_value, invalid_enum_default |
Enums. |
invalid_array_element |
Arrays. |
missing_dimensions, invalid_dimensions, invalid_index_method, invalid_distance, invalid_vector_option, invalid_vector_index |
pgvector. |
invalid_index_name, unknown_column |
Indexes. |
invalid_check_name |
Checks. |
See also: Define a schema, Schema-first design, Generated code.