Metadata-Version: 2.4
Name: nautilus-orm
Version: 0.1.2
Summary: Nautilus — Rust-powered ORM CLI
Project-URL: Repository, https://github.com/y0gm4/nautilus
License: MIT OR Apache-2.0
Requires-Python: >=3.9
Requires-Dist: nautilus-orm-darwin-arm64; sys_platform == 'darwin' and platform_machine == 'arm64'
Requires-Dist: nautilus-orm-darwin-x86-64; sys_platform == 'darwin' and platform_machine == 'x86_64'
Requires-Dist: nautilus-orm-linux-aarch64; sys_platform == 'linux' and platform_machine == 'aarch64'
Requires-Dist: nautilus-orm-linux-x86-64; sys_platform == 'linux' and platform_machine == 'x86_64'
Requires-Dist: nautilus-orm-win-amd64; sys_platform == 'win32'
Description-Content-Type: text/markdown

# Nautilus

[![CI](https://github.com/y0gm4/nautilus/actions/workflows/ci.yml/badge.svg)](https://github.com/y0gm4/nautilus/actions/workflows/ci.yml)

Nautilus is a **Rust-first, Prisma-inspired ORM and SQL query engine**.

It provides:
- A **Prisma-like schema language** (`.nautilus` files) 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

1. **AST-first design** — queries are database-agnostic data structures; SQL strings are produced only at the last step by a dialect renderer.
2. **Prisma-like user API** — end users never write SQL or ASTs; they interact with generated model delegates and fluent builders.
3. **Strict layering** — query construction, SQL rendering, and execution are separated and independently extensible.

---

## Crates Index

| Crate | Description |
|---|---|
| [nautilus-core](crates/nautilus-core/) | Query AST, expression system, typed column API, and core value types |
| [nautilus-dialect](crates/nautilus-dialect/) | SQL dialect renderers (PostgreSQL, MySQL, SQLite) |
| [nautilus-connector](crates/nautilus-connector/) | Database executors and `Client` entry point (via sqlx) |
| [nautilus-schema](crates/nautilus-schema/) | `.nautilus` schema language — lexer, parser, validator, IR, formatter |
| [nautilus-codegen](crates/nautilus-codegen/) | Code generator: emits Rust crates and Python clients from schema IR |
| [nautilus-migrate](crates/nautilus-migrate/) | DDL generation, schema diffing, and migration runner |
| [nautilus-protocol](crates/nautilus-protocol/) | JSON-RPC 2.0 wire format definitions for multi-language clients |
| [nautilus-engine](crates/nautilus-engine/) | Standalone JSON-RPC engine runtime (stdin/stdout transport) |
| [nautilus-cli](crates/nautilus-cli/) | `nautilus` CLI — `generate`, `db`, `migrate`, `format`, `engine serve` |
| [nautilus-lsp](crates/nautilus-lsp/) | LSP server for `.nautilus` schema files |

### Dependency graph

```mermaid
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

```bash
pip install nautilus-orm
```

### JavaScript / TypeScript

```bash
npm install @y0gm4/nautilus
```

### Rust

```toml
cargo install nautilus
```

### CLI (all platforms)

```bash
# 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

```prisma
// 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

```bash
nautilus db push
```

### 3. Generate the client

```bash
nautilus generate
```

---

## Usage Examples

### CRUD Operations

#### Python

**Async context manager**:

```python
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:**

```python
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:**

```python
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

```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

```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

```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

```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

```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:
- `datasource` and `generator` are fully parsed into typed structs (`DatasourceIr`, `GeneratorIr`) with the `env()` call resolved to `EnvVar`
- `@@map` / `@map` are expanded: every node carries both `logical_name` (schema name) and `db_name` (physical table/column name)
- Every `@relation` is resolved into a typed `Relation` with its foreign key column, target model, and cascade rule
- Virtual relation fields (`orders Order[]`, `user User`) carry `db_name: "-"` because they have no physical column
- All type references (`Role`, `OrderStatus`, `Address`, `Profile`, `Order`) are replaced by resolved `ResolvedFieldType` variants
- `@computed` fields carry the SQL expression and storage kind (`Stored` / `Virtual`)
- `@check` constraints are collected per model, distinguishing field-level vs table-level scope
- `@updatedAt` fields are flagged with `updated_at: true` for engine-side auto-timestamping
- Indexes carry their type (`Hash`, `Gin`, `Brin`, `BTree`) and optional physical name override
- Composite types (`Address`) are resolved into `CompositeTypeIr` with 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:**

```python
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:

```json
{
  "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 `Select` AST 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:

```sql
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:

```json
{
  "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:

```python
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:

```rust
// 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:

```toml
# Cargo.toml (workspace root)
[workspace]
members = [
    "src/db",   # <- add this
    "my-app",
]
```

```toml
# 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:

```bash
nautilus generate --standalone
```

```toml
# 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:

```bash
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

```mermaid
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:

```mermaid
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

1. Download the `.vsix` from the [latest release](https://github.com/y0gm4/nautilus/releases/latest).
2. **Extensions → ⋯ → Install from VSIX…** and select the file.
3. The extension auto-downloads the `nautilus-lsp` binary on first activation.

To override the binary path (`Settings → Extensions → Nautilus`):
```jsonc
{ "nautilus.lspPath": "/path/to/nautilus-lsp" }
```

### Neovim (via `nvim-lspconfig`)

```lua
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`):
```vim
au BufRead,BufNewFile *.nautilus set filetype=nautilus
```

### Helix

```toml
# ~/.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](LICENSE-MIT) and [Apache 2.0](LICENSE-APACHE).
