master
go 435 lines 9.46 KB
Raw
1 // Package odbcbridge provides an optimized ODBC connection interface using CGO.
2 // This version handles AS400-specific issues like negative row counts and proper data types.
3
4 //go:build cgo
5
6 package odbcbridge
7
8 /*
9 #cgo CFLAGS: -I.
10 #cgo LDFLAGS: -lodbc
11 #include "bridge.h"
12 #include <stdlib.h>
13 */
14 import "C"
15 import (
16 "bytes"
17 "context"
18 "database/sql/driver"
19 "errors"
20 "fmt"
21 "io"
22 "strconv"
23 "strings"
24 "sync"
25 "unsafe"
26 )
27
28 // OptimizedConnection represents an ODBC database connection with statement reuse
29 type OptimizedConnection struct {
30 handle C.odbc_conn_t
31 mu sync.Mutex
32 }
33
34 func cleanErrorBuffer(buf []byte) string {
35 if len(buf) == 0 {
36 return ""
37 }
38 if idx := bytes.IndexByte(buf, 0); idx >= 0 {
39 buf = buf[:idx]
40 }
41 return strings.TrimSpace(string(buf))
42 }
43
44 // ConnectOptimized establishes a new optimized ODBC connection
45 func ConnectOptimized(dsn string) (*OptimizedConnection, error) {
46 cDSN := C.CString(dsn)
47 defer C.free(unsafe.Pointer(cDSN))
48
49 errorBuf := make([]byte, 1024)
50 handle := C.odbc_connect(cDSN, (*C.char)(unsafe.Pointer(&errorBuf[0])), C.int(len(errorBuf)))
51
52 if handle == nil {
53 return nil, fmt.Errorf("connection failed: %s", cleanErrorBuffer(errorBuf))
54 }
55
56 return &OptimizedConnection{handle: handle}, nil
57 }
58
59 // Close closes the connection
60 func (c *OptimizedConnection) Close() error {
61 c.mu.Lock()
62 defer c.mu.Unlock()
63
64 if c.handle != nil {
65 C.odbc_disconnect(c.handle)
66 c.handle = nil
67 }
68 return nil
69 }
70
71 // IsConnected checks if the connection is active
72 func (c *OptimizedConnection) IsConnected() bool {
73 c.mu.Lock()
74 defer c.mu.Unlock()
75
76 if c.handle == nil {
77 return false
78 }
79 return C.odbc_is_connected(c.handle) != 0
80 }
81
82 // QueryContext executes a query and returns optimized rows
83 func (c *OptimizedConnection) QueryContext(ctx context.Context, query string) (*OptimizedRows, error) {
84 c.mu.Lock()
85 defer c.mu.Unlock()
86
87 if c.handle == nil {
88 return nil, ErrNotConnected
89 }
90
91 // Check context before executing
92 select {
93 case <-ctx.Done():
94 return nil, ctx.Err()
95 default:
96 }
97
98 cQuery := C.CString(query)
99 defer C.free(unsafe.Pointer(cQuery))
100
101 errorBuf := make([]byte, 1024)
102
103 // Use execute_direct for one-off queries (optimizes for AS400)
104 ret := C.odbc_execute_direct(c.handle, cQuery, (*C.char)(unsafe.Pointer(&errorBuf[0])), C.int(len(errorBuf)))
105
106 if ret != Success {
107 return nil, fmt.Errorf("%w: %s", ErrQueryFailed, cleanErrorBuffer(errorBuf))
108 }
109
110 // Get metadata
111 columnCount := int(C.odbc_get_column_count(c.handle))
112 if columnCount == 0 {
113 C.odbc_reset_statement(c.handle)
114 return nil, ErrNoRows
115 }
116
117 // Get row count (can be negative on AS400!)
118 rowCount := int64(C.odbc_get_row_count(c.handle))
119
120 // Get column info
121 columns := make([]ColumnInfo, columnCount)
122 for i := 0; i < columnCount; i++ {
123 var cInfo C.odbc_column_info_t
124 if C.odbc_get_column_info(c.handle, C.int(i), &cInfo) == Success {
125 columns[i] = ColumnInfo{
126 Name: C.GoString(&cInfo.name[0]),
127 DataType: DataType(cInfo._type),
128 SQLType: int(cInfo.sql_type),
129 Size: int(cInfo.size),
130 Scale: int(cInfo.scale),
131 Nullable: bool(cInfo.nullable),
132 }
133 }
134 }
135
136 return &OptimizedRows{
137 conn: c,
138 columns: columns,
139 rowCount: rowCount,
140 ctx: ctx,
141 }, nil
142 }
143
144 // PrepareContext prepares a statement for repeated execution
145 func (c *OptimizedConnection) PrepareContext(ctx context.Context, query string) (*PreparedStatement, error) {
146 c.mu.Lock()
147 defer c.mu.Unlock()
148
149 if c.handle == nil {
150 return nil, ErrNotConnected
151 }
152
153 cQuery := C.CString(query)
154 defer C.free(unsafe.Pointer(cQuery))
155
156 errorBuf := make([]byte, 1024)
157 ret := C.odbc_prepare(c.handle, cQuery, (*C.char)(unsafe.Pointer(&errorBuf[0])), C.int(len(errorBuf)))
158
159 if ret != Success {
160 return nil, fmt.Errorf("prepare failed: %s", cleanErrorBuffer(errorBuf))
161 }
162
163 return &PreparedStatement{conn: c, ctx: ctx}, nil
164 }
165
166 // Return codes from C bridge
167 const (
168 Success = C.ODBC_SUCCESS
169 Error = C.ODBC_ERROR
170 NoData = C.ODBC_NO_DATA
171 )
172
173 // Common errors
174 var (
175 ErrNotConnected = errors.New("not connected")
176 ErrQueryFailed = errors.New("query failed")
177 ErrNoRows = errors.New("no rows returned")
178 )
179
180 // DataType represents ODBC data types
181 type DataType int
182
183 const (
184 TypeNull DataType = C.ODBC_TYPE_NULL
185 TypeInt64 DataType = C.ODBC_TYPE_INT64
186 TypeDouble DataType = C.ODBC_TYPE_DOUBLE
187 TypeString DataType = C.ODBC_TYPE_STRING
188 TypeBinary DataType = C.ODBC_TYPE_BINARY
189 )
190
191 // ColumnInfo contains column metadata
192 type ColumnInfo struct {
193 Name string
194 DataType DataType
195 SQLType int
196 Size int
197 Scale int
198 Nullable bool
199 }
200
201 // OptimizedRows represents the result of a query with proper type handling
202 type OptimizedRows struct {
203 conn *OptimizedConnection
204 columns []ColumnInfo
205 rowCount int64 // Can be negative on AS400!
206 ctx context.Context
207 closed bool
208 mu sync.Mutex
209 }
210
211 // Columns returns the column names
212 func (r *OptimizedRows) Columns() []string {
213 names := make([]string, len(r.columns))
214 for i, col := range r.columns {
215 names[i] = col.Name
216 }
217 return names
218 }
219
220 // ColumnInfo returns detailed column information
221 func (r *OptimizedRows) ColumnInfo() []ColumnInfo {
222 return r.columns
223 }
224
225 // RowCount returns the number of rows affected (can be negative on AS400!)
226 func (r *OptimizedRows) RowCount() int64 {
227 return r.rowCount
228 }
229
230 // Next advances to the next row with proper type handling
231 func (r *OptimizedRows) Next(dest []driver.Value) error {
232 r.mu.Lock()
233 defer r.mu.Unlock()
234
235 if r.closed {
236 return io.EOF
237 }
238
239 // Check context
240 select {
241 case <-r.ctx.Done():
242 return r.ctx.Err()
243 default:
244 }
245
246 ret := C.odbc_fetch_row(r.conn.handle)
247 if ret == NoData {
248 return io.EOF
249 } else if ret != Success {
250 return errors.New("fetch failed")
251 }
252
253 // Get values with proper type conversion
254 for i := range dest {
255 var value C.odbc_value_t
256 if C.odbc_get_value(r.conn.handle, C.int(i), &value) == Success {
257 dest[i] = convertValue(&value)
258 C.odbc_free_value(&value)
259 } else {
260 dest[i] = nil
261 }
262 }
263
264 return nil
265 }
266
267 // ScanTyped scans the current row with type information
268 func (r *OptimizedRows) ScanTyped(dest ...interface{}) error {
269 values := make([]driver.Value, len(dest))
270 err := r.Next(values)
271 if err != nil {
272 return err
273 }
274
275 for i, v := range values {
276 if err := convertAssignTyped(dest[i], v, r.columns[i].DataType); err != nil {
277 return err
278 }
279 }
280 return nil
281 }
282
283 // Close closes the rows
284 func (r *OptimizedRows) Close() error {
285 r.mu.Lock()
286 defer r.mu.Unlock()
287
288 if r.closed {
289 return nil
290 }
291
292 r.closed = true
293 // Just close cursor, keep statement for reuse
294 C.odbc_close_cursor(r.conn.handle)
295 return nil
296 }
297
298 // PreparedStatement represents a prepared SQL statement
299 type PreparedStatement struct {
300 conn *OptimizedConnection
301 ctx context.Context
302 }
303
304 // Execute executes the prepared statement
305 func (s *PreparedStatement) Execute() (*OptimizedRows, error) {
306 s.conn.mu.Lock()
307 defer s.conn.mu.Unlock()
308
309 errorBuf := make([]byte, 1024)
310 ret := C.odbc_execute(s.conn.handle, (*C.char)(unsafe.Pointer(&errorBuf[0])), C.int(len(errorBuf)))
311
312 if ret != Success {
313 return nil, fmt.Errorf("execute failed: %s", cleanErrorBuffer(errorBuf))
314 }
315
316 // Get metadata (same as QueryContext)
317 columnCount := int(C.odbc_get_column_count(s.conn.handle))
318 rowCount := int64(C.odbc_get_row_count(s.conn.handle))
319
320 columns := make([]ColumnInfo, columnCount)
321 for i := 0; i < columnCount; i++ {
322 var cInfo C.odbc_column_info_t
323 if C.odbc_get_column_info(s.conn.handle, C.int(i), &cInfo) == Success {
324 columns[i] = ColumnInfo{
325 Name: C.GoString(&cInfo.name[0]),
326 DataType: DataType(cInfo._type),
327 SQLType: int(cInfo.sql_type),
328 Size: int(cInfo.size),
329 Scale: int(cInfo.scale),
330 Nullable: bool(cInfo.nullable),
331 }
332 }
333 }
334
335 return &OptimizedRows{
336 conn: s.conn,
337 columns: columns,
338 rowCount: rowCount,
339 ctx: s.ctx,
340 }, nil
341 }
342
343 // Close closes the prepared statement
344 func (s *PreparedStatement) Close() error {
345 s.conn.mu.Lock()
346 defer s.conn.mu.Unlock()
347
348 C.odbc_reset_statement(s.conn.handle)
349 return nil
350 }
351
352 // convertValue converts C value to Go value with proper type handling
353 func convertValue(cValue *C.odbc_value_t) driver.Value {
354 if cValue.is_null {
355 return nil
356 }
357
358 switch DataType(cValue._type) {
359 case TypeInt64:
360 return int64(C.odbc_value_get_int64(cValue))
361 case TypeDouble:
362 return float64(C.odbc_value_get_double(cValue))
363 case TypeString:
364 str := C.odbc_value_get_string(cValue)
365 if str != nil {
366 return C.GoString(str)
367 }
368 return ""
369 default:
370 return nil
371 }
372 }
373
374 // convertAssignTyped converts a driver.Value to dest with type awareness
375 func convertAssignTyped(dest interface{}, src driver.Value, dataType DataType) error {
376 if src == nil {
377 return nil
378 }
379
380 switch d := dest.(type) {
381 case *string:
382 switch s := src.(type) {
383 case string:
384 *d = s
385 return nil
386 default:
387 *d = fmt.Sprint(src)
388 return nil
389 }
390
391 case *int64:
392 switch dataType {
393 case TypeInt64:
394 if v, ok := src.(int64); ok {
395 *d = v
396 return nil
397 }
398 case TypeString:
399 // AS400 might return numbers as strings
400 if s, ok := src.(string); ok {
401 val, err := strconv.ParseInt(s, 10, 64)
402 if err != nil {
403 return err
404 }
405 *d = val
406 return nil
407 }
408 }
409
410 case *float64:
411 switch dataType {
412 case TypeDouble:
413 if v, ok := src.(float64); ok {
414 *d = v
415 return nil
416 }
417 case TypeString:
418 // AS400 might return decimals as strings
419 if s, ok := src.(string); ok {
420 val, err := strconv.ParseFloat(s, 64)
421 if err != nil {
422 return err
423 }
424 *d = val
425 return nil
426 }
427 }
428
429 case *interface{}:
430 *d = src
431 return nil
432 }
433
434 return fmt.Errorf("unsupported conversion from %T (type %v) to %T", src, dataType, dest)
435 }