Nautilus — Rust-powered ORM CLI
Project description
Nautilus
Nautilus is a Rust-first, Prisma-inspired ORM and SQL query engine.
It provides:
- A Prisma-like schema language (
.nautilusfiles) for defining models and relations - A typed, fluent query API generated automatically from the schema
- A database-agnostic SQL core built on an internal AST
- Multi-database support via pluggable SQL dialects (PostgreSQL, MySQL, SQLite)
- A standalone JSON-RPC engine usable from multiple languages (Python, and more)
- A schema-first workflow with migrations, introspection, and code generation
- An LSP server for first-class editor support
Core Philosophy
- AST-first design — queries are database-agnostic data structures; SQL strings are produced only at the last step by a dialect renderer.
- Prisma-like user API — end users never write SQL or ASTs; they interact with generated model delegates and fluent builders.
- Strict layering — query construction, SQL rendering, and execution are separated and independently extensible.
Crates Index
| Crate | Description |
|---|---|
| nautilus-core | Query AST, expression system, typed column API, and core value types |
| nautilus-dialect | SQL dialect renderers (PostgreSQL, MySQL, SQLite) |
| nautilus-connector | Database executors and Client entry point (via sqlx) |
| nautilus-schema | .nautilus schema language — lexer, parser, validator, IR, formatter |
| nautilus-codegen | Code generator: emits Rust crates and Python clients from schema IR |
| nautilus-migrate | DDL generation, schema diffing, and migration runner |
| nautilus-protocol | JSON-RPC 2.0 wire format definitions for multi-language clients |
| nautilus-engine | Standalone JSON-RPC engine runtime (stdin/stdout transport) |
| nautilus-cli | nautilus CLI — generate, db, migrate, format, engine serve |
| nautilus-lsp | LSP server for .nautilus schema files |
Dependency graph
graph LR
core[nautilus-core]
dialect[nautilus-dialect]
connector[nautilus-connector]
schema[nautilus-schema]
codegen[nautilus-codegen]
migrate[nautilus-migrate]
lsp[nautilus-lsp]
protocol[nautilus-protocol]
engine[nautilus-engine]
cli[nautilus-cli]
dialect --> core
connector --> dialect
connector --> core
codegen --> schema
migrate --> schema
lsp --> schema
cli --> schema
engine --> connector
engine --> protocol
cli --> engine
Installation
Python
pip install nautilus-orm
JavaScript / TypeScript
npm install @y0gm4/nautilus
Rust
cargo install nautilus
CLI (all platforms)
# macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/y0gm4/nautilus/releases/latest/download/nautilus-installer.sh | sh
# Windows
powershell -ExecutionPolicy ByPass -c "irm https://github.com/y0gm4/nautilus/releases/latest/download/nautilus-installer.ps1 | iex"
# Homebrew
brew install y0gm4/nautilus/nautilus
# npm (global)
npm install -g @y0gm4/nautilus
Quick Start
1. Define your schema
// schema.nautilus
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "nautilus-client-py" // or "nautilus-client-rs", "nautilus-client-js"
output = "db"
}
enum Role {
USER
ADMIN
MODERATOR
}
enum OrderStatus {
PENDING
CONFIRMED
SHIPPED
DELIVERED
CANCELLED
}
type Address {
street String
city String
zip String
country String
}
model User {
id Uuid @id @default(uuid())
email String @unique
username VarChar(30) @unique
name String
role Role @default(USER)
bio String?
tags String[]
address Address?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
profile Profile?
orders Order[]
@@index([email], type: Hash)
@@index([createdAt], type: Brin, map: "idx_users_created")
@@map("users")
}
model Profile {
id Int @id @default(autoincrement())
userId Uuid @unique @map("user_id")
avatar String?
website VarChar(255)?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("profiles")
}
model Product {
id BigInt @id @default(autoincrement())
name String
slug VarChar(100) @unique
description String?
price Decimal(10, 2) @check(price > 0)
discount Decimal(5, 2) @default(0)
finalPrice Decimal(10, 2) @computed(price - discount, Stored) @map("final_price")
stock Int @default(0) @check(stock >= 0)
tags String[]
metadata Json?
active Boolean @default(true)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
orderItems OrderItem[]
@@index([tags], type: Gin)
@@index([name, slug])
@@map("products")
}
model Order {
id BigInt @id @default(autoincrement())
userId Uuid @map("user_id")
status OrderStatus @default(PENDING)
totalAmount Decimal(12, 2) @map("total_amount")
note String?
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Restrict)
items OrderItem[]
@@check(totalAmount > 0)
@@index([userId, status])
@@index([createdAt], type: Brin, map: "idx_orders_created")
@@map("orders")
}
model OrderItem {
id BigInt @id @default(autoincrement())
orderId BigInt @map("order_id")
productId BigInt @map("product_id")
quantity Int @check(quantity > 0)
unitPrice Decimal(10, 2) @map("unit_price")
lineTotal Decimal(12, 2) @computed(quantity * unitPrice, Stored) @map("line_total")
order Order @relation(fields: [orderId], references: [id], onDelete: Cascade)
product Product @relation(fields: [productId], references: [id], onDelete: Restrict)
@@unique([orderId, productId])
@@map("order_items")
}
2. Push the schema to the database
nautilus db push
3. Generate the client
nautilus generate
Usage Examples
CRUD Operations
Python
Async context manager:
import asyncio
from db import Nautilus
async def main():
async with Nautilus() as client:
# Create a user with enum, array, and composite type
user = await client.user.create({
"email": "alice@example.com",
"username": "alice",
"name": "Alice Smith",
"role": "ADMIN",
"tags": ["vip", "early-adopter"],
"address": {
"street": "123 Main St",
"city": "Portland",
"zip": "97201",
"country": "US",
},
})
# Find unique by @unique field
found = await client.user.find_unique(where={"email": "alice@example.com"})
# Find many with enum filter
admins = await client.user.find_many(where={"role": "ADMIN"})
# Update — updatedAt is set automatically
updated = await client.user.update(
where={"email": "alice@example.com"},
data={"role": "MODERATOR", "bio": "Hello world"},
)
# Create a product — finalPrice is computed automatically
product = await client.product.create({
"name": "Mechanical Keyboard",
"slug": "mechanical-keyboard",
"price": 149.99,
"discount": 20.00,
# finalPrice = 129.99 (computed: price - discount)
"stock": 50,
"tags": ["electronics", "peripherals"],
"metadata": {"weight_kg": 0.8, "color": "black"},
})
# Delete
await client.user.delete(where={"email": "alice@example.com"})
asyncio.run(main())
Manual connect / disconnect:
client = Nautilus()
await client.connect()
user = await client.user.create({
"email": "alice@example.com",
"username": "alice",
"name": "Alice Smith",
})
await client.disconnect()
Auto-register — call operations directly from model classes:
from db import Nautilus, User, Product
async with Nautilus(auto_register=True) as client:
# No need to go through `client.user` — use User.nautilus directly
user = await User.nautilus.create({"email": "alice@example.com", "username": "alice", "name": "Alice Smith"})
admins = await User.nautilus.find_many(where={"role": "ADMIN"})
products = await Product.nautilus.find_many(where={"active": True})
JavaScript / TypeScript
import { Nautilus } from './db/client';
async function main() {
const client = new Nautilus();
await client.connect();
// Create with enum, array, and composite type
const user = await client.user.create({
data: {
email: 'alice@example.com',
username: 'alice',
name: 'Alice Smith',
role: 'ADMIN',
tags: ['vip', 'early-adopter'],
address: {
street: '123 Main St',
city: 'Portland',
zip: '97201',
country: 'US',
},
},
});
// Find unique
const found = await client.user.findUnique({
where: { email: 'alice@example.com' },
});
// Find many with enum filter
const admins = await client.user.findMany({
where: { role: 'ADMIN' },
});
// Update — updatedAt is set automatically
const updated = await client.user.update({
where: { email: 'alice@example.com' },
data: { role: 'MODERATOR', bio: 'Hello world' },
});
// Create a product — finalPrice is computed automatically
const product = await client.product.create({
data: {
name: 'Mechanical Keyboard',
slug: 'mechanical-keyboard',
price: 149.99,
discount: 20.0,
stock: 50,
tags: ['electronics', 'peripherals'],
metadata: { weight_kg: 0.8, color: 'black' },
},
});
// Delete
await client.user.delete({
where: { email: 'alice@example.com' },
});
await client.disconnect();
}
main();
Rust
use db::client::NautilusClient;
use db::{Role, Address};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = NautilusClient::new().await?;
// Create with enum, array, and composite type
let user = client.user().create(
"alice@example.com".to_string(),
"alice".to_string(),
"Alice Smith".to_string(),
db::CreateUserOptional {
role: Some(Role::Admin),
tags: Some(vec!["vip".into(), "early-adopter".into()]),
address: Some(Address {
street: "123 Main St".into(),
city: "Portland".into(),
zip: "97201".into(),
country: "US".into(),
}),
..Default::default()
},
).await?;
// Find unique
let found = client.user()
.find_unique(serde_json::json!({ "email": "alice@example.com" }))
.await?;
// Find many with enum filter
let admins = client.user()
.find_many(serde_json::json!({ "role": "ADMIN" }))
.await?;
// Update — updatedAt is set automatically
let updated = client.user()
.update(
serde_json::json!({ "email": "alice@example.com" }),
serde_json::json!({ "role": "MODERATOR", "bio": "Hello world" }),
)
.await?;
// Create a product — finalPrice is computed automatically
let product = client.product().create(
"Mechanical Keyboard".to_string(),
"mechanical-keyboard".to_string(),
serde_json::json!({
"price": 149.99,
"discount": 20.0,
"stock": 50,
"tags": ["electronics", "peripherals"],
"metadata": { "weight_kg": 0.8, "color": "black" },
}),
).await?;
// Delete
client.user()
.delete(serde_json::json!({ "email": "alice@example.com" }))
.await?;
Ok(())
}
Transactions
Python
import asyncio
from db import Nautilus
async def main():
async with Nautilus() as client:
# Context-manager style
async with client.transaction() as tx:
user = await tx.user.create({
"email": "bob@example.com",
"username": "bob",
"name": "Bob Jones",
})
order = await tx.order.create({
"userId": user.id,
"status": "CONFIRMED",
"totalAmount": 149.99,
})
await tx.order_item.create({
"orderId": order.id,
"productId": 1,
"quantity": 1,
"unitPrice": 149.99,
# lineTotal = 149.99 (computed: quantity * unitPrice)
})
# auto-committed on exit; rolled back on exception
# Callback style
async def promote(tx):
sender = await tx.user.update(
where={"email": "alice@example.com"},
data={"role": "USER"},
)
receiver = await tx.user.update(
where={"email": "bob@example.com"},
data={"role": "ADMIN"},
)
return sender, receiver
result = await client.transaction(promote, timeout_ms=10000)
asyncio.run(main())
JavaScript / TypeScript
import { Nautilus } from './db/client';
async function main() {
const client = new Nautilus();
await client.connect();
const result = await client.$transaction(async (tx) => {
const user = await tx.user.create({
data: { email: 'bob@example.com', username: 'bob', name: 'Bob Jones' },
});
const order = await tx.order.create({
data: {
userId: user!.id,
status: 'CONFIRMED',
totalAmount: 149.99,
},
});
await tx.orderItem.create({
data: {
orderId: order!.id,
productId: 1,
quantity: 1,
unitPrice: 149.99,
},
});
return order;
});
await client.disconnect();
}
main();
Rust
use db::client::NautilusClient;
use nautilus_connector::transaction::TransactionOptions;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let client = NautilusClient::new().await?;
let order = client.transaction(Default::default(), |tx| Box::pin(async move {
let user = tx.user().create(
"bob@example.com".to_string(),
"bob".to_string(),
"Bob Jones".to_string(),
Default::default(),
).await?;
let order = tx.order().create(serde_json::json!({
"userId": user.id,
"status": "CONFIRMED",
"totalAmount": 149.99,
})).await?;
tx.order_item().create(serde_json::json!({
"orderId": order.id,
"productId": 1,
"quantity": 1,
"unitPrice": 149.99,
})).await?;
Ok(order)
})).await?;
Ok(())
}
How Nautilus Works
When you run any nautilus command, the schema file goes through three stages before anything is generated or executed.
Stage 1 — Lexing
The lexer reads the source character by character and emits a flat sequence of typed tokens. Every token carries its kind and source span (line + column) for error reporting.
Below is the token output for two representative declarations — the Product model (showcasing @check, @computed, @default, arrays, Json, @@index with types) and the OrderItem model (showcasing @@unique, @computed, multi-relation):
Model Ident("Product") LBrace
Ident("id") Ident("BigInt") At Ident("id") At Ident("default") LParen Ident("autoincrement") LParen RParen RParen
Ident("name") Ident("String")
Ident("slug") Ident("VarChar") LParen Number("100") RParen At Ident("unique")
Ident("description") Ident("String") Question
Ident("price") Ident("Decimal") LParen Number("10") Comma Number("2") RParen At Ident("check") LParen Ident("price") Gt Number("0") RParen
Ident("discount") Ident("Decimal") LParen Number("5") Comma Number("2") RParen At Ident("default") LParen Number("0") RParen
Ident("finalPrice") Ident("Decimal") LParen Number("10") Comma Number("2") RParen At Ident("computed") LParen Ident("price") Minus Ident("discount") Comma Ident("Stored") RParen At Ident("map") LParen String("final_price") RParen
Ident("stock") Ident("Int") At Ident("default") LParen Number("0") RParen At Ident("check") LParen Ident("stock") Gte Number("0") RParen
Ident("tags") Ident("String") LBracket RBracket
Ident("metadata") Ident("Json") Question
Ident("active") Ident("Boolean") At Ident("default") LParen Ident("true") RParen
Ident("createdAt") Ident("DateTime") At Ident("default") LParen Ident("now") LParen RParen RParen At Ident("map") LParen String("created_at") RParen
Ident("updatedAt") Ident("DateTime") At Ident("updatedAt") At Ident("map") LParen String("updated_at") RParen
Ident("orderItems") Ident("OrderItem") LBracket RBracket
AtAt Ident("index") LParen LBracket Ident("tags") RBracket Comma Ident("type") Colon Ident("Gin") RParen
AtAt Ident("index") LParen LBracket Ident("name") Comma Ident("slug") RBracket RParen
AtAt Ident("map") LParen String("products") RParen
RBrace
Model Ident("OrderItem") LBrace
Ident("id") Ident("BigInt") At Ident("id") At Ident("default") LParen Ident("autoincrement") LParen RParen RParen
Ident("orderId") Ident("BigInt") At Ident("map") LParen String("order_id") RParen
Ident("productId") Ident("BigInt") At Ident("map") LParen String("product_id") RParen
Ident("quantity") Ident("Int") At Ident("check") LParen Ident("quantity") Gt Number("0") RParen
Ident("unitPrice") Ident("Decimal") LParen Number("10") Comma Number("2") RParen At Ident("map") LParen String("unit_price") RParen
Ident("lineTotal") Ident("Decimal") LParen Number("12") Comma Number("2") RParen At Ident("computed") LParen Ident("quantity") Star Ident("unitPrice") Comma Ident("Stored") RParen At Ident("map") LParen String("line_total") RParen
Ident("order") Ident("Order") At Ident("relation") LParen Ident("fields") Colon LBracket Ident("orderId") RBracket Comma Ident("references") Colon LBracket Ident("id") RBracket Comma Ident("onDelete") Colon Ident("Cascade") RParen
Ident("product") Ident("Product") At Ident("relation") LParen Ident("fields") Colon LBracket Ident("productId") RBracket Comma Ident("references") Colon LBracket Ident("id") RBracket Comma Ident("onDelete") Colon Ident("Restrict") RParen
AtAt Ident("unique") LParen LBracket Ident("orderId") Comma Ident("productId") RBracket RParen
AtAt Ident("map") LParen String("order_items") RParen
RBrace
Enum Ident("Role") LBrace
Ident("USER")
Ident("ADMIN")
Ident("MODERATOR")
RBrace
Enum Ident("OrderStatus") LBrace
Ident("PENDING")
Ident("CONFIRMED")
Ident("SHIPPED")
Ident("DELIVERED")
Ident("CANCELLED")
RBrace
Type Ident("Address") LBrace
Ident("street") Ident("String")
Ident("city") Ident("String")
Ident("zip") Ident("String")
Ident("country") Ident("String")
RBrace
Stage 2 — Parsing -> AST
The recursive-descent parser consumes the token stream and builds a concrete syntax tree. The AST is structural only — it does not resolve references, validate types, or interpret attributes:
Schema {
declarations: [
DatasourceDecl {
name: "db",
fields: [
ConfigField { key: "provider", value: StringLit("postgresql") },
ConfigField { key: "url", value: FuncCall("env", ["DATABASE_URL"]) },
],
},
GeneratorDecl {
name: "client",
fields: [
ConfigField { key: "provider", value: StringLit("nautilus-client-py") },
ConfigField { key: "output", value: StringLit("db") },
],
},
EnumDecl {
name: "Role",
variants: ["USER", "ADMIN", "MODERATOR"],
},
EnumDecl {
name: "OrderStatus",
variants: ["PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED"],
},
TypeDecl {
name: "Address",
fields: [
FieldDecl { name: "street", ty: FieldType::Named("String"), attrs: [] },
FieldDecl { name: "city", ty: FieldType::Named("String"), attrs: [] },
FieldDecl { name: "zip", ty: FieldType::Named("String"), attrs: [] },
FieldDecl { name: "country", ty: FieldType::Named("String"), attrs: [] },
],
},
ModelDecl {
name: "User",
fields: [
FieldDecl { name: "id", ty: FieldType::Named("Uuid"), attrs: [@id, @default(uuid())] },
FieldDecl { name: "email", ty: FieldType::Named("String"), attrs: [@unique] },
FieldDecl { name: "username", ty: FieldType::Named("VarChar(30)"),attrs: [@unique] },
FieldDecl { name: "name", ty: FieldType::Named("String"), attrs: [] },
FieldDecl { name: "role", ty: FieldType::Named("Role"), attrs: [@default(USER)] },
FieldDecl { name: "bio", ty: FieldType::Optional("String"), attrs: [] },
FieldDecl { name: "tags", ty: FieldType::List("String"), attrs: [] },
FieldDecl { name: "address", ty: FieldType::Optional("Address"), attrs: [] },
FieldDecl { name: "createdAt", ty: FieldType::Named("DateTime"), attrs: [@default(now()), @map("created_at")] },
FieldDecl { name: "updatedAt", ty: FieldType::Named("DateTime"), attrs: [@updatedAt, @map("updated_at")] },
FieldDecl { name: "profile", ty: FieldType::Optional("Profile"), attrs: [] },
FieldDecl { name: "orders", ty: FieldType::List("Order"), attrs: [] },
],
attrs: [
@@index([email], type: Hash),
@@index([createdAt], type: Brin, map: "idx_users_created"),
@@map("users"),
],
},
ModelDecl {
name: "Profile",
fields: [
FieldDecl { name: "id", ty: FieldType::Named("Int"), attrs: [@id, @default(autoincrement())] },
FieldDecl { name: "userId", ty: FieldType::Named("Uuid"), attrs: [@unique, @map("user_id")] },
FieldDecl { name: "avatar", ty: FieldType::Optional("String"), attrs: [] },
FieldDecl { name: "website", ty: FieldType::Optional("VarChar(255)"), attrs: [] },
FieldDecl { name: "user", ty: FieldType::Named("User"), attrs: [@relation(fields:[userId], references:[id], onDelete:Cascade)] },
],
attrs: [@@map("profiles")],
},
ModelDecl {
name: "Product",
fields: [
FieldDecl { name: "id", ty: FieldType::Named("BigInt"), attrs: [@id, @default(autoincrement())] },
FieldDecl { name: "name", ty: FieldType::Named("String"), attrs: [] },
FieldDecl { name: "slug", ty: FieldType::Named("VarChar(100)"), attrs: [@unique] },
FieldDecl { name: "description", ty: FieldType::Optional("String"), attrs: [] },
FieldDecl { name: "price", ty: FieldType::Named("Decimal(10,2)"), attrs: [@check(price > 0)] },
FieldDecl { name: "discount", ty: FieldType::Named("Decimal(5,2)"), attrs: [@default(0)] },
FieldDecl { name: "finalPrice", ty: FieldType::Named("Decimal(10,2)"), attrs: [@computed(price - discount, Stored), @map("final_price")] },
FieldDecl { name: "stock", ty: FieldType::Named("Int"), attrs: [@default(0), @check(stock >= 0)] },
FieldDecl { name: "tags", ty: FieldType::List("String"), attrs: [] },
FieldDecl { name: "metadata", ty: FieldType::Optional("Json"), attrs: [] },
FieldDecl { name: "active", ty: FieldType::Named("Boolean"), attrs: [@default(true)] },
FieldDecl { name: "createdAt", ty: FieldType::Named("DateTime"), attrs: [@default(now()), @map("created_at")] },
FieldDecl { name: "updatedAt", ty: FieldType::Named("DateTime"), attrs: [@updatedAt, @map("updated_at")] },
FieldDecl { name: "orderItems", ty: FieldType::List("OrderItem"), attrs: [] },
],
attrs: [
@@index([tags], type: Gin),
@@index([name, slug]),
@@map("products"),
],
},
ModelDecl {
name: "Order",
fields: [
FieldDecl { name: "id", ty: FieldType::Named("BigInt"), attrs: [@id, @default(autoincrement())] },
FieldDecl { name: "userId", ty: FieldType::Named("Uuid"), attrs: [@map("user_id")] },
FieldDecl { name: "status", ty: FieldType::Named("OrderStatus"), attrs: [@default(PENDING)] },
FieldDecl { name: "totalAmount", ty: FieldType::Named("Decimal(12,2)"), attrs: [@map("total_amount")] },
FieldDecl { name: "note", ty: FieldType::Optional("String"), attrs: [] },
FieldDecl { name: "createdAt", ty: FieldType::Named("DateTime"), attrs: [@default(now()), @map("created_at")] },
FieldDecl { name: "updatedAt", ty: FieldType::Named("DateTime"), attrs: [@updatedAt, @map("updated_at")] },
FieldDecl { name: "user", ty: FieldType::Named("User"), attrs: [@relation(fields:[userId], references:[id], onDelete:Restrict)] },
FieldDecl { name: "items", ty: FieldType::List("OrderItem"), attrs: [] },
],
attrs: [
@@check(totalAmount > 0),
@@index([userId, status]),
@@index([createdAt], type: Brin, map: "idx_orders_created"),
@@map("orders"),
],
},
ModelDecl {
name: "OrderItem",
fields: [
FieldDecl { name: "id", ty: FieldType::Named("BigInt"), attrs: [@id, @default(autoincrement())] },
FieldDecl { name: "orderId", ty: FieldType::Named("BigInt"), attrs: [@map("order_id")] },
FieldDecl { name: "productId", ty: FieldType::Named("BigInt"), attrs: [@map("product_id")] },
FieldDecl { name: "quantity", ty: FieldType::Named("Int"), attrs: [@check(quantity > 0)] },
FieldDecl { name: "unitPrice", ty: FieldType::Named("Decimal(10,2)"), attrs: [@map("unit_price")] },
FieldDecl { name: "lineTotal", ty: FieldType::Named("Decimal(12,2)"), attrs: [@computed(quantity * unitPrice, Stored), @map("line_total")] },
FieldDecl { name: "order", ty: FieldType::Named("Order"), attrs: [@relation(fields:[orderId], references:[id], onDelete:Cascade)] },
FieldDecl { name: "product", ty: FieldType::Named("Product"), attrs: [@relation(fields:[productId], references:[id], onDelete:Restrict)] },
],
attrs: [
@@unique([orderId, productId]),
@@map("order_items"),
],
},
]
}
Stage 3 — Validation -> IR
validate_schema() walks the AST, resolves all type references, expands attributes, and produces a SchemaIr — the single source of truth used by both codegen and the query engine:
SchemaIr {
datasource: DatasourceIr {
name: "db",
provider: Postgres,
url: EnvVar("DATABASE_URL"),
},
generator: GeneratorIr {
name: "client",
provider: PythonClient,
output: "db",
options: {},
},
models: {
"User": ModelIr {
logical_name: "User",
db_name: "users",
primary_key: PrimaryKeyIr { fields: ["id"] },
unique_constraints: [["email"], ["username"]],
indexes: [
IndexIr { fields: ["email"], type: Hash, map: None },
IndexIr { fields: ["createdAt"], type: Brin, map: Some("idx_users_created") },
],
check_constraints: [],
fields: [
FieldIr { logical_name: "id", db_name: "id", field_type: Scalar(Uuid), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "email", db_name: "email", field_type: Scalar(String), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "username", db_name: "username", field_type: Scalar(VarChar(30)), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "name", db_name: "name", field_type: Scalar(String), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "role", db_name: "role", field_type: Enum("Role"), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "bio", db_name: "bio", field_type: Scalar(String), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "tags", db_name: "tags", field_type: Scalar(String), is_required: true, is_array: true, has_default: false },
FieldIr { logical_name: "address", db_name: "address", field_type: Composite("Address"), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "createdAt", db_name: "created_at", field_type: Scalar(DateTime), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "updatedAt", db_name: "updated_at", field_type: Scalar(DateTime), is_required: true, is_array: false, has_default: true, updated_at: true },
FieldIr { logical_name: "profile", db_name: "-", field_type: Relation(-> Profile), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "orders", db_name: "-", field_type: Relation(-> Order), is_required: false, is_array: true, has_default: false },
],
},
"Profile": ModelIr {
logical_name: "Profile",
db_name: "profiles",
primary_key: PrimaryKeyIr { fields: ["id"] },
unique_constraints: [["userId"]],
indexes: [],
check_constraints: [],
fields: [
FieldIr { logical_name: "id", db_name: "id", field_type: Scalar(Int), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "userId", db_name: "user_id", field_type: Scalar(Uuid), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "avatar", db_name: "avatar", field_type: Scalar(String), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "website", db_name: "website", field_type: Scalar(VarChar(255)), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "user", db_name: "-", field_type: Relation(-> User, fk: user_id -> id, onDelete: Cascade), is_required: true, is_array: false },
],
},
"Product": ModelIr {
logical_name: "Product",
db_name: "products",
primary_key: PrimaryKeyIr { fields: ["id"] },
unique_constraints: [["slug"]],
indexes: [
IndexIr { fields: ["tags"], type: Gin, map: None },
IndexIr { fields: ["name", "slug"], type: BTree, map: None },
],
check_constraints: [
CheckIr { expr: "price > 0", scope: Field("price") },
CheckIr { expr: "stock >= 0", scope: Field("stock") },
],
fields: [
FieldIr { logical_name: "id", db_name: "id", field_type: Scalar(BigInt), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "name", db_name: "name", field_type: Scalar(String), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "slug", db_name: "slug", field_type: Scalar(VarChar(100)), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "description", db_name: "description", field_type: Scalar(String), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "price", db_name: "price", field_type: Scalar(Decimal(10,2)), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "discount", db_name: "discount", field_type: Scalar(Decimal(5,2)), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "finalPrice", db_name: "final_price", field_type: Scalar(Decimal(10,2)), is_required: true, is_array: false, has_default: false, computed: Computed { expr: "price - discount", kind: Stored } },
FieldIr { logical_name: "stock", db_name: "stock", field_type: Scalar(Int), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "tags", db_name: "tags", field_type: Scalar(String), is_required: true, is_array: true, has_default: false },
FieldIr { logical_name: "metadata", db_name: "metadata", field_type: Scalar(Json), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "active", db_name: "active", field_type: Scalar(Boolean), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "createdAt", db_name: "created_at", field_type: Scalar(DateTime), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "updatedAt", db_name: "updated_at", field_type: Scalar(DateTime), is_required: true, is_array: false, has_default: true, updated_at: true },
FieldIr { logical_name: "orderItems", db_name: "-", field_type: Relation(-> OrderItem), is_required: false, is_array: true, has_default: false },
],
},
"Order": ModelIr {
logical_name: "Order",
db_name: "orders",
primary_key: PrimaryKeyIr { fields: ["id"] },
unique_constraints: [],
indexes: [
IndexIr { fields: ["userId", "status"], type: BTree, map: None },
IndexIr { fields: ["createdAt"], type: Brin, map: Some("idx_orders_created") },
],
check_constraints: [
CheckIr { expr: "total_amount > 0", scope: Table },
],
fields: [
FieldIr { logical_name: "id", db_name: "id", field_type: Scalar(BigInt), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "userId", db_name: "user_id", field_type: Scalar(Uuid), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "status", db_name: "status", field_type: Enum("OrderStatus"), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "totalAmount", db_name: "total_amount", field_type: Scalar(Decimal(12,2)), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "note", db_name: "note", field_type: Scalar(String), is_required: false, is_array: false, has_default: false },
FieldIr { logical_name: "createdAt", db_name: "created_at", field_type: Scalar(DateTime), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "updatedAt", db_name: "updated_at", field_type: Scalar(DateTime), is_required: true, is_array: false, has_default: true, updated_at: true },
FieldIr { logical_name: "user", db_name: "-", field_type: Relation(-> User, fk: user_id -> id, onDelete: Restrict), is_required: true, is_array: false },
FieldIr { logical_name: "items", db_name: "-", field_type: Relation(-> OrderItem), is_required: false, is_array: true, has_default: false },
],
},
"OrderItem": ModelIr {
logical_name: "OrderItem",
db_name: "order_items",
primary_key: PrimaryKeyIr { fields: ["id"] },
unique_constraints: [["orderId", "productId"]],
indexes: [],
check_constraints: [
CheckIr { expr: "quantity > 0", scope: Field("quantity") },
],
fields: [
FieldIr { logical_name: "id", db_name: "id", field_type: Scalar(BigInt), is_required: true, is_array: false, has_default: true },
FieldIr { logical_name: "orderId", db_name: "order_id", field_type: Scalar(BigInt), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "productId", db_name: "product_id", field_type: Scalar(BigInt), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "quantity", db_name: "quantity", field_type: Scalar(Int), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "unitPrice", db_name: "unit_price", field_type: Scalar(Decimal(10,2)), is_required: true, is_array: false, has_default: false },
FieldIr { logical_name: "lineTotal", db_name: "line_total", field_type: Scalar(Decimal(12,2)), is_required: true, is_array: false, has_default: false, computed: Computed { expr: "quantity * unit_price", kind: Stored } },
FieldIr { logical_name: "order", db_name: "-", field_type: Relation(-> Order, fk: order_id -> id, onDelete: Cascade), is_required: true, is_array: false },
FieldIr { logical_name: "product", db_name: "-", field_type: Relation(-> Product, fk: product_id -> id, onDelete: Restrict), is_required: true, is_array: false },
],
},
},
enums: {
"Role": EnumIr {
logical_name: "Role",
variants: ["USER", "ADMIN", "MODERATOR"],
},
"OrderStatus": EnumIr {
logical_name: "OrderStatus",
variants: ["PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED"],
},
},
types: {
"Address": CompositeTypeIr {
logical_name: "Address",
fields: [
FieldIr { name: "street", field_type: Scalar(String), is_required: true },
FieldIr { name: "city", field_type: Scalar(String), is_required: true },
FieldIr { name: "zip", field_type: Scalar(String), is_required: true },
FieldIr { name: "country", field_type: Scalar(String), is_required: true },
],
},
},
}
Key differences from the raw AST:
datasourceandgeneratorare fully parsed into typed structs (DatasourceIr,GeneratorIr) with theenv()call resolved toEnvVar@@map/@mapare expanded: every node carries bothlogical_name(schema name) anddb_name(physical table/column name)- Every
@relationis resolved into a typedRelationwith its foreign key column, target model, and cascade rule - Virtual relation fields (
orders Order[],user User) carrydb_name: "-"because they have no physical column - All type references (
Role,OrderStatus,Address,Profile,Order) are replaced by resolvedResolvedFieldTypevariants @computedfields carry the SQL expression and storage kind (Stored/Virtual)@checkconstraints are collected per model, distinguishing field-level vs table-level scope@updatedAtfields are flagged withupdated_at: truefor engine-side auto-timestamping- Indexes carry their type (
Hash,Gin,Brin,BTree) and optional physical name override - Composite types (
Address) are resolved intoCompositeTypeIrwith their own field list
The SchemaIr is consumed by two independent systems: codegen (at build time, to emit client code) and the query engine (at runtime, to execute queries).
Example: find_first end-to-end
Here is a concrete walkthrough of what happens when the generated Python client executes a simple query.
Python call:
async with Nautilus() as client:
product = await client.product.find_first(where={"active": True, "stock": {"gt": 0}})
1. Client -> Engine (JSON-RPC over stdin)
The generated delegate serializes the call to a JSON-RPC 2.0 request and writes it line-delimited to the engine's stdin:
{
"jsonrpc": "2.0",
"id": 1,
"method": "query.findMany",
"params": {
"protocolVersion": 2,
"model": "Product",
"args": {
"where": { "active": true, "stock": { "gt": 0 } },
"take": 1
}
}
}
find_first is implemented by calling find_many with take: 1 — the engine has no separate findFirst path beyond forcing the limit.
2. Engine: routing and query building
The engine reads the line, parses it as an RpcRequest, and dispatches to handle_find_many. Using the Product ModelIr from the SchemaIr, it:
- Resolves
"active"(logical name) ->"active"(db column — same here) - Resolves
"stock"(logical name) ->"stock"(db column — same here) - Includes
final_price(computed stored column) in the SELECT — it is read like any other column - Builds a database-agnostic
SelectAST node:
Select {
table: "products",
columns: [
"products__id", "products__name", "products__slug", "products__description",
"products__price", "products__discount", "products__final_price",
"products__stock", "products__tags", "products__metadata",
"products__active", "products__created_at", "products__updated_at",
],
filter: And(
Eq(Column("products", "active"), Value::Boolean(true)),
Gt(Column("products", "stock"), Value::Int(0)),
),
limit: Some(1),
}
3. Engine: SQL rendering
The dialect renderer (PostgreSQL in this case) turns the Select AST into a parameterized SQL string:
SELECT
products.id AS "products__id",
products.name AS "products__name",
products.slug AS "products__slug",
products.description AS "products__description",
products.price AS "products__price",
products.discount AS "products__discount",
products.final_price AS "products__final_price",
products.stock AS "products__stock",
products.tags AS "products__tags",
products.metadata AS "products__metadata",
products.active AS "products__active",
products.created_at AS "products__created_at",
products.updated_at AS "products__updated_at"
FROM products
WHERE products.active = $1 AND products.stock > $2
LIMIT 1
params: [true, 0]
4. Engine: execution and response
The connector executes the query against PostgreSQL and the engine writes the result back to stdout:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"data": [
{
"products__id": 1,
"products__name": "Mechanical Keyboard",
"products__slug": "mechanical-keyboard",
"products__description": null,
"products__price": "149.99",
"products__discount": "20.00",
"products__final_price": "129.99",
"products__stock": 50,
"products__tags": ["electronics", "peripherals"],
"products__metadata": {"weight_kg": 0.8, "color": "black"},
"products__active": true,
"products__created_at": "2024-01-15T10:30:00Z",
"products__updated_at": "2024-01-15T10:30:00Z"
}
]
}
}
5. Client: deserialization
The Python client resolves the pending future for id: 1, maps the namespaced column keys back to field names, and constructs a Pydantic model instance:
Product(
id=1,
name="Mechanical Keyboard",
slug="mechanical-keyboard",
description=None,
price=Decimal("149.99"),
discount=Decimal("20.00"),
final_price=Decimal("129.99"), # computed column — read-only
stock=50,
tags=["electronics", "peripherals"],
metadata={"weight_kg": 0.8, "color": "black"},
active=True,
created_at=datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc),
updated_at=datetime(2024, 1, 15, 10, 30, 0, tzinfo=timezone.utc),
)
find_first returns this instance, or None if data was empty.
Code Generation
Rust output
Running nautilus generate on the schema above emits something like:
// src/db/models.rs (generated)
#[derive(Debug)]
pub struct User {
pub id: uuid::Uuid,
pub email: String,
pub username: String,
pub name: String,
pub role: Role,
pub bio: Option<String>,
pub tags: Vec<String>,
pub address: Option<Address>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug)]
pub struct Profile {
pub id: i32,
pub user_id: uuid::Uuid,
pub avatar: Option<String>,
pub website: Option<String>,
}
#[derive(Debug)]
pub struct Product {
pub id: i64,
pub name: String,
pub slug: String,
pub description: Option<String>,
pub price: rust_decimal::Decimal,
pub discount: rust_decimal::Decimal,
pub final_price: rust_decimal::Decimal, // computed — read-only
pub stock: i32,
pub tags: Vec<String>,
pub metadata: Option<serde_json::Value>,
pub active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug)]
pub struct Order {
pub id: i64,
pub user_id: uuid::Uuid,
pub status: OrderStatus,
pub total_amount: rust_decimal::Decimal,
pub note: Option<String>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug)]
pub struct OrderItem {
pub id: i64,
pub order_id: i64,
pub product_id: i64,
pub quantity: i32,
pub unit_price: rust_decimal::Decimal,
pub line_total: rust_decimal::Decimal, // computed — read-only
}
#[derive(Debug)]
pub struct Address {
pub street: String,
pub city: String,
pub zip: String,
pub country: String,
}
#[derive(Debug)]
pub enum Role { User, Admin, Moderator }
#[derive(Debug)]
pub enum OrderStatus { Pending, Confirmed, Shipped, Delivered, Cancelled }
Rust workspace integration
By default (provider = "nautilus-client-rs"), nautilus generate writes bare Rust source files into the output directory without a Cargo.toml:
src/db/ <- output = "src/db" in schema
├── src/
│ ├── lib.rs <- module declarations + re-exports
│ ├── user.rs <- generated model code
│ ├── profile.rs
│ ├── product.rs
│ ├── order.rs
│ ├── order_item.rs
│ ├── types.rs <- composite types (Address)
│ └── enums.rs <- enum types (Role, OrderStatus)
There are two ways to bring this crate into your workspace:
Option A — manual integration (default)
Add the output directory to [workspace] members in your root Cargo.toml, then declare a path-dependency in your application crate:
# Cargo.toml (workspace root)
[workspace]
members = [
"src/db", # <- add this
"my-app",
]
# my-app/Cargo.toml
[dependencies]
nautilus-client = { path = "../src/db" }
Option B — standalone mode
Pass --standalone to also emit a Cargo.toml inside the output directory. The generated manifest uses path-dependencies pointing back to the Nautilus source tree:
nautilus generate --standalone
# src/db/Cargo.toml (generated)
[package]
name = "nautilus-client"
version = "0.1.0"
edition = "2021"
[dependencies]
nautilus-core = { path = "../../crates/nautilus-core" }
nautilus-connector = { path = "../../crates/nautilus-connector" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
...
Option C — auto-install
Pass --install to automatically register the generated crate in the nearest workspace Cargo.toml that Nautilus can locate by walking up from the schema file:
nautilus generate --install
# -> adds "src/db" to [workspace] members in Cargo.toml automatically
--standalone and --install can be combined.
Python output
nautilus generate also supports a Python target (set provider = "nautilus-client-py" in the generator block).
The generated package includes Pydantic models and an async/sync (set interface = "async" in the generator block, by default is sync) client backed by the Nautilus engine process.
CLI Reference
nautilus generate [schema] Parse, validate, generate client code
nautilus validate [schema] Parse and validate only
nautilus format [schema] Reformat schema in canonical style
nautilus db push Diff local schema vs live DB and apply changes
nautilus db status Show pending changes without applying
nautilus db pull Introspect live DB → emit .nautilus schema
nautilus db reset Drop all tables and re-push schema
nautilus db seed <file> Execute SQL seed script
nautilus migrate generate Create a versioned migration from current diff
nautilus migrate apply Apply all pending migrations
nautilus migrate rollback Roll back the last migration
nautilus migrate status Show applied / pending migration status
nautilus engine serve Start the JSON-RPC engine (used by client libraries)
Architecture Overview
graph TD
api[User API — generated Rust / Python client]
delegate[Model Delegate + Query Builder]
ast[Query AST — nautilus-core, database-agnostic]
dialect[Dialect Renderer — nautilus-dialect — Postgres / MySQL / SQLite]
sql["Sql { text, params }"]
executor[Executor — nautilus-connector — sqlx-backed, async]
rows[Rows / Typed Models]
api --> delegate --> ast --> dialect --> sql --> executor --> rows
For multi-language clients the path goes through the engine:
graph TD
client[Python / JS client]
engine[nautilus-engine]
connector[nautilus-connector]
db[(database)]
client -->|JSON-RPC over stdin/stdout| engine
engine --> connector --> db
Editor Support
nautilus-lsp is a standalone LSP server providing diagnostics, completions, hover info, and go-to-definition for *.nautilus schema files.
VS Code
- Download the
.vsixfrom the latest release. - Extensions → ⋯ → Install from VSIX… and select the file.
- The extension auto-downloads the
nautilus-lspbinary on first activation.
To override the binary path (Settings → Extensions → Nautilus):
{ "nautilus.lspPath": "/path/to/nautilus-lsp" }
Neovim (via nvim-lspconfig)
local lspconfig = require("lspconfig")
local configs = require("lspconfig.configs")
if not configs.nautilus_lsp then
configs.nautilus_lsp = {
default_config = {
cmd = { "nautilus-lsp" },
filetypes = { "nautilus" },
root_dir = lspconfig.util.root_pattern("*.nautilus", ".git"),
},
}
end
lspconfig.nautilus_lsp.setup {}
Add file-type detection (e.g. ~/.config/nvim/ftdetect/nautilus.vim):
au BufRead,BufNewFile *.nautilus set filetype=nautilus
Helix
# ~/.config/helix/languages.toml
[[language]]
name = "nautilus"
scope = "source.nautilus"
file-types = ["nautilus"]
roots = []
comment-token = "//"
language-servers = ["nautilus-lsp"]
[language-server.nautilus-lsp]
command = "nautilus-lsp"
Pre-built binaries
| Platform | Asset |
|---|---|
| Linux x86_64 | nautilus-lsp-x86_64-unknown-linux-gnu |
| macOS x86_64 | nautilus-lsp-x86_64-apple-darwin |
| macOS Apple Silicon | nautilus-lsp-aarch64-apple-darwin |
| Windows x86_64 | nautilus-lsp-x86_64-pc-windows-msvc.exe |
License
Dual-licensed under MIT and Apache 2.0.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nautilus_orm-0.1.3-py3-none-any.whl.
File metadata
- Download URL: nautilus_orm-0.1.3-py3-none-any.whl
- Upload date:
- Size: 14.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b2494093ed95ca60a4254e3ddb1ea708b620c8f570ece1b5458dbde82781ccfb
|
|
| MD5 |
563ecc6557e6b9231b278bd9a401d539
|
|
| BLAKE2b-256 |
8e6ee6726ce485e7837d415c37f850169ef47f9f054d7c2cf1d0ac9636c5d907
|