Skip to content

sqlite support#270

Open
JasonShin wants to merge 3 commits intomainfrom
sqlite
Open

sqlite support#270
JasonShin wants to merge 3 commits intomainfrom
sqlite

Conversation

@JasonShin
Copy link
Copy Markdown
Owner

No description provided.

Copilot AI review requested due to automatic review settings March 29, 2026 10:48
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds first-class SQLite support to sqlx-ts, enabling validation and TS type generation against SQLite databases alongside existing Postgres/MySQL support.

Changes:

  • Introduces SQLite connection pooling + query validation path in core and wires it through shared connection/config code.
  • Adds SQLite schema introspection and SQLite-specific SQL parsing/type-affinity mapping for TS generation.
  • Adds SQLite-focused integration tests for ? parameters and type output.

Reviewed changes

Copilot reviewed 15 out of 16 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
tests/sqlite_query_parameters.rs New integration tests covering SQLite ? parameters, result typing, and invalid SQL handling.
test-utils/src/sandbox.rs Extends test harness to allow running the CLI without host/user/pass when db_type=sqlite.
src/ts_generator/types/ts_query.rs Adds SQLite column-type-affinity → TS type mapping.
src/ts_generator/information_schema.rs Adds SQLite table introspection via PRAGMA table_info and integrates into DBSchema::fetch_table.
src/ts_generator/generator.rs Uses sqlparser’s SQLiteDialect when generating types for SQLite connections.
src/core/sqlite/prepare.rs Adds SQLite query validation implementation (currently via EXPLAIN).
src/core/sqlite/pool.rs Adds bb8 manager for SQLite using rusqlite connections.
src/core/sqlite/mod.rs Exposes SQLite core modules.
src/core/mod.rs Registers core::sqlite module.
src/core/connection.rs Adds DBConn::SqliteConn and routes prepare()/get_db_type() accordingly.
src/common/types.rs Adds DatabaseType::Sqlite.
src/common/lazy.rs Creates and caches SQLite pools from config.
src/common/dotenv.rs Parses DB_TYPE=sqlite from .env.
src/common/config.rs Adjusts default-connection config rules for SQLite and adds get_sqlite_path().
Cargo.toml Adds rusqlite dependency (and duplicates it in dev-deps).
Cargo.lock Locks new rusqlite/sqlite transitive dependencies.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

let mut all_fields: HashMap<String, Field> = HashMap::new();

