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
433 changes: 417 additions & 16 deletions crates/db2-client/src/connection.rs

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions crates/db2-client/src/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,11 @@ impl PreparedStatement {
ddm.add_code_point(0x215D, &[0x01]); // QRYCLSIMP = 1
ddm.build()
};
let sqldta_data = build_sqldta(params, &self.param_descriptors)?;
let sqldta = build_sqldta(params, &self.param_descriptors)?;

let mut writer = DssWriter::new(corr_id);
writer.write_request_next_same_corr(&opnqry_data, true);
writer.write_object(&sqldta_data, false);
sqldta.write(&mut writer, false);
let send_buf = writer.finish();
if env::var_os("DB2_WIRE_DEBUG_HEX").is_some() {
eprintln!(
Expand Down Expand Up @@ -171,12 +171,12 @@ impl PreparedStatement {
} else {
db2_proto::commands::excsqlstt::build_excsqlstt_default(&pkgnamcsn)
};
let sqldta_data = build_sqldta(params, &self.param_descriptors)?;
let sqldta = build_sqldta(params, &self.param_descriptors)?;
let rdbcmm_data = db2_proto::commands::rdbcmm::build_rdbcmm();

let mut writer = DssWriter::new(corr_id);
writer.write_request_next_same_corr(&excsqlstt_data, true);
writer.write_object(&sqldta_data, guard.auto_commit);
sqldta.write(&mut writer, guard.auto_commit);
if guard.auto_commit {
writer.write_request(&rdbcmm_data, false);
}
Expand Down
11 changes: 6 additions & 5 deletions crates/db2-napi/index.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,15 @@ function normalizeParam(value) {
}
return value.toISOString().replace('T', ' ').replace(/Z$/, '')
}
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) {
return Array.from(value)
}
// Byte views reach the addon as-is; copying them into a JS array would cost
// one property access per byte for large BLOB parameters.
if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView(value)) {
return Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength))
return value instanceof Uint8Array
? value
: new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
}
if (typeof ArrayBuffer !== 'undefined' && value instanceof ArrayBuffer) {
return Array.from(new Uint8Array(value))
return new Uint8Array(value)
}
return value
}
Expand Down
5 changes: 1 addition & 4 deletions crates/db2-napi/src/js_connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,7 @@ impl JsClient {
.as_mut()
.ok_or_else(|| napi::Error::from_reason("Client is not connected"))?;

let db2_params = match &params {
Some(p) => js_params_to_db2(p),
None => Vec::new(),
};
let db2_params = params.map(js_params_to_db2).unwrap_or_default();

let param_refs: Vec<&dyn db2_client::ToSql> = db2_params
.iter()
Expand Down
5 changes: 1 addition & 4 deletions crates/db2-napi/src/js_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,7 @@ impl JsPool {
let mut napi_diagnostics = Vec::new();

let params_started = collect_diagnostics.then(Instant::now);
let db2_params = match &params {
Some(p) => js_params_to_db2(p),
None => Vec::new(),
};
let db2_params = params.map(js_params_to_db2).unwrap_or_default();
push_elapsed_diagnostic(&mut napi_diagnostics, "napi_pool_params_ms", params_started);

let refs_started = collect_diagnostics.then(Instant::now);
Expand Down
7 changes: 2 additions & 5 deletions crates/db2-napi/src/js_statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,7 @@ impl JsPreparedStatement {
.as_mut()
.ok_or_else(|| napi::Error::from_reason("PreparedStatement is closed"))?;

let db2_params = match &params {
Some(p) => js_params_to_db2(p),
None => Vec::new(),
};
let db2_params = params.map(js_params_to_db2).unwrap_or_default();

let param_refs: Vec<&dyn db2_client::ToSql> = db2_params
.iter()
Expand Down Expand Up @@ -86,7 +83,7 @@ impl JsPreparedStatement {

// Convert all rows from JSON to Db2Value
let db2_rows: Vec<Vec<db2_proto::types::Db2Value>> =
param_rows.iter().map(|row| js_params_to_db2(row)).collect();
param_rows.into_iter().map(js_params_to_db2).collect();

// Build references for each row
let param_ref_rows: Vec<Vec<&dyn db2_client::ToSql>> = db2_rows
Expand Down
5 changes: 1 addition & 4 deletions crates/db2-napi/src/js_transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,7 @@ impl JsTransaction {
napi::Error::from_reason("Transaction is already committed or rolled back")
})?;

let db2_params = match &params {
Some(p) => js_params_to_db2(p),
None => Vec::new(),
};
let db2_params = params.map(js_params_to_db2).unwrap_or_default();

let param_refs: Vec<&dyn db2_client::ToSql> = db2_params
.iter()
Expand Down
56 changes: 24 additions & 32 deletions crates/db2-napi/src/js_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,11 @@ fn env_truthy(name: &str) -> bool {
}

/// Convert parameters before napi's JSON decoder can panic on JavaScript BigInt.
pub struct JsParameter(serde_json::Value);
/// Binary values bypass JSON so a large Buffer is copied once, not per byte.
pub enum JsParameter {
Json(serde_json::Value),
Bytes(Vec<u8>),
}

impl TypeName for JsParameter {
fn type_name() -> &'static str {
Expand Down Expand Up @@ -661,21 +665,11 @@ impl FromNapiValue for JsParameter {
}
ValueType::Object if value.is_buffer()? => {
let buffer = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(env, raw)? };
serde_json::Value::Array(
buffer
.iter()
.map(|byte| serde_json::Value::from(*byte))
.collect(),
)
return Ok(Self::Bytes(buffer.to_vec()));
}
ValueType::Object if value.is_typedarray()? => {
let bytes = napi::bindgen_prelude::Uint8Array::from_napi_value(env, raw)?;
serde_json::Value::Array(
bytes
.iter()
.map(|byte| serde_json::Value::from(*byte))
.collect(),
)
return Ok(Self::Bytes(bytes.to_vec()));
}
ValueType::Object
if {
Expand All @@ -697,12 +691,7 @@ impl FromNapiValue for JsParameter {
} else {
std::slice::from_raw_parts(data.cast::<u8>(), length)
};
serde_json::Value::Array(
bytes
.iter()
.map(|byte| serde_json::Value::from(*byte))
.collect(),
)
return Ok(Self::Bytes(bytes.to_vec()));
}
ValueType::Object if value.is_array()? => {
let object = value.coerce_to_object()?;
Expand All @@ -722,9 +711,9 @@ impl FromNapiValue for JsParameter {
"Array parameters must contain only integer bytes (0..255)",
));
}
bytes.push(serde_json::Value::from(byte as u8));
bytes.push(byte as u8);
}
serde_json::Value::Array(bytes)
return Ok(Self::Bytes(bytes));
}
ValueType::Object => {
return Err(parameter_error(
Expand All @@ -743,23 +732,26 @@ impl FromNapiValue for JsParameter {
ValueType::String | ValueType::Boolean => serde_json::Value::from_napi_value(env, raw)?,
_ => return Err(parameter_error(env, "Unsupported parameter type")),
};
Ok(Self(json))
Ok(Self::Json(json))
}
}

pub fn js_params_to_db2(params: &[JsParameter]) -> Vec<db2_proto::types::Db2Value> {
pub fn js_params_to_db2(params: Vec<JsParameter>) -> Vec<db2_proto::types::Db2Value> {
params
.iter()
.map(|param| json_to_db2_value(&param.0))
.into_iter()
.map(|param| match param {
JsParameter::Json(value) => json_to_db2_value(value),
JsParameter::Bytes(bytes) => db2_proto::types::Db2Value::Binary(bytes),
})
.collect()
}

/// Convert a single JSON value to a Db2Value.
fn json_to_db2_value(val: &serde_json::Value) -> db2_proto::types::Db2Value {
fn json_to_db2_value(val: serde_json::Value) -> db2_proto::types::Db2Value {
use db2_proto::types::Db2Value;
match val {
serde_json::Value::Null => Db2Value::Null,
serde_json::Value::Bool(b) => Db2Value::Boolean(*b),
serde_json::Value::Bool(b) => Db2Value::Boolean(b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
Expand All @@ -773,7 +765,7 @@ fn json_to_db2_value(val: &serde_json::Value) -> db2_proto::types::Db2Value {
Db2Value::Null
}
}
serde_json::Value::String(s) => Db2Value::VarChar(s.clone()),
serde_json::Value::String(s) => Db2Value::VarChar(s),
serde_json::Value::Array(arr) => {
// Treat arrays of numbers as binary data
let bytes: Vec<u8> = arr
Expand All @@ -782,9 +774,9 @@ fn json_to_db2_value(val: &serde_json::Value) -> db2_proto::types::Db2Value {
.collect();
Db2Value::Binary(bytes)
}
serde_json::Value::Object(obj) => buffer_like_object_to_bytes(obj)
serde_json::Value::Object(obj) => buffer_like_object_to_bytes(&obj)
.map(Db2Value::Binary)
.unwrap_or_else(|| Db2Value::VarChar(val.to_string())),
.unwrap_or_else(|| Db2Value::VarChar(serde_json::Value::Object(obj).to_string())),
}
}

Expand Down Expand Up @@ -1052,7 +1044,7 @@ mod tests {
"data": [0, 1, 127, 255]
});
assert_eq!(
json_to_db2_value(&node_buffer_json),
json_to_db2_value(node_buffer_json),
db2_proto::types::Db2Value::Binary(vec![0, 1, 127, 255])
);

Expand All @@ -1063,7 +1055,7 @@ mod tests {
"3": 239
});
assert_eq!(
json_to_db2_value(&indexed_object),
json_to_db2_value(indexed_object),
db2_proto::types::Db2Value::Binary(vec![222, 173, 190, 239])
);
}
Expand Down
1 change: 1 addition & 0 deletions crates/db2-proto/src/codepoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ pub const ENDQRYRM: u16 = 0x220B; // End of Query Reply Message
pub const QRYNOPRM: u16 = 0x2202; // Query Not Open Reply Message
pub const OPNQRYRM: u16 = 0x2205; // Open Query Complete Reply Message
pub const DTAMCHRM: u16 = 0x220E; // Data Descriptor Mismatch Reply Message
pub const DSCINVRM: u16 = 0x220A; // Invalid Description Reply Message
pub const RDBUPDRM: u16 = 0x2218; // RDB Update Reply Message
pub const SYNTAXRM: u16 = 0x124C; // Data Stream Syntax Error Reply Message
pub const PRCCNVRM: u16 = 0x1245; // Conversational Protocol Error Reply Message
Expand Down
99 changes: 98 additions & 1 deletion tests/integration/lob_regressions_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! Live regressions for GitHub issues #19 and #20 (Db2 LUW 11.5 and 12.1).
//! Live regressions for GitHub issues #19, #20 and #31 (Db2 LUW 11.5 and 12.1).
#[path = "../common/mod.rs"]
mod common;
use common::*;
Expand Down Expand Up @@ -196,3 +196,100 @@ async fn unicode_lobs_and_graphic_values_survive_single_row_direct_and_prepared_
assert_eq!(followup.rows[0].get_by_index::<i32>(0), Some(42));
client.close().await.unwrap();
}

