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>(&current, &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(&current, &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(&current, &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(&current, &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};