# SeaQuery builds SQL as an AST so parameters and dialects stay in order

> SeaQuery is a Rust query builder that constructs expressions, queries and schema as abstract syntax trees, which means parameter numbering is handled for you and one query can be rendered for MySQL, Postgres or SQLite. The library is written in safe Rust with every dependency optional, and it is the foundation of the SeaORM ORM.

**SeaQL/sea-query** — 🔱 A dynamic SQL query builder for MySQL, Postgres and SQLite

- Repository: https://github.com/SeaQL/sea-query
- Website: https://www.sea-ql.org
- Stars: 1,773 · Forks: 277
- Language: Rust
- License: NOASSERTION
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/seaql-sea-query

## Parameter numbering is the reason to use it

The first motivation the README gives is parameter binding, and it is the one that matters most in practice. One of the headaches of raw SQL is binding parameters, and with SeaQuery you can inject parameters right alongside the expression and the $N sequencing is handled for you, which the documentation describes as no more off-by-one errors. The example makes the point concretely, with an expression that adds one and multiplies by two, a like condition and an in list, producing a Postgres statement with the placeholders in order:

```rust
Query::select()
    .expr(Expr::col("size_w").add(1).mul(2))
    .from("glyph")
    .and_where(Expr::col("image").like("A"))
    .and_where(Expr::col("id").is_in([3, 4, 5]))
    .build(PostgresQueryBuilder)
```

There is also an escape hatch, because some queries need hand-written SQL. `Expr::cust_with_values` lets you write a raw expression with your own placeholders, and the global sequence is rewritten, so a fragment that uses $2 and $1 out of order still ends up correctly numbered in the final statement.

## apply_if removes the conditional WHERE

The second motivation is dynamic queries, and the mechanism is a small combinator rather than a builder convention. You construct the query at runtime based on user inputs with a fluent interface, so you do not have to append WHERE or AND conditionally. The `apply_if` method takes an option and a closure, and applies the closure only when the option is present:

```rust
fn query(a: Option<i32>, b: Option<char>) -> SelectStatement {
    Query::select()
        .column("id")
        .from("character")
        .apply_if(a, |q, v| {
            q.and_where(Expr::col("font_id").eq(v));
        })
        .apply_if(b, |q, v| {
            q.and_where(Expr::col("ascii").like(v));
        })
        .take()
}
```

The documentation shows the three resulting statements, with one condition, with both, and with neither, and the point is that the shape of the query is the same in all three cases. A builder that made you write `if` blocks around `and_where` calls would produce a different code path for every combination, and a builder that took an `Option` for the whole where clause would not scale to three conditions.

## Parentheses follow precedence, not nesting

Conditions can be arbitrarily complex thanks to the internal AST, and the example builds a disjunction of two conjunctions with `Cond::any()` and `Cond::all()`. What the documentation emphasises about the output is what is not there: there is no superfluous parentheses cluttering the query, because SeaQuery respects operator precedence when injecting them. That is a detail you notice only if you have used a builder that wraps every group in its own pair of parentheses, and it matters when you read the generated SQL in a log or compare it against a fixture. The `cond_where` form takes the condition tree directly, while the `and_where` form takes a single expression, and the two cover the two ways you normally end up with a compound condition.

## One AST, three dialects

The third motivation is cross database support: with SeaQuery you can target multiple database backends while maintaining a single source of query logic. MySQL, Postgres and SQLite sit behind a common interface that aligns their behaviour where appropriate, and the dialect is chosen at build time by passing a query builder, `PostgresQueryBuilder` or `MysqlQueryBuilder` in the examples. The difference is visible in the output: the Postgres builder emits double quoted identifiers and `$1` placeholders, while the MySQL builder emits backticks and inlines the values in the string representation. MS SQL Server support is available but not in this repository, it is under SeaORM X. The design consequence is worth stating plainly: the library normalises dialects where it can, and where it cannot, the difference shows up in the rendered SQL rather than in your code.

## Everything is a feature flag, including the types

The install is one line, and the README stresses that SeaQuery is very lightweight with all dependencies optional:

