diff CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/tdbc1.1.5/tdbc.tcl @ 69:33d812a61356

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 17:55:14 -0400
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/tdbc1.1.5/tdbc.tcl	Tue Mar 18 17:55:14 2025 -0400
@@ -0,0 +1,922 @@
+# 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.
+
+}
\ No newline at end of file