# lift

`lift` is a Rust workspace for the Lift migration language.

This repository contains the language parser, a migration CLI, a small language
server, and a tree-sitter grammar. It is for tools that need to read, validate,
edit, scaffold, or run Lift migration files.

## What Lift looks like

Lift is a small migration DSL with tables, enums, inline SQL, and optional
rollback blocks.

```lift
create users {
  id
  email text unique
  tenant_id uuid
  remember_token
  timestamps
  soft_deletes
  unique (tenant_id, email)
}

alter users {
  add column timezone text default "UTC"
}

backfill {
  update users
  set timezone = 'UTC'
  where timezone is null;
}

down {
  drop users
}
```

The language also supports enum migrations:

```lift
create enum post_status { draft, published, archived }

alter enum post_status {
  add value deleted
}
```

## Relationships

Lift handles relationships at the schema level with foreign keys.

Use `ref` on a column to point at another table:

```lift
create posts {
  id
  author_id ref users on delete cascade
  title text
}
```

`author_id ref users` means `author_id` references `users(id)`.

You can combine `ref` with a few common modifiers:

- `nullable` for optional relationships
- `on delete cascade`, `on delete restrict`, `on delete set null`, or `on delete no action`
- `on update cascade`, `on update restrict`, `on update set null`, or `on update no action`
- `no index` if you do not want Lift to add the default index for the relation column

Example:

```lift
create comments {
  id
  post_id bigint ref posts on delete cascade
  reviewer_id uuid ref users nullable on delete set null
  body text
}
```

If you need a specific index name or a composite index, declare it yourself:

```lift
create comments {
  id
  post_id bigint ref posts no index
  reviewer_id uuid ref users nullable no index
  body text

  index comments_post_id_idx (post_id)
  index comments_post_reviewer_idx (post_id, reviewer_id)
}
```

## Migration files

The parser reads migrations from a directory. It recognizes:

- Lift files named `<id>_<name>.lift`
- SQL files named `<id>_<name>.sql`
- optional SQL rollback files named `<id>_<name>.down.sql`

Examples:

- `202604210001_create_users.lift`
- `202604220001_enable_pgcrypto.sql`
- `202604220001_enable_pgcrypto.down.sql`

Lift migrations may contain one optional `down { ... }` block. SQL migrations
use a separate `.down.sql` file when a rollback is needed.

When loading a directory, the parser sorts migrations by numeric id and returns
them as a single ordered list.

## Workspace

This workspace has four crates.

### `lift`

`crates/lift` parses Lift source and SQL migration files into Rust data
structures such as `MigrationFile`, `Statement`, `TableItem`, and `Column`. It
also ships the `lift` CLI for migration scaffolding and execution.

Use it when you need to:

- read a migration directory
- validate file names and migration contents
- inspect schema statements programmatically
- accept either Lift or SQL migrations
- scaffold and run migrations from the terminal

Install the CLI with an explicit backend feature:

```bash
cargo install lift --features sqlite
cargo install lift --no-default-features --features postgres
cargo install lift --no-default-features --features mariadb
```

Example commands:

```bash
lift migrate init
lift migrate make create_users
lift migrate make enable_pgcrypto --sql
lift migrate export users
lift migrate run --database-url sqlite:///tmp/app.db
lift migrate validate --path db/migrations
lift migrate rollback --steps 1
lift migrate refresh
lift migrate reset
```

Small example:

```rust
use lift::{Statement, default_migration_dir, load_migrations};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let migrations = load_migrations(&default_migration_dir())?;

    for migration in migrations {
        println!("{} {}", migration.id, migration.name);

        for statement in &migration.up {
            match statement {
                Statement::CreateTable { name, .. } => println!("- create {name}"),
                Statement::AlterTable { name, .. } => println!("- alter {name}"),
                Statement::CreateEnum { name, .. } => println!("- create enum {name}"),
                Statement::Sql { .. } => println!("- sql"),
                Statement::Backfill { .. } => println!("- backfill"),
                _ => {}
            }
        }
    }

    Ok(())
}
```

### `lift-migration`

`crates/lift-migration` is the vendored migration runtime used by the `lift`
CLI. It applies parsed Lift and SQL migrations to supported databases and keeps
the execution logic local to this workspace.

The crate also exposes `parse_migration_file` and `parse_source` when you want
to parse one migration at a time.

### `lift-lsp`

`crates/lift-lsp` is a language server for `.lift` files. It runs over stdio.

```bash
cargo run -p lift-lsp
```

Today it focuses on a small set of editor features:

- diagnostics for invalid Lift syntax
- diagnostics for invalid migration file names
- keyword completion for Lift syntax

### `tree-sitter-lift`

`crates/tree-sitter-lift` provides a tree-sitter grammar for Lift, along with
queries for highlighting, indentation, and injections.

Rust example:

```rust
let mut parser = tree_sitter::Parser::new();
parser.set_language(&tree_sitter_lift::language())?;
```

## Development

Build the workspace:

```bash
cargo build --workspace
```

Run tests:

```bash
cargo test --workspace
```

If you change `crates/tree-sitter-lift/grammar.js`, regenerate the parser:

```bash
tree-sitter generate crates/tree-sitter-lift
```

## License

Licensed under either of:

- MIT, see `LICENSE-MIT`
- Apache-2.0, see `LICENSE-APACHE`
