From 2923172cfb8c0a649d764bd47b708ec9f110ada1 Mon Sep 17 00:00:00 2001 From: celsowm <369336+celsowm@users.noreply.github.com> Date: Tue, 21 Apr 2026 01:29:32 +0000 Subject: [PATCH] Refactor largest .rs files to adhere to SRP - Identified `coercion.rs` and `system_procedures.rs` as the largest `iridium_core` files violating SRP. - Split `coercion.rs` into `numeric.rs`, `string.rs`, `datetime.rs`, and `binary.rs` submodules. - Split `system_procedures.rs` into `metadata.rs`, `session.rs`, and `security.rs` submodules. - Maintained all existing functionality and addressed local visibility/compilation bounds. --- .../iridium_core/src/executor/predicates.rs | 2 +- .../metadata.rs} | 523 ++-------- .../procedural/system_procedures/mod.rs | 144 +++ .../procedural/system_procedures/security.rs | 64 ++ .../procedural/system_procedures/session.rs | 231 +++++ .../src/executor/script/procedural/throw.rs | 2 +- .../src/executor/value_ops/coercion.rs | 905 ------------------ .../src/executor/value_ops/coercion/binary.rs | 98 ++ .../executor/value_ops/coercion/datetime.rs | 118 +++ .../src/executor/value_ops/coercion/mod.rs | 156 +++ .../executor/value_ops/coercion/numeric.rs | 409 ++++++++ .../src/executor/value_ops/coercion/string.rs | 153 +++ .../src/executor/value_ops/mod.rs | 2 +- .../iridium_core/src/storage/btree_index.rs | 5 +- .../iridium_core/src/storage/redb_storage.rs | 2 +- crates/iridium_server/bin/compat-query.rs | 2 +- crates/iridium_server/src/session/mod.rs | 16 +- crates/iridium_server/src/tds/bulk.rs | 2 +- crates/iridium_server/src/tds/type_mapping.rs | 2 +- 19 files changed, 1442 insertions(+), 1394 deletions(-) rename crates/iridium_core/src/executor/script/procedural/{system_procedures.rs => system_procedures/metadata.rs} (51%) create mode 100644 crates/iridium_core/src/executor/script/procedural/system_procedures/mod.rs create mode 100644 crates/iridium_core/src/executor/script/procedural/system_procedures/security.rs create mode 100644 crates/iridium_core/src/executor/script/procedural/system_procedures/session.rs delete mode 100644 crates/iridium_core/src/executor/value_ops/coercion.rs create mode 100644 crates/iridium_core/src/executor/value_ops/coercion/binary.rs create mode 100644 crates/iridium_core/src/executor/value_ops/coercion/datetime.rs create mode 100644 crates/iridium_core/src/executor/value_ops/coercion/mod.rs create mode 100644 crates/iridium_core/src/executor/value_ops/coercion/numeric.rs create mode 100644 crates/iridium_core/src/executor/value_ops/coercion/string.rs diff --git a/crates/iridium_core/src/executor/predicates.rs b/crates/iridium_core/src/executor/predicates.rs index ce2c1c3..e1e0d51 100644 --- a/crates/iridium_core/src/executor/predicates.rs +++ b/crates/iridium_core/src/executor/predicates.rs @@ -360,7 +360,7 @@ fn is_uncorrelated(stmt: &crate::ast::SelectStmt) -> bool { .projection .iter() .all(|i| is_expr_uncorrelated(&i.expr)) - && stmt.selection.as_ref().map_or(true, is_expr_uncorrelated); + && stmt.selection.as_ref().is_none_or(is_expr_uncorrelated); } // For now, only literals are considered uncorrelated. false diff --git a/crates/iridium_core/src/executor/script/procedural/system_procedures.rs b/crates/iridium_core/src/executor/script/procedural/system_procedures/metadata.rs similarity index 51% rename from crates/iridium_core/src/executor/script/procedural/system_procedures.rs rename to crates/iridium_core/src/executor/script/procedural/system_procedures/metadata.rs index bfdc2f6..7c702cd 100644 --- a/crates/iridium_core/src/executor/script/procedural/system_procedures.rs +++ b/crates/iridium_core/src/executor/script/procedural/system_procedures/metadata.rs @@ -1,189 +1,11 @@ -use super::super::ScriptExecutor; -use crate::ast::ExecProcedureStmt; use crate::error::DbError; use crate::executor::context::ExecutionContext; -use crate::executor::evaluator::eval_expr; -use crate::executor::metadata::{type_max_length, type_name}; use crate::executor::result::QueryResult; +use crate::executor::script::ScriptExecutor; use crate::types::{DataType, Value}; +use crate::executor::metadata::{type_name, type_max_length}; -const SYSTEM_PROCEDURES: &[&str] = &[ - "sp_rename", - "sp_help", - "sp_helptext", - "sp_columns", - "sp_tables", - "sp_helpindex", - "sp_helpconstraint", - "sp_set_session_context", - "xp_instance_regread", - "sp_msgetversion", - "sp_who", - "sp_databases", - "sp_helpdb", - "sp_server_info", - "sp_monitor", - "sp_helpuser", - "sp_helprole", - "sp_helprolemember", - "sp_helpsrvrole", - "sp_helpsrvrolemember", - "sp_helpfile", - "sp_helpfilegroup", -]; - -pub(crate) fn is_system_procedure(name: &str) -> bool { - SYSTEM_PROCEDURES - .iter() - .any(|sp| name.eq_ignore_ascii_case(sp)) -} - -pub(crate) fn execute_system_procedure( - exec: &mut ScriptExecutor<'_>, - stmt: &ExecProcedureStmt, - ctx: &mut ExecutionContext<'_>, -) -> Result, DbError> { - let name = &stmt.name.name; - let args = eval_args(exec, &stmt.args, ctx)?; - - let result = if name.eq_ignore_ascii_case("sp_rename") { - execute_sp_rename(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_help") { - execute_sp_help(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_helptext") { - execute_sp_helptext(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_columns") { - execute_sp_columns(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_tables") { - execute_sp_tables(exec)? - } else if name.eq_ignore_ascii_case("sp_helpindex") { - execute_sp_helpindex(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_helpconstraint") { - execute_sp_helpconstraint(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_set_session_context") { - execute_sp_set_session_context(stmt, ctx, exec)? - } else if name.eq_ignore_ascii_case("sp_who") { - execute_sp_who(ctx)? - } else if name.eq_ignore_ascii_case("sp_databases") { - execute_sp_databases()? - } else if name.eq_ignore_ascii_case("sp_helpdb") { - execute_sp_helpdb(exec, &args)? - } else if name.eq_ignore_ascii_case("sp_server_info") { - execute_sp_server_info()? - } else if name.eq_ignore_ascii_case("sp_monitor") { - execute_sp_monitor(exec)? - } else if name.eq_ignore_ascii_case("sp_helpuser") { - execute_sp_helpuser(exec, ctx)? - } else if name.eq_ignore_ascii_case("sp_helprole") { - execute_sp_helprole(exec, ctx)? - } else if name.eq_ignore_ascii_case("sp_helprolemember") { - execute_sp_helprolemember(exec, ctx)? - } else if name.eq_ignore_ascii_case("sp_helpsrvrole") { - execute_sp_helpsrvrole(exec, ctx)? - } else if name.eq_ignore_ascii_case("sp_helpsrvrolemember") { - execute_sp_helpsrvrolemember(exec, ctx)? - } else if name.eq_ignore_ascii_case("sp_helpfile") { - execute_sp_helpfile(exec, ctx)? - } else if name.eq_ignore_ascii_case("sp_helpfilegroup") { - execute_sp_helpfilegroup(exec, ctx)? - } else if name.eq_ignore_ascii_case("xp_instance_regread") { - // Stub for registry reads. If it has an output parameter, set it to a default. - for arg in &stmt.args { - if arg.is_output { - if let crate::ast::Expr::Identifier(ref var_name) = arg.expr { - if let Some((ty, val)) = ctx.session.variables.get_mut(var_name) { - *val = crate::executor::value_ops::coerce_value_to_type_with_dateformat( - Value::Int(0), - ty, - &ctx.options.dateformat, - )?; - } - } - } - } - QueryResult::default() - } else if name.eq_ignore_ascii_case("sp_msgetversion") { - // Stub for version check - QueryResult { - columns: vec!["Character_Value".into()], - column_types: vec![DataType::NVarChar { max_len: 128 }], - column_nullabilities: vec![false], - rows: vec![vec![Value::NVarChar("16.0.1000.0".into())]], - ..Default::default() - } - } else { - return Err(DbError::Execution(format!( - "unknown system procedure '{}'", - name - ))); - }; - - let mut res = result; - res.return_status = Some(0); - res.is_procedure = true; - Ok(Some(res)) -} - -fn eval_args( - exec: &mut ScriptExecutor<'_>, - args: &[crate::ast::ExecArgument], - ctx: &mut ExecutionContext<'_>, -) -> Result, DbError> { - let mut result = Vec::new(); - for arg in args { - let val = eval_expr(&arg.expr, &[], ctx, exec.catalog, exec.storage, exec.clock)?; - result.push(val.to_string_value()); - } - Ok(result) -} - -fn execute_sp_rename( - exec: &mut ScriptExecutor<'_>, - args: &[String], -) -> Result { - if args.len() < 2 { - return Err(DbError::Execution( - "sp_rename requires at least 2 arguments: @objname, @newname".into(), - )); - } - let objname = &args[0]; - let newname = &args[1]; - let objtype = args.get(2).map(|s| s.as_str()).unwrap_or("OBJECT"); - - if objtype.eq_ignore_ascii_case("COLUMN") { - let parts: Vec<&str> = objname.splitn(2, '.').collect(); - if parts.len() != 2 { - return Err(DbError::Execution( - "sp_rename with @objtype='COLUMN' expects @objname as 'table.column'".into(), - )); - } - let table_name = parts[0]; - let old_col = parts[1]; - let table = exec - .catalog - .find_table_mut("dbo", table_name) - .ok_or_else(|| DbError::object_not_found(format!("table '{}'", table_name)))?; - let col = table - .columns - .iter_mut() - .find(|c| c.name.eq_ignore_ascii_case(old_col)) - .ok_or_else(|| { - DbError::object_not_found(format!("column '{}.{}'", table_name, old_col)) - })?; - col.name = newname.clone(); - } else { - let table = exec - .catalog - .find_table_mut("dbo", objname) - .ok_or_else(|| DbError::object_not_found(format!("object '{}'", objname)))?; - table.name = newname.clone(); - exec.catalog.rebuild_maps(); - } - - Ok(QueryResult::default()) -} - -fn execute_sp_help(exec: &mut ScriptExecutor<'_>, args: &[String]) -> Result { +pub(crate) fn execute_sp_help(exec: &mut ScriptExecutor<'_>, args: &[String]) -> Result { if args.is_empty() { let mut rows = Vec::new(); for t in exec.catalog.get_tables() { @@ -279,7 +101,7 @@ fn execute_sp_help(exec: &mut ScriptExecutor<'_>, args: &[String]) -> Result, args: &[String], ) -> Result { @@ -318,7 +140,7 @@ fn execute_sp_helptext( ))) } -fn execute_sp_columns( +pub(crate) fn execute_sp_columns( exec: &mut ScriptExecutor<'_>, args: &[String], ) -> Result { @@ -380,12 +202,12 @@ fn execute_sp_columns( }) } -fn execute_sp_helpdb( +pub(crate) fn execute_sp_helpdb( _exec: &mut ScriptExecutor<'_>, args: &[String], ) -> Result { let mut rows = Vec::new(); - let filter_name = args.get(0); + let filter_name = args.first(); for db in crate::executor::database_catalog::builtin_databases() { if let Some(name) = filter_name { @@ -428,190 +250,7 @@ fn execute_sp_helpdb( }) } -fn execute_sp_who(ctx: &ExecutionContext<'_>) -> Result { - let rows = vec![vec![ - Value::Int(ctx.metadata.id as i32), - Value::Int(0), // ecid - Value::NVarChar("running".to_string()), - Value::NVarChar( - ctx.metadata - .user - .clone() - .unwrap_or_else(|| "sa".to_string()), - ), - Value::NVarChar( - ctx.metadata - .host_name - .clone() - .unwrap_or_else(|| "localhost".to_string()), - ), - Value::Char("0".to_string()), // blk - Value::NVarChar(ctx.metadata.database.clone().unwrap_or_default()), - Value::NVarChar("SELECT".to_string()), // cmd - Value::Int(0), // request_id - ]]; - - Ok(QueryResult { - columns: vec![ - "spid".into(), - "ecid".into(), - "status".into(), - "loginame".into(), - "hostname".into(), - "blk".into(), - "dbname".into(), - "cmd".into(), - "request_id".into(), - ], - column_types: vec![ - DataType::Int, - DataType::Int, - DataType::NVarChar { max_len: 30 }, - DataType::NVarChar { max_len: 128 }, - DataType::NVarChar { max_len: 128 }, - DataType::Char { len: 5 }, - DataType::NVarChar { max_len: 128 }, - DataType::NVarChar { max_len: 16 }, - DataType::Int, - ], - column_nullabilities: vec![ - false, false, false, false, false, false, false, false, false, - ], - rows, - ..Default::default() - }) -} - -fn execute_sp_databases() -> Result { - let mut rows = Vec::new(); - for db in crate::executor::database_catalog::builtin_databases() { - rows.push(vec![ - Value::VarChar(db.name.to_string()), - Value::Int(0), // DATABASE_SIZE - Value::Null, // REMARKS - ]); - } - Ok(QueryResult { - columns: vec!["DATABASE_NAME".into(), "DATABASE_SIZE".into(), "REMARKS".into()], - column_types: vec![ - DataType::VarChar { max_len: 128 }, - DataType::Int, - DataType::VarChar { max_len: 254 }, - ], - column_nullabilities: vec![false, false, true], - rows, - ..Default::default() - }) -} - -fn execute_sp_server_info() -> Result { - let rows = vec![ - vec![ - Value::Int(1), - Value::VarChar("DBMS_NAME".into()), - Value::VarChar("SQL Server".into()), - ], - vec![ - Value::Int(2), - Value::VarChar("DBMS_VER".into()), - Value::VarChar("Microsoft SQL Server 2025 - 17.0.1000.0".into()), - ], - vec![ - Value::Int(10), - Value::VarChar("OWNER_TERM".into()), - Value::VarChar("owner".into()), - ], - vec![ - Value::Int(11), - Value::VarChar("TABLE_TERM".into()), - Value::VarChar("table".into()), - ], - vec![ - Value::Int(12), - Value::VarChar("MAX_OWNER_NAME_LENGTH".into()), - Value::VarChar("128".into()), - ], - vec![ - Value::Int(13), - Value::VarChar("TABLE_LENGTH".into()), - Value::VarChar("128".into()), - ], - ]; - Ok(QueryResult { - columns: vec![ - "ATTRIBUTE_ID".into(), - "ATTRIBUTE_NAME".into(), - "ATTRIBUTE_VALUE".into(), - ], - column_types: vec![ - DataType::Int, - DataType::VarChar { max_len: 60 }, - DataType::VarChar { max_len: 255 }, - ], - column_nullabilities: vec![false, false, false], - rows, - ..Default::default() - }) -} - -fn execute_sp_monitor(exec: &ScriptExecutor<'_>) -> Result { - let now = Value::DateTime(exec.clock.now_datetime_literal()); - let rows = vec![vec![ - now.clone(), // last_run - now.clone(), // current_run - Value::Int(0), // seconds - Value::Int(0), // cpu_busy - Value::Int(0), // io_busy - Value::Int(0), // idle - Value::Int(0), // packets_received - Value::Int(0), // packets_sent - Value::Int(0), // packet_errors - Value::Int(0), // total_read - Value::Int(0), // total_write - Value::Int(0), // total_errors - Value::Int(0), // connections - ]]; - Ok(QueryResult { - columns: vec![ - "last_run".into(), - "current_run".into(), - "seconds".into(), - "cpu_busy".into(), - "io_busy".into(), - "idle".into(), - "packets_received".into(), - "packets_sent".into(), - "packet_errors".into(), - "total_read".into(), - "total_write".into(), - "total_errors".into(), - "connections".into(), - ], - column_types: vec![ - DataType::DateTime, - DataType::DateTime, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - DataType::Int, - ], - column_nullabilities: vec![ - false, false, false, false, false, false, false, false, false, false, false, false, - false, - ], - rows, - ..Default::default() - }) -} - -fn execute_sp_helpindex( +pub(crate) fn execute_sp_helpindex( exec: &mut ScriptExecutor<'_>, args: &[String], ) -> Result { @@ -689,48 +328,7 @@ fn execute_sp_helpindex( }) } -fn execute_sp_set_session_context( - stmt: &ExecProcedureStmt, - ctx: &mut ExecutionContext<'_>, - exec: &mut ScriptExecutor<'_>, -) -> Result { - let mut key = String::new(); - let mut value = Value::Null; - let mut read_only = false; - - for arg in &stmt.args { - let val = eval_expr(&arg.expr, &[], ctx, exec.catalog, exec.storage, exec.clock)?; - match arg.name.as_ref().map(|s| s.to_ascii_lowercase()) { - Some(ref n) if n == "@key" => key = val.to_string_value(), - Some(ref n) if n == "@value" => value = val, - Some(ref n) if n == "@read_only" => read_only = val.to_bool().unwrap_or(false), - _ => { - // Positional arguments fallback if needed, but MSSQL usually uses named for this - } - } - } - - if key.is_empty() { - return Err(DbError::Execution( - "sp_set_session_context: @key is required".into(), - )); - } - - if let Some((_, is_ro)) = ctx.session.session_context.get(&key) { - if *is_ro { - return Err(DbError::Execution(format!( - "Cannot set value for read-only session context key '{}'", - key - ))); - } - } - - ctx.session.session_context.insert(key, (value, read_only)); - - Ok(QueryResult::default()) -} - -fn execute_sp_helpconstraint( +pub(crate) fn execute_sp_helpconstraint( exec: &mut ScriptExecutor<'_>, args: &[String], ) -> Result { @@ -794,7 +392,7 @@ fn execute_sp_helpconstraint( }) } -fn execute_sp_tables(exec: &mut ScriptExecutor<'_>) -> Result { +pub(crate) fn execute_sp_tables(exec: &mut ScriptExecutor<'_>) -> Result { let rows: Vec> = exec .catalog .get_tables() @@ -828,86 +426,73 @@ fn execute_sp_tables(exec: &mut ScriptExecutor<'_>) -> Result, - ctx: &mut ExecutionContext<'_>, -) -> Result { - let sql = "SELECT p.name AS UserName, p.type_desc AS RoleName, '' AS LoginName, '' AS DefDBName, '' AS DefSchemaName, p.principal_id AS UserId, p.principal_id AS SID FROM sys.database_principals p"; - let batch = crate::parser::parse_batch(sql)?; - match exec.execute_batch(&batch, ctx)? { - crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helpuser query".into())), - } -} - -fn execute_sp_helprole( +pub(crate) fn execute_sp_helpfile( exec: &mut ScriptExecutor<'_>, ctx: &mut ExecutionContext<'_>, ) -> Result { - let sql = "SELECT name AS RoleName, principal_id AS RoleId, 0 AS IsAppRole FROM sys.database_principals WHERE type = 'R'"; + let sql = "SELECT name, file_id, physical_name, type_desc AS usage, size FROM sys.database_files"; let batch = crate::parser::parse_batch(sql)?; match exec.execute_batch(&batch, ctx)? { crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helprole query".into())), + _ => Err(DbError::Execution("Failed to execute sp_helpfile query".into())), } } -fn execute_sp_helprolemember( +pub(crate) fn execute_sp_helpfilegroup( exec: &mut ScriptExecutor<'_>, ctx: &mut ExecutionContext<'_>, ) -> Result { - let sql = "SELECT r.name AS DbRole, m.name AS MemberName, m.principal_id AS MemberSID FROM sys.database_role_members rm JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id"; + let sql = "SELECT name, data_space_id AS groupid, type_desc AS groupname FROM sys.filegroups"; let batch = crate::parser::parse_batch(sql)?; match exec.execute_batch(&batch, ctx)? { crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helprolemember query".into())), + _ => Err(DbError::Execution("Failed to execute sp_helpfilegroup query".into())), } } -fn execute_sp_helpsrvrole( +pub(crate) fn execute_sp_rename( exec: &mut ScriptExecutor<'_>, - ctx: &mut ExecutionContext<'_>, + args: &[String], ) -> Result { - let sql = "SELECT name AS ServerRole, principal_id AS RoleId FROM sys.server_principals WHERE type = 'R'"; - let batch = crate::parser::parse_batch(sql)?; - match exec.execute_batch(&batch, ctx)? { - crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helpsrvrole query".into())), + if args.len() < 2 { + return Err(DbError::Execution( + "sp_rename requires at least 2 arguments: @objname, @newname".into(), + )); } -} + let objname = &args[0]; + let newname = &args[1]; + let objtype = args.get(2).map(|s| s.as_str()).unwrap_or("OBJECT"); -fn execute_sp_helpsrvrolemember( - exec: &mut ScriptExecutor<'_>, - ctx: &mut ExecutionContext<'_>, -) -> Result { - let sql = "SELECT r.name AS ServerRole, m.name AS MemberName, m.principal_id AS MemberSID FROM sys.server_role_members srm JOIN sys.server_principals r ON srm.role_principal_id = r.principal_id JOIN sys.server_principals m ON srm.member_principal_id = m.principal_id"; - let batch = crate::parser::parse_batch(sql)?; - match exec.execute_batch(&batch, ctx)? { - crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helpsrvrolemember query".into())), + if objtype.eq_ignore_ascii_case("COLUMN") { + let parts: Vec<&str> = objname.splitn(2, '.').collect(); + if parts.len() != 2 { + return Err(DbError::Execution( + "sp_rename with @objtype='COLUMN' expects @objname as 'table.column'".into(), + )); + } + let table_name = parts[0]; + let old_col = parts[1]; + let table = exec + .catalog + .find_table_mut("dbo", table_name) + .ok_or_else(|| DbError::object_not_found(format!("table '{}'", table_name)))?; + let col = table + .columns + .iter_mut() + .find(|c| c.name.eq_ignore_ascii_case(old_col)) + .ok_or_else(|| { + DbError::object_not_found(format!("column '{}.{}'", table_name, old_col)) + })?; + col.name = newname.clone(); + } else { + let table = exec + .catalog + .find_table_mut("dbo", objname) + .ok_or_else(|| DbError::object_not_found(format!("object '{}'", objname)))?; + table.name = newname.clone(); + exec.catalog.rebuild_maps(); } -} -fn execute_sp_helpfile( - exec: &mut ScriptExecutor<'_>, - ctx: &mut ExecutionContext<'_>, -) -> Result { - let sql = "SELECT name, file_id, physical_name, type_desc AS usage, size FROM sys.database_files"; - let batch = crate::parser::parse_batch(sql)?; - match exec.execute_batch(&batch, ctx)? { - crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helpfile query".into())), - } + Ok(QueryResult::default()) } -fn execute_sp_helpfilegroup( - exec: &mut ScriptExecutor<'_>, - ctx: &mut ExecutionContext<'_>, -) -> Result { - let sql = "SELECT name, data_space_id AS groupid, type_desc AS groupname FROM sys.filegroups"; - let batch = crate::parser::parse_batch(sql)?; - match exec.execute_batch(&batch, ctx)? { - crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), - _ => Err(DbError::Execution("Failed to execute sp_helpfilegroup query".into())), - } -} diff --git a/crates/iridium_core/src/executor/script/procedural/system_procedures/mod.rs b/crates/iridium_core/src/executor/script/procedural/system_procedures/mod.rs new file mode 100644 index 0000000..0e7f1ea --- /dev/null +++ b/crates/iridium_core/src/executor/script/procedural/system_procedures/mod.rs @@ -0,0 +1,144 @@ +use super::super::ScriptExecutor; +use crate::ast::ExecProcedureStmt; +use crate::error::DbError; +use crate::executor::context::ExecutionContext; +use crate::executor::evaluator::eval_expr; +use crate::executor::result::QueryResult; +use crate::types::{DataType, Value}; +pub mod metadata; +pub mod session; +pub mod security; + +use metadata::*; +use session::*; +use security::*; + +const SYSTEM_PROCEDURES: &[&str] = &[ + "sp_rename", + "sp_help", + "sp_helptext", + "sp_columns", + "sp_tables", + "sp_helpindex", + "sp_helpconstraint", + "sp_set_session_context", + "xp_instance_regread", + "sp_msgetversion", + "sp_who", + "sp_databases", + "sp_helpdb", + "sp_server_info", + "sp_monitor", + "sp_helpuser", + "sp_helprole", + "sp_helprolemember", + "sp_helpsrvrole", + "sp_helpsrvrolemember", + "sp_helpfile", + "sp_helpfilegroup", +]; + +pub(crate) fn is_system_procedure(name: &str) -> bool { + SYSTEM_PROCEDURES + .iter() + .any(|sp| name.eq_ignore_ascii_case(sp)) +} + +pub(crate) fn execute_system_procedure( + exec: &mut ScriptExecutor<'_>, + stmt: &ExecProcedureStmt, + ctx: &mut ExecutionContext<'_>, +) -> Result, DbError> { + let name = &stmt.name.name; + let args = eval_args(exec, &stmt.args, ctx)?; + + let result = if name.eq_ignore_ascii_case("sp_rename") { + execute_sp_rename(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_help") { + execute_sp_help(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_helptext") { + execute_sp_helptext(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_columns") { + execute_sp_columns(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_tables") { + execute_sp_tables(exec)? + } else if name.eq_ignore_ascii_case("sp_helpindex") { + execute_sp_helpindex(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_helpconstraint") { + execute_sp_helpconstraint(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_set_session_context") { + execute_sp_set_session_context(stmt, ctx, exec)? + } else if name.eq_ignore_ascii_case("sp_who") { + execute_sp_who(ctx)? + } else if name.eq_ignore_ascii_case("sp_databases") { + execute_sp_databases()? + } else if name.eq_ignore_ascii_case("sp_helpdb") { + execute_sp_helpdb(exec, &args)? + } else if name.eq_ignore_ascii_case("sp_server_info") { + execute_sp_server_info()? + } else if name.eq_ignore_ascii_case("sp_monitor") { + execute_sp_monitor(exec)? + } else if name.eq_ignore_ascii_case("sp_helpuser") { + execute_sp_helpuser(exec, ctx)? + } else if name.eq_ignore_ascii_case("sp_helprole") { + execute_sp_helprole(exec, ctx)? + } else if name.eq_ignore_ascii_case("sp_helprolemember") { + execute_sp_helprolemember(exec, ctx)? + } else if name.eq_ignore_ascii_case("sp_helpsrvrole") { + execute_sp_helpsrvrole(exec, ctx)? + } else if name.eq_ignore_ascii_case("sp_helpsrvrolemember") { + execute_sp_helpsrvrolemember(exec, ctx)? + } else if name.eq_ignore_ascii_case("sp_helpfile") { + execute_sp_helpfile(exec, ctx)? + } else if name.eq_ignore_ascii_case("sp_helpfilegroup") { + execute_sp_helpfilegroup(exec, ctx)? + } else if name.eq_ignore_ascii_case("xp_instance_regread") { + // Stub for registry reads. If it has an output parameter, set it to a default. + for arg in &stmt.args { + if arg.is_output { + if let crate::ast::Expr::Identifier(ref var_name) = arg.expr { + if let Some((ty, val)) = ctx.session.variables.get_mut(var_name) { + *val = crate::executor::value_ops::coerce_value_to_type_with_dateformat( + Value::Int(0), + ty, + &ctx.options.dateformat, + )?; + } + } + } + } + QueryResult::default() + } else if name.eq_ignore_ascii_case("sp_msgetversion") { + // Stub for version check + QueryResult { + columns: vec!["Character_Value".into()], + column_types: vec![DataType::NVarChar { max_len: 128 }], + column_nullabilities: vec![false], + rows: vec![vec![Value::NVarChar("16.0.1000.0".into())]], + ..Default::default() + } + } else { + return Err(DbError::Execution(format!( + "unknown system procedure '{}'", + name + ))); + }; + + let mut res = result; + res.return_status = Some(0); + res.is_procedure = true; + Ok(Some(res)) +} + +fn eval_args( + exec: &mut ScriptExecutor<'_>, + args: &[crate::ast::ExecArgument], + ctx: &mut ExecutionContext<'_>, +) -> Result, DbError> { + let mut result = Vec::new(); + for arg in args { + let val = eval_expr(&arg.expr, &[], ctx, exec.catalog, exec.storage, exec.clock)?; + result.push(val.to_string_value()); + } + Ok(result) +} diff --git a/crates/iridium_core/src/executor/script/procedural/system_procedures/security.rs b/crates/iridium_core/src/executor/script/procedural/system_procedures/security.rs new file mode 100644 index 0000000..ba8805c --- /dev/null +++ b/crates/iridium_core/src/executor/script/procedural/system_procedures/security.rs @@ -0,0 +1,64 @@ +use crate::error::DbError; +use crate::executor::context::ExecutionContext; +use crate::executor::result::QueryResult; +use crate::executor::script::ScriptExecutor; + +pub(crate) fn execute_sp_helpuser( + exec: &mut ScriptExecutor<'_>, + ctx: &mut ExecutionContext<'_>, +) -> Result { + let sql = "SELECT p.name AS UserName, p.type_desc AS RoleName, '' AS LoginName, '' AS DefDBName, '' AS DefSchemaName, p.principal_id AS UserId, p.principal_id AS SID FROM sys.database_principals p"; + let batch = crate::parser::parse_batch(sql)?; + match exec.execute_batch(&batch, ctx)? { + crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), + _ => Err(DbError::Execution("Failed to execute sp_helpuser query".into())), + } +} + +pub(crate) fn execute_sp_helprole( + exec: &mut ScriptExecutor<'_>, + ctx: &mut ExecutionContext<'_>, +) -> Result { + let sql = "SELECT name AS RoleName, principal_id AS RoleId, 0 AS IsAppRole FROM sys.database_principals WHERE type = 'R'"; + let batch = crate::parser::parse_batch(sql)?; + match exec.execute_batch(&batch, ctx)? { + crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), + _ => Err(DbError::Execution("Failed to execute sp_helprole query".into())), + } +} + +pub(crate) fn execute_sp_helprolemember( + exec: &mut ScriptExecutor<'_>, + ctx: &mut ExecutionContext<'_>, +) -> Result { + let sql = "SELECT r.name AS DbRole, m.name AS MemberName, m.principal_id AS MemberSID FROM sys.database_role_members rm JOIN sys.database_principals r ON rm.role_principal_id = r.principal_id JOIN sys.database_principals m ON rm.member_principal_id = m.principal_id"; + let batch = crate::parser::parse_batch(sql)?; + match exec.execute_batch(&batch, ctx)? { + crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), + _ => Err(DbError::Execution("Failed to execute sp_helprolemember query".into())), + } +} + +pub(crate) fn execute_sp_helpsrvrole( + exec: &mut ScriptExecutor<'_>, + ctx: &mut ExecutionContext<'_>, +) -> Result { + let sql = "SELECT name AS ServerRole, principal_id AS RoleId FROM sys.server_principals WHERE type = 'R'"; + let batch = crate::parser::parse_batch(sql)?; + match exec.execute_batch(&batch, ctx)? { + crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), + _ => Err(DbError::Execution("Failed to execute sp_helpsrvrole query".into())), + } +} + +pub(crate) fn execute_sp_helpsrvrolemember( + exec: &mut ScriptExecutor<'_>, + ctx: &mut ExecutionContext<'_>, +) -> Result { + let sql = "SELECT r.name AS ServerRole, m.name AS MemberName, m.principal_id AS MemberSID FROM sys.server_role_members srm JOIN sys.server_principals r ON srm.role_principal_id = r.principal_id JOIN sys.server_principals m ON srm.member_principal_id = m.principal_id"; + let batch = crate::parser::parse_batch(sql)?; + match exec.execute_batch(&batch, ctx)? { + crate::error::StmtOutcome::Ok(Some(res)) => Ok(res), + _ => Err(DbError::Execution("Failed to execute sp_helpsrvrolemember query".into())), + } +} diff --git a/crates/iridium_core/src/executor/script/procedural/system_procedures/session.rs b/crates/iridium_core/src/executor/script/procedural/system_procedures/session.rs new file mode 100644 index 0000000..11d1d4a --- /dev/null +++ b/crates/iridium_core/src/executor/script/procedural/system_procedures/session.rs @@ -0,0 +1,231 @@ +use crate::error::DbError; +use crate::executor::context::ExecutionContext; +use crate::executor::result::QueryResult; +use crate::executor::script::ScriptExecutor; +use crate::types::{DataType, Value}; +use crate::ast::statements::procedural::ExecProcedureStmt; +use crate::executor::evaluator::eval_expr; + +pub(crate) fn execute_sp_who(ctx: &ExecutionContext<'_>) -> Result { + let rows = vec![vec![ + Value::Int(ctx.metadata.id as i32), + Value::Int(0), // ecid + Value::NVarChar("running".to_string()), + Value::NVarChar( + ctx.metadata + .user + .clone() + .unwrap_or_else(|| "sa".to_string()), + ), + Value::NVarChar( + ctx.metadata + .host_name + .clone() + .unwrap_or_else(|| "localhost".to_string()), + ), + Value::Char("0".to_string()), // blk + Value::NVarChar(ctx.metadata.database.clone().unwrap_or_default()), + Value::NVarChar("SELECT".to_string()), // cmd + Value::Int(0), // request_id + ]]; + + Ok(QueryResult { + columns: vec![ + "spid".into(), + "ecid".into(), + "status".into(), + "loginame".into(), + "hostname".into(), + "blk".into(), + "dbname".into(), + "cmd".into(), + "request_id".into(), + ], + column_types: vec![ + DataType::Int, + DataType::Int, + DataType::NVarChar { max_len: 30 }, + DataType::NVarChar { max_len: 128 }, + DataType::NVarChar { max_len: 128 }, + DataType::Char { len: 5 }, + DataType::NVarChar { max_len: 128 }, + DataType::NVarChar { max_len: 16 }, + DataType::Int, + ], + column_nullabilities: vec![ + false, false, false, false, false, false, false, false, false, + ], + rows, + ..Default::default() + }) +} + +pub(crate) fn execute_sp_databases() -> Result { + let mut rows = Vec::new(); + for db in crate::executor::database_catalog::builtin_databases() { + rows.push(vec![ + Value::VarChar(db.name.to_string()), + Value::Int(0), // DATABASE_SIZE + Value::Null, // REMARKS + ]); + } + Ok(QueryResult { + columns: vec!["DATABASE_NAME".into(), "DATABASE_SIZE".into(), "REMARKS".into()], + column_types: vec![ + DataType::VarChar { max_len: 128 }, + DataType::Int, + DataType::VarChar { max_len: 254 }, + ], + column_nullabilities: vec![false, false, true], + rows, + ..Default::default() + }) +} + +pub(crate) fn execute_sp_server_info() -> Result { + let rows = vec![ + vec![ + Value::Int(1), + Value::VarChar("DBMS_NAME".into()), + Value::VarChar("SQL Server".into()), + ], + vec![ + Value::Int(2), + Value::VarChar("DBMS_VER".into()), + Value::VarChar("Microsoft SQL Server 2025 - 17.0.1000.0".into()), + ], + vec![ + Value::Int(10), + Value::VarChar("OWNER_TERM".into()), + Value::VarChar("owner".into()), + ], + vec![ + Value::Int(11), + Value::VarChar("TABLE_TERM".into()), + Value::VarChar("table".into()), + ], + vec![ + Value::Int(12), + Value::VarChar("MAX_OWNER_NAME_LENGTH".into()), + Value::VarChar("128".into()), + ], + vec![ + Value::Int(13), + Value::VarChar("TABLE_LENGTH".into()), + Value::VarChar("128".into()), + ], + ]; + Ok(QueryResult { + columns: vec![ + "ATTRIBUTE_ID".into(), + "ATTRIBUTE_NAME".into(), + "ATTRIBUTE_VALUE".into(), + ], + column_types: vec![ + DataType::Int, + DataType::VarChar { max_len: 60 }, + DataType::VarChar { max_len: 255 }, + ], + column_nullabilities: vec![false, false, false], + rows, + ..Default::default() + }) +} + +pub(crate) fn execute_sp_monitor(exec: &ScriptExecutor<'_>) -> Result { + let now = Value::DateTime(exec.clock.now_datetime_literal()); + let rows = vec![vec![ + now.clone(), // last_run + now.clone(), // current_run + Value::Int(0), // seconds + Value::Int(0), // cpu_busy + Value::Int(0), // io_busy + Value::Int(0), // idle + Value::Int(0), // packets_received + Value::Int(0), // packets_sent + Value::Int(0), // packet_errors + Value::Int(0), // total_read + Value::Int(0), // total_write + Value::Int(0), // total_errors + Value::Int(0), // connections + ]]; + Ok(QueryResult { + columns: vec![ + "last_run".into(), + "current_run".into(), + "seconds".into(), + "cpu_busy".into(), + "io_busy".into(), + "idle".into(), + "packets_received".into(), + "packets_sent".into(), + "packet_errors".into(), + "total_read".into(), + "total_write".into(), + "total_errors".into(), + "connections".into(), + ], + column_types: vec![ + DataType::DateTime, + DataType::DateTime, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + DataType::Int, + ], + column_nullabilities: vec![ + false, false, false, false, false, false, false, false, false, false, false, false, + false, + ], + rows, + ..Default::default() + }) +} + +pub(crate) fn execute_sp_set_session_context( + stmt: &ExecProcedureStmt, + ctx: &mut ExecutionContext<'_>, + exec: &mut ScriptExecutor<'_>, +) -> Result { + let mut key = String::new(); + let mut value = Value::Null; + let mut read_only = false; + + for arg in &stmt.args { + let val = eval_expr(&arg.expr, &[], ctx, exec.catalog, exec.storage, exec.clock)?; + match arg.name.as_ref().map(|s| s.to_ascii_lowercase()) { + Some(ref n) if n == "@key" => key = val.to_string_value(), + Some(ref n) if n == "@value" => value = val, + Some(ref n) if n == "@read_only" => read_only = val.to_bool().unwrap_or(false), + _ => { + // Positional arguments fallback if needed, but MSSQL usually uses named for this + } + } + } + + if key.is_empty() { + return Err(DbError::Execution( + "sp_set_session_context: @key is required".into(), + )); + } + + if let Some((_, is_ro)) = ctx.session.session_context.get(&key) { + if *is_ro { + return Err(DbError::Execution(format!( + "Cannot set value for read-only session context key '{}'", + key + ))); + } + } + + ctx.session.session_context.insert(key, (value, read_only)); + + Ok(QueryResult::default()) +} diff --git a/crates/iridium_core/src/executor/script/procedural/throw.rs b/crates/iridium_core/src/executor/script/procedural/throw.rs index c7d4de5..ad0102e 100644 --- a/crates/iridium_core/src/executor/script/procedural/throw.rs +++ b/crates/iridium_core/src/executor/script/procedural/throw.rs @@ -42,7 +42,7 @@ impl<'a> ScriptExecutor<'a> { self.storage, self.clock, )?; - let msg = format!("{}", message.to_string_value()); + let msg = message.to_string_value().to_string(); Err(DbError::Custom { class: 16, number: error_number.to_integer_i64().unwrap_or(50000) as i32, diff --git a/crates/iridium_core/src/executor/value_ops/coercion.rs b/crates/iridium_core/src/executor/value_ops/coercion.rs deleted file mode 100644 index 43743bf..0000000 --- a/crates/iridium_core/src/executor/value_ops/coercion.rs +++ /dev/null @@ -1,905 +0,0 @@ -use crate::error::DbError; -use crate::types::{parse_vector_literal, DataType, Value}; -use std::fmt::Debug; -use uuid::Uuid; - -use super::super::value_helpers::{pad_binary_right, pad_right, rescale_raw}; -use super::formatting::parse_datetime_string; - -pub fn coerce_value_to_type(value: Value, ty: &DataType) -> Result { - coerce_value_to_type_with_dateformat(value, ty, "mdy") -} - -pub fn coerce_value_to_type_with_dateformat( - value: Value, - ty: &DataType, - dateformat: &str, -) -> Result { - if matches!(ty, DataType::SqlVariant) { - let nested = match &value { - Value::SqlVariant(inner) => inner.as_ref(), - other => other, - }; - if matches!(nested, Value::Vector(_)) { - return Err(DbError::Execution( - "cannot convert VECTOR to SQL_VARIANT".into(), - )); - } - return Ok(match value { - Value::Null => Value::Null, - Value::SqlVariant(inner) => Value::SqlVariant(inner), - other => Value::SqlVariant(Box::new(other)), - }); - } - - let value = match value { - Value::SqlVariant(inner) => *inner, - other => other, - }; - - match value { - Value::Null => Ok(Value::Null), - Value::Bit(v) => coerce_bit(v, ty), - Value::TinyInt(v) => coerce_int(v as i64, ty), - Value::SmallInt(v) => coerce_int(v as i64, ty), - Value::Int(v) => coerce_int(v as i64, ty), - Value::BigInt(v) => coerce_int(v, ty), - Value::Float(v) => coerce_float(v, ty), - Value::Decimal(raw, scale) => coerce_decimal(raw, scale, ty), - Value::Money(v) => coerce_money(v, ty), - Value::SmallMoney(v) => coerce_money(v as i128, ty), - Value::Char(v) | Value::VarChar(v) | Value::NChar(v) | Value::NVarChar(v) => { - coerce_string(&v, ty, dateformat) - } - Value::Binary(v) | Value::VarBinary(v) => coerce_binary(&v, ty), - Value::Vector(bits) => coerce_vector(Value::Vector(bits), ty), - Value::Date(v) => coerce_date_value(v, ty), - Value::Time(v) => coerce_time_value(v, ty), - Value::DateTime(v) => coerce_datetime_value(v, ty), - Value::DateTime2(v) => coerce_datetime_value(v, ty), - Value::SmallDateTime(v) => coerce_datetime_value(v, ty), - Value::DateTimeOffset(v) => coerce_string(&v, ty, dateformat), - Value::UniqueIdentifier(v) => coerce_uuid_value(v, ty), - Value::SqlVariant(inner) => coerce_value_to_type_with_dateformat(*inner, ty, dateformat), - } -} - -fn coerce_bit(v: bool, ty: &DataType) -> Result { - let int_val: i64 = if v { 1 } else { 0 }; - match ty { - DataType::Bit => Ok(Value::Bit(v)), - DataType::TinyInt => Ok(Value::TinyInt(int_val as u8)), - DataType::SmallInt => Ok(Value::SmallInt(int_val as i16)), - DataType::Int => Ok(Value::Int(int_val as i32)), - DataType::BigInt => Ok(Value::BigInt(int_val)), - DataType::Float => Ok(Value::Float((int_val as f64).to_bits())), - DataType::Decimal { scale, .. } => Ok(Value::Decimal(int_val as i128, *scale)), - DataType::Money => Ok(Value::Money(int_val as i128 * 10000)), - DataType::SmallMoney => Ok(Value::SmallMoney(int_val * 10000)), - DataType::Char { .. } | DataType::VarChar { .. } => Ok(Value::VarChar(int_val.to_string())), - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(int_val.to_string())) - } - DataType::Binary { .. } => Ok(Value::Binary(int_val.to_le_bytes().to_vec())), - DataType::VarBinary { .. } => Ok(Value::VarBinary(int_val.to_le_bytes().to_vec())), - DataType::DateTime - | DataType::DateTime2 - | DataType::SmallDateTime - | DataType::DateTimeOffset - | DataType::Date - | DataType::Time => Err(DbError::Execution(format!( - "cannot convert bit to {:?}", - ty - ))), - DataType::UniqueIdentifier => Err(DbError::Execution( - "cannot convert bit to UNIQUEIDENTIFIER".into(), - )), - DataType::Vector { .. } => Err(DbError::Execution(format!( - "cannot convert bit to {:?}", - ty - ))), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Bit(v)))), - DataType::Xml => Ok(Value::VarChar(int_val.to_string())), - } -} - -fn coerce_int(v: i64, ty: &DataType) -> Result { - match ty { - DataType::Bit => Ok(Value::Bit(v != 0)), - DataType::TinyInt => check_int_range::(v, "TINYINT").map(Value::TinyInt), - DataType::SmallInt => check_int_range::(v, "SMALLINT").map(Value::SmallInt), - DataType::Int => check_int_range::(v, "INT").map(Value::Int), - DataType::BigInt => Ok(Value::BigInt(v)), - DataType::Float => Ok(Value::Float((v as f64).to_bits())), - DataType::Decimal { scale, .. } => { - let raw = v as i128 * 10i128.pow(*scale as u32); - Ok(Value::Decimal(raw, *scale)) - } - DataType::Money => Ok(Value::Money(v as i128 * 10000)), - DataType::SmallMoney => Ok(Value::SmallMoney(v * 10000)), - DataType::Char { .. } | DataType::VarChar { .. } => Ok(Value::VarChar(v.to_string())), - DataType::NChar { .. } | DataType::NVarChar { .. } => Ok(Value::NVarChar(v.to_string())), - DataType::Binary { .. } => Ok(Value::Binary(v.to_le_bytes().to_vec())), - DataType::VarBinary { .. } => Ok(Value::VarBinary(v.to_le_bytes().to_vec())), - DataType::DateTime - | DataType::DateTime2 - | DataType::SmallDateTime - | DataType::DateTimeOffset - | DataType::Date - | DataType::Time => Err(DbError::Execution(format!( - "cannot convert integer to {:?}", - ty - ))), - DataType::UniqueIdentifier => Err(DbError::Execution( - "cannot convert integer to UNIQUEIDENTIFIER".into(), - )), - DataType::Vector { .. } => Err(DbError::Execution(format!( - "cannot convert integer to {:?}", - ty - ))), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::BigInt(v)))), - DataType::Xml => Ok(Value::VarChar(v.to_string())), - } -} - -fn coerce_decimal(raw: i128, scale: u8, ty: &DataType) -> Result { - match ty { - DataType::Decimal { scale: ts, .. } => { - if *ts == scale { - Ok(Value::Decimal(raw, scale)) - } else { - let converted = rescale_raw(raw, scale, *ts); - Ok(Value::Decimal(converted, *ts)) - } - } - DataType::Bit => Ok(Value::Bit(raw != 0)), - DataType::Float => { - let divisor = 10f64.powi(scale as i32); - Ok(Value::Float((raw as f64 / divisor).to_bits())) - } - DataType::TinyInt => { - let v = if scale > 0 { - raw / 10i128.pow(scale as u32) - } else { - raw - }; - if !(0..=255).contains(&v) { - Err(DbError::Execution( - "Arithmetic overflow error converting DECIMAL to TINYINT".into(), - )) - } else { - Ok(Value::TinyInt(v as u8)) - } - } - DataType::SmallInt => { - let v = if scale > 0 { - raw / 10i128.pow(scale as u32) - } else { - raw - }; - if v < i16::MIN as i128 || v > i16::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting DECIMAL to SMALLINT".into(), - )) - } else { - Ok(Value::SmallInt(v as i16)) - } - } - DataType::Int => { - let v = if scale > 0 { - raw / 10i128.pow(scale as u32) - } else { - raw - }; - if v < i32::MIN as i128 || v > i32::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting DECIMAL to INT".into(), - )) - } else { - Ok(Value::Int(v as i32)) - } - } - DataType::BigInt => { - let v = if scale > 0 { - raw / 10i128.pow(scale as u32) - } else { - raw - }; - if v < i64::MIN as i128 || v > i64::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting DECIMAL to BIGINT".into(), - )) - } else { - Ok(Value::BigInt(v as i64)) - } - } - DataType::Money => { - let money_scale = 4u8; - let converted = rescale_raw(raw, scale, money_scale); - Ok(Value::Money(converted)) - } - DataType::SmallMoney => { - let money_scale = 4u8; - let converted = rescale_raw(raw, scale, money_scale); - if converted < i64::MIN as i128 || converted > i64::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting DECIMAL to SMALLMONEY".into(), - )) - } else { - Ok(Value::SmallMoney(converted as i64)) - } - } - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(crate::types::format_decimal(raw, scale))) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(crate::types::format_decimal(raw, scale))) - } - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Decimal(raw, scale)))), - _ => Err(DbError::Execution(format!( - "cannot convert DECIMAL to {:?}", - ty - ))), - } -} - -fn coerce_float(bits: u64, ty: &DataType) -> Result { - let f = f64::from_bits(bits); - match ty { - DataType::Float => Ok(Value::Float(bits)), - DataType::Bit => Ok(Value::Bit(f != 0.0)), - DataType::TinyInt => { - if !(0.0..=255.0).contains(&f) { - Err(DbError::Execution( - "Arithmetic overflow error converting FLOAT to TINYINT".into(), - )) - } else { - Ok(Value::TinyInt(f as u8)) - } - } - DataType::SmallInt => { - if f < i16::MIN as f64 || f > i16::MAX as f64 { - Err(DbError::Execution( - "Arithmetic overflow error converting FLOAT to SMALLINT".into(), - )) - } else { - Ok(Value::SmallInt(f as i16)) - } - } - DataType::Int => { - if f < i32::MIN as f64 || f > i32::MAX as f64 { - Err(DbError::Execution( - "Arithmetic overflow error converting FLOAT to INT".into(), - )) - } else { - Ok(Value::Int(f as i32)) - } - } - DataType::BigInt => { - if f < i64::MIN as f64 || f > i64::MAX as f64 { - Err(DbError::Execution( - "Arithmetic overflow error converting FLOAT to BIGINT".into(), - )) - } else { - Ok(Value::BigInt(f as i64)) - } - } - DataType::Decimal { scale, .. } => { - let raw = (f * 10f64.powi(*scale as i32)).round() as i128; - Ok(Value::Decimal(raw, *scale)) - } - DataType::Money => { - let raw = (f * 10000.0) as i128; - Ok(Value::Money(raw)) - } - DataType::SmallMoney => { - let raw = (f * 10000.0) as i64; - Ok(Value::SmallMoney(raw)) - } - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(crate::types::format_float(f))) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(crate::types::format_float(f))) - } - DataType::Binary { .. } => Ok(Value::Binary((f as i64).to_le_bytes().to_vec())), - DataType::VarBinary { .. } => Ok(Value::VarBinary((f as i64).to_le_bytes().to_vec())), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Float(bits)))), - _ => Err(DbError::Execution(format!( - "cannot convert FLOAT to {:?}", - ty - ))), - } -} - -fn coerce_money(raw: i128, ty: &DataType) -> Result { - match ty { - DataType::Money => Ok(Value::Money(raw)), - DataType::SmallMoney => { - if raw < i64::MIN as i128 || raw > i64::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting MONEY to SMALLMONEY".into(), - )) - } else { - Ok(Value::SmallMoney(raw as i64)) - } - } - DataType::Bit => Ok(Value::Bit(raw != 0)), - DataType::TinyInt => { - let v = raw / 10000; - if !(0..=255).contains(&v) { - Err(DbError::Execution( - "Arithmetic overflow error converting MONEY to TINYINT".into(), - )) - } else { - Ok(Value::TinyInt(v as u8)) - } - } - DataType::SmallInt => { - let v = raw / 10000; - if v < i16::MIN as i128 || v > i16::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting MONEY to SMALLINT".into(), - )) - } else { - Ok(Value::SmallInt(v as i16)) - } - } - DataType::Int => { - let v = raw / 10000; - if v < i32::MIN as i128 || v > i32::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting MONEY to INT".into(), - )) - } else { - Ok(Value::Int(v as i32)) - } - } - DataType::BigInt => { - let v = raw / 10000; - if v < i64::MIN as i128 || v > i64::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting MONEY to BIGINT".into(), - )) - } else { - Ok(Value::BigInt(v as i64)) - } - } - DataType::Float => Ok(Value::Float((raw as f64 / 10000.0).to_bits())), - DataType::Decimal { scale, .. } => { - let money_scale = 4u8; - let converted = rescale_raw(raw, money_scale, *scale); - Ok(Value::Decimal(converted, *scale)) - } - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(crate::types::format_money(raw))) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(crate::types::format_money(raw))) - } - DataType::Binary { .. } => Ok(Value::Binary(raw.to_le_bytes().to_vec())), - DataType::VarBinary { .. } => Ok(Value::VarBinary(raw.to_le_bytes().to_vec())), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Money(raw)))), - _ => Err(DbError::Execution(format!( - "cannot convert MONEY to {:?}", - ty - ))), - } -} - -fn coerce_string(v: &str, ty: &DataType, dateformat: &str) -> Result { - match ty { - DataType::Bit => Ok(Value::Bit(v != "0" && !v.is_empty())), - DataType::TinyInt => v - .parse::() - .map(Value::TinyInt) - .map_err(|_| DbError::conversion_failed("varchar", v, "tinyint")), - DataType::SmallInt => v - .parse::() - .map(Value::SmallInt) - .map_err(|_| DbError::conversion_failed("varchar", v, "smallint")), - DataType::Int => v - .parse::() - .map(Value::Int) - .map_err(|_| DbError::conversion_failed("varchar", v, "int")), - DataType::BigInt => v - .parse::() - .map(Value::BigInt) - .map_err(|_| DbError::conversion_failed("varchar", v, "bigint")), - DataType::Float => v - .parse::() - .map(|f| Value::Float(f.to_bits())) - .map_err(|_| DbError::conversion_failed("varchar", v, "float")), - DataType::Decimal { scale, .. } => parse_decimal_string(v, *scale), - DataType::Money => parse_money_string(v), - DataType::SmallMoney => { - let m = parse_money_string(v)?; - match m { - Value::Money(raw) => { - if raw < i64::MIN as i128 || raw > i64::MAX as i128 { - Err(DbError::Execution( - "Arithmetic overflow error converting to SMALLMONEY".into(), - )) - } else { - Ok(Value::SmallMoney(raw as i64)) - } - } - other => Ok(other), - } - } - DataType::Char { len } => { - let padded = pad_right(v, *len as usize); - Ok(Value::Char(padded)) - } - DataType::VarChar { .. } => Ok(Value::VarChar(v.to_string())), - DataType::NChar { len } => { - let padded = pad_right(v, *len as usize); - Ok(Value::NChar(padded)) - } - DataType::NVarChar { .. } => Ok(Value::NVarChar(v.to_string())), - DataType::Binary { len } => { - let bytes = if v.starts_with("0x") || v.starts_with("0X") { - parse_hex_string(&v[2..])? - } else { - v.as_bytes().to_vec() - }; - let padded = pad_binary_right(&bytes, *len as usize); - Ok(Value::Binary(padded)) - } - DataType::VarBinary { .. } => { - let bytes = if v.starts_with("0x") || v.starts_with("0X") { - parse_hex_string(&v[2..])? - } else { - v.as_bytes().to_vec() - }; - Ok(Value::VarBinary(bytes)) - } - DataType::Vector { dimensions } => { - let bits = parse_vector_literal(v)?; - if bits.len() != *dimensions as usize { - return Err(DbError::Execution(format!( - "vector dimension mismatch: expected {}, got {}", - dimensions, - bits.len() - ))); - } - Ok(Value::Vector(bits)) - } - DataType::Date => { - let parsed = parse_date_string(v, dateformat) - .or_else(|_| parse_datetime_string(v, dateformat).map(|dt| dt.date())); - match parsed { - Ok(d) => Ok(Value::Date(d)), - Err(_) => Err(DbError::Execution(format!("invalid date: {}", v))), - } - } - DataType::Time => { - let parsed = chrono::NaiveTime::parse_from_str(v, "%H:%M:%S") - .or_else(|_| chrono::NaiveTime::parse_from_str(v, "%H:%M:%S%.f")); - match parsed { - Ok(t) => Ok(Value::Time(t)), - Err(_) => Err(DbError::Execution(format!("invalid time: {}", v))), - } - } - DataType::DateTime - | DataType::DateTime2 - | DataType::SmallDateTime - | DataType::DateTimeOffset => { - let parsed = parse_datetime_string(v, dateformat); - match parsed { - Ok(dt) => Ok(match ty { - DataType::DateTimeOffset => Value::DateTimeOffset(v.to_string()), - DataType::SmallDateTime => Value::SmallDateTime(dt), - _ => Value::DateTime(dt), - }), - Err(_) => Err(DbError::Execution(format!("invalid datetime: {}", v))), - } - } - DataType::UniqueIdentifier => { - let uuid = Uuid::parse_str(v) - .map_err(|_| DbError::Execution(format!("invalid UNIQUEIDENTIFIER: {}", v)))?; - Ok(Value::UniqueIdentifier(uuid)) - } - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::VarChar(v.to_string())))), - DataType::Xml => Ok(Value::VarChar(v.to_string())), - } -} - -fn coerce_vector(value: Value, ty: &DataType) -> Result { - match (value, ty) { - (Value::Vector(bits), DataType::Vector { dimensions }) => { - if bits.len() != *dimensions as usize { - Err(DbError::Execution(format!( - "vector dimension mismatch: expected {}, got {}", - dimensions, - bits.len() - ))) - } else { - Ok(Value::Vector(bits)) - } - } - (Value::Vector(bits), DataType::Char { len }) => Ok(Value::Char(pad_right( - &crate::types::format_vector(&bits), - *len as usize, - ))), - (Value::Vector(bits), DataType::VarChar { .. }) => { - Ok(Value::VarChar(crate::types::format_vector(&bits))) - } - (Value::Vector(bits), DataType::NChar { len }) => Ok(Value::NChar(pad_right( - &crate::types::format_vector(&bits), - *len as usize, - ))), - (Value::Vector(bits), DataType::NVarChar { .. }) => { - Ok(Value::NVarChar(crate::types::format_vector(&bits))) - } - (Value::Vector(_), DataType::SqlVariant) => Err(DbError::Execution( - "cannot convert VECTOR to SQL_VARIANT".into(), - )), - (Value::Vector(_), other) => Err(DbError::Execution(format!( - "cannot convert VECTOR to {:?}", - other - ))), - (other, _) => Err(DbError::Execution(format!( - "cannot convert {:?} to VECTOR", - other.data_type() - ))), - } -} - -fn coerce_date_value(v: chrono::NaiveDate, ty: &DataType) -> Result { - match ty { - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(v.format("%Y-%m-%d").to_string())) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(v.format("%Y-%m-%d").to_string())) - } - DataType::Date => Ok(Value::Date(v)), - DataType::DateTime | DataType::DateTime2 => { - let dt = v.and_hms_opt(0, 0, 0).unwrap(); - Ok(Value::DateTime(dt)) - } - DataType::SmallDateTime => { - let dt = v.and_hms_opt(0, 0, 0).unwrap(); - Ok(Value::SmallDateTime(dt)) - } - DataType::DateTimeOffset => { - let dt = v.and_hms_opt(0, 0, 0).unwrap(); - Ok(Value::DateTimeOffset( - dt.format("%Y-%m-%dT%H:%M:%S").to_string(), - )) - } - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Date(v)))), - _ => Err(DbError::Execution(format!( - "cannot convert DATE value to {:?}", - ty - ))), - } -} - -fn coerce_time_value(v: chrono::NaiveTime, ty: &DataType) -> Result { - match ty { - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(v.format("%H:%M:%S%.f").to_string())) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(v.format("%H:%M:%S%.f").to_string())) - } - DataType::Time => Ok(Value::Time(v)), - DataType::DateTime | DataType::DateTime2 => { - let dt = chrono::NaiveDate::from_ymd_opt(1900, 1, 1) - .unwrap() - .and_time(v); - Ok(Value::DateTime(dt)) - } - DataType::SmallDateTime => { - let dt = chrono::NaiveDate::from_ymd_opt(1900, 1, 1) - .unwrap() - .and_time(v); - Ok(Value::SmallDateTime(dt)) - } - DataType::DateTimeOffset => Ok(Value::DateTimeOffset(format!( - "1900-01-01T{}", - v.format("%H:%M:%S%.f") - ))), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Time(v)))), - _ => Err(DbError::Execution(format!( - "cannot convert TIME value to {:?}", - ty - ))), - } -} - -fn coerce_datetime_value(v: chrono::NaiveDateTime, ty: &DataType) -> Result { - match ty { - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(v.format("%Y-%m-%d %H:%M:%S%.f").to_string())) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => Ok(Value::NVarChar( - v.format("%Y-%m-%d %H:%M:%S%.f").to_string(), - )), - DataType::DateTime | DataType::DateTime2 => Ok(Value::DateTime(v)), - DataType::SmallDateTime => Ok(Value::SmallDateTime(v)), - DataType::DateTimeOffset => Ok(Value::DateTimeOffset( - v.format("%Y-%m-%dT%H:%M:%S%.f").to_string(), - )), - DataType::Date => Ok(Value::Date(v.date())), - DataType::Time => Ok(Value::Time(v.time())), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::DateTime(v)))), - _ => Err(DbError::Execution(format!( - "cannot convert DATETIME value to {:?}", - ty - ))), - } -} - -fn parse_date_string(v: &str, dateformat: &str) -> Result { - if let Ok(date) = chrono::NaiveDate::parse_from_str(v, "%Y-%m-%d") { - return Ok(date); - } - if let Ok(date) = chrono::NaiveDate::parse_from_str(v, "%Y/%m/%d") { - return Ok(date); - } - if let Ok(date) = chrono::NaiveDate::parse_from_str(v, "%Y.%m.%d") { - return Ok(date); - } - - let fmt = match dateformat.to_ascii_lowercase().as_str() { - "dmy" => ["%d/%m/%Y", "%d-%m-%Y", "%d.%m.%Y"], - "ymd" => ["%Y/%m/%d", "%Y-%m-%d", "%Y.%m.%d"], - "ydm" => ["%Y/%d/%m", "%Y-%d-%m", "%Y.%d.%m"], - "myd" => ["%m/%Y/%d", "%m-%Y-%d", "%m.%Y.%d"], - "dym" => ["%d/%Y/%m", "%d-%Y-%m", "%d.%Y.%m"], - _ => ["%m/%d/%Y", "%m-%d-%Y", "%m.%d.%Y"], - }; - - for candidate in fmt { - if let Ok(date) = chrono::NaiveDate::parse_from_str(v, candidate) { - return Ok(date); - } - } - - chrono::NaiveDate::parse_from_str(v, "%d/%m/%Y").map_err(|_| ()) -} - -fn coerce_binary(data: &[u8], ty: &DataType) -> Result { - match ty { - DataType::Bit - | DataType::TinyInt - | DataType::SmallInt - | DataType::Int - | DataType::BigInt - | DataType::Float - | DataType::Decimal { .. } - | DataType::Money - | DataType::SmallMoney => { - let i = parse_binary_to_i64(data)?; - coerce_int(i, ty) - } - DataType::Binary { len } => { - let padded = pad_binary_right(data, *len as usize); - Ok(Value::Binary(padded)) - } - DataType::VarBinary { .. } => Ok(Value::VarBinary(data.to_vec())), - DataType::Char { .. } | DataType::VarChar { .. } => { - Ok(Value::VarChar(crate::types::format_binary(data))) - } - DataType::NChar { .. } | DataType::NVarChar { .. } => { - Ok(Value::NVarChar(crate::types::format_binary(data))) - } - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Binary(data.to_vec())))), - DataType::UniqueIdentifier => { - if data.len() == 16 { - let arr: [u8; 16] = data[..16].try_into().map_err(|_| { - DbError::Execution("cannot convert BINARY to UNIQUEIDENTIFIER".into()) - })?; - let uuid = uuid::Uuid::from_bytes_le(arr); - Ok(Value::UniqueIdentifier(uuid)) - } else { - Err(DbError::Execution( - "cannot convert BINARY to UNIQUEIDENTIFIER: expected 16 bytes".into(), - )) - } - } - DataType::DateTime - | DataType::DateTime2 - | DataType::SmallDateTime - | DataType::DateTimeOffset - | DataType::Date - | DataType::Time => Err(DbError::Execution(format!( - "cannot convert BINARY to {:?}", - ty - ))), - _ => Err(DbError::Execution(format!( - "cannot convert BINARY to {:?}", - ty - ))), - } -} - -fn parse_binary_to_i64(data: &[u8]) -> Result { - if data.is_empty() { - return Ok(0); - } - if data.len() > 8 { - return Err(DbError::Execution( - "cannot convert BINARY longer than 8 bytes to integer".into(), - )); - } - - let mut n: u64 = 0; - for b in data { - n = (n << 8) | (*b as u64); - } - - // Sign-extend according to payload width (SQL Server-style binary-to-int casts). - let bit_width = (data.len() * 8) as u32; - if bit_width < 64 && (n & (1u64 << (bit_width - 1))) != 0 { - let mask = (!0u64) << bit_width; - n |= mask; - } - - Ok(n as i64) -} - -fn coerce_uuid_value(v: Uuid, ty: &DataType) -> Result { - match ty { - DataType::Char { .. } | DataType::VarChar { .. } => Ok(Value::VarChar(v.to_string())), - DataType::NChar { .. } | DataType::NVarChar { .. } => Ok(Value::NVarChar(v.to_string())), - DataType::UniqueIdentifier => Ok(Value::UniqueIdentifier(v)), - DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::UniqueIdentifier(v)))), - _ => Err(DbError::Execution(format!( - "cannot convert UNIQUEIDENTIFIER to {:?}", - ty - ))), - } -} - -pub fn parse_decimal_string(s: &str, scale: u8) -> Result { - let trimmed = s.trim(); - if trimmed.is_empty() { - return Err(DbError::Execution( - "cannot convert empty string to DECIMAL".into(), - )); - } - let negative = trimmed.starts_with('-'); - let abs_str = if negative || trimmed.starts_with('+') { - &trimmed[1..] - } else { - trimmed - }; - - let parts: Vec<&str> = abs_str.splitn(2, '.').collect(); - let whole_str = parts[0]; - let frac_str = parts.get(1).copied().unwrap_or(""); - - let whole: i128 = whole_str - .parse() - .map_err(|_| DbError::Execution(format!("cannot convert '{}' to DECIMAL", s)))?; - - let mut frac: i128 = 0; - if scale > 0 && !frac_str.is_empty() { - let truncated = if frac_str.len() > scale as usize { - &frac_str[..scale as usize] - } else { - frac_str - }; - frac = truncated - .parse() - .map_err(|_| DbError::Execution(format!("cannot convert '{}' to DECIMAL", s)))?; - if frac_str.len() < scale as usize { - frac *= 10i128.pow((scale as usize - frac_str.len()) as u32); - } - } - - let raw = whole * 10i128.pow(scale as u32) + frac; - let raw = if negative { -raw } else { raw }; - Ok(Value::Decimal(raw, scale)) -} - -pub fn parse_numeric_literal(s: &str) -> Result { - let trimmed = s.trim(); - if trimmed.is_empty() { - return Err(DbError::Execution("invalid numeric literal ''".into())); - } - - if trimmed.contains('e') || trimmed.contains('E') { - let f = trimmed - .parse::() - .map_err(|_| DbError::Execution(format!("invalid float literal '{}'", s)))?; - return Ok(Value::Float(f.to_bits())); - } - - if let Some(dot_idx) = trimmed.find('.') { - let scale = (trimmed.len() - dot_idx - 1) as u8; - return parse_decimal_string(trimmed, scale); - } - - let f = trimmed - .parse::() - .map_err(|_| DbError::Execution(format!("invalid float literal '{}'", s)))?; - Ok(Value::Float(f.to_bits())) -} - -pub fn parse_money_string(s: &str) -> Result { - let trimmed = s.trim().trim_start_matches('$'); - if trimmed.is_empty() { - return Err(DbError::Execution( - "cannot convert empty string to MONEY".into(), - )); - } - let scale = 4u8; - let negative = trimmed.starts_with('-'); - let abs_str = if negative || trimmed.starts_with('+') { - &trimmed[1..] - } else { - trimmed - }; - - let parts: Vec<&str> = abs_str.splitn(2, '.').collect(); - let whole_str = parts[0]; - let frac_str = parts.get(1).copied().unwrap_or(""); - - let whole: i128 = whole_str - .parse() - .map_err(|_| DbError::Execution(format!("cannot convert '{}' to MONEY", s)))?; - - let mut frac: i128 = 0; - if !frac_str.is_empty() { - let truncated = if frac_str.len() > scale as usize { - &frac_str[..scale as usize] - } else { - frac_str - }; - frac = truncated - .parse() - .map_err(|_| DbError::Execution(format!("cannot convert '{}' to MONEY", s)))?; - if frac_str.len() < scale as usize { - frac *= 10i128.pow((scale as usize - frac_str.len()) as u32); - } - } - - let raw = whole * 10i128.pow(scale as u32) + frac; - let raw = if negative { -raw } else { raw }; - Ok(Value::Money(raw)) -} - -pub fn parse_hex_string(s: &str) -> Result, DbError> { - let s = s.trim(); - if !s.len().is_multiple_of(2) { - return Err(DbError::Execution( - "hex string must have even number of digits".into(), - )); - } - let mut bytes = Vec::with_capacity(s.len() / 2); - let chars: Vec = s.chars().collect(); - for i in (0..chars.len()).step_by(2) { - let hi = hex_char_to_val(chars[i]) - .ok_or_else(|| DbError::Execution(format!("invalid hex digit '{}'", chars[i])))?; - let lo = hex_char_to_val(chars[i + 1]) - .ok_or_else(|| DbError::Execution(format!("invalid hex digit '{}'", chars[i + 1])))?; - bytes.push((hi << 4) | lo); - } - Ok(bytes) -} - -fn hex_char_to_val(c: char) -> Option { - match c { - '0'..='9' => Some(c as u8 - b'0'), - 'a'..='f' => Some(c as u8 - b'a' + 10), - 'A'..='F' => Some(c as u8 - b'A' + 10), - _ => None, - } -} - -fn check_int_range>(v: i64, type_name: &str) -> Result -where - T::Error: Debug, -{ - T::try_from(v).map_err(|_| { - DbError::Execution(format!( - "Arithmetic overflow error converting value {} to {}", - v, type_name - )) - }) -} diff --git a/crates/iridium_core/src/executor/value_ops/coercion/binary.rs b/crates/iridium_core/src/executor/value_ops/coercion/binary.rs new file mode 100644 index 0000000..26627c1 --- /dev/null +++ b/crates/iridium_core/src/executor/value_ops/coercion/binary.rs @@ -0,0 +1,98 @@ +use crate::error::DbError; +use crate::types::{DataType, Value}; +use crate::executor::value_helpers::pad_binary_right; +use uuid::Uuid; +use super::numeric::coerce_int; + +pub(crate) fn coerce_binary(data: &[u8], ty: &DataType) -> Result { + match ty { + DataType::Bit + | DataType::TinyInt + | DataType::SmallInt + | DataType::Int + | DataType::BigInt + | DataType::Float + | DataType::Decimal { .. } + | DataType::Money + | DataType::SmallMoney => { + let i = parse_binary_to_i64(data)?; + coerce_int(i, ty) + } + DataType::Binary { len } => { + let padded = pad_binary_right(data, *len as usize); + Ok(Value::Binary(padded)) + } + DataType::VarBinary { .. } => Ok(Value::VarBinary(data.to_vec())), + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(crate::types::format_binary(data))) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(crate::types::format_binary(data))) + } + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Binary(data.to_vec())))), + DataType::UniqueIdentifier => { + if data.len() == 16 { + let arr: [u8; 16] = data[..16].try_into().map_err(|_| { + DbError::Execution("cannot convert BINARY to UNIQUEIDENTIFIER".into()) + })?; + let uuid = uuid::Uuid::from_bytes_le(arr); + Ok(Value::UniqueIdentifier(uuid)) + } else { + Err(DbError::Execution( + "cannot convert BINARY to UNIQUEIDENTIFIER: expected 16 bytes".into(), + )) + } + } + DataType::DateTime + | DataType::DateTime2 + | DataType::SmallDateTime + | DataType::DateTimeOffset + | DataType::Date + | DataType::Time => Err(DbError::Execution(format!( + "cannot convert BINARY to {:?}", + ty + ))), + _ => Err(DbError::Execution(format!( + "cannot convert BINARY to {:?}", + ty + ))), + } +} + +pub(crate) fn parse_binary_to_i64(data: &[u8]) -> Result { + if data.is_empty() { + return Ok(0); + } + if data.len() > 8 { + return Err(DbError::Execution( + "cannot convert BINARY longer than 8 bytes to integer".into(), + )); + } + + let mut n: u64 = 0; + for b in data { + n = (n << 8) | (*b as u64); + } + + // Sign-extend according to payload width (SQL Server-style binary-to-int casts). + let bit_width = (data.len() * 8) as u32; + if bit_width < 64 && (n & (1u64 << (bit_width - 1))) != 0 { + let mask = (!0u64) << bit_width; + n |= mask; + } + + Ok(n as i64) +} + +pub(crate) fn coerce_uuid_value(v: Uuid, ty: &DataType) -> Result { + match ty { + DataType::Char { .. } | DataType::VarChar { .. } => Ok(Value::VarChar(v.to_string())), + DataType::NChar { .. } | DataType::NVarChar { .. } => Ok(Value::NVarChar(v.to_string())), + DataType::UniqueIdentifier => Ok(Value::UniqueIdentifier(v)), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::UniqueIdentifier(v)))), + _ => Err(DbError::Execution(format!( + "cannot convert UNIQUEIDENTIFIER to {:?}", + ty + ))), + } +} diff --git a/crates/iridium_core/src/executor/value_ops/coercion/datetime.rs b/crates/iridium_core/src/executor/value_ops/coercion/datetime.rs new file mode 100644 index 0000000..79c6279 --- /dev/null +++ b/crates/iridium_core/src/executor/value_ops/coercion/datetime.rs @@ -0,0 +1,118 @@ +use crate::error::DbError; +use crate::types::{DataType, Value}; + +pub(crate) fn coerce_date_value(v: chrono::NaiveDate, ty: &DataType) -> Result { + match ty { + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(v.format("%Y-%m-%d").to_string())) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(v.format("%Y-%m-%d").to_string())) + } + DataType::Date => Ok(Value::Date(v)), + DataType::DateTime | DataType::DateTime2 => { + let dt = v.and_hms_opt(0, 0, 0).unwrap(); + Ok(Value::DateTime(dt)) + } + DataType::SmallDateTime => { + let dt = v.and_hms_opt(0, 0, 0).unwrap(); + Ok(Value::SmallDateTime(dt)) + } + DataType::DateTimeOffset => { + let dt = v.and_hms_opt(0, 0, 0).unwrap(); + Ok(Value::DateTimeOffset( + dt.format("%Y-%m-%dT%H:%M:%S").to_string(), + )) + } + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Date(v)))), + _ => Err(DbError::Execution(format!( + "cannot convert DATE value to {:?}", + ty + ))), + } +} + +pub(crate) fn coerce_time_value(v: chrono::NaiveTime, ty: &DataType) -> Result { + match ty { + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(v.format("%H:%M:%S%.f").to_string())) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(v.format("%H:%M:%S%.f").to_string())) + } + DataType::Time => Ok(Value::Time(v)), + DataType::DateTime | DataType::DateTime2 => { + let dt = chrono::NaiveDate::from_ymd_opt(1900, 1, 1) + .unwrap() + .and_time(v); + Ok(Value::DateTime(dt)) + } + DataType::SmallDateTime => { + let dt = chrono::NaiveDate::from_ymd_opt(1900, 1, 1) + .unwrap() + .and_time(v); + Ok(Value::SmallDateTime(dt)) + } + DataType::DateTimeOffset => Ok(Value::DateTimeOffset(format!( + "1900-01-01T{}", + v.format("%H:%M:%S%.f") + ))), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Time(v)))), + _ => Err(DbError::Execution(format!( + "cannot convert TIME value to {:?}", + ty + ))), + } +} + +pub(crate) fn coerce_datetime_value(v: chrono::NaiveDateTime, ty: &DataType) -> Result { + match ty { + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(v.format("%Y-%m-%d %H:%M:%S%.f").to_string())) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => Ok(Value::NVarChar( + v.format("%Y-%m-%d %H:%M:%S%.f").to_string(), + )), + DataType::DateTime | DataType::DateTime2 => Ok(Value::DateTime(v)), + DataType::SmallDateTime => Ok(Value::SmallDateTime(v)), + DataType::DateTimeOffset => Ok(Value::DateTimeOffset( + v.format("%Y-%m-%dT%H:%M:%S%.f").to_string(), + )), + DataType::Date => Ok(Value::Date(v.date())), + DataType::Time => Ok(Value::Time(v.time())), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::DateTime(v)))), + _ => Err(DbError::Execution(format!( + "cannot convert DATETIME value to {:?}", + ty + ))), + } +} + +pub(crate) fn parse_date_string(v: &str, dateformat: &str) -> Result { + if let Ok(date) = chrono::NaiveDate::parse_from_str(v, "%Y-%m-%d") { + return Ok(date); + } + if let Ok(date) = chrono::NaiveDate::parse_from_str(v, "%Y/%m/%d") { + return Ok(date); + } + if let Ok(date) = chrono::NaiveDate::parse_from_str(v, "%Y.%m.%d") { + return Ok(date); + } + + let fmt = match dateformat.to_ascii_lowercase().as_str() { + "dmy" => ["%d/%m/%Y", "%d-%m-%Y", "%d.%m.%Y"], + "ymd" => ["%Y/%m/%d", "%Y-%m-%d", "%Y.%m.%d"], + "ydm" => ["%Y/%d/%m", "%Y-%d-%m", "%Y.%d.%m"], + "myd" => ["%m/%Y/%d", "%m-%Y-%d", "%m.%Y.%d"], + "dym" => ["%d/%Y/%m", "%d-%Y-%m", "%d.%Y.%m"], + _ => ["%m/%d/%Y", "%m-%d-%Y", "%m.%d.%Y"], + }; + + for candidate in fmt { + if let Ok(date) = chrono::NaiveDate::parse_from_str(v, candidate) { + return Ok(date); + } + } + + chrono::NaiveDate::parse_from_str(v, "%d/%m/%Y").map_err(|_| ()) +} diff --git a/crates/iridium_core/src/executor/value_ops/coercion/mod.rs b/crates/iridium_core/src/executor/value_ops/coercion/mod.rs new file mode 100644 index 0000000..d7a8bdc --- /dev/null +++ b/crates/iridium_core/src/executor/value_ops/coercion/mod.rs @@ -0,0 +1,156 @@ +use crate::error::DbError; +use crate::types::{DataType, Value}; + +use super::super::value_helpers::pad_right; +pub mod numeric; +pub mod string; +pub mod datetime; +pub mod binary; + +use numeric::*; +use string::*; +use datetime::*; +use binary::*; + +pub fn coerce_value_to_type(value: Value, ty: &DataType) -> Result { + coerce_value_to_type_with_dateformat(value, ty, "mdy") +} + +pub fn coerce_value_to_type_with_dateformat( + value: Value, + ty: &DataType, + dateformat: &str, +) -> Result { + if matches!(ty, DataType::SqlVariant) { + let nested = match &value { + Value::SqlVariant(inner) => inner.as_ref(), + other => other, + }; + if matches!(nested, Value::Vector(_)) { + return Err(DbError::Execution( + "cannot convert VECTOR to SQL_VARIANT".into(), + )); + } + return Ok(match value { + Value::Null => Value::Null, + Value::SqlVariant(inner) => Value::SqlVariant(inner), + other => Value::SqlVariant(Box::new(other)), + }); + } + + let value = match value { + Value::SqlVariant(inner) => *inner, + other => other, + }; + + match value { + Value::Null => Ok(Value::Null), + Value::Bit(v) => coerce_bit(v, ty), + Value::TinyInt(v) => coerce_int(v as i64, ty), + Value::SmallInt(v) => coerce_int(v as i64, ty), + Value::Int(v) => coerce_int(v as i64, ty), + Value::BigInt(v) => coerce_int(v, ty), + Value::Float(v) => coerce_float(v, ty), + Value::Decimal(raw, scale) => coerce_decimal(raw, scale, ty), + Value::Money(v) => coerce_money(v, ty), + Value::SmallMoney(v) => coerce_money(v as i128, ty), + Value::Char(v) | Value::VarChar(v) | Value::NChar(v) | Value::NVarChar(v) => { + coerce_string(&v, ty, dateformat) + } + Value::Binary(v) | Value::VarBinary(v) => coerce_binary(&v, ty), + Value::Vector(bits) => coerce_vector(Value::Vector(bits), ty), + Value::Date(v) => coerce_date_value(v, ty), + Value::Time(v) => coerce_time_value(v, ty), + Value::DateTime(v) => coerce_datetime_value(v, ty), + Value::DateTime2(v) => coerce_datetime_value(v, ty), + Value::SmallDateTime(v) => coerce_datetime_value(v, ty), + Value::DateTimeOffset(v) => coerce_string(&v, ty, dateformat), + Value::UniqueIdentifier(v) => coerce_uuid_value(v, ty), + Value::SqlVariant(inner) => coerce_value_to_type_with_dateformat(*inner, ty, dateformat), + } +} + +fn coerce_bit(v: bool, ty: &DataType) -> Result { + let int_val: i64 = if v { 1 } else { 0 }; + match ty { + DataType::Bit => Ok(Value::Bit(v)), + DataType::TinyInt => Ok(Value::TinyInt(int_val as u8)), + DataType::SmallInt => Ok(Value::SmallInt(int_val as i16)), + DataType::Int => Ok(Value::Int(int_val as i32)), + DataType::BigInt => Ok(Value::BigInt(int_val)), + DataType::Float => Ok(Value::Float((int_val as f64).to_bits())), + DataType::Decimal { scale, .. } => Ok(Value::Decimal(int_val as i128, *scale)), + DataType::Money => Ok(Value::Money(int_val as i128 * 10000)), + DataType::SmallMoney => Ok(Value::SmallMoney(int_val * 10000)), + DataType::Char { .. } | DataType::VarChar { .. } => Ok(Value::VarChar(int_val.to_string())), + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(int_val.to_string())) + } + DataType::Binary { .. } => Ok(Value::Binary(int_val.to_le_bytes().to_vec())), + DataType::VarBinary { .. } => Ok(Value::VarBinary(int_val.to_le_bytes().to_vec())), + DataType::DateTime + | DataType::DateTime2 + | DataType::SmallDateTime + | DataType::DateTimeOffset + | DataType::Date + | DataType::Time => Err(DbError::Execution(format!( + "cannot convert bit to {:?}", + ty + ))), + DataType::UniqueIdentifier => Err(DbError::Execution( + "cannot convert bit to UNIQUEIDENTIFIER".into(), + )), + DataType::Vector { .. } => Err(DbError::Execution(format!( + "cannot convert bit to {:?}", + ty + ))), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Bit(v)))), + DataType::Xml => Ok(Value::VarChar(int_val.to_string())), + } +} + + + + + + +fn coerce_vector(value: Value, ty: &DataType) -> Result { + match (value, ty) { + (Value::Vector(bits), DataType::Vector { dimensions }) => { + if bits.len() != *dimensions as usize { + Err(DbError::Execution(format!( + "vector dimension mismatch: expected {}, got {}", + dimensions, + bits.len() + ))) + } else { + Ok(Value::Vector(bits)) + } + } + (Value::Vector(bits), DataType::Char { len }) => Ok(Value::Char(pad_right( + &crate::types::format_vector(&bits), + *len as usize, + ))), + (Value::Vector(bits), DataType::VarChar { .. }) => { + Ok(Value::VarChar(crate::types::format_vector(&bits))) + } + (Value::Vector(bits), DataType::NChar { len }) => Ok(Value::NChar(pad_right( + &crate::types::format_vector(&bits), + *len as usize, + ))), + (Value::Vector(bits), DataType::NVarChar { .. }) => { + Ok(Value::NVarChar(crate::types::format_vector(&bits))) + } + (Value::Vector(_), DataType::SqlVariant) => Err(DbError::Execution( + "cannot convert VECTOR to SQL_VARIANT".into(), + )), + (Value::Vector(_), other) => Err(DbError::Execution(format!( + "cannot convert VECTOR to {:?}", + other + ))), + (other, _) => Err(DbError::Execution(format!( + "cannot convert {:?} to VECTOR", + other.data_type() + ))), + } +} diff --git a/crates/iridium_core/src/executor/value_ops/coercion/numeric.rs b/crates/iridium_core/src/executor/value_ops/coercion/numeric.rs new file mode 100644 index 0000000..fe842a5 --- /dev/null +++ b/crates/iridium_core/src/executor/value_ops/coercion/numeric.rs @@ -0,0 +1,409 @@ +use crate::error::DbError; +use crate::types::{DataType, Value}; +use std::fmt::Debug; +use crate::executor::value_helpers::rescale_raw; + +pub(crate) fn coerce_int(v: i64, ty: &DataType) -> Result { + match ty { + DataType::Bit => Ok(Value::Bit(v != 0)), + DataType::TinyInt => check_int_range::(v, "TINYINT").map(Value::TinyInt), + DataType::SmallInt => check_int_range::(v, "SMALLINT").map(Value::SmallInt), + DataType::Int => check_int_range::(v, "INT").map(Value::Int), + DataType::BigInt => Ok(Value::BigInt(v)), + DataType::Float => Ok(Value::Float((v as f64).to_bits())), + DataType::Decimal { scale, .. } => { + let raw = v as i128 * 10i128.pow(*scale as u32); + Ok(Value::Decimal(raw, *scale)) + } + DataType::Money => Ok(Value::Money(v as i128 * 10000)), + DataType::SmallMoney => Ok(Value::SmallMoney(v * 10000)), + DataType::Char { .. } | DataType::VarChar { .. } => Ok(Value::VarChar(v.to_string())), + DataType::NChar { .. } | DataType::NVarChar { .. } => Ok(Value::NVarChar(v.to_string())), + DataType::Binary { .. } => Ok(Value::Binary(v.to_le_bytes().to_vec())), + DataType::VarBinary { .. } => Ok(Value::VarBinary(v.to_le_bytes().to_vec())), + DataType::DateTime + | DataType::DateTime2 + | DataType::SmallDateTime + | DataType::DateTimeOffset + | DataType::Date + | DataType::Time => Err(DbError::Execution(format!( + "cannot convert integer to {:?}", + ty + ))), + DataType::UniqueIdentifier => Err(DbError::Execution( + "cannot convert integer to UNIQUEIDENTIFIER".into(), + )), + DataType::Vector { .. } => Err(DbError::Execution(format!( + "cannot convert integer to {:?}", + ty + ))), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::BigInt(v)))), + DataType::Xml => Ok(Value::VarChar(v.to_string())), + } +} + +pub(crate) fn coerce_decimal(raw: i128, scale: u8, ty: &DataType) -> Result { + match ty { + DataType::Decimal { scale: ts, .. } => { + if *ts == scale { + Ok(Value::Decimal(raw, scale)) + } else { + let converted = rescale_raw(raw, scale, *ts); + Ok(Value::Decimal(converted, *ts)) + } + } + DataType::Bit => Ok(Value::Bit(raw != 0)), + DataType::Float => { + let divisor = 10f64.powi(scale as i32); + Ok(Value::Float((raw as f64 / divisor).to_bits())) + } + DataType::TinyInt => { + let v = if scale > 0 { + raw / 10i128.pow(scale as u32) + } else { + raw + }; + if !(0..=255).contains(&v) { + Err(DbError::Execution( + "Arithmetic overflow error converting DECIMAL to TINYINT".into(), + )) + } else { + Ok(Value::TinyInt(v as u8)) + } + } + DataType::SmallInt => { + let v = if scale > 0 { + raw / 10i128.pow(scale as u32) + } else { + raw + }; + if v < i16::MIN as i128 || v > i16::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting DECIMAL to SMALLINT".into(), + )) + } else { + Ok(Value::SmallInt(v as i16)) + } + } + DataType::Int => { + let v = if scale > 0 { + raw / 10i128.pow(scale as u32) + } else { + raw + }; + if v < i32::MIN as i128 || v > i32::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting DECIMAL to INT".into(), + )) + } else { + Ok(Value::Int(v as i32)) + } + } + DataType::BigInt => { + let v = if scale > 0 { + raw / 10i128.pow(scale as u32) + } else { + raw + }; + if v < i64::MIN as i128 || v > i64::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting DECIMAL to BIGINT".into(), + )) + } else { + Ok(Value::BigInt(v as i64)) + } + } + DataType::Money => { + let money_scale = 4u8; + let converted = rescale_raw(raw, scale, money_scale); + Ok(Value::Money(converted)) + } + DataType::SmallMoney => { + let money_scale = 4u8; + let converted = rescale_raw(raw, scale, money_scale); + if converted < i64::MIN as i128 || converted > i64::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting DECIMAL to SMALLMONEY".into(), + )) + } else { + Ok(Value::SmallMoney(converted as i64)) + } + } + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(crate::types::format_decimal(raw, scale))) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(crate::types::format_decimal(raw, scale))) + } + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Decimal(raw, scale)))), + _ => Err(DbError::Execution(format!( + "cannot convert DECIMAL to {:?}", + ty + ))), + } +} + +pub(crate) fn coerce_float(bits: u64, ty: &DataType) -> Result { + let f = f64::from_bits(bits); + match ty { + DataType::Float => Ok(Value::Float(bits)), + DataType::Bit => Ok(Value::Bit(f != 0.0)), + DataType::TinyInt => { + if !(0.0..=255.0).contains(&f) { + Err(DbError::Execution( + "Arithmetic overflow error converting FLOAT to TINYINT".into(), + )) + } else { + Ok(Value::TinyInt(f as u8)) + } + } + DataType::SmallInt => { + if f < i16::MIN as f64 || f > i16::MAX as f64 { + Err(DbError::Execution( + "Arithmetic overflow error converting FLOAT to SMALLINT".into(), + )) + } else { + Ok(Value::SmallInt(f as i16)) + } + } + DataType::Int => { + if f < i32::MIN as f64 || f > i32::MAX as f64 { + Err(DbError::Execution( + "Arithmetic overflow error converting FLOAT to INT".into(), + )) + } else { + Ok(Value::Int(f as i32)) + } + } + DataType::BigInt => { + if f < i64::MIN as f64 || f > i64::MAX as f64 { + Err(DbError::Execution( + "Arithmetic overflow error converting FLOAT to BIGINT".into(), + )) + } else { + Ok(Value::BigInt(f as i64)) + } + } + DataType::Decimal { scale, .. } => { + let raw = (f * 10f64.powi(*scale as i32)).round() as i128; + Ok(Value::Decimal(raw, *scale)) + } + DataType::Money => { + let raw = (f * 10000.0) as i128; + Ok(Value::Money(raw)) + } + DataType::SmallMoney => { + let raw = (f * 10000.0) as i64; + Ok(Value::SmallMoney(raw)) + } + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(crate::types::format_float(f))) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(crate::types::format_float(f))) + } + DataType::Binary { .. } => Ok(Value::Binary((f as i64).to_le_bytes().to_vec())), + DataType::VarBinary { .. } => Ok(Value::VarBinary((f as i64).to_le_bytes().to_vec())), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Float(bits)))), + _ => Err(DbError::Execution(format!( + "cannot convert FLOAT to {:?}", + ty + ))), + } +} + +pub(crate) fn coerce_money(raw: i128, ty: &DataType) -> Result { + match ty { + DataType::Money => Ok(Value::Money(raw)), + DataType::SmallMoney => { + if raw < i64::MIN as i128 || raw > i64::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting MONEY to SMALLMONEY".into(), + )) + } else { + Ok(Value::SmallMoney(raw as i64)) + } + } + DataType::Bit => Ok(Value::Bit(raw != 0)), + DataType::TinyInt => { + let v = raw / 10000; + if !(0..=255).contains(&v) { + Err(DbError::Execution( + "Arithmetic overflow error converting MONEY to TINYINT".into(), + )) + } else { + Ok(Value::TinyInt(v as u8)) + } + } + DataType::SmallInt => { + let v = raw / 10000; + if v < i16::MIN as i128 || v > i16::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting MONEY to SMALLINT".into(), + )) + } else { + Ok(Value::SmallInt(v as i16)) + } + } + DataType::Int => { + let v = raw / 10000; + if v < i32::MIN as i128 || v > i32::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting MONEY to INT".into(), + )) + } else { + Ok(Value::Int(v as i32)) + } + } + DataType::BigInt => { + let v = raw / 10000; + if v < i64::MIN as i128 || v > i64::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting MONEY to BIGINT".into(), + )) + } else { + Ok(Value::BigInt(v as i64)) + } + } + DataType::Float => Ok(Value::Float((raw as f64 / 10000.0).to_bits())), + DataType::Decimal { scale, .. } => { + let money_scale = 4u8; + let converted = rescale_raw(raw, money_scale, *scale); + Ok(Value::Decimal(converted, *scale)) + } + DataType::Char { .. } | DataType::VarChar { .. } => { + Ok(Value::VarChar(crate::types::format_money(raw))) + } + DataType::NChar { .. } | DataType::NVarChar { .. } => { + Ok(Value::NVarChar(crate::types::format_money(raw))) + } + DataType::Binary { .. } => Ok(Value::Binary(raw.to_le_bytes().to_vec())), + DataType::VarBinary { .. } => Ok(Value::VarBinary(raw.to_le_bytes().to_vec())), + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::Money(raw)))), + _ => Err(DbError::Execution(format!( + "cannot convert MONEY to {:?}", + ty + ))), + } +} + +pub fn parse_decimal_string(s: &str, scale: u8) -> Result { + let trimmed = s.trim(); + if trimmed.is_empty() { + return Err(DbError::Execution( + "cannot convert empty string to DECIMAL".into(), + )); + } + let negative = trimmed.starts_with('-'); + let abs_str = if negative || trimmed.starts_with('+') { + &trimmed[1..] + } else { + trimmed + }; + + let parts: Vec<&str> = abs_str.splitn(2, '.').collect(); + let whole_str = parts[0]; + let frac_str = parts.get(1).copied().unwrap_or(""); + + let whole: i128 = whole_str + .parse() + .map_err(|_| DbError::Execution(format!("cannot convert '{}' to DECIMAL", s)))?; + + let mut frac: i128 = 0; + if scale > 0 && !frac_str.is_empty() { + let truncated = if frac_str.len() > scale as usize { + &frac_str[..scale as usize] + } else { + frac_str + }; + frac = truncated + .parse() + .map_err(|_| DbError::Execution(format!("cannot convert '{}' to DECIMAL", s)))?; + if frac_str.len() < scale as usize { + frac *= 10i128.pow((scale as usize - frac_str.len()) as u32); + } + } + + let raw = whole * 10i128.pow(scale as u32) + frac; + let raw = if negative { -raw } else { raw }; + Ok(Value::Decimal(raw, scale)) +} + +pub fn parse_numeric_literal(s: &str) -> Result { + let trimmed = s.trim(); + if trimmed.is_empty() { + return Err(DbError::Execution("invalid numeric literal ''".into())); + } + + if trimmed.contains('e') || trimmed.contains('E') { + let f = trimmed + .parse::() + .map_err(|_| DbError::Execution(format!("invalid float literal '{}'", s)))?; + return Ok(Value::Float(f.to_bits())); + } + + if let Some(dot_idx) = trimmed.find('.') { + let scale = (trimmed.len() - dot_idx - 1) as u8; + return parse_decimal_string(trimmed, scale); + } + + let f = trimmed + .parse::() + .map_err(|_| DbError::Execution(format!("invalid float literal '{}'", s)))?; + Ok(Value::Float(f.to_bits())) +} + +pub fn parse_money_string(s: &str) -> Result { + let trimmed = s.trim().trim_start_matches('$'); + if trimmed.is_empty() { + return Err(DbError::Execution( + "cannot convert empty string to MONEY".into(), + )); + } + let scale = 4u8; + let negative = trimmed.starts_with('-'); + let abs_str = if negative || trimmed.starts_with('+') { + &trimmed[1..] + } else { + trimmed + }; + + let parts: Vec<&str> = abs_str.splitn(2, '.').collect(); + let whole_str = parts[0]; + let frac_str = parts.get(1).copied().unwrap_or(""); + + let whole: i128 = whole_str + .parse() + .map_err(|_| DbError::Execution(format!("cannot convert '{}' to MONEY", s)))?; + + let mut frac: i128 = 0; + if !frac_str.is_empty() { + let truncated = if frac_str.len() > scale as usize { + &frac_str[..scale as usize] + } else { + frac_str + }; + frac = truncated + .parse() + .map_err(|_| DbError::Execution(format!("cannot convert '{}' to MONEY", s)))?; + if frac_str.len() < scale as usize { + frac *= 10i128.pow((scale as usize - frac_str.len()) as u32); + } + } + + let raw = whole * 10i128.pow(scale as u32) + frac; + let raw = if negative { -raw } else { raw }; + Ok(Value::Money(raw)) +} + +pub(crate) fn check_int_range>(v: i64, type_name: &str) -> Result +where + T::Error: Debug, +{ + T::try_from(v).map_err(|_| { + DbError::Execution(format!( + "Arithmetic overflow error converting value {} to {}", + v, type_name + )) + }) +} diff --git a/crates/iridium_core/src/executor/value_ops/coercion/string.rs b/crates/iridium_core/src/executor/value_ops/coercion/string.rs new file mode 100644 index 0000000..2916fa6 --- /dev/null +++ b/crates/iridium_core/src/executor/value_ops/coercion/string.rs @@ -0,0 +1,153 @@ +use crate::error::DbError; +use crate::types::{DataType, Value, parse_vector_literal}; +use crate::executor::value_helpers::{pad_binary_right, pad_right}; +use crate::executor::value_ops::formatting::parse_datetime_string; +use super::numeric::{parse_money_string, parse_decimal_string}; +use uuid::Uuid; +use super::datetime::parse_date_string; + +pub(crate) fn coerce_string(v: &str, ty: &DataType, dateformat: &str) -> Result { + match ty { + DataType::Bit => Ok(Value::Bit(v != "0" && !v.is_empty())), + DataType::TinyInt => v + .parse::() + .map(Value::TinyInt) + .map_err(|_| DbError::conversion_failed("varchar", v, "tinyint")), + DataType::SmallInt => v + .parse::() + .map(Value::SmallInt) + .map_err(|_| DbError::conversion_failed("varchar", v, "smallint")), + DataType::Int => v + .parse::() + .map(Value::Int) + .map_err(|_| DbError::conversion_failed("varchar", v, "int")), + DataType::BigInt => v + .parse::() + .map(Value::BigInt) + .map_err(|_| DbError::conversion_failed("varchar", v, "bigint")), + DataType::Float => v + .parse::() + .map(|f| Value::Float(f.to_bits())) + .map_err(|_| DbError::conversion_failed("varchar", v, "float")), + DataType::Decimal { scale, .. } => parse_decimal_string(v, *scale), + DataType::Money => parse_money_string(v), + DataType::SmallMoney => { + let m = parse_money_string(v)?; + match m { + Value::Money(raw) => { + if raw < i64::MIN as i128 || raw > i64::MAX as i128 { + Err(DbError::Execution( + "Arithmetic overflow error converting to SMALLMONEY".into(), + )) + } else { + Ok(Value::SmallMoney(raw as i64)) + } + } + other => Ok(other), + } + } + DataType::Char { len } => { + let padded = pad_right(v, *len as usize); + Ok(Value::Char(padded)) + } + DataType::VarChar { .. } => Ok(Value::VarChar(v.to_string())), + DataType::NChar { len } => { + let padded = pad_right(v, *len as usize); + Ok(Value::NChar(padded)) + } + DataType::NVarChar { .. } => Ok(Value::NVarChar(v.to_string())), + DataType::Binary { len } => { + let bytes = if v.starts_with("0x") || v.starts_with("0X") { + parse_hex_string(&v[2..])? + } else { + v.as_bytes().to_vec() + }; + let padded = pad_binary_right(&bytes, *len as usize); + Ok(Value::Binary(padded)) + } + DataType::VarBinary { .. } => { + let bytes = if v.starts_with("0x") || v.starts_with("0X") { + parse_hex_string(&v[2..])? + } else { + v.as_bytes().to_vec() + }; + Ok(Value::VarBinary(bytes)) + } + DataType::Vector { dimensions } => { + let bits = parse_vector_literal(v)?; + if bits.len() != *dimensions as usize { + return Err(DbError::Execution(format!( + "vector dimension mismatch: expected {}, got {}", + dimensions, + bits.len() + ))); + } + Ok(Value::Vector(bits)) + } + DataType::Date => { + let parsed = parse_date_string(v, dateformat) + .or_else(|_| parse_datetime_string(v, dateformat).map(|dt| dt.date())); + match parsed { + Ok(d) => Ok(Value::Date(d)), + Err(_) => Err(DbError::Execution(format!("invalid date: {}", v))), + } + } + DataType::Time => { + let parsed = chrono::NaiveTime::parse_from_str(v, "%H:%M:%S") + .or_else(|_| chrono::NaiveTime::parse_from_str(v, "%H:%M:%S%.f")); + match parsed { + Ok(t) => Ok(Value::Time(t)), + Err(_) => Err(DbError::Execution(format!("invalid time: {}", v))), + } + } + DataType::DateTime + | DataType::DateTime2 + | DataType::SmallDateTime + | DataType::DateTimeOffset => { + let parsed = parse_datetime_string(v, dateformat); + match parsed { + Ok(dt) => Ok(match ty { + DataType::DateTimeOffset => Value::DateTimeOffset(v.to_string()), + DataType::SmallDateTime => Value::SmallDateTime(dt), + _ => Value::DateTime(dt), + }), + Err(_) => Err(DbError::Execution(format!("invalid datetime: {}", v))), + } + } + DataType::UniqueIdentifier => { + let uuid = Uuid::parse_str(v) + .map_err(|_| DbError::Execution(format!("invalid UNIQUEIDENTIFIER: {}", v)))?; + Ok(Value::UniqueIdentifier(uuid)) + } + DataType::SqlVariant => Ok(Value::SqlVariant(Box::new(Value::VarChar(v.to_string())))), + DataType::Xml => Ok(Value::VarChar(v.to_string())), + } +} + +pub fn parse_hex_string(s: &str) -> Result, DbError> { + let s = s.trim(); + if !s.len().is_multiple_of(2) { + return Err(DbError::Execution( + "hex string must have even number of digits".into(), + )); + } + let mut bytes = Vec::with_capacity(s.len() / 2); + let chars: Vec = s.chars().collect(); + for i in (0..chars.len()).step_by(2) { + let hi = hex_char_to_val(chars[i]) + .ok_or_else(|| DbError::Execution(format!("invalid hex digit '{}'", chars[i])))?; + let lo = hex_char_to_val(chars[i + 1]) + .ok_or_else(|| DbError::Execution(format!("invalid hex digit '{}'", chars[i + 1])))?; + bytes.push((hi << 4) | lo); + } + Ok(bytes) +} + +pub(crate) fn hex_char_to_val(c: char) -> Option { + match c { + '0'..='9' => Some(c as u8 - b'0'), + 'a'..='f' => Some(c as u8 - b'a' + 10), + 'A'..='F' => Some(c as u8 - b'A' + 10), + _ => None, + } +} diff --git a/crates/iridium_core/src/executor/value_ops/mod.rs b/crates/iridium_core/src/executor/value_ops/mod.rs index f782d89..5b39e53 100644 --- a/crates/iridium_core/src/executor/value_ops/mod.rs +++ b/crates/iridium_core/src/executor/value_ops/mod.rs @@ -3,6 +3,6 @@ pub mod comparison; pub mod formatting; pub use coercion::coerce_value_to_type_with_dateformat; -pub use coercion::parse_numeric_literal; +pub use coercion::numeric::parse_numeric_literal; pub use comparison::{compare_values, truthy}; pub use formatting::convert_with_style; diff --git a/crates/iridium_core/src/storage/btree_index.rs b/crates/iridium_core/src/storage/btree_index.rs index 24c0b94..ff71866 100644 --- a/crates/iridium_core/src/storage/btree_index.rs +++ b/crates/iridium_core/src/storage/btree_index.rs @@ -51,13 +51,12 @@ impl BTreeIndex { let key = IndexKey::from_row(row_values, &self.column_ids) .ok_or_else(|| DbError::Storage("failed to extract index key from row".into()))?; - if self.is_unique { - if self.tree.contains_key(&key) { + if self.is_unique + && self.tree.contains_key(&key) { return Err(DbError::Execution( "Cannot insert duplicate key in unique index".into(), )); } - } self.tree.entry(key).or_default().push(row_index); Ok(()) diff --git a/crates/iridium_core/src/storage/redb_storage.rs b/crates/iridium_core/src/storage/redb_storage.rs index 36f035e..a261aea 100644 --- a/crates/iridium_core/src/storage/redb_storage.rs +++ b/crates/iridium_core/src/storage/redb_storage.rs @@ -437,7 +437,7 @@ impl IndexStorage for RedbStorage { .get(&index_id) .ok_or_else(|| DbError::Storage(format!("index {} not found", index_id)))?; - let result = index.seek(key).map(|v| v.clone()).unwrap_or_default(); + let result = index.seek(key).cloned().unwrap_or_default(); Ok(result) } diff --git a/crates/iridium_server/bin/compat-query.rs b/crates/iridium_server/bin/compat-query.rs index 898ca01..a2327c8 100644 --- a/crates/iridium_server/bin/compat-query.rs +++ b/crates/iridium_server/bin/compat-query.rs @@ -126,7 +126,7 @@ fn to_envelope_result_set(result: &QueryResult) -> ResultSetEnvelope { .iter() .map(|row| row.iter().map(format_compat_value).collect::>()) .collect::>(); - rows.sort_by(|left, right| left.cmp(right)); + rows.sort(); ResultSetEnvelope { columns, diff --git a/crates/iridium_server/src/session/mod.rs b/crates/iridium_server/src/session/mod.rs index e13c1b1..03bfad4 100644 --- a/crates/iridium_server/src/session/mod.rs +++ b/crates/iridium_server/src/session/mod.rs @@ -458,7 +458,7 @@ impl TdsSession { } else { let mut buf = PacketBuilder::new(); let col_types: Vec<_> = fetch_result.column_types.iter() - .map(|ct| super::tds::type_mapping::runtime_type_to_tds(ct)) + .map(super::tds::type_mapping::runtime_type_to_tds) .collect(); tokens::write_colmetadata( &mut buf, @@ -645,7 +645,7 @@ impl TdsSession { } else { let mut buf = PacketBuilder::new(); let col_types: Vec<_> = fetch_result.column_types.iter() - .map(|ct| super::tds::type_mapping::runtime_type_to_tds(ct)) + .map(super::tds::type_mapping::runtime_type_to_tds) .collect(); tokens::write_output_int( &mut buf, "@cursor", handle, @@ -840,8 +840,7 @@ impl TdsSession { let sql = match cat_req.proc { CatalogProc::Tables => { let table_name = cat_req - .params - .get(0) + .params.first() .map(|p| p.value_sql.trim_matches('\'')) .unwrap_or("%"); let table_owner = cat_req @@ -856,8 +855,7 @@ impl TdsSession { } CatalogProc::Columns => { let table_name = cat_req - .params - .get(0) + .params.first() .map(|p| p.value_sql.trim_matches('\'')) .unwrap_or("%"); format!( @@ -867,8 +865,7 @@ impl TdsSession { } CatalogProc::SprocColumns => { let proc_name = cat_req - .params - .get(0) + .params.first() .map(|p| p.value_sql.trim_matches('\'')) .unwrap_or("%"); format!( @@ -878,8 +875,7 @@ impl TdsSession { } CatalogProc::PrimaryKeys => { let table_name = cat_req - .params - .get(0) + .params.first() .map(|p| p.value_sql.trim_matches('\'')) .unwrap_or("%"); format!( diff --git a/crates/iridium_server/src/tds/bulk.rs b/crates/iridium_server/src/tds/bulk.rs index 797e293..a7a3bc8 100644 --- a/crates/iridium_server/src/tds/bulk.rs +++ b/crates/iridium_server/src/tds/bulk.rs @@ -70,7 +70,7 @@ pub fn parse_bulk_load_data( Ok(BulkLoadData { columns: column_names, - column_types: column_types, + column_types, rows, }) } diff --git a/crates/iridium_server/src/tds/type_mapping.rs b/crates/iridium_server/src/tds/type_mapping.rs index 4c52338..c35e091 100644 --- a/crates/iridium_server/src/tds/type_mapping.rs +++ b/crates/iridium_server/src/tds/type_mapping.rs @@ -716,7 +716,7 @@ pub fn read_type_info(reader: &mut PacketReader) -> io::Result { 0x26 | 0x68 | 0x6D | 0x6E | 0x6F | 0x24 | 0x28 => { length_prefix.push(reader.read_u8()?); } - 0x29 | 0x2A | 0x2B => { + 0x29..=0x2B => { let s = reader.read_u8()?; length_prefix.push(s); scale = Some(s);