comparison CSP2/CSP2_env/env-d9b9114564458d9d-741b3de822f2aaca6c6caa4325c4afce/lib/tdbc1.1.5/tdbc.tcl @ 68:5028fdace37b

planemo upload commit 2e9511a184a1ca667c7be0c6321a36dc4e3d116d
author jpayne
date Tue, 18 Mar 2025 16:23:26 -0400
parents
children
comparison
equal deleted inserted replaced
67:0e9998148a16 68:5028fdace37b
1 # tdbc.tcl --
2 #
3 # Definitions of base classes from which TDBC drivers' connections,
4 # statements and result sets may inherit.
5 #
6 # Copyright (c) 2008 by Kevin B. Kenny
7 # See the file "license.terms" for information on usage and redistribution
8 # of this file, and for a DISCLAIMER OF ALL WARRANTIES.
9 #
10 # RCS: @(#) $Id$
11 #
12 #------------------------------------------------------------------------------
13
14 package require TclOO
15
16 namespace eval ::tdbc {
17 namespace export connection statement resultset
18 variable generalError [list TDBC GENERAL_ERROR HY000 {}]
19 }
20
21 #------------------------------------------------------------------------------
22 #
23 # tdbc::ParseConvenienceArgs --
24 #
25 # Parse the convenience arguments to a TDBC 'execute',
26 # 'executewithdictionary', or 'foreach' call.
27 #
28 # Parameters:
29 # argv - Arguments to the call
30 # optsVar -- Name of a variable in caller's scope that will receive
31 # a dictionary of the supplied options
32 #
33 # Results:
34 # Returns any args remaining after parsing the options.
35 #
36 # Side effects:
37 # Sets the 'opts' dictionary to the options.
38 #
39 #------------------------------------------------------------------------------
40
41 proc tdbc::ParseConvenienceArgs {argv optsVar} {
42
43 variable generalError
44 upvar 1 $optsVar opts
45
46 set opts [dict create -as dicts]
47 set i 0
48
49 # Munch keyword options off the front of the command arguments
50
51 foreach {key value} $argv {
52 if {[string index $key 0] eq {-}} {
53 switch -regexp -- $key {
54 -as? {
55 if {$value ne {dicts} && $value ne {lists}} {
56 set errorcode $generalError
57 lappend errorcode badVarType $value
58 return -code error \
59 -errorcode $errorcode \
60 "bad variable type \"$value\":\
61 must be lists or dicts"
62 }
63 dict set opts -as $value
64 }
65 -c(?:o(?:l(?:u(?:m(?:n(?:s(?:v(?:a(?:r(?:i(?:a(?:b(?:le?)?)?)?)?)?)?)?)?)?)?)?)?) {
66 dict set opts -columnsvariable $value
67 }
68 -- {
69 incr i
70 break
71 }
72 default {
73 set errorcode $generalError
74 lappend errorcode badOption $key
75 return -code error \
76 -errorcode $errorcode \
77 "bad option \"$key\":\
78 must be -as or -columnsvariable"
79 }
80 }
81 } else {
82 break
83 }
84 incr i 2
85 }
86
87 return [lrange $argv[set argv {}] $i end]
88
89 }
90
91
92
93 #------------------------------------------------------------------------------
94 #
95 # tdbc::connection --
96 #
97 # Class that represents a generic connection to a database.
98 #
99 #-----------------------------------------------------------------------------
100
101 oo::class create ::tdbc::connection {
102
103 # statementSeq is the sequence number of the last statement created.
104 # statementClass is the name of the class that implements the
105 # 'statement' API.
106 # primaryKeysStatement is the statement that queries primary keys
107 # foreignKeysStatement is the statement that queries foreign keys
108
109 variable statementSeq primaryKeysStatement foreignKeysStatement
110
111 # The base class constructor accepts no arguments. It sets up the
112 # machinery to do the bookkeeping to keep track of what statements
113 # are associated with the connection. The derived class constructor
114 # is expected to set the variable, 'statementClass' to the name
115 # of the class that represents statements, so that the 'prepare'
116 # method can invoke it.
117
118 constructor {} {
119 set statementSeq 0
120 namespace eval Stmt {}
121 }
122
123 # The 'close' method is simply an alternative syntax for destroying
124 # the connection.
125
126 method close {} {
127 my destroy
128 }
129
130 # The 'prepare' method creates a new statement against the connection,
131 # giving its constructor the current statement and the SQL code to
132 # prepare. It uses the 'statementClass' variable set by the constructor
133 # to get the class to instantiate.
134
135 method prepare {sqlcode} {
136 return [my statementCreate Stmt::[incr statementSeq] [self] $sqlcode]
137 }
138
139 # The 'statementCreate' method delegates to the constructor
140 # of the class specified by the 'statementClass' variable. It's
141 # intended for drivers designed before tdbc 1.0b10. Current ones
142 # should forward this method to the constructor directly.
143
144 method statementCreate {name instance sqlcode} {
145 my variable statementClass
146 return [$statementClass create $name $instance $sqlcode]
147 }
148
149 # Derived classes are expected to implement the 'prepareCall' method,
150 # and have it call 'prepare' as needed (or do something else and
151 # install the resulting statement)
152
153 # The 'statements' method lists the statements active against this
154 # connection.
155
156 method statements {} {
157 info commands Stmt::*
158 }
159
160 # The 'resultsets' method lists the result sets active against this
161 # connection.
162
163 method resultsets {} {
164 set retval {}
165 foreach statement [my statements] {
166 foreach resultset [$statement resultsets] {
167 lappend retval $resultset
168 }
169 }
170 return $retval
171 }
172
173 # The 'transaction' method executes a block of Tcl code as an
174 # ACID transaction against the database.
175
176 method transaction {script} {
177 my begintransaction
178 set status [catch {uplevel 1 $script} result options]
179 if {$status in {0 2 3 4}} {
180 set status2 [catch {my commit} result2 options2]
181 if {$status2 == 1} {
182 set status 1
183 set result $result2
184 set options $options2
185 }
186 }
187 switch -exact -- $status {
188 0 {
189 # do nothing
190 }
191 2 - 3 - 4 {
192 set options [dict merge {-level 1} $options[set options {}]]
193 dict incr options -level
194 }
195 default {
196 my rollback
197 }
198 }
199 return -options $options $result
200 }
201
202 # The 'allrows' method prepares a statement, then executes it with
203 # a given set of substituents, returning a list of all the rows
204 # that the statement returns. Optionally, it stores the names of
205 # the columns in '-columnsvariable'.
206 # Usage:
207 # $db allrows ?-as lists|dicts? ?-columnsvariable varName? ?--?
208 # sql ?dictionary?
209
210 method allrows args {
211
212 variable ::tdbc::generalError
213
214 # Grab keyword-value parameters
215
216 set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
217
218 # Check postitional parameters
219
220 set cmd [list [self] prepare]
221 if {[llength $args] == 1} {
222 set sqlcode [lindex $args 0]
223 } elseif {[llength $args] == 2} {
224 lassign $args sqlcode dict
225 } else {
226 set errorcode $generalError
227 lappend errorcode wrongNumArgs
228 return -code error -errorcode $errorcode \
229 "wrong # args: should be [lrange [info level 0] 0 1]\
230 ?-option value?... ?--? sqlcode ?dictionary?"
231 }
232 lappend cmd $sqlcode
233
234 # Prepare the statement
235
236 set stmt [uplevel 1 $cmd]
237
238 # Delegate to the statement to accumulate the results
239
240 set cmd [list $stmt allrows {*}$opts --]
241 if {[info exists dict]} {
242 lappend cmd $dict
243 }
244 set status [catch {
245 uplevel 1 $cmd
246 } result options]
247
248 # Destroy the statement
249
250 catch {
251 $stmt close
252 }
253
254 return -options $options $result
255 }
256
257 # The 'foreach' method prepares a statement, then executes it with
258 # a supplied set of substituents. For each row of the result,
259 # it sets a variable to the row and invokes a script in the caller's
260 # scope.
261 #
262 # Usage:
263 # $db foreach ?-as lists|dicts? ?-columnsVariable varName? ?--?
264 # varName sql ?dictionary? script
265
266 method foreach args {
267
268 variable ::tdbc::generalError
269
270 # Grab keyword-value parameters
271
272 set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
273
274 # Check postitional parameters
275
276 set cmd [list [self] prepare]
277 if {[llength $args] == 3} {
278 lassign $args varname sqlcode script
279 } elseif {[llength $args] == 4} {
280 lassign $args varname sqlcode dict script
281 } else {
282 set errorcode $generalError
283 lappend errorcode wrongNumArgs
284 return -code error -errorcode $errorcode \
285 "wrong # args: should be [lrange [info level 0] 0 1]\
286 ?-option value?... ?--? varname sqlcode ?dictionary? script"
287 }
288 lappend cmd $sqlcode
289
290 # Prepare the statement
291
292 set stmt [uplevel 1 $cmd]
293
294 # Delegate to the statement to iterate over the results
295
296 set cmd [list $stmt foreach {*}$opts -- $varname]
297 if {[info exists dict]} {
298 lappend cmd $dict
299 }
300 lappend cmd $script
301 set status [catch {
302 uplevel 1 $cmd
303 } result options]
304
305 # Destroy the statement
306
307 catch {
308 $stmt close
309 }
310
311 # Adjust return level in the case that the script [return]s
312
313 if {$status == 2} {
314 set options [dict merge {-level 1} $options[set options {}]]
315 dict incr options -level
316 }
317 return -options $options $result
318 }
319
320 # The 'BuildPrimaryKeysStatement' method builds a SQL statement to
321 # retrieve the primary keys from a database. (It executes once the
322 # first time the 'primaryKeys' method is executed, and retains the
323 # prepared statement for reuse.)
324
325 method BuildPrimaryKeysStatement {} {
326
327 # On some databases, CONSTRAINT_CATALOG is always NULL and
328 # JOINing to it fails. Check for this case and include that
329 # JOIN only if catalog names are supplied.
330
331 set catalogClause {}
332 if {[lindex [set count [my allrows -as lists {
333 SELECT COUNT(*)
334 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
335 WHERE CONSTRAINT_CATALOG IS NOT NULL}]] 0 0] != 0} {
336 set catalogClause \
337 {AND xtable.CONSTRAINT_CATALOG = xcolumn.CONSTRAINT_CATALOG}
338 }
339 set primaryKeysStatement [my prepare "
340 SELECT xtable.TABLE_SCHEMA AS \"tableSchema\",
341 xtable.TABLE_NAME AS \"tableName\",
342 xtable.CONSTRAINT_CATALOG AS \"constraintCatalog\",
343 xtable.CONSTRAINT_SCHEMA AS \"constraintSchema\",
344 xtable.CONSTRAINT_NAME AS \"constraintName\",
345 xcolumn.COLUMN_NAME AS \"columnName\",
346 xcolumn.ORDINAL_POSITION AS \"ordinalPosition\"
347 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS xtable
348 INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE xcolumn
349 ON xtable.CONSTRAINT_SCHEMA = xcolumn.CONSTRAINT_SCHEMA
350 AND xtable.TABLE_NAME = xcolumn.TABLE_NAME
351 AND xtable.CONSTRAINT_NAME = xcolumn.CONSTRAINT_NAME
352 $catalogClause
353 WHERE xtable.TABLE_NAME = :tableName
354 AND xtable.CONSTRAINT_TYPE = 'PRIMARY KEY'
355 "]
356 }
357
358 # The default implementation of the 'primarykeys' method uses the
359 # SQL INFORMATION_SCHEMA to retrieve primary key information. Databases
360 # that might not have INFORMATION_SCHEMA must overload this method.
361
362 method primarykeys {tableName} {
363 if {![info exists primaryKeysStatement]} {
364 my BuildPrimaryKeysStatement
365 }
366 tailcall $primaryKeysStatement allrows [list tableName $tableName]
367 }
368
369 # The 'BuildForeignKeysStatements' method builds a SQL statement to
370 # retrieve the foreign keys from a database. (It executes once the
371 # first time the 'foreignKeys' method is executed, and retains the
372 # prepared statements for reuse.)
373
374 method BuildForeignKeysStatement {} {
375
376 # On some databases, CONSTRAINT_CATALOG is always NULL and
377 # JOINing to it fails. Check for this case and include that
378 # JOIN only if catalog names are supplied.
379
380 set catalogClause1 {}
381 set catalogClause2 {}
382 if {[lindex [set count [my allrows -as lists {
383 SELECT COUNT(*)
384 FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
385 WHERE CONSTRAINT_CATALOG IS NOT NULL}]] 0 0] != 0} {
386 set catalogClause1 \
387 {AND fkc.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG}
388 set catalogClause2 \
389 {AND pkc.CONSTRAINT_CATALOG = rc.CONSTRAINT_CATALOG}
390 }
391
392 foreach {exists1 clause1} {
393 0 {}
394 1 { AND pkc.TABLE_NAME = :primary}
395 } {
396 foreach {exists2 clause2} {
397 0 {}
398 1 { AND fkc.TABLE_NAME = :foreign}
399 } {
400 set stmt [my prepare "
401 SELECT rc.CONSTRAINT_CATALOG AS \"foreignConstraintCatalog\",
402 rc.CONSTRAINT_SCHEMA AS \"foreignConstraintSchema\",
403 rc.CONSTRAINT_NAME AS \"foreignConstraintName\",
404 rc.UNIQUE_CONSTRAINT_CATALOG
405 AS \"primaryConstraintCatalog\",
406 rc.UNIQUE_CONSTRAINT_SCHEMA AS \"primaryConstraintSchema\",
407 rc.UNIQUE_CONSTRAINT_NAME AS \"primaryConstraintName\",
408 rc.UPDATE_RULE AS \"updateAction\",
409 rc.DELETE_RULE AS \"deleteAction\",
410 pkc.TABLE_CATALOG AS \"primaryCatalog\",
411 pkc.TABLE_SCHEMA AS \"primarySchema\",
412 pkc.TABLE_NAME AS \"primaryTable\",
413 pkc.COLUMN_NAME AS \"primaryColumn\",
414 fkc.TABLE_CATALOG AS \"foreignCatalog\",
415 fkc.TABLE_SCHEMA AS \"foreignSchema\",
416 fkc.TABLE_NAME AS \"foreignTable\",
417 fkc.COLUMN_NAME AS \"foreignColumn\",
418 pkc.ORDINAL_POSITION AS \"ordinalPosition\"
419 FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
420 INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE fkc
421 ON fkc.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
422 AND fkc.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
423 $catalogClause1
424 INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE pkc
425 ON pkc.CONSTRAINT_NAME = rc.UNIQUE_CONSTRAINT_NAME
426 AND pkc.CONSTRAINT_SCHEMA = rc.UNIQUE_CONSTRAINT_SCHEMA
427 $catalogClause2
428 AND pkc.ORDINAL_POSITION = fkc.ORDINAL_POSITION
429 WHERE 1=1
430 $clause1
431 $clause2
432 ORDER BY \"foreignConstraintCatalog\", \"foreignConstraintSchema\", \"foreignConstraintName\", \"ordinalPosition\"
433 "]
434 dict set foreignKeysStatement $exists1 $exists2 $stmt
435 }
436 }
437 }
438
439 # The default implementation of the 'foreignkeys' method uses the
440 # SQL INFORMATION_SCHEMA to retrieve primary key information. Databases
441 # that might not have INFORMATION_SCHEMA must overload this method.
442
443 method foreignkeys {args} {
444
445 variable ::tdbc::generalError
446
447 # Check arguments
448
449 set argdict {}
450 if {[llength $args] % 2 != 0} {
451 set errorcode $generalError
452 lappend errorcode wrongNumArgs
453 return -code error -errorcode $errorcode \
454 "wrong # args: should be [lrange [info level 0] 0 1]\
455 ?-option value?..."
456 }
457 foreach {key value} $args {
458 if {$key ni {-primary -foreign}} {
459 set errorcode $generalError
460 lappend errorcode badOption
461 return -code error -errorcode $errorcode \
462 "bad option \"$key\", must be -primary or -foreign"
463 }
464 set key [string range $key 1 end]
465 if {[dict exists $argdict $key]} {
466 set errorcode $generalError
467 lappend errorcode dupOption
468 return -code error -errorcode $errorcode \
469 "duplicate option \"$key\" supplied"
470 }
471 dict set argdict $key $value
472 }
473
474 # Build the statements that query foreign keys. There are four
475 # of them, one for each combination of whether -primary
476 # and -foreign is specified.
477
478 if {![info exists foreignKeysStatement]} {
479 my BuildForeignKeysStatement
480 }
481 set stmt [dict get $foreignKeysStatement \
482 [dict exists $argdict primary] \
483 [dict exists $argdict foreign]]
484 tailcall $stmt allrows $argdict
485 }
486
487 # Derived classes are expected to implement the 'begintransaction',
488 # 'commit', and 'rollback' methods.
489
490 # Derived classes are expected to implement 'tables' and 'columns' method.
491
492 }
493
494 #------------------------------------------------------------------------------
495 #
496 # Class: tdbc::statement
497 #
498 # Class that represents a SQL statement in a generic database
499 #
500 #------------------------------------------------------------------------------
501
502 oo::class create tdbc::statement {
503
504 # resultSetSeq is the sequence number of the last result set created.
505 # resultSetClass is the name of the class that implements the 'resultset'
506 # API.
507
508 variable resultSetClass resultSetSeq
509
510 # The base class constructor accepts no arguments. It initializes
511 # the machinery for tracking the ownership of result sets. The derived
512 # constructor is expected to invoke the base constructor, and to
513 # set a variable 'resultSetClass' to the fully-qualified name of the
514 # class that represents result sets.
515
516 constructor {} {
517 set resultSetSeq 0
518 namespace eval ResultSet {}
519 }
520
521 # The 'execute' method on a statement runs the statement with
522 # a particular set of substituted variables. It actually works
523 # by creating the result set object and letting that objects
524 # constructor do the work of running the statement. The creation
525 # is wrapped in an [uplevel] call because the substitution proces
526 # may need to access variables in the caller's scope.
527
528 # WORKAROUND: Take out the '0 &&' from the next line when
529 # Bug 2649975 is fixed
530 if {0 && [package vsatisfies [package provide Tcl] 8.6]} {
531 method execute args {
532 tailcall my resultSetCreate \
533 [namespace current]::ResultSet::[incr resultSetSeq] \
534 [self] {*}$args
535 }
536 } else {
537 method execute args {
538 return \
539 [uplevel 1 \
540 [list \
541 [self] resultSetCreate \
542 [namespace current]::ResultSet::[incr resultSetSeq] \
543 [self] {*}$args]]
544 }
545 }
546
547 # The 'ResultSetCreate' method is expected to be a forward to the
548 # appropriate result set constructor. If it's missing, the driver must
549 # have been designed for tdbc 1.0b9 and earlier, and the 'resultSetClass'
550 # variable holds the class name.
551
552 method resultSetCreate {name instance args} {
553 return [uplevel 1 [list $resultSetClass create \
554 $name $instance {*}$args]]
555 }
556
557 # The 'resultsets' method returns a list of result sets produced by
558 # the current statement
559
560 method resultsets {} {
561 info commands ResultSet::*
562 }
563
564 # The 'allrows' method executes a statement with a given set of
565 # substituents, and returns a list of all the rows that the statement
566 # returns. Optionally, it stores the names of columns in
567 # '-columnsvariable'.
568 #
569 # Usage:
570 # $statement allrows ?-as lists|dicts? ?-columnsvariable varName? ?--?
571 # ?dictionary?
572
573
574 method allrows args {
575
576 variable ::tdbc::generalError
577
578 # Grab keyword-value parameters
579
580 set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
581
582 # Check postitional parameters
583
584 set cmd [list [self] execute]
585 if {[llength $args] == 0} {
586 # do nothing
587 } elseif {[llength $args] == 1} {
588 lappend cmd [lindex $args 0]
589 } else {
590 set errorcode $generalError
591 lappend errorcode wrongNumArgs
592 return -code error -errorcode $errorcode \
593 "wrong # args: should be [lrange [info level 0] 0 1]\
594 ?-option value?... ?--? ?dictionary?"
595 }
596
597 # Get the result set
598
599 set resultSet [uplevel 1 $cmd]
600
601 # Delegate to the result set's [allrows] method to accumulate
602 # the rows of the result.
603
604 set cmd [list $resultSet allrows {*}$opts]
605 set status [catch {
606 uplevel 1 $cmd
607 } result options]
608
609 # Destroy the result set
610
611 catch {
612 rename $resultSet {}
613 }
614
615 # Adjust return level in the case that the script [return]s
616
617 if {$status == 2} {
618 set options [dict merge {-level 1} $options[set options {}]]
619 dict incr options -level
620 }
621 return -options $options $result
622 }
623
624 # The 'foreach' method executes a statement with a given set of
625 # substituents. It runs the supplied script, substituting the supplied
626 # named variable. Optionally, it stores the names of columns in
627 # '-columnsvariable'.
628 #
629 # Usage:
630 # $statement foreach ?-as lists|dicts? ?-columnsvariable varName? ?--?
631 # variableName ?dictionary? script
632
633 method foreach args {
634
635 variable ::tdbc::generalError
636
637 # Grab keyword-value parameters
638
639 set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
640
641 # Check positional parameters
642
643 set cmd [list [self] execute]
644 if {[llength $args] == 2} {
645 lassign $args varname script
646 } elseif {[llength $args] == 3} {
647 lassign $args varname dict script
648 lappend cmd $dict
649 } else {
650 set errorcode $generalError
651 lappend errorcode wrongNumArgs
652 return -code error -errorcode $errorcode \
653 "wrong # args: should be [lrange [info level 0] 0 1]\
654 ?-option value?... ?--? varName ?dictionary? script"
655 }
656
657 # Get the result set
658
659 set resultSet [uplevel 1 $cmd]
660
661 # Delegate to the result set's [foreach] method to evaluate
662 # the script for each row of the result.
663
664 set cmd [list $resultSet foreach {*}$opts -- $varname $script]
665 set status [catch {
666 uplevel 1 $cmd
667 } result options]
668
669 # Destroy the result set
670
671 catch {
672 rename $resultSet {}
673 }
674
675 # Adjust return level in the case that the script [return]s
676
677 if {$status == 2} {
678 set options [dict merge {-level 1} $options[set options {}]]
679 dict incr options -level
680 }
681 return -options $options $result
682 }
683
684 # The 'close' method is syntactic sugar for invoking the destructor
685
686 method close {} {
687 my destroy
688 }
689
690 # Derived classes are expected to implement their own constructors,
691 # plus the following methods:
692
693 # paramtype paramName ?direction? type ?scale ?precision??
694 # Declares the type of a parameter in the statement
695
696 }
697
698 #------------------------------------------------------------------------------
699 #
700 # Class: tdbc::resultset
701 #
702 # Class that represents a result set in a generic database.
703 #
704 #------------------------------------------------------------------------------
705
706 oo::class create tdbc::resultset {
707
708 constructor {} { }
709
710 # The 'allrows' method returns a list of all rows that a given
711 # result set returns.
712
713 method allrows args {
714
715 variable ::tdbc::generalError
716
717 # Parse args
718
719 set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
720 if {[llength $args] != 0} {
721 set errorcode $generalError
722 lappend errorcode wrongNumArgs
723 return -code error -errorcode $errorcode \
724 "wrong # args: should be [lrange [info level 0] 0 1]\
725 ?-option value?... ?--? varName script"
726 }
727
728 # Do -columnsvariable if requested
729
730 if {[dict exists $opts -columnsvariable]} {
731 upvar 1 [dict get $opts -columnsvariable] columns
732 }
733
734 # Assemble the results
735
736 if {[dict get $opts -as] eq {lists}} {
737 set delegate nextlist
738 } else {
739 set delegate nextdict
740 }
741 set results [list]
742 while {1} {
743 set columns [my columns]
744 while {[my $delegate row]} {
745 lappend results $row
746 }
747 if {![my nextresults]} break
748 }
749 return $results
750
751 }
752
753 # The 'foreach' method runs a script on each row from a result set.
754
755 method foreach args {
756
757 variable ::tdbc::generalError
758
759 # Grab keyword-value parameters
760
761 set args [::tdbc::ParseConvenienceArgs $args[set args {}] opts]
762
763 # Check positional parameters
764
765 if {[llength $args] != 2} {
766 set errorcode $generalError
767 lappend errorcode wrongNumArgs
768 return -code error -errorcode $errorcode \
769 "wrong # args: should be [lrange [info level 0] 0 1]\
770 ?-option value?... ?--? varName script"
771 }
772
773 # Do -columnsvariable if requested
774
775 if {[dict exists $opts -columnsvariable]} {
776 upvar 1 [dict get $opts -columnsvariable] columns
777 }
778
779 # Iterate over the groups of results
780 while {1} {
781
782 # Export column names to caller
783
784 set columns [my columns]
785
786 # Iterate over the rows of one group of results
787
788 upvar 1 [lindex $args 0] row
789 if {[dict get $opts -as] eq {lists}} {
790 set delegate nextlist
791 } else {
792 set delegate nextdict
793 }
794 while {[my $delegate row]} {
795 set status [catch {
796 uplevel 1 [lindex $args 1]
797 } result options]
798 switch -exact -- $status {
799 0 - 4 { # OK or CONTINUE
800 }
801 2 { # RETURN
802 set options \
803 [dict merge {-level 1} $options[set options {}]]
804 dict incr options -level
805 return -options $options $result
806 }
807 3 { # BREAK
808 set broken 1
809 break
810 }
811 default { # ERROR or unknown status
812 return -options $options $result
813 }
814 }
815 }
816
817 # Advance to the next group of results if there is one
818
819 if {[info exists broken] || ![my nextresults]} {
820 break
821 }
822 }
823
824 return
825 }
826
827
828 # The 'nextrow' method retrieves a row in the form of either
829 # a list or a dictionary.
830
831 method nextrow {args} {
832
833 variable ::tdbc::generalError
834
835 set opts [dict create -as dicts]
836 set i 0
837
838 # Munch keyword options off the front of the command arguments
839
840 foreach {key value} $args {
841 if {[string index $key 0] eq {-}} {
842 switch -regexp -- $key {
843 -as? {
844 dict set opts -as $value
845 }
846 -- {
847 incr i
848 break
849 }
850 default {
851 set errorcode $generalError
852 lappend errorcode badOption $key
853 return -code error -errorcode $errorcode \
854 "bad option \"$key\":\
855 must be -as or -columnsvariable"
856 }
857 }
858 } else {
859 break
860 }
861 incr i 2
862 }
863
864 set args [lrange $args $i end]
865 if {[llength $args] != 1} {
866 set errorcode $generalError
867 lappend errorcode wrongNumArgs
868 return -code error -errorcode $errorcode \
869 "wrong # args: should be [lrange [info level 0] 0 1]\
870 ?-option value?... ?--? varName"
871 }
872 upvar 1 [lindex $args 0] row
873 if {[dict get $opts -as] eq {lists}} {
874 set delegate nextlist
875 } else {
876 set delegate nextdict
877 }
878 return [my $delegate row]
879 }
880
881 # Derived classes must override 'nextresults' if a single
882 # statement execution can yield multiple sets of results
883
884 method nextresults {} {
885 return 0
886 }
887
888 # Derived classes must override 'outputparams' if statements can
889 # have output parameters.
890
891 method outputparams {} {
892 return {}
893 }
894
895 # The 'close' method is syntactic sugar for destroying the result set.
896
897 method close {} {
898 my destroy
899 }
900
901 # Derived classes are expected to implement the following methods:
902
903 # constructor and destructor.
904 # Constructor accepts a statement and an optional
905 # a dictionary of substituted parameters and
906 # executes the statement against the database. If
907 # the dictionary is not supplied, then the default
908 # is to get params from variables in the caller's scope).
909 # columns
910 # -- Returns a list of the names of the columns in the result.
911 # nextdict variableName
912 # -- Stores the next row of the result set in the given variable
913 # in caller's scope, in the form of a dictionary that maps
914 # column names to values.
915 # nextlist variableName
916 # -- Stores the next row of the result set in the given variable
917 # in caller's scope, in the form of a list of cells.
918 # rowcount
919 # -- Returns a count of rows affected by the statement, or -1
920 # if the count of rows has not been determined.
921
922 }