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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions .claude/settings.local.json

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
test_odbc.sqlite
lars_notes.md
.claude/
ODBC
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,4 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }

[dev-dependencies]
odbc-api = "14.3.0"
odbc-api = { version = "14.3.0", features = ["wide"] }
12 changes: 9 additions & 3 deletions plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,10 @@
- [ ] **Fix StatementHandle lifetime issues** (`src/odbc/implementation/alloc_handles.rs:24-29`)
- Replace raw references with proper ownership patterns
- Ensure statements don't outlive connections
- [ ] **Remove all `.unwrap()` from FFI functions**
- Replace with proper error return codes
- Add error logging without panicking
- [DONE - 2026-02-13] **Remove all `.unwrap()` from FFI functions**
- SOLUTION: Replaced 3 `.unwrap()` calls in FFI code with proper error handling
- FILES: Modified src/odbc/api/sqltables.rs (prepare/query unwraps → match with SqlReturn::ERROR), src/odbc/implementation/alloc_handles.rs (allocate_stmt_handle returns Option), src/odbc/api/sqlallochandle.rs (handles None from allocate_stmt_handle)
- OUTCOME: No more panics possible from FFI code; all errors return SqlReturn::ERROR with logging

### **Phase 2: Robustness & Completeness**
*Goal: Production-ready error handling and core functionality*
Expand All @@ -127,6 +128,11 @@
- Implement proper handle deallocation based on handle type
- Free both `HandleWrapper` and inner objects
- Add comprehensive tests
- [DONE - 2026-02-13] **SQLDescribeColW implementation**
- SOLUTION: Full implementation returning column name (UTF-16), data type (SQL_VARCHAR), column size (255), decimal digits (0), nullable (SQL_NULLABLE)
- FILES: Rewrote src/odbc/api/sqldescribecol.rs, added test_describe_columns integration test
- ALSO: Enabled `wide` feature on odbc-api dev-dependency for proper Unicode ODBC client testing, discovered `cargo build` is needed before integration tests (cdylib loaded at runtime by DM)
- KEY INSIGHT: ODBC spec for SQLDescribeColW uses character counts (not byte counts) for buffer_length and name_length_ptr
- [ ] **SQLDriverConnect implementation**
- Parse connection strings properly
- Support standard SQLite connection parameters
Expand Down
5 changes: 0 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,31 +10,26 @@ mod odbc;

// Windows: Use the built-in Windows ODBC library
#[cfg_attr(windows, link(name = "odbc32"))]

// Unix + Dynamic + unixODBC (default case for Linux)
#[cfg_attr(
all(not(windows), not(feature = "static"), not(feature = "iodbc")),
link(name = "odbcinst")
)]

// Unix + Static + unixODBC (for self-contained binaries)
#[cfg_attr(
all(not(windows), feature = "static", not(feature = "iodbc")),
link(name = "odbcinst", kind = "static")
)]

// Unix + Dynamic + iODBC (common on macOS)
#[cfg_attr(
all(not(windows), not(feature = "static"), feature = "iodbc"),
link(name = "iodbcinst")
)]

// Unix + Static + iODBC (self-contained binaries with iODBC)
#[cfg_attr(
all(not(windows), feature = "static", feature = "iodbc"),
link(name = "iodbcinst", kind = "static")
)]

