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//! - A schema diff engine for automatic migration generation
11//!
12//! ## Defining Tables with `#[derive(Table)]`
13//!
14//! The `#[derive(Table)]` macro (from [`oxide-sql-derive`]) turns a plain
15//! struct into a full schema definition with compile-time checked column
16//! names, types, and metadata.
17//!
18//! ```rust
19//! # #![allow(clippy::needless_doctest_main)]
20//! use oxide_sql_derive::Table;
21//! use oxide_sql_core::schema::{Column, Table};
22//!
23//! #[derive(Table)]
24//! #[table(name = "users")]
25//! pub struct User {
26//! #[column(primary_key)]
27//! id: i64,
28//! name: String,
29//! #[column(nullable)]
30//! email: Option<String>,
31//! }
32//!
33//! fn main() {
34//! // The macro generates all of the following:
35//! //
36//! // UserTable – unit struct implementing the Table trait
37//! // UserColumns – module with typed column structs (Id, Name, Email)
38//! // User::id() – accessor returning UserColumns::Id
39//! // User::name() – accessor returning UserColumns::Name
40//! // User::email() – accessor returning UserColumns::Email
41//! // UserTable::id(), UserTable::name(), ... (same accessors)
42//!
43//! // Table metadata
44//! assert_eq!(UserTable::NAME, "users");
45//! assert_eq!(UserTable::COLUMNS, &["id", "name", "email"]);
46//! assert_eq!(UserTable::PRIMARY_KEY, Some("id"));
47//!
48//! // Column metadata
49//! assert_eq!(UserColumns::Id::NAME, "id");
50//! assert!(UserColumns::Id::PRIMARY_KEY);
51//! assert!(!UserColumns::Id::NULLABLE);
52//!
53//! assert_eq!(UserColumns::Email::NAME, "email");
54//! assert!(UserColumns::Email::NULLABLE);
55//! }
56//! ```
57//!
58//! ### Attributes
59//!
60//! | Attribute | Level | Effect |
61//! |---|---|---|
62//! | `#[table(name = "...")]` | struct | Sets the SQL table name (default: `snake_case` of struct name) |
63//! | `#[column(primary_key)]` | field | Marks the column as the primary key |
64//! | `#[column(nullable)]` | field | Marks the column as nullable |
65//! | `#[column(name = "...")]` | field | Overrides the SQL column name (default: field name) |
66//!
67//! ### What the macro generates — under the hood
68//!
69//! Given the `User` struct above, `#[derive(Table)]` expands to roughly:
70//!
71//! ```rust
72//! use oxide_sql_core::schema::{Column, Table, TypedColumn};
73//!
74//! pub struct User { id: i64, name: String, email: Option<String> }
75//!
76//! // 1. A table unit struct that implements the Table trait.
77//! #[derive(Debug, Clone, Copy)]
78//! pub struct UserTable;
79//!
80//! impl Table for UserTable {
81//! type Row = User;
82//! const NAME: &'static str = "users";
83//! const COLUMNS: &'static [&'static str] = &["id", "name", "email"];
84//! const PRIMARY_KEY: Option<&'static str> = Some("id");
85//! }
86//!
87//! // 2. A columns module with one zero-sized struct per field.
88//! // Each struct implements Column (with the table, Rust type,
89//! // name, nullable, and primary_key metadata) and TypedColumn<T>.
90//! #[allow(non_snake_case)]
91//! mod UserColumns {
92//! use super::*;
93//!
94//! #[derive(Debug, Clone, Copy)]
95//! pub struct Id;
96//! impl Column for Id {
97//! type Table = super::UserTable;
98//! type Type = i64;
99//! const NAME: &'static str = "id";
100//! const NULLABLE: bool = false;
101//! const PRIMARY_KEY: bool = true;
102//! }
103//! impl TypedColumn<i64> for Id {}
104//!
105//! #[derive(Debug, Clone, Copy)]
106//! pub struct Name;
107//! impl Column for Name {
108//! type Table = super::UserTable;
109//! type Type = String;
110//! const NAME: &'static str = "name";
111//! const NULLABLE: bool = false;
112//! const PRIMARY_KEY: bool = false;
113//! }
114//! impl TypedColumn<String> for Name {}
115//!
116//! #[derive(Debug, Clone, Copy)]
117//! pub struct Email;
118//! impl Column for Email {
119//! type Table = super::UserTable;
120//! type Type = Option<String>;
121//! const NAME: &'static str = "email";
122//! const NULLABLE: bool = true;
123//! const PRIMARY_KEY: bool = false;
124//! }
125//! impl TypedColumn<Option<String>> for Email {}
126//! }
127//!
128//! // 3. Const accessor methods on both UserTable and User so you
129//! // can write `User::id()` or `UserTable::id()` to obtain
130//! // the zero-sized column type for use in query builders.
131//! impl UserTable {
132//! pub const fn id() -> UserColumns::Id { UserColumns::Id }
133//! pub const fn name() -> UserColumns::Name { UserColumns::Name }
134//! pub const fn email() -> UserColumns::Email { UserColumns::Email }
135//! }
136//! // (User also gets the same accessors and a `table()` method)
137//! # fn main() {}
138//! ```
139//!
140//! Because every column is a distinct zero-sized type that carries its
141//! table association via `Column::Table`, the typed query builders can
142//! verify at compile time that you only reference columns that belong to
143//! the table you are querying.
144//!
145//! ## Type-Safe Queries
146//!
147//! The typed builders — [`Select`], [`Insert`], [`Update`], [`Delete`] —
148//! use the typestate pattern so that incomplete queries (missing columns,
149//! missing table, missing SET values) simply do not compile.
150//!
151//! All examples below reuse the `User` / `UserTable` definition from
152//! the section above.
153//!
154//! ### SELECT
155//!
156//! ```rust
157//! # #![allow(clippy::needless_doctest_main)]
158//! use oxide_sql_derive::Table;
159//! use oxide_sql_core::builder::{Select, col};
160//! use oxide_sql_core::schema::Table;
161//!
162//! #[derive(Table)]
163//! #[table(name = "users")]
164//! pub struct User {
165//! #[column(primary_key)]
166//! id: i64,
167//! name: String,
168//! #[column(nullable)]
169//! email: Option<String>,
170//! }
171//!
172//! fn main() {
173//! // SELECT all columns
174//! let (sql, _params) = Select::<UserTable, _, _>::new()
175//! .select_all()
176//! .from_table()
177//! .build();
178//! assert_eq!(sql, "SELECT id, name, email FROM users");
179//!
180//! // SELECT with WHERE, ORDER BY, LIMIT
181//! let (sql, params) = Select::<UserTable, _, _>::new()
182//! .select_all()
183//! .from_table()
184//! .where_col(User::id(), col(User::id()).gt(100))
185//! .order_by(User::name(), true)
186//! .limit(10)
187//! .build();
188//! assert_eq!(
189//! sql,
190//! "SELECT id, name, email FROM users \
191//! WHERE id > ? ORDER BY name LIMIT 10"
192//! );
193//! }
194//! ```
195//!
196//! ### INSERT
197//!
198//! ```rust
199//! # #![allow(clippy::needless_doctest_main)]
200//! use oxide_sql_derive::Table;
201//! use oxide_sql_core::builder::Insert;
202//! use oxide_sql_core::schema::Table;
203//!
204//! #[derive(Table)]
205//! #[table(name = "users")]
206//! pub struct User {
207//! #[column(primary_key)]
208//! id: i64,
209//! name: String,
210//! #[column(nullable)]
211//! email: Option<String>,
212//! }
213//!
214//! fn main() {
215//! let (sql, params) = Insert::<UserTable, _>::new()
216//! .set(User::name(), "Alice")
217//! .set(User::email(), "alice@example.com")
218//! .build();
219//! assert_eq!(sql, "INSERT INTO users (name, email) VALUES (?, ?)");
220//! }
221//! ```
222//!
223//! ### UPDATE
224//!
225//! ```rust
226//! # #![allow(clippy::needless_doctest_main)]
227//! use oxide_sql_derive::Table;
228//! use oxide_sql_core::builder::{Update, col};
229//! use oxide_sql_core::schema::Table;
230//!
231//! #[derive(Table)]
232//! #[table(name = "users")]
233//! pub struct User {
234//! #[column(primary_key)]
235//! id: i64,
236//! name: String,
237//! #[column(nullable)]
238//! email: Option<String>,
239//! }
240//!
241//! fn main() {
242//! let (sql, params) = Update::<UserTable, _>::new()
243//! .set(User::name(), "Bob")
244//! .where_col(User::id(), col(User::id()).eq(42))
245//! .build();
246//! assert_eq!(sql, "UPDATE users SET name = ? WHERE id = ?");
247//! }
248//! ```
249//!
250//! ### DELETE
251//!
252//! ```rust
253//! # #![allow(clippy::needless_doctest_main)]
254//! use oxide_sql_derive::Table;
255//! use oxide_sql_core::builder::{Delete, col};
256//! use oxide_sql_core::schema::Table;
257//!
258//! #[derive(Table)]
259//! #[table(name = "users")]
260//! pub struct User {
261//! #[column(primary_key)]
262//! id: i64,
263//! name: String,
264//! #[column(nullable)]
265//! email: Option<String>,
266//! }
267//!
268//! fn main() {
269//! let (sql, params) = Delete::<UserTable>::new()
270//! .where_col(User::id(), col(User::id()).eq(1))
271//! .build();
272//! assert_eq!(sql, "DELETE FROM users WHERE id = ?");
273//! }
274//! ```
275//!
276//! ## Dynamic SQL Building
277//!
278//! For string-based queries without compile-time validation, use `SelectDyn`,
279//! `InsertDyn`, `UpdateDyn`, `DeleteDyn` with `dyn_col`:
280//!
281//! ```rust
282//! use oxide_sql_core::builder::{SelectDyn, dyn_col};
283//!
284//! let (sql, params) = SelectDyn::new()
285//! .columns(&["id", "name"])
286//! .from("users")
287//! .where_clause(dyn_col("active").eq(true))
288//! .build();
289//!
290//! assert_eq!(sql, "SELECT id, name FROM users WHERE active = ?");
291//! ```
292//!
293//! ## SQL Injection Prevention
294//!
295//! All values are automatically parameterized:
296//!
297//! ```rust
298//! use oxide_sql_core::builder::{SelectDyn, dyn_col};
299//!
300//! let user_input = "'; DROP TABLE users; --";
301//! let (sql, params) = SelectDyn::new()
302//! .columns(&["id"])
303//! .from("users")
304//! .where_clause(dyn_col("name").eq(user_input))
305//! .build();
306//!
307//! // sql = "SELECT id FROM users WHERE name = ?"
308//! // The malicious input is safely parameterized
309//! assert_eq!(sql, "SELECT id FROM users WHERE name = ?");
310//! ```
311//!
312//! ## Type-Safe Migrations
313//!
314//! The migrations module provides a Django-like system for evolving
315//! database schemas. Each migration is a struct implementing the
316//! [`Migration`] trait with `up()` (apply) and `down()` (rollback)
317//! methods that return a list of [`Operation`]s.
318//!
319//! ### Defining a migration
320//!
321//! ```rust
322//! use oxide_sql_core::migrations::{
323//! Migration, Operation, CreateTableBuilder,
324//! bigint, varchar, timestamp,
325//! };
326//!
327//! pub struct Migration0001;
328//!
329//! impl Migration for Migration0001 {
330//! const ID: &'static str = "0001_create_users";
331//!
332//! fn up() -> Vec<Operation> {
333//! vec![
334//! CreateTableBuilder::new()
335//! .name("users")
336//! .column(bigint("id").primary_key().autoincrement().build())
337//! .column(varchar("username", 255).not_null().unique().build())
338//! .column(
339//! timestamp("created_at")
340//! .not_null()
341//! .default_expr("CURRENT_TIMESTAMP")
342//! .build(),
343//! )
344//! .build()
345//! .into(),
346//! ]
347//! }
348//!
349//! fn down() -> Vec<Operation> {
350//! vec![Operation::drop_table("users")]
351//! }
352//! }
353//! ```
354//!
355//! ### Column helpers
356//!
357//! Shorthand functions create a [`ColumnBuilder`](migrations::ColumnBuilder)
358//! for each SQL type. Chain constraints then call `.build()`:
359//!
360//! | Function | SQL type |
361//! |---|---|
362//! | [`bigint`](migrations::bigint), [`integer`](migrations::integer), [`smallint`](migrations::smallint) | `BIGINT`, `INTEGER`, `SMALLINT` |
363//! | [`varchar`](migrations::varchar), [`text`](migrations::text), [`char`](migrations::char) | `VARCHAR(n)`, `TEXT`, `CHAR(n)` |
364//! | [`boolean`](migrations::boolean) | `BOOLEAN` |
365//! | [`timestamp`](migrations::timestamp), [`datetime`](migrations::datetime), [`date`](migrations::date), [`time`](migrations::time) | date/time types |
366//! | [`decimal`](migrations::decimal), [`numeric`](migrations::numeric), [`real`](migrations::real), [`double`](migrations::double) | floating-point/decimal types |
367//! | [`blob`](migrations::blob), [`binary`](migrations::binary), [`varbinary`](migrations::varbinary) | binary types |
368//!
369//! ```rust
370//! use oxide_sql_core::migrations::{bigint, varchar, boolean, timestamp};
371//!
372//! // Primary key with auto-increment
373//! let id = bigint("id").primary_key().autoincrement().build();
374//!
375//! // NOT NULL + UNIQUE
376//! let email = varchar("email", 255).not_null().unique().build();
377//!
378//! // Default value
379//! let active = boolean("active").not_null().default_bool(true).build();
380//!
381//! // Default expression
382//! let ts = timestamp("created_at")
383//! .not_null()
384//! .default_expr("CURRENT_TIMESTAMP")
385//! .build();
386//! ```
387//!
388//! ### Operations
389//!
390//! [`Operation`] covers all DDL changes. Beyond `CreateTable`, the most
391//! common factory methods are:
392//!
393//! ```rust
394//! use oxide_sql_core::migrations::{Operation, varchar};
395//!
396//! // Drop a table
397//! let _ = Operation::drop_table("users");
398//!
399//! // Rename a table
400//! let _ = Operation::rename_table("users", "accounts");
401//!
402//! // Add a column to an existing table
403//! let _ = Operation::add_column(
404//! "users",
405//! varchar("bio", 1000).nullable().build(),
406//! );
407//!
408//! // Drop a column
409//! let _ = Operation::drop_column("users", "bio");
410//!
411//! // Rename a column
412//! let _ = Operation::rename_column("users", "name", "full_name");
413//!
414//! // Raw SQL (with optional reverse for rollback)
415//! let _ = Operation::run_sql_reversible(
416//! "CREATE VIEW active_users AS SELECT * FROM users WHERE active",
417//! "DROP VIEW active_users",
418//! );
419//! ```
420//!
421//! ### Dependencies between migrations
422//!
423//! Migrations can declare dependencies via `DEPENDENCIES`. The runner
424//! topologically sorts them so dependees always run first:
425//!
426//! ```rust
427//! use oxide_sql_core::migrations::{
428//! Migration, Operation, CreateTableBuilder, bigint, varchar,
429//! };
430//!
431//! pub struct Migration0001;
432//! impl Migration for Migration0001 {
433//! const ID: &'static str = "0001_create_users";
434//! fn up() -> Vec<Operation> {
435//! vec![
436//! CreateTableBuilder::new()
437//! .name("users")
438//! .column(bigint("id").primary_key().build())
439//! .column(varchar("name", 255).not_null().build())
440//! .build()
441//! .into(),
442//! ]
443//! }
444//! fn down() -> Vec<Operation> {
445//! vec![Operation::drop_table("users")]
446//! }
447//! }
448//!
449//! pub struct Migration0002;
450//! impl Migration for Migration0002 {
451//! const ID: &'static str = "0002_create_posts";
452//! // This migration depends on 0001
453//! const DEPENDENCIES: &'static [&'static str] = &["0001_create_users"];
454//! fn up() -> Vec<Operation> {
455//! vec![
456//! CreateTableBuilder::new()
457//! .name("posts")
458//! .column(bigint("id").primary_key().build())
459//! .column(bigint("user_id").not_null().build())
460//! .column(varchar("title", 255).not_null().build())
461//! .build()
462//! .into(),
463//! ]
464//! }
465//! fn down() -> Vec<Operation> {
466//! vec![Operation::drop_table("posts")]
467//! }
468//! }
469//! ```
470//!
471//! ### Running migrations — under the hood
472//!
473//! [`MigrationRunner`](migrations::MigrationRunner) registers migrations,
474//! resolves dependencies, and generates dialect-specific SQL.
475//! [`MigrationState`](migrations::MigrationState) tracks which migrations
476//! have already been applied (backed by the `_oxide_migrations` table in
477//! your database).
478//!
479//! ```rust
480//! use oxide_sql_core::migrations::{
481//! Migration, MigrationRunner, MigrationState,
482//! SqliteDialect, Operation, CreateTableBuilder,
483//! bigint, varchar,
484//! };
485//!
486//! pub struct Mig0001;
487//! impl Migration for Mig0001 {
488//! const ID: &'static str = "0001_create_users";
489//! fn up() -> Vec<Operation> {
490//! vec![
491//! CreateTableBuilder::new()
492//! .name("users")
493//! .column(bigint("id").primary_key().build())
494//! .column(varchar("name", 255).not_null().build())
495//! .build()
496//! .into(),
497//! ]
498//! }
499//! fn down() -> Vec<Operation> {
500//! vec![Operation::drop_table("users")]
501//! }
502//! }
503//!
504//! // 1. Create a runner with a dialect (SQLite, Postgres, DuckDB)
505//! let mut runner = MigrationRunner::new(SqliteDialect::new());
506//!
507//! // 2. Register all migrations
508//! runner.register::<Mig0001>();
509//!
510//! // 3. Validate dependencies (detects cycles / missing deps)
511//! runner.validate().expect("dependency graph is valid");
512//!
513//! // 4. Build state from the database (here: empty = fresh DB)
514//! let state = MigrationState::new();
515//!
516//! // 5. Generate SQL for pending migrations
517//! let pending_sql = runner.sql_for_pending(&state).unwrap();
518//! for (id, statements) in &pending_sql {
519//! for sql in statements {
520//! // execute `sql` against your database connection
521//! assert!(!sql.is_empty());
522//! }
523//! // then mark applied: state.mark_applied(id);
524//! }
525//!
526//! // 6. Rollback the last N migrations
527//! let mut applied_state = MigrationState::from_applied(
528//! vec!["0001_create_users".to_string()],
529//! );
530//! let rollback_sql = runner.sql_for_rollback(&applied_state, 1).unwrap();
531//! for (id, statements) in &rollback_sql {
532//! for sql in statements {
533//! assert!(!sql.is_empty());
534//! }
535//! }
536//! ```
537//!
538//! ### Dialects
539//!
540//! The same migration operations produce different SQL depending on the
541//! dialect:
542//!
543//! | Dialect | Auto-increment strategy | Notes |
544//! |---|---|---|
545//! | [`SqliteDialect`](migrations::SqliteDialect) | `AUTOINCREMENT` keyword | Limited `ALTER TABLE`; dates stored as `TEXT` |
546//! | [`PostgresDialect`](migrations::PostgresDialect) | `SERIAL` / `BIGSERIAL` types | Full `ALTER COLUMN` support |
547//! | [`DuckDbDialect`](migrations::DuckDbDialect) | `CREATE SEQUENCE` + `DEFAULT nextval(...)` | Sequence name: `seq_<table>_<column>` |
548//!
549//! ## Schema Diff Engine — Automatic Migration Generation
550//!
551//! The schema diff engine compares two schema snapshots and produces
552//! `Vec<Operation>`, enabling Django-like `makemigrations`. Instead
553//! of writing migration operations by hand, you can diff the current
554//! database state against your `#[derive(Table)]` structs to
555//! automatically detect what changed.
556//!
557//! ### Core types
558//!
559//! | Type | Purpose |
560//! |---|---|
561//! | [`TableSnapshot`] | Dialect-resolved snapshot of a single table (columns with `DataType`, not Rust type strings) |
562//! | [`ColumnSnapshot`] | A single column's resolved name, type, nullable, primary key, unique, autoincrement, and default |
563//! | [`SchemaSnapshot`] | A collection of `TableSnapshot`s keyed by table name |
564//! | [`SchemaDiff`] | Result of a diff: `operations: Vec<Operation>` + `ambiguous: Vec<AmbiguousChange>` |
565//! | [`AmbiguousChange`] | Changes that cannot be auto-resolved (possible column/table renames) |
566//!
567//! ### Building snapshots from `#[derive(Table)]` structs
568//!
569//! [`TableSnapshot::from_table_schema`] resolves Rust types to SQL
570//! `DataType` using the dialect's [`RustTypeMapping`], producing a
571//! dialect-aware snapshot ready for comparison:
572//!
573//! ```rust
574//! # #![allow(clippy::needless_doctest_main)]
575//! use oxide_sql_derive::Table;
576//! use oxide_sql_core::migrations::{TableSnapshot, SqliteDialect};
577//! use oxide_sql_core::schema::Table;
578//!
579//! #[derive(Table)]
580//! #[table(name = "articles")]
581//! pub struct Article {
582//! #[column(primary_key, autoincrement)]
583//! pub id: i64,
584//! pub title: String,
585//! #[column(nullable)]
586//! pub body: Option<String>,
587//! #[column(default = "FALSE")]
588//! pub published: bool,
589//! }
590//!
591//! fn main() {
592//! let dialect = SqliteDialect::new();
593//! let snapshot = TableSnapshot::from_table_schema::<ArticleTable>(&dialect);
594//!
595//! assert_eq!(snapshot.name, "articles");
596//! assert_eq!(snapshot.columns.len(), 4);
597//! assert!(snapshot.column("id").unwrap().primary_key);
598//! assert!(snapshot.column("body").unwrap().nullable);
599//! }
600//! ```
601//!
602//! ### Diffing a single table
603//!
604//! [`auto_diff_table`] compares a table's current snapshot against the
605//! desired schema from a `#[derive(Table)]` struct:
606//!
607//! ```rust
608//! # #![allow(clippy::needless_doctest_main)]
609//! use oxide_sql_derive::Table;
610//! use oxide_sql_core::migrations::{
611//! TableSnapshot, SqliteDialect, auto_diff_table, Operation,
612//! };
613//! use oxide_sql_core::schema::Table;
614//! use oxide_sql_core::ast::DataType;
615//!
616//! #[derive(Table)]
617//! #[table(name = "articles")]
618//! pub struct ArticleV2 {
619//! #[column(primary_key, autoincrement)]
620//! pub id: i64,
621//! pub title: String,
622//! #[column(nullable)]
623//! pub body: Option<String>,
624//! #[column(default = "FALSE")]
625//! pub published: bool,
626//! #[column(nullable)]
627//! pub category: Option<String>,
628//! }
629//!
630//! fn main() {
631//! let dialect = SqliteDialect::new();
632//!
633//! // Simulate "current" DB state: articles without the category column.
634//! let current = TableSnapshot {
635//! name: "articles".to_string(),
636//! columns: vec![
637//! oxide_sql_core::migrations::ColumnSnapshot {
638//! name: "id".into(),
639//! data_type: DataType::Bigint,
640//! nullable: false,
641//! primary_key: true,
642//! unique: false,
643//! autoincrement: true,
644//! default: None,
645//! },
646//! oxide_sql_core::migrations::ColumnSnapshot {
647//! name: "title".into(),
648//! data_type: DataType::Text,
649//! nullable: false,
650//! primary_key: false,
651//! unique: false,
652//! autoincrement: false,
653//! default: None,
654//! },
655//! oxide_sql_core::migrations::ColumnSnapshot {
656//! name: "body".into(),
657//! data_type: DataType::Text,
658//! nullable: true,
659//! primary_key: false,
660//! unique: false,
661//! autoincrement: false,
662//! default: None,
663//! },
664//! oxide_sql_core::migrations::ColumnSnapshot {
665//! name: "published".into(),
666//! data_type: DataType::Integer,
667//! nullable: false,
668//! primary_key: false,
669//! unique: false,
670//! autoincrement: false,
671//! default: Some(oxide_sql_core::migrations::DefaultValue::Expression(
672//! "FALSE".into(),
673//! )),
674//! },
675//! ],
676//! };
677//!
678//! let diff = auto_diff_table::<ArticleV2Table>(¤t, &dialect);
679//!
680//! // Detects that "category" was added.
681//! assert!(diff.operations.iter().any(|op| matches!(
682//! op,
683//! Operation::AddColumn(add) if add.column.name == "category"
684//! )));
685//! }
686//! ```
687//!
688//! ### Diffing entire schemas
689//!
690//! [`auto_diff_schema`] compares two [`SchemaSnapshot`]s and detects
691//! new tables, dropped tables, and per-table column changes:
692//!
693//! ```rust
694//! use oxide_sql_core::migrations::{
695//! SchemaSnapshot, TableSnapshot, ColumnSnapshot, Operation,
696//! auto_diff_schema,
697//! };
698//! use oxide_sql_core::ast::DataType;
699//!
700//! let mut current = SchemaSnapshot::new();
701//! let mut desired = SchemaSnapshot::new();
702//!
703//! // "current" has a "users" table; "desired" adds a "posts" table.
704//! current.add_table(TableSnapshot {
705//! name: "users".into(),
706//! columns: vec![ColumnSnapshot {
707//! name: "id".into(),
708//! data_type: DataType::Bigint,
709//! nullable: false,
710//! primary_key: true,
711//! unique: false,
712//! autoincrement: true,
713//! default: None,
714//! }],
715//! });
716//! desired.add_table(TableSnapshot {
717//! name: "users".into(),
718//! columns: vec![ColumnSnapshot {
719//! name: "id".into(),
720//! data_type: DataType::Bigint,
721//! nullable: false,
722//! primary_key: true,
723//! unique: false,
724//! autoincrement: true,
725//! default: None,
726//! }],
727//! });
728//! desired.add_table(TableSnapshot {
729//! name: "posts".into(),
730//! columns: vec![ColumnSnapshot {
731//! name: "id".into(),
732//! data_type: DataType::Bigint,
733//! nullable: false,
734//! primary_key: true,
735//! unique: false,
736//! autoincrement: true,
737//! default: None,
738//! }],
739//! });
740//!
741//! let diff = auto_diff_schema(¤t, &desired);
742//! assert!(diff.operations.iter().any(|op| matches!(
743//! op,
744//! Operation::CreateTable(ct) if ct.name == "posts"
745//! )));
746//! ```
747//!
748//! ### What the diff engine detects
749//!
750//! | Change | Detected? | Mechanism |
751//! |---|---|---|
752//! | New table | Yes | In desired, not in current |
753//! | Dropped table | Yes | In current, not in desired |
754//! | Added column | Yes | Column in desired, not in current |
755//! | Dropped column | Yes | Column in current, not in desired |
756//! | Column type change | Yes | `data_type` differs |
757//! | Nullable change | Yes | `nullable` differs |
758//! | Default add/change | Yes | `default` differs |
759//! | Default removed | Yes | Old has default, new does not |
760//! | Column rename | **No** | Flagged as [`AmbiguousChange::PossibleRename`] |
761//! | Table rename | **No** | Flagged as [`AmbiguousChange::PossibleTableRename`] |
762//! | Foreign key changes | **No** | Not tracked in `ColumnSchema` |
763//!
764//! ### Ambiguous changes
765//!
766//! When exactly one column is dropped and one is added with the same
767//! type, the diff engine flags this as a possible rename rather than
768//! generating a drop+add pair. The same heuristic applies at the table
769//! level. Your migration tooling should present these to the user for
770//! confirmation:
771//!
772//! ```rust
773//! use oxide_sql_core::migrations::{
774//! AmbiguousChange, TableSnapshot, ColumnSnapshot, auto_diff_schema,
775//! SchemaSnapshot,
776//! };
777//! use oxide_sql_core::ast::DataType;
778//!
779//! let mut current = SchemaSnapshot::new();
780//! current.add_table(TableSnapshot {
781//! name: "users".into(),
782//! columns: vec![ColumnSnapshot {
783//! name: "id".into(),
784//! data_type: DataType::Bigint,
785//! nullable: false,
786//! primary_key: true,
787//! unique: false,
788//! autoincrement: false,
789//! default: None,
790//! }],
791//! });
792//!
793//! // "desired" has the same structure but the table is named "accounts".
794//! let mut desired = SchemaSnapshot::new();
795//! desired.add_table(TableSnapshot {
796//! name: "accounts".into(),
797//! columns: vec![ColumnSnapshot {
798//! name: "id".into(),
799//! data_type: DataType::Bigint,
800//! nullable: false,
801//! primary_key: true,
802//! unique: false,
803//! autoincrement: false,
804//! default: None,
805//! }],
806//! });
807//!
808//! let diff = auto_diff_schema(¤t, &desired);
809//! // No operations generated — flagged as ambiguous instead.
810//! assert!(diff.operations.is_empty());
811//! assert!(matches!(
812//! &diff.ambiguous[0],
813//! AmbiguousChange::PossibleTableRename {
814//! old_table,
815//! new_table,
816//! } if old_table == "users" && new_table == "accounts"
817//! ));
818//! ```
819//!
820//! ### Operation ordering
821//!
822//! Operations are returned in a safe order to avoid FK constraint
823//! violations:
824//!
825//! 1. `CreateTable` — new tables first
826//! 2. `AddColumn` — new columns on existing tables
827//! 3. `AlterColumn` — type/nullable/default changes
828//! 4. `DropColumn` — removed columns
829//! 5. `DropTable` — removed tables last
830//!
831//! ### Database introspection
832//!
833//! The [`Introspect`] trait defines how to read the current schema from
834//! a live database connection:
835//!
836//! ```rust,ignore
837//! use oxide_sql_core::migrations::{Introspect, SchemaSnapshot};
838//!
839//! // Driver crates implement this:
840//! impl Introspect for MyDatabaseConnection {
841//! type Error = MyError;
842//! fn introspect_schema(&self) -> Result<SchemaSnapshot, MyError> {
843//! // Query information_schema / sqlite_master / etc.
844//! }
845//! }
846//! ```
847//!
848//! With introspection + diff, the full `makemigrations` workflow is:
849//!
850//! ```rust,ignore
851//! // 1. Introspect the current database.
852//! let current = db.introspect_schema()?;
853//!
854//! // 2. Build desired schema from derive(Table) structs.
855//! let mut desired = SchemaSnapshot::new();
856//! desired.add_from_table_schema::<UserTable>(&dialect);
857//! desired.add_from_table_schema::<PostTable>(&dialect);
858//!
859//! // 3. Diff.
860//! let diff = auto_diff_schema(¤t, &desired);
861//!
862//! // 4. Generate SQL for each operation.
863//! for op in &diff.operations {
864//! let sql = dialect.generate_sql(op);
865//! println!("{sql}");
866//! }
867//!
868//! // 5. Handle ambiguous changes (ask user about renames).
869//! for change in &diff.ambiguous {
870//! println!("Ambiguous: {change:?}");
871//! }
872//! ```
873
874pub mod ast;
875pub mod builder;
876pub mod dialect;
877pub mod lexer;
878pub mod migrations;
879pub mod parser;
880pub mod schema;
881
882pub use ast::{Expr, Statement};
883pub use builder::{
884 Delete, DeleteDyn, Insert, InsertDyn, Select, SelectDyn, Update, UpdateDyn, col, dyn_col,
885};
886pub use lexer::{Lexer, Token, TokenKind};
887pub use migrations::{
888 AmbiguousChange, ColumnSnapshot, Introspect, SchemaDiff, SchemaSnapshot, TableSnapshot,
889 auto_diff_schema, auto_diff_table,
890};
891pub use parser::{ParseError, Parser};
892pub use schema::{
893 Column, ColumnSchema, RustTypeMapping, Selectable, Table, TableSchema, TypedColumn,
894};