Spaces:
Running
Running
# tdbc.tcl -- | |
# | |
# Definitions of base classes from which TDBC drivers' connections, | |
# statements and result sets may inherit. | |
# | |
# 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$ | |
# | |
#------------------------------------------------------------------------------ | |
package require TclOO | |
namespace eval ::tdbc { | |
namespace export connection statement resultset | |
variable generalError [list TDBC GENERAL_ERROR HY000 {}] | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::ParseConvenienceArgs -- | |
# | |
# Parse the convenience arguments to a TDBC 'execute', | |
# 'executewithdictionary', or 'foreach' call. | |
# | |
# Parameters: | |
# argv - Arguments to the call | |
# optsVar -- Name of a variable in caller's scope that will receive | |
# a dictionary of the supplied options | |
# | |
# Results: | |
# Returns any args remaining after parsing the options. | |
# | |
# Side effects: | |
# Sets the 'opts' dictionary to the options. | |
# | |
#------------------------------------------------------------------------------ | |
proc tdbc::ParseConvenienceArgs {argv optsVar} { | |
variable generalError | |
upvar 1 $optsVar opts | |
set opts [dict create -as dicts] | |
set i 0 | |
# Munch keyword options off the front of the command arguments | |
foreach {key value} $argv { | |
if {[string index $key 0] eq {-}} { | |
switch -regexp -- $key { | |
-as? { | |
if {$value ne {dicts} && $value ne {lists}} { | |
set errorcode $generalError | |
lappend errorcode badVarType $value | |
return -code error \ | |
-errorcode $errorcode \ | |
"bad variable type \"$value\":\ | |
must be lists or dicts" | |
} | |
dict set opts -as $value | |
} | |
-c(?:o(?:l(?:u(?:m(?:n(?:s(?:v(?:a(?:r(?:i(?:a(?:b(?:le?)?)?)?)?)?)?)?)?)?)?)?)?) { | |
dict set opts -columnsvariable $value | |
} | |
-- { | |
incr i | |
break | |
} | |
default { | |
set errorcode $generalError | |
lappend errorcode badOption $key | |
return -code error \ | |
-errorcode $errorcode \ | |
"bad option \"$key\":\ | |
must be -as or -columnsvariable" | |
} | |
} | |
} else { | |
break | |
} | |
incr i 2 | |
} | |
return [lrange $argv[set argv {}] $i end] | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# tdbc::connection -- | |
# | |
# Class that represents a generic connection to a database. | |
# | |
#----------------------------------------------------------------------------- | |
oo::class create ::tdbc::connection { | |
# statementSeq is the sequence number of the last statement created. | |
# statementClass is the name of the class that implements the | |
# 'statement' API. | |
# primaryKeysStatement is the statement that queries primary keys | |
# foreignKeysStatement is the statement that queries foreign keys | |
variable statementSeq primaryKeysStatement foreignKeysStatement | |
# The base class constructor accepts no arguments. It sets up the | |
# machinery to do the bookkeeping to keep track of what statements | |
# are associated with the connection. The derived class constructor | |
# is expected to set the variable, 'statementClass' to the name | |
# of the class that represents statements, so that the 'prepare' | |
# method can invoke it. | |
constructor {} { | |
set statementSeq 0 | |
namespace eval Stmt {} | |
} | |
# The 'close' method is simply an alternative syntax for destroying | |
# the connection. | |
method close {} { | |
my destroy | |
} | |
# The 'prepare' method creates a new statement against the connection, | |
# giving its constructor the current statement and the SQL code to | |
# prepare. It uses the 'statementClass' variable set by the constructor | |
# to get the class to instantiate. | |
method prepare {sqlcode} { | |
return [my statementCreate Stmt::[incr statementSeq] [self] $sqlcode] | |
} | |
# The 'statementCreate' method delegates to the constructor | |
# of the class specified by the 'statementClass' variable. It's | |
# intended for drivers designed before tdbc 1.0b10. Current ones | |
# should forward this method to the constructor directly. | |
method statementCreate {name instance sqlcode} { | |
my variable statementClass | |
return [$statementClass create $name $instance $sqlcode] | |
} | |
# Derived classes are expected to implement the 'prepareCall' method, | |
# and have it call 'prepare' as needed (or do something else and | |
# install the resulting statement) | |
# The 'statements' method lists the statements active against this | |
# connection. | |
method statements {} { | |
info commands Stmt::* | |
} | |
# The 'resultsets' method lists the result sets active against this | |
# connection. | |
method resultsets {} { | |
set retval {} | |
foreach statement [my statements] { | |
foreach resultset [$statement resultsets] { | |
lappend retval $resultset | |
} | |
} | |
return $retval | |
} | |
# The 'transaction' method executes a block of Tcl code as an | |
# ACID transaction against the database. | |
method transaction {script} { | |
my begintransaction | |
set status [catch {uplevel 1 $script} result options] | |
if {$status in {0 2 3 4}} { | |
set status2 [catch {my commit} result2 options2] | |
if {$status2 == 1} { | |
set status 1 | |
set result $result2 | |
set options $options2 | |
} | |
} | |
switch -exact -- $status { | |
0 { | |
# do nothing | |
} | |
2 - 3 - 4 { | |
set options [dict merge {-level 1} $options[set options {}]] | |
dict incr options -level | |
} | |
default { | |
my rollback | |
} | |
} | |
return -options $options $result | |
} | |
# The 'allrows' method prepares a statement, then executes it with | |
# a given set of substituents, returning a list of all the rows | |
# that the statement returns. Optionally, it stores the names of | |
# the columns in '-columnsvariable'. | |
# Usage: | |
# $db allrows ?-as lists|dicts? ?-columnsvariable varName? ?--? | |
# sql ?dictionary? | |
method allrows args { | |
variable ::tdbc::generalError | |
# Grab keyword-value parameters | |
set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts] | |
# Check postitional parameters | |
set cmd [list [self] prepare] | |
if {[llength $args] == 1} { | |
set sqlcode [lindex $args 0] | |
} elseif {[llength $args] == 2} { | |
lassign $args sqlcode dict | |
} else { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? sqlcode ?dictionary?" | |
} | |
lappend cmd $sqlcode | |
# Prepare the statement | |
set stmt [uplevel 1 $cmd] | |
# Delegate to the statement to accumulate the results | |
set cmd [list $stmt allrows {*}$opts --] | |
if {[info exists dict]} { | |
lappend cmd $dict | |
} | |
set status [catch { | |
uplevel 1 $cmd | |
} result options] | |
# Destroy the statement | |
catch { | |
$stmt close | |
} | |
return -options $options $result | |
} | |
# The 'foreach' method prepares a statement, then executes it with | |
# a supplied set of substituents. For each row of the result, | |
# it sets a variable to the row and invokes a script in the caller's | |
# scope. | |
# | |
# Usage: | |
# $db foreach ?-as lists|dicts? ?-columnsVariable varName? ?--? | |
# varName sql ?dictionary? script | |
method foreach args { | |
variable ::tdbc::generalError | |
# Grab keyword-value parameters | |
set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts] | |
# Check postitional parameters | |
set cmd [list [self] prepare] | |
if {[llength $args] == 3} { | |
lassign $args varname sqlcode script | |
} elseif {[llength $args] == 4} { | |
lassign $args varname sqlcode dict script | |
} else { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? varname sqlcode ?dictionary? script" | |
} | |
lappend cmd $sqlcode | |
# Prepare the statement | |
set stmt [uplevel 1 $cmd] | |
# Delegate to the statement to iterate over the results | |
set cmd [list $stmt foreach {*}$opts -- $varname] | |
if {[info exists dict]} { | |
lappend cmd $dict | |
} | |
lappend cmd $script | |
set status [catch { | |
uplevel 1 $cmd | |
} result options] | |
# Destroy the statement | |
catch { | |
$stmt close | |
} | |
# Adjust return level in the case that the script [return]s | |
if {$status == 2} { | |
set options [dict merge {-level 1} $options[set options {}]] | |
dict incr options -level | |
} | |
return -options $options $result | |
} | |
# The 'BuildPrimaryKeysStatement' method builds a SQL statement to | |
# retrieve the primary keys from a database. (It executes once the | |
# first time the 'primaryKeys' method is executed, and retains the | |
# prepared statement for reuse.) | |
method BuildPrimaryKeysStatement {} { | |
# On some databases, CONSTRAINT_CATALOG is always NULL and | |
# JOINing to it fails. Check for this case and include that | |
# JOIN only if catalog names are supplied. | |
set catalogClause {} | |
if {[lindex [set count [my allrows -as lists { | |
SELECT COUNT(*) | |
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS | |
WHERE CONSTRAINT_CATALOG IS NOT NULL}]] 0 0] != 0} { | |
set catalogClause \ | |
{AND xtable.CONSTRAINT_CATALOG = xcolumn.CONSTRAINT_CATALOG} | |
} | |
set primaryKeysStatement [my prepare " | |
SELECT xtable.TABLE_SCHEMA AS \"tableSchema\", | |
xtable.TABLE_NAME AS \"tableName\", | |
xtable.CONSTRAINT_CATALOG AS \"constraintCatalog\", | |
xtable.CONSTRAINT_SCHEMA AS \"constraintSchema\", | |
xtable.CONSTRAINT_NAME AS \"constraintName\", | |
xcolumn.COLUMN_NAME AS \"columnName\", | |
xcolumn.ORDINAL_POSITION AS \"ordinalPosition\" | |
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS xtable | |
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE xcolumn | |
ON xtable.CONSTRAINT_SCHEMA = xcolumn.CONSTRAINT_SCHEMA | |
AND xtable.TABLE_NAME = xcolumn.TABLE_NAME | |
AND xtable.CONSTRAINT_NAME = xcolumn.CONSTRAINT_NAME | |
$catalogClause | |
WHERE xtable.TABLE_NAME = :tableName | |
AND xtable.CONSTRAINT_TYPE = 'PRIMARY KEY' | |
"] | |
} | |
# The default implementation of the 'primarykeys' method uses the | |
# SQL INFORMATION_SCHEMA to retrieve primary key information. Databases | |
# that might not have INFORMATION_SCHEMA must overload this method. | |
method primarykeys {tableName} { | |
if {![info exists primaryKeysStatement]} { | |
my BuildPrimaryKeysStatement | |
} | |
tailcall $primaryKeysStatement allrows [list tableName $tableName] | |
} | |
# The 'BuildForeignKeysStatements' method builds a SQL statement to | |
# retrieve the foreign keys from a database. (It executes once the | |
# first time the 'foreignKeys' method is executed, and retains the | |
# prepared statements for reuse.) | |
method BuildForeignKeysStatement {} { | |
# On some databases, CONSTRAINT_CATALOG is always NULL and | |
# JOINing to it fails. Check for this case and include that | |
# JOIN only if catalog names are supplied. | |
set catalogClause1 {} | |
set catalogClause2 {} | |
if {[lindex [set count [my allrows -as lists { | |
SELECT COUNT(*) | |
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS | |
WHERE CONSTRAINT_CATALOG IS NOT NULL}]] 0 0] != 0} { | |
set catalogClause1 \ | |
{AND fkc.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG} | |
set catalogClause2 \ | |
{AND pkc.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG} | |
} | |
foreach {exists1 clause1} { | |
0 {} | |
1 { AND pkc.TABLE_NAME = :primary} | |
} { | |
foreach {exists2 clause2} { | |
0 {} | |
1 { AND fkc.TABLE_NAME = :foreign} | |
} { | |
set stmt [my prepare " | |
SELECT rc.CONSTRAINT_CATALOG AS \"foreignConstraintCatalog\", | |
rc.CONSTRAINT_SCHEMA AS \"foreignConstraintSchema\", | |
rc.CONSTRAINT_NAME AS \"foreignConstraintName\", | |
rc.UNIQUE_CONSTRAINT_CATALOG | |
AS \"primaryConstraintCatalog\", | |
rc.UNIQUE_CONSTRAINT_SCHEMA AS \"primaryConstraintSchema\", | |
rc.UNIQUE_CONSTRAINT_NAME AS \"primaryConstraintName\", | |
rc.UPDATE_RULE AS \"updateAction\", | |
rc.DELETE_RULE AS \"deleteAction\", | |
pkc.TABLE_CATALOG AS \"primaryCatalog\", | |
pkc.TABLE_SCHEMA AS \"primarySchema\", | |
pkc.TABLE_NAME AS \"primaryTable\", | |
pkc.COLUMN_NAME AS \"primaryColumn\", | |
fkc.TABLE_CATALOG AS \"foreignCatalog\", | |
fkc.TABLE_SCHEMA AS \"foreignSchema\", | |
fkc.TABLE_NAME AS \"foreignTable\", | |
fkc.COLUMN_NAME AS \"foreignColumn\", | |
pkc.ORDINAL_POSITION AS \"ordinalPosition\" | |
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc | |
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fkc | |
ON fkc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME | |
AND fkc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA | |
$catalogClause1 | |
INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pkc | |
ON pkc.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME | |
AND pkc.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA | |
$catalogClause2 | |
AND pkc.ORDINAL_POSITION = fkc.ORDINAL_POSITION | |
WHERE 1=1 | |
$clause1 | |
$clause2 | |
ORDER BY \"foreignConstraintCatalog\", \"foreignConstraintSchema\", \"foreignConstraintName\", \"ordinalPosition\" | |
"] | |
dict set foreignKeysStatement $exists1 $exists2 $stmt | |
} | |
} | |
} | |
# The default implementation of the 'foreignkeys' method uses the | |
# SQL INFORMATION_SCHEMA to retrieve primary key information. Databases | |
# that might not have INFORMATION_SCHEMA must overload this method. | |
method foreignkeys {args} { | |
variable ::tdbc::generalError | |
# Check arguments | |
set argdict {} | |
if {[llength $args] % 2 != 0} { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?..." | |
} | |
foreach {key value} $args { | |
if {$key ni {-primary -foreign}} { | |
set errorcode $generalError | |
lappend errorcode badOption | |
return -code error -errorcode $errorcode \ | |
"bad option \"$key\", must be -primary or -foreign" | |
} | |
set key [string range $key 1 end] | |
if {[dict exists $argdict $key]} { | |
set errorcode $generalError | |
lappend errorcode dupOption | |
return -code error -errorcode $errorcode \ | |
"duplicate option \"$key\" supplied" | |
} | |
dict set argdict $key $value | |
} | |
# Build the statements that query foreign keys. There are four | |
# of them, one for each combination of whether -primary | |
# and -foreign is specified. | |
if {![info exists foreignKeysStatement]} { | |
my BuildForeignKeysStatement | |
} | |
set stmt [dict get $foreignKeysStatement \ | |
[dict exists $argdict primary] \ | |
[dict exists $argdict foreign]] | |
tailcall $stmt allrows $argdict | |
} | |
# Derived classes are expected to implement the 'begintransaction', | |
# 'commit', and 'rollback' methods. | |
# Derived classes are expected to implement 'tables' and 'columns' method. | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# Class: tdbc::statement | |
# | |
# Class that represents a SQL statement in a generic database | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create tdbc::statement { | |
# resultSetSeq is the sequence number of the last result set created. | |
# resultSetClass is the name of the class that implements the 'resultset' | |
# API. | |
variable resultSetClass resultSetSeq | |
# The base class constructor accepts no arguments. It initializes | |
# the machinery for tracking the ownership of result sets. The derived | |
# constructor is expected to invoke the base constructor, and to | |
# set a variable 'resultSetClass' to the fully-qualified name of the | |
# class that represents result sets. | |
constructor {} { | |
set resultSetSeq 0 | |
namespace eval ResultSet {} | |
} | |
# The 'execute' method on a statement runs the statement with | |
# a particular set of substituted variables. It actually works | |
# by creating the result set object and letting that objects | |
# constructor do the work of running the statement. The creation | |
# is wrapped in an [uplevel] call because the substitution proces | |
# may need to access variables in the caller's scope. | |
# WORKAROUND: Take out the '0 &&' from the next line when | |
# Bug 2649975 is fixed | |
if {0 && [package vsatisfies [package provide Tcl] 8.6]} { | |
method execute args { | |
tailcall my resultSetCreate \ | |
[namespace current]::ResultSet::[incr resultSetSeq] \ | |
[self] {*}$args | |
} | |
} else { | |
method execute args { | |
return \ | |
[uplevel 1 \ | |
[list \ | |
[self] resultSetCreate \ | |
[namespace current]::ResultSet::[incr resultSetSeq] \ | |
[self] {*}$args]] | |
} | |
} | |
# The 'ResultSetCreate' method is expected to be a forward to the | |
# appropriate result set constructor. If it's missing, the driver must | |
# have been designed for tdbc 1.0b9 and earlier, and the 'resultSetClass' | |
# variable holds the class name. | |
method resultSetCreate {name instance args} { | |
return [uplevel 1 [list $resultSetClass create \ | |
$name $instance {*}$args]] | |
} | |
# The 'resultsets' method returns a list of result sets produced by | |
# the current statement | |
method resultsets {} { | |
info commands ResultSet::* | |
} | |
# The 'allrows' method executes a statement with a given set of | |
# substituents, and returns a list of all the rows that the statement | |
# returns. Optionally, it stores the names of columns in | |
# '-columnsvariable'. | |
# | |
# Usage: | |
# $statement allrows ?-as lists|dicts? ?-columnsvariable varName? ?--? | |
# ?dictionary? | |
method allrows args { | |
variable ::tdbc::generalError | |
# Grab keyword-value parameters | |
set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts] | |
# Check postitional parameters | |
set cmd [list [self] execute] | |
if {[llength $args] == 0} { | |
# do nothing | |
} elseif {[llength $args] == 1} { | |
lappend cmd [lindex $args 0] | |
} else { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? ?dictionary?" | |
} | |
# Get the result set | |
set resultSet [uplevel 1 $cmd] | |
# Delegate to the result set's [allrows] method to accumulate | |
# the rows of the result. | |
set cmd [list $resultSet allrows {*}$opts] | |
set status [catch { | |
uplevel 1 $cmd | |
} result options] | |
# Destroy the result set | |
catch { | |
rename $resultSet {} | |
} | |
# Adjust return level in the case that the script [return]s | |
if {$status == 2} { | |
set options [dict merge {-level 1} $options[set options {}]] | |
dict incr options -level | |
} | |
return -options $options $result | |
} | |
# The 'foreach' method executes a statement with a given set of | |
# substituents. It runs the supplied script, substituting the supplied | |
# named variable. Optionally, it stores the names of columns in | |
# '-columnsvariable'. | |
# | |
# Usage: | |
# $statement foreach ?-as lists|dicts? ?-columnsvariable varName? ?--? | |
# variableName ?dictionary? script | |
method foreach args { | |
variable ::tdbc::generalError | |
# Grab keyword-value parameters | |
set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts] | |
# Check positional parameters | |
set cmd [list [self] execute] | |
if {[llength $args] == 2} { | |
lassign $args varname script | |
} elseif {[llength $args] == 3} { | |
lassign $args varname dict script | |
lappend cmd $dict | |
} else { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? varName ?dictionary? script" | |
} | |
# Get the result set | |
set resultSet [uplevel 1 $cmd] | |
# Delegate to the result set's [foreach] method to evaluate | |
# the script for each row of the result. | |
set cmd [list $resultSet foreach {*}$opts -- $varname $script] | |
set status [catch { | |
uplevel 1 $cmd | |
} result options] | |
# Destroy the result set | |
catch { | |
rename $resultSet {} | |
} | |
# Adjust return level in the case that the script [return]s | |
if {$status == 2} { | |
set options [dict merge {-level 1} $options[set options {}]] | |
dict incr options -level | |
} | |
return -options $options $result | |
} | |
# The 'close' method is syntactic sugar for invoking the destructor | |
method close {} { | |
my destroy | |
} | |
# Derived classes are expected to implement their own constructors, | |
# plus the following methods: | |
# paramtype paramName ?direction? type ?scale ?precision?? | |
# Declares the type of a parameter in the statement | |
} | |
#------------------------------------------------------------------------------ | |
# | |
# Class: tdbc::resultset | |
# | |
# Class that represents a result set in a generic database. | |
# | |
#------------------------------------------------------------------------------ | |
oo::class create tdbc::resultset { | |
constructor {} { } | |
# The 'allrows' method returns a list of all rows that a given | |
# result set returns. | |
method allrows args { | |
variable ::tdbc::generalError | |
# Parse args | |
set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts] | |
if {[llength $args] != 0} { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? varName script" | |
} | |
# Do -columnsvariable if requested | |
if {[dict exists $opts -columnsvariable]} { | |
upvar 1 [dict get $opts -columnsvariable] columns | |
} | |
# Assemble the results | |
if {[dict get $opts -as] eq {lists}} { | |
set delegate nextlist | |
} else { | |
set delegate nextdict | |
} | |
set results [list] | |
while {1} { | |
set columns [my columns] | |
while {[my $delegate row]} { | |
lappend results $row | |
} | |
if {![my nextresults]} break | |
} | |
return $results | |
} | |
# The 'foreach' method runs a script on each row from a result set. | |
method foreach args { | |
variable ::tdbc::generalError | |
# Grab keyword-value parameters | |
set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts] | |
# Check positional parameters | |
if {[llength $args] != 2} { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? varName script" | |
} | |
# Do -columnsvariable if requested | |
if {[dict exists $opts -columnsvariable]} { | |
upvar 1 [dict get $opts -columnsvariable] columns | |
} | |
# Iterate over the groups of results | |
while {1} { | |
# Export column names to caller | |
set columns [my columns] | |
# Iterate over the rows of one group of results | |
upvar 1 [lindex $args 0] row | |
if {[dict get $opts -as] eq {lists}} { | |
set delegate nextlist | |
} else { | |
set delegate nextdict | |
} | |
while {[my $delegate row]} { | |
set status [catch { | |
uplevel 1 [lindex $args 1] | |
} result options] | |
switch -exact -- $status { | |
0 - 4 { # OK or CONTINUE | |
} | |
2 { # RETURN | |
set options \ | |
[dict merge {-level 1} $options[set options {}]] | |
dict incr options -level | |
return -options $options $result | |
} | |
3 { # BREAK | |
set broken 1 | |
break | |
} | |
default { # ERROR or unknown status | |
return -options $options $result | |
} | |
} | |
} | |
# Advance to the next group of results if there is one | |
if {[info exists broken] || ![my nextresults]} { | |
break | |
} | |
} | |
return | |
} | |
# The 'nextrow' method retrieves a row in the form of either | |
# a list or a dictionary. | |
method nextrow {args} { | |
variable ::tdbc::generalError | |
set opts [dict create -as dicts] | |
set i 0 | |
# Munch keyword options off the front of the command arguments | |
foreach {key value} $args { | |
if {[string index $key 0] eq {-}} { | |
switch -regexp -- $key { | |
-as? { | |
dict set opts -as $value | |
} | |
-- { | |
incr i | |
break | |
} | |
default { | |
set errorcode $generalError | |
lappend errorcode badOption $key | |
return -code error -errorcode $errorcode \ | |
"bad option \"$key\":\ | |
must be -as or -columnsvariable" | |
} | |
} | |
} else { | |
break | |
} | |
incr i 2 | |
} | |
set args [lrange $args $i end] | |
if {[llength $args] != 1} { | |
set errorcode $generalError | |
lappend errorcode wrongNumArgs | |
return -code error -errorcode $errorcode \ | |
"wrong # args: should be [lrange [info level 0] 0 1]\ | |
?-option value?... ?--? varName" | |
} | |
upvar 1 [lindex $args 0] row | |
if {[dict get $opts -as] eq {lists}} { | |
set delegate nextlist | |
} else { | |
set delegate nextdict | |
} | |
return [my $delegate row] | |
} | |
# Derived classes must override 'nextresults' if a single | |
# statement execution can yield multiple sets of results | |
method nextresults {} { | |
return 0 | |
} | |
# Derived classes must override 'outputparams' if statements can | |
# have output parameters. | |
method outputparams {} { | |
return {} | |
} | |
# The 'close' method is syntactic sugar for destroying the result set. | |
method close {} { | |
my destroy | |
} | |
# Derived classes are expected to implement the following methods: | |
# constructor and destructor. | |
# Constructor accepts a statement and an optional | |
# a dictionary of substituted parameters and | |
# executes the statement against the database. If | |
# the dictionary is not supplied, then the 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 variableName | |
# -- Stores the next row of the result set in the given variable | |
# in caller's scope, in the form of a dictionary that maps | |
# column names to values. | |
# nextlist variableName | |
# -- Stores the next row of the result set in the given variable | |
# in caller's scope, in the form of 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. | |
} |