nstcl 1, 2 is a Tcl package which reimplements many of the useful APIs and commands from AOLserver3 and OpenACS4, making them available for use in Tcl/Tk applications and throughout scripts.
AOLserver is a multi-threaded webserver that embeds Tcl and is famous for it's high performance and pooled connections to various flavors of relational database management systems. AOLserver began life in 1994 as NaviServer, a commercial product from NaviSoft, but was renamed after NaviSoft was acquired by America Online. AOL initially distributed free binary versions of AOLserver and in 1999 released version 3.0 under an OSS license.5
OpenACS (the Open Architecture Community System) is "an advanced toolkit for building scalable, community-oriented web applications."6 Written largely in Tcl, and running on AOLserver against either Postgres or Oracle, it is often compared as the Tcl equivalent to Python's Zope.7
The OpenACS is an outgrowth of the ArsDigita Community System. The project began originally to make the ACS work with Postgres, so that the entire stack (AOLserver, Tcl, and database server) could be built using Open Source tools. After ArsDigita went from being a profitable company to taking upon millions in VC funding, and the new management decided to discontinue their Tcl product in favor of a (then) yet-to-be-developed Java version, the OpenACS community took over active development of both the Oracle and Postgres versions. Since then, OpenACS has made numerous advances while ArsDigita imploded in late 2001 and it's remaining assets sold to Red Hat in February of 2002. 8, 9, 10
nstcl is actually a collection of Tcl packages:
In this paper we will look at the core, database and nssets packages, as they generally have the widest appeal. The other packages are more specialized (for instance, the html and http packages simplify the programming of web clients and robots11), but aren't likely to be as widely used, and so we will not cover them here.
An ns_set is a data structure which shares properties of both arrays (key/value pairs) and lists (numerically indexed). In some ways it is similar to the new "Dictionary Values and Manipulators" proposed by TIP #111;12 however unlike dictionaries, an ns_set may contain multiple keys with the same name but with (possibly) differing values, and the order keys and values are stored is defined. Methods exist13 to create, copy, split, move, put, conditionally put, update, find, delete, and get (by key or by index position) the value of an ns_set.
Results from a database query fit nicely into ns_set's. Each row of data can be encapsulated in an ns_set, and there is no risk of losing data if multiple columns happen to have the same name.
The Tcl core does not provide any commands for accessing databases, leaving this task to extensions. An extension exists (in some cases multiple extensions) to just about every flavor of RDBMS.14
Source | ||
---|---|---|
Informix | Informix/Tcl | http://isqltclsourceforge.net |
MySQL | mysqltcl | http://www.xdobry.de/mysqltcl |
fbmysql | http://www.fastbase.co.nz/fbsql/index.html | |
ODBC | tclodbc | http://sourceforge.net/projects/tclodbc |
Oracle | Oratcl | http://oratcl.sourceforge.net |
Postgres | pgtcl | https://www.postgresql.org/docs/7.4/static/pgtcl.html |
pgin.tcl | http://pgintcl.sourceforge.net/ | |
Solid | Soltcl | http://bolts.guam.net/soltcl |
SQLite | tclsqlite | http://www.hwaci.com/sw/sqlite |
Sybase | Sybtcl | http://sybtcl.sourceforge.net |
Each Tcl database extension tends to expose it's own unique API at the script level (examples are included in an appendix at the end of this paper). This introduces a "Tower of Babel" effect, forcing the script author up-front to either commit to a particular database or write an abstraction layer.
Perl has a standard database interface package, Perl::DBI, which exposes a common API to the Perl programmer, regardless of the database platform, leaving the actual implemenation differences to a low-level driver.15
Python has defined a database API to "encourage similarity between the Python modules that are used to access databases."16 Python modules, which implement this API, exist for a variety of databases.17
PHP does not have a common database API.18 Like Tcl, various extensions exist, each providing it's own API.19 There are a number of database wrapper libraries for PHP. Some of the more prominent appear to be PEAR20 and Metabase21.
AOLserver has had, since it's initial release in 1994, a database abstraction layer that is exposed at the Tcl level via the ns_db command. Each C coded database driver hooks into the ns_db API22. In nstcl, in contrast, an existing Tcl database extension is "wrapped" into the ns_db API by defining a mere six procedures (see the Implementation Details below). nstcl currently provides wrappers around mysqltcl, tclodbc, Oratcl, pgtcl (and pgin.tcl), Soltcl, tclsqlite, and Sybtcl.
In the AOLserver configuration file, the appropriate database driver modules are loaded and one or more "database pools" are defined. A given pool connects to one specific database host with a specified username and password, and a maximum number of concurrent connections is defined.
Multiple pools can exist (even to the same database host) with any number of connections per pool. If more than one pool is defined, one of them may be designated as the "default pool."
ns_section ns/db/pool/main ns_param Driver ora8 ns_param Connections 8 ns_param DataSource "" ns_param User michael ns_param Password ******** ns_param Verbose Off ns_sections ns/db/pool/log ns_param Driver solid ns_param Connections 10 ns_param DataSource {tcp localhost 1313} ns_param User michael ns_param Password ******** ns_param Verbose Off ns_section ns/server/$servername/db ns_param pools main,log ns_param defaultpool main
In nstcl we define two commands to load and configure (so as not to burden all nstcl users with a separate configuration file a la AOLserver).
::nstcl::load_driver oracle ::nstcl::configure_pool -default oracle main 8 "" michael ******** ::nstcl::load_driver solid ::nstcl::configure_pool solid log 10 "tcp localhost 1313" michael ********
Communication with the database is done by way of an opaque handle to the database, much like the opaque handle returned by Tcl's open or socket commands, and like each of the Tcl database extensions we review in the appendix.
To obtain a handle we use ns_db gethandle. When we are done we call ns_db releasehandle.
set db [ns_db gethandle] # # do whatever with the handle ... # ns_db releasehandle $db
DML (Data Manipulation Language) statements alter the contents of the database. Common examples include SQL commands such as update, insert, and delete. DDL (Data Definition Language) statements alter the structure of the database. Common examples include SQL commands such as create, alter, and drop. We use ns_db dml to execute DML and DDL statements.
# Give all employees a 4% cost of living raise ns_db dml $db "update employees set salary = salary * 1.04" # Get rid of an index ns_db dml $db "drop index employees"
The principal command for running a query is ns_db select. It returns an ns_set that we'll use to fetch the data from each row using ns_db getrow.
set data [ns_db select $db "select name, salary from employees"] while {[ns_db getrow $db $data]} { set name [ns_set get $data name] set salary [ns_set get $data salary] puts "$name's new salary is \$[format %0.2f $salary]" }
There are two convenience methods, ns_db 0or1row and ns_db 1row that save us from having to call ns_db getrow when we know that our query will always return at most one row.
set data [ns_db 0or1row $db " select name from employees where employee_id = '24601'"] if {$data == ""} { puts "We don't employ #24601" } else { puts "#24601 is [ns_set value $data 0]" }
set data [ns_db 1row $db " select count(*) from employees where salary < 18000"] set qty [ns_set value $data 0]
In addition to the foregoing methods, there are a number of others which are not as frequently used. They include: bindrow, bouncepool, cancel, close, connected, datasource, dbtype, driver, exception, exec, flush, open, poolname, pools, password, setexception, user, and verbose. More information on these methods can be found in either the AOLserver or nstcl documentation. 23, 24
It turns out that of the twenty-five ns_db subcommands, there are only a half-dozen that need to be customized to support a new Tcl database extension. These functions are:
Method | Tcl Procedure |
---|---|
bindrow | ::nstcl::database::${driver}::bindrow |
close | ::nstcl::database::${driver}::close |
exec | ::nstcl::database::${driver}::exec |
flush | ::nstcl::database::${driver}::flush |
gethandle | ::nstcl::database::${driver}::gethandle |
getrow | ::nstcl::database::${driver}::getrow |
All of the other ns_db methods can be built on top of these six primitives. This makes sense when you consider that the basic operations needed to talk to any RDBMS are:
The ::nstcl::load_driver command does a package require nstcl-database-$driver. The ::nstcl::configure_pool creates a pool, using a loaded driver, for n_connections to a particular datasource. The format of the datasource will depend upon the underlying Tcl database extension that is being wrapped.
We take advantage of the opaque nature of the ns_db API's database handle to implement an OOP-like system to track the state of each handle.
Each handle is actually the fully qualified name of an ns_set that resides in the ::nstcl::database namespace. This ns_set stores various settings for the handle. Using Tcl's interp alias command, the fully qualified variable name becomes an alias to a dispatch procedure which receives the driver, poolname and fully qualified handle as arguments.
This dispatch procedure accepts either zero, one, or two additional arguments. With no arguments it returns its own setId. With one argument it returns the specified key from its own ns_set. With two arguments it updates the value of the specified key in its setId.
Any abstraction layer, by definition, will cost more in performance than using a lower-level system. Most usage patterns do not involve pushing hundreds of thousands of rows back and forth across a network. Such work is better done within the database itself (either using SQL or a vendor supplied stored procedure language, such as Oracle's PL/SQL). For normal usage patterns we have found that nstcl does not pose an undue burden. The Solid, Postgres, and Oracle drivers have been the most used. Other drivers may well benefit from additional tuning. In general the time savings to the programmer outweigh the cost of a few more CPU cycles.
With the next release of AOLserver, currently in beta, the AOLserver developers have refactored portions of the code base into a loadable module, libnsd.so, which can be loaded as a Tcl package. The database drivers and ns_db API are not (as of this writing) included within libnsd.so. However, the next release of nstcl will, once AOLserver 4 is finalized, be able to take advantage of libnsd.so for a C implementation of ns_set's, which should enhance performance. While writing this paper, and reviewing the nstcl source, we have noted several possible speed enhancements which will be incorporated into the next release.
As nice as ns_db is (not having to change APIs when you switch to a different database), dealing with handle management and manually iterating over query results can become tedious.
Luckily for the Tcl programmer who craves laziness, OpenACS provides a higher-level database API on top of ns_db that hides all the mundane house-keeping chores. Additionaly, bind-variable emulation is available which frees the programmer from worrying about always escaping apostrophes in SQL statements. Bind variables are prefaced with a colon.
set name "Jim O'Connor" set salary 40000 db_dml new_hire { insert into employees (name, salary, dept) values (:name, :salary, 'Accounting') }
In the previous example you'll notice the first argument to db_dml is new_hire. This is known as a "statement name" and serves several purposes. First and foremost, it helps document the intent of a query. OpenACS has a "query dispatcher" that can use the statement name to lookup an appropriate database-specific query in an XML file and execute it instead.
There are a number of different approaches to retrieving the results of a query. Some Tcl database extensions require you to fetch a row at a time. Others return all the results in one fell swoop. Some may choose to provide both options. The OpenACS Database API allows either style, regardless of the underlying Tcl database extension.
db_foreach can be used to iterate and execute a block of Tcl code for every row returned from the database. A variable is pre-set for each of the database columns.
db_foreach recent_hires { select name, salary, hire_date from employees where hire_date >= '2003-01-01' order by name } { puts "$name makes \$[format %0.2f $salary]" }
db_list will return a Tcl list made up of the first column of each row. db_list_of_lists will return a list of lists, where the inner list contains each of the database columns in the order they were specified in the query.
set ids [db_list employee_ids "select employee_id from employees"]
array set employees [join [db_list_of_lists " select employee_id, name from employees"]]
Like ns_db, commands exist to retrieve queries which return at most one row.
if {[db_0or1row specific_employee { select name, hire_date from employees where employee_id = 24601}]} { puts "We hired #24601, $name, on $hire_date" }
db_1row burn_rate " select sum(salary)/24 as cash from employees" puts "We pay out \$[format %0.2f $cash] every payroll"
A series of database queries can be evaluated within a transaction. This means that either all changes will occur, or none will. We'll look at implementing, within a transaction, a hypothetical algorithm PHB's25 might use to cut payroll expenses.
# Implement an algorithm to reduce our payroll expenses db_transaction { db_dml pay_cut_for_execs { update employees set salary = salary * 0.985 where salary > 175000 } set qty [db_string number_of_peons { select count(*) from employees }] set laid_off 0 db_foreach peons { select employee_id, salary from employees where salary <= 65000 } { # roll the dice if {int(rand()*6) == 0} { incr laid_off db_dml layoff_somebody { delete from employees where employee_id = :employee_id } } } # Did we lay off too many people? if {$laid_off >= $qty/2} then db_abort_transaction }
The nstcl implementation of the OpenACS Database API adds support for specifying which database pool to use. This makes it really easy to write quick Tcl scripts that glue one database to another. In nstcl, the statement name can be prefixed with the name of a pool to specify which database to use (otherwise the default pool is assumed).
# Assume layoffs weren't enough and we need to save money by # cutting software licensing costs. We've defined an "oracle" # pool and a "postgres" pool previously. db_transaction { db_foreach oracle:old_system { select name, employee_id, salary, hire_date from employees } { set first_names [lrange $name 0 end-1] set last_name [lindex $name end] db_dml postgres:new_system { insert into wage_slaves (first_names, last_name, salary, ssn) values (:first_names, :last_name, :salary, :employee_id) } } }
ad_proc is an enhanced version of Tcl's standard proc command. Procedures defined with ad_proc can support several different styles of switches.
Switch parameters must be listed first, before any positional parameters. When invoking a procedure which has optional switches (and there is a possibility that the first positional parameter may begin with a hyphen) use "--" to indicate that there are no more switches to parse.
To illustrate, here is the definition of the nstcl sysdate command from the nstcl-time package:
ad_proc sysdate {-seconds:boolean {-format "%Y-%m-%d"} {-offset "0 days ago"} -base:optional} { if {![info exists base]} { set time [clock seconds] } else { if {![string is integer -strict $base]} { set time [clock scan $base] } else { set time $base } } set time [clock scan $offset -base $time] if {$seconds_p} { return $time } else { return [clock format $time -format $format] } }
Tcl has a plethora of database extensions like most other programming languages. Unlike Perl and Python, which tend to ship large batteries included distributions, the Tcl core is purposefully lean. Using an abstraction layer, such as nstcl, puts Tcl on the same footing, in terms of code re-use and a consistent API, as these other languages.
To illustrate the differences between extensions, consider the following code snippets that make a database connection, create a table, populate it with a few rows of data, and then retrieve a result.
These are the generic SQL statements that we'll use in our example:
# # SQL statements common to all examples # set SQL(create) " CREATE TABLE zipcodes ( zipcode CHAR(5) UNIQUE NOT NULL PRIMARY KEY, state CHAR(2) NOT NULL, city VARCHAR(30) NOT NULL )" set SQL(insert,1) "INSERT INTO zipcodes VALUES ('95054', 'CA', 'Santa Clara')" set SQL(insert,2) "INSERT INTO zipcodes VALUES ('90210', 'CA', 'Hollywod')" set SQL(insert,3) "INSERT INTO zipcodes VALUES ('84041', 'UT', 'Layton')" set SQL(insert,4) "INSERT INTO zipcodes VALUES ('48103', 'MI', 'Ann Arbor')" set SQL(insert,5) "INSERT INTO zipcodes VALUES ('94024', 'CA', 'Los Altos')" set SQL(select) " SELECT state, count(*) AS qty FROM zipcodes GROUP BY state ORDER BY state"
package require Oratcl 3 set conn [oralogon "user/pass@host"] oraautocom $conn 1 set cursor [oraopen $conn] orasql $cursor $SQL(create) for {set i 1} {$i <= 5} {incr i} { orasql $cursor $SQL(insert,$i) } orasql $cursor $SQL(select) set data [orafetch $cursor] while {$::oramsg(rc) != 1403} { puts "[lindex $data 0] has [lindex $data 1] zipcode(s) on file" set data [orafetch $cursor] } oraclose $cursor oralogoff $conn
package require Sybtcl 3 set conn [sybconnect sa "" SYBASE ""] sybsql $conn $SQL(create) for {set i 1} {$i <= 5} {incr i} { sybsql $conn $SQL(insert,$i) } if {[string equal "REG_ROW" [sybsql $conn $SQL(select)]]} { set data [sybnext $conn] while {[llength $data]} { puts "[lindex $data 0] has [lindex $data 1] zipcode(s) on file" set data [sybnext $conn] } } sybclose $conn
load soltcl.so Solid sol AllocConnect conn sol Connect $conn "tcp localhost 1313" username password sol setConnectOption $conn autocommit on sol AllocStmt $conn cursor sol Prepare $cursor $SQL(create) sol Execute $cursor sol FreeStmt $cursor drop for {set i 1} {$i <= 5} {incr i} { sol AllocStmt $conn cursor sol Prepare $cursor $SQL(insert,$i) sol Execute $cursor sol FreeStmt $cursor drop } sol AllocStmt $conn cursor sol Prepare $cursor $SQL(select) sol Execute $cursor sol NumResultCols $cursor num_cols while {[sol Fetch $cursor] == "SQL_SUCCESS"} { set values [list] for {set i 1} {$i <= $num_cols} {incr i} { sol getData $cursor $i value lappend values $value } puts "[lindex $values 0] has [lindex $values 1] zipcode(s) on file" } sol FreeStmt $cursor drop sol Disconnect $conn sol FreeConnect $conn
load libpgtcl.so set conn [pg_connect -conninfo [list dbname=dbname host=localhost user=user password=pass]] set cursor [pg_exec $conn $SQL(create)] pg_result $cursor -clear for {set i 1} {$i <= 5} {incr i} { set cursor [pg_exec $conn $SQL(insert,$i)] pg_result $cursor -clear } set cursor [pg_exec $conn $SQL(select)] set n_rows [pg_result $cursor -numTuples] for {set i 0} {$i < $n_rows} {incr i} { pg_result $cursor -tupleArray $i data puts "$data(state) has $data(qty) zipcode(s) on file" } pg_result $cursor -clear pg_disconnect $conn
package require sqlite sqlite db /path/to/database db eval $SQL(create) for {set i 1} {$i <= 5} {incr i} { db eval $SQL(insert,$i) } db eval $SQL(select) data { puts "$data(state) has $data(qty) zipcode(s) on file" }
set db [ns_db gethandle] ns_db dml $db $SQL(create) ns_db dml $db $SQL(insert,1) ns_db dml $db $SQL(insert,2) ns_db dml $db $SQL(insert,3) ns_db dml $db $SQL(insert,4) ns_db dml $db $SQL(insert,5) set selection [ns_db select $db $SQL(select)] while {[ns_db getrow $db $selection]} { puts "[ns_set $selection value 0] has [ns_set $selection value 1] zipcode(s) on file" } ns_set free $selection ns_db releasehandle $db
db_dml create_table_example $SQL(create) db_dml insert_example_1 $SQL(insert,1) db_dml insert_example_2 $SQL(insert,2) db_dml insert_example_3 $SQL(insert,3) db_dml insert_example_4 $SQL(insert,4) db_dml insert_example_5 $SQL(insert,5) db_foreach select_example $SQL(select) { puts "$state has $qty zipcode(s) on file" }
This is the nstcl database "driver" (or wrapper) for Postgres. It was the first driver written for nstcl 0.1.
package require nstcl-core package require nstcl-fwdcompat package require nstcl-database # nstcl-1.1/nstcl-database-postgres.tcl # $Id: nstcl-database-postgres.tcl,v 1.5 2002/11/21 20:04:59 cleverly Exp $ # # nstcl -- AOLserver/OpenNSD routines for tclsh # # Copyright (c) 2000, 2001, 2002 Michael A. Cleverly # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Author Contact Information: # # Michael A. Cleverly # 1448 W Pebblecreek Dr # Layton, Utah 84041-8112 # # [email protected] # http://michael.cleverly.com # # nstcl home page: http://nstcl.sourceforge.net namespace eval ::nstcl::database::postgres {} # # ... load_driver # ::nstcl::ad_proc ::nstcl::database::postgres::load_driver {arguments} { if {[info commands pg_connect] != ""} then return foreach {library name} $arguments break if {![info exists library] || [string equal "" $library]} { set library libpgtcl.so } if {![info exists name] || [string equal "" $name]} { load [::nstcl::find_shared_library $library] } else { load [::nstcl::find_shared_library $library $name] } } # # ... bindrow # ::nstcl::ad_proc -private ::nstcl::database::postgres::bindrow {dbhandle} { set cursor [$dbhandle cursor] set setId [::nstcl::ns_set create $cursor] foreach attribute [pg_result $cursor -attributes] { ::nstcl::ns_set put $setId $attribute "" } return $setId } # # ... close # ::nstcl::ad_proc -private ::nstcl::database::postgres::close {dbhandle} { pg_disconnect [$dbhandle conn] $dbhandle conn {} } # # ... dbtype # ::nstcl::ad_proc -private ::nstcl::database::postgres::dbtype {} { return "Postgres" } # # ... exec # ::nstcl::ad_proc -private ::nstcl::database::postgres::exec {dbhandle sql} { set cursor [$dbhandle cursor] set conn [$dbhandle conn] # free previously allocated memory if {![string equal "" $cursor]} { catch { pg_result $cursor -clear } $dbhandle cursor {} } set cursor [pg_exec $conn $sql] set error [pg_result $cursor -error] if {![string equal "" $error]} { pg_result $cursor -clear $dbhandle exception_code NSDB $dbhandle exception_text $error $dbhandle num_rows {} $dbhandle curr_row {} $dbhandle mode ERROR return -code error $error } else { $dbhandle exception_code {} $dbhandle exception_text {} } if {[pg_result $cursor -numAttrs]} { $dbhandle num_rows [pg_result $cursor -numTuples] $dbhandle curr_row 0 $dbhandle cursor $cursor $dbhandle mode NS_ROWS return NS_ROWS } else { pg_result $cursor -clear $dbhandle num_rows 0 $dbhandle curr_row 0 $dbhandle mode NS_DML return NS_DML } } # # ... flush # ::nstcl::ad_proc -private ::nstcl::database::postgres::flush {dbhandle} { catch { pg_result [$dbhandle cursor] -clear } $dbhandle cursor {} $dbhandle exception_code {} $dbhandle exception_text {} $dbhandle num_rows {} $dbhandle curr_row {} $dbhandle mode {} } # # ... gethandle # ::nstcl::ad_proc -private ::nstcl::database::postgres::gethandle {pool dbhandle} { upvar 0 ::nstcl::database::pools pools set datasource $pools($pool,datasource) set user $pools($pool,user) set pass $pools($pool,pass) if {[string equal "" $datasource]} { set datasource :: } foreach {host port dbname} [split $datasource :] break set conninfo [list "dbname=$dbname"] if {[string length $host]} { lappend conninfo "host=$host" } if {[string length $port]} { lappend conninfo "port=$port" } if {[string length $user]} { lappend conninfo "user=$user" } if {[string length $pass]} { lappend conninfo "password=$pass" } # override the default of ::nstcl::database::pseudo_bind_variables # since Postgres has a non-standard escape of \ (i.e. \' along w/ '') $dbhandle bind_vars ::nstcl::database::postgres::pg_bind_vars $dbhandle conn [pg_connect -conninfo $conninfo] } # # ... getrow # ::nstcl::ad_proc -private ::nstcl::database::postgres::getrow {dbhandle setId} { set size [::nstcl::ns_set size $setId] set cursor [$dbhandle cursor] set num_rows [$dbhandle num_rows] set curr_row [$dbhandle curr_row] if {$num_rows > 0 && $curr_row > $num_rows} { return -code error "Database operation \"getrow\" failed" } if {$num_rows == 0} { return 0 } if {$num_rows == $curr_row} { return 0 } if {$size} { ::nstcl::ns_set truncate $setId 0 } pg_result $cursor -tupleArray $curr_row tupleArray foreach key [lsort -dictionary [array names tupleArray]] { ::nstcl::ns_set put $setId $key $tupleArray($key) } $dbhandle curr_row [incr curr_row] return 1 } # # pg_bind_vars: a custom pseudo_bind_variables # ::nstcl::ad_proc -private ::nstcl::database::postgres::pg_bind_vars {sql} { uplevel 1 [list ::nstcl::database::pseudo_bind_variables $sql 1] } package provide nstcl-database-postgres 1.1
1 http://nstcl.sourceforge.net
5 http://philip.greenspun.com/wtr/aolserver/introduction-1.html
7 http://www.linuxjournal.com/article.php?sid=6223
8 http://openacs.org/about/history
9 http://michael.yoon.org/arsdigita
10 http://wiki.tcl.tk/ArsDigita
12 https://tip.tcl-lang.org/111
13 http://nstcl.sourceforge.net/docs/nstcl-nssets/ns%5fset.html
15 http://www.perl.com/pub/a/1999/10/DBI.html
16 http://www.python.org/topics/database/DatabaseAPI-2.0.html
17 http://www.python.org/topics/database/modules.html
18 http://www.webkreator.com/php/concepts/php-database-wrappers.html
19 http://www.php.net/manual/en/funcref.php
20 http://pear.php.net/manual/en/core.db.php
21 http://www.phpclasses.org/browse.html/package/20.html
22 http://www.aolserver.com/docs/devel/driver/db
23 http://aolserver.com/docs/devel/tcl/api/db.html
24 http://nstcl.sourceforge.net/docs/nstcl-database/ns%5fdb.html
25 http://www.dilbert.com/comics/dilbert/the_characters/index.html