From c6298519f382b2ae10a646aae616a8b6db0ca985 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sun, 29 Mar 2026 21:48:06 +1100 Subject: [PATCH 01/15] sqlite --- Cargo.lock | 53 ++++- Cargo.toml | 2 + src/common/config.rs | 21 +- src/common/dotenv.rs | 10 +- src/common/lazy.rs | 15 ++ src/common/types.rs | 1 + src/core/connection.rs | 5 + src/core/mod.rs | 1 + src/core/sqlite/mod.rs | 2 + src/core/sqlite/pool.rs | 82 ++++++++ src/core/sqlite/prepare.rs | 54 ++++++ src/ts_generator/generator.rs | 3 +- src/ts_generator/information_schema.rs | 65 +++++++ src/ts_generator/types/ts_query.rs | 42 ++++ test-utils/src/sandbox.rs | 59 ++++-- tests/sqlite_query_parameters.rs | 255 +++++++++++++++++++++++++ 16 files changed, 641 insertions(+), 29 deletions(-) create mode 100644 src/core/sqlite/mod.rs create mode 100644 src/core/sqlite/pool.rs create mode 100644 src/core/sqlite/prepare.rs create mode 100644 tests/sqlite_query_parameters.rs diff --git a/Cargo.lock b/Cargo.lock index 6979e2c3..b0211ae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,18 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.3.0" @@ -715,6 +727,15 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -946,6 +967,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "libz-sys" version = "1.1.25" @@ -1316,7 +1348,7 @@ dependencies = [ "base64", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "hmac", "md-5", "memchr", @@ -1332,7 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20" dependencies = [ "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "postgres-protocol", ] @@ -1577,6 +1609,20 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "rusqlite" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae" +dependencies = [ + "bitflags", + "fallible-iterator 0.3.0", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.27" @@ -1815,6 +1861,7 @@ dependencies = [ "predicates", "pretty_assertions", "regex", + "rusqlite", "serde", "serde_json", "sqlparser", @@ -2147,7 +2194,7 @@ dependencies = [ "async-trait", "byteorder", "bytes", - "fallible-iterator", + "fallible-iterator 0.2.0", "futures-channel", "futures-util", "log", diff --git a/Cargo.toml b/Cargo.toml index e04e4976..755e5eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ tokio-postgres = "0.7.16" tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "default"]} async-recursion = "1.1.1" bb8 = "0.9.1" +rusqlite = { version = "0.31", features = ["bundled"] } log = "0.4.29" [dev-dependencies] @@ -39,3 +40,4 @@ predicates = "3.1.4" tempfile = "3.27.0" test_utils = { path="test-utils" } pretty_assertions = "1.4.1" +rusqlite = { version = "0.31", features = ["bundled"] } diff --git a/src/common/config.rs b/src/common/config.rs index 02adc67f..0bbad139 100644 --- a/src/common/config.rs +++ b/src/common/config.rs @@ -234,7 +234,9 @@ impl Config { .or_else(|| default_config.map(|x| x.db_host.clone())) }; - let db_host = match (db_url.is_some(), db_host_chain()) { + let is_sqlite = matches!(db_type, DatabaseType::Sqlite); + + let db_host = match (db_url.is_some() || is_sqlite, db_host_chain()) { (true, Some(v)) => v, (true, None) => String::new(), (false, Some(v)) => v, @@ -254,7 +256,7 @@ impl Config { .or_else(|| default_config.map(|x| x.db_port)) }; - let db_port = match (db_url.is_some(), db_port_chain()) { + let db_port = match (db_url.is_some() || is_sqlite, db_port_chain()) { (true, Some(v)) => v, (true, None) => 0, (false, Some(v)) => v, @@ -275,7 +277,7 @@ impl Config { .or_else(|| default_config.map(|x| x.db_user.clone())) }; - let db_user = match (db_url.is_some(), db_user_chain()) { + let db_user = match (db_url.is_some() || is_sqlite, db_user_chain()) { (true, Some(v)) => v, (true, None) => String::new(), (false, Some(v)) => v, @@ -381,6 +383,19 @@ impl Config { .to_string() } + /// Returns the file path for a SQLite database connection. + /// If DB_URL is provided, it's used directly. Otherwise DB_NAME is used as the file path. + pub fn get_sqlite_path(&self, conn: &DbConnectionConfig) -> String { + if let Some(db_url) = &conn.db_url { + return db_url.to_owned(); + } + + conn + .db_name + .clone() + .unwrap_or_else(|| panic!("DB_NAME (file path) is required for SQLite connections")) + } + pub fn get_postgres_cred(&self, conn: &DbConnectionConfig) -> String { // If custom DB_URL is provided, use it directly if let Some(db_url) = &conn.db_url { diff --git a/src/common/dotenv.rs b/src/common/dotenv.rs index 17b65472..ddcfce42 100644 --- a/src/common/dotenv.rs +++ b/src/common/dotenv.rs @@ -32,12 +32,10 @@ impl Dotenv { Dotenv { db_type: match Self::get_var("DB_TYPE") { None => None, - Some(val) => { - if val == "mysql" { - Some(DatabaseType::Mysql) - } else { - Some(DatabaseType::Postgres) - } + Some(val) => match val.as_str() { + "mysql" => Some(DatabaseType::Mysql), + "sqlite" => Some(DatabaseType::Sqlite), + _ => Some(DatabaseType::Postgres), } }, db_user: Self::get_var("DB_USER"), diff --git a/src/common/lazy.rs b/src/common/lazy.rs index 75e561e6..3df1faa3 100644 --- a/src/common/lazy.rs +++ b/src/common/lazy.rs @@ -4,6 +4,7 @@ use crate::common::types::DatabaseType; use crate::core::connection::{DBConn, DBConnections}; use crate::core::mysql::pool::MySqlConnectionManager; use crate::core::postgres::pool::PostgresConnectionManager; +use crate::core::sqlite::pool::SqliteConnectionManager; use crate::ts_generator::information_schema::DBSchema; use clap::Parser; use std::sync::LazyLock; @@ -49,6 +50,20 @@ pub static DB_CONN_CACHE: LazyLock>>> = LazyLo DBConn::MySQLPooledConn(Mutex::new(pool)) }) }), + DatabaseType::Sqlite => task::block_in_place(|| { + Handle::current().block_on(async { + let sqlite_path = CONFIG.get_sqlite_path(connection_config); + let manager = SqliteConnectionManager::new(sqlite_path, connection.to_string()); + let pool = bb8::Pool::builder() + .max_size(connection_config.pool_size) + .connection_timeout(std::time::Duration::from_secs(connection_config.connection_timeout)) + .build(manager) + .await + .expect(&ERR_DB_CONNECTION_ISSUE); + + DBConn::SqliteConn(Mutex::new(pool)) + }) + }), DatabaseType::Postgres => task::block_in_place(|| { Handle::current().block_on(async { let postgres_cred = CONFIG.get_postgres_cred(connection_config); diff --git a/src/common/types.rs b/src/common/types.rs index 88a536b8..bc7f0268 100644 --- a/src/common/types.rs +++ b/src/common/types.rs @@ -18,6 +18,7 @@ pub enum FileExtension { pub enum DatabaseType { Postgres, Mysql, + Sqlite, } #[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)] diff --git a/src/core/connection.rs b/src/core/connection.rs index b9fa51cc..6493ba71 100644 --- a/src/core/connection.rs +++ b/src/core/connection.rs @@ -3,6 +3,7 @@ use crate::common::types::DatabaseType; use crate::common::SQL; use crate::core::mysql::prepare as mysql_explain; use crate::core::postgres::prepare as postgres_explain; +use crate::core::sqlite::prepare as sqlite_explain; use crate::ts_generator::types::ts_query::TsQuery; use bb8::Pool; use std::collections::HashMap; @@ -11,6 +12,7 @@ use tokio::sync::Mutex; use super::mysql::pool::MySqlConnectionManager; use super::postgres::pool::PostgresConnectionManager; +use super::sqlite::pool::SqliteConnectionManager; use crate::common::errors::DB_CONN_FROM_LOCAL_CACHE_ERROR; use color_eyre::Result; use swc_common::errors::Handler; @@ -19,6 +21,7 @@ use swc_common::errors::Handler; pub enum DBConn { MySQLPooledConn(Mutex>), PostgresConn(Mutex>), + SqliteConn(Mutex>), } impl DBConn { @@ -31,6 +34,7 @@ impl DBConn { let (explain_failed, ts_query) = match &self { DBConn::MySQLPooledConn(_conn) => mysql_explain::prepare(self, sql, should_generate_types, handler).await?, DBConn::PostgresConn(_conn) => postgres_explain::prepare(self, sql, should_generate_types, handler).await?, + DBConn::SqliteConn(_conn) => sqlite_explain::prepare(self, sql, should_generate_types, handler).await?, }; Ok((explain_failed, ts_query)) @@ -41,6 +45,7 @@ impl DBConn { match self { DBConn::MySQLPooledConn(_) => DatabaseType::Mysql, DBConn::PostgresConn(_) => DatabaseType::Postgres, + DBConn::SqliteConn(_) => DatabaseType::Sqlite, } } } diff --git a/src/core/mod.rs b/src/core/mod.rs index bc280965..f1d630d6 100644 --- a/src/core/mod.rs +++ b/src/core/mod.rs @@ -2,3 +2,4 @@ pub mod connection; pub mod execute; pub mod mysql; pub mod postgres; +pub mod sqlite; diff --git a/src/core/sqlite/mod.rs b/src/core/sqlite/mod.rs new file mode 100644 index 00000000..42fe7aa8 --- /dev/null +++ b/src/core/sqlite/mod.rs @@ -0,0 +1,2 @@ +pub mod pool; +pub mod prepare; diff --git a/src/core/sqlite/pool.rs b/src/core/sqlite/pool.rs new file mode 100644 index 00000000..24b4ab53 --- /dev/null +++ b/src/core/sqlite/pool.rs @@ -0,0 +1,82 @@ +use rusqlite::Connection; +use std::sync::{Arc, Mutex}; +use tokio::task; + +/// A connection manager for SQLite that wraps rusqlite's synchronous Connection +/// behind an Arc> for thread-safe access with bb8 connection pooling. +#[derive(Clone, Debug)] +pub struct SqliteConnectionManager { + db_path: String, + connection_name: String, +} + +/// Wrapper around rusqlite::Connection to make it Send + Sync for bb8 +pub struct SqliteConnection { + pub conn: Arc>, +} + +// Safety: rusqlite::Connection is not Send by default, but we protect it with Mutex +// and only access it via spawn_blocking +unsafe impl Send for SqliteConnection {} +unsafe impl Sync for SqliteConnection {} + +impl SqliteConnectionManager { + pub fn new(db_path: String, connection_name: String) -> Self { + Self { + db_path, + connection_name, + } + } +} + +#[derive(Debug)] +pub struct SqlitePoolError(pub String); + +impl std::fmt::Display for SqlitePoolError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SQLite pool error: {}", self.0) + } +} + +impl std::error::Error for SqlitePoolError {} + +impl bb8::ManageConnection for SqliteConnectionManager { + type Connection = SqliteConnection; + type Error = SqlitePoolError; + + async fn connect(&self) -> Result { + let db_path = self.db_path.clone(); + let connection_name = self.connection_name.clone(); + + let conn = task::spawn_blocking(move || { + Connection::open(&db_path).unwrap_or_else(|err| { + panic!( + "Failed to open SQLite database at '{}' for connection '{}': {}", + db_path, connection_name, err + ) + }) + }) + .await + .map_err(|e| SqlitePoolError(format!("Failed to spawn blocking task: {e}")))?; + + Ok(SqliteConnection { + conn: Arc::new(Mutex::new(conn)), + }) + } + + async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> { + let inner = conn.conn.clone(); + task::spawn_blocking(move || { + let conn = inner.lock().unwrap(); + conn + .execute_batch("SELECT 1") + .map_err(|e| SqlitePoolError(format!("SQLite connection validation failed: {e}"))) + }) + .await + .map_err(|e| SqlitePoolError(format!("Failed to spawn blocking task: {e}")))? + } + + fn has_broken(&self, _conn: &mut Self::Connection) -> bool { + false + } +} diff --git a/src/core/sqlite/prepare.rs b/src/core/sqlite/prepare.rs new file mode 100644 index 00000000..90fa4da1 --- /dev/null +++ b/src/core/sqlite/prepare.rs @@ -0,0 +1,54 @@ +use crate::common::SQL; +use crate::core::connection::DBConn; +use crate::ts_generator::generator::generate_ts_interface; +use crate::ts_generator::types::ts_query::TsQuery; +use color_eyre::eyre::Result; + +use swc_common::errors::Handler; + +/// Runs the prepare statement on the input SQL. +/// Validates the query is right by directly connecting to the configured SQLite database. +/// It also processes ts interfaces if the configuration is set to generate_types = true +pub async fn prepare( + db_conn: &DBConn, + sql: &SQL, + should_generate_types: &bool, + handler: &Handler, +) -> Result<(bool, Option)> { + let mut failed = false; + + let conn = match &db_conn { + DBConn::SqliteConn(conn) => conn, + _ => panic!("Invalid connection type"), + }; + + { + let span = sql.span.to_owned(); + let query = sql.query.clone(); + let conn = conn.lock().await; + let pool_conn = conn.get().await.unwrap(); + let inner = pool_conn.conn.clone(); + + let result = tokio::task::spawn_blocking(move || { + let conn = inner.lock().unwrap(); + // Use EXPLAIN to validate the SQL without executing it + let explain_query = format!("EXPLAIN {}", query); + conn.execute_batch(&explain_query) + }) + .await + .unwrap(); + + if let Err(e) = result { + handler.span_bug_no_panic(span, &e.to_string()); + failed = true; + } + } + + let mut ts_query = None; + + if should_generate_types == &true { + ts_query = Some(generate_ts_interface(sql, db_conn).await?); + } + + Ok((failed, ts_query)) +} diff --git a/src/ts_generator/generator.rs b/src/ts_generator/generator.rs index e8265e36..3e0c2bb0 100644 --- a/src/ts_generator/generator.rs +++ b/src/ts_generator/generator.rs @@ -17,7 +17,7 @@ use color_eyre::eyre::Result; use convert_case::{Case, Casing}; use regex::Regex; use sqlparser::{ - dialect::{Dialect, MySqlDialect, PostgreSqlDialect}, + dialect::{Dialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect}, parser::Parser, }; @@ -125,6 +125,7 @@ pub async fn generate_ts_interface(sql: &SQL, db_conn: &DBConn) -> Result = match db_conn.get_db_type() { DatabaseType::Postgres => Box::new(PostgreSqlDialect {}), DatabaseType::Mysql => Box::new(MySqlDialect {}), + DatabaseType::Sqlite => Box::new(SQLiteDialect {}), }; let sql_ast = Parser::parse_sql(&*dialect, &sql.query)?; diff --git a/src/ts_generator/information_schema.rs b/src/ts_generator/information_schema.rs index 0ae93f07..e8d5fe59 100644 --- a/src/ts_generator/information_schema.rs +++ b/src/ts_generator/information_schema.rs @@ -3,6 +3,7 @@ use crate::common::logger::*; use crate::core::connection::DBConn; use crate::core::mysql::pool::MySqlConnectionManager; use crate::core::postgres::pool::PostgresConnectionManager; +use crate::core::sqlite::pool::SqliteConnectionManager; use bb8::Pool; use mysql_async::prelude::Queryable; use std::collections::HashMap; @@ -54,6 +55,7 @@ impl DBSchema { let result = match &conn { DBConn::MySQLPooledConn(conn) => Self::mysql_fetch_table(self, table_name, conn).await, DBConn::PostgresConn(conn) => Self::postgres_fetch_table(self, &"public".to_string(), table_name, conn).await, + DBConn::SqliteConn(conn) => Self::sqlite_fetch_table(self, table_name, conn).await, }; if let Some(result) = &result { @@ -210,4 +212,67 @@ impl DBSchema { None } + + async fn sqlite_fetch_table( + &self, + table_names: &Vec<&str>, + conn: &Mutex>, + ) -> Option { + let mut fields: HashMap = HashMap::new(); + let conn = conn.lock().await; + let pool_conn = conn.get().await.expect(DB_CONN_POOL_RETRIEVE_ERROR); + let inner = pool_conn.conn.clone(); + + let table_names_owned: Vec = table_names.iter().map(|s| s.to_string()).collect(); + + let result = tokio::task::spawn_blocking(move || { + let conn = inner.lock().unwrap(); + let mut all_fields: HashMap = HashMap::new(); + + for table_name in &table_names_owned { + let query = format!("PRAGMA table_info('{}')", table_name); + let mut stmt = match conn.prepare(&query) { + Ok(stmt) => stmt, + Err(_) => continue, + }; + + let rows = match stmt.query_map([], |row| { + let name: String = row.get(1)?; + let type_name: String = row.get(2)?; + let notnull: bool = row.get(3)?; + Ok((name, type_name, notnull, table_name.clone())) + }) { + Ok(rows) => rows, + Err(_) => continue, + }; + + for row in rows { + if let Ok((field_name, field_type, notnull, tbl_name)) = row { + let field = Field { + field_type: TsFieldType::get_ts_field_type_from_sqlite_field_type( + field_type, + tbl_name, + field_name.clone(), + ), + is_nullable: !notnull, + }; + all_fields.insert(field_name, field); + } + } + } + + all_fields + }) + .await; + + if let Ok(result) = result { + if result.is_empty() { + return None; + } + fields.extend(result); + return Some(fields); + } + + None + } } diff --git a/src/ts_generator/types/ts_query.rs b/src/ts_generator/types/ts_query.rs index 65e2a756..6a3342d9 100644 --- a/src/ts_generator/types/ts_query.rs +++ b/src/ts_generator/types/ts_query.rs @@ -242,6 +242,48 @@ impl TsFieldType { } } + /// Converts SQLite type affinity strings to TsFieldType. + /// SQLite uses type affinity rules, so we match common type names. + pub fn get_ts_field_type_from_sqlite_field_type( + field_type: String, + table_name: String, + field_name: String, + ) -> Self { + let upper = field_type.to_uppercase(); + // SQLite type affinity rules (see https://www.sqlite.org/datatype3.html) + if upper.contains("INT") { + return Self::Number; + } + if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") { + return Self::String; + } + if upper.contains("BLOB") || upper.is_empty() { + // Empty type name in SQLite means BLOB affinity + return Self::String; + } + if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") { + return Self::Number; + } + if upper.contains("BOOL") { + return Self::Boolean; + } + if upper.contains("DATE") || upper.contains("TIME") { + return Self::Date; + } + if upper.contains("NUMERIC") || upper.contains("DECIMAL") { + return Self::Number; + } + if upper.contains("JSON") { + return Self::Object; + } + // Default: SQLite NUMERIC affinity + let message = format!( + "The column {field_name} of type {field_type} in table {table_name} will be translated as any (unsupported SQLite type)" + ); + info!(message); + Self::Any + } + pub fn get_ts_field_from_annotation(annotated_type: &str) -> Self { if annotated_type == "string" { return Self::String; diff --git a/test-utils/src/sandbox.rs b/test-utils/src/sandbox.rs index 859e19fa..85f42cc4 100644 --- a/test-utils/src/sandbox.rs +++ b/test-utils/src/sandbox.rs @@ -46,6 +46,21 @@ impl TestConfig { config_file_name, } } + if db_type == "sqlite" { + return TestConfig { + db_type: "sqlite".into(), + file_extension: "ts".to_string(), + db_host: String::new(), + db_port: 0, + db_user: String::new(), + db_pass: None, + // db_name will be overridden per-test with the actual temp SQLite file path + db_name: ":memory:".to_string(), + generate_path, + generate_types, + config_file_name, + } + } TestConfig { db_type: "postgres".into(), file_extension: "ts".to_string(), @@ -148,6 +163,7 @@ $( let db_name = test_config.db_name; let config_file_name = test_config.config_file_name; let generate_path = test_config.generate_path; + let is_sqlite = db_type == "sqlite"; // SETUP let dir = tempdir()?; @@ -164,11 +180,14 @@ $( cmd.arg(parent_path.to_str().unwrap()) .arg(format!("--ext={file_extension}")) .arg(format!("--db-type={db_type}")) - .arg(format!("--db-host={db_host}")) - .arg(format!("--db-port={db_port}")) - .arg(format!("--db-user={db_user}")) .arg(format!("--db-name={db_name}")); + if !is_sqlite { + cmd.arg(format!("--db-host={db_host}")) + .arg(format!("--db-port={db_port}")) + .arg(format!("--db-user={db_user}")); + } + if &generate_path.is_some() == &true { let generate_path = generate_path.clone(); let generate_path = generate_path.unwrap(); @@ -190,11 +209,13 @@ $( cmd.arg(format!("--config={config_path}")); } - if (db_pass.is_some()) { - let db_pass = db_pass.unwrap(); - cmd.arg(format!("--db-pass={db_pass}")); - } else { - cmd.arg("--db-pass="); + if !is_sqlite { + if (db_pass.is_some()) { + let db_pass = db_pass.unwrap(); + cmd.arg(format!("--db-pass={db_pass}")); + } else { + cmd.arg("--db-pass="); + } } cmd.assert() @@ -248,6 +269,7 @@ $( let db_name = test_config.db_name; let config_file_name = test_config.config_file_name; let generate_path = test_config.generate_path; + let is_sqlite = db_type == "sqlite"; // SETUP let dir = tempdir()?; @@ -264,11 +286,14 @@ $( cmd.arg(parent_path.to_str().unwrap()) .arg(format!("--ext={file_extension}")) .arg(format!("--db-type={db_type}")) - .arg(format!("--db-host={db_host}")) - .arg(format!("--db-port={db_port}")) - .arg(format!("--db-user={db_user}")) .arg(format!("--db-name={db_name}")); + if !is_sqlite { + cmd.arg(format!("--db-host={db_host}")) + .arg(format!("--db-port={db_port}")) + .arg(format!("--db-user={db_user}")); + } + if &generate_path.is_some() == &true { let generate_path = generate_path.clone(); let generate_path = generate_path.unwrap(); @@ -290,11 +315,13 @@ $( cmd.arg(format!("--config={config_path}")); } - if (db_pass.is_some()) { - let db_pass = db_pass.unwrap(); - cmd.arg(format!("--db-pass={db_pass}")); - } else { - cmd.arg("--db-pass="); + if !is_sqlite { + if (db_pass.is_some()) { + let db_pass = db_pass.unwrap(); + cmd.arg(format!("--db-pass={db_pass}")); + } else { + cmd.arg("--db-pass="); + } } cmd.assert() diff --git a/tests/sqlite_query_parameters.rs b/tests/sqlite_query_parameters.rs new file mode 100644 index 00000000..b21c5abd --- /dev/null +++ b/tests/sqlite_query_parameters.rs @@ -0,0 +1,255 @@ +#[cfg(test)] +mod sqlite_query_parameters_tests { + use std::env; + use std::fs; + use std::io::Write; + use tempfile::tempdir; + + use assert_cmd::cargo::cargo_bin_cmd; + use pretty_assertions::assert_eq; + use test_utils::test_utils::TSString; + + /// Helper: creates a temporary SQLite database with the given schema, + /// then runs sqlx-ts on the given TS content, and returns the generated types. + fn run_sqlite_test( + schema_sql: &str, + ts_content: &str, + generate_types: bool, + ) -> Result<(String, String), Box> { + let dir = tempdir()?; + let parent_path = dir.path(); + + // Create the SQLite database and populate it with the schema + let db_path = parent_path.join("test.db"); + let conn = rusqlite::Connection::open(&db_path)?; + conn.execute_batch(schema_sql)?; + drop(conn); + + // Write the TS file + let file_path = parent_path.join("index.ts"); + let mut temp_file = fs::File::create(&file_path)?; + writeln!(temp_file, "{}", ts_content)?; + + // Run sqlx-ts + let mut cmd = cargo_bin_cmd!("sqlx-ts"); + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=sqlite") + .arg(format!("--db-name={}", db_path.display())); + + if generate_types { + cmd.arg("-g"); + } + + let output = cmd.output()?; + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + + assert!( + output.status.success(), + "sqlx-ts failed!\nstdout: {stdout}\nstderr: {stderr}" + ); + assert!( + stdout.contains("No SQL errors detected!"), + "Expected success message in stdout: {stdout}" + ); + + // Read generated types + let type_file_path = parent_path.join("index.queries.ts"); + let type_file = if type_file_path.exists() { + fs::read_to_string(type_file_path)? + } else { + String::new() + }; + + Ok((stdout, type_file)) + } + + #[test] + fn should_validate_simple_select() -> Result<(), Box> { + let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, price REAL);"; + + let ts_content = r#" +import { sql } from 'sqlx-ts' + +const someQuery = sql`SELECT * FROM items` +"#; + + let (_, type_file) = run_sqlite_test(schema, ts_content, true)?; + + let expected = r#" +export type SomeQueryParams = []; + +export interface ISomeQueryResult { + id: number; + name: string; + price: number | null; +} + +export interface ISomeQueryQuery { + params: SomeQueryParams; + result: ISomeQueryResult; +} +"#; + + assert_eq!( + expected.trim().to_string().flatten(), + type_file.trim().to_string().flatten() + ); + Ok(()) + } + + #[test] + fn should_handle_query_params_with_question_mark() -> Result<(), Box> { + let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, price REAL);"; + + let ts_content = r#" +import { sql } from 'sqlx-ts' + +const someQuery = sql`SELECT * FROM items WHERE id = ? AND name = ?` +"#; + + let (_, type_file) = run_sqlite_test(schema, ts_content, true)?; + + let expected = r#" +export type SomeQueryParams = [number, string]; + +export interface ISomeQueryResult { + id: number; + name: string; + price: number | null; +} + +export interface ISomeQueryQuery { + params: SomeQueryParams; + result: ISomeQueryResult; +} +"#; + + assert_eq!( + expected.trim().to_string().flatten(), + type_file.trim().to_string().flatten() + ); + Ok(()) + } + + #[test] + fn should_handle_insert_with_params() -> Result<(), Box> { + let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, price REAL);"; + + let ts_content = r#" +import { sql } from 'sqlx-ts' + +const someQuery = sql`INSERT INTO items (name, price) VALUES (?, ?)` +"#; + + let (_, type_file) = run_sqlite_test(schema, ts_content, true)?; + + let expected = r#" +export type SomeQueryParams = [[string, number | null]]; + +export interface ISomeQueryResult { +} + +export interface ISomeQueryQuery { + params: SomeQueryParams; + result: ISomeQueryResult; +} +"#; + + assert_eq!( + expected.trim().to_string().flatten(), + type_file.trim().to_string().flatten() + ); + Ok(()) + } + + #[test] + fn should_handle_multiple_types() -> Result<(), Box> { + let schema = r" + CREATE TABLE events ( + id INTEGER PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + description TEXT, + start_date DATETIME, + is_active BOOLEAN NOT NULL DEFAULT 1, + score REAL, + metadata JSON + ); + "; + + let ts_content = r#" +import { sql } from 'sqlx-ts' + +const someQuery = sql`SELECT * FROM events WHERE id = ?` +"#; + + let (_, type_file) = run_sqlite_test(schema, ts_content, true)?; + + let expected = r#" +export type SomeQueryParams = [number]; + +export interface ISomeQueryResult { + description: string | null; + id: number; + is_active: boolean; + metadata: object | null; + name: string; + score: number | null; + start_date: Date | null; +} + +export interface ISomeQueryQuery { + params: SomeQueryParams; + result: ISomeQueryResult; +} +"#; + + assert_eq!( + expected.trim().to_string().flatten(), + type_file.trim().to_string().flatten() + ); + Ok(()) + } + + #[test] + fn should_detect_invalid_sql() -> Result<(), Box> { + let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL);"; + + let ts_content = r#" +import { sql } from 'sqlx-ts' + +const someQuery = sql`SELECT * FROM nonexistent_table` +"#; + + let dir = tempdir()?; + let parent_path = dir.path(); + + let db_path = parent_path.join("test.db"); + let conn = rusqlite::Connection::open(&db_path)?; + conn.execute_batch(schema)?; + drop(conn); + + let file_path = parent_path.join("index.ts"); + let mut temp_file = fs::File::create(&file_path)?; + writeln!(temp_file, "{}", ts_content)?; + + let mut cmd = cargo_bin_cmd!("sqlx-ts"); + cmd + .arg(parent_path.to_str().unwrap()) + .arg("--ext=ts") + .arg("--db-type=sqlite") + .arg(format!("--db-name={}", db_path.display())); + + // This should fail because the table doesn't exist + let output = cmd.output()?; + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + // The command should report SQL errors + assert!( + !stdout.contains("No SQL errors detected!"), + "Expected SQL errors but got success: {stdout}" + ); + Ok(()) + } +} From 2c2b4b5614ee48272341b7f885a7673c27427a3e Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sun, 29 Mar 2026 21:53:01 +1100 Subject: [PATCH 02/15] fmt --- src/common/dotenv.rs | 2 +- src/ts_generator/types/ts_query.rs | 6 +----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/common/dotenv.rs b/src/common/dotenv.rs index ddcfce42..effcaea1 100644 --- a/src/common/dotenv.rs +++ b/src/common/dotenv.rs @@ -36,7 +36,7 @@ impl Dotenv { "mysql" => Some(DatabaseType::Mysql), "sqlite" => Some(DatabaseType::Sqlite), _ => Some(DatabaseType::Postgres), - } + }, }, db_user: Self::get_var("DB_USER"), db_host: Self::get_var("DB_HOST"), diff --git a/src/ts_generator/types/ts_query.rs b/src/ts_generator/types/ts_query.rs index 6a3342d9..3c78ec8f 100644 --- a/src/ts_generator/types/ts_query.rs +++ b/src/ts_generator/types/ts_query.rs @@ -244,11 +244,7 @@ impl TsFieldType { /// Converts SQLite type affinity strings to TsFieldType. /// SQLite uses type affinity rules, so we match common type names. - pub fn get_ts_field_type_from_sqlite_field_type( - field_type: String, - table_name: String, - field_name: String, - ) -> Self { + pub fn get_ts_field_type_from_sqlite_field_type(field_type: String, table_name: String, field_name: String) -> Self { let upper = field_type.to_uppercase(); // SQLite type affinity rules (see https://www.sqlite.org/datatype3.html) if upper.contains("INT") { From b5a7c1c01d9afacd09e596a61a159234bb56c1d0 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sun, 29 Mar 2026 22:25:04 +1100 Subject: [PATCH 03/15] sqlite tests --- README.md | 4 +- book/docs/connect/README.md | 8 ++ book/docs/connect/config-file.md | 18 ++- book/docs/connect/environment-variables.md | 13 ++- playpen/db/sqlite_migration.sql | 124 +++++++++++++++++++++ src/core/connection.rs | 1 + src/ts_generator/information_schema.rs | 18 +-- tests/demo_happy_path.rs | 77 +++++++++++++ tests/demo_sqlite/delete_basic.queries.ts | 21 ++++ tests/demo_sqlite/delete_basic.snapshot.ts | 22 ++++ tests/demo_sqlite/delete_basic.ts | 11 ++ tests/demo_sqlite/insert_basic.queries.ts | 21 ++++ tests/demo_sqlite/insert_basic.snapshot.ts | 22 ++++ tests/demo_sqlite/insert_basic.ts | 11 ++ tests/demo_sqlite/join_basic.queries.ts | 13 +++ tests/demo_sqlite/join_basic.snapshot.ts | 14 +++ tests/demo_sqlite/join_basic.ts | 9 ++ tests/demo_sqlite/select_basic.queries.ts | 41 +++++++ tests/demo_sqlite/select_basic.snapshot.ts | 42 +++++++ tests/demo_sqlite/select_basic.ts | 16 +++ tests/demo_sqlite/update_basic.queries.ts | 21 ++++ tests/demo_sqlite/update_basic.snapshot.ts | 22 ++++ tests/demo_sqlite/update_basic.ts | 11 ++ 23 files changed, 544 insertions(+), 16 deletions(-) create mode 100644 playpen/db/sqlite_migration.sql create mode 100644 tests/demo_sqlite/delete_basic.queries.ts create mode 100644 tests/demo_sqlite/delete_basic.snapshot.ts create mode 100644 tests/demo_sqlite/delete_basic.ts create mode 100644 tests/demo_sqlite/insert_basic.queries.ts create mode 100644 tests/demo_sqlite/insert_basic.snapshot.ts create mode 100644 tests/demo_sqlite/insert_basic.ts create mode 100644 tests/demo_sqlite/join_basic.queries.ts create mode 100644 tests/demo_sqlite/join_basic.snapshot.ts create mode 100644 tests/demo_sqlite/join_basic.ts create mode 100644 tests/demo_sqlite/select_basic.queries.ts create mode 100644 tests/demo_sqlite/select_basic.snapshot.ts create mode 100644 tests/demo_sqlite/select_basic.ts create mode 100644 tests/demo_sqlite/update_basic.queries.ts create mode 100644 tests/demo_sqlite/update_basic.snapshot.ts create mode 100644 tests/demo_sqlite/update_basic.ts diff --git a/README.md b/README.md index f1002898..6248a838 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ SQLx-ts is a CLI application featuring compile-time checked queries without a DSL and generates types against SQLs to keep your code type-safe - **Compile time checked queries** - never ship a broken SQL query to production (and [sqlx-ts is not an ORM](https://github.com/JasonShin/sqlx-ts#sqlx-ts-is-not-an-orm)) -- **TypeScript type generations** - generates type definitions based on the raw SQLs and you can use them with any MySQL or PostgreSQL driver -- **Database Agnostic** - support for [PostgreSQL](http://postgresql.org/) and [MySQL](https://www.mysql.com/) (and more DB supports to come) +- **TypeScript type generations** - generates type definitions based on the raw SQLs and you can use them with any MySQL, PostgreSQL, or SQLite driver +- **Database Agnostic** - support for [PostgreSQL](http://postgresql.org/), [MySQL](https://www.mysql.com/), and [SQLite](https://www.sqlite.org/) - **TypeScript and JavaScript** - supports for both [TypeScript](https://jasonshin.github.io/sqlx-ts/reference-guide/4.typescript-types-generation.html) and [JavaScript](https://github.com/JasonShin/sqlx-ts#using-sqlx-ts-in-vanilla-javascript)
diff --git a/book/docs/connect/README.md b/book/docs/connect/README.md index 41768914..32de2392 100644 --- a/book/docs/connect/README.md +++ b/book/docs/connect/README.md @@ -46,6 +46,14 @@ $ sqlx-ts --db-type postgres --db-url postgres://user:pass@localhost:5432 $ sqlx-ts --db-type mysql --db-url mysql://user:pass@localhost:3306/mydb ``` +#### SQLite + +For SQLite, you only need to provide the database file path. No host, port, or user credentials are required: + +```bash +$ sqlx-ts --db-type sqlite --db-name ./mydb.sqlite +``` + **Note:** When `--db-url` is provided, it takes precedence over individual connection parameters (`--db-host`, `--db-port`, `--db-user`, `--db-pass`, `--db-name`). Run the following command for more details: diff --git a/book/docs/connect/config-file.md b/book/docs/connect/config-file.md index a0280fda..15efa981 100644 --- a/book/docs/connect/config-file.md +++ b/book/docs/connect/config-file.md @@ -66,6 +66,22 @@ Alternatively, you can use `DB_URL` to specify the connection string directly: } ``` +For SQLite, only `DB_TYPE` and `DB_NAME` (the file path) are required: + +```json +{ + "generate_types": { + "enabled": true + }, + "connections": { + "default": { + "DB_TYPE": "sqlite", + "DB_NAME": "./mydb.sqlite" + } + } +} +``` + ## Configuration options ### connections (required) @@ -92,7 +108,7 @@ const postgresSQL = sql` Supported fields of each connection include - `DB_URL`: Database connection URL (e.g. `postgres://user:pass@host:port/dbname` or `mysql://user:pass@host:port/dbname`). If provided, this overrides individual connection parameters (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`) -- `DB_TYPE`: type of database connection (mysql | postgres) +- `DB_TYPE`: type of database connection (mysql | postgres | sqlite) - `DB_USER`: database user name - `DB_PASS`: database password - `DB_HOST`: database host (e.g. 127.0.0.1) diff --git a/book/docs/connect/environment-variables.md b/book/docs/connect/environment-variables.md index d57574ad..08210770 100644 --- a/book/docs/connect/environment-variables.md +++ b/book/docs/connect/environment-variables.md @@ -7,7 +7,7 @@ | DB_HOST | Primary DB host | | DB_PASS | Primary DB password | | DB_PORT | Primary DB port number | -| DB_TYPE | Type of primary database to connect [default: postgres] [possible values: postgres, mysql] | +| DB_TYPE | Type of primary database to connect [default: postgres] [possible values: postgres, mysql, sqlite] | | DB_USER | Primary DB user name | | DB_NAME | Primary DB name | | PG_SEARCH_PATH | PostgreSQL schema search path (default is "$user,public") [https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH](https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH) | @@ -47,3 +47,14 @@ sqlx-ts **Note:** When `DB_URL` is set, it takes precedence over individual connection parameters (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`). +### SQLite + +For SQLite, only `DB_TYPE` and `DB_NAME` (file path) are required: + +```bash +export DB_TYPE=sqlite +export DB_NAME=./mydb.sqlite + +sqlx-ts +``` + diff --git a/playpen/db/sqlite_migration.sql b/playpen/db/sqlite_migration.sql new file mode 100644 index 00000000..827ae379 --- /dev/null +++ b/playpen/db/sqlite_migration.sql @@ -0,0 +1,124 @@ +-- SQLite Migration +-- This migration creates the same tables as the PostgreSQL and MySQL migrations +-- but using SQLite-compatible syntax. + +-- Factions Table +CREATE TABLE IF NOT EXISTS factions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + description TEXT +); + +-- Races Table +CREATE TABLE IF NOT EXISTS races ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + faction_id INTEGER REFERENCES factions(id) ON DELETE CASCADE +); + +-- Classes Table +CREATE TABLE IF NOT EXISTS classes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + specialization TEXT +); + +-- Characters Table +CREATE TABLE IF NOT EXISTS characters ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + race_id INTEGER REFERENCES races(id), + class_id INTEGER REFERENCES classes(id), + level INTEGER DEFAULT 1, + experience INTEGER DEFAULT 0, + gold REAL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Guilds Table +CREATE TABLE IF NOT EXISTS guilds ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT UNIQUE NOT NULL, + description TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- Guild Members Table +CREATE TABLE IF NOT EXISTS guild_members ( + guild_id INTEGER REFERENCES guilds(id) ON DELETE CASCADE, + character_id INTEGER REFERENCES characters(id) ON DELETE CASCADE, + rank TEXT, + joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (guild_id, character_id) +); + +-- Inventory Table +CREATE TABLE IF NOT EXISTS inventory ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + character_id INTEGER REFERENCES characters(id) ON DELETE CASCADE, + quantity INTEGER DEFAULT 1 +); + +-- Items Table +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + rarity TEXT, + flavor_text TEXT, + inventory_id INTEGER REFERENCES inventory(id) ON DELETE CASCADE +); + +-- Quests Table +CREATE TABLE IF NOT EXISTS quests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + rewards TEXT, + completed BOOLEAN DEFAULT 0, + required_level INTEGER DEFAULT 1 +); + +-- Character Quests Table +CREATE TABLE IF NOT EXISTS character_quests ( + character_id INTEGER REFERENCES characters(id) ON DELETE CASCADE, + quest_id INTEGER REFERENCES quests(id) ON DELETE CASCADE, + status TEXT DEFAULT 'In Progress', + PRIMARY KEY (character_id, quest_id) +); + +-- Random types table for testing SQLite type mappings +CREATE TABLE IF NOT EXISTS random ( + int1 INTEGER, + real1 REAL, + text1 TEXT, + blob1 BLOB, + numeric1 NUMERIC, + bool1 BOOLEAN, + date1 DATE, + datetime1 DATETIME, + float1 FLOAT, + double1 DOUBLE, + varchar1 VARCHAR(100), + char1 CHAR(10), + json1 JSON +); + +--- SEED DATA + +INSERT INTO factions (name, description) VALUES +('alliance', 'The noble and righteous faction'), +('horde', 'The fierce and battle-hardened faction'); + +INSERT INTO races (name, faction_id) VALUES +('human', 1), +('night elf', 1), +('dwarf', 1), +('gnome', 1), +('orc', 2), +('troll', 2), +('tauren', 2), +('undead', 2); + +INSERT INTO classes (name, specialization) VALUES +('warrior', '{"role": "tank", "weapon": "sword", "abilities": ["charge", "slam", "shield block"]}'), +('hunter', '{"role": "ranged", "weapon": "bow", "abilities": ["aimed shot", "multi-shot", "trap"]}'); diff --git a/src/core/connection.rs b/src/core/connection.rs index 6493ba71..01129926 100644 --- a/src/core/connection.rs +++ b/src/core/connection.rs @@ -18,6 +18,7 @@ use color_eyre::Result; use swc_common::errors::Handler; /// Enum to hold a specific database connection instance +#[allow(clippy::enum_variant_names)] pub enum DBConn { MySQLPooledConn(Mutex>), PostgresConn(Mutex>), diff --git a/src/ts_generator/information_schema.rs b/src/ts_generator/information_schema.rs index e8d5fe59..5814ed1e 100644 --- a/src/ts_generator/information_schema.rs +++ b/src/ts_generator/information_schema.rs @@ -246,18 +246,12 @@ impl DBSchema { Err(_) => continue, }; - for row in rows { - if let Ok((field_name, field_type, notnull, tbl_name)) = row { - let field = Field { - field_type: TsFieldType::get_ts_field_type_from_sqlite_field_type( - field_type, - tbl_name, - field_name.clone(), - ), - is_nullable: !notnull, - }; - all_fields.insert(field_name, field); - } + for (field_name, field_type, notnull, tbl_name) in rows.flatten() { + let field = Field { + field_type: TsFieldType::get_ts_field_type_from_sqlite_field_type(field_type, tbl_name, field_name.clone()), + is_nullable: !notnull, + }; + all_fields.insert(field_name, field); } } diff --git a/tests/demo_happy_path.rs b/tests/demo_happy_path.rs index 9ce9c394..43f8d4a5 100644 --- a/tests/demo_happy_path.rs +++ b/tests/demo_happy_path.rs @@ -7,6 +7,7 @@ mod demo_happy_path_tests { use std::fs; use std::io::Write; use std::path::Path; + use tempfile::tempdir; use walkdir::WalkDir; fn run_demo_test(demo_path: &Path) -> Result<(), Box> { @@ -283,4 +284,80 @@ mod demo_happy_path_tests { Ok(()) } + + #[test] + fn all_demo_sqlite_should_pass() -> Result<(), Box> { + let root_path = current_dir().unwrap(); + let demo_path = root_path.join("tests/demo_sqlite"); + let migration_path = root_path.join("playpen/db/sqlite_migration.sql"); + + // Create a temporary SQLite database and run the migration + let tmp_dir = tempdir()?; + let db_path = tmp_dir.path().join("demo_test.db"); + let conn = rusqlite::Connection::open(&db_path)?; + let migration_sql = fs::read_to_string(&migration_path)?; + conn.execute_batch(&migration_sql)?; + drop(conn); + + // Create a temporary config file pointing to the SQLite database + let config_path = tmp_dir.path().join(".sqlxrc.json"); + let config_content = format!( + r#"{{ + "generateTypes": {{ + "enabled": true + }}, + "connections": {{ + "default": {{ + "DB_TYPE": "sqlite", + "DB_NAME": "{}" + }} + }} +}}"#, + db_path.display() + ); + fs::write(&config_path, &config_content)?; + + // Run sqlx-ts against the demo_sqlite directory + let mut cmd = cargo_bin_cmd!("sqlx-ts"); + cmd + .arg(demo_path.to_str().unwrap()) + .arg("--ext=ts") + .arg(format!("--config={}", config_path.display())) + .arg("-g"); + + cmd + .assert() + .success() + .stdout(predicates::str::contains("No SQL errors detected!")); + + // Verify all generated types match snapshots + for entry in WalkDir::new(&demo_path) { + if entry.is_ok() { + let entry = entry.unwrap(); + let path = entry.path(); + let parent = entry.path().parent().unwrap(); + let file_name = path.file_name().unwrap().to_str().unwrap().to_string(); + + if path.is_file() && file_name.ends_with(".queries.ts") { + let base_file_name = file_name.split('.').collect::>(); + let base_file_name = base_file_name.first().unwrap(); + let snapshot_path = parent.join(format!("{base_file_name}.snapshot.ts")); + + let generated_types = fs::read_to_string(path)?; + + if !snapshot_path.exists() { + let mut snapshot_file = fs::File::create(&snapshot_path)?; + writeln!(snapshot_file, "{generated_types}")?; + } + + assert_eq!( + generated_types.trim().to_string().trim(), + fs::read_to_string(&snapshot_path)?.to_string().trim(), + ) + } + } + } + + Ok(()) + } } diff --git a/tests/demo_sqlite/delete_basic.queries.ts b/tests/demo_sqlite/delete_basic.queries.ts new file mode 100644 index 00000000..de5a2dd7 --- /dev/null +++ b/tests/demo_sqlite/delete_basic.queries.ts @@ -0,0 +1,21 @@ +export type DeleteItemParams = [number]; + +export interface IDeleteItemResult { + +} + +export interface IDeleteItemQuery { + params: DeleteItemParams; + result: IDeleteItemResult; +} + +export type DeleteCharacterParams = [number]; + +export interface IDeleteCharacterResult { + +} + +export interface IDeleteCharacterQuery { + params: DeleteCharacterParams; + result: IDeleteCharacterResult; +} diff --git a/tests/demo_sqlite/delete_basic.snapshot.ts b/tests/demo_sqlite/delete_basic.snapshot.ts new file mode 100644 index 00000000..c37449f4 --- /dev/null +++ b/tests/demo_sqlite/delete_basic.snapshot.ts @@ -0,0 +1,22 @@ +export type DeleteItemParams = [number]; + +export interface IDeleteItemResult { + +} + +export interface IDeleteItemQuery { + params: DeleteItemParams; + result: IDeleteItemResult; +} + +export type DeleteCharacterParams = [number]; + +export interface IDeleteCharacterResult { + +} + +export interface IDeleteCharacterQuery { + params: DeleteCharacterParams; + result: IDeleteCharacterResult; +} + diff --git a/tests/demo_sqlite/delete_basic.ts b/tests/demo_sqlite/delete_basic.ts new file mode 100644 index 00000000..f86c04ee --- /dev/null +++ b/tests/demo_sqlite/delete_basic.ts @@ -0,0 +1,11 @@ +import { sql } from 'sqlx-ts' + +const deleteItem = sql` +-- @name: delete item +DELETE FROM items WHERE id = $1 +` + +const deleteCharacter = sql` +-- @name: delete character +DELETE FROM characters WHERE id = $1 +` diff --git a/tests/demo_sqlite/insert_basic.queries.ts b/tests/demo_sqlite/insert_basic.queries.ts new file mode 100644 index 00000000..ce6c9ba0 --- /dev/null +++ b/tests/demo_sqlite/insert_basic.queries.ts @@ -0,0 +1,21 @@ +export type InsertItemParams = [string, string | null, string | null, number | null]; + +export interface IInsertItemResult { + +} + +export interface IInsertItemQuery { + params: InsertItemParams; + result: IInsertItemResult; +} + +export type InsertCharacterParams = [string, number | null, number | null, number | null]; + +export interface IInsertCharacterResult { + +} + +export interface IInsertCharacterQuery { + params: InsertCharacterParams; + result: IInsertCharacterResult; +} diff --git a/tests/demo_sqlite/insert_basic.snapshot.ts b/tests/demo_sqlite/insert_basic.snapshot.ts new file mode 100644 index 00000000..8d833e7f --- /dev/null +++ b/tests/demo_sqlite/insert_basic.snapshot.ts @@ -0,0 +1,22 @@ +export type InsertItemParams = [string, string | null, string | null, number | null]; + +export interface IInsertItemResult { + +} + +export interface IInsertItemQuery { + params: InsertItemParams; + result: IInsertItemResult; +} + +export type InsertCharacterParams = [string, number | null, number | null, number | null]; + +export interface IInsertCharacterResult { + +} + +export interface IInsertCharacterQuery { + params: InsertCharacterParams; + result: IInsertCharacterResult; +} + diff --git a/tests/demo_sqlite/insert_basic.ts b/tests/demo_sqlite/insert_basic.ts new file mode 100644 index 00000000..1d17e91c --- /dev/null +++ b/tests/demo_sqlite/insert_basic.ts @@ -0,0 +1,11 @@ +import { sql } from 'sqlx-ts' + +const insertItem = sql` +-- @name: insert item +INSERT INTO items (name, rarity, flavor_text, inventory_id) VALUES ($1, $2, $3, $4) +` + +const insertCharacter = sql` +-- @name: insert character +INSERT INTO characters (name, race_id, class_id, level) VALUES ($1, $2, $3, $4) +` diff --git a/tests/demo_sqlite/join_basic.queries.ts b/tests/demo_sqlite/join_basic.queries.ts new file mode 100644 index 00000000..dcc4473b --- /dev/null +++ b/tests/demo_sqlite/join_basic.queries.ts @@ -0,0 +1,13 @@ +export type SelectItemsWithInventoryParams = [number | null]; + +export interface ISelectItemsWithInventoryResult { + inventory_quantity: number | null; + items_id: number; + items_name: string; + items_rarity: string | null; +} + +export interface ISelectItemsWithInventoryQuery { + params: SelectItemsWithInventoryParams; + result: ISelectItemsWithInventoryResult; +} diff --git a/tests/demo_sqlite/join_basic.snapshot.ts b/tests/demo_sqlite/join_basic.snapshot.ts new file mode 100644 index 00000000..735baf23 --- /dev/null +++ b/tests/demo_sqlite/join_basic.snapshot.ts @@ -0,0 +1,14 @@ +export type SelectItemsWithInventoryParams = [number | null]; + +export interface ISelectItemsWithInventoryResult { + inventory_quantity: number | null; + items_id: number; + items_name: string; + items_rarity: string | null; +} + +export interface ISelectItemsWithInventoryQuery { + params: SelectItemsWithInventoryParams; + result: ISelectItemsWithInventoryResult; +} + diff --git a/tests/demo_sqlite/join_basic.ts b/tests/demo_sqlite/join_basic.ts new file mode 100644 index 00000000..55e8cb4d --- /dev/null +++ b/tests/demo_sqlite/join_basic.ts @@ -0,0 +1,9 @@ +import { sql } from 'sqlx-ts' + +const selectItemsWithInventory = sql` +-- @name: select items with inventory +SELECT items.id, items.name, items.rarity, inventory.quantity +FROM items +JOIN inventory ON items.inventory_id = inventory.id +WHERE inventory.quantity > $1 +` diff --git a/tests/demo_sqlite/select_basic.queries.ts b/tests/demo_sqlite/select_basic.queries.ts new file mode 100644 index 00000000..e8bb2342 --- /dev/null +++ b/tests/demo_sqlite/select_basic.queries.ts @@ -0,0 +1,41 @@ +export type SelectAllItemsParams = []; + +export interface ISelectAllItemsResult { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface ISelectAllItemsQuery { + params: SelectAllItemsParams; + result: ISelectAllItemsResult; +} + +export type SelectItemByIdParams = [number]; + +export interface ISelectItemByIdResult { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface ISelectItemByIdQuery { + params: SelectItemByIdParams; + result: ISelectItemByIdResult; +} + +export type SelectItemsByNameParams = [string]; + +export interface ISelectItemsByNameResult { + id: number; + name: string; +} + +export interface ISelectItemsByNameQuery { + params: SelectItemsByNameParams; + result: ISelectItemsByNameResult; +} diff --git a/tests/demo_sqlite/select_basic.snapshot.ts b/tests/demo_sqlite/select_basic.snapshot.ts new file mode 100644 index 00000000..ce5421f5 --- /dev/null +++ b/tests/demo_sqlite/select_basic.snapshot.ts @@ -0,0 +1,42 @@ +export type SelectAllItemsParams = []; + +export interface ISelectAllItemsResult { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface ISelectAllItemsQuery { + params: SelectAllItemsParams; + result: ISelectAllItemsResult; +} + +export type SelectItemByIdParams = [number]; + +export interface ISelectItemByIdResult { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface ISelectItemByIdQuery { + params: SelectItemByIdParams; + result: ISelectItemByIdResult; +} + +export type SelectItemsByNameParams = [string]; + +export interface ISelectItemsByNameResult { + id: number; + name: string; +} + +export interface ISelectItemsByNameQuery { + params: SelectItemsByNameParams; + result: ISelectItemsByNameResult; +} + diff --git a/tests/demo_sqlite/select_basic.ts b/tests/demo_sqlite/select_basic.ts new file mode 100644 index 00000000..350cd7fc --- /dev/null +++ b/tests/demo_sqlite/select_basic.ts @@ -0,0 +1,16 @@ +import { sql } from 'sqlx-ts' + +const selectAllItems = sql` +-- @name: select all items +SELECT * FROM items +` + +const selectItemById = sql` +-- @name: select item by id +SELECT * FROM items WHERE id = $1 +` + +const selectItemsByName = sql` +-- @name: select items by name +SELECT id, name FROM items WHERE name = $1 +` diff --git a/tests/demo_sqlite/update_basic.queries.ts b/tests/demo_sqlite/update_basic.queries.ts new file mode 100644 index 00000000..4fa690b9 --- /dev/null +++ b/tests/demo_sqlite/update_basic.queries.ts @@ -0,0 +1,21 @@ +export type UpdateItemNameParams = [string, number]; + +export interface IUpdateItemNameResult { + +} + +export interface IUpdateItemNameQuery { + params: UpdateItemNameParams; + result: IUpdateItemNameResult; +} + +export type UpdateCharacterLevelParams = [number | null, unknown | null, number]; + +export interface IUpdateCharacterLevelResult { + +} + +export interface IUpdateCharacterLevelQuery { + params: UpdateCharacterLevelParams; + result: IUpdateCharacterLevelResult; +} diff --git a/tests/demo_sqlite/update_basic.snapshot.ts b/tests/demo_sqlite/update_basic.snapshot.ts new file mode 100644 index 00000000..c6a08264 --- /dev/null +++ b/tests/demo_sqlite/update_basic.snapshot.ts @@ -0,0 +1,22 @@ +export type UpdateItemNameParams = [string, number]; + +export interface IUpdateItemNameResult { + +} + +export interface IUpdateItemNameQuery { + params: UpdateItemNameParams; + result: IUpdateItemNameResult; +} + +export type UpdateCharacterLevelParams = [number | null, unknown | null, number]; + +export interface IUpdateCharacterLevelResult { + +} + +export interface IUpdateCharacterLevelQuery { + params: UpdateCharacterLevelParams; + result: IUpdateCharacterLevelResult; +} + diff --git a/tests/demo_sqlite/update_basic.ts b/tests/demo_sqlite/update_basic.ts new file mode 100644 index 00000000..8e7d3493 --- /dev/null +++ b/tests/demo_sqlite/update_basic.ts @@ -0,0 +1,11 @@ +import { sql } from 'sqlx-ts' + +const updateItemName = sql` +-- @name: update item name +UPDATE items SET name = $1 WHERE id = $2 +` + +const updateCharacterLevel = sql` +-- @name: update character level +UPDATE characters SET level = $1, experience = $2 WHERE id = $3 +` From e42f6c10acf6b3009a95724d84a7851aac12cead Mon Sep 17 00:00:00 2001 From: JasonShin Date: Fri, 3 Apr 2026 00:09:02 +1100 Subject: [PATCH 04/15] fix --- src/ts_generator/information_schema.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ts_generator/information_schema.rs b/src/ts_generator/information_schema.rs index 5814ed1e..79fd86ef 100644 --- a/src/ts_generator/information_schema.rs +++ b/src/ts_generator/information_schema.rs @@ -240,16 +240,17 @@ impl DBSchema { let name: String = row.get(1)?; let type_name: String = row.get(2)?; let notnull: bool = row.get(3)?; - Ok((name, type_name, notnull, table_name.clone())) + let pk: i32 = row.get(5)?; + Ok((name, type_name, notnull, pk, table_name.clone())) }) { Ok(rows) => rows, Err(_) => continue, }; - for (field_name, field_type, notnull, tbl_name) in rows.flatten() { + for (field_name, field_type, notnull, pk, tbl_name) in rows.flatten() { let field = Field { field_type: TsFieldType::get_ts_field_type_from_sqlite_field_type(field_type, tbl_name, field_name.clone()), - is_nullable: !notnull, + is_nullable: !notnull && pk == 0, }; all_fields.insert(field_name, field); } From 9363c29f752b747115fd788a8b3e2dac3ec180a3 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Fri, 3 Apr 2026 00:42:58 +1100 Subject: [PATCH 05/15] fix --- tests/demo_happy_path.rs | 3 +++ tests/demo_sqlite/update_basic.queries.ts | 2 +- tests/demo_sqlite/update_basic.snapshot.ts | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/demo_happy_path.rs b/tests/demo_happy_path.rs index 43f8d4a5..75bb631b 100644 --- a/tests/demo_happy_path.rs +++ b/tests/demo_happy_path.rs @@ -318,11 +318,14 @@ mod demo_happy_path_tests { fs::write(&config_path, &config_content)?; // Run sqlx-ts against the demo_sqlite directory + // Use --db-type and --db-name CLI args to override any .env file values let mut cmd = cargo_bin_cmd!("sqlx-ts"); cmd .arg(demo_path.to_str().unwrap()) .arg("--ext=ts") .arg(format!("--config={}", config_path.display())) + .arg("--db-type=sqlite") + .arg(format!("--db-name={}", db_path.display())) .arg("-g"); cmd diff --git a/tests/demo_sqlite/update_basic.queries.ts b/tests/demo_sqlite/update_basic.queries.ts index 4fa690b9..094ff6b8 100644 --- a/tests/demo_sqlite/update_basic.queries.ts +++ b/tests/demo_sqlite/update_basic.queries.ts @@ -9,7 +9,7 @@ export interface IUpdateItemNameQuery { result: IUpdateItemNameResult; } -export type UpdateCharacterLevelParams = [number | null, unknown | null, number]; +export type UpdateCharacterLevelParams = [number | null, number | null, number]; export interface IUpdateCharacterLevelResult { diff --git a/tests/demo_sqlite/update_basic.snapshot.ts b/tests/demo_sqlite/update_basic.snapshot.ts index c6a08264..5072cf08 100644 --- a/tests/demo_sqlite/update_basic.snapshot.ts +++ b/tests/demo_sqlite/update_basic.snapshot.ts @@ -9,7 +9,7 @@ export interface IUpdateItemNameQuery { result: IUpdateItemNameResult; } -export type UpdateCharacterLevelParams = [number | null, unknown | null, number]; +export type UpdateCharacterLevelParams = [number | null, number | null, number]; export interface IUpdateCharacterLevelResult { From d6c72472ffb69461f58a3831cba850120f7ac06b Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:08:10 +1000 Subject: [PATCH 06/15] demo github action --- .github/workflows/demo.yaml | 149 ++++++++ README.md | 2 +- demo/.gitignore | 5 + demo/.sqlxrc.json | 27 ++ demo/README.md | 25 ++ demo/create-sqlite.sh | 9 + demo/mysql2/index.queries.ts | 47 +++ demo/mysql2/index.ts | 47 +++ demo/package-lock.json | 610 ++++++++++++++++++++++++++++++++ demo/package.json | 25 ++ demo/pg/index.queries.ts | 58 +++ demo/pg/index.ts | 58 +++ demo/sequelize/index.queries.ts | 47 +++ demo/sequelize/index.ts | 60 ++++ demo/setup.sql | 39 ++ demo/sqlite/index.queries.ts | 92 +++++ demo/sqlite/index.ts | 62 ++++ demo/tsconfig.json | 10 + 18 files changed, 1371 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/demo.yaml create mode 100644 demo/.gitignore create mode 100644 demo/.sqlxrc.json create mode 100644 demo/README.md create mode 100755 demo/create-sqlite.sh create mode 100644 demo/mysql2/index.queries.ts create mode 100644 demo/mysql2/index.ts create mode 100644 demo/package-lock.json create mode 100644 demo/package.json create mode 100644 demo/pg/index.queries.ts create mode 100644 demo/pg/index.ts create mode 100644 demo/sequelize/index.queries.ts create mode 100644 demo/sequelize/index.ts create mode 100644 demo/setup.sql create mode 100644 demo/sqlite/index.queries.ts create mode 100644 demo/sqlite/index.ts create mode 100644 demo/tsconfig.json diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml new file mode 100644 index 00000000..b37a67b5 --- /dev/null +++ b/.github/workflows/demo.yaml @@ -0,0 +1,149 @@ +name: demo + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + demo-test: + name: Demo type-generation (${{ matrix.db.name }}) + if: "!contains(github.event.head_commit.message, 'Release')" + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + db: + - name: postgres-16 + postgres: "16" + mysql: "" + - name: postgres-13 + postgres: "13" + mysql: "" + - name: mysql-8 + postgres: "" + mysql: "8" +# uncomment when we add SQLite support +# - name: sqlite +# postgres: "" +# mysql: "" + + steps: + - uses: actions/checkout@v4 + + - name: Install stable Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - uses: Swatinem/rust-cache@v2 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + cache-provider: "github" + + - name: Build sqlx-ts binary + run: cargo build --release + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Start database services + if: matrix.db.postgres != '' || matrix.db.mysql != '' + run: docker compose -f docker-compose.yml up -d + env: + PG_VERSION: ${{ matrix.db.postgres || '16' }} + MYSQL_VERSION: ${{ matrix.db.mysql || '8' }} + + - name: Wait for databases to be ready + if: matrix.db.postgres != '' || matrix.db.mysql != '' + run: | + echo "Waiting for databases to initialize..." + sleep 15 + if [ -n "${{ matrix.db.postgres }}" ]; then + until docker compose exec -T postgres pg_isready -U postgres 2>/dev/null; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + echo "PostgreSQL is ready" + fi + if [ -n "${{ matrix.db.mysql }}" ]; then + until docker compose exec -T mysql mysqladmin ping -h localhost --silent 2>/dev/null; do + echo "Waiting for MySQL..." + sleep 2 + done + echo "MySQL is ready" + fi + + - name: Create SQLite database + if: matrix.db.name == 'sqlite' + working-directory: ./demo + run: | + sudo apt-get update && sudo apt-get install -y sqlite3 + bash sqlite/create-db.sh + + - name: Install demo dependencies + working-directory: ./demo + run: npm install + + - name: Install sqlx-ts globally for demo + run: npm install -g sqlx-ts + + - name: Compile demo TypeScript code (to verify it compiles before generating types) + working-directory: ./demo + run: npm run compile:all + + - name: Copy sqlx-ts binary into node package + run: cp target/release/sqlx-ts node/sqlx-ts + + - name: Generate types (PostgreSQL demos) + if: matrix.db.postgres != '' + working-directory: ./demo + run: | + npx sqlx-ts ./pg --config ./.sqlxrc.json -g + npx sqlx-ts ./sequelize --config ./.sqlxrc.json -g + + - name: Generate types (MySQL demo) + if: matrix.db.mysql != '' + working-directory: ./demo + run: | + npx sqlx-ts ./mysql2 --config ./.sqlxrc.json -g + + - name: Generate types (SQLite demo) + if: matrix.db.name == 'sqlite' + working-directory: ./demo + run: | + npx sqlx-ts ./sqlite --config ./.sqlxrc.json -g + + - name: Type-check generated code + working-directory: ./demo + run: npm run build + + - name: Verify generated types are not empty + working-directory: ./demo + run: | + check_file() { + if [ ! -f "$1" ]; then + echo "FAIL: $1 not found" + exit 1 + fi + if [ ! -s "$1" ]; then + echo "FAIL: $1 is empty" + exit 1 + fi + echo "OK: $1" + } + if [ -n "${{ matrix.db.postgres }}" ]; then + check_file "pg/index.queries.ts" + check_file "sequelize/index.queries.ts" + fi + if [ -n "${{ matrix.db.mysql }}" ]; then + check_file "mysql2/index.queries.ts" + fi + if [ "${{ matrix.db.name }}" = "sqlite" ]; then + check_file "sqlite/index.queries.ts" + fi diff --git a/README.md b/README.md index 6248a838..be5ebd2e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ SQLx-ts is a CLI application featuring compile-time checked queries without a DS | - 🤓 Demo + 🤓 Demo
diff --git a/demo/.gitignore b/demo/.gitignore new file mode 100644 index 00000000..62d9aaee --- /dev/null +++ b/demo/.gitignore @@ -0,0 +1,5 @@ +node_modules +.idea +dist +.DS_Store +*.db \ No newline at end of file diff --git a/demo/.sqlxrc.json b/demo/.sqlxrc.json new file mode 100644 index 00000000..fa0e49b8 --- /dev/null +++ b/demo/.sqlxrc.json @@ -0,0 +1,27 @@ +{ + "generate_types": { + "enabled": true, + "convertToCamelCaseColumnName": true + }, + "connections": { + "default": { + "DB_TYPE": "postgres", + "DB_HOST": "127.0.0.1", + "DB_PORT": 54321, + "DB_USER": "postgres", + "DB_PASS": "postgres", + "DB_NAME": "postgres" + }, + "db_mysql": { + "DB_TYPE": "mysql", + "DB_HOST": "127.0.0.1", + "DB_PORT": 33306, + "DB_USER": "root", + "DB_NAME": "sqlx-ts" + }, + "db_sqlite": { + "DB_TYPE": "sqlite", + "DB_NAME": "./sqlite/demo.db" + } + } + } \ No newline at end of file diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 00000000..1fd51a9c --- /dev/null +++ b/demo/README.md @@ -0,0 +1,25 @@ +# sqlx-ts demo + +Usage examples showing sqlx-ts with different database drivers. + +## Examples + +- [pg](./pg) - PostgreSQL with `pg` driver +- [mysql2](./mysql2) - MySQL with `mysql2` driver +- [sequelize](./sequelize) - PostgreSQL with Sequelize ORM +- [sqlite](./sqlite) - SQLite with `better-sqlite3` (no Docker needed) + +## Running locally + +```bash +# Start databases (from repo root) +docker compose up -d + +# Install demo dependencies +cd demo +npm install + +# Generate types and type-check +npm run compile:all +npm run typecheck +``` diff --git a/demo/create-sqlite.sh b/demo/create-sqlite.sh new file mode 100755 index 00000000..0c75a415 --- /dev/null +++ b/demo/create-sqlite.sh @@ -0,0 +1,9 @@ +#!/bin/bash +# Creates the SQLite demo database from setup.sql +set -e +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +DB_PATH="$SCRIPT_DIR/sqlite/demo.db" + +rm -f "$DB_PATH" +sqlite3 "$DB_PATH" < "$SCRIPT_DIR/setup.sql" +echo "Created SQLite database at $DB_PATH" diff --git a/demo/mysql2/index.queries.ts b/demo/mysql2/index.queries.ts new file mode 100644 index 00000000..6d35739c --- /dev/null +++ b/demo/mysql2/index.queries.ts @@ -0,0 +1,47 @@ +export type GetItems2Params = []; + +export interface IGetItems2Result { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface IGetItems2Query { + params: GetItems2Params; + result: IGetItems2Result; +} + +export type TestInsertParams = [[number, string]]; + +export interface ITestInsertResult { + +} + +export interface ITestInsertQuery { + params: TestInsertParams; + result: ITestInsertResult; +} + +export type TestUpdateParams = [string | null]; + +export interface ITestUpdateResult { + +} + +export interface ITestUpdateQuery { + params: TestUpdateParams; + result: ITestUpdateResult; +} + +export type TestDeleteParams = [string | null]; + +export interface ITestDeleteResult { + +} + +export interface ITestDeleteQuery { + params: TestDeleteParams; + result: ITestDeleteResult; +} diff --git a/demo/mysql2/index.ts b/demo/mysql2/index.ts new file mode 100644 index 00000000..a8909cdb --- /dev/null +++ b/demo/mysql2/index.ts @@ -0,0 +1,47 @@ +import { sql } from 'sqlx-ts' +import * as mysql from 'mysql2/promise' +import { IGetItems2Result, TestInsertParams, TestUpdateParams, TestDeleteParams } from './index.queries' + +type Rows = Array + +(async () => { + + const connection = await mysql.createConnection({ + host: '127.0.0.1', + user: 'root', + port: 33306, + database: 'sqlx-ts', + }); + + const [rows] = await connection.execute>(sql` + -- @name: getItems2 + SELECT * FROM items + `) + + for (const row of rows) { + const { id, rarity, name } = row + console.log(id, rarity, name) + } + + await connection.execute(sql` + -- @name: testInsert + -- @db: db_mysql + INSERT INTO items (id, name, rarity, flavor_text) VALUES (?, ?, 'test', 'test'); + `, [1, 'test'] as TestInsertParams[0]) + + const rarityType = 'test' + + await connection.query(sql` + -- @name: testUpdate + -- @db: db_mysql + UPDATE items SET rarity = ? WHERE id = 1; + `, [rarityType] as TestUpdateParams) + + await connection.query(sql` + -- @name: testDelete + -- @db: db_mysql + DELETE FROM items WHERE rarity = ?; + `, [rarityType] as TestDeleteParams) + + connection.destroy() +})() diff --git a/demo/package-lock.json b/demo/package-lock.json new file mode 100644 index 00000000..037fa4c6 --- /dev/null +++ b/demo/package-lock.json @@ -0,0 +1,610 @@ +{ + "name": "sqlx-ts-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sqlx-ts-demo", + "version": "1.0.0", + "dependencies": { + "mysql2": "^3.5.2", + "pg": "^8.11.1", + "sequelize": "^6.32.1", + "sqlx-ts": "file:../node" + }, + "devDependencies": { + "@types/pg": "^8.10.2", + "@types/sequelize": "^4.28.14", + "typescript": "^5.1.6" + } + }, + "../node": { + "name": "sqlx-ts", + "version": "0.44.0", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "adm-zip": "^0.5.16" + }, + "bin": { + "sqlx-ts": "sqlx-ts" + }, + "devDependencies": { + "@types/jest": "^27.4.1", + "@types/node": "^20.14.8", + "@typescript-eslint/eslint-plugin": "^5.19.0", + "@typescript-eslint/parser": "^5.19.0", + "eslint": "^8.13.0", + "jest": "^27.5.1", + "ts-jest": "^27.1.4", + "typescript": "^4.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/bluebird": { + "version": "3.5.42", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", + "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/continuation-local-storage": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz", + "integrity": "sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "pg-protocol": "*", + "pg-types": "^2.2.0" + } + }, + "node_modules/@types/sequelize": { + "version": "4.28.20", + "resolved": "https://registry.npmjs.org/@types/sequelize/-/sequelize-4.28.20.tgz", + "integrity": "sha512-XaGOKRhdizC87hDgQ0u3btxzbejlF+t6Hhvkek1HyphqCI4y7zVBIVAGmuc4cWJqGpxusZ1RiBToHHnNK/Edlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bluebird": "*", + "@types/continuation-local-storage": "*", + "@types/lodash": "*", + "@types/validator": "*" + } + }, + "node_modules/@types/validator": { + "version": "13.15.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz", + "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", + "license": "MIT" + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dottie": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz", + "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT" + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "license": "MIT", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inflection": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", + "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", + "engines": [ + "node >= 0.4.0" + ], + "license": "MIT" + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/moment-timezone": { + "version": "0.5.48", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", + "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", + "license": "MIT", + "dependencies": { + "moment": "^2.29.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mysql2": { + "version": "3.22.2", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.2.tgz", + "integrity": "sha512-snC/L6YoCJPFpozZo3p3hiOlt9ItQ7sCnLSziFLlIttEzsPhrdcPT8g21BiQ7Oqif25W4Xq1IFuBzBvoFYDf0Q==", + "license": "MIT", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "license": "MIT", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/pg": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", + "license": "MIT", + "dependencies": { + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", + "pg-types": "2.2.0", + "pgpass": "1.0.5" + }, + "engines": { + "node": ">= 16.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.3.0" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz", + "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==", + "license": "MIT", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", + "license": "MIT" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "license": "ISC", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", + "license": "MIT", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", + "license": "MIT" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "license": "MIT", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "license": "MIT", + "dependencies": { + "split2": "^4.1.0" + } + }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz", + "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "license": "MIT", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/retry-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", + "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sequelize": { + "version": "6.37.8", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz", + "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/sequelize" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.1.8", + "@types/validator": "^13.7.17", + "debug": "^4.3.4", + "dottie": "^2.0.6", + "inflection": "^1.13.4", + "lodash": "^4.17.21", + "moment": "^2.29.4", + "moment-timezone": "^0.5.43", + "pg-connection-string": "^2.6.1", + "retry-as-promised": "^7.0.4", + "semver": "^7.5.4", + "sequelize-pool": "^7.1.0", + "toposort-class": "^1.0.1", + "uuid": "^8.3.2", + "validator": "^13.9.0", + "wkx": "^0.5.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependenciesMeta": { + "ibm_db": { + "optional": true + }, + "mariadb": { + "optional": true + }, + "mysql2": { + "optional": true + }, + "oracledb": { + "optional": true + }, + "pg": { + "optional": true + }, + "pg-hstore": { + "optional": true + }, + "snowflake-sdk": { + "optional": true + }, + "sqlite3": { + "optional": true + }, + "tedious": { + "optional": true + } + } + }, + "node_modules/sequelize-pool": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", + "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "license": "MIT", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/sqlx-ts": { + "resolved": "../node", + "link": true + }, + "node_modules/toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/wkx": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", + "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "license": "MIT", + "engines": { + "node": ">=0.4" + } + } + } +} diff --git a/demo/package.json b/demo/package.json new file mode 100644 index 00000000..94435da1 --- /dev/null +++ b/demo/package.json @@ -0,0 +1,25 @@ +{ + "name": "sqlx-ts-demo", + "version": "1.0.0", + "private": true, + "description": "Demo project showing sqlx-ts usage with pg, mysql2, and sequelize", + "scripts": { + "compile:pg": "../target/debug/sqlx-ts ./pg --config ./.sqlxrc.json -g", + "compile:mysql": "../target/debug/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g", + "compile:sequelize": "../target/debug/sqlx-ts ./sequelize --config ./.sqlxrc.json -g", + "compile:sqlite": "../target/debug/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", + "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize", + "build": "npm run compile:all && npm run typecheck", + "typecheck": "npx tsc -p ./tsconfig.json --noEmit" + }, + "dependencies": { + "mysql2": "^3.5.2", + "pg": "^8.11.1", + "sequelize": "^6.32.1" + }, + "devDependencies": { + "@types/pg": "^8.10.2", + "@types/sequelize": "^4.28.14", + "typescript": "^5.1.6" + } +} diff --git a/demo/pg/index.queries.ts b/demo/pg/index.queries.ts new file mode 100644 index 00000000..dc011bc3 --- /dev/null +++ b/demo/pg/index.queries.ts @@ -0,0 +1,58 @@ +export type SomeQueryParams = []; + +export interface ISomeQueryResult { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface ISomeQueryQuery { + params: SomeQueryParams; + result: ISomeQueryResult; +} + +export type TestInsertParams = [number, string]; + +export interface ITestInsertResult { + +} + +export interface ITestInsertQuery { + params: TestInsertParams; + result: ITestInsertResult; +} + +export type TestUpdateParams = [string | null]; + +export interface ITestUpdateResult { + +} + +export interface ITestUpdateQuery { + params: TestUpdateParams; + result: ITestUpdateResult; +} + +export type TestDeleteParams = [string | null]; + +export interface ITestDeleteResult { + +} + +export interface ITestDeleteQuery { + params: TestDeleteParams; + result: ITestDeleteResult; +} + +export type GetItemsParams = []; + +export interface IGetItemsResult { + inventoryId: number; +} + +export interface IGetItemsQuery { + params: GetItemsParams; + result: IGetItemsResult; +} diff --git a/demo/pg/index.ts b/demo/pg/index.ts new file mode 100644 index 00000000..fdc4370d --- /dev/null +++ b/demo/pg/index.ts @@ -0,0 +1,58 @@ +import { sql } from 'sqlx-ts' +import { Client } from 'pg' +import { + TestInsertParams, ITestInsertResult, + TestUpdateParams, ITestUpdateResult, + TestDeleteParams, ITestDeleteResult, IGetItemsResult, + } from './index.queries' + + +const client = new Client({ + host: 'localhost', + port: 54321, + database: 'postgres', + user: 'postgres', + password: 'postgres', +}); + +(async () => { + const someQuery = await client.query(sql` + SELECT * FROM items; + `) + + for (const row of someQuery.rows) { + const { id, food_type, points } = row + console.log(id, food_type, points) + } + + await client.query(sql` + -- @name: testInsert + INSERT INTO items (id, name, rarity, flavor_text) VALUES ($1, $2, 'test', 'test'); + `, [1, "hello"]) + + const rarityType = 'test' + + await client.query(sql` + -- @name: testUpdate + UPDATE items SET rarity = $1 WHERE id = (SELECT id FROM items WHERE rarity = 'test' LIMIT 1); + `, [rarityType]) + + await client.query(sql` + -- @name: testDelete + DELETE FROM items WHERE rarity = $1; + `, [rarityType]) + + await client.end() + + class TestQueryRepository { + getItems() { + return client.query(sql` + -- @name: getItems + SELECT inventory.id as inventoryId FROM items + JOIN inventory ON items.inventory_id = inventory.id; + `) + } + } + + new TestQueryRepository() +})(); diff --git a/demo/sequelize/index.queries.ts b/demo/sequelize/index.queries.ts new file mode 100644 index 00000000..a9af09fe --- /dev/null +++ b/demo/sequelize/index.queries.ts @@ -0,0 +1,47 @@ +export type SomeQueryParams = []; + +export interface ISomeQueryResult { + flavor_text: string | null; + id: number; + inventory_id: number | null; + name: string; + rarity: string | null; +} + +export interface ISomeQueryQuery { + params: SomeQueryParams; + result: ISomeQueryResult; +} + +export type TestInsertParams = [number, string]; + +export interface ITestInsertResult { + +} + +export interface ITestInsertQuery { + params: TestInsertParams; + result: ITestInsertResult; +} + +export type TestUpdateParams = [string | null]; + +export interface ITestUpdateResult { + +} + +export interface ITestUpdateQuery { + params: TestUpdateParams; + result: ITestUpdateResult; +} + +export type TestDeleteParams = [string | null]; + +export interface ITestDeleteResult { + +} + +export interface ITestDeleteQuery { + params: TestDeleteParams; + result: ITestDeleteResult; +} diff --git a/demo/sequelize/index.ts b/demo/sequelize/index.ts new file mode 100644 index 00000000..fcf72cab --- /dev/null +++ b/demo/sequelize/index.ts @@ -0,0 +1,60 @@ +import {sql} from 'sqlx-ts' +import {QueryTypes, Sequelize} from 'sequelize' +import { + ISomeQueryResult, + TestInsertParams, + TestUpdateParams, + TestDeleteParams, + } from "./index.queries"; + + +const sequelize = new Sequelize('postgres://postgres:postgres@127.0.0.1:54321', { + dialect: 'postgres' +}) + +async function demo() { + const someQuery = await sequelize.query(sql` + SELECT * FROM items; + `, { + type: QueryTypes.SELECT, + replacements: [], + }) + + for (const row of someQuery) { + const { id, rarity, name } = row + console.log(id, rarity, name) + } + + await sequelize.query(sql` + -- @name: testInsert + INSERT INTO items (id, name, rarity, flavor_text) VALUES ($1, $2, 'test', 'test'); + `, { + type: QueryTypes.INSERT, + // Unfortunately sequelize query does not allow you to type binding params for INSERT + bind: [1, 'test'] as TestInsertParams, + }) + + const rarityType = 'rare' + + await sequelize.query(sql` + -- @name: testUpdate + UPDATE items SET rarity = $1 WHERE id = (SELECT id FROM items WHERE rarity = 'test' LIMIT 1); + `, { + type: QueryTypes.UPDATE, + // Unfortunately sequelize query does not allow you to type binding params for UPDATE + bind: [rarityType] as TestUpdateParams, + }) + + await sequelize.query(sql` + -- @name: testDelete + DELETE FROM items WHERE rarity = $1; + `, { + type: QueryTypes.DELETE, + bind: [rarityType] as TestDeleteParams, + }) +} + +(async () => { + await demo() + await sequelize.close() +})(); diff --git a/demo/setup.sql b/demo/setup.sql new file mode 100644 index 00000000..4ee50656 --- /dev/null +++ b/demo/setup.sql @@ -0,0 +1,39 @@ +CREATE TABLE IF NOT EXISTS tables ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + number INTEGER NOT NULL, + occupied BOOLEAN NOT NULL DEFAULT 0 +); + +CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + food_type TEXT NOT NULL, + time_takes_to_cook INTEGER NOT NULL, + table_id INTEGER NOT NULL, + points INTEGER NOT NULL, + FOREIGN KEY (table_id) REFERENCES tables (id) +); + +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + name TEXT NOT NULL, + description TEXT, + start_date DATETIME, + is_active BOOLEAN NOT NULL DEFAULT 1, + score REAL, + metadata JSON +); + +INSERT INTO tables (number) VALUES (1), (2), (3), (4), (5); + +INSERT INTO items (food_type, time_takes_to_cook, table_id, points) +VALUES + ('korean', 10, 1, 2), + ('chinese', 10, 1, 2), + ('japanese', 10, 1, 2), + ('italian', 10, 1, 2), + ('french', 10, 1, 2); + +INSERT INTO events (name, description, start_date, is_active, score) +VALUES + ('Lunch Special', 'Daily lunch menu', '2024-01-15 12:00:00', 1, 4.5), + ('Happy Hour', NULL, '2024-01-15 17:00:00', 1, 3.8); diff --git a/demo/sqlite/index.queries.ts b/demo/sqlite/index.queries.ts new file mode 100644 index 00000000..f8736182 --- /dev/null +++ b/demo/sqlite/index.queries.ts @@ -0,0 +1,92 @@ +export type GetAllItemsParams = []; + +export interface IGetAllItemsResult { + food_type: string; + id: number; + points: number; + table_id: number; + time_takes_to_cook: number; +} + +export interface IGetAllItemsQuery { + params: GetAllItemsParams; + result: IGetAllItemsResult; +} + +export type GetItemsByFoodTypeParams = [string]; + +export interface IGetItemsByFoodTypeResult { + food_type: string; + id: number; + points: number; + table_id: number; + time_takes_to_cook: number; +} + +export interface IGetItemsByFoodTypeQuery { + params: GetItemsByFoodTypeParams; + result: IGetItemsByFoodTypeResult; +} + +export type InsertItemParams = [[string, number, number, number]]; + +export interface IInsertItemResult { + +} + +export interface IInsertItemQuery { + params: InsertItemParams; + result: IInsertItemResult; +} + +export type UpdateItemParams = [string, number]; + +export interface IUpdateItemResult { + +} + +export interface IUpdateItemQuery { + params: UpdateItemParams; + result: IUpdateItemResult; +} + +export type DeleteItemParams = [string]; + +export interface IDeleteItemResult { + +} + +export interface IDeleteItemQuery { + params: DeleteItemParams; + result: IDeleteItemResult; +} + +export type GetItemsWithTableParams = []; + +export interface IGetItemsWithTableResult { + items_food_type: string; + items_id: number; + table_number: number; +} + +export interface IGetItemsWithTableQuery { + params: GetItemsWithTableParams; + result: IGetItemsWithTableResult; +} + +export type GetEventsWithScoreParams = [number | null]; + +export interface IGetEventsWithScoreResult { + description: string | null; + id: number; + is_active: boolean; + metadata: object | null; + name: string; + score: number | null; + start_date: Date | null; +} + +export interface IGetEventsWithScoreQuery { + params: GetEventsWithScoreParams; + result: IGetEventsWithScoreResult; +} diff --git a/demo/sqlite/index.ts b/demo/sqlite/index.ts new file mode 100644 index 00000000..0dd168a7 --- /dev/null +++ b/demo/sqlite/index.ts @@ -0,0 +1,62 @@ +import { sql } from 'sqlx-ts' +import { + IGetAllItemsResult, + IGetItemsByFoodTypeResult, GetItemsByFoodTypeParams, + InsertItemParams, IInsertItemResult, + UpdateItemParams, IUpdateItemResult, + DeleteItemParams, IDeleteItemResult, + IGetEventsWithScoreResult, GetEventsWithScoreParams, + IGetItemsWithTableResult, +} from './index.queries' + +// SELECT all items +const getAllItems = sql` + -- @name: getAllItems + -- @db: db_sqlite + SELECT * FROM items; +` + +// SELECT with WHERE clause using ? placeholder +const getItemsByFoodType = sql` + -- @name: getItemsByFoodType + -- @db: db_sqlite + SELECT * FROM items WHERE food_type = ?; +` + +// INSERT with parameters +const insertItem = sql` + -- @name: insertItem + -- @db: db_sqlite + INSERT INTO items (food_type, time_takes_to_cook, table_id, points) + VALUES (?, ?, ?, ?); +` + +// UPDATE with parameters +const updateItem = sql` + -- @name: updateItem + -- @db: db_sqlite + UPDATE items SET food_type = ? WHERE id = ?; +` + +// DELETE with parameter +const deleteItem = sql` + -- @name: deleteItem + -- @db: db_sqlite + DELETE FROM items WHERE food_type = ?; +` + +// SELECT with JOIN +const getItemsWithTable = sql` + -- @name: getItemsWithTable + -- @db: db_sqlite + SELECT items.id, items.food_type, tables.number as table_number + FROM items + JOIN tables ON items.table_id = tables.id; +` + +// Query with multiple types (nullable, boolean, real, datetime, json) +const getEventsWithScore = sql` + -- @name: getEventsWithScore + -- @db: db_sqlite + SELECT * FROM events WHERE score > ?; +` diff --git a/demo/tsconfig.json b/demo/tsconfig.json new file mode 100644 index 00000000..8764e095 --- /dev/null +++ b/demo/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "outDir": "dist", + "module": "CommonJS", + "strict": true + }, + "exclude": [ + "node_modules" + ] +} From c6a9a92967a2afce5c7cc409642b96c1f1234df7 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:14:36 +1000 Subject: [PATCH 07/15] fix demo test --- .github/workflows/demo.yaml | 46 ++++++------------------------------- demo/package.json | 10 ++++---- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml index b37a67b5..cd01c30d 100644 --- a/.github/workflows/demo.yaml +++ b/.github/workflows/demo.yaml @@ -28,10 +28,9 @@ jobs: - name: mysql-8 postgres: "" mysql: "8" -# uncomment when we add SQLite support -# - name: sqlite -# postgres: "" -# mysql: "" + - name: sqlite + postgres: "" + mysql: "" steps: - uses: actions/checkout@v4 @@ -90,60 +89,29 @@ jobs: working-directory: ./demo run: npm install - - name: Install sqlx-ts globally for demo - run: npm install -g sqlx-ts - - name: Compile demo TypeScript code (to verify it compiles before generating types) working-directory: ./demo run: npm run compile:all - - name: Copy sqlx-ts binary into node package - run: cp target/release/sqlx-ts node/sqlx-ts - - name: Generate types (PostgreSQL demos) if: matrix.db.postgres != '' working-directory: ./demo run: | - npx sqlx-ts ./pg --config ./.sqlxrc.json -g - npx sqlx-ts ./sequelize --config ./.sqlxrc.json -g + ../target/release/sqlx-ts ./pg --config ./.sqlxrc.json -g + ../target/release/sqlx-ts ./sequelize --config ./.sqlxrc.json -g - name: Generate types (MySQL demo) if: matrix.db.mysql != '' working-directory: ./demo run: | - npx sqlx-ts ./mysql2 --config ./.sqlxrc.json -g + ../target/release/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g - name: Generate types (SQLite demo) if: matrix.db.name == 'sqlite' working-directory: ./demo run: | - npx sqlx-ts ./sqlite --config ./.sqlxrc.json -g + ../target/release/sqlx-ts ./sqlite --config ./.sqlxrc.json -g - name: Type-check generated code working-directory: ./demo run: npm run build - - - name: Verify generated types are not empty - working-directory: ./demo - run: | - check_file() { - if [ ! -f "$1" ]; then - echo "FAIL: $1 not found" - exit 1 - fi - if [ ! -s "$1" ]; then - echo "FAIL: $1 is empty" - exit 1 - fi - echo "OK: $1" - } - if [ -n "${{ matrix.db.postgres }}" ]; then - check_file "pg/index.queries.ts" - check_file "sequelize/index.queries.ts" - fi - if [ -n "${{ matrix.db.mysql }}" ]; then - check_file "mysql2/index.queries.ts" - fi - if [ "${{ matrix.db.name }}" = "sqlite" ]; then - check_file "sqlite/index.queries.ts" - fi diff --git a/demo/package.json b/demo/package.json index 94435da1..0fc3e1ef 100644 --- a/demo/package.json +++ b/demo/package.json @@ -4,11 +4,11 @@ "private": true, "description": "Demo project showing sqlx-ts usage with pg, mysql2, and sequelize", "scripts": { - "compile:pg": "../target/debug/sqlx-ts ./pg --config ./.sqlxrc.json -g", - "compile:mysql": "../target/debug/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g", - "compile:sequelize": "../target/debug/sqlx-ts ./sequelize --config ./.sqlxrc.json -g", - "compile:sqlite": "../target/debug/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", - "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize", + "compile:pg": "../target/release/sqlx-ts ./pg --config ./.sqlxrc.json -g", + "compile:mysql": "../target/release/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g", + "compile:sequelize": "../target/release/sqlx-ts ./sequelize --config ./.sqlxrc.json -g", + "compile:sqlite": "../target/release/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", + "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize && npm run compile:sqlite", "build": "npm run compile:all && npm run typecheck", "typecheck": "npx tsc -p ./tsconfig.json --noEmit" }, From 4e5bc768821f9af2fea580740b937e5c5607807f Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:16:20 +1000 Subject: [PATCH 08/15] fix --- .github/workflows/demo.yaml | 6 ++++-- demo/package.json | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml index cd01c30d..c054f222 100644 --- a/.github/workflows/demo.yaml +++ b/.github/workflows/demo.yaml @@ -1,4 +1,4 @@ -name: demo +name: demo projects concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -12,9 +12,11 @@ on: jobs: demo-test: - name: Demo type-generation (${{ matrix.db.name }}) + name: Demo project sqlx-ts (${{ matrix.db.name }}) if: "!contains(github.event.head_commit.message, 'Release')" runs-on: ubuntu-latest + env: + BUILD_MODE: release strategy: fail-fast: false matrix: diff --git a/demo/package.json b/demo/package.json index 0fc3e1ef..a8ecda6f 100644 --- a/demo/package.json +++ b/demo/package.json @@ -4,10 +4,10 @@ "private": true, "description": "Demo project showing sqlx-ts usage with pg, mysql2, and sequelize", "scripts": { - "compile:pg": "../target/release/sqlx-ts ./pg --config ./.sqlxrc.json -g", - "compile:mysql": "../target/release/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g", - "compile:sequelize": "../target/release/sqlx-ts ./sequelize --config ./.sqlxrc.json -g", - "compile:sqlite": "../target/release/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", + "compile:pg": "../target/${BUILD_MODE:-debug}/sqlx-ts ./pg --config ./.sqlxrc.json -g", + "compile:mysql": "../target/${BUILD_MODE:-debug}/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g", + "compile:sequelize": "../target/${BUILD_MODE:-debug}/sqlx-ts ./sequelize --config ./.sqlxrc.json -g", + "compile:sqlite": "../target/${BUILD_MODE:-debug}/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize && npm run compile:sqlite", "build": "npm run compile:all && npm run typecheck", "typecheck": "npx tsc -p ./tsconfig.json --noEmit" From d2d84662d59828977a4a196a82ba78c12c64129e Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:22:51 +1000 Subject: [PATCH 09/15] update --- node/package.json | 31 +++++++++++++++++++++++++++++-- src/parser/sql_parser.rs | 29 +++++++++++++---------------- 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/node/package.json b/node/package.json index 0f0904a0..18d38754 100644 --- a/node/package.json +++ b/node/package.json @@ -1,8 +1,35 @@ { "name": "sqlx-ts", "version": "0.44.0", - "description": "sqlx-ts ensures your raw SQLs are compile-time checked", + "description": "Compile-time checked raw SQL queries with TypeScript type generation for PostgreSQL, MySQL, and SQLite", "main": "dist/index.js", + "types": "dist/index.d.ts", + "keywords": [ + "sql", + "typescript", + "type-safety", + "codegen", + "postgres", + "postgresql", + "mysql", + "sqlite", + "database", + "query", + "raw-sql", + "compile-time", + "type-generation", + "sql-validation", + "tagged-template", + "orm-alternative" + ], + "homepage": "https://jasonshin.github.io/sqlx-ts", + "repository": { + "type": "git", + "url": "https://github.com/JasonShin/sqlx-ts" + }, + "bugs": { + "url": "https://github.com/JasonShin/sqlx-ts/issues" + }, "maintainers": [ "visualbbasic@gmail.com" ], @@ -31,6 +58,6 @@ "typescript": "^4.6.3" }, "engines": { - "node": ">=12" + "node": ">=16" } } diff --git a/src/parser/sql_parser.rs b/src/parser/sql_parser.rs index f5689693..feb750ab 100644 --- a/src/parser/sql_parser.rs +++ b/src/parser/sql_parser.rs @@ -95,25 +95,22 @@ fn split_sql_queries(content: &str) -> Vec { } // Handle single-line comments - '-' if !in_string && !in_comment => { - if chars.peek() == Some(&'-') { - in_comment = true; - current_query.push(ch); - current_query.push(chars.next().unwrap()); // consume second dash - } else { - current_query.push(ch); - } + '-' if !in_string && !in_comment && chars.peek() == Some(&'-') => { + in_comment = true; + current_query.push(ch); + current_query.push(chars.next().unwrap()); // consume second dash } // Handle multi-line comments - '/' if !in_string && !in_comment => { - if chars.peek() == Some(&'*') { - in_comment = true; - current_query.push(ch); - current_query.push(chars.next().unwrap()); // consume asterisk - } else { - current_query.push(ch); - } + '/' if !in_string && !in_comment && chars.peek() == Some(&'*') => { + in_comment = true; + current_query.push(ch); + current_query.push(chars.next().unwrap()); // consume asterisk + } + + // Non-comment dash or slash + '-' | '/' if !in_string && !in_comment => { + current_query.push(ch); } '*' if in_comment && !in_string => { From 488a80191ecb29074df9a8baf8b9f27e7ae3142e Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:26:01 +1000 Subject: [PATCH 10/15] update --- demo/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demo/package.json b/demo/package.json index a8ecda6f..ed17fffc 100644 --- a/demo/package.json +++ b/demo/package.json @@ -7,8 +7,9 @@ "compile:pg": "../target/${BUILD_MODE:-debug}/sqlx-ts ./pg --config ./.sqlxrc.json -g", "compile:mysql": "../target/${BUILD_MODE:-debug}/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g", "compile:sequelize": "../target/${BUILD_MODE:-debug}/sqlx-ts ./sequelize --config ./.sqlxrc.json -g", - "compile:sqlite": "../target/${BUILD_MODE:-debug}/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", + "compile:sqlite": "npm run sqlite && ../target/${BUILD_MODE:-debug}/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize && npm run compile:sqlite", + "sqlite": "rm -f sqlite/demo.db && ./create-sqlite.sh", "build": "npm run compile:all && npm run typecheck", "typecheck": "npx tsc -p ./tsconfig.json --noEmit" }, From 727d0732d8754b99f44fe93454c45afbae4594ea Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:27:29 +1000 Subject: [PATCH 11/15] npm i --- node/package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/package-lock.json b/node/package-lock.json index f0a6ffcf..1be31f36 100644 --- a/node/package-lock.json +++ b/node/package-lock.json @@ -26,7 +26,7 @@ "typescript": "^4.6.3" }, "engines": { - "node": ">=12" + "node": ">=16" } }, "node_modules/@ampproject/remapping": { From acbc9a26370318e2f594393fc09f6b148ed7bf18 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:34:40 +1000 Subject: [PATCH 12/15] add sqlx-ts --- demo/package-lock.json | 52 +++++++++++++++++++----------------------- demo/package.json | 3 ++- 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/demo/package-lock.json b/demo/package-lock.json index 037fa4c6..2c09b83b 100644 --- a/demo/package-lock.json +++ b/demo/package-lock.json @@ -11,7 +11,7 @@ "mysql2": "^3.5.2", "pg": "^8.11.1", "sequelize": "^6.32.1", - "sqlx-ts": "file:../node" + "sqlx-ts": "^0.45.0" }, "devDependencies": { "@types/pg": "^8.10.2", @@ -19,31 +19,6 @@ "typescript": "^5.1.6" } }, - "../node": { - "name": "sqlx-ts", - "version": "0.44.0", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "adm-zip": "^0.5.16" - }, - "bin": { - "sqlx-ts": "sqlx-ts" - }, - "devDependencies": { - "@types/jest": "^27.4.1", - "@types/node": "^20.14.8", - "@typescript-eslint/eslint-plugin": "^5.19.0", - "@typescript-eslint/parser": "^5.19.0", - "eslint": "^8.13.0", - "jest": "^27.5.1", - "ts-jest": "^27.1.4", - "typescript": "^4.6.3" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@types/bluebird": { "version": "3.5.42", "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz", @@ -123,6 +98,15 @@ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==", "license": "MIT" }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/aws-ssl-profiles": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", @@ -541,8 +525,20 @@ } }, "node_modules/sqlx-ts": { - "resolved": "../node", - "link": true + "version": "0.45.0", + "resolved": "https://registry.npmjs.org/sqlx-ts/-/sqlx-ts-0.45.0.tgz", + "integrity": "sha512-ArdK6hc2O1KVIqux3mA3KCsXRQXLeVSGMYQgpB8yuVaYVQnol09lRwMTTBWnG61HPo2YePRXHV5APPsrXMTzug==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "adm-zip": "^0.5.16" + }, + "bin": { + "sqlx-ts": "sqlx-ts" + }, + "engines": { + "node": ">=12" + } }, "node_modules/toposort-class": { "version": "1.0.1", diff --git a/demo/package.json b/demo/package.json index ed17fffc..3f45c2e3 100644 --- a/demo/package.json +++ b/demo/package.json @@ -16,7 +16,8 @@ "dependencies": { "mysql2": "^3.5.2", "pg": "^8.11.1", - "sequelize": "^6.32.1" + "sequelize": "^6.32.1", + "sqlx-ts": "^0.45.0" }, "devDependencies": { "@types/pg": "^8.10.2", From b9903008d501772466c29bca2d3a20b6091e41fd Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 12:39:50 +1000 Subject: [PATCH 13/15] fix --- .github/workflows/demo.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml index c054f222..7ee60023 100644 --- a/.github/workflows/demo.yaml +++ b/.github/workflows/demo.yaml @@ -85,16 +85,12 @@ jobs: working-directory: ./demo run: | sudo apt-get update && sudo apt-get install -y sqlite3 - bash sqlite/create-db.sh + bash ./create-db.sh - name: Install demo dependencies working-directory: ./demo run: npm install - - name: Compile demo TypeScript code (to verify it compiles before generating types) - working-directory: ./demo - run: npm run compile:all - - name: Generate types (PostgreSQL demos) if: matrix.db.postgres != '' working-directory: ./demo From 1d5d64e84d8227f7322e72dafea17a85312b1b25 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 17:31:49 +1000 Subject: [PATCH 14/15] fix ci --- .github/workflows/demo.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml index 7ee60023..89bc3dc2 100644 --- a/.github/workflows/demo.yaml +++ b/.github/workflows/demo.yaml @@ -85,7 +85,7 @@ jobs: working-directory: ./demo run: | sudo apt-get update && sudo apt-get install -y sqlite3 - bash ./create-db.sh + bash ./create-sqlite.sh - name: Install demo dependencies working-directory: ./demo From 3b6aca3828acf46a45c9b7dd2e81633f3e5dbb84 Mon Sep 17 00:00:00 2001 From: JasonShin Date: Sat, 25 Apr 2026 18:14:12 +1000 Subject: [PATCH 15/15] fix github action --- demo/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/demo/package.json b/demo/package.json index 3f45c2e3..a3b2b909 100644 --- a/demo/package.json +++ b/demo/package.json @@ -10,8 +10,7 @@ "compile:sqlite": "npm run sqlite && ../target/${BUILD_MODE:-debug}/sqlx-ts ./sqlite --config ./.sqlxrc.json -g", "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize && npm run compile:sqlite", "sqlite": "rm -f sqlite/demo.db && ./create-sqlite.sh", - "build": "npm run compile:all && npm run typecheck", - "typecheck": "npx tsc -p ./tsconfig.json --noEmit" + "build": "npx tsc -p ./tsconfig.json --noEmit" }, "dependencies": { "mysql2": "^3.5.2",