```toml
# Cargo.toml
[dependencies]
sea-query = "1.0"
```

The feature flags fall into three groups. The macro support is behind `derive`. The SQL engine selection is `backend-mysql`, `backend-postgres` and `backend-sqlite`. And the type support is a long list of optional flags: `with-chrono`, `with-time`, `with-json`, `with-rust_decimal`, `with-bigdecimal`, `with-uuid`, `with-ipnetwork` and `with-mac_address`, plus the Postgres specific ones `postgres-array`, `postgres-interval` and `postgres-vector`. That last group is the one to read carefully, because the flags decide which Rust types you can bind to a parameter at all, and turning on the wrong combination of them is a common source of feature resolution conflicts in a workspace.

## The repository is a workspace with dialect and driver crates

The tree shows a workspace with the main crate plus several integration crates. There is `sea-query-derive` for the derive macros, and then a family of driver-specific crates: `sea-query-sqlx`, `sea-query-postgres`, `sea-query-rusqlite`, `sea-query-diesel` and `sea-query-rbatis`. The manifest declares the package at version 1.0.2 with edition 2024, a rust-version floor of 1.88.0, and the licence as MIT OR Apache-2.0, with the two licence files in the root. The package is published with the library name `sea_query` while the crate is `sea-query`, the usual Rust convention. The examples directory has a matrix of driver and database combinations, including diesel with MySQL, Postgres and SQLite, plus the plain Postgres, rusqlite and sqlx variants, which is a good indication of which combinations are actually exercised.

## Where it sits in the ecosystem

SeaQuery is the foundation of SeaORM, an async and dynamic ORM for Rust, so the two projects are developed together and the query builder is the layer below the ORM. The README also states that SeaQuery is written in 100% safe Rust and that all workspace crates have `#![forbid(unsafe_code)]`, which for a library that ends up building SQL strings is a meaningful guarantee. The project is written in 100% safe Rust with a documentation site at sea-ql.org, a Discord for the community, and a release history that shows the 1.0 line settling: 1.0.0-rc.34 in May 2026, 1.0.0 at the end of that month, and 1.0.2 in August. The last push to the master branch was on 2026-10-13.

## Conclusion

Adopt SeaQuery if you need SQL that is assembled at runtime and you want the parameter numbering handled rather than hand-maintained, since the AST approach removes the class of bug where a placeholder is out of order. Do not adopt it if you want a full ORM, because SeaQuery builds statements and does not map your types to tables, though SeaORM does that on top of it. Verify first which backend feature flags you enable, since the type support flags decide which Rust types you can bind, and note that MS SQL Server support is not in this crate but under SeaORM X.

## FAQ

### What is SeaQuery?

It is a query builder to help you construct dynamic SQL queries in Rust. Expressions, queries and schema are constructed as abstract syntax trees through an ergonomic API, with MySQL, Postgres and SQLite supported behind a common interface.

### How do I install SeaQuery?

Add `sea-query = "1.0"` to the dependencies in your Cargo.toml. The library is very lightweight and all dependencies are optional, with the SQL engine selected through backend-mysql, backend-postgres or backend-sqlite.

### How does SeaQuery handle parameter placeholders?

The $N sequencing is handled for you, so parameters can be injected right alongside the expression without off-by-one errors. A custom expression written with `Expr::cust_with_values` can use its own local placeholder order because the global sequence is rewritten.

### How do I build a query with optional conditions in SeaQuery?

Use `apply_if`, which takes an option and a closure and applies the closure only when the option is present, so you do not append WHERE or AND conditionally.

### Does SeaQuery support MS SQL Server?

Not in this crate. The README points to SeaORM X for MS SQL Server support, while SeaQuery itself supports MySQL, Postgres and SQLite.

## Sources

- [Issues](https://github.com/SeaQL/sea-query/issues)
- [Project website](https://www.sea-ql.org)
- [README](https://github.com/SeaQL/sea-query/blob/master/README.md)
- [Releases](https://github.com/SeaQL/sea-query/releases)
- [SeaQL/sea-query on GitHub](https://github.com/SeaQL/sea-query)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/seaql-sea-query