// This function is here because it's needed for ODBC configuration file parsing
// but doesn't exist in the standard odbc-sys crate.
// It was added here directly temporarily, eventually this might move to a separate file or library.
Expand Down
23 changes: 14 additions & 9 deletions src/odbc/api/sqlallochandle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub extern "C" fn SQLAllocHandle(
// From the spec:
// When allocating a handle other than an environment handle, if SQLAllocHandle returns SQL_ERROR, it sets OutputHandlePtr to SQL_NULL_HDBC, SQL_NULL_HSTMT, or SQL_NULL_HDESC, depending on the value of HandleType, unless the output argument is a null pointer.
// The application can then obtain additional information from the diagnostic data structure associated with the handle in the InputHandle argument.
let result = match handle_type {
match handle_type {
HandleType::Env => {
// Spec: If HandleType is SQL_HANDLE_ENV, this is SQL_NULL_HANDLE.
if !input_handle.is_null() {
Expand Down Expand Up @@ -73,9 +73,9 @@ pub extern "C" fn SQLAllocHandle(
match get_from_wrapper(&HandleType::Env, input_handle) {
Ok(env) => env,
Err(err) => {
error!("Getting environment handle: {}", err);
error!("Failed to get environment handle: {}", err);
unsafe { *output_handle = std::ptr::null_mut() }
return SqlReturn::ERROR;
return SqlReturn::INVALID_HANDLE;
}
};

Expand All @@ -91,13 +91,20 @@ pub extern "C" fn SQLAllocHandle(
match get_from_wrapper(&HandleType::Dbc, input_handle) {
Ok(env) => env,
Err(err) => {
info!("Getting connection handle: {}", err);
error!("Failed to get connection handle: {}", err);
unsafe { *output_handle = std::ptr::null_mut() }
return SqlReturn::ERROR;
return SqlReturn::INVALID_HANDLE;
}
};

let handle = allocate_stmt_handle(connection_handle);
let handle = match allocate_stmt_handle(connection_handle) {
Some(handle) => handle,
None => {
error!("Cannot allocate statement handle: no active connection");
unsafe { *output_handle = std::ptr::null_mut() }
return SqlReturn::ERROR;
}
};
wrap_and_set(handle_type, handle, output_handle);

info!("Successfully allocated a Stmt handle");
Expand All @@ -106,9 +113,7 @@ pub extern "C" fn SQLAllocHandle(
}
HandleType::Desc => SqlReturn::SUCCESS,
HandleType::DbcInfoToken => SqlReturn::SUCCESS,
};

result
}
}

#[cfg(test)]
Expand Down
4 changes: 2 additions & 2 deletions src/odbc/api/sqlcolattribute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ pub extern "C" fn SQLColAttributeW(
// Return column name
match stmt.column_name(col_index) {
Ok(name) => return_string_attribute(
&name,
name,
character_attribute_ptr,
buffer_length,
string_length_ptr,
Expand Down Expand Up @@ -172,7 +172,7 @@ pub extern "C" fn SQLColAttributeW(
// Return column label (same as name for SQLite)
match stmt.column_name(col_index) {
Ok(name) => return_string_attribute(
&name,
name,
character_attribute_ptr,
buffer_length,
string_length_ptr,
Expand Down
6 changes: 3 additions & 3 deletions src/odbc/api/sqlconnect.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//!
//! https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlconnect-function?view=sql-server-ver16
//! <https://learn.microsoft.com/en-us/sql/odbc/reference/syntax/sqlconnect-function?view=sql-server-ver16>
//!
//! ```c
//! SQLRETURN SQLConnect(
Expand Down Expand Up @@ -41,8 +41,8 @@ pub extern "C" fn SQLConnectW(
match get_from_wrapper(&HandleType::Dbc, connection_handle) {
Ok(env) => env,
Err(e) => {
error!("{}", e);
return SqlReturn::ERROR;
error!("Failed to get connection handle: {}", e);
return SqlReturn::INVALID_HANDLE;
}
};

Expand Down
142 changes: 128 additions & 14 deletions src/odbc/api/sqldescribecol.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,134 @@
use odbc_sys::SqlReturn;
use crate::odbc::implementation::alloc_handles::StatementHandle;
use crate::odbc::utils::get_from_wrapper;
use odbc_sys::{HandleType, SqlReturn};
use std::ffi::c_void;
use tracing::info;
use std::ptr;
use tracing::{debug, error, info};

/// SQLDescribeColW returns the result descriptor for one column in the result set.
///
/// This function provides column metadata including name, data type, size,
/// decimal digits, and nullability. It must be called after a statement has
/// been prepared (via SQLPrepareW or SQLExecDirectW).
#[allow(non_snake_case)]
#[unsafe(no_mangle)]
pub extern "C" fn SQLDescribeCol(
_statement_handle: *mut c_void,
_column_number: u16,
_column_name: *mut u16,
_buffer_length: i16,
_name_length_ptr: *mut i16,
_data_type_ptr: *mut i16,
_column_size_ptr: *mut usize,
_decimal_digits_ptr: *mut i16,
_nullable_ptr: *mut i16,
pub extern "C" fn SQLDescribeColW(
statement_handle: *mut c_void,
column_number: u16,
column_name: *mut u16,
buffer_length: i16,
name_length_ptr: *mut i16,
data_type_ptr: *mut i16,
column_size_ptr: *mut usize,
decimal_digits_ptr: *mut i16,
nullable_ptr: *mut i16,
) -> SqlReturn {
info!("SQLDescribeCol");
SqlReturn::SUCCESS
info!(
"column_number={}, buffer_length={}",
column_number, buffer_length
);

// Get the statement handle
let statement_handle: &mut StatementHandle =
match get_from_wrapper(&HandleType::Stmt, statement_handle) {
Ok(handle) => handle,
Err(err) => {
error!("Failed to get statement handle: {}", err);
return SqlReturn::INVALID_HANDLE;
}
};

// Check if we have a prepared statement
let stmt = match &statement_handle.statement {
Some(stmt) => stmt,
None => {
error!("No prepared statement found");
return SqlReturn::ERROR;
}
};

// Validate column number (1-indexed in ODBC)
if column_number == 0 || column_number as usize > stmt.column_count() {
error!(
"Invalid column number {}. Valid range: 1-{}",
column_number,
stmt.column_count()
);
return SqlReturn::ERROR;
}

// Convert to 0-indexed for SQLite
let col_index = (column_number - 1) as usize;

// Write column name as UTF-16
let mut result = SqlReturn::SUCCESS;
match stmt.column_name(col_index) {
Ok(name) => {
debug!("Column {} name: '{}'", column_number, name);

let utf16_value: Vec<u16> = name.encode_utf16().collect();
let utf16_len = utf16_value.len() as i16;

// Set the actual name length in characters (excluding null terminator)
if !name_length_ptr.is_null() {
unsafe {
*name_length_ptr = utf16_len;
}
}

// Copy the name string if buffer is provided
// buffer_length is in characters (u16 elements) per ODBC spec
if !column_name.is_null() && buffer_length > 0 {
let max_chars = buffer_length as usize;
let copy_len = std::cmp::min(utf16_value.len(), max_chars.saturating_sub(1));

unsafe {
ptr::copy_nonoverlapping(utf16_value.as_ptr(), column_name, copy_len);
// Null-terminate
*column_name.add(copy_len) = 0;
}

if copy_len < utf16_value.len() {
result = SqlReturn::SUCCESS_WITH_INFO;
}
}
}
Err(err) => {
error!("Could not get column name for index {}: {}", col_index, err);
return SqlReturn::ERROR;
}
}

// Set data type - SQLite is dynamically typed, report VARCHAR for all columns
if !data_type_ptr.is_null() {
unsafe {
*data_type_ptr = 12; // SQL_VARCHAR
}
debug!("Returning type: SQL_VARCHAR");
}

// Set column size - default VARCHAR length
if !column_size_ptr.is_null() {
unsafe {
*column_size_ptr = 255;
}
debug!("Returning column size: 255");
}

// Set decimal digits - 0 for VARCHAR
if !decimal_digits_ptr.is_null() {
unsafe {
*decimal_digits_ptr = 0;
}
}

// Set nullable - SQLite columns are generally nullable
if !nullable_ptr.is_null() {
unsafe {
*nullable_ptr = 1; // SQL_NULLABLE
}
debug!("Returning nullable: SQL_NULLABLE");
}

result
}
Loading