Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions crates/litewire-mysql/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ fn ok_response(affected_rows: u64, last_insert_id: u64, in_transaction: bool) ->
}
}

use crate::types::sqlite_to_mysql_column_type;
use crate::types::{mysql_type_for_value, sqlite_to_mysql_column_type};

/// A cached prepared statement.
struct PreparedStmt {
Expand Down Expand Up @@ -103,11 +103,29 @@ impl LiteWireHandler {
let columns: Vec<Column> = rs
.columns
.iter()
.map(|c| Column {
table: String::new(),
column: c.name.clone(),
coltype: sqlite_to_mysql_column_type(c.decltype.as_deref()),
colflags: ColumnFlags::empty(),
.enumerate()
.map(|(i, c)| {
// Declared type wins; for untyped expression columns
// (`SELECT 1`, decltype None) infer the wire type from
// the first non-NULL value so the declared column type
// matches how the row writer encodes it. Scan past
// leading NULLs; empty/all-NULL columns stay VAR_STRING
// (NULL is valid against any column type).
let coltype = if c.decltype.is_some() {
sqlite_to_mysql_column_type(c.decltype.as_deref())
} else {
rs.rows
.iter()
.filter_map(|r| r.get(i))
.find(|v| !matches!(v, Value::Null))
.map_or(ColumnType::MYSQL_TYPE_VAR_STRING, mysql_type_for_value)
};
Column {
table: String::new(),
column: c.name.clone(),
coltype,
colflags: ColumnFlags::empty(),
}
})
.collect();

Expand Down
23 changes: 23 additions & 0 deletions crates/litewire-mysql/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
//! MySQL type mapping from SQLite column declarations.

use litewire_backend::Value;
use opensrv_mysql::ColumnType;

/// Map a runtime [`Value`] to the MySQL column type its binary encoding
/// requires.
///
/// SQLite is dynamically typed: expression columns (`SELECT 1`, `SELECT
/// a + b`) have no declared type, so `decltype` is `None` and the
/// declaration-based mapping would fall back to `VAR_STRING`. But the row
/// writer emits each value in its native form (an integer as `LONGLONG`),
/// and opensrv's binary protocol rejects a declared/actual type mismatch
/// ("tried to use 1 as MYSQL_TYPE_VAR_STRING") and drops the connection —
/// surfacing to the client as `2006 server has gone away`. For untyped
/// columns we therefore derive the wire type from the actual data.
#[must_use]
pub fn mysql_type_for_value(value: &Value) -> ColumnType {
match value {
Value::Null => ColumnType::MYSQL_TYPE_VAR_STRING,
Value::Integer(_) => ColumnType::MYSQL_TYPE_LONGLONG,
Value::Float(_) => ColumnType::MYSQL_TYPE_DOUBLE,
Value::Text(_) => ColumnType::MYSQL_TYPE_VAR_STRING,
Value::Blob(_) => ColumnType::MYSQL_TYPE_BLOB,
}
}

/// Map a SQLite column declared type to a MySQL column type.
///
/// Uses the declared type string from `PRAGMA table_info` or the column
Expand Down
25 changes: 25 additions & 0 deletions crates/litewire/tests/mysql_e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,31 @@ async fn prepared_select_with_param() {
drop(conn);
}

#[tokio::test]
async fn prepared_select_expression_no_table() {
// A prepared SELECT of a bare expression (no FROM clause) — e.g. the
// `SELECT 1` liveness check some ORMs issue with prepare-emulation off.
// Regressed with "2006 server has gone away": the frontend dropped the
// connection because on_prepare and on_execute disagreed on column count
// for a table-less expression column.
init_tracing();
let port = free_port().await;
let _server = start_litewire(port).await;
let mut conn = connect(port).await;

let rows: Vec<i64> = conn.exec("SELECT 1", ()).await.unwrap();
assert_eq!(rows, vec![1]);

// Also exercise a multi-column expression select and an aliased one.
let rows: Vec<(i64, i64)> = conn.exec("SELECT 1 + 1, 40 + 2", ()).await.unwrap();
assert_eq!(rows, vec![(2, 42)]);

let rows: Vec<i64> = conn.exec("SELECT 7 AS answer", ()).await.unwrap();
assert_eq!(rows, vec![7]);

drop(conn);
}

#[tokio::test]
async fn prepared_insert() {
init_tracing();
Expand Down
Loading