oxide_sql_core/lib.rs
1//! # oxide-sql-core
2//!
3//! A type-safe SQL parser and builder with compile-time validation.
4//!
5//! This crate provides:
6//! - A hand-written recursive descent parser with Pratt expression parsing
7//! - Type-safe SQL builders using the typestate pattern
8//! - Protection against SQL injection through parameterized queries
9//! - A type-safe migrations system (Django-like)
10//!
11//! ## Defining Tables with `#[derive(Table)]`
12//!
13//! The `#[derive(Table)]` macro (from [`oxide-sql-derive`]) turns a plain
14//! struct into a full schema definition with compile-time checked column
15//! names, types, and metadata.
16//!
17//! ```rust
18//! # #![allow(clippy::needless_doctest_main)]
19//! use oxide_sql_derive::Table;
20//! use oxide_sql_core::schema::{Column, Table};
21//!
22//! #[derive(Table)]
23//! #[table(name = "users")]
24//! pub struct User {
25//! #[column(primary_key)]
26//! id: i64,
27//! name: String,
28//! #[column(nullable)]
29//! email: Option<String>,
30//! }
31//!
32//! fn main() {
33//! // The macro generates all of the following:
34//! //
35//! // UserTable – unit struct implementing the Table trait
36//! // UserColumns – module with typed column structs (Id, Name, Email)
37//! // User::id() – accessor returning UserColumns::Id
38//! // User::name() – accessor returning UserColumns::Name
39//! // User::email() – accessor returning UserColumns::Email
40//! // UserTable::id(), UserTable::name(), ... (same accessors)
41//!
42//! // Table metadata
43//! assert_eq!(UserTable::NAME, "users");
44//! assert_eq!(UserTable::COLUMNS, &["id", "name", "email"]);
45//! assert_eq!(UserTable::PRIMARY_KEY, Some("id"));
46//!
47//! // Column metadata
48//! assert_eq!(UserColumns::Id::NAME, "id");
49//! assert!(UserColumns::Id::PRIMARY_KEY);
50//! assert!(!UserColumns::Id::NULLABLE);
51//!
52//! assert_eq!(UserColumns::Email::NAME, "email");
53//! assert!(UserColumns::Email::NULLABLE);
54//! }
55//! ```
56//!
57//! ### Attributes
58//!
59//! | Attribute | Level | Effect |
60//! |---|---|---|
61//! | `#[table(name = "...")]` | struct | Sets the SQL table name (default: `snake_case` of struct name) |
62//! | `#[column(primary_key)]` | field | Marks the column as the primary key |
63//! | `#[column(nullable)]` | field | Marks the column as nullable |
64//! | `#[column(name = "...")]` | field | Overrides the SQL column name (default: field name) |
65//!
66//! ### What the macro generates — under the hood
67//!
68//! Given the `User` struct above, `#[derive(Table)]` expands to roughly:
69//!
70//! ```rust
71//! use oxide_sql_core::schema::{Column, Table, TypedColumn};
72//!
73//! pub struct User { id: i64, name: String, email: Option<String> }
74//!
75//! // 1. A table unit struct that implements the Table trait.
76//! #[derive(Debug, Clone, Copy)]
77//! pub struct UserTable;
78//!
79//! impl Table for UserTable {
80//! type Row = User;
81//! const NAME: &'static str = "users";
82//! const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
83//! const PRIMARY_KEY: Option<&'static str> = Some("id");
84//! }
85//!
86//! // 2. A columns module with one zero-sized struct per field.
87//! // Each struct implements Column (with the table, Rust type,
88//! // name, nullable, and primary_key metadata) and TypedColumn<T>.
89//! #[allow(non_snake_case)]
90//! mod UserColumns {
91//! use super::*;
92//!
93//! #[derive(Debug, Clone, Copy)]
94//! pub struct Id;
95//! impl Column for Id {
96//! type Table = super::UserTable;
97//! type Type = i64;
98//! const NAME: &'static str = "id";
99//! const NULLABLE: bool = false;
100//! const PRIMARY_KEY: bool = true;
101//! }
102//! impl TypedColumn<i64> for Id {}
103//!
104//! #[derive(Debug, Clone, Copy)]
105//! pub struct Name;
106//! impl Column for Name {
107//! type Table = super::UserTable;
108//! type Type = String;
109//! const NAME: &'static str = "name";
110//! const NULLABLE: bool = false;
111//! const PRIMARY_KEY: bool = false;
112//! }
113//! impl TypedColumn<String> for Name {}
114//!
115//! #[derive(Debug, Clone, Copy)]
116//! pub struct Email;
117//! impl Column for Email {
118//! type Table = super::UserTable;
119//! type Type = Option<String>;
120//! const NAME: &'static str = "email";
121//! const NULLABLE: bool = true;
122//! const PRIMARY_KEY: bool = false;
123//! }
124//! impl TypedColumn<Option<String>> for Email {}
125//! }
126//!
127//! // 3. Const accessor methods on both UserTable and User so you
128//! // can write `User::id()` or `UserTable::id()` to obtain
129//! // the zero-sized column type for use in query builders.
130//! impl UserTable {
131//! pub const fn id() -> UserColumns::Id { UserColumns::Id }
132//! pub const fn name() -> UserColumns::Name { UserColumns::Name }
133//! pub const fn email() -> UserColumns::Email { UserColumns::Email }
134//! }
135//! // (User also gets the same accessors and a `table()` method)
136//! # fn main() {}
137//! ```
138//!
139//! Because every column is a distinct zero-sized type that carries its
140//! table association via `Column::Table`, the typed query builders can
141//! verify at compile time that you only reference columns that belong to
142//! the table you are querying.
143//!
144//! ## Type-Safe Queries
145//!
146//! The typed builders — [`Select`], [`Insert`], [`Update`], [`Delete`] —
147//! use the typestate pattern so that incomplete queries (missing columns,
148//! missing table, missing SET values) simply do not compile.
149//!
150//! All examples below reuse the `User` / `UserTable` definition from
151//! the section above.
152//!
153//! ### SELECT
154//!
155//! ```rust
156//! # #![allow(clippy::needless_doctest_main)]
157//! use oxide_sql_derive::Table;
158//! use oxide_sql_core::builder::{Select, col};
159//! use oxide_sql_core::schema::Table;
160//!
161//! #[derive(Table)]
162//! #[table(name = "users")]
163//! pub struct User {
164//! #[column(primary_key)]
165//! id: i64,
166//! name: String,
167//! #[column(nullable)]
168//! email: Option<String>,
169//! }
170//!
171//! fn main() {
172//! // SELECT all columns
173//! let (sql, _params) = Select::<UserTable, _, _>::new()
174//! .select_all()
175//! .from_table()
176//! .build();
177//! assert_eq!(sql, "SELECT id, name, email FROM users");
178//!
179//! // SELECT with WHERE, ORDER BY, LIMIT
180//! let (sql, params) = Select::<UserTable, _, _>::new()
181//! .select_all()
182//! .from_table()
183//! .where_col(User::id(), col(User::id()).gt(100))
184//! .order_by(User::name(), true)
185//! .limit(10)
186//! .build();
187//! assert_eq!(
188//! sql,
189//! "SELECT id, name, email FROM users \
190//! WHERE id > ? ORDER BY name LIMIT 10"
191//! );
192//! }
193//! ```
194//!
195//! ### INSERT
196//!
197//! ```rust
198//! # #![allow(clippy::needless_doctest_main)]
199//! use oxide_sql_derive::Table;
200//! use oxide_sql_core::builder::Insert;
201//! use oxide_sql_core::schema::Table;
202//!
203//! #[derive(Table)]
204//! #[table(name = "users")]
205//! pub struct User {
206//! #[column(primary_key)]
207//! id: i64,
208//! name: String,
209//! #[column(nullable)]
210//! email: Option<String>,
211//! }
212//!
213//! fn main() {
214//! let (sql, params) = Insert::<UserTable, _>::new()
215//! .set(User::name(), "Alice")
216//! .set(User::email(), "alice@example.com")
217//! .build();
218//! assert_eq!(sql, "INSERT INTO users (name, email) VALUES (?, ?)");
219//! }
220//! ```
221//!
222//! ### UPDATE
223//!
224//! ```rust
225//! # #![allow(clippy::needless_doctest_main)]
226//! use oxide_sql_derive::Table;
227//! use oxide_sql_core::builder::{Update, col};
228//! use oxide_sql_core::schema::Table;
229//!
230//! #[derive(Table)]
231//! #[table(name = "users")]
232//! pub struct User {
233//! #[column(primary_key)]
234//! id: i64,
235//! name: String,
236//! #[column(nullable)]
237//! email: Option<String>,
238//! }
239//!
240//! fn main() {
241//! let (sql, params) = Update::<UserTable, _>::new()
242//! .set(User::name(), "Bob")
243//! .where_col(User::id(), col(User::id()).eq(42))
244//! .build();
245//! assert_eq!(sql, "UPDATE users SET name = ? WHERE id = ?");
246//! }
247//! ```
248//!
249//! ### DELETE
250//!
251//! ```rust
252//! # #![allow(clippy::needless_doctest_main)]
253//! use oxide_sql_derive::Table;
254//! use oxide_sql_core::builder::{Delete, col};
255//! use oxide_sql_core::schema::Table;
256//!
257//! #[derive(Table)]
258//! #[table(name = "users")]
259//! pub struct User {
260//! #[column(primary_key)]
261//! id: i64,
262//! name: String,
263//! #[column(nullable)]
264//! email: Option<String>,
265//! }
266//!
267//! fn main() {
268//! let (sql, params) = Delete::<UserTable>::new()
269//! .where_col(User::id(), col(User::id()).eq(1))
270//! .build();
271//! assert_eq!(sql, "DELETE FROM users WHERE id = ?");
272//! }
273//! ```
274//!
275//! ## Dynamic SQL Building
276//!
277//! For string-based queries without compile-time validation, use `SelectDyn`,
278//! `InsertDyn`, `UpdateDyn`, `DeleteDyn` with `dyn_col`:
279//!
280//! ```rust
281//! use oxide_sql_core::builder::{SelectDyn, dyn_col};
282//!
283//! let (sql, params) = SelectDyn::new()
284//! .columns(&["id", "name"])
285//! .from("users")
286//! .where_clause(dyn_col("active").eq(true))
287//! .build();
288//!
289//! assert_eq!(sql, "SELECT id, name FROM users WHERE active = ?");
290//! ```
291//!
292//! ## SQL Injection Prevention
293//!
294//! All values are automatically parameterized:
295//!
296//! ```rust
297//! use oxide_sql_core::builder::{SelectDyn, dyn_col};
298//!
299//! let user_input = "'; DROP TABLE users; --";
300//! let (sql, params) = SelectDyn::new()
301//! .columns(&["id"])
302//! .from("users")
303//! .where_clause(dyn_col("name").eq(user_input))
304//! .build();
305//!
306//! // sql = "SELECT id FROM users WHERE name = ?"
307//! // The malicious input is safely parameterized
308//! assert_eq!(sql, "SELECT id FROM users WHERE name = ?");
309//! ```
310//!
311//! ## Type-Safe Migrations
312//!
313//! The migrations module provides a Django-like system for evolving
314//! database schemas. Each migration is a struct implementing the
315//! [`Migration`] trait with `up()` (apply) and `down()` (rollback)
316//! methods that return a list of [`Operation`]s.
317//!
318//! ### Defining a migration
319//!
320//! ```rust
321//! use oxide_sql_core::migrations::{
322//! Migration, Operation, CreateTableBuilder,
323//! bigint, varchar, timestamp,
324//! };
325//!
326//! pub struct Migration0001;
327//!
328//! impl Migration for Migration0001 {
329//! const ID: &'static str = "0001_create_users";
330//!
331//! fn up() -> Vec<Operation> {
332//! vec![
333//! CreateTableBuilder::new()
334//! .name("users")
335//! .column(bigint("id").primary_key().autoincrement().build())
336//! .column(varchar("username", 255).not_null().unique().build())
337//! .column(
338//! timestamp("created_at")
339//! .not_null()
340//! .default_expr("CURRENT_TIMESTAMP")
341//! .build(),
342//! )
343//! .build()
344//! .into(),
345//! ]
346//! }
347//!
348//! fn down() -> Vec<Operation> {
349//! vec![Operation::drop_table("users")]
350//! }
351//! }
352//! ```
353//!
354//! ### Column helpers
355//!
356//! Shorthand functions create a [`ColumnBuilder`](migrations::ColumnBuilder)
357//! for each SQL type. Chain constraints then call `.build()`:
358//!
359//! | Function | SQL type |
360//! |---|---|
361//! | [`bigint`](migrations::bigint), [`integer`](migrations::integer), [`smallint`](migrations::smallint) | `BIGINT`, `INTEGER`, `SMALLINT` |
362//! | [`varchar`](migrations::varchar), [`text`](migrations::text), [`char`](migrations::char) | `VARCHAR(n)`, `TEXT`, `CHAR(n)` |
363//! | [`boolean`](migrations::boolean) | `BOOLEAN` |
364//! | [`timestamp`](migrations::timestamp), [`datetime`](migrations::datetime), [`date`](migrations::date), [`time`](migrations::time) | date/time types |
365//! | [`decimal`](migrations::decimal), [`numeric`](migrations::numeric), [`real`](migrations::real), [`double`](migrations::double) | floating-point/decimal types |
366//! | [`blob`](migrations::blob), [`binary`](migrations::binary), [`varbinary`](migrations::varbinary) | binary types |
367//!
368//! ```rust
369//! use oxide_sql_core::migrations::{bigint, varchar, boolean, timestamp};
370//!
371//! // Primary key with auto-increment
372//! let id = bigint("id").primary_key().autoincrement().build();
373//!
374//! // NOT NULL + UNIQUE
375//! let email = varchar("email", 255).not_null().unique().build();
376//!
377//! // Default value
378//! let active = boolean("active").not_null().default_bool(true).build();
379//!
380//! // Default expression
381//! let ts = timestamp("created_at")
382//! .not_null()
383//! .default_expr("CURRENT_TIMESTAMP")
384//! .build();
385//! ```
386//!
387//! ### Operations
388//!
389//! [`Operation`] covers all DDL changes. Beyond `CreateTable`, the most
390//! common factory methods are:
391//!
392//! ```rust
393//! use oxide_sql_core::migrations::{Operation, varchar};
394//!
395//! // Drop a table
396//! let _ = Operation::drop_table("users");
397//!
398//! // Rename a table
399//! let _ = Operation::rename_table("users", "accounts");
400//!
401//! // Add a column to an existing table
402//! let _ = Operation::add_column(
403//! "users",
404//! varchar("bio", 1000).nullable().build(),
405//! );
406//!
407//! // Drop a column
408//! let _ = Operation::drop_column("users", "bio");
409//!
410//! // Rename a column
411//! let _ = Operation::rename_column("users", "name", "full_name");
412//!
413//! // Raw SQL (with optional reverse for rollback)
414//! let _ = Operation::run_sql_reversible(
415//! "CREATE VIEW active_users AS SELECT * FROM users WHERE active",
416//! "DROP VIEW active_users",
417//! );
418//! ```
419//!
420//! ### Dependencies between migrations
421//!
422//! Migrations can declare dependencies via `DEPENDENCIES`. The runner
423//! topologically sorts them so dependees always run first:
424//!
425//! ```rust
426//! use oxide_sql_core::migrations::{
427//! Migration, Operation, CreateTableBuilder, bigint, varchar,
428//! };
429//!
430//! pub struct Migration0001;
431//! impl Migration for Migration0001 {
432//! const ID: &'static str = "0001_create_users";
433//! fn up() -> Vec<Operation> {
434//! vec![
435//! CreateTableBuilder::new()
436//! .name("users")
437//! .column(bigint("id").primary_key().build())
438//! .column(varchar("name", 255).not_null().build())
439//! .build()
440//! .into(),
441//! ]
442//! }
443//! fn down() -> Vec<Operation> {
444//! vec![Operation::drop_table("users")]
445//! }
446//! }
447//!
448//! pub struct Migration0002;
449//! impl Migration for Migration0002 {
450//! const ID: &'static str = "0002_create_posts";
451//! // This migration depends on 0001
452//! const DEPENDENCIES: &'static [&'static str] = &["0001_create_users"];
453//! fn up() -> Vec<Operation> {
454//! vec![
455//! CreateTableBuilder::new()
456//! .name("posts")
457//! .column(bigint("id").primary_key().build())
458//! .column(bigint("user_id").not_null().build())
459//! .column(varchar("title", 255).not_null().build())
460//! .build()
461//! .into(),
462//! ]
463//! }
464//! fn down() -> Vec<Operation> {
465//! vec![Operation::drop_table("posts")]
466//! }
467//! }
468//! ```
469//!
470//! ### Running migrations — under the hood
471//!
472//! [`MigrationRunner`](migrations::MigrationRunner) registers migrations,
473//! resolves dependencies, and generates dialect-specific SQL.
474//! [`MigrationState`](migrations::MigrationState) tracks which migrations
475//! have already been applied (backed by the `_oxide_migrations` table in
476//! your database).
477//!
478//! ```rust
479//! use oxide_sql_core::migrations::{
480//! Migration, MigrationRunner, MigrationState,
481//! SqliteDialect, Operation, CreateTableBuilder,
482//! bigint, varchar,
483//! };
484//!
485//! pub struct Mig0001;
486//! impl Migration for Mig0001 {
487//! const ID: &'static str = "0001_create_users";
488//! fn up() -> Vec<Operation> {
489//! vec![
490//! CreateTableBuilder::new()
491//! .name("users")
492//! .column(bigint("id").primary_key().build())
493//! .column(varchar("name", 255).not_null().build())
494//! .build()
495//! .into(),
496//! ]
497//! }
498//! fn down() -> Vec<Operation> {
499//! vec![Operation::drop_table("users")]
500//! }
501//! }
502//!
503//! // 1. Create a runner with a dialect (SQLite, Postgres, DuckDB)
504//! let mut runner = MigrationRunner::new(SqliteDialect::new());
505//!
506//! // 2. Register all migrations
507//! runner.register::<Mig0001>();
508//!
509//! // 3. Validate dependencies (detects cycles / missing deps)
510//! runner.validate().expect("dependency graph is valid");
511//!
512//! // 4. Build state from the database (here: empty = fresh DB)
513//! let state = MigrationState::new();
514//!
515//! // 5. Generate SQL for pending migrations
516//! let pending_sql = runner.sql_for_pending(&state).unwrap();
517//! for (id, statements) in &pending_sql {
518//! for sql in statements {
519//! // execute `sql` against your database connection
520//! assert!(!sql.is_empty());
521//! }
522//! // then mark applied: state.mark_applied(id);
523//! }
524//!
525//! // 6. Rollback the last N migrations
526//! let mut applied_state = MigrationState::from_applied(
527//! vec!["0001_create_users".to_string()],
528//! );
529//! let rollback_sql = runner.sql_for_rollback(&applied_state, 1).unwrap();
530//! for (id, statements) in &rollback_sql {
531//! for sql in statements {
532//! assert!(!sql.is_empty());
533//! }
534//! }
535//! ```
536//!
537//! ### Dialects
538//!
539//! The same migration operations produce different SQL depending on the
540//! dialect:
541//!
542//! | Dialect | Auto-increment strategy | Notes |
543//! |---|---|---|
544//! | [`SqliteDialect`](migrations::SqliteDialect) | `AUTOINCREMENT` keyword | Limited `ALTER TABLE`; dates stored as `TEXT` |
545//! | [`PostgresDialect`](migrations::PostgresDialect) | `SERIAL` / `BIGSERIAL` types | Full `ALTER COLUMN` support |
546//! | [`DuckDbDialect`](migrations::DuckDbDialect) | `CREATE SEQUENCE` + `DEFAULT nextval(...)` | Sequence name: `seq_<table>_<column>` |
547
548pub mod ast;
549pub mod builder;
550pub mod dialect;
551pub mod lexer;
552pub mod migrations;
553pub mod parser;
554pub mod schema;
555
556pub use ast::{Expr, Statement};
557pub use builder::{
558 col, dyn_col, Delete, DeleteDyn, Insert, InsertDyn, Select, SelectDyn, Update, UpdateDyn,
559};
560pub use lexer::{Lexer, Token, TokenKind};
561pub use parser::{ParseError, Parser};
562pub use schema::{Column, Selectable, Table, TypedColumn};