516 lines
15 KiB
Tcl
516 lines
15 KiB
Tcl
|
# 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 '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::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.
|
|||
|
|
|||
|
}
|