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