Pascal Scheffers 15Dec2004 - An introduction to Tcl database access with nstcl.
This example shows how to connect to a database, create tables, insert some data, how to select some data for reporting, and shows how transactions work.
The code assumes you have a database server somewhere, to test it. sqlite will do just fine.
This code has been tested with several databases/drivers:
Note: The create table statements are somewhat sensitive to specific databases. You may need to alter the data types slightly if you decide to run the demo against another database type.
# Choose a driver to run the demo #set driver odbc set driver sqlite #set driver postgres # Alter the datasource to your needs if you use driver other than odbc, sqlite or postgres # See http://nstcl.sourceforge.net/docs/nstcl-database/configure%5fpool.html for the correct # content of this variable. set datasource localhost:5432:bank set username bankuser set password bankpass set odbc_dsn bank package require nstcl namespace import nstcl::* if { $driver eq "sqlite" } { set loaded "" foreach version {sqlite3 sqlite} { if { [catch { package require $version set loaded $version } E] } { if { [file exists tcl$version[info sharedlibextension]] } { load tcl$version[info sharedlibextension] tcl$version set loaded $version } else { puts "$version not found." } } if { $loaded ne "" } {break} } if { $loaded eq "" } { puts "Sqlite not installed. Just drop the libsqlite[info sharedlibextension] \ or libsqlite3[info sharedlibextension] in the same directory as this demoscript." exit 1 } if { $loaded eq "sqlite3" } { interp alias {} sqlite {} sqlite3 } puts "Loaded $loaded." } elseif { $driver eq "postgres" } { if { [catch { #The next generation Postgresql driver package require Pgtcl } E] } { puts "Could not load Pgtcl (pgtcl-ng): $E" puts "Trying libpgtcl..." package require libpgtcl } } # Before you can use any type of database, nstcl needs to load the appropriate # database drivers. For this example, we use either odbc or sqlite. # # Load driver will automatically load the tclodbc package. sqlite needs to be # preloaded, as no default package require exists for it. # # Doc: http://nstcl.sourceforge.net/docs/nstcl-database/load%5fdriver.html nstcl::load_driver $driver # nstcl works with database pools, later on, we will use the pool name for # our statements. # # Doc: http://nstcl.sourceforge.net/docs/nstcl-database/configure%5fpool.html # ::nstcl::configure_pool ?-immediately? ?-default? driver poolname connections ?datasource? ?username? ?password? ?verbose? switch $driver { odbc { # driver pool #connections DSN user password nstcl::configure_pool odbc bank 1 $odbc_dsn $username $password catch { db_dml bank:drop "drop table account;" db_dml bank:drop "drop table orders;" } } postgres { # driver pool #connections DSN user password nstcl::configure_pool postgres bank 1 $datasource $username $password catch { db_dml bank:drop "drop table account;" db_dml bank:drop "drop table orders;" } } sqlite { catch { file delete [file join [file dirname [info script]] bank.dat] file delete [file join [file dirname [info script]] bank.dat-journal] } # driver pool #connections sqlite-file nstcl::configure_pool sqlite bank 1 [file join [file dirname [info script]] bank.dat] } default { # driver pool #connections DSN user password nstcl::configure_pool $driver bank 1 $datasource $username $password catch { db_dml bank:drop "drop table account;" db_dml bank:drop "drop table orders;" } } } # We now have a working database connection! # Lets create some tables: db_dml bank:table_accounts { create table account ( id integer, name varchar(200), balance numeric(10,2) ); } # There is no reason to have only one statement in a db_dml # Putting in more than one does *NOT* make it a transaction! db_dml bank:statement2 { create table orders ( id integer, account integer, description varchar(200), amount numeric(10,2) ); } db_dml bank:statement2 { insert into account (id, name, balance) values (1, 'Pascal Scheffers', 100.00); } #So far so good. You see you can execute properly formatted sql statements. #That is no big surprise. # For data entry, there is something better, however. Some values may need # quoting, and depending on the database type, quoting may differ between # database types and drivers. nstcl takes care of that. set accounts { 4 "Arjen Markus" 500.00 2 "Jean-Claude Wippler" 23.15 3 "Julian Scheffers" 56.87 } foreach {accountno name amount} $accounts { db_dml bank:new_accounts { insert into account (id, name, balance) values (:accountno, :name, :amount); } } # Okay, we have something in the database. Lets display the content: # in a proc so we can do it again! proc list_accounts {} { db_foreach bank:accounts { select id as a_id, name, balance from account order by id; } { # In the db_foreach loop, the column names are available as a normal # tcl variable. Becareful they don't clash with local variables! # rename them if you must! puts [format " %4d %8.2f %s" $a_id $balance $name] } } list_accounts # A very convenient function is db_string, which will let you get a single # value from the database: set total [db_string bank:all_accounts_total "select sum(balance) from account"] puts "The total in the bank is: [format %8.2f $total]\n" # db_foreach is one of my personal favorites, but you may need something # different. # # The important ones are: # # db_list # Obtains a list of the first rows of the query: set idList [db_list bank:ids "select id, name from account"] puts "Account numbers: $idList\n" # Note that the name column was dropped from the result! # # If you want the column names too, use db_list_of_lists: set idNameList [db_list_of_lists bank:idAndNames "select id, name from account"] puts "Account numbers and names: $idNameList\n" # # Now, db_foreach sets names variables for each row/column retrieved. # I find that I frequently need the variables for a single row: proc single_account { id } { # Note the bind variable again! db_1row bank:oneaccount "select name, balance from account where id=:id" puts "Account : $id" puts "Name : $name" puts "Balance : [format %.2f $balance]" puts "" } single_account 2 single_account 4 # # db_1row will raise an error if the statement does not return exactly 1 row # there is a companion function, db_0or1row which allows for checking the existence # of a row. # Both of these functions can also set an array, instead of the variables # this is very convenient, as it won't clutter your local variables! proc have_account? { id } { if { [db_0or1row bank:oneaccount "select id, name, balance from account where id=:id" \ -column_array row] } { puts "Account : $row(id)" puts "Name : $row(name)" puts "Balance : [format %.2f $row(balance)]" } else { puts "Account $id does not exist!" } puts "" } have_account? 1 have_account? 5 # # Similarly, [db_string] has a -default option, so it does not throw an error # but returns the default value: puts "Account 6 is owned by [db_string bank:oneacct "select name from account where id=6" -default "nobody"]" puts "Account 3 is owned by [db_string bank:oneacct "select name from account where id=3" -default "nobody"]" # # # That covers most of the data access and modification functions. # With a banking system, transactions are important: here is some code which # demonstrates transactions: proc transfer {fromAcct toAcct amount} { catch { db_transaction { set initial_balance [db_string bank:total "select sum(balance) from account"] db_dml bank:transferfrom "update account set balance=balance-:amount where id=:fromAcct" db_dml bank:transferto "update account set balance=balance+:amount where id=:toAcct" set final_balance [db_string bank:total "select sum(balance) from account"] if { [format %.2f $initial_balance] ne [format %.2f $final_balance] } { puts "Balance mismatch: [format %.2f $initial_balance] ne [format %.2f $final_balance] abort!" db_abort_transaction } else { puts "Transfered 25.00 from account $fromAcct to account $toAcct" } } } } puts "\nTransfer money (correctly):" list_accounts transfer 1 2 25.00 list_accounts puts "\nTransfer money (incorrectly):" transfer 4 5 105.22 list_accounts
LES 13-08-2007: The above script didn't quite work for me. Although I have the tclsqlite extension installed with ActiveTcl, also in another directory in my PATH and also in the same directory where I ran the script, the line 59 nstcl::load_driver $driver gives me an error:
couldn't load file "tclsqlite.so": tclsqlite.so: cannot open shared object file: No such file or directory while executing "load [::nstcl::find_shared_library tclsqlite]" (procedure "::nstcl::database::sqlite::load_driver" line 3) invoked from within "::nstcl::database::${driver}::load_driver $args" (procedure "::nstcl::database::load_driver" line 3) invoked from within "nstcl::load_driver $driver" (file "./nsdb.tcl" line 28)
Eventually, I edited /path/ActiveTcl/lib/nstcl-1.2/nstcl-database-sqlite.tcl, changed
load [::nstcl::find_shared_library tclsqlite]
to
load [file normalize [::nstcl::find_shared_library tclsqlite]]
...and it worked.
But then the transfer proc towards the end of the script gives me an error too:
could not allocate 1 handle(s) from pool "bank" while executing "::nstcl::ns_db gethandle $pool" (procedure "::nstcl::database::api_get_dbhandle" line 37) invoked from within "::nstcl::database::api_get_dbhandle $statement_name" (procedure "db_foreach" line 29) invoked from within "db_foreach bank:accounts { select id as a_id, name, balance from account order by id; } { # In the db_foreach loop, the column nam..." (procedure "list_accounts" line 2) invoked from within "list_accounts" (file "./nsdb.tcl" line 244)
I haven't been able to fix that one. I'll try again later. cd zzz now.