# tdbcodbc.tcl -- | |
# | |
# Class definitions and Tcl-level methods for the tdbc::odbc bridge. | |
# | |
# Copyright (c) 2008 by Kevin B. Kenny | |
# See the file "license.terms" for information on usage and redistribution | |
# of this file, and for a DISCLAIMER OF ALL WARRANTIES. | |
# | |
# RCS: @(#) $Id: tdbcodbc.tcl,v 1.47 2008/02/27 02:08:27 kennykb Exp $ | |
# | |
#------------------------------------------------------------------------------ | |
package require tdbc | |
::namespace eval ::tdbc::odbc { | |
namespace export connection datasources drivers | |
# Data types that are predefined in ODBC | |
variable sqltypes [dict create \ | |
1 char \ | |
2 numeric \ | |
3 decimal \ | |
4 integer \ | |
5 smallint \ | |
6 float \ | |
7 real \ | |
8 double \ | |
9 datetime \ | |
12 varchar \ | |
91 date \ | |
92 time \ | |
93 timestamp \ | |
-1 longvarchar \ | |
-2 binary \ | |
-3 varbinary \ | |
-4 longvarbinary \ | |
-5 bigint \ | |
-6 tinyint \ | |
-7 bit \ | |
-8 wchar \ | |
-9 wvarchar \ | |
-10 wlongvarchar \ | |
-11 guid] | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::connection -- | |
# | |
# Class representing a connection to a database through ODBC. | |
# | |
#------------------------------------------------------------------------------- | |
::oo::class create ::tdbc::odbc::connection { | |
superclass ::tdbc::connection | |
variable statementSeq typemap | |
# The constructor is written in C. It takes the connection string | |
# as its argument It sets up a namespace to hold the statements | |
# associated with the connection, and then delegates to the 'init' | |
# method (written in C) to do the actual work of attaching to the | |
# database. When that comes back, it sets up a statement to query | |
# the support types, makes a dictionary to enumerate them, and | |
# calls back to set a flag if WVARCHAR is seen (If WVARCHAR is | |
# seen, the database supports Unicode.) | |
# The 'statementCreate' method forwards to the constructor of the | |
# statement class | |
forward statementCreate ::tdbc::odbc::statement create | |
# The 'tables' method returns a dictionary describing the tables | |
# in the database | |
method tables {{pattern %}} { | |
set stmt [::tdbc::odbc::tablesStatement create \ | |
Stmt::[incr statementSeq] [self] $pattern] | |
set status [catch { | |
set retval {} | |
$stmt foreach -as dicts row { | |
if {[dict exists $row TABLE_NAME]} { | |
dict set retval [dict get $row TABLE_NAME] $row | |
} | |
} | |
set retval | |
} result options] | |
catch {rename $stmt {}} | |
return -level 0 -options $options $result | |
} | |
# The 'columns' method returns a dictionary describing the tables | |
# in the database | |
method columns {table {pattern %}} { | |
# Make sure that the type map is initialized | |
my typemap | |
# Query the columns from the database | |
set stmt [::tdbc::odbc::columnsStatement create \ | |
Stmt::[incr statementSeq] [self] $table $pattern] | |
set status [catch { | |
set retval {} | |
$stmt foreach -as dicts origrow { | |
# Map the type, precision, scale and nullable indicators | |
# to tdbc's notation | |
set row {} | |
dict for {key value} $origrow { | |
dict set row [string tolower $key] $value | |
} | |
if {[dict exists $row column_name]} { | |
if {[dict exists $typemap \ | |
[dict get $row data_type]]} { | |
dict set row type \ | |
[dict get $typemap \ | |
[dict get $row data_type]] | |
} else { | |
dict set row type [dict get $row type_name] | |
} | |
if {[dict exists $row column_size]} { | |
dict set row precision \ | |
[dict get $row column_size] | |
} | |
if {[dict exists $row decimal_digits]} { | |
dict set row scale \ | |
[dict get $row decimal_digits] | |
} | |
if {![dict exists $row nullable]} { | |
dict set row nullable \ | |
[expr {!![string trim [dict get $row is_nullable]]}] | |
} | |
dict set retval [dict get $row column_name] $row | |
} | |
} | |
set retval | |
} result options] | |
catch {rename $stmt {}} | |
return -level 0 -options $options $result | |
} | |
# The 'primarykeys' method returns a dictionary describing the primary | |
# keys of a table | |
method primarykeys {tableName} { | |
set stmt [::tdbc::odbc::primarykeysStatement create \ | |
Stmt::[incr statementSeq] [self] $tableName] | |
set status [catch { | |
set retval {} | |
$stmt foreach -as dicts row { | |
foreach {odbcKey tdbcKey} { | |
TABLE_CAT tableCatalog | |
TABLE_SCHEM tableSchema | |
TABLE_NAME tableName | |
COLUMN_NAME columnName | |
KEY_SEQ ordinalPosition | |
PK_NAME constraintName | |
} { | |
if {[dict exists $row $odbcKey]} { | |
dict set row $tdbcKey [dict get $row $odbcKey] | |
dict unset row $odbcKey | |
} | |
} | |
lappend retval $row | |
} | |
set retval | |
} result options] | |
catch {rename $stmt {}} | |
return -level 0 -options $options $result | |
} | |
# The 'foreignkeys' method returns a dictionary describing the foreign | |
# keys of a table | |
method foreignkeys {args} { | |
set stmt [::tdbc::odbc::foreignkeysStatement create \ | |
Stmt::[incr statementSeq] [self] {*}$args] | |
set status [catch { | |
set fkseq 0 | |
set retval {} | |
$stmt foreach -as dicts row { | |
foreach {odbcKey tdbcKey} { | |
PKTABLE_CAT primaryCatalog | |
PKTABLE_SCHEM primarySchema | |
PKTABLE_NAME primaryTable | |
PKCOLUMN_NAME primaryColumn | |
FKTABLE_CAT foreignCatalog | |
FKTABLE_SCHEM foreignSchema | |
FKTABLE_NAME foreignTable | |
FKCOLUMN_NAME foreignColumn | |
UPDATE_RULE updateRule | |
DELETE_RULE deleteRule | |
DEFERRABILITY deferrable | |
KEY_SEQ ordinalPosition | |
FK_NAME foreignConstraintName | |
} { | |
if {[dict exists $row $odbcKey]} { | |
dict set row $tdbcKey [dict get $row $odbcKey] | |
dict unset row $odbcKey | |
} | |
} | |
# Horrible kludge: If the driver doesn't report FK_NAME, | |
# make one up. | |
if {![dict exists $row foreignConstraintName]} { | |
if {![dict exists $row ordinalPosition] | |
|| [dict get $row ordinalPosition] == 1} { | |
set fkname ?[dict get $row foreignTable]?[incr fkseq] | |
} | |
dict set row foreignConstraintName $fkname | |
} | |
lappend retval $row | |
} | |
set retval | |
} result options] | |
catch {rename $stmt {}} | |
return -level 0 -options $options $result | |
} | |
# The 'evaldirect' evaluates driver-native SQL code without preparing it, | |
# and returns a list of dicts (similar to '$connection allrows -as dicts'). | |
method evaldirect {sqlStatement} { | |
set stmt [::tdbc::odbc::evaldirectStatement create \ | |
Stmt::[incr statementSeq] [self] $sqlStatement] | |
set status [catch { | |
$stmt allrows -as dicts | |
} result options] | |
catch {rename $stmt {}} | |
return -level 0 -options $options $result | |
} | |
# The 'prepareCall' method gives a portable interface to prepare | |
# calls to stored procedures. It delegates to 'prepare' to do the | |
# actual work. | |
method preparecall {call} { | |
regexp {^[[:space:]]*(?:([A-Za-z_][A-Za-z_0-9]*)[[:space:]]*=)?(.*)} \ | |
$call -> varName rest | |
if {$varName eq {}} { | |
my prepare \\{CALL $rest\\} | |
} else { | |
my prepare \\{:$varName=CALL $rest\\} | |
} | |
if 0 { | |
# Kevin thinks this is going to be | |
if {![regexp -expanded { | |
^\s* # leading whitespace | |
(?::([[:alpha:]_][[:alnum:]_]*)\s*=\s*) # possible variable name | |
(?:(?:([[:alpha:]_][[:alnum:]_]*)\s*[.]\s*)? # catalog | |
([[:alpha:]_][[:alnum:]_]*)\s*[.]\s*)? # schema | |
([[:alpha:]_][[:alnum:]_]*)\s* # procedure | |
(.*)$ # argument list | |
} $call -> varName catalog schema procedure arglist]} { | |
return -code error \ | |
-errorCode [list TDBC \ | |
SYNTAX_ERROR_OR_ACCESS_RULE_VIOLATION \ | |
42000 ODBC -1] \ | |
"Syntax error in stored procedure call" | |
} else { | |
my PrepareCall $varName $catalog $schema $procedure $arglist | |
} | |
# at least if making all parameters 'inout' doesn't work. | |
} | |
} | |
# The 'typemap' method returns the type map | |
method typemap {} { | |
if {![info exists typemap]} { | |
set typemap $::tdbc::odbc::sqltypes | |
set typesStmt [tdbc::odbc::typesStatement new [self]] | |
$typesStmt foreach row { | |
set typeNum [dict get $row DATA_TYPE] | |
if {![dict exists $typemap $typeNum]} { | |
dict set typemap $typeNum [string tolower \ | |
[dict get $row TYPE_NAME]] | |
} | |
switch -exact -- $typeNum { | |
-9 { | |
[self] HasWvarchar 1 | |
} | |
-5 { | |
[self] HasBigint 1 | |
} | |
} | |
} | |
rename $typesStmt {} | |
} | |
return $typemap | |
} | |
# The 'begintransaction', 'commit' and 'rollback' methods are | |
# implemented in C. | |
} | |
#------------------------------------------------------------------------------- | |
# | |
# tdbc::odbc::statement -- | |
# | |
# The class 'tdbc::odbc::statement' models one statement against a | |
# database accessed through an ODBC connection | |
# | |
#------------------------------------------------------------------------------- | |
::oo::class create ::tdbc::odbc::statement { | |
superclass ::tdbc::statement | |
# The constructor is implemented in C. It accepts the handle to | |
# the connection and the SQL code for the statement to prepare. | |
# It creates a subordinate namespace to hold the statement's | |
# active result sets, and then delegates to the 'init' method, | |
# written in C, to do the actual work of preparing the statement. | |
# The 'resultSetCreate' method forwards to the result set constructor | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
# The 'params' method describes the parameters to the statement | |
method params {} { | |
set typemap [[my connection] typemap] | |
set result {} | |
foreach {name flags typeNum precision scale nullable} [my ParamList] { | |
set lst [dict create \ | |
name $name \ | |
direction [lindex {unknown in out inout} \ | |
[expr {($flags & 0x06) >> 1}]] \ | |
type [dict get $typemap $typeNum] \ | |
precision $precision \ | |
scale $scale] | |
if {$nullable in {0 1}} { | |
dict set list nullable $nullable | |
} | |
dict set result $name $lst | |
} | |
return $result | |
} | |
# Methods implemented in C: | |
# init statement ?dictionary? | |
# Does the heavy lifting for the constructor | |
# connection | |
# Returns the connection handle to which this statement belongs | |
# paramtype paramname ?direction? type ?precision ?scale?? | |
# Declares the type of a parameter in the statement | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::tablesStatement -- | |
# | |
# The class 'tdbc::odbc::tablesStatement' represents the special | |
# statement that queries the tables in a database through an ODBC | |
# connection. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create ::tdbc::odbc::tablesStatement { | |
superclass ::tdbc::statement | |
# The constructor is written in C. It accepts the handle to the | |
# connection and a pattern to match table names. It works in all | |
# ways like the constructor of the 'statement' class except that | |
# its 'init' method sets up to enumerate tables and not run a SQL | |
# query. | |
# The 'resultSetCreate' method forwards to the result set constructor | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::columnsStatement -- | |
# | |
# The class 'tdbc::odbc::tablesStatement' represents the special | |
# statement that queries the columns of a table or view | |
# in a database through an ODBC connection. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create ::tdbc::odbc::columnsStatement { | |
superclass ::tdbc::statement | |
# The constructor is written in C. It accepts the handle to the | |
# connection, a table name, and a pattern to match column | |
# names. It works in all ways like the constructor of the | |
# 'statement' class except that its 'init' method sets up to | |
# enumerate tables and not run a SQL query. | |
# The 'resultSetCreate' class forwards to the constructor of the | |
# result set | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::primarykeysStatement -- | |
# | |
# The class 'tdbc::odbc::primarykeysStatement' represents the special | |
# statement that queries the primary keys on a table through an ODBC | |
# connection. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create ::tdbc::odbc::primarykeysStatement { | |
superclass ::tdbc::statement | |
# The constructor is written in C. It accepts the handle to the | |
# connection and a table name. It works in all | |
# ways like the constructor of the 'statement' class except that | |
# its 'init' method sets up to enumerate primary keys and not run a SQL | |
# query. | |
# The 'resultSetCreate' method forwards to the result set constructor | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::foreignkeysStatement -- | |
# | |
# The class 'tdbc::odbc::foreignkeysStatement' represents the special | |
# statement that queries the foreign keys on a table through an ODBC | |
# connection. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create ::tdbc::odbc::foreignkeysStatement { | |
superclass ::tdbc::statement | |
# The constructor is written in C. It accepts the handle to the | |
# connection and the -primary and -foreign options. It works in all | |
# ways like the constructor of the 'statement' class except that | |
# its 'init' method sets up to enumerate foreign keys and not run a SQL | |
# query. | |
# The 'resultSetCreate' method forwards to the result set constructor | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::evaldirectStatement -- | |
# | |
# The class 'tdbc::odbc::evaldirectStatement' provides a mechanism to | |
# execute driver-name SQL code through an ODBC connection. The SQL code | |
# is not prepared and no tokenization or variable substitution is done. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create ::tdbc::odbc::evaldirectStatement { | |
superclass ::tdbc::statement | |
# The constructor is written in C. It accepts the handle to the | |
# connection and a SQL statement. It works in all | |
# ways like the constructor of the 'statement' class except that | |
# its 'init' method does not tokenize or prepare the SQL statement, and | |
# sets up to run the SQL query without performing variable substitution. | |
# The 'resultSetCreate' method forwards to the result set constructor | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::typesStatement -- | |
# | |
# The class 'tdbc::odbc::typesStatement' represents the special | |
# statement that queries the types available in a database through | |
# an ODBC connection. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create ::tdbc::odbc::typesStatement { | |
superclass ::tdbc::statement | |
# The constructor is written in C. It accepts the handle to the | |
# connection, and (optionally) a data type number. It works in all | |
# ways like the constructor of the 'statement' class except that | |
# its 'init' method sets up to enumerate types and not run a SQL | |
# query. | |
# The 'resultSetCreate' method forwards to the constructor of result sets | |
forward resultSetCreate ::tdbc::odbc::resultset create | |
# The C code contains a variant implementation of the 'init' method. | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::odbc::resultset -- | |
# | |
# The class 'tdbc::odbc::resultset' models the result set that is | |
# produced by executing a statement against an ODBC database. | |
# | |
#------------------------------------------------------------------------------ | |
::oo::class create ::tdbc::odbc::resultset { | |
superclass ::tdbc::resultset | |
# Methods implemented in C include: | |
# constructor statement ?dictionary? | |
# -- Executes the statement against the database, optionally providing | |
# a dictionary of substituted parameters (default is to get params | |
# from variables in the caller's scope). | |
# columns | |
# -- Returns a list of the names of the columns in the result. | |
# nextdict | |
# -- Stores the next row of the result set in the given variable in | |
# the caller's scope as a dictionary whose keys are | |
# column names and whose values are column values. | |
# nextlist | |
# -- Stores the next row of the result set in the given variable in | |
# the caller's scope as a list of cells. | |
# rowcount | |
# -- Returns a count of rows affected by the statement, or -1 | |
# if the count of rows has not been determined. | |
} | |