In Locus, you define your application’s data structure inside database
blocks. You can have database
blocks in any .locus
file; the compiler will intelligently merge them into a single, unified schema for your entire project.
This document serves as a complete reference for all data modeling features.
entity
An entity
is the core of your data model. It represents a table in your database and is defined by a collection of fields and relationships.
database {
entity Product {
// fields and relationships go here
}
}
Fields represent the scalar data in your entity (the columns in your database table).
entity Product {
name: String
stockCount: Integer @default(0)
description: Text?
tags: list of String
}
Locus provides a set of built-in primitive types.
Type | Description |
---|---|
String |
For short text, like names, titles, or emails. Maps to String . |
Text |
For long-form text. Maps to String with a @db.Text attribute. |
Integer |
For whole numbers. Maps to Int . |
Decimal |
For numbers with decimal points. Maps to Decimal . |
Boolean |
For true or false values. Maps to Boolean . |
DateTime |
For storing specific dates and times. Maps to DateTime . |
Json |
For storing arbitrary JSON data. Maps to Json . |
BigInt |
For very large integers. Maps to BigInt . |
Float |
For floating-point numbers. Maps to Float . |
UUID |
For UUID values. Maps to String with UUID validation. |
Email |
For email addresses. Maps to String with email validation. |
URL |
For URLs. Maps to String with URL validation. |
You can also define a field as a list of a primitive type. This is useful for things like tags, roles, or simple arrays of strings or numbers.
entity Article {
tags: list of String
visitorScores: list of Integer
}
Important Rules for List Types:
?
.@default(...)
attribute.You can modify field behavior with annotations (prefixed by @
) or suffix markers.
Optional and nullable are distinct concepts:
Form | Meaning | May be omitted in create/update input? | Can store NULL in DB? |
---|---|---|---|
description: Text? |
Optional (NOT nullable) | Yes | No (future: absence means not written) |
middleName: String | Null |
Nullable (required presence) | No (must appear; value may be null) | Yes |
comment: Text? | Null |
Optional + Nullable | Yes | Yes |
To allow actual NULL storage, include | Null
(or forthcoming nullable
keyword form). For deeper discussion see Nullable vs Optional and Syntax Conventions.
?
(Optional Marker)Applies only to non‑list scalar or relation foreign key fields. Do not use on list types (Tags: String[]?
invalid).
@unique
Ensures every record has a unique value for this field.
email: String @unique
@default(...)
Provides a default value when a record is created.
// Static values
isActive: Boolean @default(true)
role: String @default("user")
stock: Integer @default(0)
// Dynamic values (function calls)
createdAt: DateTime @default(now())
Integer Range: Numeric defaults for
Integer
must be within 32‑bit signed range (−2,147,483,648 .. 2,147,483,647).
Only the following function-style defaults are currently supported:
Function | Description |
---|---|
now() |
Current timestamp |
uuid() |
Random UUID v4 |
cuid() |
Collision-resistant id |
autoincrement() |
Auto-incrementing integer |
If you use a non-whitelisted function (e.g. random()
), validation fails with an error: Unsupported default function 'random' ...
.
@map("...")
Overrides the underlying column name (helpful for legacy schemas or different naming conventions).
// Named 'emailAddress' in code, stored as 'user_email'
emailAddress: String @map("user_email")
Locus supports several validation attributes for enhanced data integrity:
@min(n)
/ @max(n)
Sets minimum and maximum values for numeric fields:
age: Integer @min(0) @max(120)
price: Decimal @min(0.01)
@length(min: n, max: n)
Sets length constraints for string fields:
username: String @length(min: 3, max: 20)
@pattern("regex")
Validates string fields against a regular expression:
phoneNumber: String @pattern("^\\+?[1-9]\\d{1,14}$")
@email
Validates that a string is a valid email address:
email: String @email
@enum([values])
Constrains a field to specific allowed values:
status: String @enum(["draft", "published", "archived"])
@policy("rule")
Applies a security policy to the field:
sensitiveData: String @policy("admin_only")
When multiple annotations are present they are normalized to this order for deterministic generation:
@id
@unique
@default(...)
@map(...)
@policy(...)
Applying this ordering avoids needless diff churn in generated artifacts & snapshots.
Warning:
- List fields cannot be marked optional with
?
; represent absence as an empty list.- List fields cannot have
@default(...)
(future enhancement may allow@default([])
).- A default of
null
on an optional‑only (non‑nullable) field is invalid; either remove the default or mark the field nullable (| Null
).
You define relationships between entities using special keywords.
This is the most common relationship. Use has_many
on one side and belongs_to
on the other.
Key Rule: A belongs_to
relationship requires you to explicitly define the foreign key field. The foreign key field must be named relationNameId
.
database {
entity Author {
// An author can have many posts
posts: has_many Post
}
entity Post {
// A post belongs to one author
author: belongs_to Author
// You MUST define the foreign key field for 'author'
authorId: Integer
}
}
In this example, the author
relation on Post
is linked to the authorId
field.
A one-to-one relationship is created by making a belongs_to
field unique.
database {
entity User {
// A user has one profile
profile: has_one Profile
}
entity Profile {
// A profile belongs to one user, and this link must be unique
user: belongs_to User @unique
// You still need the foreign key field
userId: Integer
}
}
Here, has_one
is used for clarity, but the uniqueness is enforced by the @unique
annotation on the belongs_to
side.
You can specify what happens when the referenced entity is deleted:
entity Post {
author: belongs_to User (onDelete: cascade)
authorId: Integer
}
Supported cascade options:
cascade
: Delete this record when the referenced record is deletedrestrict
: Prevent deletion of the referenced record if this record existsset_null
: Set the foreign key to null when the referenced record is deletedTo create a many-to-many relationship, use has_many
on both sides of the relationship. Locus will automatically create and manage the hidden join table for you.
database {
entity Article {
title: String
// An article can have many categories
categories: has_many Category
}
entity Category {
name: String
// A category can have many articles
articles: has_many Article
}
}
The compiler now distinguishes between optional presence (?
) and nullable value (| Null
). Use this guide when updating legacy schemas:
?
to mean it can store nulls, migrate to Type | Null
.?
for cases where the field may be omitted entirely from inputs.?
and | Null
unless both semantics are desired; the validator will warn on conflicting patterns in future releases.Null
.Example:
// Legacy (still supported)
entity User { middleName: String? }
// Nullable but required
entity User { middleName: String | Null }
// Optional (absent vs present) - note: suffix form
entity User { middleName: String? }
This section is a living migration aid; expect future enhancements with automated quick‑fixes.