Skip to content

Commit acd2284

Browse files
authored
fix(mysql): Work around for Issue #2206 (ColumnNotFound error when querying) (#4086)
* Re-read mysql column info during execute MariaDB may change the column info between PREPARE and EXECUTE if the schema changes (https://jira.mariadb.org/browse/MDEV-27013). Therefore, always read column info from the execute metadata and use it for the row column_names field. Fixes: #2206, #1530 * doc: caution that ColumnIndex may differ between Statement and Row
1 parent 18ffed2 commit acd2284

File tree

3 files changed

+42
-32
lines changed

3 files changed

+42
-32
lines changed

sqlx-core/src/column.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ impl ColumnOrigin {
8484
/// This trait is implemented for strings which are used to look up a column by name, and for
8585
/// `usize` which is used as a positional index into the row.
8686
///
87+
/// *Caution*: The column index may differ between a [`Statement`] and a [`Row`] returned by the
88+
/// statement. This can happen with some databases if, for example, the schema changes between
89+
/// prepare and execute or if the database does not provide column information when the statement
90+
/// is prepared.
91+
///
8792
/// [`Row`]: crate::row::Row
8893
/// [`Statement`]: crate::statement::Statement
8994
/// [`get`]: crate::row::Row::get

sqlx-mysql/src/connection/executor.rs

Lines changed: 7 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ impl MySqlConnection {
120120
// to re-use this memory freely between result sets
121121
let mut columns = Arc::new(Vec::new());
122122

123-
let (mut column_names, format, mut needs_metadata) = if let Some(arguments) = arguments {
123+
let format = if let Some(arguments) = arguments {
124124
if persistent && self.inner.cache_statement.is_enabled() {
125125
let (id, metadata) = self
126126
.get_or_prepare_statement(sql)
@@ -144,7 +144,7 @@ impl MySqlConnection {
144144
})
145145
.await?;
146146

147-
(metadata.column_names, MySqlValueFormat::Binary, false)
147+
MySqlValueFormat::Binary
148148
} else {
149149
let (id, metadata) = self
150150
.prepare_statement(sql)
@@ -170,13 +170,13 @@ impl MySqlConnection {
170170

171171
self.inner.stream.send_packet(StmtClose { statement: id }).await?;
172172

173-
(metadata.column_names, MySqlValueFormat::Binary, false)
173+
MySqlValueFormat::Binary
174174
}
175175
} else {
176176
// https://dev.mysql.com/doc/internals/en/com-query.html
177177
self.inner.stream.send_packet(Query(sql)).await?;
178178

179-
(Arc::default(), MySqlValueFormat::Text, true)
179+
MySqlValueFormat::Text
180180
};
181181

182182
loop {
@@ -216,15 +216,9 @@ impl MySqlConnection {
216216
let num_columns = usize::try_from(num_columns)
217217
.map_err(|_| err_protocol!("column count overflows usize: {num_columns}"))?;
218218

219-
if needs_metadata {
220-
column_names = Arc::new(recv_result_metadata(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?);
221-
} else {
222-
// next time we hit here, it'll be a new result set and we'll need the
223-
// full metadata
224-
needs_metadata = true;
225-
226-
recv_result_columns(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?;
227-
}
219+
// Always reload column names, even for prepared statements (the schema
220+
// may change between PREPARE and EXECUTE).
221+
let column_names = Arc::new(recv_result_metadata(&mut self.inner.stream, num_columns, Arc::make_mut(&mut columns)).await?);
228222

229223
// finally, there will be none or many result-rows
230224
loop {
@@ -395,25 +389,6 @@ impl<'c> Executor<'c> for &'c mut MySqlConnection {
395389
}
396390
}
397391

398-
async fn recv_result_columns(
399-
stream: &mut MySqlStream,
400-
num_columns: usize,
401-
columns: &mut Vec<MySqlColumn>,
402-
) -> Result<(), Error> {
403-
columns.clear();
404-
columns.reserve(num_columns);
405-
406-
for ordinal in 0..num_columns {
407-
columns.push(recv_next_result_column(&stream.recv().await?, ordinal)?);
408-
}
409-
410-
if num_columns > 0 {
411-
stream.maybe_recv_eof().await?;
412-
}
413-
414-
Ok(())
415-
}
416-
417392
fn recv_next_result_column(def: &ColumnDefinition, ordinal: usize) -> Result<MySqlColumn, Error> {
418393
// if the alias is empty, use the alias
419394
// only then use the name

tests/mysql/mysql.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,3 +636,33 @@ async fn issue_3200() -> anyhow::Result<()> {
636636

637637
Ok(())
638638
}
639+
640+
#[cfg(mariadb)]
641+
#[sqlx_macros::test]
642+
async fn it_can_name_columns_issue_2206() -> anyhow::Result<()> {
643+
let mut conn = new::<MySql>().await?;
644+
645+
sqlx::raw_sql(
646+
"\
647+
CREATE TABLE IF NOT EXISTS issue_2206
648+
(
649+
`id` BIGINT AUTO_INCREMENT,
650+
`name` VARCHAR(128) NOT NULL,
651+
PRIMARY KEY (id)
652+
);
653+
",
654+
)
655+
.execute(&mut conn)
656+
.await?;
657+
658+
let row = sqlx::query("INSERT INTO issue_2206 (name) VALUES (?) RETURNING *")
659+
.bind("Alice")
660+
.fetch_one(&mut conn)
661+
.await?;
662+
let _id: i64 = row.get("id");
663+
let name: String = row.get("name");
664+
665+
assert_eq!(&name, "Alice");
666+
667+
Ok(())
668+
}

0 commit comments

Comments
 (0)