for table_name in &table_names_owned {
let query = format!("PRAGMA table_info('{}')", table_name);
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRAGMA table_info is built by interpolating table_name into a quoted string. This will break for table names containing ' and can be abused by crafting a query with a malicious table identifier. At minimum, escape single-quotes; ideally use the pragma_table_info(?) table-valued function (bind parameter) or proper identifier quoting.

Suggested change
let query = format!("PRAGMA table_info('{}')", table_name);
let safe_table_name = table_name.replace('\'', "''");
let query = format!("PRAGMA table_info('{}')", safe_table_name);

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +257
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,
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQLite PRIMARY KEY columns are implicitly NOT NULL even if notnull is 0 in PRAGMA table_info. This code only uses the notnull flag, so it can incorrectly mark INTEGER PRIMARY KEY columns as nullable. Consider reading the pk column from PRAGMA table_info and treating pk > 0 as non-nullable.

Suggested change
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,
let pk: i64 = row.get(5)?;
Ok((name, type_name, notnull, pk, table_name.clone()))
}) {
Ok(rows) => rows,
Err(_) => continue,
};
for row in rows {
if let Ok((field_name, field_type, notnull, pk, tbl_name)) = row {
let field = Field {
field_type: TsFieldType::get_ts_field_type_from_sqlite_field_type(
field_type,
tbl_name,
field_name.clone(),
),
// SQLite PRIMARY KEY columns are implicitly NOT NULL, even if `notnull` is 0.
is_nullable: !(notnull || pk > 0),

Copilot uses AI. Check for mistakes.
Comment on lines +248 to +249
// The command should report SQL errors
assert!(
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "invalid SQL" test only checks for absence of the success message in stdout; it does not assert a non-zero exit status. Other integration tests (e.g. tests/exit_code_on_error.rs) assert .failure(). Consider asserting !output.status.success() (and/or checking stderr) so the test catches regressions where sqlite errors don't fail the process.

Suggested change
// The command should report SQL errors
assert!(
// The process should fail and report SQL errors
assert!(
!output.status.success(),
"Expected command to fail, but it exited successfully.\nstdout: {stdout}\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert!(

Copilot uses AI. Check for mistakes.
tempfile = "3.27.0"
test_utils = { path="test-utils" }
pretty_assertions = "1.4.1"
rusqlite = { version = "0.31", features = ["bundled"] }
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rusqlite is added both under [dependencies] and [dev-dependencies] with the same version/features. Since the crate code now depends on rusqlite, the dev-dependency entry is redundant and can be removed to avoid duplication.

Suggested change
rusqlite = { version = "0.31", features = ["bundled"] }

Copilot uses AI. Check for mistakes.
Comment on lines +386 to +397
/// 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"))
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_sqlite_path treats DB_URL as a filesystem path and passes it to rusqlite::Connection::open. This differs from the existing meaning of DB_URL as a full connection URL (postgres/mysql) and may surprise users who provide a sqlite://... style URL. Consider either documenting that SQLite expects a file path here, or parsing/handling common SQLite URL/URI formats explicitly.

Copilot uses AI. Check for mistakes.
Comment on lines +34 to +36
// Use EXPLAIN to validate the SQL without executing it
let explain_query = format!("EXPLAIN {}", query);
conn.execute_batch(&explain_query)
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SQLite validation currently runs EXPLAIN ... via execute_batch. In rusqlite this executes the statement and will fail for queries containing ? placeholders ("incorrect number of bindings") even though the SQL is valid. Use prepare (optionally on EXPLAIN ...) to validate parse/semantic correctness without executing or requiring bound params.

Suggested change
// Use EXPLAIN to validate the SQL without executing it
let explain_query = format!("EXPLAIN {}", query);
conn.execute_batch(&explain_query)
// Use EXPLAIN with `prepare` to validate the SQL without executing it
let explain_query = format!("EXPLAIN {}", query);
conn.prepare(&explain_query).map(|_| ())

Copilot uses AI. Check for mistakes.
Comment on lines +13 to +22
/// Wrapper around rusqlite::Connection to make it Send + Sync for bb8
pub struct SqliteConnection {
pub conn: Arc<Mutex<Connection>>,
}

// 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 {}

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SqliteConnection declares unsafe impl Send/Sync, and the wrapped rusqlite::Connection is then moved into spawn_blocking closures across threads. This bypasses rusqlite's thread-safety guarantees and can be unsound unless the connection is opened/configured specifically for cross-thread use. Prefer a safe async wrapper (e.g. tokio-rusqlite/sqlx) or avoid unsafe by using a connection type that is already Send/Sync (and documenting the invariants).

Copilot uses AI. Check for mistakes.
Comment on lines +51 to +61
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}")))?;

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

connect() panics on Connection::open failure. Since this is inside the pool manager, a bad path/permission issue will crash the whole process instead of returning a SqlitePoolError. Map the open error into Self::Error and let callers handle/report it gracefully.

Suggested change
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}")))?;
let conn_res = task::spawn_blocking(move || {
Connection::open(&db_path).map_err(|err| {
SqlitePoolError(format!(
"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}")))?;
let conn = conn_res?;

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +62
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,
}
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SQLite default here sets db_name to :memory: and mentions it will be overridden per-test, but the run_test! macro always passes --db-name={db_name} without overriding. This can lead to tests accidentally pointing at an empty in-memory DB. Consider requiring callers to set an explicit temp file path (or generating one here) instead of defaulting to :memory:.

Copilot uses AI. Check for mistakes.
@@ -0,0 +1,255 @@
#[cfg(test)]
mod sqlite_query_parameters_tests {
use std::env;
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

std::env is imported but never used in this test module. Removing the unused import will keep the test build warning-free.

Suggested change
use std::env;

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants