A quick tutorial to get you up and running with FluentORM in minutes.
- Zig 0.15.1 or later
- PostgreSQL 12+ running locally or remotely
- Basic understanding of SQL and Zig
mkdir my-app
cd my-app
zig initAdd FluentORM to your project:
zig fetch --save git+https://github.com/SOG-web/fluentorm#mainThis updates build.zig.zon:
.dependencies = .{
.fluentorm = .{
.url = "git+https://github.com/SOG-web/fluentorm#<hash>",
.hash = "<hash>",
},
},Replace the contents of build.zig:
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
// Migration directory option (customizable)
const migrations_dir = b.option([]const u8, "migrations-dir", "Directory containing migration files") orelse "migrations";
// Skip migrations option
const skip_migrations = b.option(bool, "skip-migrations", "Skip generating SQL migrations") orelse false;
// Get FluentORM dependency
const fluentorm_dep = b.dependency("fluentorm", .{
.target = target,
.optimize = optimize,
});
const fluentorm = fluentorm_dep.module("fluentorm");
// Main executable
const exe = b.addExecutable(.{
.name = "my-app",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "fluentorm", .module = fluentorm },
},
}),
});
b.installArtifact(exe);
// Step 1: Generate registry and runner
const gen_step = b.step("generate", "Generate registry and runner");
const gen_exe = fluentorm_dep.artifact("fluentzig-gen");
const gen_cmd = b.addRunArtifact(gen_exe);
gen_cmd.addArgs(&.{ "schemas", "src/models/generated" });
if (!skip_migrations) {
gen_cmd.addArg(migrations_dir);
} else {
gen_cmd.addArg("--skip-migrations");
}
gen_step.dependOn(&gen_cmd.step);
// Step 2: Generate models
const runner_exe = b.addExecutable(.{
.name = "model-runner",
.root_module = b.createModule(.{
.root_source_file = b.path("schemas/runner.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "fluentorm", .module = fluentorm },
},
}),
});
const gen_models_step = b.step("generate-models", "Generate models");
gen_models_step.dependOn(&b.addRunArtifact(runner_exe).step);
// Migration steps
const migrate_exe = fluentorm_dep.artifact("fluent-migrate");
// migrate-up: Run pending migrations
const migrate_up_cmd = b.addRunArtifact(migrate_exe);
migrate_up_cmd.addArgs(&.{ "--migrations-dir", migrations_dir, "up" });
const migrate_up_step = b.step("migrate-up", "Run pending database migrations");
migrate_up_step.dependOn(&migrate_up_cmd.step);
// migrate-status: Show migration status
const migrate_status_cmd = b.addRunArtifact(migrate_exe);
migrate_status_cmd.addArgs(&.{ "--migrations-dir", migrations_dir, "status" });
const migrate_status_step = b.step("migrate-status", "Show database migration status");
migrate_status_step.dependOn(&migrate_status_cmd.step);
// migrate-down: Rollback last migration
const migrate_down_cmd = b.addRunArtifact(migrate_exe);
migrate_down_cmd.addArgs(&.{ "--migrations-dir", migrations_dir, "down" });
const migrate_down_step = b.step("migrate-down", "Rollback last database migration");
migrate_down_step.dependOn(&migrate_down_cmd.step);
// Default migrate step (alias)
const migrate_step = b.step("migrate", "Run database migrations");
migrate_step.dependOn(&migrate_up_cmd.step);
// Run step
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
}Create schemas/01_users.zig:
const fluentorm = @import("fluentorm");
const TableSchema = fluentorm.TableSchema;
pub const table_name = "users";
pub fn build(t: *TableSchema) void {
// Primary key
t.uuid(.{
.name = "id",
.primary_key = true,
.unique = true,
.create_input = .excluded,
.update_input = false,
});
// User fields
t.string(.{ .name = "email", .unique = true });
t.string(.{ .name = "name" });
t.string(.{ .name = "password_hash", .redacted = true });
// Timestamps (adds created_at and updated_at)
t.timestamps();
// Soft deletes (adds deleted_at)
t.softDelete();
}Run the generation commands:
# Generate registry and runner
zig build generate
# Generate model files
zig build generate-modelsTo generate only models without SQL migrations:
# Generate registry and runner (skip migrations)
zig build generate -Dskip-migrations=true
# Generate model files
zig build generate-modelsThis creates:
schemas/registry.zig- Auto-imports your schemasschemas/runner.zig- Code generator runnersrc/models/generated/users.zig- User modelsrc/models/generated/base.zig- Base CRUD operationssrc/models/generated/query.zig- Query buildersrc/models/generated/transaction.zig- Transaction supportsrc/models/generated/root.zig- Barrel exportsmigrations/users.sql- SQL migration file
Create your database and set environment variables:
createdb my_app_db
export FLUENT_DB_HOST=localhost
export FLUENT_DB_PORT=5432
export FLUENT_DB_NAME=my_app_db
export FLUENT_DB_USER=postgres
export FLUENT_DB_PASSWORD=your_passwordRun the migrations using the built-in migration system:
# Run all pending migrations
zig build migrate
# Check migration status
zig build migrate-status
# Rollback if needed
zig build migrate-downThe migration system will:
- Create the necessary database tables
- Track applied migrations in a
_fluent_migrationstable - Verify migration checksums for safety
- Run migrations in transactions
Update src/main.zig:
const std = @import("std");
const pg = @import("pg");
const models = @import("models/generated/root.zig");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Connect to PostgreSQL
var pool = try pg.Pool.init(allocator, .{
.size = 5,
.connect = .{
.host = "localhost",
.port = 5432,
},
.auth = .{
.username = "postgres",
.password = "your_password",
.database = "my_app_db",
},
});
defer pool.deinit();
// Insert a user
const user_id = try models.Users.insert(&pool, allocator, .{
.email = "alice@example.com",
.name = "Alice",
.password_hash = "hashed_password_here",
}).unwrap();
defer allocator.free(user_id);
std.debug.print("Created user with ID: {s}\n", .{user_id});
// Find the user by ID
if (try models.Users.findById(&pool, allocator, user_id).unwrap()) |user| {
std.debug.print("Found user: {s} ({s})\n", .{ user.name, user.email });
defer allocator.free(user);
}
// Query users
var query = models.Users.query();
defer query.deinit();
const users = try query
.where(.{ .field = .email, .operator = .eq, .value = .{ .string = "$1" } })
.fetch(&pool, allocator, .{"alice@example.com"})
.unwrap();
defer allocator.free(users);
std.debug.print("Found {d} user(s)\n", .{users.len});
// Update the user
try models.Users.update(&pool, user_id, .{
.name = "Alice Smith",
}).unwrap();
std.debug.print("User updated!\n", .{});
// Delete the user (soft delete)
try models.Users.softDelete(&pool, user_id).unwrap();
std.debug.print("User soft deleted!\n", .{});
}zig build runExpected output:
Created user with ID: 550e8400-e29b-41d4-a716-446655440000
Found user: Alice (alice@example.com)
Found 1 user(s)
User updated!
User soft deleted!
Create schemas/02_posts.zig:
const fluentorm = @import("fluentorm");
const TableSchema = fluentorm.TableSchema;
pub const table_name = "posts";
pub fn build(t: *TableSchema) void {
t.uuid(.{
.name = "id",
.primary_key = true,
.unique = true,
.create_input = .excluded,
.update_input = false,
});
t.string(.{ .name = "title" });
t.string(.{ .name = "content" });
t.uuid(.{ .name = "user_id" });
// Timestamps (adds created_at and updated_at)
t.timestamps();
// Relationship: post belongs to user
t.belongsTo(.{
.name = "post_author",
.column = "user_id",
.references_table = "users",
.on_delete = .cascade,
});
}And update schemas/01_users.zig to add hasMany:
const fluentorm = @import("fluentorm");
const TableSchema = fluentorm.TableSchema;
pub const table_name = "users";
pub fn build(t: *TableSchema) void {
// ... existing fields ...
// One-to-many: User has many posts
t.hasMany(.{
.name = "user_posts",
.foreign_table = "posts",
.foreign_column = "user_id",
});
}Regenerate:
zig build generate
zig build generate-modelsUse the relationship:
// Using hasMany on Users
const user = (try models.Users.findById(&pool, allocator, user_id).unwrap()).?;
defer allocator.free(user);
// Fetch all posts by this user
const posts = try user.fetchPosts(&pool, allocator).unwrap();
defer allocator.free(posts);
for (posts) |p| {
std.debug.print("Post: {s}\n", .{p.title});
}
// Using belongsTo on Posts
const post = (try models.Posts.findById(&pool, allocator, post_id).unwrap()).?;
defer allocator.free(post);
// Fetch the author
if (try post.fetchPostAuthor(&pool, allocator).unwrap()) |author| {
defer allocator.free(author);
std.debug.print("Post by: {s}\n", .{author.name});
}- Complex Queries: QUERY.md
- Transactions: TRANSACTION.md
- Relationships: RELATIONSHIPS.md
- Field Types: SCHEMA.md
- Migrations: MIGRATIONS.md
Solution: The migration system automatically creates the UUID extension. If it fails, ensure your database user has the necessary permissions.
Solution: Ensure PostgreSQL is running and environment variables are set correctly:
# Test connection
psql -U $FLUENT_DB_USER -d $FLUENT_DB_NAME -h $FLUENT_DB_HOST -p $FLUENT_DB_PORTSolution: Never modify migration files after they've been applied. If you need to change something, create a new migration.
Solution: Check migration status and only run pending migrations:
zig build migrate-status
zig build migrateSolution: Ensure schema files are numbered correctly for dependency order (e.g., 01_users.zig before 02_posts.zig).
After setup, your project should look like:
my-app/
├── build.zig
├── build.zig.zon
├── schemas/
│ ├── 01_users.zig
│ ├── 02_posts.zig
│ ├── registry.zig # Auto-generated
│ └── runner.zig # Auto-generated
├── src/
│ ├── main.zig
│ └── models/
│ └── generated/
│ ├── base.zig
│ ├── query.zig
│ ├── transaction.zig
│ ├── users.zig
│ ├── posts.zig
│ └── root.zig
├── migrations/ # Migration files
│ ├── 1764673549_create_users.sql
│ ├── 1764673549_create_users_down.sql
│ └── ...
└── zig-out/
└── bin/
└── fluent-migrate # Migration runner
You've now:
- ✅ Installed FluentORM
- ✅ Configured your build system
- ✅ Created a schema with the TableSchema builder
- ✅ Generated type-safe models
- ✅ Connected to PostgreSQL
- ✅ Performed CRUD operations
Happy coding with FluentORM! 🚀