#[tokio::test]
async fn lob_parameters_beyond_varying_descriptor_limit_are_written() {
use db2_proto::types::Db2Value::*;
let mut client = db2_client::Client::new(test_config());
client.connect().await.unwrap();
let table = unique_table(31);
client
.query(
&format!(
"CREATE TABLE {table} (ID INTEGER NOT NULL PRIMARY KEY, SMALL CLOB(32767), \
BIG CLOB(32768), BB BLOB(1M), DB DBCLOB(32768))"
),
&[],
)
.await
.unwrap();
let check = async {
client
.query(
&format!("INSERT INTO {table} (ID, SMALL, BIG) VALUES (1, 'old', 'old')"),
&[],
)
.await?;
// Exactly the declared 32768 bytes.
let big = "β".repeat(16_384);
let blob = (0..70_000).map(|index| index as u8).collect::<Vec<_>>();
let graphic = "漢\u{1D11E}".repeat(5_000);
let id = 1i32;
let update = format!("UPDATE {table} SET SMALL = ?, BIG = ?, BB = ?, DB = ? WHERE ID = ?");
let direct = client
.query(&update, &[&"small", &big, &blob, &graphic, &id])
.await?;
assert_eq!(direct.row_count, 1);
let select = format!("SELECT SMALL, BIG, BB, DB FROM {table} WHERE ID = 1");
let row = client.query(&select, &[]).await?.rows[0].values().to_vec();
assert_eq!(
row,
[
Clob("small".into()),
Clob(big),
Blob(blob),
Clob(graphic.clone())
]
);

let overflow = "β".repeat(16_385);
let err = client
.query(
&format!("UPDATE {table} SET BIG = ? WHERE ID = ?"),
&[&overflow, &id],
)
.await
.unwrap_err();
assert!(
matches!(&err, db2_client::Error::Sql { sqlstate, .. } if sqlstate == "22001"),
"{err:?}"
);

let statement = client.prepare(&update).await?;
let empty: Vec<u8> = Vec::new();
let null_text: Option<&str> = None;
let prepared = statement
.execute(&[&"", &"", &empty, &null_text, &id])
.await?;
statement.close().await?;
assert_eq!(prepared.row_count, 1);
let row = client.query(&select, &[]).await?.rows[0].values().to_vec();
assert_eq!(row, [Clob("".into()), Clob("".into()), Blob(vec![]), Null]);

// Empty LOBs before a nonempty LOB send no EXTDTA of their own.
let statement = client.prepare(&update).await?;
let prepared = statement
.execute(&[&"s", &"", &empty, &graphic, &id])
.await?;
statement.close().await?;
assert_eq!(prepared.row_count, 1);
let row = client.query(&select, &[]).await?.rows[0].values().to_vec();
assert_eq!(
row,
[
Clob("s".into()),
Clob("".into()),
Blob(vec![]),
Clob(graphic)
]
);
Ok::<_, db2_client::Error>(())
}
.await;
client
.query(&format!("DROP TABLE {table}"), &[])
.await
.unwrap();
check.unwrap();
client.close().await.unwrap();
}
Loading
Loading