Index: openacs-4/packages/acs-tcl/tcl/00-database-procs-oracle.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/00-database-procs-oracle.tcl,v diff -u -N --- openacs-4/packages/acs-tcl/tcl/00-database-procs-oracle.tcl 5 Jun 2019 10:21:43 -0000 1.23.2.1 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,19 +0,0 @@ -ad_library { - - Oracle-specific database API and utility procs - - @creation-date 15 Apr 2000 - @author Jon Salz (jsalz@arsdigita.com) - @cvs-id $Id: 00-database-procs-oracle.tcl,v 1.23.2.1 2019/06/05 10:21:43 gustafn Exp $ -} - -# This file is now obsolete. All procs have been merged into -# 00-database-procs.tcl, so that all supported databases are usable -# with the db_* API all the time, regardless of which database type -# OpenACS is using. --atp@piskorski.com, 2003/04/09 12:04 EDT - -# Local variables: -# mode: tcl -# tcl-indent-level: 4 -# indent-tabs-mode: nil -# End: Index: openacs-4/packages/acs-tcl/tcl/00-database-procs-postgresql-postgresql.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/00-database-procs-postgresql-postgresql.xql,v diff -u -N --- openacs-4/packages/acs-tcl/tcl/00-database-procs-postgresql-postgresql.xql 30 Nov 2002 17:23:54 -0000 1.2 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,21 +0,0 @@ - - - postgresql7.1 - - - - select nextval(:sequence) as nextval - where (select relkind - from pg_class - where relname = :sequence) = 'S' - - - - - - select nextval - from ${sequence} - - - - Index: openacs-4/packages/acs-tcl/tcl/00-database-procs-postgresql.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/00-database-procs-postgresql.tcl,v diff -u -N --- openacs-4/packages/acs-tcl/tcl/00-database-procs-postgresql.tcl 5 Jun 2019 10:21:43 -0000 1.44.2.1 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,19 +0,0 @@ -ad_library { - - Postgres-specific database API and utility procs. - - @creation-date 15 Apr 2000 - @author Jon Salz (jsalz@arsdigita.com) - @cvs-id $Id: 00-database-procs-postgresql.tcl,v 1.44.2.1 2019/06/05 10:21:43 gustafn Exp $ -} - -# This file is now obsolete. All procs have been merged into -# 00-database-procs.tcl, so that all supported databases are usable -# with the db_* API all the time, regardless of which database type -# OpenACS is using. --atp@piskorski.com, 2003/04/09 12:04 EDT - -# Local variables: -# mode: tcl -# tcl-indent-level: 4 -# indent-tabs-mode: nil -# End: Index: openacs-4/packages/acs-tcl/tcl/00-database-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/00-database-procs.tcl,v diff -u -N --- openacs-4/packages/acs-tcl/tcl/00-database-procs.tcl 17 Jun 2019 10:19:23 -0000 1.126.2.6 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,3800 +0,0 @@ -ad_library { - - An API for managing database queries. - - @creation-date 15 Apr 2000 - @author Jon Salz (jsalz@arsdigita.com) - @cvs-id $Id: 00-database-procs.tcl,v 1.126.2.6 2019/06/17 10:19:23 gustafn Exp $ -} - -# Database caching. -# -# Values returned by a query are cached if you pass the "-cache_key" switch -# to the database procedure. The switch value will be used as the key in the -# ns_cache eval call used to execute the query and processing code. The -# db_flush proc should be called to flush the cache when appropriate. The -# "-cache_pool" parameter can be used to specify the cache pool to be used, -# and defaults to db_cache_pool. The # size of the default cache is governed -# by the kernel parameter "DBCacheSize" in the "caching" section. -# -# Currently db_string, db_list, db_list_of_lists, db_0or1row, and db_multirow support -# caching. -# -# Don Baccus 2/25/2006 - my 52nd birthday! - -# As originally released in (at least) ACS 4.2 through OpenACS 4.6, -# this DB API supported only a single, default database. You could -# define any number of different database drivers and pools in -# AOLserver, but could only use ONE database here. -# -# I have eliminated this restriction. Now, in OpenACS 5.0 and later, -# to access a non-default database, simply pass the optional -dbn -# (Database Name) switch to any of the DB API procs which support it. -# -# Supported AOLserver database drivers: -# -# - Oracle (nsoracle): Everything should work. -# -# - PostgreSQL (nspostgres): Everything should work. -# -# - ODBC (nsodbc): -# - Anything using bind variables will only work if you're using a -# version of the driver with bind variable emulation hacked in -# (copied from the PostgreSQL driver). -# - Some features, like LOBs, simply won't work at all. -# - The basic functionality worked fine back in Sept. 2001, but I -# have NOT tested it since then at all, so maybe there are bugs. -# -# - Any others: Basic stuff using only the standard ns_db API will -# likely work, but any special features of the driver (e.g., LOBs) -# definitely won't. Feel free to add support! -# -# --atp@piskorski.com, 2003/04/09 19:18 EDT - -# Note that "-dbn" specifies a "Database Name", NOT a database pool! -# -# I could have provided access to secondary databases via a -pool -# rather than a -dbn switch, but chose not to, as the existing DB API -# already had the nicely general feature that if you try to do nested -# queries, the DB API will transparently grab a second database handle -# from another pool to make it work. You can nest your queries as -# many levels deep as you have database pools defined for that -# database. So, the existing API essentially already supported the -# notion of "binning" database pools into logical "databases", it just -# didn't provide any way to define more than the single, default -# database! Thus I chose to preserve this "binning" by specifying -# databases via the -dbn switch rather than database pools via a -pool -# switch. - -# (JoelA, 27 Dec 2004 - replaced example config.tcl with link) -# -# see http://openacs.org/doc/openacs-5-1/tutorial-second-database -# for config and usage examples - -# TODO: The "driverkey_" overrides in the config file are NOT -# implemented yet! -# -# --atp@piskorski.com, 2003/03/16 21:30 EST - -# NOTE: don't forget to add your new pools into the -# ns_section ns/db/pools - - -# The "driverkey" indirection layer: -# -# Note that in the AOLserver config file, you may optionally add one -# entry for each database defining its "driver key". If you do NOT -# specify a driver key in the AOLserver config file, the appropriate -# key will be determined for you by calling "ns_db driver" once on -# startup for the first pool defined in each database. Therefore, -# most people should NOT bother to give a driverkey in the config -# file. -# -# So, just what is this "driverkey" thing used for anyway? AOLserver -# defines the ns_db API, and the OpenACS db_* API depends utterly on -# it. However, there are a few holes in the functionality of the -# ns_db API, and each AOLserver database driver tends to fill in those -# holes by adding extra functionality with its own, drive specific -# functions. Therefore, in order to make the db_* API work with -# multiple db drivers, we need to introduce some switches or if -# statements in our code. -# -# Currently (2003/04/08), at least for the Oracle, PostgreSQL, and -# ODBC drivers, the database driver name returned by "ns_db driver" is -# completely sufficient for these switch statements. But, rather than -# using ns_db driver directly in the switches, we add the simple -# "driver key" layer of indirection between the two, to make the -# default behavior easier to override if that should ever be -# necessary. -# -# --atp@piskorski.com, 2003/04/08 03:39 EDT - - -# We now use the following global variables: -# -# Server-Wide NSV arrays, keys: -# db_driverkey $dbn -# db_pool_to_dbn $pool -# -# Global Variables -# ::acs::default_database -# ::acs::db_pools($dbn) (used in db_available_pools) -# ::acs::db_pool_to_dbn($pool) (used for caching access to nsv db_pool_to_dbn) -# ::acs::db_driverkey($dbn) (used for caching access to nsv db_driverkey) -# -# Per-thread Tcl global variables: -# One Tcl Array per Database Name: -# db_state_${dbn} -# -# The db_available_pools and db_state arrays are used in exactly the -# same manner as they were originally (in ACS 4.0 to OpenACS 4.6 -# code), except that in the original DB API we had only one of each -# array total, while now we have one of each array per database. -# -# The db_pool_to_dbn nsv is simply a map to quickly tell use which dbn -# each AOLserver database pool belongs to. (Any pools which do not -# belong to any dbn have no entry here.) -# -# We use the procs db_state_array_name_is, db_available_pools, and -# db_driverkey to help keep track of these different arrays. -# Note that most code should now NEVER read from any of the -# db_available_pools nsvs listed above, but should instead use the -# proc db_available_pools provided for that purpose. -# -# The original implementation comments on the use of these global -# variables are below: -# -# --atp@piskorski.com, 2003/03/16 21:30 EST - - -ad_proc -private db_state_array_name_is { - {-dbn ""} -} { - @return the name of the global db_state array for the given - database name. - - @param dbn The database name to use. If empty_string, uses the - default database. - - @author Andrew Piskorski (atp@piskorski.com) - @creation-date 2003/03/16 -} { - if { $dbn eq "" } { - set dbn $::acs::default_database - } - #if {[llength [trace info variable ::db_state_${dbn}]] == 0} { - # trace add variable ::db_state_${dbn} {array read write unset} [list ::db_tracer ::db_state_${dbn}] - #} - return "::db_state_${dbn}" -} - -# proc db_tracer {varname name1 name2 op} { -# if {$name2 eq "handles"} { -# #ns_log notice "### variable $varname: $name1 ($name2) $op" -# if {$op eq "write"} { -# ns_log notice "###### handles updated to <[set ::${varname}($name2)]>" -# } -# } -# } - -ad_proc -public db_driverkey { - {-handle_p 0} - dbn -} { - Normally, a dbn is passed to this proc. Unfortunately, there are - one or two cases where a proc that needs to call this one has only - a db handle, not the dbn that handle came from. Therefore, they - instead use -handle_p 1 and pass the db handle. - - Hmm, as of 2018, it seems that in most cases, db_driverkey is - called with a handle. - - @return The driverkey for use in db_* API switch statements. - - @author Andrew Piskorski (atp@piskorski.com) - @creation-date 2003/04/08 -} { - if { $handle_p } { - # - # In the case, the passed "dbn" is actually a - # handle. Determine from the handle the "pool" and from the - # "pool" the "dbn". - # - set handle $dbn - set pool [ns_db poolname $handle] - set key ::acs::db_pool_to_dbn($pool) - if {[info exists $key]} { - # - # First, try to get the variable from the per-thread - # variable (which is part of the blueprint). - # - set dbn [set $key] - } elseif { [nsv_exists db_pool_to_dbn $pool] } { - # - # Fallback to nsv (old style), when for whatever - # reasons, the namespaced variable is not available. - # - ns_log notice "db_driverkey $handle_p dbn <$dbn> VIA NSV" - set dbn [nsv_get db_pool_to_dbn $pool] - } else { - # - # db_pool_to_dbn_init runs on startup, so other than some - # broken code deleting the nsv key (very unlikely), the - # only way this could happen is for someone to call this - # proc with a db handle from a pool which is not part of - # any dbn. - - error "No database name (dbn) found for pool '$pool'. Check the 'ns/server/[ns_info server]/acs/database' section of your config file." - } - } - - set key ::acs::db_driverkey($dbn) - if {[info exists $key]} { - return [set $key] - } - - if { ![nsv_exists db_driverkey $dbn] } { - # This ASSUMES that any overriding of this default value via - # "ns_param driverkey_dbn" has already been done: - - if { $handle_p } { - set driver [ns_db driver $handle] - } else { - db_with_handle -dbn $dbn handle { - set driver [ns_db driver $handle] - } - } - - # These are the default driverkey values, if they are not set - # in the config file: - - if { [string match "Oracle*" $driver] } { - set driverkey {oracle} - } elseif { $driver eq "PostgreSQL" } { - set driverkey "postgresql" - } elseif { $driver eq "ODBC" } { - set driverkey "nsodbc" - } else { - set driverkey {} - ns_log Error "db_driverkey: Unknown driver '$driver'." - } - - nsv_set db_driverkey $dbn $driverkey - } - - return [set $key [nsv_get db_driverkey $dbn]] -} - - -ad_proc -public db_type {} { - @return the RDBMS type (i.e. oracle, postgresql) this OpenACS installation is using. - The nsv ad_database_type is set up during the bootstrap process. -} { - # - # Currently this should always be either "oracle" or "postgresql": - # --atp@piskorski.com, 2003/03/16 22:01 EST - # - # First check, if the database type exists in the namespaced - # variable. This should be always the case. If this fail, fall - # back to the old-style nsv (which can be costly in tight db loops) - # - if {[info exists ::acs::database_type]} { - set result $::acs::database_type - } else { - set result [nsv_get ad_database_type .] - ns_log Warning "db_type '$result' had to be obtained from the nsv 'ad_database_type'" - set ::acs::database_type $result - } - return $result -} - -ad_proc -public db_compatible_rdbms_p { db_type } { - @return 1 if the given db_type is compatible with the current RDBMS. -} { - return [expr { $db_type eq "" || [db_type] eq $db_type }] -} - - - -ad_proc -private db_legacy_package_p { db_type_list } { - @return 1 if the package is a legacy package. We can only tell for certain if it explicitly supports Oracle 8.1.6 rather than the OpenACS more general oracle. -} { - if {"oracle-8.1.6" in $db_type_list} { - return 1 - } - return 0 -} - -ad_proc -public db_version {} { - @return the RDBMS version (i.e. 8.1.6 is a recent Oracle version; 7.1 a - recent PostgreSQL version) -} { - return [nsv_get ad_database_version .] -} - -ad_proc -public db_current_rdbms {} { - @return the current rdbms type and version. -} { - return [db_rdbms_create [db_type] [db_version]] -} - -ad_proc -public db_known_database_types {} { - @return a list of three-element lists describing the database engines known - to OpenACS. Each sublist contains the internal database name (used in file - paths, etc), the driver name, and a "pretty name" to be used in selection - forms displayed to the user. - - The nsv containing the list is initialized by the bootstrap script and should - never be referenced directly by user code. -} { - return $::acs::known_database_types -} - - -# db_null, db_quote, db_nullify_empty_string - were all previously -# defined Oracle only, no Postgres equivalent existed at all. So, it -# can't hurt anything to have them defined in when OpenACS is using -# Postgres too. --atp@piskorski.com, 2003/04/08 05:34 EDT - -ad_proc -deprecated db_null {} { - - @return an empty string, which Oracle thinks is null. - - Deprecated: This routine was invented to provide an RDBMS-specific null - value but doesn't actually work. I (DRB) left it in to speed porting - we - should really clean up the code and pull out the calls instead, though. - - @see "" -} { - return "" -} - -ad_proc -public db_quote { string } { - Quotes a string value to be placed in a SQL statement. -} { - regsub -all {'} "$string" {''} result - return $result -} - -ad_proc -public -deprecated db_nullify_empty_string { string } { - A convenience function that returns [db_null] if $string is the empty string. - - Deprecated: essentially just returns the passed string. - - @see: db_null -} { - return $string -} - -ad_proc -public db_boolean { bool } { - Converts a Tcl boolean (1/0) into a SQL boolean (t/f) - @return t or f -} { - if { $bool } { - return "t" - } else { - return "f" - } -} - -ad_proc -public db_nextval { - { -dbn "" } - sequence -} { - - Example: - -
-    set new_object_id [db_nextval acs_object_id_seq]
-    
- - @return the next value for a sequence. This can utilize a pool of - sequence values. - - @param sequence the name of an SQL sequence - - @param dbn The database name to use. If empty_string, uses the default database. - - @see /doc/db-api-detailed -} { - set driverkey [db_driverkey $dbn] - - # PostgreSQL has a special implementation here, any other db will - # probably work with the default: - - switch -- $driverkey { - - postgresql { - # # the following query will return a nextval if the sequnce - # # is of relkind = 'S' (a sequnce). if it is not of relkind = 'S' - # # we will try querying it as a view: - - # if { [db_0or1row -dbn $dbn nextval_sequence " - # select nextval('${sequence}') as nextval - # where (select relkind - # from pg_class - # where relname = '${sequence}') = 'S' - # "]} { - # return $nextval - # } else { - # ns_log debug "db_nextval: sequence($sequence) is not a real sequence. perhaps it uses the view hack." - # db_0or1row -dbn $dbn nextval_view "select nextval from ${sequence}" - # return $nextval - # } - # - # The code above is just for documentation, how it worked - # before the change below. We keep now a per-thread table of - # the "known" sequences to avoid at runtime the query, - # whether the specified sequence is a real sequence or a - # view. This change makes this function more than a factor - # of 2 faster than before. - # - # Note that solely the per-thread information won't work for - # freshly created sequences. Therefore, we keep the old - # code for checking at runtime in the database for such - # occurrences. - # - # Note that the sequence handling in OpenACS is quite a - # mess. Some sequences are named t_SEQUENCE (10 in - # dotlrn), others are called just SEQUENCE (18 in dotlrn), - # for some sequences, additional views are defined with an - # attribute 'nextval', and on top of this, db_nextval is - # called sometimes with the view name and sometimes with - # the sequence name. Checking this at runtime is - # unnecessary complex and costly. - # - # The best solution would certainly be to call "db_nextval" - # only with real sequence names (as defined in SQL). In that - # case, the whole function would for postgres would collapse - # to a single line, without any need for sequence name - # caching. But in that case, one should rename the sequences - # from t_SEQUENCE to SEQUENCE for postgres. - # - # However, since Oracle uses the pseudo column ".nextval", - # which is emulated via the view, it is not clear, how - # feasible this is to remove all such views without breaking - # installed applications. We keep for such cases the view, - # but nevertheless, the function "db_nextval" should always - # be called with names without the "t_" prefix to achieve - # Oracle compatibility. - - if {![info exists ::db::sequences]} { - ns_log notice "-- creating per thread sequence table" - namespace eval ::db {} - foreach s [db_list -dbn $dbn relnames "select relname, relkind from pg_class where relkind = 'S'"] { - set ::db::sequences($s) 1 - } - } - if {[info exists ::db::sequences(t_$sequence)]} { - #ns_log notice "-- found t_$sequence - #ad_log Warning "Deprecated sequence name 't_$sequence' is used. Use instead 't_$sequence'" - set nextval [db_string -dbn $dbn nextval "select nextval('t_$sequence')"] - } elseif {[info exists ::db::sequences($sequence)]} { - #ns_log notice "-- found $sequence" - set nextval [db_string -dbn $dbn nextval "select nextval('$sequence')"] - if {[string match t_* $sequence]} { - ad_log Warning "For portability, db_nextval should be called without the leading 't_' prefix: 't_$sequence'" - } - } elseif { [db_0or1row -dbn $dbn nextval_sequence " - select nextval('${sequence}') as nextval - where (select relkind - from pg_class - where relname = '${sequence}') = 'S' - "]} { - # - # We do not have an according sequence-table. Use the system catalog to check - # for the sequence - # - # ... the query sets nextval if it succeeds - # - ad_log Warning "Probably deprecated sequence name '$sequence' is used (no sequence table found)" - } else { - # - # Finally, there might be a view with a nextval - # - ns_log debug "db_nextval: sequence($sequence) is not a real sequence. perhaps it uses the view hack." - set nextval [db_string -dbn $dbn nextval "select nextval from $sequence"] - ad_log Warning "Using deprecated sequence view hack for '$sequence'. Is there not real sequence?" - } - - return $nextval - } - - oracle - - nsodbc - - default { - return [db_string -dbn $dbn nextval "select $sequence.nextval from dual"] - } - } -} - -ad_proc -public db_nth_pool_name { - { -dbn "" } - n -} { - @return the name of the pool used for the nth-nested selection (0-relative). - @param dbn The database name to use. If empty_string, uses the default database. -} { - set available_pools [db_available_pools $dbn] - - if { $n < [llength $available_pools] } { - set pool [lindex $available_pools $n] - } else { - return -code error "Ran out of database pools ($available_pools)" - } - return $pool -} - -if {[acs::icanuse "ns_db currenthandles"]} { - - ns_log notice "... I can use 'ns_db currenthandles'" - - # - # This branch uses "ns_db currenthandles" to implement - # "db_with_handle" instead of the old approach based on the global - # db_state variables. The new approach has the advantage that it - # is: - # - # - more robust (deletion and creation of the per-request variables, - # no coherency problem), - # - simpler, and - # - faster (less overhead per db_with_handle call) - # - # time {db_string . {select object_id from acs_objects limit 1}} 1000 - # old: 160-190 microseconds per iteration - # new: 150-180 microseconds per iteration - # - # time {xo::dc get_value . {select object_id from acs_objects limit 1}} 1000 - # old: 110-120 - # new: 105-110 - # - # set id -1 - # time {xo::dc get_value -prepare {int} . {select object_id from acs_objects where object_id=:id}} 1000 - # old: 80-100 - # new: 76-90 - # - # Still, more improvement can be done (GN). - # - ad_proc -public db_with_handle { - { -dbn "" } - db code_block - } { - Place a usable database handle in db and executes - code_block. - - @param dbn Database name to use. If empty_string, use the default database - @param db Name of the handle variable used in the code block - @param code_block code block to be executed with handle - } { - # - # Let the caller decide, how the handle variable is called in - # the code block. - # - upvar 1 $db dbh - - # - # Get the pools and the current allocated handles for this thread. - # - set pools [db_available_pools $dbn] - set currentHandles [ns_db currenthandles] - #ns_log notice "### pools <$pools> currentHandles <$currentHandles>" - - set db "" - set n 0 - foreach pool $pools { - # - # Do we have already handles allocated from this pool? - # - if {[dict exists $currentHandles $pool]} { - # - # Are there handles, which are not active (i.e. not in - # a currently open "ns_db select" and "ns_db getrow" - # context. - # - foreach {handle active} [dict get $currentHandles $pool] { - #ns_log notice "### FOUND pool $pool handle $handle active $active" - if {$active eq "0"} { - # - # We can use this handle - # - set db $handle - break - } - } - } else { - break - } - incr n - } - # - # In case, we got no handle above, we have to allocate a - # handle from the next pool, from which we have not got a - # handle before. - # - if {$db eq ""} { - # - # We were not successful above - # - set pool [lindex $pools $n] - if {$pool eq ""} { - ad_log error "handles from all pools <$pools> are exhausted" - error "could not obtain handle, all pools are exhausted" - } - set start_time [expr {[clock clicks -microseconds]/1000.0}] - #ns_log notice "### BEFORE gethandle $pool ($n)" - set errno [catch { - set db [ns_db gethandle $pool] - } error] - #ad_log notice "### AFTER gethandle $pool errno $errno handle <$db> currentHandles [ns_db currenthandles]" - ds_collect_db_call $db gethandle "" $pool $start_time $errno $error - if { $errno } { - ns_log notice "### RETURNING error $error" - return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error - } - } - #ns_log notice "### db_with_handle has handle <$db>" - - set dbh $db - set errno [catch { uplevel 1 $code_block } error] - - # Unset dbh, so any subsequence use of this variable will bomb. - unset -nocomplain dbh - - # If errno is 1, it's an error, so return errorCode and errorInfo; - # if errno = 2, it's a return, so don't try to return errorCode/errorInfo - # errno = 3 or 4 give undefined results - - if { $errno == 1 } { - # A real error occurred - ns_log notice "### db_with_handle returned error <$error> for statement $code_block" - return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error - } - - if { $errno == 2 } { - - # The code block called a "return", so pass the message through but don't try - # to return errorCode or errorInfo since they may not exist - - return -code $errno $error - } - } - - # - # db_last_used_handle - # - ad_proc -private db_last_used_handle {{-dbn ""}} { - Get the last used inactive handle. - - @param dbn database name - @return last active handle or empty string - } { - set pools [db_available_pools $dbn] - set currentHandles [ns_db currenthandles] - - set last_used_handle "" - foreach pool $pools { - if {[dict exists $currentHandles $pool]} { - foreach {handle active} [dict get $currentHandles $pool] { - #ns_log notice "### FOUND pool $pool handle $handle active $active" - if {$active eq 0} { - set last_used_handle $handle - } - } - } - } - #ns_log notice "###### db_last_used_handle: <$currentHandles> last used $last_used_handle" - return $last_used_handle - } - - # - # db_release_unused_handles - # - ad_proc -public db_release_unused_handles {{-dbn ""}} { - Releases any database handles that are presently unused. - - @param dbn The database name to use. If empty_string, uses the default database. - } { - # we need the state array still for transaction handling - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - set pools [db_available_pools $dbn] - set currentHandles [ns_db currenthandles] - - foreach pool $pools { - if {[dict exists $currentHandles $pool]} { - foreach {handle active} [dict get $currentHandles $pool] { - #ns_log notice "### FOUND pool $pool handle $handle active $active" - if {$active eq 0} { - # Don't release handles which are part of a transaction. - if { [info exists db_state(transaction_level,$handle)] - && $db_state(transaction_level,$handle) > 0 - } { - continue - } - set start_time [expr {[clock clicks -microseconds]/1000.0}] - ns_db releasehandle $handle - #ns_log notice "### AFTER releasehandle [ns_db currenthandles $pool]" - ds_collect_db_call $handle releasehandle "" "" $start_time 0 "" - } - } - } - } - } - - -} else { - - # - # This is the legacy branch without [ns_db currenthandles], using - # the global state variables. - # - ns_log notice "... cannot use 'ns_db currenthandles'" - - ad_proc -public db_with_handle { - { -dbn "" } - db code_block - } { - - Places a usable database handle in db and executes code_block. - - @param dbn The database name to use. If empty_string, uses the default database. - } { - upvar 1 $db dbh - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - # Initialize bookkeeping variables. - if { ![info exists db_state(handles)] } { - set db_state(handles) [list] - } - if { ![info exists db_state(n_handles_used)] } { - set db_state(n_handles_used) 0 - } - if { $db_state(n_handles_used) >= [llength $db_state(handles)] } { - set pool [db_nth_pool_name -dbn $dbn $db_state(n_handles_used)] - set start_time [expr {[clock clicks -microseconds]/1000.0}] - set errno [catch { - set db [ns_db gethandle $pool] - } error] - ds_collect_db_call $db gethandle "" $pool $start_time $errno $error - lappend db_state(handles) $db - if { $errno } { - return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error - } - } - set my_dbh [lindex $db_state(handles) $db_state(n_handles_used)] - set dbh $my_dbh - set db_state(last_used) $my_dbh - - incr db_state(n_handles_used) - set errno [catch { uplevel 1 $code_block } error] - incr db_state(n_handles_used) -1 - - # This may have changed while the code_block was being evaluated. - set db_state(last_used) $my_dbh - - # Unset dbh, so any subsequence use of this variable will bomb. - unset -nocomplain dbh - - # If errno is 1, it's an error, so return errorCode and errorInfo; - # if errno = 2, it's a return, so don't try to return errorCode/errorInfo - # errno = 3 or 4 give undefined results - - if { $errno == 1 } { - # A real error occurred - return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error - } - - if { $errno == 2 } { - - # The code block called a "return", so pass the message through but don't try - # to return errorCode or errorInfo since they may not exist - - return -code $errno $error - } - } - - ad_proc -private db_last_used_handle {{-dbn ""}} { - Get the last used handle - - @param dbn database name - @return last active handle or empty string - } { - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - return $db_state(last_used) - } - - ad_proc -public db_release_unused_handles {{-dbn ""}} { - - Releases any database handles that are presently unused. - - @param dbn The database name to use. If empty_string, uses the default database. - } { - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - if { [info exists db_state(n_handles_used)] } { - # Examine the elements at the end of db_state(handles), killing off - # handles that are unused and not engaged in a transaction. - - set index_to_examine [expr { [llength $db_state(handles)] - 1 }] - while { $index_to_examine >= $db_state(n_handles_used) } { - set db [lindex $db_state(handles) $index_to_examine] - - # Stop now if the handle is part of a transaction. - if { [info exists db_state(transaction_level,$db)] - && $db_state(transaction_level,$db) > 0 - } { - break - } - - set pool [db_nth_pool_name -dbn $dbn $db_state(n_handles_used)] - set start_time [expr {[clock clicks -microseconds]/1000.0}] - ns_db releasehandle $db - ds_collect_db_call $db releasehandle "" "" $start_time 0 "" - incr index_to_examine -1 - } - set db_state(handles) [lrange $db_state(handles) 0 $index_to_examine] - } - } - - -} - -ad_proc -public db_resultrows {{-dbn ""}} { - @return the number of rows affected by the last DML command. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set driverkey [db_driverkey $dbn] - - switch -- $driverkey { - oracle { - return [ns_ora resultrows [db_last_used_handle -dbn $dbn]] - } - postgresql { - return [ns_pg ntuples [db_last_used_handle -dbn $dbn]] - } - nsodbc { - error "db_resultrows is not supported for this database." - } - default { - error "Unknown database driver. db_resultrows is not supported for this database." - } - } -} - - - -ad_proc -public db_exec_plsql { - {-dbn ""} - statement_name - sql - args -} { - - Oracle: - Executes a PL/SQL statement, and returns the variable of bind - variable :1. - -

- PostgreSQL: - Performs a pl/pgsql function or procedure call. The caller must - perform a select query that returns the value of the function. - -

- Examples: - -

-

-    # Oracle:
-    db_exec_plsql delete_note {
-        begin  note.del(:note_id);  end;
-    }
-
-    # PostgreSQL:
-    db_exec_plsql delete_note {
-        select note__delete(:note_id);
-    }
-    
- -

- If you need the return value, then do something like this: - -

-

-    # Oracle:
-    set new_note_id [db_exec_plsql create_note {
-        begin
-        :1 := note.new(
-                       owner_id => :user_id,
-                       title    => :title,
-                       body     => :body,
-                       creation_user => :user_id,
-                       creation_ip   => :peeraddr,
-                       context_id    => :package_id
-                       );
-        end;
-    }]
-
-    # PostgreSQL:
-    set new_note_id [db_exec_plsql create_note {
-        select note__new(
-                         null,
-                         :user_id,
-                         :title,
-                         :body,
-                         'note',
-                         now(),
-                         :user_id,
-                         :peeraddr,
-                         :package_id
-                         );
-    }]
-    
- -

- You can call several pl/sql statements at once, like this: - -

-

-    # Oracle:
-    db_exec_plsql delete_note {
-        begin
-        note.del(:note_id);
-        note.del(:another_note_id);
-        note.del(:yet_another_note_id);
-        end;
-    }
-
-    # PostgreSQL:
-    db_exec_plsql delete_note {
-        select note__delete(:note_id);
-        select note__delete(:another_note_id);
-        select note__delete(:yet_another_note_id);
-    }
-    
- - If you are using xql files then put the body of the query in a - yourfilename-oracle.xql or yourfilename-postgresql.xql file, as appropriate. E.g. the first example - transformed to use xql files looks like this: - - -

- yourfilename.tcl:
-

-

-    db_exec_plsql delete_note {}
- -

- yourfilename-oracle.xql:
-

-

-    <fullquery name="delete_note">
-    <querytext>
-    begin
-    note.del(:note_id);
-    end;
-    </querytext>
-    </fullquery>
- -

- yourfilename-postgresql.xql:
-

-

-    <fullquery name="delete_note">
-    <querytext>
-    select note__delete(:note_id);
-    </querytext>
-    </fullquery>
- - - @param dbn The database name to use. If empty_string, uses the default database. - - @see /doc/db-api-detailed -} { - ad_arg_parser { bind_output bind } $args - - # Query Dispatcher (OpenACS - ben) - set full_statement_name [db_qd_get_fullname $statement_name] - - if { [info exists bind_output] } { - return -code error "the -bind_output switch is not currently supported" - } - - set driverkey [db_driverkey $dbn] - switch -- $driverkey { - postgresql { - set postgres_p 1 - } - - oracle - - nsodbc - - default { - set postgres_p 0 - } - } - - if { ! $postgres_p } { - db_with_handle -dbn $dbn db { - # Right now, use :1 as the output value if it occurs in the statement, - # or not otherwise. - set test_sql [db_qd_replace_sql $full_statement_name $sql] - if { [regexp {:1} $test_sql] } { - return [db_exec exec_plsql_bind $db $full_statement_name $sql 2 1 ""] - } else { - return [db_exec dml $db $full_statement_name $sql] - } - } - } else { - # Postgres doesn't have PL/SQL, of course, but it does have - # PL/pgSQL and other procedural languages. Rather than assign the - # result to a bind variable which is then returned to the caller, - # the Postgres version of OpenACS requires the caller to perform a - # select query that returns the value of the function. - - # We are no longer calling db_string, which screws up the bind - # variable stuff otherwise because of calling environments. (ben) - - ad_arg_parser { bind_output bind } $args - - # I'm not happy about having to get the fullname here, but right now - # I can't figure out a cleaner way to do it. I will have to - # revisit this ASAP. (ben) - set full_statement_name [db_qd_get_fullname $statement_name] - - if { [info exists bind_output] } { - return -code error "the -bind_output switch is not currently supported" - } - - db_with_handle -dbn $dbn db { - # plsql calls that are simple selects bypass the plpgsql - # mechanism for creating anonymous functions (OpenACS - Dan). - # if a table is being created, we need to bypass things, too (OpenACS - Ben). - set test_sql [db_qd_replace_sql $full_statement_name $sql] - if {[regexp -nocase -- {^\s*select} $test_sql match]} { - # ns_log Debug "PLPGSQL: bypassed anon function" - set selection [db_exec 0or1row $db $full_statement_name $sql] - } elseif {[regexp -nocase -- {^\s*(create|drop) table} $test_sql match]} { - ns_log Debug "PLPGSQL: bypassed anon function for create/drop table" - set selection [db_exec dml $db $full_statement_name $sql] - return "" - } else { - # ns_log Debug "PLPGSQL: using anonymous function" - set selection [db_exec_plpgsql $db $full_statement_name $sql $statement_name] - } - return [ns_set value $selection 0] - } - } -} - - -ad_proc -private db_exec_plpgsql { db statement_name pre_sql fname } { - - PostgreSQL only. -

- - A helper procedure to execute a SQL statement, potentially binding - depending on the value of the $bind variable in the calling environment - (if set). - -

- Low level replacement for db_exec which replaces inline code with a proc. - db proc is dropped after execution. This is a temporary fix until we can - port all of the db_exec_plsql calls to simple selects of the inline code - wrapped in function calls. - -

- emulation of plsql calls from oracle. This routine takes the plsql - statements and wraps them in a function call, calls the function, and then - drops the function. Future work might involve converting this to cache the - function calls - -

- This proc is private - use db_exec_plsql instead! - - @see db_exec_plsql - -} { - set start_time [expr {[clock clicks -microseconds]/1000.0}] - - set sql [db_qd_replace_sql $statement_name $pre_sql] - - set unique_id [db_nextval "anon_func_seq"] - - set function_name "__exec_${unique_id}_${fname}" - - # insert Tcl variable values (OpenACS - Dan) - if {$sql ne $pre_sql } { - set sql [uplevel 2 [list subst -nobackslashes $sql]] - } - ns_log Debug "PLPGSQL: converted: $sql to: select $function_name ()" - - # create a function definition statement for the inline code - # binding is emulated in tcl. (OpenACS - Dan) - - set errno [catch { - upvar bind bind - if { [info exists bind] && [llength $bind] != 0 } { - if { [llength $bind] == 1 } { - set bind_vars [list] - set len [ns_set size $bind] - for {set i 0} {$i < $len} {incr i} { - lappend bind_vars [ns_set key $bind $i] \ - [ns_set value $bind $i] - } - set proc_sql [db_bind_var_substitution $sql $bind_vars] - } else { - set proc_sql [db_bind_var_substitution $sql $bind] - } - } else { - set proc_sql [uplevel 2 [list db_bind_var_substitution $sql]] - } - - ns_db dml $db "create function $function_name () returns varchar as [::ns_dbquotevalue $proc_sql] language 'plpgsql'" - - set ret_val [ns_db 0or1row $db "select $function_name ()"] - - # drop the anonymous function (OpenACS - Dan) - # JCD: ignore return code -- maybe we should be smarter about this though. - catch {ns_db dml $db "drop function $function_name ()"} - - return $ret_val - - } error] - - set errinfo $::errorInfo - set errcode $::errorCode - - ds_collect_db_call $db 0or1row $statement_name $sql $start_time $errno $error - - if { $errno == 2 } { - return $error - } else { - catch {ns_db dml $db "drop function $function_name ()"} - } - - return -code $errno -errorinfo $errinfo -errorcode $errcode $error -} - -ad_proc -private db_get_quote_indices { sql } { - Given a piece of SQL, return the indices of single quotes. - This is useful when we do bind var substitution because we should - not attempt bind var substitution inside quotes. Examples: - -

-    sql          return value
-    {'a'}           {0 2}
-    {'a''}           {}
-    {'a'a'a'}       {0 2 4 6}
-    {a'b'c'd'}      {1 3 5 7}
-    
- - @see db_bind_var_substitution -} { - set quote_indices [list] - - # Returns a list on the format - # Example - for sql={'a'a'a'} returns - # {0 2} {0 0} {2 2} {3 6} {4 4} {6 6} - set all_indices [regexp -inline -indices -all -- {(?:^|[^'])(')(?:[^']|'')+(')(?=$|[^'])} $sql] - - for {set i 0} { $i < [llength $all_indices] } { incr i 3 } { - lappend quote_indices [lindex $all_indices $i+1 0] [lindex $all_indices $i+2 0] - } - - return $quote_indices -} - -ad_proc -private db_bind_var_quoted_p { sql bind_start_idx bind_end_idx} { - -} { - foreach {quote_start_idx quote_end_idx} [db_get_quote_indices $sql] { - if { $bind_start_idx > $quote_start_idx && $bind_end_idx < $quote_end_idx } { - return 1 - } - } - - return 0 -} - -ad_proc -private db_bind_var_substitution { sql { bind "" } } { - - This proc emulates the bind variable substitution in the PostgreSQL driver. - Since this is a temporary hack, we do it in Tcl instead of hacking up the - driver to support plsql calls. This is only used for the db_exec_plpgsql - function. - -} { - if {$bind eq ""} { - upvar __db_sql lsql - set lsql $sql - uplevel { - set __db_lst [regexp -inline -indices -all -- {:?:\w+} $__db_sql] - for {set __db_i [expr {[llength $__db_lst] - 1}]} {$__db_i >= 0} {incr __db_i -1} { - set __db_ws [lindex $__db_lst $__db_i 0] - set __db_we [lindex $__db_lst $__db_i 1] - set __db_bind_var [string range $__db_sql $__db_ws $__db_we] - if {![string match "::*" $__db_bind_var] && ![db_bind_var_quoted_p $__db_sql $__db_ws $__db_we]} { - set __db_tcl_var [string range $__db_bind_var 1 end] - set __db_tcl_var [set $__db_tcl_var] - if {$__db_tcl_var eq ""} { - set __db_tcl_var null - } else { - set __db_tcl_var "[::ns_dbquotevalue $__db_tcl_var]" - } - set __db_sql [string replace $__db_sql $__db_ws $__db_we $__db_tcl_var] - } - } - } - } else { - - array set bind_vars $bind - - set lsql $sql - set lst [regexp -inline -indices -all -- {:?:\w+} $sql] - for {set i [expr {[llength $lst] - 1}]} {$i >= 0} {incr i -1} { - set ws [lindex $lst $i 0] - set we [lindex $lst $i 1] - set bind_var [string range $sql $ws $we] - if {![string match "::*" $bind_var] && ![db_bind_var_quoted_p $lsql $ws $we]} { - set tcl_var [string range $bind_var 1 end] - set val $bind_vars($tcl_var) - if {$val eq ""} { - set val null - } else { - set val "[::ns_dbquotevalue $val]" - } - set lsql [string replace $lsql $ws $we $val] - } - } - } - - return $lsql -} - - -ad_proc -private db_getrow { db selection } { - - A helper procedure to perform an ns_db getrow, invoking developer support - routines as necessary. - -} { - set start_time [expr {[clock clicks -microseconds]/1000.0}] - set errno [catch { return [ns_db getrow $db $selection] } error] - ds_collect_db_call $db getrow "" "" $start_time $errno $error - if { $errno == 2 } { - return $error - } - return -code $errno -errorinfo $::errorInfo -errorcode $::errorCode $error -} - - -ad_proc -public db_exec { type db statement_name pre_sql {ulevel 2} args } { - - A helper procedure to execute a SQL statement, potentially binding - depending on the value of the $bind variable in the calling environment - (if set). - -} { - set start_time [expr {[clock clicks -microseconds]/1000.0}] - set driverkey [db_driverkey -handle_p 1 $db] - - # Note: Although marked as private, db_exec is in fact called - # extensively from several other packages. We DEFINITELY don't - # want to have to change all those procs to pass in the - # (redundant) $dbn just so we can use it in the call to - # db_driverkey, so db_driverkey MUST support its -handle switch. - # --atp@piskorski.com, 2003/04/09 12:13 EDT - - set sql [db_qd_replace_sql $statement_name $pre_sql] - - # insert Tcl variable values (OpenACS - Dan) - if {$sql ne $pre_sql } { - set sql [uplevel $ulevel [list subst -nobackslashes $sql]] - } - - set errno [catch { - upvar bind bind - - if { [info exists bind] && [llength $bind] != 0 } { - if { [llength $bind] == 1 } { - # $bind is an ns_set id: - - switch -- $driverkey { - oracle { - return [ns_ora $type $db -bind $bind $sql {*}$args] - } - postgresql { - return [ns_pg_bind $type $db -bind $bind $sql] - } - nsodbc { - return [ns_odbc_bind $type $db -bind $bind $sql] - } - default { - error "Unknown database driver. Bind variables not supported for this database." - } - } - - } else { - # $bind is a Tcl list, convert it to an ns_set: - set bind_vars [ns_set create] - foreach { name value } $bind { - ns_set put $bind_vars $name $value - } - } - - switch -- $driverkey { - oracle { - # TODO: Using $args outside the list is - # potentially bad here, depending on what is in - # args and if the items contain any embedded - # whitespace. Or maybe it works fine. But it's - # hard to know. Document or fix. - # --atp@piskorski.com, 2003/04/09 15:33 EDT - - return [ns_ora $type $db -bind $bind_vars $sql {*}$args] - } - postgresql { - return [ns_pg_bind $type $db -bind $bind_vars $sql] - } - nsodbc { - return [ns_odbc_bind $type $db -bind $bind_vars $sql] - } - default { - error "Unknown database driver. Bind variables not supported for this database." - } - } - - } else { - # Bind variables, if any, are defined solely as individual - # Tcl variables: - - switch -- $driverkey { - oracle { - return [uplevel $ulevel [list ns_ora $type $db $sql] $args] - } - postgresql { - return [uplevel $ulevel [list ns_pg_bind $type $db $sql]] - } - nsodbc { - return [uplevel $ulevel [list ns_odbc_bind $type $db $sql]] - } - default { - # Using plain ns_db like this will work ONLY if - # the query is NOT using bind variables: - # --atp@piskorski.com, 2001/09/03 08:41 EDT - return [uplevel $ulevel [list ns_db $type $db $sql] $args] - } - } - } - } error] - - # JCD: we log the clicks, dbname, query time, and statement to catch long running queries. - # If we took more than 3 seconds yack about it. - if { [clock clicks -milliseconds] - $start_time > 3000 } { - set duration [format %.2f [expr {[clock clicks -milliseconds] - $start_time}]] - ns_log Warning "db_exec: longdb $duration ms $db $type $statement_name" - } else { - #set duration [format %.2f [expr {[clock clicks -milliseconds] - $start_time}]] - #ns_log Debug "db_exec: timing $duration seconds $db $type $statement_name" - } - - ds_collect_db_call $db $type $statement_name $sql $start_time $errno $error - if { $errno == 2 } { - return $error - } - - return -code $errno -errorinfo $::errorInfo -errorcode $::errorCode $error -} - - -ad_proc -public db_string { - {-dbn ""} - -cache_key - {-cache_pool db_cache_pool} - statement_name - sql - args -} { - - Usage: db_string statement-name sql [ -default default ] [ -bind bind_set_id | -bind bind_value_list ] - - @return the first column of the result of the SQL query sql. If the query doesn't return a row, returns default or raises an error if no default is provided. - - @param dbn The database name to use. If empty_string, uses the default database. - @param cache_key Cache the result using given value as the key. Default is to not cache. - @param cache_pool Override the default db_cache_pool -} { - # Query Dispatcher (OpenACS - ben) - set full_name [db_qd_get_fullname $statement_name] - - ad_arg_parser { default bind } $args - - set code { - db_with_handle -dbn $dbn db { - set selection [db_exec 0or1row $db $full_name $sql] - } - if { $selection eq ""} { - if { [info exists default] } { - return $default - } - error "Selection did not return a value, and no default was provided" - } - return [ns_set value $selection 0] - } - - if { [info exists cache_key] } { - return [ns_cache eval $cache_pool $cache_key $code] - } else { - return [eval $code] - } -} - - -ad_proc -public db_list { - {-dbn ""} - -cache_key - {-cache_pool db_cache_pool} - statement_name - sql - args -} { - - Usage: db_list statement-name sql [ -bind bind_set_id | -bind bind_value_list ] - - @return a Tcl list of the values in the first column of the result of SQL query sql. - If sql doesn't return any rows, returns an empty list. - - @param dbn The database name to use. If empty_string, uses the default database. - @param cache_key Cache the result using given value as the key. Default is to not cache. - @param cache_pool Override the default db_cache_pool -} { - ad_arg_parser { bind } $args - - # Query Dispatcher (OpenACS - SDW) - set full_statement_name [db_qd_get_fullname $statement_name] - - # Can't use db_foreach in this proc, since we need to use the ns_set directly. - - set code { - db_with_handle -dbn $dbn db { - set selection [db_exec select $db $full_statement_name $sql] - set result [list] - while { [db_getrow $db $selection] } { - lappend result [ns_set value $selection 0] - } - } - return $result - } - if { [info exists cache_key] } { - return [ns_cache eval $cache_pool $cache_key $code] - } else { - return [eval $code] - } -} - - -ad_proc -public db_list_of_lists { - {-dbn ""} - -cache_key - {-cache_pool db_cache_pool} - -with_headers:boolean - statement_name - sql - args -} { - - Usage: db_list_of_lists statement-name sql [ -bind bind_set_id | -bind bind_value_list ] - - @param with_headers when specified, first line of returned list of - lists will always be the list of column names as reported by the - database. Useful when you want to dynamically assign variables to - values returned in the list of lists. - - @return a Tcl list, each element of which is a list of all column - values in a row of the result of the SQL querysql. If - sql doesn't return any rows, returns an empty list, - unless with_headers flag was specified and in this case the only - element in the list will be the list of headers. - - It checks if the element is I18N and replaces it, thereby - reducing the need to do this with every single package - - @param dbn The database name to use. If empty_string, uses the default database. - @param cache_key Cache the result using given value as the key. Default is to not cache. - @param cache_pool Override the default db_cache_pool -} { - ad_arg_parser { bind } $args - - set code { - set result [list] - foreach selection [uplevel [list db_list_of_ns_sets -dbn $dbn $statement_name $sql]] { - set selection_array [ns_set array $selection] - if {[llength $result] == 0 && $with_headers_p} { - set headers [list] - foreach {key value} $selection_array { - lappend headers $key - } - lappend result $headers - } - set row [list] - foreach {key value} $selection_array { - lappend row $value - } - lappend result $row - } - set result - } - if { [info exists cache_key] } { - return [ns_cache eval $cache_pool $cache_key $code] - } else { - return [eval $code] - } -} - - -ad_proc -public db_list_of_ns_sets { - {-dbn ""} - statement_name - sql - args -} { - Usage: db_list_of_ns_sets statement-name sql [ -bind bind_set_id | -bind bind_value_list ] - - @return a list of ns_sets with the values of each column of each row - returned by the sql query specified. - - @param statement_name The name of the query. - @param sql The SQL to be executed. - @param args Any additional arguments. - - @return list of ns_sets, one per each row return by the SQL query - - @param dbn The database name to use. If empty_string, uses the default database. -} { - ad_arg_parser { bind } $args - - set full_statement_name [db_qd_get_fullname $statement_name] - - db_with_handle -dbn $dbn db { - set result [list] - set selection [db_exec select $db $full_statement_name $sql] - - while {[db_getrow $db $selection]} { - lappend result [ns_set copy $selection] - } - } - - return $result -} - - -ad_proc -public db_foreach { - {-dbn ""} - statement_name - sql - args -} { - - Usage: -
- db_foreach statement-name sql [ -bind bind_set_id | -bind bind_value_list ] \ - [ -column_array array_name | -column_set set_name ] \ - code_block [ if_no_rows if_no_rows_block ] - -
- -

Performs the SQL query sql, executing - code_block once for each row with variables set to - column values (or a set or array populated if -column_array or - column_set is specified). If the query returns no rows, executes - if_no_rows_block (if provided). In place of 'if_no_rows' also the 'else' keyword can be used.

- -

Example: - -

db_foreach greeble_query "select foo, bar from greeble" {
-        ns_write "<li>foo=$foo; bar=$bar\n"
-    } if_no_rows {
-        # This block is optional.
-        ns_write "<li>No greebles!\n"
-    }
- - @param dbn The database name to use. If empty_string, uses the default database. -} { - ad_arg_parser { bind column_array column_set args } $args - - # Do some syntax checking. - set arglength [llength $args] - if { $arglength == 1 } { - # Have only a code block. - set code_block [lindex $args 0] - } elseif { $arglength == 3 } { - # Should have code block + if_no_rows + code block. - if { [lindex $args 1] ni {"if_no_rows" "else"}} { - return -code error "Expected if_no_rows or else as second-to-last argument" - } - lassign $args code_block . if_no_rows_code_block - } else { - return -code error "Expected 1 or 3 arguments after switches" - } - - if { [info exists column_array] && [info exists column_set] } { - return -code error "Can't specify both column_array and column_set" - } - - if { [info exists column_array] } { - upvar 1 $column_array array_val - } - - if { [info exists column_set] } { - upvar 1 $column_set selection - } - - set counter 0 - foreach selection [uplevel [list db_list_of_ns_sets -dbn $dbn $statement_name $sql]] { - incr counter - if { ![info exists column_set] } { - set set_array [ns_set array $selection] - if { [info exists column_array] } { - unset -nocomplain array_val - array set array_val $set_array - } else { - foreach {a v} $set_array { uplevel [list set $a $v] } - } - } - set errno [catch { uplevel 1 $code_block } error] - - # - # Handle or propagate the error. - # - switch -- $errno { - 0 { - # TCL_OK - } - 1 { - # TCL_ERROR - error $error $::errorInfo $::errorCode - } - 2 { - # TCL_RETURN - error "Cannot return from inside a db_foreach loop" - } - 3 { - # TCL_BREAK - break - } - 4 { - # TCL_CONTINUE - just ignore and continue looping. - } - default { - error "Unknown return code: $errno" - } - } - } - # If the if_no_rows_code is defined, go ahead and run it. - if { $counter == 0 && [info exists if_no_rows_code_block] } { - uplevel 1 $if_no_rows_code_block - } -} - - -proc db_multirow_helper {} { - uplevel 1 { - if { !$append_p || ![info exists counter]} { - set counter 0 - } - - db_with_handle -dbn $dbn db { - set selection [db_exec select $db $full_statement_name $sql] - set local_counter 0 - - # Make sure 'next_row' array doesn't exist - # The this_row and next_row variables are used to always execute the code block one result set row behind, - # so that we have the opportunity to peek ahead, which allows us to do group by's inside - # the multirow generation - # Also make the 'next_row' array available as a magic __db_multirow__next_row variable - upvar 1 __db_multirow__next_row next_row - unset -nocomplain next_row - - set more_rows_p 1 - while { 1 } { - - if { $more_rows_p } { - set more_rows_p [db_getrow $db $selection] - } else { - break - } - - # Setup the 'columns' part, now that we know the columns in the result set - # And save variables which we might clobber, if '-unclobber' switch is specified. - if { $local_counter == 0 } { - for { set i 0 } { $i < [ns_set size $selection] } { incr i } { - lappend local_columns [ns_set key $selection $i] - } - lappend local_columns {*}$extend - if { !$append_p || ![info exists columns] } { - # store the list of columns in the var_name:columns variable - set columns $local_columns - } else { - # Check that the columns match, if not throw an error - if { [join [lsort -ascii $local_columns]] ne [join [lsort -ascii $columns]] } { - error "Appending to a multirow with differing columns. - Original columns : [join [lsort -ascii $columns] ", "]. - Columns in this query: [join [lsort -ascii $local_columns] ", "]" "" "ACS_MULTIROW_APPEND_COLUMNS_MISMATCH" - } - } - - # Save values of columns which we might clobber - if { $unclobber_p && $code_block ne "" } { - foreach col $columns { - upvar 1 $col column_value __saved_$col column_save - - if { [info exists column_value] } { - if { [array exists column_value] } { - array set column_save [array get column_value] - } else { - set column_save $column_value - } - - # Clear the variable - unset column_value - } - } - } - } - - if { $code_block eq "" } { - # No code block - pull values directly into the var_name array. - - # The extra loop after the last row is only for when there's a code block - if { !$more_rows_p } { - break - } - incr counter - upvar $level_up "$var_name:$counter" array_val - set array_val(rownum) $counter - for { set i 0 } { $i < [ns_set size $selection] } { incr i } { - set array_val([ns_set key $selection $i]) \ - [ns_set value $selection $i] - } - } else { - # There is a code block to execute - - # Copy next_row to this_row, if it exists - unset -nocomplain this_row - set array_get_next_row [array get next_row] - if { $array_get_next_row ne "" } { - array set this_row [array get next_row] - } - - # Pull values from the query into next_row - unset -nocomplain next_row - if { $more_rows_p } { - for { set i 0 } { $i < [ns_set size $selection] } { incr i } { - set next_row([ns_set key $selection $i]) [ns_set value $selection $i] - } - } - - # Process the row - if { [info exists this_row] } { - # Pull values from this_row into local variables - foreach name [array names this_row] { - upvar 1 $name column_value - set column_value $this_row($name) - } - - # Initialize the "extend" columns to the empty string - foreach column_name $extend { - upvar 1 $column_name column_value - set column_value "" - } - - # Execute the code block - set errno [catch { uplevel 1 $code_block } error] - - # Handle or propagate the error. Can't use the usual - # "return -code $errno..." trick due to the db_with_handle - # wrapped around this loop, so propagate it explicitly. - switch -- $errno { - 0 { - # TCL_OK - } - 1 { - # TCL_ERROR - error $error $::errorInfo $::errorCode - } - 2 { - # TCL_RETURN - error "Cannot return from inside a db_multirow loop" - } - 3 { - # TCL_BREAK - ns_db flush $db - break - } - 4 { - # TCL_CONTINUE - continue - } - default { - error "Unknown return code: $errno" - } - } - - # Pull the local variables back out and into the array. - incr counter - upvar $level_up "$var_name:$counter" array_val - set array_val(rownum) $counter - foreach column_name $columns { - upvar 1 $column_name column_value - set array_val($column_name) $column_value - } - } - } - incr local_counter - } - } - - # Restore values of columns which we've saved - if { $unclobber_p && $code_block ne "" && $local_counter > 0 } { - foreach col $columns { - upvar 1 $col column_value __saved_$col column_save - - # Unset it first, so the road's paved to restoring - unset -nocomplain column_value - - # Restore it - if { [info exists column_save] } { - if { [array exists column_save] } { - array set column_value [array get column_save] - } else { - set column_value $column_save - } - - # And then remove the saved col - unset column_save - } - } - } - # Unset the next_row variable, just in case - unset -nocomplain next_row - } -} - -ad_proc -public db_multirow { - -local:boolean - -append:boolean - {-upvar_level 1} - -unclobber:boolean - {-extend {}} - {-dbn ""} - -cache_key - {-cache_pool db_cache_pool} - var_name - statement_name - sql - args -} { - @param dbn The database name to use. If empty_string, uses the default database. - @param cache_key Cache the result using given value as the key. Default is to not cache. - @param cache_pool Override the default db_cache_pool - - @param unclobber If set, will cause the proc to not overwrite local variables. Actually, what happens - is that the local variables will be overwritten, so you can access them within the code block. However, - if you specify -unclobber, we will revert them to their original state after execution of this proc. - - Usage: -
- db_multirow [ -local ] [ -upvar_level n_levels_up ] [ -append ] [ -extend column_list ] \ - var-name statement-name sql [ -bind bind_set_id | -bind bind_value_list ] \ - code_block [ if_no_rows if_no_rows_block ] - -
- -

Performs the SQL query sql, saving results in variables - of the form - var_name:1, var_name:2, etc, - setting var_name:rowcount to the total number - of rows, and setting var_name:columns to a - list of column names. - -

- - If "cache_key" is set, cache the array that results from the query *and* - any code block for future use. When this result is returned from cache, - THE CODE BLOCK IS NOT EXECUTED. Therefore any values calculated by the - code block that aren't listed as arguments to "extend" will - not be created. In practice this impacts relatively few queries, but do - take care. - -

- - You can not simultaneously append to and cache a non-empty multirow. - -

- - Each row also has a column, rownum, automatically - added and set to the row number, starting with 1. Note that this will - override any column in the SQL statement named 'rownum', also if you're - using the Oracle rownum pseudo-column. - -

- - If the -local is passed, the variables defined - by db_multirow will be set locally (useful if you're compiling dynamic templates - in a function or similar situations). Use the -upvar_level - switch to specify how many levels up the variable should be set. - -

- - You may supply a code block, which will be executed for each row in - the loop. This is very useful if you need to make computations that - are better done in Tcl than in SQL, for example using ns_urlencode - or ad_quotehtml, etc. When the Tcl code is executed, all the columns - from the SQL query will be set as local variables in that code. Any - changes made to these local variables will be copied back into the - multirow. - -

- - You may also add additional, computed columns to the multirow, using the - -extend { col_1 col_2 ... } switch. This is - useful for things like constructing a URL for the object retrieved by - the query. - -

- - If you're constructing your multirow through multiple queries with the - same set of columns, but with different rows, you can use the - -append switch. This causes the rows returned by this query - to be appended to the rows already in the multirow, instead of starting - a clean multirow, as is the normal behavior. The columns must match the - columns in the original multirow, or an error will be thrown. - -

- - Your code block may call continue in order to skip a row - and not include it in the multirow. Or you can call break - to skip this row and quit looping. - -

- - Notice the nonstandard numbering (everything - else in Tcl starts at 0); the reason is that the graphics designer, a non - programmer, may wish to work with row numbers. - -

- - Example: -

db_multirow -extend { user_url } users users_query {
-        select user_id first_names, last_name, email from cc_users
-    } {
-        set user_url [acs_community_member_url -user_id $user_id]
-    }
- - @see template::multirow -} { - # Query Dispatcher (OpenACS - ben) - set full_statement_name [db_qd_get_fullname $statement_name] - - if { $local_p } { - set level_up $upvar_level - } else { - set level_up \#[template::adp_level] - } - - ad_arg_parser { bind args } $args - - # Do some syntax checking. - set arglength [llength $args] - if { $arglength == 0 } { - # No code block. - set code_block "" - } elseif { $arglength == 1 } { - # Have only a code block. - set code_block [lindex $args 0] - } elseif { $arglength == 3 } { - # Should have code block + if_no_rows + code block. - if { [lindex $args 1] ne "if_no_rows" - && [lindex $args 1] ne "else" - } { - return -code error "Expected if_no_rows as second-to-last argument" - } - lassign $args code_block . if_no_rows_code_block - } else { - return -code error "Expected 1 or 3 arguments after switches" - } - - upvar $level_up "$var_name:rowcount" counter - upvar $level_up "$var_name:columns" columns - - if { [info exists cache_key] - && $append_p - && [info exists counter] && $counter > 0 - } { - return -code error "Can't append and cache a non-empty multirow datasource simultaneously" - } - - if { [info exists cache_key] } { - - set value [ns_cache eval $cache_pool $cache_key { - db_multirow_helper - - set values [list] - - for { set count 1 } { $count <= $counter } { incr count } { - upvar $level_up "$var_name:[expr {$count}]" array_val - lappend values [array get array_val] - } - - return [list $counter $columns $values] - }] - - lassign $value counter columns values - - set count 1 - foreach value $values { - upvar $level_up "$var_name:[expr {$count}]" array_val - array set array_val $value - incr count - } - } else { - db_multirow_helper - } - - - # If the if_no_rows_code is defined, go ahead and run it. - if { $counter == 0 && [info exists if_no_rows_code_block] } { - uplevel 1 $if_no_rows_code_block - } -} - -ad_proc -public db_multirow_group_last_row_p { - {-column:required} -} { - Used inside the code_block to db_multirow to ask whether this row is the last row - before the value of 'column' changes, or the last row of the result set. - -

- - This is useful when you want to build up a multirow for a master/slave table pair, - where you only want one row per row in the master table, but you want to include - data from the slave table in a column of the multirow. - -

- - Here's an example: - -

-    # Initialize the lines variable to hold a list of order line summaries
-    set lines [list]
-
-    # Start building the multirow. We add the dynamic column 'lines_pretty', which will
-    # contain the pretty summary of the order lines.
-    db_multirow -extend { lines_pretty } orders select_orders_and_lines {
-        select o.order_id,
-        o.customer_name,
-        l.item_name,
-        l.quantity
-        from   orders o,
-        order_lines l
-        where  l.order_id = o.order_id
-        order  by o.order_id, l.item_name
-    } {
-        lappend lines "$quantity $item_name"
-        if { [db_multirow_group_last_row_p -column order_id] } {
-            # Last row of this order, prepare the pretty version of the order lines
-            set lines_pretty [join $lines ", "]
-
-            # Reset the lines list, so we start from a fresh with the next row
-            set lines [list]
-        } else {
-            # There are yet more order lines to come for this order,
-            # continue until we've collected all the order lines
-            # The 'continue' keyword means this line will not be added to the resulting multirow
-            continue
-        }
-    }
-    
- - @author Lars Pind (lars@collaboraid.biz) - - @param column The name of the column defining the groups. - - @return 1 if this is the last row before the column value changes, 0 otherwise. -} { - upvar 1 __db_multirow__next_row next_row - if { ![info exists next_row] } { - # If there is no next row, this is the last row - return 1 - } - upvar 1 $column column_value - # Otherwise, it's the last row in the group if the next row has a different value than this row - return [expr {$column_value ne $next_row($column) }] -} - - -ad_proc -public db_dml {{-dbn ""} statement_name sql args } { - Do a DML statement. - -

- - args can be one of: -clobs, -blobs, -clob_files or -blob_files. See the db-api doc referenced below for more information. - - @param dbn The database name to use. If empty_string, uses the default database. - - @see /doc/db-api-detailed -} { - ad_arg_parser { clobs blobs clob_files blob_files bind } $args - set driverkey [db_driverkey $dbn] - - switch -- $driverkey { - postgresql { - set postgres_p 1 - } - oracle - - nsodbc - - default { - set postgres_p 0 - } - } - - # Query Dispatcher (OpenACS - ben) - set full_statement_name [db_qd_get_fullname $statement_name] - - # This "only one of..." check didn't exist in the PostgreSQL - # version, but it shouldn't't hurt anything: --atp@piskorski.com, - # 2003/04/08 06:19 EDT - - # Only one of clobs, blobs, clob_files, and blob_files is allowed. - # Remember which one (if any) is provided: - - set lob_argc 0 - set lob_argv [list] - set command "dml" - if { [info exists clobs] } { - set command "clob_dml" - set lob_argv $clobs - incr lob_argc - } - if { [info exists blobs] } { - set command "blob_dml" - set lob_argv $blobs - incr lob_argc - } - if { [info exists clob_files] } { - set command "clob_dml_file" - set lob_argv $clob_files - incr lob_argc - } - if { [info exists blob_files] } { - set command "blob_dml_file" - set lob_argv $blob_files - incr lob_argc - } - if { $lob_argc > 1 } { - error "Only one of -clobs, -blobs, -clob_files, or -blob_files may be specified as an argument to db_dml" - } - - if { ! $postgres_p } { - # Oracle: - db_with_handle -dbn $dbn db { - if { $lob_argc == 1 } { - # Bind :1, :2, ..., :n as LOBs (where n = [llength $lob_argv]) - set bind_vars [list] - for { set i 1 } { $i <= [llength $lob_argv] } { incr i } { - lappend bind_vars $i - } - eval [list db_exec "${command}_bind" $db $full_statement_name $sql 2 $bind_vars] $lob_argv - } else { - eval [list db_exec $command $db $full_statement_name $sql] $lob_argv - } - } - - } elseif {$command eq "blob_dml_file"} { - # PostgreSQL: - db_with_handle -dbn $dbn db { - # another ugly hack to avoid munging Tcl files. - # __lob_id needs to be set inside of a query (.xql) file for this - # to work. Say for example that you need to create a lob. In - # Oracle, you would do something like: - - # db_dml update_photo "update foo set bar = empty_blob() - # where bar = :bar - # returning foo into :1" -blob_files [list $file] - # for PostgreSQL we can do the equivalent by placing the following - # in a query file: - # update foo set bar = [set __lob_id [db_string get_id "select empty_lob()"]] - # where bar = :bar - - # __lob_id acts as a flag that signals that blob_dml_file is - # required, and it is also used to pass along the lob_id. It - # is unsert afterwards to avoid name clashes with other invocations - # of this routine. - # (DanW - Openacs) - - db_exec dml $db $full_statement_name $sql - if {[uplevel {info exists __lob_id}]} { - ns_pg blob_dml_file $db [uplevel {set __lob_id}] $blob_files - uplevel {unset __lob_id} - } - } - - } else { - # PostgreSQL: - db_with_handle -dbn $dbn db { - db_exec dml $db $full_statement_name $sql - } - } -} - - - - -ad_proc -public db_0or1row { - {-dbn ""} - -cache_key - {-cache_pool db_cache_pool} - statement_name - sql - args -} { - - Usage: -

- db_0or1row statement-name sql [ -bind bind_set_id | -bind bind_value_list ] \ - [ -column_array array_name | -column_set set_name ] - -
- -

Performs the SQL query sql. If a row is returned, sets variables - to column values (or a set or array populated if -column_array - or column_set is specified) and returns 1. If no rows are returned, - returns 0. - - @return 1 if variables are set, 0 if no rows are returned. If more than one row is returned, throws an error. - - @param dbn The database name to use. If empty_string, uses the default database. - @param cache_key Cache the result using given value as the key. Default is to not cache. - @param cache_pool Override the default db_cache_pool -} { - ad_arg_parser { bind column_array column_set } $args - - # Query Dispatcher (OpenACS - ben) - set full_statement_name [db_qd_get_fullname $statement_name] - - if { [info exists column_array] && [info exists column_set] } { - return -code error "Can't specify both column_array and column_set" - } - - if { [info exists column_array] } { - upvar 1 $column_array array_val - unset -nocomplain array_val - } - - if { [info exists column_set] } { - upvar 1 $column_set selection - } - - if { [info exists cache_key] } { - set values [ns_cache eval $cache_pool $cache_key { - db_with_handle -dbn $dbn db { - set selection [db_exec 0or1row $db $full_statement_name $sql] - } - - set values [list] - - if { $selection ne "" } { - for {set i 0} { $i < [ns_set size $selection] } {incr i} { - lappend values [list [ns_set key $selection $i] [ns_set value $selection $i]] - } - } - - set values - }] - - if { $values eq "" } { - set selection "" - } else { - set selection [ns_set create] - - foreach value $values { - ns_set put $selection [lindex $value 0] [lindex $value 1] - } - } - } else { - db_with_handle -dbn $dbn db { - set selection [db_exec 0or1row $db $full_statement_name $sql] - } - } - - if { $selection eq "" } { - return 0 - } - - if { [info exists column_array] } { - array set array_val [ns_set array $selection] - } elseif { ![info exists column_set] } { - for { set i 0 } { $i < [ns_set size $selection] } { incr i } { - uplevel 1 [list set [ns_set key $selection $i] [ns_set value $selection $i]] - } - } - - return 1 -} - - -ad_proc -public db_1row { args } { - - A wrapper for db_0or1row, which produces an error if no rows are returned. - - @param args Arguments to be passed to db_0or1row. Check db_0or1row proc doc - for details. - - @see db_0or1row - - @return 1 if variables are set. - -} { - if { ![uplevel ::db_0or1row $args] } { - return -code error "Query did not return any rows." - } -} - -if {[info commands ns_cache_transaction_begin] eq ""} { - # - # When the server has no support for ns_cache_transaction_*, - # provide dummy procs to avoid runtime "if" statements. - # - proc ns_cache_transaction_begin args {;} - proc ns_cache_transaction_commit args {;} - proc ns_cache_transaction_rollback args {;} -} - -ad_proc -public db_transaction {{ -dbn ""} transaction_code args } { - Usage: db_transaction transaction_code [ on_error { error_code_block } ] - - Executes transaction_code with transactional semantics. This means that either all of the database commands - within transaction_code are committed to the database or none of them are. Multiple db_transactions may be - nested (end transaction is transparently ns_db dml'ed when the outermost transaction completes).

- - To handle errors, use db_transaction {transaction_code} on_error {error_code_block}. Any error generated in - transaction_code will be caught automatically and process control will transfer to error_code_block - with a variable errmsg set. The error_code block can then clean up after the error, such as presenting a usable - error message to the user. Following the execution of error_code_block the transaction will be aborted. - If you want to explicitly abort the transaction, call db_abort_transaction - from within the transaction_code block or the error_code block.

- - Example 1:
- In this example, db_dml triggers an error, so control passes to the on_error block which prints a readable error. -

-    db_transaction {
-        db_dml test "nonsense"
-    } on_error {
-        ad_return_error "Error in blah/foo/bar" "The error was: $errmsg"
-    }
-    
- - Example 2:
- In this example, the second command, "nonsense" triggers an error. There is no on_error block, so the - transaction is immediately halted and aborted. -
-    db_transaction {
-        db_dml test {insert into footest values(1)}
-        nonsense
-        db_dml test {insert into footest values(2)}
-    }
-    
- - @param dbn The database name to use. If empty_string, uses the default database. -} { - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - set syn_err "db_transaction: Invalid arguments. Use db_transaction { code } \[on_error { error_code_block }\] " - set arg_c [llength $args] - - if { $arg_c != 0 && $arg_c != 2 } { - # Either this is a transaction with no error handling or there must be an on_error { code } block. - error $syn_err - } elseif { $arg_c == 2 } { - # We think they're specifying an on_error block - if {[lindex $args 0] ne "on_error" } { - # Unexpected: they put something besides on_error as a connector. - error $syn_err - } else { - # Success! We got an on_error code block. - set on_error [lindex $args 1] - } - } - # Make the error message and database handle available to the on_error block. - upvar errmsg errmsg - - db_with_handle -dbn $dbn db { - # Preserve the handle, since db_with_handle kills it after executing - # this block. - set dbh $db - # Remember that there's a transaction happening on this handle. - if { ![info exists db_state(transaction_level,$dbh)] } { - set db_state(transaction_level,$dbh) 0 - } - set level [incr db_state(transaction_level,$dbh)] - if { $level == 1 } { - ns_db dml $dbh "begin transaction" - ns_cache_transaction_begin - } - } - # Execute the transaction code. - set errno [catch { - uplevel 1 $transaction_code - } errmsg] - incr db_state(transaction_level,$dbh) -1 - - set err_p 0 - switch -- $errno { - 0 { - # TCL_OK - } - 2 { - # TCL_RETURN - } - 3 { - # TCL_BREAK - Abort the transaction and do the break. - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - db_release_unused_handles -dbn $dbn - break - } - 4 { - # TCL_CONTINUE - just ignore. - } - default { - # TCL_ERROR or unknown error code: Its a real error. - set err_p 1 - } - } - - if { $err_p || [db_abort_transaction_p -dbn $dbn]} { - # An error was triggered or the transaction has been aborted. - db_abort_transaction -dbn $dbn - if { [info exists on_error] && $on_error ne "" } { - - if {"postgresql" eq [db_type]} { - - # JCD: with postgres we abort the transaction prior to - # executing the on_error block since there is nothing - # you can do to "fix it" and keeping it meant things like - # queries in the on_error block would then fail. - # - # Note that the semantics described in the proc doc - # are not possible to support on PostgreSQL. - - # DRB: I removed the db_release_unused_handles call that - # this patch included because additional aborts further - # down triggered an illegal db handle error. I'm going to - # have the code start a new transaction as well. If we - # don't, if a transaction fails and the on_error block - # fails, the on_error block DML will have been committed. - # Starting a new transaction here means that DML by both - # the transaction and on_error clause will be rolled back. - # On the other hand, if the on_error clause doesn't fail, - # any DML in that block will be committed. This seems more - # useful than simply punting ... - - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - ns_db dml $dbh "begin transaction" - ns_cache_transaction_begin - - } - - # An on_error block exists, so execute it. - - set errno [catch { - uplevel 1 $on_error - } on_errmsg] - - # Determine what do with the error. - set err_p 0 - switch -- $errno { - 0 { - # TCL_OK - } - - 2 { - # TCL_RETURN - } - 3 { - # TCL_BREAK - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - db_release_unused_handles - break - } - 4 { - # TCL_CONTINUE - just ignore. - } - default { - # TCL_ERROR or unknown error code: Its a real error. - set err_p 1 - } - } - - if { $err_p } { - # An error was generated from the $on_error block. - if { $level == 1} { - # We're at the top level, so we abort the transaction. - set db_state(db_abort_p,$dbh) 0 - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - } - # We throw this error because it was thrown from the error handling code that the programmer must fix. - error $on_errmsg $::errorInfo $::errorCode - } else { - # Good, no error thrown by the on_error block. - if { [db_abort_transaction_p -dbn $dbn] } { - # This means we should abort the transaction. - if { $level == 1 } { - set db_state(db_abort_p,$dbh) 0 - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - # We still have the transaction generated error. We don't want to throw it, so we log it. - ns_log Error "Aborting transaction due to error:\n$errmsg" - } else { - # Propagate the error up to the next level. - error $errmsg $::errorInfo $::errorCode - } - } else { - # The on_error block has resolved the transaction error. If we're at the top, commit and exit. - # Otherwise, we continue on through the lower transaction levels. - if { $level == 1} { - ns_db dml $dbh "end transaction" - ns_cache_transaction_commit - } - } - } - } else { - # There is no on_error block, yet there is an error, so we propagate it. - if { $level == 1 } { - set db_state(db_abort_p,$dbh) 0 - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - error "Transaction aborted: $errmsg" $::errorInfo $::errorCode - } else { - db_abort_transaction -dbn $dbn - error $errmsg $::errorInfo $::errorCode - } - } - } else { - # There was no error from the transaction code. - if { [db_abort_transaction_p -dbn $dbn] } { - # The user requested the transaction be aborted. - if { $level == 1 } { - set db_state(db_abort_p,$dbh) 0 - ns_db dml $dbh "abort transaction" - ns_cache_transaction_rollback - } - } elseif { $level == 1 } { - # Success! No errors and no requested abort. Commit. - ns_db dml $dbh "end transaction" - ns_cache_transaction_commit - } - } -} - - -ad_proc -public db_abort_transaction {{-dbn ""}} { - - Aborts all levels of a transaction. That is if this is called within - several nested transactions, all of them are terminated. Use this - instead of db_dml "abort" "abort transaction". - - @param dbn The database name to use. If empty_string, uses the default database. -} { - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - db_with_handle -dbn $dbn db { - # We set the abort flag to true. - set db_state(db_abort_p,$db) 1 - } -} - - -ad_proc -private db_abort_transaction_p {{-dbn ""}} { - @param dbn The database name to use. If empty_string, uses the default database. -} { - upvar "#0" [db_state_array_name_is -dbn $dbn] db_state - - db_with_handle -dbn $dbn db { - if { [info exists db_state(db_abort_p,$db)] } { - return $db_state(db_abort_p,$db) - } else { - # No abort flag registered, so we assume everything is ok. - return 0 - } - } -} - - -ad_proc -public db_name {{-dbn ""}} { - - @return the name of the database as reported by the driver. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - db_with_handle -dbn $dbn db { - set dbtype [ns_db dbtype $db] - } - return $dbtype -} - - -ad_proc -public db_get_username {{-dbn ""}} { - @return the username parameter from the driver section of the - first database pool for the dbn. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - return [ns_config "ns/db/pool/$pool" User] -} - -ad_proc -public db_get_password {{-dbn ""}} { - @return the password parameter from the driver section of the - first database pool for the dbn. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - return [ns_config "ns/db/pool/$pool" Password] -} - -ad_proc -public db_get_sql_user {{-dbn ""}} { - Oracle only. - -

- @return a valid Oracle user@database/password string to access a - database through sqlplus. - -

- This proc may well work for databases other than Oracle, - but its return value won't really be of any use. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - set datasource [ns_config "ns/db/pool/$pool" DataSource] - if { $datasource ne "" && ![string is space $datasource] } { - return "[ns_config ns/db/pool/$pool User]/[ns_config ns/db/pool/$pool Password]@$datasource" - } else { - return "[ns_config ns/db/pool/$pool User]/[ns_config ns/db/pool/$pool Password]" - } -} - -ad_proc -public db_get_pgbin {{-dbn ""}} { - PostgreSQL only. - -

- @return the pgbin parameter from the driver section of the first database pool. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - set driver [ns_config "ns/db/pool/$pool" Driver] - return [ns_config "ns/db/driver/$driver" pgbin] -} - - -ad_proc -public db_get_port {{-dbn ""}} { - PostgreSQL only. - -

- @return the port number from the first database pool. It assumes the - datasource is properly formatted since we've already verified that we - can connect to the pool. - It returns an empty string for an empty port value. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - set datasource [ns_config "ns/db/pool/$pool" DataSource] - set last_colon_pos [string last ":" $datasource] - if { $last_colon_pos == -1 } { - ns_log Error "datasource contains no \":\"? datasource = $datasource" - return "" - } - set first_colon_pos [string first ":" $datasource] - - if { $first_colon_pos == $last_colon_pos || ($last_colon_pos - $first_colon_pos) == 1 } { - # No port specified - return "" - } - - return [string range $datasource $first_colon_pos+1 $last_colon_pos-1] -} - - -ad_proc -public db_get_database {{-dbn ""}} { - PostgreSQL only. - -

- @return the database name from the first database pool. It assumes the - datasource is properly formatted since we've already verified that we - can connect to the pool. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - set datasource [ns_config "ns/db/pool/$pool" DataSource] - set last_colon_pos [string last ":" $datasource] - if { $last_colon_pos == -1 } { - ns_log Error "datasource contains no \":\"? datasource = $datasource" - return "" - } - return [string range $datasource $last_colon_pos+1 end] -} - - -ad_proc -public db_get_dbhost { - {-dbn ""} -} { - PostgreSQL only. - -

- @return the name of the database host from the first database pool. - It assumes the datasource is properly formatted since we've already - verified that we can connect to the pool. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set pool [lindex [db_available_pools $dbn] 0] - set datasource [ns_config "ns/db/pool/$pool" DataSource] - set first_colon_pos [string first ":" $datasource] - if { $first_colon_pos == -1 } { - ns_log Error "datasource contains no \":\"? datasource = $datasource" - return "" - } - return [string range $datasource 0 $first_colon_pos-1] -} - -ad_proc -public db_source_sql_file { - {-dbn ""} - {-callback apm_ns_write_callback} - file -} { - Sources a SQL file into Oracle (SQL*Plus format file) or - PostgreSQL (psql format file). - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set proc_name {db_source_sql_file} - set driverkey [db_driverkey $dbn] - - switch -- $driverkey { - - oracle { - set user_pass [db_get_sql_user -dbn $dbn] - cd [file dirname $file] - set fp [open "|[file join $::env(ORACLE_HOME) bin sqlplus] $user_pass @$file" "r+"] - fconfigure $fp -buffering line - puts $fp "exit" - - while { [gets $fp line] >= 0 } { - # Don't bother writing out lines which are purely whitespace. - if { ![string is space $line] } { - apm_callback_and_log $callback "[ns_quotehtml $line]\n" - } - } - close $fp - } - - postgresql { - set file_name [file tail $file] - - set pguser [db_get_username] - if { $pguser ne "" } { - set pguser "-U $pguser" - } - - set pgport [db_get_port] - if { $pgport ne "" } { - set pgport "-p $pgport" - } - - set pgpass [db_get_password] - if { $pgpass ne "" } { - set pgpass "<<$pgpass" - } - - # DRB: Submitted patch was in error - the driver opens a -h hostname connection - # unless the hostname is localhost. We need to do the same here. The submitted - # patch checked for a blank hostname, which fails in the driver. Arguably the - # driver's wrong but a lot of non-OpenACS folks use it, and even though I'm the - # maintainer we shouldn't break existing code over such trivialities... - # GN: windows requires $pghost "-h ..." - - if { ([db_get_dbhost] eq "localhost" || [db_get_dbhost] eq "") - && $::tcl_platform(platform) ne "windows" - } { - set pghost "" - } else { - set pghost "-h [db_get_dbhost]" - } - - set errno [catch { - cd [file dirname $file] - set fp [open "|[file join [db_get_pgbin] psql] $pghost $pgport $pguser -f $file [db_get_database] $pgpass" "r"] - } errorMsg] - - if {$errno > 0} { - set error_found 1 - set error_lines $errorMsg - } else { - while { [gets $fp line] >= 0 } { - # Don't bother writing out lines which are purely whitespace. - if { ![string is space $line] } { - apm_callback_and_log $callback "[ns_quotehtml $line]\n" - } - } - - # PSQL dumps errors and notice information on stderr, and has no option to turn - # this off. So we have to chug through the "error" lines looking for those that - # really signal an error. - - set errno [ catch { - close $fp - } error] - - if { $errno == 2 } { - return $error - } - - # Just filter out the "NOTICE" lines, so we get the stack dump along with real - # ERRORs. This could be done with a couple of opaque-looking regexps... - - set error_found 0 - foreach line [split $error "\n"] { - if { [string first NOTICE $line] == -1 } { - append error_lines "$line\n" - set error_found [expr { $error_found - || [string first ERROR $line] != -1 - || [string first FATAL $line] != -1 } ] - } - } - } - - if { $error_found } { - return -code error -errorinfo $error_lines -errorcode $::errorCode $error_lines - } - - } - - nsodbc { - error "$proc_name is not supported for this database." - } - default { - error "$proc_name is not supported for this database." - } - } -} - -ad_proc -public db_load_sql_data { - {-dbn ""} - {-callback apm_ns_write_callback} - file -} { - Loads a CSV formatted file into a table using PostgreSQL's COPY command or - Oracle's SQL*Loader utility. The file name format consists of a sequence - number used to control the order in which tables are loaded, and the table - name with "-" replacing "_". This is a bit of a kludge but greatly speeds - the loading of large amounts of data, such as is done when various "ref-*" - packages are installed. - - @param dbn The database name to use. If empty_string, uses the default database. - @param file Filename in the format dd-table-name.ctl where 'dd' is a sequence number - used to control the order in which data is loaded. This file is an - RDBMS-specific data loader control file. - -} { - - switch [db_driverkey $dbn] { - - oracle { - set user_pass [db_get_sql_user -dbn $dbn] - set tmpnam [ad_tmpnam] - - set fd [open $file r] - set file_contents [read $fd] - close $fd - - set file_contents [subst $file_contents] - - set fd1 [open "${tmpnam}.ctl" w] - puts $fd1 $file_contents - close $fd1 - - cd [file dirname $file] - - set fd [open "|[file join $::env(ORACLE_HOME) bin sqlldr] userid=$user_pass control=$tmpnam" "r"] - - while { [gets $fd line] >= 0 } { - # Don't bother writing out lines which are purely whitespace. - if { ![string is space $line] } { - apm_callback_and_log $callback "[ns_quotehtml $line]\n" - } - } - close $fd - } - - postgresql { - set pguser [db_get_username] - if { $pguser ne "" } { - set pguser "-U $pguser" - } - - set pgport [db_get_port] - if { $pgport ne "" } { - set pgport "-p $pgport" - } - - set pgpass [db_get_password] - if { $pgpass ne "" } { - set pgpass "<<$pgpass" - } - - if { [db_get_dbhost] eq "localhost" || [db_get_dbhost] eq "" } { - set pghost "" - } else { - set pghost "-h [db_get_dbhost]" - } - - set fd [open $file r] - set copy_command [subst -nobackslashes [read $fd]] - close $fd - set copy_file [ns_mktemp [ad_tmpdir]/psql-copyfile-XXXXXX] - set fd [open $copy_file "CREAT EXCL WRONLY" 0600] - puts $fd $copy_command - close $fd - - if { $::tcl_platform(platform) eq "windows" } { - set fp [open "|[file join [db_get_pgbin] psql] -f $copy_file $pghost $pgport $pguser [db_get_database]" "r"] - } else { - set fp [open "|[file join [db_get_pgbin] psql] -f $copy_file $pghost $pgport $pguser [db_get_database] $pgpass" "r"] - } - - while { [gets $fp line] >= 0 } { - # Don't bother writing out lines which are purely whitespace. - if { ![string is space $line] } { - apm_callback_and_log $callback "[ns_quotehtml $line]\n" - } - } - - # PSQL dumps errors and notice information on stderr, and has no option to turn - # this off. So we have to chug through the "error" lines looking for those that - # really signal an error. - - set errno [ catch { - close $fp - } error] - - # remove the copy file. - file delete -force -- $copy_file - - if { $errno == 2 } { - return $error - } - - # Just filter out the "NOTICE" lines, so we get the stack dump along with real - # ERRORs. This could be done with a couple of opaque-looking regexps... - - set error_found 0 - foreach line [split $error "\n"] { - if { [string first NOTICE $line] == -1 } { - append error_lines "$line\n" - set error_found [expr { $error_found - || [string first ERROR $line] != -1 - || [string first FATAL $line] != -1 } ] - } - } - - if { $error_found } { - return -code error -errorinfo $error_lines -errorcode $::errorCode $error_lines - } - - } - - nsodbc { - error "db_load_sql_data is not supported for this database." - } - default { - error "db_load_sql_data is not supported for this database." - } - } -} - -ad_proc -public db_source_sqlj_file { - {-dbn ""} - {-callback apm_ns_write_callback} - file -} { - Oracle only. -

- Sources a SQLJ file using loadjava. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - set user_pass [db_get_sql_user -dbn $dbn] - set fp [open "|[file join $::env(ORACLE_HOME) bin loadjava] -verbose -user $user_pass $file" "r"] - - # Despite the fact that this works, the text does not get written to the stream. - # The output is generated as an error when you attempt to close the input stream as - # done below. - while { [gets $fp line] >= 0 } { - # Don't bother writing out lines which are purely whitespace. - if { ![string is space $line] } { - apm_callback_and_log $callback "[ns_quotehtml $line]\n" - } - } - if { [catch { - close $fp - } errmsg] } { - apm_callback_and_log $callback "[ns_quotehtml $errmsg]\n" - } -} - - -ad_proc -public db_tables { - -pattern - {-dbn ""} -} { - @return a Tcl list of all the tables owned by the connected user. - - @param pattern Will be used as LIKE 'pattern%' to limit the number of tables returned. - - @param dbn The database name to use. If empty_string, uses the default database. - - @author Don Baccus (dhogaza@pacifier.com) - @author Lars Pind (lars@pinds.com) - - @change-log yon@arsdigita.com 20000711 changed to return lower case table names -} { - set proc_name {db_tables} - set driverkey [db_driverkey $dbn] - - switch -- $driverkey { - oracle { - set sql_table_names_with_pattern { - select lower(table_name) as table_name - from user_tables - where table_name like upper(:pattern) - } - set sql_table_names_without_pattern { - select lower(table_name) as table_name - from user_tables - } - } - - postgresql { - set sql_table_names_with_pattern { - select relname as table_name - from pg_class - where relname like lower(:pattern) and - relname !~ '^pg_' and relkind = 'r' - } - set sql_table_names_without_pattern { - select relname as table_name - from pg_class - where relname !~ '^pg_' and relkind = 'r' - } - } - - nsodbc - - default { - error "$proc_name is not supported for this database." - } - } - - set tables [list] - if { [info exists pattern] } { - db_foreach -dbn $dbn table_names_with_pattern \ - $sql_table_names_with_pattern { - lappend tables $table_name - } - } else { - db_foreach -dbn $dbn table_names_without_pattern \ - $sql_table_names_without_pattern { - lappend tables $table_name - } - } - - return $tables -} - - -ad_proc -public db_table_exists {{-dbn ""} table_name } { - @return 1 if a table with the specified name exists in the database, otherwise 0. - - @param dbn The database name to use. If empty_string, uses the default database. - - @author Don Baccus (dhogaza@pacifier.com) - @author Lars Pind (lars@pinds.com) -} { - set proc_name {db_table_exists} - set driverkey [db_driverkey $dbn] - - switch -- $driverkey { - oracle { - set n_rows [db_string -dbn $dbn table_count { - select count(*) from user_tables - where table_name = upper(:table_name) - }] - } - - postgresql { - set n_rows [db_string -dbn $dbn table_count { - select count(*) from pg_class - where relname = lower(:table_name) and - relname !~ '^pg_' and relkind = 'r' - }] - } - - nsodbc - - default { - error "$proc_name is not supported for this database." - } - } - - return $n_rows -} - - -ad_proc -public db_columns {{-dbn ""} table_name } { - @return a Tcl list of all the columns in the table with the given name. - - @param dbn The database name to use. If empty_string, uses the default database. - - @author Lars Pind (lars@pinds.com) - - @change-log yon@arsdigita.com 20000711 changed to return lower case column names -} { - set columns [list] - - # Works for both Oracle and PostgreSQL: - db_foreach -dbn $dbn table_column_names { - select lower(column_name) as column_name - from user_tab_columns - where table_name = upper(:table_name) - } { - lappend columns $column_name - } - - return $columns -} - - -ad_proc -public db_column_exists {{-dbn ""} table_name column_name } { - @return 1 if the row exists in the table, 0 if not. - - @param dbn The database name to use. If empty_string, uses the default database. - - @author Lars Pind (lars@pinds.com) -} { - set columns [list] - - # Works for both Oracle and PostgreSQL: - set n_rows [db_string -dbn $dbn column_exists { - select count(*) - from user_tab_columns - where table_name = upper(:table_name) - and column_name = upper(:column_name) - }] - - return [expr {$n_rows > 0}] -} - - -ad_proc -public db_column_type {{-dbn ""} {-complain:boolean} table_name column_name } { - - @return the Oracle Data Type for the specified column. - @return -1 if the table or column doesn't exist. - @return an error if table or column doesn't exist and -complain flag was specified - - @param dbn The database name to use. If empty_string, uses the default database. - @param complain throw an error when datatype is not found - - @author Yon Feldman (yon@arsdigita.com) - - @change-log 10 July, 2000: changed to return error - if column name doesn't exist - (mdettinger@arsdigita.com) - - @change-log 11 July, 2000: changed to return lower case data types - (yon@arsdigita.com) - - @change-log 11 July, 2000: changed to return error using the db_string default clause - (yon@arsdigita.com) - -} { - # Works for both Oracle and PostgreSQL: - set datatype [db_string -dbn $dbn column_type_select " - select data_type as data_type - from user_tab_columns - where upper(table_name) = upper(:table_name) - and upper(column_name) = upper(:column_name) - " -default "-1"] - if {$complain_p && $datatype == -1} { - error "Datatype for $table_name.$column_name not found." - } else { - return $datatype - } -} - - -ad_proc -public ad_column_type {{-dbn ""} table_name column_name } { - - @return 'numeric' for number type columns, 'text' otherwise - Throws an error if no such column exists. - - @param dbn The database name to use. If empty_string, uses the default database. - - @author Yon Feldman (yon@arsdigita.com) - -} { - set column_type [db_column_type -dbn $dbn $table_name $column_name] - - if { $column_type == -1 } { - return "Either table $table_name doesn't exist or column $column_name doesn't exist" - } elseif {$column_type ne "NUMBER" } { - return "numeric" - } else { - return "text" - } -} - - -ad_proc -public db_write_clob {{-dbn ""} statement_name sql args } { - @param dbn The database name to use. If empty_string, uses the default database. -} { - ad_arg_parser { bind } $args - set proc_name {db_write_clob} - set driverkey [db_driverkey $dbn] - - # TODO: Below, is db_qd_get_fullname necessary? Why this - # difference between Oracle and Postgres code? - # --atp@piskorski.com, 2003/04/09 10:00 EDT - - switch -- $driverkey { - oracle { - set full_statement_name [db_qd_get_fullname $statement_name] - db_with_handle -dbn $dbn db { - db_exec write_clob $db $full_statement_name $sql - } - } - - postgresql { - db_with_handle -dbn $dbn db { - db_exec write_clob $db $statement_name $sql - } - } - - nsodbc - - default { - error "$proc_name is not supported for this database." - } - } -} - - -ad_proc -public db_write_blob {{-dbn ""} statement_name sql args } { - @param dbn The database name to use. If empty_string, uses the default database. -} { - ad_arg_parser { bind } $args - set full_statement_name [db_qd_get_fullname $statement_name] - db_with_handle -dbn $dbn db { - db_exec_lob write_blob $db $full_statement_name $sql - } -} - - -ad_proc -public db_blob_get_file {{-dbn ""} statement_name sql args } { - @param dbn The database name to use. If empty_string, uses the default database. - -

- TODO: - This proc should probably be changed to take a final - file argument, only, rather than the current - args variable length argument list. Currently, it is - called only 4 places in OpenACS, and each place args, - if used at all, is always "-file $file". However, - such a change might break custom code... I'm not sure. - --atp@piskorski.com, 2003/04/09 11:39 EDT - -} { - ad_arg_parser { bind file args } $args - set proc_name {db_blob_get_file} - set driverkey [db_driverkey $dbn] - - set full_statement_name [db_qd_get_fullname $statement_name] - - switch -- $driverkey { - oracle { - db_with_handle -dbn $dbn db { - db_exec_lob blob_get_file $db $full_statement_name $sql $file - } - } - - postgresql { - db_with_handle -dbn $dbn db { - db_exec_lob blob_select_file $db $full_statement_name $sql $file - } - } - - nsodbc - - default { - error "$proc_name is not supported for this database." - } - } -} - - -ad_proc -public db_blob_get {{-dbn ""} statement_name sql args } { - PostgreSQL only. - - @param dbn The database name to use. If empty_string, uses the default database. -} { - ad_arg_parser { bind } $args - set proc_name {db_blob_get} - set driverkey [db_driverkey $dbn] - - switch -- $driverkey { - - postgresql { - set full_statement_name [db_qd_get_fullname $statement_name] - db_with_handle -dbn $dbn db { - set data [db_exec_lob blob_get $db $full_statement_name $sql] - } - return $data - } - - oracle { - set pre_sql $sql - set full_statement_name [db_qd_get_fullname $statement_name] - set sql [db_qd_replace_sql $full_statement_name $pre_sql] - - # insert Tcl variable values (borrowed from Dan W - olah) - if {$sql ne $pre_sql } { - set sql [uplevel 2 [list subst -nobackslashes $sql]] - } - - set data [db_string dummy_statement_name $sql] - return $data - } - - nsodbc - - default { - error "$proc_name is not supported for this database." - } - } -} - - -ad_proc -private db_exec_lob { - {-ulevel 2} - type - db - statement_name - pre_sql - {file ""} -} { - A helper procedure to execute a SQL statement, potentially binding - depending on the value of the $bind variable in the calling environment - (if set). -} { - set proc_name {db_exec_lob} - set driverkey [db_driverkey -handle_p 1 $db] - - # Note: db_exec_lob is marked as private and in the entire - # toolkit, is ONLY called from a few other procs defined in this - # same file. So we definitely could change it to take a -dbn - # switch and remove the passed in db handle altogether, and call - # 'db_driverkey -dbn' rather than 'db_driverkey -handle'. But, - # db_exec NEEDS 'db_driverkey -handle', so we might as well use it - # here too. --atp@piskorski.com, 2003/04/09 12:13 EDT - - # TODO: Using this as a wrapper for the separate _oracle and - # _postgresql versions of this proc is ugly. But also simplest - # and safest at this point, as it let me change as little as - # possible of those two relatively complex procs. - # --atp@piskorski.com, 2003/04/09 11:55 EDT - - switch -- $driverkey { - oracle { - set which_proc {db_exec_lob_oracle} - } - postgresql { - set which_proc {db_exec_lob_postgresql} - } - - nsodbc - - default { - error "$proc_name is not supported for this database." - } - } - - ns_log Debug "$proc_name: $which_proc -ulevel [expr {$ulevel +1}] $type $db $statement_name $pre_sql $file" - return [$which_proc -ulevel [expr {$ulevel +1}] $type $db $statement_name $pre_sql $file] -} - - -ad_proc -private db_exec_lob_oracle { - {-ulevel 2} - type - db - statement_name - pre_sql - {file ""} -} { - A helper procedure to execute a SQL statement, potentially binding - depending on the value of the $bind variable in the calling environment - (if set). -} { - set start_time [expr {[clock clicks -microseconds]/1000.0}] - - set sql [db_qd_replace_sql $statement_name $pre_sql] - - # insert Tcl variable values (OpenACS - Dan) - if {$sql ne $pre_sql } { - set sql [uplevel $ulevel [list subst -nobackslashes $sql]] - } - - set file_storage_p 0 - upvar $ulevel storage_type storage_type - - if {[info exists storage_type] && $storage_type eq "file"} { - set file_storage_p 1 - set original_type $type - set qtype 1row - ns_log Debug "db_exec_lob: file storage in use" - } else { - set qtype $type - ns_log Debug "db_exec_lob: blob storage in use" - } - - set errno [catch { - upvar bind bind - - # Below, note that 'ns_ora blob_get_file' takes 3 parameters, - # while 'ns_ora write_blob' takes only 2. So if file is empty - # string (which it always will/should be for $qtype - # write_blob), we must not pass any 3rd parameter to the - # ns_ora command: --atp@piskorski.com, 2003/04/09 15:10 EDT - - if { [info exists bind] && [llength $bind] != 0 } { - if { [llength $bind] == 1 } { - if { $file eq "" } { - # gn: not sure, why the eval was ever needed (4 times) - set selection [eval [list ns_ora $qtype $db -bind $bind $sql]] - } else { - set selection [eval [list ns_ora $qtype $db -bind $bind $sql $file]] - } - - } else { - set bind_vars [ns_set create] - foreach { name value } $bind { - ns_set put $bind_vars $name $value - } - if { $file eq "" } { - set selection [eval [list ns_ora $qtype $db -bind $bind_vars $sql]] - } else { - set selection [eval [list ns_ora $qtype $db -bind $bind_vars $sql $file]] - } - } - - } else { - if { $file eq "" } { - set selection [uplevel $ulevel [list ns_ora $qtype $db $sql]] - } else { - set selection [uplevel $ulevel [list ns_ora $qtype $db $sql $file]] - } - } - - if {$file_storage_p} { - set content [ns_set value $selection 0] - for {set i 0} {$i < [ns_set size $selection]} {incr i} { - set name [ns_set key $selection $i] - if {$name eq "content"} { - set content [ns_set value $selection $i] - } - } - - switch -- $original_type { - - blob_get_file { - if {[file exists $content]} { - file copy -- $content $file - return $selection - } else { - error "file: $content doesn't exist" - } - } - - write_blob { - - if {[file exists $content]} { - set ofp [open $content r] - fconfigure $ofp -encoding binary - ns_writefp $ofp - close $ofp - return $selection - } else { - error "file: $content doesn't exist" - } - } - } - } else { - return $selection - } - - } error] - - ds_collect_db_call $db $type $statement_name $sql $start_time $errno $error - if { $errno == 2 } { - return $error - } - - return -code $errno -errorinfo $::errorInfo -errorcode $::errorCode $error -} - - -ad_proc -private db_exec_lob_postgresql { - {-ulevel 2} - type - db - statement_name - pre_sql - {file ""} -} { - A helper procedure to execute a SQL statement, potentially binding - depending on the value of the $bind variable in the calling environment - (if set). - - Low level replacement for db_exec which emulates blob handling. - -} { - set start_time [expr {[clock clicks -microseconds]/1000.0}] - - # Query Dispatcher (OpenACS - ben) - set sql [db_qd_replace_sql $statement_name $pre_sql] - - # insert Tcl variable values (OpenACS - Dan) - if {$sql ne $pre_sql } { - set sql [uplevel $ulevel [list subst -nobackslashes $sql]] - } - # create a function definition statement for the inline code - # binding is emulated in tcl. (OpenACS - Dan) - - set errno [catch { - upvar bind bind - if { [info exists bind] && [llength $bind] != 0 } { - if { [llength $bind] == 1 } { - set bind_vars [list] - set len [ns_set size $bind] - for {set i 0} {$i < $len} {incr i} { - lappend bind_vars [ns_set key $bind $i] \ - [ns_set value $bind $i] - } - set lob_sql [db_bind_var_substitution $sql $bind_vars] - } else { - set lob_sql [db_bind_var_substitution $sql $bind] - } - } else { - set lob_sql [uplevel $ulevel [list db_bind_var_substitution $sql]] - } - - # get the content - asssume it is in column 0, or optionally it can - # be returned as "content" with the storage type indicated by the - # "storage_type" column. - - set selection [ns_db 1row $db $lob_sql] - set content [ns_set value $selection 0] - for {set i 0} {$i < [ns_set size $selection]} {incr i} { - set name [ns_set key $selection $i] - if {$name eq "storage_type"} { - set storage_type [ns_set value $selection $i] - } elseif {$name eq "content"} { - set content [ns_set value $selection $i] - } - } - - # this is an ugly hack, but it allows content to be written - # to a file/connection if it is stored as a lob or if it is - # stored in the content-repository as a file. (DanW - Openacs) - - switch -- $type { - - blob_get { - - if {[info exists storage_type]} { - switch -- $storage_type { - file { - if {[file exists $content]} { - set ifp [open $content r] - - # DRB: this could be made faster by setting the buffersize - # to the size of the file, but for very large files allocating - # that much more memory on top of that needed by Tcl for storage - # of the data might not be wise. - - fconfigure $ifp -translation binary - - set data [read $ifp] - close $ifp - return $data - } else { - error "file: $content doesn't exist" - } - } - - lob { - if {[regexp {^[0-9]+$} $content match]} { - return [ns_pg blob_get $db $content] - } else { - error "invalid lob_id: should be an integer" - } - } - - default { - error "invalid storage type" - } - } - } elseif {[file exists $content]} { - set ifp [open $content r] - fconfigure $ifp -translation binary - set data [read $ifp] - close $ifp - return $data - } elseif {[regexp {^[0-9]+$} $content match]} { - return [ns_pg blob_get $db $content] - } else { - error "invalid query" - } - } - - blob_select_file { - - if {[info exists storage_type]} { - switch -- $storage_type { - file { - if {[file exists $content]} { - file copy -- $content $file - } else { - error "file: $content doesn't exist" - } - } - - lob { - if {[regexp {^[0-9]+$} $content match]} { - ns_pg blob_select_file $db $content $file - } else { - error "invalid lob_id: should be an integer" - } - } - - default { - error "invalid storage type" - } - } - } elseif {[file exists $content]} { - file copy -- $content $file - } elseif {[regexp {^[0-9]+$} $content match]} { - ns_pg blob_select_file $db $content $file - } else { - error "invalid query" - - # TODO: Page /file-storage/download-archive/index - # fails here on cvs head both before and after my - # mult-db db_* API work, I don't know why. See bug: - # http://openacs.org/bugtracker/openacs/com/file-storage/bug?bug%5fnumber=427 - # --atp@piskorski.com, 2003/04/09 18:04 EDT - } - } - - write_blob { - - if {[info exists storage_type]} { - switch -- $storage_type { - file { - if {[file exists $content]} { - set ofp [open $content r] - fconfigure $ofp -encoding binary - ns_writefp $ofp - close $ofp - } else { - error "file: $content doesn't exist" - } - } - - text { - ns_write $content - } - - lob { - if {[regexp {^[0-9]+$} $content match]} { - ns_pg blob_write $db $content - } else { - error "invalid lob_id: should be an integer" - } - } - - default { - error "invalid storage type" - } - } - } elseif {[file exists $content]} { - set ofp [open $content r] - fconfigure $ofp -encoding binary - ns_writefp $ofp - close $ofp - } elseif {[regexp {^[0-9]+$} $content match]} { - ns_pg blob_write $db $content - } else { - ns_write $content - } - } - } - - return - - } error] - - set errinfo $::errorInfo - set errcode $::errorCode - - ds_collect_db_call $db 0or1row $statement_name $sql $start_time $errno $error - - if { $errno == 2 } { - return $error - } - - return -code $errno -errorinfo $errinfo -errorcode $errcode $error -} - -ad_proc -public db_flush_cache { - {-cache_key_pattern *} - {-cache_pool db_cache_pool} -} { - - Flush the given cache of entries with keys that match the given pattern. - - @param cache_key_pattern The "string match" pattern used to flush keys (default is to flush all entries) - @param cache_pool The pool to flush (default is to flush db_cache_pool) - @author Don Baccus (dhogasa@pacifier.com) - -} { - # - # If the key pattern has meta characters, iterate over the entries. - # Otherwise, make a direct lookup, without retrieving the all keys - # from the cache, which can cause large mutex lock times. - # - if {[regexp {[*\]\[]} $cache_key_pattern]} { - foreach key [ns_cache names $cache_pool $cache_key_pattern] { - ns_cache flush $cache_pool $key - } - } else { - ns_cache flush $cache_pool $cache_key_pattern - } -} - -ad_proc -public db_bounce_pools {{-dbn ""}} { - @return Call ns_db bouncepool on all pools for the named database. - @param dbn The database name to use. Uses the default database if not supplied. -} { - foreach pool [db_available_pools $dbn] { - ns_db bouncepool $pool - } -} - -# Local variables: -# mode: tcl -# tcl-indent-level: 4 -# indent-tabs-mode: nil -# End: Index: openacs-4/packages/acs-tcl/tcl/01-database-procs-oracle.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/Attic/01-database-procs-oracle.tcl,v diff -u -N --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/acs-tcl/tcl/01-database-procs-oracle.tcl 17 Jul 2019 17:44:11 -0000 1.1.2.1 @@ -0,0 +1,19 @@ +ad_library { + + Oracle-specific database API and utility procs + + @creation-date 15 Apr 2000 + @author Jon Salz (jsalz@arsdigita.com) + @cvs-id $Id: 01-database-procs-oracle.tcl,v 1.1.2.1 2019/07/17 17:44:11 hectorr Exp $ +} + +# This file is now obsolete. All procs have been merged into +# 00-database-procs.tcl, so that all supported databases are usable +# with the db_* API all the time, regardless of which database type +# OpenACS is using. --atp@piskorski.com, 2003/04/09 12:04 EDT + +# Local variables: +# mode: tcl +# tcl-indent-level: 4 +# indent-tabs-mode: nil +# End: Index: openacs-4/packages/acs-tcl/tcl/01-database-procs-postgresql-postgresql.xql =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/Attic/01-database-procs-postgresql-postgresql.xql,v diff -u -N --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/acs-tcl/tcl/01-database-procs-postgresql-postgresql.xql 17 Jul 2019 17:44:11 -0000 1.1.2.1 @@ -0,0 +1,21 @@ + + + postgresql7.1 + + + + select nextval(:sequence) as nextval + where (select relkind + from pg_class + where relname = :sequence) = 'S' + + + + + + select nextval + from ${sequence} + + + + Index: openacs-4/packages/acs-tcl/tcl/01-database-procs-postgresql.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/Attic/01-database-procs-postgresql.tcl,v diff -u -N --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/acs-tcl/tcl/01-database-procs-postgresql.tcl 17 Jul 2019 17:44:11 -0000 1.1.2.1 @@ -0,0 +1,19 @@ +ad_library { + + Postgres-specific database API and utility procs. + + @creation-date 15 Apr 2000 + @author Jon Salz (jsalz@arsdigita.com) + @cvs-id $Id: 01-database-procs-postgresql.tcl,v 1.1.2.1 2019/07/17 17:44:11 hectorr Exp $ +} + +# This file is now obsolete. All procs have been merged into +# 00-database-procs.tcl, so that all supported databases are usable +# with the db_* API all the time, regardless of which database type +# OpenACS is using. --atp@piskorski.com, 2003/04/09 12:04 EDT + +# Local variables: +# mode: tcl +# tcl-indent-level: 4 +# indent-tabs-mode: nil +# End: Index: openacs-4/packages/acs-tcl/tcl/01-database-procs.tcl =================================================================== RCS file: /usr/local/cvsroot/openacs-4/packages/acs-tcl/tcl/Attic/01-database-procs.tcl,v diff -u -N --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ openacs-4/packages/acs-tcl/tcl/01-database-procs.tcl 17 Jul 2019 17:44:11 -0000 1.1.2.1 @@ -0,0 +1,3800 @@ +ad_library { + + An API for managing database queries. + + @creation-date 15 Apr 2000 + @author Jon Salz (jsalz@arsdigita.com) + @cvs-id $Id: 01-database-procs.tcl,v 1.1.2.1 2019/07/17 17:44:11 hectorr Exp $ +} + +# Database caching. +# +# Values returned by a query are cached if you pass the "-cache_key" switch +# to the database procedure. The switch value will be used as the key in the +# ns_cache eval call used to execute the query and processing code. The +# db_flush proc should be called to flush the cache when appropriate. The +# "-cache_pool" parameter can be used to specify the cache pool to be used, +# and defaults to db_cache_pool. The # size of the default cache is governed +# by the kernel parameter "DBCacheSize" in the "caching" section. +# +# Currently db_string, db_list, db_list_of_lists, db_0or1row, and db_multirow support +# caching. +# +# Don Baccus 2/25/2006 - my 52nd birthday! + +# As originally released in (at least) ACS 4.2 through OpenACS 4.6, +# this DB API supported only a single, default database. You could +# define any number of different database drivers and pools in +# AOLserver, but could only use ONE database here. +# +# I have eliminated this restriction. Now, in OpenACS 5.0 and later, +# to access a non-default database, simply pass the optional -dbn +# (Database Name) switch to any of the DB API procs which support it. +# +# Supported AOLserver database drivers: +# +# - Oracle (nsoracle): Everything should work. +# +# - PostgreSQL (nspostgres): Everything should work. +# +# - ODBC (nsodbc): +# - Anything using bind variables will only work if you're using a +# version of the driver with bind variable emulation hacked in +# (copied from the PostgreSQL driver). +# - Some features, like LOBs, simply won't work at all. +# - The basic functionality worked fine back in Sept. 2001, but I +# have NOT tested it since then at all, so maybe there are bugs. +# +# - Any others: Basic stuff using only the standard ns_db API will +# likely work, but any special features of the driver (e.g., LOBs) +# definitely won't. Feel free to add support! +# +# --atp@piskorski.com, 2003/04/09 19:18 EDT + +# Note that "-dbn" specifies a "Database Name", NOT a database pool! +# +# I could have provided access to secondary databases via a -pool +# rather than a -dbn switch, but chose not to, as the existing DB API +# already had the nicely general feature that if you try to do nested +# queries, the DB API will transparently grab a second database handle +# from another pool to make it work. You can nest your queries as +# many levels deep as you have database pools defined for that +# database. So, the existing API essentially already supported the +# notion of "binning" database pools into logical "databases", it just +# didn't provide any way to define more than the single, default +# database! Thus I chose to preserve this "binning" by specifying +# databases via the -dbn switch rather than database pools via a -pool +# switch. + +# (JoelA, 27 Dec 2004 - replaced example config.tcl with link) +# +# see http://openacs.org/doc/openacs-5-1/tutorial-second-database +# for config and usage examples + +# TODO: The "driverkey_" overrides in the config file are NOT +# implemented yet! +# +# --atp@piskorski.com, 2003/03/16 21:30 EST + +# NOTE: don't forget to add your new pools into the +# ns_section ns/db/pools + + +# The "driverkey" indirection layer: +# +# Note that in the AOLserver config file, you may optionally add one +# entry for each database defining its "driver key". If you do NOT +# specify a driver key in the AOLserver config file, the appropriate +# key will be determined for you by calling "ns_db driver" once on +# startup for the first pool defined in each database. Therefore, +# most people should NOT bother to give a driverkey in the config +# file. +# +# So, just what is this "driverkey" thing used for anyway? AOLserver +# defines the ns_db API, and the OpenACS db_* API depends utterly on +# it. However, there are a few holes in the functionality of the +# ns_db API, and each AOLserver database driver tends to fill in those +# holes by adding extra functionality with its own, drive specific +# functions. Therefore, in order to make the db_* API work with +# multiple db drivers, we need to introduce some switches or if +# statements in our code. +# +# Currently (2003/04/08), at least for the Oracle, PostgreSQL, and +# ODBC drivers, the database driver name returned by "ns_db driver" is +# completely sufficient for these switch statements. But, rather than +# using ns_db driver directly in the switches, we add the simple +# "driver key" layer of indirection between the two, to make the +# default behavior easier to override if that should ever be +# necessary. +# +# --atp@piskorski.com, 2003/04/08 03:39 EDT + + +# We now use the following global variables: +# +# Server-Wide NSV arrays, keys: +# db_driverkey $dbn +# db_pool_to_dbn $pool +# +# Global Variables +# ::acs::default_database +# ::acs::db_pools($dbn) (used in db_available_pools) +# ::acs::db_pool_to_dbn($pool) (used for caching access to nsv db_pool_to_dbn) +# ::acs::db_driverkey($dbn) (used for caching access to nsv db_driverkey) +# +# Per-thread Tcl global variables: +# One Tcl Array per Database Name: +# db_state_${dbn} +# +# The db_available_pools and db_state arrays are used in exactly the +# same manner as they were originally (in ACS 4.0 to OpenACS 4.6 +# code), except that in the original DB API we had only one of each +# array total, while now we have one of each array per database. +# +# The db_pool_to_dbn nsv is simply a map to quickly tell use which dbn +# each AOLserver database pool belongs to. (Any pools which do not +# belong to any dbn have no entry here.) +# +# We use the procs db_state_array_name_is, db_available_pools, and +# db_driverkey to help keep track of these different arrays. +# Note that most code should now NEVER read from any of the +# db_available_pools nsvs listed above, but should instead use the +# proc db_available_pools provided for that purpose. +# +# The original implementation comments on the use of these global +# variables are below: +# +# --atp@piskorski.com, 2003/03/16 21:30 EST + + +ad_proc -private db_state_array_name_is { + {-dbn ""} +} { + @return the name of the global db_state array for the given + database name. + + @param dbn The database name to use. If empty_string, uses the + default database. + + @author Andrew Piskorski (atp@piskorski.com) + @creation-date 2003/03/16 +} { + if { $dbn eq "" } { + set dbn $::acs::default_database + } + #if {[llength [trace info variable ::db_state_${dbn}]] == 0} { + # trace add variable ::db_state_${dbn} {array read write unset} [list ::db_tracer ::db_state_${dbn}] + #} + return "::db_state_${dbn}" +} + +# proc db_tracer {varname name1 name2 op} { +# if {$name2 eq "handles"} { +# #ns_log notice "### variable $varname: $name1 ($name2) $op" +# if {$op eq "write"} { +# ns_log notice "###### handles updated to <[set ::${varname}($name2)]>" +# } +# } +# } + +ad_proc -public db_driverkey { + {-handle_p 0} + dbn +} { + Normally, a dbn is passed to this proc. Unfortunately, there are + one or two cases where a proc that needs to call this one has only + a db handle, not the dbn that handle came from. Therefore, they + instead use -handle_p 1 and pass the db handle. + + Hmm, as of 2018, it seems that in most cases, db_driverkey is + called with a handle. + + @return The driverkey for use in db_* API switch statements. + + @author Andrew Piskorski (atp@piskorski.com) + @creation-date 2003/04/08 +} { + if { $handle_p } { + # + # In the case, the passed "dbn" is actually a + # handle. Determine from the handle the "pool" and from the + # "pool" the "dbn". + # + set handle $dbn + set pool [ns_db poolname $handle] + set key ::acs::db_pool_to_dbn($pool) + if {[info exists $key]} { + # + # First, try to get the variable from the per-thread + # variable (which is part of the blueprint). + # + set dbn [set $key] + } elseif { [nsv_exists db_pool_to_dbn $pool] } { + # + # Fallback to nsv (old style), when for whatever + # reasons, the namespaced variable is not available. + # + ns_log notice "db_driverkey $handle_p dbn <$dbn> VIA NSV" + set dbn [nsv_get db_pool_to_dbn $pool] + } else { + # + # db_pool_to_dbn_init runs on startup, so other than some + # broken code deleting the nsv key (very unlikely), the + # only way this could happen is for someone to call this + # proc with a db handle from a pool which is not part of + # any dbn. + + error "No database name (dbn) found for pool '$pool'. Check the 'ns/server/[ns_info server]/acs/database' section of your config file." + } + } + + set key ::acs::db_driverkey($dbn) + if {[info exists $key]} { + return [set $key] + } + + if { ![nsv_exists db_driverkey $dbn] } { + # This ASSUMES that any overriding of this default value via + # "ns_param driverkey_dbn" has already been done: + + if { $handle_p } { + set driver [ns_db driver $handle] + } else { + db_with_handle -dbn $dbn handle { + set driver [ns_db driver $handle] + } + } + + # These are the default driverkey values, if they are not set + # in the config file: + + if { [string match "Oracle*" $driver] } { + set driverkey {oracle} + } elseif { $driver eq "PostgreSQL" } { + set driverkey "postgresql" + } elseif { $driver eq "ODBC" } { + set driverkey "nsodbc" + } else { + set driverkey {} + ns_log Error "db_driverkey: Unknown driver '$driver'." + } + + nsv_set db_driverkey $dbn $driverkey + } + + return [set $key [nsv_get db_driverkey $dbn]] +} + + +ad_proc -public db_type {} { + @return the RDBMS type (i.e. oracle, postgresql) this OpenACS installation is using. + The nsv ad_database_type is set up during the bootstrap process. +} { + # + # Currently this should always be either "oracle" or "postgresql": + # --atp@piskorski.com, 2003/03/16 22:01 EST + # + # First check, if the database type exists in the namespaced + # variable. This should be always the case. If this fail, fall + # back to the old-style nsv (which can be costly in tight db loops) + # + if {[info exists ::acs::database_type]} { + set result $::acs::database_type + } else { + set result [nsv_get ad_database_type .] + ns_log Warning "db_type '$result' had to be obtained from the nsv 'ad_database_type'" + set ::acs::database_type $result + } + return $result +} + +ad_proc -public db_compatible_rdbms_p { db_type } { + @return 1 if the given db_type is compatible with the current RDBMS. +} { + return [expr { $db_type eq "" || [db_type] eq $db_type }] +} + + + +ad_proc -private db_legacy_package_p { db_type_list } { + @return 1 if the package is a legacy package. We can only tell for certain if it explicitly supports Oracle 8.1.6 rather than the OpenACS more general oracle. +} { + if {"oracle-8.1.6" in $db_type_list} { + return 1 + } + return 0 +} + +ad_proc -public db_version {} { + @return the RDBMS version (i.e. 8.1.6 is a recent Oracle version; 7.1 a + recent PostgreSQL version) +} { + return [nsv_get ad_database_version .] +} + +ad_proc -public db_current_rdbms {} { + @return the current rdbms type and version. +} { + return [db_rdbms_create [db_type] [db_version]] +} + +ad_proc -public db_known_database_types {} { + @return a list of three-element lists describing the database engines known + to OpenACS. Each sublist contains the internal database name (used in file + paths, etc), the driver name, and a "pretty name" to be used in selection + forms displayed to the user. + + The nsv containing the list is initialized by the bootstrap script and should + never be referenced directly by user code. +} { + return $::acs::known_database_types +} + + +# db_null, db_quote, db_nullify_empty_string - were all previously +# defined Oracle only, no Postgres equivalent existed at all. So, it +# can't hurt anything to have them defined in when OpenACS is using +# Postgres too. --atp@piskorski.com, 2003/04/08 05:34 EDT + +ad_proc -deprecated db_null {} { + + @return an empty string, which Oracle thinks is null. + + Deprecated: This routine was invented to provide an RDBMS-specific null + value but doesn't actually work. I (DRB) left it in to speed porting - we + should really clean up the code and pull out the calls instead, though. + + @see "" +} { + return "" +} + +ad_proc -public db_quote { string } { + Quotes a string value to be placed in a SQL statement. +} { + regsub -all {'} "$string" {''} result + return $result +} + +ad_proc -public -deprecated db_nullify_empty_string { string } { + A convenience function that returns [db_null] if $string is the empty string. + + Deprecated: essentially just returns the passed string. + + @see: db_null +} { + return $string +} + +ad_proc -public db_boolean { bool } { + Converts a Tcl boolean (1/0) into a SQL boolean (t/f) + @return t or f +} { + if { $bool } { + return "t" + } else { + return "f" + } +} + +ad_proc -public db_nextval { + { -dbn "" } + sequence +} { + + Example: + +

+    set new_object_id [db_nextval acs_object_id_seq]
+    
+ + @return the next value for a sequence. This can utilize a pool of + sequence values. + + @param sequence the name of an SQL sequence + + @param dbn The database name to use. If empty_string, uses the default database. + + @see /doc/db-api-detailed +} { + set driverkey [db_driverkey $dbn] + + # PostgreSQL has a special implementation here, any other db will + # probably work with the default: + + switch -- $driverkey { + + postgresql { + # # the following query will return a nextval if the sequnce + # # is of relkind = 'S' (a sequnce). if it is not of relkind = 'S' + # # we will try querying it as a view: + + # if { [db_0or1row -dbn $dbn nextval_sequence " + # select nextval('${sequence}') as nextval + # where (select relkind + # from pg_class + # where relname = '${sequence}') = 'S' + # "]} { + # return $nextval + # } else { + # ns_log debug "db_nextval: sequence($sequence) is not a real sequence. perhaps it uses the view hack." + # db_0or1row -dbn $dbn nextval_view "select nextval from ${sequence}" + # return $nextval + # } + # + # The code above is just for documentation, how it worked + # before the change below. We keep now a per-thread table of + # the "known" sequences to avoid at runtime the query, + # whether the specified sequence is a real sequence or a + # view. This change makes this function more than a factor + # of 2 faster than before. + # + # Note that solely the per-thread information won't work for + # freshly created sequences. Therefore, we keep the old + # code for checking at runtime in the database for such + # occurrences. + # + # Note that the sequence handling in OpenACS is quite a + # mess. Some sequences are named t_SEQUENCE (10 in + # dotlrn), others are called just SEQUENCE (18 in dotlrn), + # for some sequences, additional views are defined with an + # attribute 'nextval', and on top of this, db_nextval is + # called sometimes with the view name and sometimes with + # the sequence name. Checking this at runtime is + # unnecessary complex and costly. + # + # The best solution would certainly be to call "db_nextval" + # only with real sequence names (as defined in SQL). In that + # case, the whole function would for postgres would collapse + # to a single line, without any need for sequence name + # caching. But in that case, one should rename the sequences + # from t_SEQUENCE to SEQUENCE for postgres. + # + # However, since Oracle uses the pseudo column ".nextval", + # which is emulated via the view, it is not clear, how + # feasible this is to remove all such views without breaking + # installed applications. We keep for such cases the view, + # but nevertheless, the function "db_nextval" should always + # be called with names without the "t_" prefix to achieve + # Oracle compatibility. + + if {![info exists ::db::sequences]} { + ns_log notice "-- creating per thread sequence table" + namespace eval ::db {} + foreach s [db_list -dbn $dbn relnames "select relname, relkind from pg_class where relkind = 'S'"] { + set ::db::sequences($s) 1 + } + } + if {[info exists ::db::sequences(t_$sequence)]} { + #ns_log notice "-- found t_$sequence + #ad_log Warning "Deprecated sequence name 't_$sequence' is used. Use instead 't_$sequence'" + set nextval [db_string -dbn $dbn nextval "select nextval('t_$sequence')"] + } elseif {[info exists ::db::sequences($sequence)]} { + #ns_log notice "-- found $sequence" + set nextval [db_string -dbn $dbn nextval "select nextval('$sequence')"] + if {[string match t_* $sequence]} { + ad_log Warning "For portability, db_nextval should be called without the leading 't_' prefix: 't_$sequence'" + } + } elseif { [db_0or1row -dbn $dbn nextval_sequence " + select nextval('${sequence}') as nextval + where (select relkind + from pg_class + where relname = '${sequence}') = 'S' + "]} { + # + # We do not have an according sequence-table. Use the system catalog to check + # for the sequence + # + # ... the query sets nextval if it succeeds + # + ad_log Warning "Probably deprecated sequence name '$sequence' is used (no sequence table found)" + } else { + # + # Finally, there might be a view with a nextval + # + ns_log debug "db_nextval: sequence($sequence) is not a real sequence. perhaps it uses the view hack." + set nextval [db_string -dbn $dbn nextval "select nextval from $sequence"] + ad_log Warning "Using deprecated sequence view hack for '$sequence'. Is there not real sequence?" + } + + return $nextval + } + + oracle - + nsodbc - + default { + return [db_string -dbn $dbn nextval "select $sequence.nextval from dual"] + } + } +} + +ad_proc -public db_nth_pool_name { + { -dbn "" } + n +} { + @return the name of the pool used for the nth-nested selection (0-relative). + @param dbn The database name to use. If empty_string, uses the default database. +} { + set available_pools [db_available_pools $dbn] + + if { $n < [llength $available_pools] } { + set pool [lindex $available_pools $n] + } else { + return -code error "Ran out of database pools ($available_pools)" + } + return $pool +} + +if {[acs::icanuse "ns_db currenthandles"]} { + + ns_log notice "... I can use 'ns_db currenthandles'" + + # + # This branch uses "ns_db currenthandles" to implement + # "db_with_handle" instead of the old approach based on the global + # db_state variables. The new approach has the advantage that it + # is: + # + # - more robust (deletion and creation of the per-request variables, + # no coherency problem), + # - simpler, and + # - faster (less overhead per db_with_handle call) + # + # time {db_string . {select object_id from acs_objects limit 1}} 1000 + # old: 160-190 microseconds per iteration + # new: 150-180 microseconds per iteration + # + # time {xo::dc get_value . {select object_id from acs_objects limit 1}} 1000 + # old: 110-120 + # new: 105-110 + # + # set id -1 + # time {xo::dc get_value -prepare {int} . {select object_id from acs_objects where object_id=:id}} 1000 + # old: 80-100 + # new: 76-90 + # + # Still, more improvement can be done (GN). + # + ad_proc -public db_with_handle { + { -dbn "" } + db code_block + } { + Place a usable database handle in db and executes + code_block. + + @param dbn Database name to use. If empty_string, use the default database + @param db Name of the handle variable used in the code block + @param code_block code block to be executed with handle + } { + # + # Let the caller decide, how the handle variable is called in + # the code block. + # + upvar 1 $db dbh + + # + # Get the pools and the current allocated handles for this thread. + # + set pools [db_available_pools $dbn] + set currentHandles [ns_db currenthandles] + #ns_log notice "### pools <$pools> currentHandles <$currentHandles>" + + set db "" + set n 0 + foreach pool $pools { + # + # Do we have already handles allocated from this pool? + # + if {[dict exists $currentHandles $pool]} { + # + # Are there handles, which are not active (i.e. not in + # a currently open "ns_db select" and "ns_db getrow" + # context. + # + foreach {handle active} [dict get $currentHandles $pool] { + #ns_log notice "### FOUND pool $pool handle $handle active $active" + if {$active eq "0"} { + # + # We can use this handle + # + set db $handle + break + } + } + } else { + break + } + incr n + } + # + # In case, we got no handle above, we have to allocate a + # handle from the next pool, from which we have not got a + # handle before. + # + if {$db eq ""} { + # + # We were not successful above + # + set pool [lindex $pools $n] + if {$pool eq ""} { + ad_log error "handles from all pools <$pools> are exhausted" + error "could not obtain handle, all pools are exhausted" + } + set start_time [expr {[clock clicks -microseconds]/1000.0}] + #ns_log notice "### BEFORE gethandle $pool ($n)" + set errno [catch { + set db [ns_db gethandle $pool] + } error] + #ad_log notice "### AFTER gethandle $pool errno $errno handle <$db> currentHandles [ns_db currenthandles]" + ds_collect_db_call $db gethandle "" $pool $start_time $errno $error + if { $errno } { + ns_log notice "### RETURNING error $error" + return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error + } + } + #ns_log notice "### db_with_handle has handle <$db>" + + set dbh $db + set errno [catch { uplevel 1 $code_block } error] + + # Unset dbh, so any subsequence use of this variable will bomb. + unset -nocomplain dbh + + # If errno is 1, it's an error, so return errorCode and errorInfo; + # if errno = 2, it's a return, so don't try to return errorCode/errorInfo + # errno = 3 or 4 give undefined results + + if { $errno == 1 } { + # A real error occurred + ns_log notice "### db_with_handle returned error <$error> for statement $code_block" + return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error + } + + if { $errno == 2 } { + + # The code block called a "return", so pass the message through but don't try + # to return errorCode or errorInfo since they may not exist + + return -code $errno $error + } + } + + # + # db_last_used_handle + # + ad_proc -private db_last_used_handle {{-dbn ""}} { + Get the last used inactive handle. + + @param dbn database name + @return last active handle or empty string + } { + set pools [db_available_pools $dbn] + set currentHandles [ns_db currenthandles] + + set last_used_handle "" + foreach pool $pools { + if {[dict exists $currentHandles $pool]} { + foreach {handle active} [dict get $currentHandles $pool] { + #ns_log notice "### FOUND pool $pool handle $handle active $active" + if {$active eq 0} { + set last_used_handle $handle + } + } + } + } + #ns_log notice "###### db_last_used_handle: <$currentHandles> last used $last_used_handle" + return $last_used_handle + } + + # + # db_release_unused_handles + # + ad_proc -public db_release_unused_handles {{-dbn ""}} { + Releases any database handles that are presently unused. + + @param dbn The database name to use. If empty_string, uses the default database. + } { + # we need the state array still for transaction handling + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + set pools [db_available_pools $dbn] + set currentHandles [ns_db currenthandles] + + foreach pool $pools { + if {[dict exists $currentHandles $pool]} { + foreach {handle active} [dict get $currentHandles $pool] { + #ns_log notice "### FOUND pool $pool handle $handle active $active" + if {$active eq 0} { + # Don't release handles which are part of a transaction. + if { [info exists db_state(transaction_level,$handle)] + && $db_state(transaction_level,$handle) > 0 + } { + continue + } + set start_time [expr {[clock clicks -microseconds]/1000.0}] + ns_db releasehandle $handle + #ns_log notice "### AFTER releasehandle [ns_db currenthandles $pool]" + ds_collect_db_call $handle releasehandle "" "" $start_time 0 "" + } + } + } + } + } + + +} else { + + # + # This is the legacy branch without [ns_db currenthandles], using + # the global state variables. + # + ns_log notice "... cannot use 'ns_db currenthandles'" + + ad_proc -public db_with_handle { + { -dbn "" } + db code_block + } { + + Places a usable database handle in db and executes code_block. + + @param dbn The database name to use. If empty_string, uses the default database. + } { + upvar 1 $db dbh + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + # Initialize bookkeeping variables. + if { ![info exists db_state(handles)] } { + set db_state(handles) [list] + } + if { ![info exists db_state(n_handles_used)] } { + set db_state(n_handles_used) 0 + } + if { $db_state(n_handles_used) >= [llength $db_state(handles)] } { + set pool [db_nth_pool_name -dbn $dbn $db_state(n_handles_used)] + set start_time [expr {[clock clicks -microseconds]/1000.0}] + set errno [catch { + set db [ns_db gethandle $pool] + } error] + ds_collect_db_call $db gethandle "" $pool $start_time $errno $error + lappend db_state(handles) $db + if { $errno } { + return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error + } + } + set my_dbh [lindex $db_state(handles) $db_state(n_handles_used)] + set dbh $my_dbh + set db_state(last_used) $my_dbh + + incr db_state(n_handles_used) + set errno [catch { uplevel 1 $code_block } error] + incr db_state(n_handles_used) -1 + + # This may have changed while the code_block was being evaluated. + set db_state(last_used) $my_dbh + + # Unset dbh, so any subsequence use of this variable will bomb. + unset -nocomplain dbh + + # If errno is 1, it's an error, so return errorCode and errorInfo; + # if errno = 2, it's a return, so don't try to return errorCode/errorInfo + # errno = 3 or 4 give undefined results + + if { $errno == 1 } { + # A real error occurred + return -code $errno -errorcode $::errorCode -errorinfo $::errorInfo $error + } + + if { $errno == 2 } { + + # The code block called a "return", so pass the message through but don't try + # to return errorCode or errorInfo since they may not exist + + return -code $errno $error + } + } + + ad_proc -private db_last_used_handle {{-dbn ""}} { + Get the last used handle + + @param dbn database name + @return last active handle or empty string + } { + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + return $db_state(last_used) + } + + ad_proc -public db_release_unused_handles {{-dbn ""}} { + + Releases any database handles that are presently unused. + + @param dbn The database name to use. If empty_string, uses the default database. + } { + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + if { [info exists db_state(n_handles_used)] } { + # Examine the elements at the end of db_state(handles), killing off + # handles that are unused and not engaged in a transaction. + + set index_to_examine [expr { [llength $db_state(handles)] - 1 }] + while { $index_to_examine >= $db_state(n_handles_used) } { + set db [lindex $db_state(handles) $index_to_examine] + + # Stop now if the handle is part of a transaction. + if { [info exists db_state(transaction_level,$db)] + && $db_state(transaction_level,$db) > 0 + } { + break + } + + set pool [db_nth_pool_name -dbn $dbn $db_state(n_handles_used)] + set start_time [expr {[clock clicks -microseconds]/1000.0}] + ns_db releasehandle $db + ds_collect_db_call $db releasehandle "" "" $start_time 0 "" + incr index_to_examine -1 + } + set db_state(handles) [lrange $db_state(handles) 0 $index_to_examine] + } + } + + +} + +ad_proc -public db_resultrows {{-dbn ""}} { + @return the number of rows affected by the last DML command. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set driverkey [db_driverkey $dbn] + + switch -- $driverkey { + oracle { + return [ns_ora resultrows [db_last_used_handle -dbn $dbn]] + } + postgresql { + return [ns_pg ntuples [db_last_used_handle -dbn $dbn]] + } + nsodbc { + error "db_resultrows is not supported for this database." + } + default { + error "Unknown database driver. db_resultrows is not supported for this database." + } + } +} + + + +ad_proc -public db_exec_plsql { + {-dbn ""} + statement_name + sql + args +} { + + Oracle: + Executes a PL/SQL statement, and returns the variable of bind + variable :1. + +

+ PostgreSQL: + Performs a pl/pgsql function or procedure call. The caller must + perform a select query that returns the value of the function. + +

+ Examples: + +

+

+    # Oracle:
+    db_exec_plsql delete_note {
+        begin  note.del(:note_id);  end;
+    }
+
+    # PostgreSQL:
+    db_exec_plsql delete_note {
+        select note__delete(:note_id);
+    }
+    
+ +

+ If you need the return value, then do something like this: + +

+

+    # Oracle:
+    set new_note_id [db_exec_plsql create_note {
+        begin
+        :1 := note.new(
+                       owner_id => :user_id,
+                       title    => :title,
+                       body     => :body,
+                       creation_user => :user_id,
+                       creation_ip   => :peeraddr,
+                       context_id    => :package_id
+                       );
+        end;
+    }]
+
+    # PostgreSQL:
+    set new_note_id [db_exec_plsql create_note {
+        select note__new(
+                         null,
+                         :user_id,
+                         :title,
+                         :body,
+                         'note',
+                         now(),
+                         :user_id,
+                         :peeraddr,
+                         :package_id
+                         );
+    }]
+    
+ +

+ You can call several pl/sql statements at once, like this: + +

+

+    # Oracle:
+    db_exec_plsql delete_note {
+        begin
+        note.del(:note_id);
+        note.del(:another_note_id);
+        note.del(:yet_another_note_id);
+        end;
+    }
+
+    # PostgreSQL:
+    db_exec_plsql delete_note {
+        select note__delete(:note_id);
+        select note__delete(:another_note_id);
+        select note__delete(:yet_another_note_id);
+    }
+    
+ + If you are using xql files then put the body of the query in a + yourfilename-oracle.xql or yourfilename-postgresql.xql file, as appropriate. E.g. the first example + transformed to use xql files looks like this: + + +

+ yourfilename.tcl:
+

+

+    db_exec_plsql delete_note {}
+ +

+ yourfilename-oracle.xql:
+

+

+    <fullquery name="delete_note">
+    <querytext>
+    begin
+    note.del(:note_id);
+    end;
+    </querytext>
+    </fullquery>
+ +

+ yourfilename-postgresql.xql:
+

+

+    <fullquery name="delete_note">
+    <querytext>
+    select note__delete(:note_id);
+    </querytext>
+    </fullquery>
+ + + @param dbn The database name to use. If empty_string, uses the default database. + + @see /doc/db-api-detailed +} { + ad_arg_parser { bind_output bind } $args + + # Query Dispatcher (OpenACS - ben) + set full_statement_name [db_qd_get_fullname $statement_name] + + if { [info exists bind_output] } { + return -code error "the -bind_output switch is not currently supported" + } + + set driverkey [db_driverkey $dbn] + switch -- $driverkey { + postgresql { + set postgres_p 1 + } + + oracle - + nsodbc - + default { + set postgres_p 0 + } + } + + if { ! $postgres_p } { + db_with_handle -dbn $dbn db { + # Right now, use :1 as the output value if it occurs in the statement, + # or not otherwise. + set test_sql [db_qd_replace_sql $full_statement_name $sql] + if { [regexp {:1} $test_sql] } { + return [db_exec exec_plsql_bind $db $full_statement_name $sql 2 1 ""] + } else { + return [db_exec dml $db $full_statement_name $sql] + } + } + } else { + # Postgres doesn't have PL/SQL, of course, but it does have + # PL/pgSQL and other procedural languages. Rather than assign the + # result to a bind variable which is then returned to the caller, + # the Postgres version of OpenACS requires the caller to perform a + # select query that returns the value of the function. + + # We are no longer calling db_string, which screws up the bind + # variable stuff otherwise because of calling environments. (ben) + + ad_arg_parser { bind_output bind } $args + + # I'm not happy about having to get the fullname here, but right now + # I can't figure out a cleaner way to do it. I will have to + # revisit this ASAP. (ben) + set full_statement_name [db_qd_get_fullname $statement_name] + + if { [info exists bind_output] } { + return -code error "the -bind_output switch is not currently supported" + } + + db_with_handle -dbn $dbn db { + # plsql calls that are simple selects bypass the plpgsql + # mechanism for creating anonymous functions (OpenACS - Dan). + # if a table is being created, we need to bypass things, too (OpenACS - Ben). + set test_sql [db_qd_replace_sql $full_statement_name $sql] + if {[regexp -nocase -- {^\s*select} $test_sql match]} { + # ns_log Debug "PLPGSQL: bypassed anon function" + set selection [db_exec 0or1row $db $full_statement_name $sql] + } elseif {[regexp -nocase -- {^\s*(create|drop) table} $test_sql match]} { + ns_log Debug "PLPGSQL: bypassed anon function for create/drop table" + set selection [db_exec dml $db $full_statement_name $sql] + return "" + } else { + # ns_log Debug "PLPGSQL: using anonymous function" + set selection [db_exec_plpgsql $db $full_statement_name $sql $statement_name] + } + return [ns_set value $selection 0] + } + } +} + + +ad_proc -private db_exec_plpgsql { db statement_name pre_sql fname } { + + PostgreSQL only. +

+ + A helper procedure to execute a SQL statement, potentially binding + depending on the value of the $bind variable in the calling environment + (if set). + +

+ Low level replacement for db_exec which replaces inline code with a proc. + db proc is dropped after execution. This is a temporary fix until we can + port all of the db_exec_plsql calls to simple selects of the inline code + wrapped in function calls. + +

+ emulation of plsql calls from oracle. This routine takes the plsql + statements and wraps them in a function call, calls the function, and then + drops the function. Future work might involve converting this to cache the + function calls + +

+ This proc is private - use db_exec_plsql instead! + + @see db_exec_plsql + +} { + set start_time [expr {[clock clicks -microseconds]/1000.0}] + + set sql [db_qd_replace_sql $statement_name $pre_sql] + + set unique_id [db_nextval "anon_func_seq"] + + set function_name "__exec_${unique_id}_${fname}" + + # insert Tcl variable values (OpenACS - Dan) + if {$sql ne $pre_sql } { + set sql [uplevel 2 [list subst -nobackslashes $sql]] + } + ns_log Debug "PLPGSQL: converted: $sql to: select $function_name ()" + + # create a function definition statement for the inline code + # binding is emulated in tcl. (OpenACS - Dan) + + set errno [catch { + upvar bind bind + if { [info exists bind] && [llength $bind] != 0 } { + if { [llength $bind] == 1 } { + set bind_vars [list] + set len [ns_set size $bind] + for {set i 0} {$i < $len} {incr i} { + lappend bind_vars [ns_set key $bind $i] \ + [ns_set value $bind $i] + } + set proc_sql [db_bind_var_substitution $sql $bind_vars] + } else { + set proc_sql [db_bind_var_substitution $sql $bind] + } + } else { + set proc_sql [uplevel 2 [list db_bind_var_substitution $sql]] + } + + ns_db dml $db "create function $function_name () returns varchar as [::ns_dbquotevalue $proc_sql] language 'plpgsql'" + + set ret_val [ns_db 0or1row $db "select $function_name ()"] + + # drop the anonymous function (OpenACS - Dan) + # JCD: ignore return code -- maybe we should be smarter about this though. + catch {ns_db dml $db "drop function $function_name ()"} + + return $ret_val + + } error] + + set errinfo $::errorInfo + set errcode $::errorCode + + ds_collect_db_call $db 0or1row $statement_name $sql $start_time $errno $error + + if { $errno == 2 } { + return $error + } else { + catch {ns_db dml $db "drop function $function_name ()"} + } + + return -code $errno -errorinfo $errinfo -errorcode $errcode $error +} + +ad_proc -private db_get_quote_indices { sql } { + Given a piece of SQL, return the indices of single quotes. + This is useful when we do bind var substitution because we should + not attempt bind var substitution inside quotes. Examples: + +

+    sql          return value
+    {'a'}           {0 2}
+    {'a''}           {}
+    {'a'a'a'}       {0 2 4 6}
+    {a'b'c'd'}      {1 3 5 7}
+    
+ + @see db_bind_var_substitution +} { + set quote_indices [list] + + # Returns a list on the format + # Example - for sql={'a'a'a'} returns + # {0 2} {0 0} {2 2} {3 6} {4 4} {6 6} + set all_indices [regexp -inline -indices -all -- {(?:^|[^'])(')(?:[^']|'')+(')(?=$|[^'])} $sql] + + for {set i 0} { $i < [llength $all_indices] } { incr i 3 } { + lappend quote_indices [lindex $all_indices $i+1 0] [lindex $all_indices $i+2 0] + } + + return $quote_indices +} + +ad_proc -private db_bind_var_quoted_p { sql bind_start_idx bind_end_idx} { + +} { + foreach {quote_start_idx quote_end_idx} [db_get_quote_indices $sql] { + if { $bind_start_idx > $quote_start_idx && $bind_end_idx < $quote_end_idx } { + return 1 + } + } + + return 0 +} + +ad_proc -private db_bind_var_substitution { sql { bind "" } } { + + This proc emulates the bind variable substitution in the PostgreSQL driver. + Since this is a temporary hack, we do it in Tcl instead of hacking up the + driver to support plsql calls. This is only used for the db_exec_plpgsql + function. + +} { + if {$bind eq ""} { + upvar __db_sql lsql + set lsql $sql + uplevel { + set __db_lst [regexp -inline -indices -all -- {:?:\w+} $__db_sql] + for {set __db_i [expr {[llength $__db_lst] - 1}]} {$__db_i >= 0} {incr __db_i -1} { + set __db_ws [lindex $__db_lst $__db_i 0] + set __db_we [lindex $__db_lst $__db_i 1] + set __db_bind_var [string range $__db_sql $__db_ws $__db_we] + if {![string match "::*" $__db_bind_var] && ![db_bind_var_quoted_p $__db_sql $__db_ws $__db_we]} { + set __db_tcl_var [string range $__db_bind_var 1 end] + set __db_tcl_var [set $__db_tcl_var] + if {$__db_tcl_var eq ""} { + set __db_tcl_var null + } else { + set __db_tcl_var "[::ns_dbquotevalue $__db_tcl_var]" + } + set __db_sql [string replace $__db_sql $__db_ws $__db_we $__db_tcl_var] + } + } + } + } else { + + array set bind_vars $bind + + set lsql $sql + set lst [regexp -inline -indices -all -- {:?:\w+} $sql] + for {set i [expr {[llength $lst] - 1}]} {$i >= 0} {incr i -1} { + set ws [lindex $lst $i 0] + set we [lindex $lst $i 1] + set bind_var [string range $sql $ws $we] + if {![string match "::*" $bind_var] && ![db_bind_var_quoted_p $lsql $ws $we]} { + set tcl_var [string range $bind_var 1 end] + set val $bind_vars($tcl_var) + if {$val eq ""} { + set val null + } else { + set val "[::ns_dbquotevalue $val]" + } + set lsql [string replace $lsql $ws $we $val] + } + } + } + + return $lsql +} + + +ad_proc -private db_getrow { db selection } { + + A helper procedure to perform an ns_db getrow, invoking developer support + routines as necessary. + +} { + set start_time [expr {[clock clicks -microseconds]/1000.0}] + set errno [catch { return [ns_db getrow $db $selection] } error] + ds_collect_db_call $db getrow "" "" $start_time $errno $error + if { $errno == 2 } { + return $error + } + return -code $errno -errorinfo $::errorInfo -errorcode $::errorCode $error +} + + +ad_proc -public db_exec { type db statement_name pre_sql {ulevel 2} args } { + + A helper procedure to execute a SQL statement, potentially binding + depending on the value of the $bind variable in the calling environment + (if set). + +} { + set start_time [expr {[clock clicks -microseconds]/1000.0}] + set driverkey [db_driverkey -handle_p 1 $db] + + # Note: Although marked as private, db_exec is in fact called + # extensively from several other packages. We DEFINITELY don't + # want to have to change all those procs to pass in the + # (redundant) $dbn just so we can use it in the call to + # db_driverkey, so db_driverkey MUST support its -handle switch. + # --atp@piskorski.com, 2003/04/09 12:13 EDT + + set sql [db_qd_replace_sql $statement_name $pre_sql] + + # insert Tcl variable values (OpenACS - Dan) + if {$sql ne $pre_sql } { + set sql [uplevel $ulevel [list subst -nobackslashes $sql]] + } + + set errno [catch { + upvar bind bind + + if { [info exists bind] && [llength $bind] != 0 } { + if { [llength $bind] == 1 } { + # $bind is an ns_set id: + + switch -- $driverkey { + oracle { + return [ns_ora $type $db -bind $bind $sql {*}$args] + } + postgresql { + return [ns_pg_bind $type $db -bind $bind $sql] + } + nsodbc { + return [ns_odbc_bind $type $db -bind $bind $sql] + } + default { + error "Unknown database driver. Bind variables not supported for this database." + } + } + + } else { + # $bind is a Tcl list, convert it to an ns_set: + set bind_vars [ns_set create] + foreach { name value } $bind { + ns_set put $bind_vars $name $value + } + } + + switch -- $driverkey { + oracle { + # TODO: Using $args outside the list is + # potentially bad here, depending on what is in + # args and if the items contain any embedded + # whitespace. Or maybe it works fine. But it's + # hard to know. Document or fix. + # --atp@piskorski.com, 2003/04/09 15:33 EDT + + return [ns_ora $type $db -bind $bind_vars $sql {*}$args] + } + postgresql { + return [ns_pg_bind $type $db -bind $bind_vars $sql] + } + nsodbc { + return [ns_odbc_bind $type $db -bind $bind_vars $sql] + } + default { + error "Unknown database driver. Bind variables not supported for this database." + } + } + + } else { + # Bind variables, if any, are defined solely as individual + # Tcl variables: + + switch -- $driverkey { + oracle { + return [uplevel $ulevel [list ns_ora $type $db $sql] $args] + } + postgresql { + return [uplevel $ulevel [list ns_pg_bind $type $db $sql]] + } + nsodbc { + return [uplevel $ulevel [list ns_odbc_bind $type $db $sql]] + } + default { + # Using plain ns_db like this will work ONLY if + # the query is NOT using bind variables: + # --atp@piskorski.com, 2001/09/03 08:41 EDT + return [uplevel $ulevel [list ns_db $type $db $sql] $args] + } + } + } + } error] + + # JCD: we log the clicks, dbname, query time, and statement to catch long running queries. + # If we took more than 3 seconds yack about it. + if { [clock clicks -milliseconds] - $start_time > 3000 } { + set duration [format %.2f [expr {[clock clicks -milliseconds] - $start_time}]] + ns_log Warning "db_exec: longdb $duration ms $db $type $statement_name" + } else { + #set duration [format %.2f [expr {[clock clicks -milliseconds] - $start_time}]] + #ns_log Debug "db_exec: timing $duration seconds $db $type $statement_name" + } + + ds_collect_db_call $db $type $statement_name $sql $start_time $errno $error + if { $errno == 2 } { + return $error + } + + return -code $errno -errorinfo $::errorInfo -errorcode $::errorCode $error +} + + +ad_proc -public db_string { + {-dbn ""} + -cache_key + {-cache_pool db_cache_pool} + statement_name + sql + args +} { + + Usage: db_string statement-name sql [ -default default ] [ -bind bind_set_id | -bind bind_value_list ] + + @return the first column of the result of the SQL query sql. If the query doesn't return a row, returns default or raises an error if no default is provided. + + @param dbn The database name to use. If empty_string, uses the default database. + @param cache_key Cache the result using given value as the key. Default is to not cache. + @param cache_pool Override the default db_cache_pool +} { + # Query Dispatcher (OpenACS - ben) + set full_name [db_qd_get_fullname $statement_name] + + ad_arg_parser { default bind } $args + + set code { + db_with_handle -dbn $dbn db { + set selection [db_exec 0or1row $db $full_name $sql] + } + if { $selection eq ""} { + if { [info exists default] } { + return $default + } + error "Selection did not return a value, and no default was provided" + } + return [ns_set value $selection 0] + } + + if { [info exists cache_key] } { + return [ns_cache eval $cache_pool $cache_key $code] + } else { + return [eval $code] + } +} + + +ad_proc -public db_list { + {-dbn ""} + -cache_key + {-cache_pool db_cache_pool} + statement_name + sql + args +} { + + Usage: db_list statement-name sql [ -bind bind_set_id | -bind bind_value_list ] + + @return a Tcl list of the values in the first column of the result of SQL query sql. + If sql doesn't return any rows, returns an empty list. + + @param dbn The database name to use. If empty_string, uses the default database. + @param cache_key Cache the result using given value as the key. Default is to not cache. + @param cache_pool Override the default db_cache_pool +} { + ad_arg_parser { bind } $args + + # Query Dispatcher (OpenACS - SDW) + set full_statement_name [db_qd_get_fullname $statement_name] + + # Can't use db_foreach in this proc, since we need to use the ns_set directly. + + set code { + db_with_handle -dbn $dbn db { + set selection [db_exec select $db $full_statement_name $sql] + set result [list] + while { [db_getrow $db $selection] } { + lappend result [ns_set value $selection 0] + } + } + return $result + } + if { [info exists cache_key] } { + return [ns_cache eval $cache_pool $cache_key $code] + } else { + return [eval $code] + } +} + + +ad_proc -public db_list_of_lists { + {-dbn ""} + -cache_key + {-cache_pool db_cache_pool} + -with_headers:boolean + statement_name + sql + args +} { + + Usage: db_list_of_lists statement-name sql [ -bind bind_set_id | -bind bind_value_list ] + + @param with_headers when specified, first line of returned list of + lists will always be the list of column names as reported by the + database. Useful when you want to dynamically assign variables to + values returned in the list of lists. + + @return a Tcl list, each element of which is a list of all column + values in a row of the result of the SQL querysql. If + sql doesn't return any rows, returns an empty list, + unless with_headers flag was specified and in this case the only + element in the list will be the list of headers. + + It checks if the element is I18N and replaces it, thereby + reducing the need to do this with every single package + + @param dbn The database name to use. If empty_string, uses the default database. + @param cache_key Cache the result using given value as the key. Default is to not cache. + @param cache_pool Override the default db_cache_pool +} { + ad_arg_parser { bind } $args + + set code { + set result [list] + foreach selection [uplevel [list db_list_of_ns_sets -dbn $dbn $statement_name $sql]] { + set selection_array [ns_set array $selection] + if {[llength $result] == 0 && $with_headers_p} { + set headers [list] + foreach {key value} $selection_array { + lappend headers $key + } + lappend result $headers + } + set row [list] + foreach {key value} $selection_array { + lappend row $value + } + lappend result $row + } + set result + } + if { [info exists cache_key] } { + return [ns_cache eval $cache_pool $cache_key $code] + } else { + return [eval $code] + } +} + + +ad_proc -public db_list_of_ns_sets { + {-dbn ""} + statement_name + sql + args +} { + Usage: db_list_of_ns_sets statement-name sql [ -bind bind_set_id | -bind bind_value_list ] + + @return a list of ns_sets with the values of each column of each row + returned by the sql query specified. + + @param statement_name The name of the query. + @param sql The SQL to be executed. + @param args Any additional arguments. + + @return list of ns_sets, one per each row return by the SQL query + + @param dbn The database name to use. If empty_string, uses the default database. +} { + ad_arg_parser { bind } $args + + set full_statement_name [db_qd_get_fullname $statement_name] + + db_with_handle -dbn $dbn db { + set result [list] + set selection [db_exec select $db $full_statement_name $sql] + + while {[db_getrow $db $selection]} { + lappend result [ns_set copy $selection] + } + } + + return $result +} + + +ad_proc -public db_foreach { + {-dbn ""} + statement_name + sql + args +} { + + Usage: +
+ db_foreach statement-name sql [ -bind bind_set_id | -bind bind_value_list ] \ + [ -column_array array_name | -column_set set_name ] \ + code_block [ if_no_rows if_no_rows_block ] + +
+ +

Performs the SQL query sql, executing + code_block once for each row with variables set to + column values (or a set or array populated if -column_array or + column_set is specified). If the query returns no rows, executes + if_no_rows_block (if provided). In place of 'if_no_rows' also the 'else' keyword can be used.

+ +

Example: + +

db_foreach greeble_query "select foo, bar from greeble" {
+        ns_write "<li>foo=$foo; bar=$bar\n"
+    } if_no_rows {
+        # This block is optional.
+        ns_write "<li>No greebles!\n"
+    }
+ + @param dbn The database name to use. If empty_string, uses the default database. +} { + ad_arg_parser { bind column_array column_set args } $args + + # Do some syntax checking. + set arglength [llength $args] + if { $arglength == 1 } { + # Have only a code block. + set code_block [lindex $args 0] + } elseif { $arglength == 3 } { + # Should have code block + if_no_rows + code block. + if { [lindex $args 1] ni {"if_no_rows" "else"}} { + return -code error "Expected if_no_rows or else as second-to-last argument" + } + lassign $args code_block . if_no_rows_code_block + } else { + return -code error "Expected 1 or 3 arguments after switches" + } + + if { [info exists column_array] && [info exists column_set] } { + return -code error "Can't specify both column_array and column_set" + } + + if { [info exists column_array] } { + upvar 1 $column_array array_val + } + + if { [info exists column_set] } { + upvar 1 $column_set selection + } + + set counter 0 + foreach selection [uplevel [list db_list_of_ns_sets -dbn $dbn $statement_name $sql]] { + incr counter + if { ![info exists column_set] } { + set set_array [ns_set array $selection] + if { [info exists column_array] } { + unset -nocomplain array_val + array set array_val $set_array + } else { + foreach {a v} $set_array { uplevel [list set $a $v] } + } + } + set errno [catch { uplevel 1 $code_block } error] + + # + # Handle or propagate the error. + # + switch -- $errno { + 0 { + # TCL_OK + } + 1 { + # TCL_ERROR + error $error $::errorInfo $::errorCode + } + 2 { + # TCL_RETURN + error "Cannot return from inside a db_foreach loop" + } + 3 { + # TCL_BREAK + break + } + 4 { + # TCL_CONTINUE - just ignore and continue looping. + } + default { + error "Unknown return code: $errno" + } + } + } + # If the if_no_rows_code is defined, go ahead and run it. + if { $counter == 0 && [info exists if_no_rows_code_block] } { + uplevel 1 $if_no_rows_code_block + } +} + + +proc db_multirow_helper {} { + uplevel 1 { + if { !$append_p || ![info exists counter]} { + set counter 0 + } + + db_with_handle -dbn $dbn db { + set selection [db_exec select $db $full_statement_name $sql] + set local_counter 0 + + # Make sure 'next_row' array doesn't exist + # The this_row and next_row variables are used to always execute the code block one result set row behind, + # so that we have the opportunity to peek ahead, which allows us to do group by's inside + # the multirow generation + # Also make the 'next_row' array available as a magic __db_multirow__next_row variable + upvar 1 __db_multirow__next_row next_row + unset -nocomplain next_row + + set more_rows_p 1 + while { 1 } { + + if { $more_rows_p } { + set more_rows_p [db_getrow $db $selection] + } else { + break + } + + # Setup the 'columns' part, now that we know the columns in the result set + # And save variables which we might clobber, if '-unclobber' switch is specified. + if { $local_counter == 0 } { + for { set i 0 } { $i < [ns_set size $selection] } { incr i } { + lappend local_columns [ns_set key $selection $i] + } + lappend local_columns {*}$extend + if { !$append_p || ![info exists columns] } { + # store the list of columns in the var_name:columns variable + set columns $local_columns + } else { + # Check that the columns match, if not throw an error + if { [join [lsort -ascii $local_columns]] ne [join [lsort -ascii $columns]] } { + error "Appending to a multirow with differing columns. + Original columns : [join [lsort -ascii $columns] ", "]. + Columns in this query: [join [lsort -ascii $local_columns] ", "]" "" "ACS_MULTIROW_APPEND_COLUMNS_MISMATCH" + } + } + + # Save values of columns which we might clobber + if { $unclobber_p && $code_block ne "" } { + foreach col $columns { + upvar 1 $col column_value __saved_$col column_save + + if { [info exists column_value] } { + if { [array exists column_value] } { + array set column_save [array get column_value] + } else { + set column_save $column_value + } + + # Clear the variable + unset column_value + } + } + } + } + + if { $code_block eq "" } { + # No code block - pull values directly into the var_name array. + + # The extra loop after the last row is only for when there's a code block + if { !$more_rows_p } { + break + } + incr counter + upvar $level_up "$var_name:$counter" array_val + set array_val(rownum) $counter + for { set i 0 } { $i < [ns_set size $selection] } { incr i } { + set array_val([ns_set key $selection $i]) \ + [ns_set value $selection $i] + } + } else { + # There is a code block to execute + + # Copy next_row to this_row, if it exists + unset -nocomplain this_row + set array_get_next_row [array get next_row] + if { $array_get_next_row ne "" } { + array set this_row [array get next_row] + } + + # Pull values from the query into next_row + unset -nocomplain next_row + if { $more_rows_p } { + for { set i 0 } { $i < [ns_set size $selection] } { incr i } { + set next_row([ns_set key $selection $i]) [ns_set value $selection $i] + } + } + + # Process the row + if { [info exists this_row] } { + # Pull values from this_row into local variables + foreach name [array names this_row] { + upvar 1 $name column_value + set column_value $this_row($name) + } + + # Initialize the "extend" columns to the empty string + foreach column_name $extend { + upvar 1 $column_name column_value + set column_value "" + } + + # Execute the code block + set errno [catch { uplevel 1 $code_block } error] + + # Handle or propagate the error. Can't use the usual + # "return -code $errno..." trick due to the db_with_handle + # wrapped around this loop, so propagate it explicitly. + switch -- $errno { + 0 { + # TCL_OK + } + 1 { + # TCL_ERROR + error $error $::errorInfo $::errorCode + } + 2 { + # TCL_RETURN + error "Cannot return from inside a db_multirow loop" + } + 3 { + # TCL_BREAK + ns_db flush $db + break + } + 4 { + # TCL_CONTINUE + continue + } + default { + error "Unknown return code: $errno" + } + } + + # Pull the local variables back out and into the array. + incr counter + upvar $level_up "$var_name:$counter" array_val + set array_val(rownum) $counter + foreach column_name $columns { + upvar 1 $column_name column_value + set array_val($column_name) $column_value + } + } + } + incr local_counter + } + } + + # Restore values of columns which we've saved + if { $unclobber_p && $code_block ne "" && $local_counter > 0 } { + foreach col $columns { + upvar 1 $col column_value __saved_$col column_save + + # Unset it first, so the road's paved to restoring + unset -nocomplain column_value + + # Restore it + if { [info exists column_save] } { + if { [array exists column_save] } { + array set column_value [array get column_save] + } else { + set column_value $column_save + } + + # And then remove the saved col + unset column_save + } + } + } + # Unset the next_row variable, just in case + unset -nocomplain next_row + } +} + +ad_proc -public db_multirow { + -local:boolean + -append:boolean + {-upvar_level 1} + -unclobber:boolean + {-extend {}} + {-dbn ""} + -cache_key + {-cache_pool db_cache_pool} + var_name + statement_name + sql + args +} { + @param dbn The database name to use. If empty_string, uses the default database. + @param cache_key Cache the result using given value as the key. Default is to not cache. + @param cache_pool Override the default db_cache_pool + + @param unclobber If set, will cause the proc to not overwrite local variables. Actually, what happens + is that the local variables will be overwritten, so you can access them within the code block. However, + if you specify -unclobber, we will revert them to their original state after execution of this proc. + + Usage: +
+ db_multirow [ -local ] [ -upvar_level n_levels_up ] [ -append ] [ -extend column_list ] \ + var-name statement-name sql [ -bind bind_set_id | -bind bind_value_list ] \ + code_block [ if_no_rows if_no_rows_block ] + +
+ +

Performs the SQL query sql, saving results in variables + of the form + var_name:1, var_name:2, etc, + setting var_name:rowcount to the total number + of rows, and setting var_name:columns to a + list of column names. + +

+ + If "cache_key" is set, cache the array that results from the query *and* + any code block for future use. When this result is returned from cache, + THE CODE BLOCK IS NOT EXECUTED. Therefore any values calculated by the + code block that aren't listed as arguments to "extend" will + not be created. In practice this impacts relatively few queries, but do + take care. + +

+ + You can not simultaneously append to and cache a non-empty multirow. + +

+ + Each row also has a column, rownum, automatically + added and set to the row number, starting with 1. Note that this will + override any column in the SQL statement named 'rownum', also if you're + using the Oracle rownum pseudo-column. + +

+ + If the -local is passed, the variables defined + by db_multirow will be set locally (useful if you're compiling dynamic templates + in a function or similar situations). Use the -upvar_level + switch to specify how many levels up the variable should be set. + +

+ + You may supply a code block, which will be executed for each row in + the loop. This is very useful if you need to make computations that + are better done in Tcl than in SQL, for example using ns_urlencode + or ad_quotehtml, etc. When the Tcl code is executed, all the columns + from the SQL query will be set as local variables in that code. Any + changes made to these local variables will be copied back into the + multirow. + +

+ + You may also add additional, computed columns to the multirow, using the + -extend { col_1 col_2 ... } switch. This is + useful for things like constructing a URL for the object retrieved by + the query. + +

+ + If you're constructing your multirow through multiple queries with the + same set of columns, but with different rows, you can use the + -append switch. This causes the rows returned by this query + to be appended to the rows already in the multirow, instead of starting + a clean multirow, as is the normal behavior. The columns must match the + columns in the original multirow, or an error will be thrown. + +

+ + Your code block may call continue in order to skip a row + and not include it in the multirow. Or you can call break + to skip this row and quit looping. + +

+ + Notice the nonstandard numbering (everything + else in Tcl starts at 0); the reason is that the graphics designer, a non + programmer, may wish to work with row numbers. + +

+ + Example: +

db_multirow -extend { user_url } users users_query {
+        select user_id first_names, last_name, email from cc_users
+    } {
+        set user_url [acs_community_member_url -user_id $user_id]
+    }
+ + @see template::multirow +} { + # Query Dispatcher (OpenACS - ben) + set full_statement_name [db_qd_get_fullname $statement_name] + + if { $local_p } { + set level_up $upvar_level + } else { + set level_up \#[template::adp_level] + } + + ad_arg_parser { bind args } $args + + # Do some syntax checking. + set arglength [llength $args] + if { $arglength == 0 } { + # No code block. + set code_block "" + } elseif { $arglength == 1 } { + # Have only a code block. + set code_block [lindex $args 0] + } elseif { $arglength == 3 } { + # Should have code block + if_no_rows + code block. + if { [lindex $args 1] ne "if_no_rows" + && [lindex $args 1] ne "else" + } { + return -code error "Expected if_no_rows as second-to-last argument" + } + lassign $args code_block . if_no_rows_code_block + } else { + return -code error "Expected 1 or 3 arguments after switches" + } + + upvar $level_up "$var_name:rowcount" counter + upvar $level_up "$var_name:columns" columns + + if { [info exists cache_key] + && $append_p + && [info exists counter] && $counter > 0 + } { + return -code error "Can't append and cache a non-empty multirow datasource simultaneously" + } + + if { [info exists cache_key] } { + + set value [ns_cache eval $cache_pool $cache_key { + db_multirow_helper + + set values [list] + + for { set count 1 } { $count <= $counter } { incr count } { + upvar $level_up "$var_name:[expr {$count}]" array_val + lappend values [array get array_val] + } + + return [list $counter $columns $values] + }] + + lassign $value counter columns values + + set count 1 + foreach value $values { + upvar $level_up "$var_name:[expr {$count}]" array_val + array set array_val $value + incr count + } + } else { + db_multirow_helper + } + + + # If the if_no_rows_code is defined, go ahead and run it. + if { $counter == 0 && [info exists if_no_rows_code_block] } { + uplevel 1 $if_no_rows_code_block + } +} + +ad_proc -public db_multirow_group_last_row_p { + {-column:required} +} { + Used inside the code_block to db_multirow to ask whether this row is the last row + before the value of 'column' changes, or the last row of the result set. + +

+ + This is useful when you want to build up a multirow for a master/slave table pair, + where you only want one row per row in the master table, but you want to include + data from the slave table in a column of the multirow. + +

+ + Here's an example: + +

+    # Initialize the lines variable to hold a list of order line summaries
+    set lines [list]
+
+    # Start building the multirow. We add the dynamic column 'lines_pretty', which will
+    # contain the pretty summary of the order lines.
+    db_multirow -extend { lines_pretty } orders select_orders_and_lines {
+        select o.order_id,
+        o.customer_name,
+        l.item_name,
+        l.quantity
+        from   orders o,
+        order_lines l
+        where  l.order_id = o.order_id
+        order  by o.order_id, l.item_name
+    } {
+        lappend lines "$quantity $item_name"
+        if { [db_multirow_group_last_row_p -column order_id] } {
+            # Last row of this order, prepare the pretty version of the order lines
+            set lines_pretty [join $lines ", "]
+
+            # Reset the lines list, so we start from a fresh with the next row
+            set lines [list]
+        } else {
+            # There are yet more order lines to come for this order,
+            # continue until we've collected all the order lines
+            # The 'continue' keyword means this line will not be added to the resulting multirow
+            continue
+        }
+    }
+    
+ + @author Lars Pind (lars@collaboraid.biz) + + @param column The name of the column defining the groups. + + @return 1 if this is the last row before the column value changes, 0 otherwise. +} { + upvar 1 __db_multirow__next_row next_row + if { ![info exists next_row] } { + # If there is no next row, this is the last row + return 1 + } + upvar 1 $column column_value + # Otherwise, it's the last row in the group if the next row has a different value than this row + return [expr {$column_value ne $next_row($column) }] +} + + +ad_proc -public db_dml {{-dbn ""} statement_name sql args } { + Do a DML statement. + +

+ + args can be one of: -clobs, -blobs, -clob_files or -blob_files. See the db-api doc referenced below for more information. + + @param dbn The database name to use. If empty_string, uses the default database. + + @see /doc/db-api-detailed +} { + ad_arg_parser { clobs blobs clob_files blob_files bind } $args + set driverkey [db_driverkey $dbn] + + switch -- $driverkey { + postgresql { + set postgres_p 1 + } + oracle - + nsodbc - + default { + set postgres_p 0 + } + } + + # Query Dispatcher (OpenACS - ben) + set full_statement_name [db_qd_get_fullname $statement_name] + + # This "only one of..." check didn't exist in the PostgreSQL + # version, but it shouldn't't hurt anything: --atp@piskorski.com, + # 2003/04/08 06:19 EDT + + # Only one of clobs, blobs, clob_files, and blob_files is allowed. + # Remember which one (if any) is provided: + + set lob_argc 0 + set lob_argv [list] + set command "dml" + if { [info exists clobs] } { + set command "clob_dml" + set lob_argv $clobs + incr lob_argc + } + if { [info exists blobs] } { + set command "blob_dml" + set lob_argv $blobs + incr lob_argc + } + if { [info exists clob_files] } { + set command "clob_dml_file" + set lob_argv $clob_files + incr lob_argc + } + if { [info exists blob_files] } { + set command "blob_dml_file" + set lob_argv $blob_files + incr lob_argc + } + if { $lob_argc > 1 } { + error "Only one of -clobs, -blobs, -clob_files, or -blob_files may be specified as an argument to db_dml" + } + + if { ! $postgres_p } { + # Oracle: + db_with_handle -dbn $dbn db { + if { $lob_argc == 1 } { + # Bind :1, :2, ..., :n as LOBs (where n = [llength $lob_argv]) + set bind_vars [list] + for { set i 1 } { $i <= [llength $lob_argv] } { incr i } { + lappend bind_vars $i + } + eval [list db_exec "${command}_bind" $db $full_statement_name $sql 2 $bind_vars] $lob_argv + } else { + eval [list db_exec $command $db $full_statement_name $sql] $lob_argv + } + } + + } elseif {$command eq "blob_dml_file"} { + # PostgreSQL: + db_with_handle -dbn $dbn db { + # another ugly hack to avoid munging Tcl files. + # __lob_id needs to be set inside of a query (.xql) file for this + # to work. Say for example that you need to create a lob. In + # Oracle, you would do something like: + + # db_dml update_photo "update foo set bar = empty_blob() + # where bar = :bar + # returning foo into :1" -blob_files [list $file] + # for PostgreSQL we can do the equivalent by placing the following + # in a query file: + # update foo set bar = [set __lob_id [db_string get_id "select empty_lob()"]] + # where bar = :bar + + # __lob_id acts as a flag that signals that blob_dml_file is + # required, and it is also used to pass along the lob_id. It + # is unsert afterwards to avoid name clashes with other invocations + # of this routine. + # (DanW - Openacs) + + db_exec dml $db $full_statement_name $sql + if {[uplevel {info exists __lob_id}]} { + ns_pg blob_dml_file $db [uplevel {set __lob_id}] $blob_files + uplevel {unset __lob_id} + } + } + + } else { + # PostgreSQL: + db_with_handle -dbn $dbn db { + db_exec dml $db $full_statement_name $sql + } + } +} + + + + +ad_proc -public db_0or1row { + {-dbn ""} + -cache_key + {-cache_pool db_cache_pool} + statement_name + sql + args +} { + + Usage: +

+ db_0or1row statement-name sql [ -bind bind_set_id | -bind bind_value_list ] \ + [ -column_array array_name | -column_set set_name ] + +
+ +

Performs the SQL query sql. If a row is returned, sets variables + to column values (or a set or array populated if -column_array + or column_set is specified) and returns 1. If no rows are returned, + returns 0. + + @return 1 if variables are set, 0 if no rows are returned. If more than one row is returned, throws an error. + + @param dbn The database name to use. If empty_string, uses the default database. + @param cache_key Cache the result using given value as the key. Default is to not cache. + @param cache_pool Override the default db_cache_pool +} { + ad_arg_parser { bind column_array column_set } $args + + # Query Dispatcher (OpenACS - ben) + set full_statement_name [db_qd_get_fullname $statement_name] + + if { [info exists column_array] && [info exists column_set] } { + return -code error "Can't specify both column_array and column_set" + } + + if { [info exists column_array] } { + upvar 1 $column_array array_val + unset -nocomplain array_val + } + + if { [info exists column_set] } { + upvar 1 $column_set selection + } + + if { [info exists cache_key] } { + set values [ns_cache eval $cache_pool $cache_key { + db_with_handle -dbn $dbn db { + set selection [db_exec 0or1row $db $full_statement_name $sql] + } + + set values [list] + + if { $selection ne "" } { + for {set i 0} { $i < [ns_set size $selection] } {incr i} { + lappend values [list [ns_set key $selection $i] [ns_set value $selection $i]] + } + } + + set values + }] + + if { $values eq "" } { + set selection "" + } else { + set selection [ns_set create] + + foreach value $values { + ns_set put $selection [lindex $value 0] [lindex $value 1] + } + } + } else { + db_with_handle -dbn $dbn db { + set selection [db_exec 0or1row $db $full_statement_name $sql] + } + } + + if { $selection eq "" } { + return 0 + } + + if { [info exists column_array] } { + array set array_val [ns_set array $selection] + } elseif { ![info exists column_set] } { + for { set i 0 } { $i < [ns_set size $selection] } { incr i } { + uplevel 1 [list set [ns_set key $selection $i] [ns_set value $selection $i]] + } + } + + return 1 +} + + +ad_proc -public db_1row { args } { + + A wrapper for db_0or1row, which produces an error if no rows are returned. + + @param args Arguments to be passed to db_0or1row. Check db_0or1row proc doc + for details. + + @see db_0or1row + + @return 1 if variables are set. + +} { + if { ![uplevel ::db_0or1row $args] } { + return -code error "Query did not return any rows." + } +} + +if {[info commands ns_cache_transaction_begin] eq ""} { + # + # When the server has no support for ns_cache_transaction_*, + # provide dummy procs to avoid runtime "if" statements. + # + proc ns_cache_transaction_begin args {;} + proc ns_cache_transaction_commit args {;} + proc ns_cache_transaction_rollback args {;} +} + +ad_proc -public db_transaction {{ -dbn ""} transaction_code args } { + Usage: db_transaction transaction_code [ on_error { error_code_block } ] + + Executes transaction_code with transactional semantics. This means that either all of the database commands + within transaction_code are committed to the database or none of them are. Multiple db_transactions may be + nested (end transaction is transparently ns_db dml'ed when the outermost transaction completes).

+ + To handle errors, use db_transaction {transaction_code} on_error {error_code_block}. Any error generated in + transaction_code will be caught automatically and process control will transfer to error_code_block + with a variable errmsg set. The error_code block can then clean up after the error, such as presenting a usable + error message to the user. Following the execution of error_code_block the transaction will be aborted. + If you want to explicitly abort the transaction, call db_abort_transaction + from within the transaction_code block or the error_code block.

+ + Example 1:
+ In this example, db_dml triggers an error, so control passes to the on_error block which prints a readable error. +

+    db_transaction {
+        db_dml test "nonsense"
+    } on_error {
+        ad_return_error "Error in blah/foo/bar" "The error was: $errmsg"
+    }
+    
+ + Example 2:
+ In this example, the second command, "nonsense" triggers an error. There is no on_error block, so the + transaction is immediately halted and aborted. +
+    db_transaction {
+        db_dml test {insert into footest values(1)}
+        nonsense
+        db_dml test {insert into footest values(2)}
+    }
+    
+ + @param dbn The database name to use. If empty_string, uses the default database. +} { + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + set syn_err "db_transaction: Invalid arguments. Use db_transaction { code } \[on_error { error_code_block }\] " + set arg_c [llength $args] + + if { $arg_c != 0 && $arg_c != 2 } { + # Either this is a transaction with no error handling or there must be an on_error { code } block. + error $syn_err + } elseif { $arg_c == 2 } { + # We think they're specifying an on_error block + if {[lindex $args 0] ne "on_error" } { + # Unexpected: they put something besides on_error as a connector. + error $syn_err + } else { + # Success! We got an on_error code block. + set on_error [lindex $args 1] + } + } + # Make the error message and database handle available to the on_error block. + upvar errmsg errmsg + + db_with_handle -dbn $dbn db { + # Preserve the handle, since db_with_handle kills it after executing + # this block. + set dbh $db + # Remember that there's a transaction happening on this handle. + if { ![info exists db_state(transaction_level,$dbh)] } { + set db_state(transaction_level,$dbh) 0 + } + set level [incr db_state(transaction_level,$dbh)] + if { $level == 1 } { + ns_db dml $dbh "begin transaction" + ns_cache_transaction_begin + } + } + # Execute the transaction code. + set errno [catch { + uplevel 1 $transaction_code + } errmsg] + incr db_state(transaction_level,$dbh) -1 + + set err_p 0 + switch -- $errno { + 0 { + # TCL_OK + } + 2 { + # TCL_RETURN + } + 3 { + # TCL_BREAK - Abort the transaction and do the break. + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + db_release_unused_handles -dbn $dbn + break + } + 4 { + # TCL_CONTINUE - just ignore. + } + default { + # TCL_ERROR or unknown error code: Its a real error. + set err_p 1 + } + } + + if { $err_p || [db_abort_transaction_p -dbn $dbn]} { + # An error was triggered or the transaction has been aborted. + db_abort_transaction -dbn $dbn + if { [info exists on_error] && $on_error ne "" } { + + if {"postgresql" eq [db_type]} { + + # JCD: with postgres we abort the transaction prior to + # executing the on_error block since there is nothing + # you can do to "fix it" and keeping it meant things like + # queries in the on_error block would then fail. + # + # Note that the semantics described in the proc doc + # are not possible to support on PostgreSQL. + + # DRB: I removed the db_release_unused_handles call that + # this patch included because additional aborts further + # down triggered an illegal db handle error. I'm going to + # have the code start a new transaction as well. If we + # don't, if a transaction fails and the on_error block + # fails, the on_error block DML will have been committed. + # Starting a new transaction here means that DML by both + # the transaction and on_error clause will be rolled back. + # On the other hand, if the on_error clause doesn't fail, + # any DML in that block will be committed. This seems more + # useful than simply punting ... + + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + ns_db dml $dbh "begin transaction" + ns_cache_transaction_begin + + } + + # An on_error block exists, so execute it. + + set errno [catch { + uplevel 1 $on_error + } on_errmsg] + + # Determine what do with the error. + set err_p 0 + switch -- $errno { + 0 { + # TCL_OK + } + + 2 { + # TCL_RETURN + } + 3 { + # TCL_BREAK + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + db_release_unused_handles + break + } + 4 { + # TCL_CONTINUE - just ignore. + } + default { + # TCL_ERROR or unknown error code: Its a real error. + set err_p 1 + } + } + + if { $err_p } { + # An error was generated from the $on_error block. + if { $level == 1} { + # We're at the top level, so we abort the transaction. + set db_state(db_abort_p,$dbh) 0 + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + } + # We throw this error because it was thrown from the error handling code that the programmer must fix. + error $on_errmsg $::errorInfo $::errorCode + } else { + # Good, no error thrown by the on_error block. + if { [db_abort_transaction_p -dbn $dbn] } { + # This means we should abort the transaction. + if { $level == 1 } { + set db_state(db_abort_p,$dbh) 0 + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + # We still have the transaction generated error. We don't want to throw it, so we log it. + ns_log Error "Aborting transaction due to error:\n$errmsg" + } else { + # Propagate the error up to the next level. + error $errmsg $::errorInfo $::errorCode + } + } else { + # The on_error block has resolved the transaction error. If we're at the top, commit and exit. + # Otherwise, we continue on through the lower transaction levels. + if { $level == 1} { + ns_db dml $dbh "end transaction" + ns_cache_transaction_commit + } + } + } + } else { + # There is no on_error block, yet there is an error, so we propagate it. + if { $level == 1 } { + set db_state(db_abort_p,$dbh) 0 + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + error "Transaction aborted: $errmsg" $::errorInfo $::errorCode + } else { + db_abort_transaction -dbn $dbn + error $errmsg $::errorInfo $::errorCode + } + } + } else { + # There was no error from the transaction code. + if { [db_abort_transaction_p -dbn $dbn] } { + # The user requested the transaction be aborted. + if { $level == 1 } { + set db_state(db_abort_p,$dbh) 0 + ns_db dml $dbh "abort transaction" + ns_cache_transaction_rollback + } + } elseif { $level == 1 } { + # Success! No errors and no requested abort. Commit. + ns_db dml $dbh "end transaction" + ns_cache_transaction_commit + } + } +} + + +ad_proc -public db_abort_transaction {{-dbn ""}} { + + Aborts all levels of a transaction. That is if this is called within + several nested transactions, all of them are terminated. Use this + instead of db_dml "abort" "abort transaction". + + @param dbn The database name to use. If empty_string, uses the default database. +} { + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + db_with_handle -dbn $dbn db { + # We set the abort flag to true. + set db_state(db_abort_p,$db) 1 + } +} + + +ad_proc -private db_abort_transaction_p {{-dbn ""}} { + @param dbn The database name to use. If empty_string, uses the default database. +} { + upvar "#0" [db_state_array_name_is -dbn $dbn] db_state + + db_with_handle -dbn $dbn db { + if { [info exists db_state(db_abort_p,$db)] } { + return $db_state(db_abort_p,$db) + } else { + # No abort flag registered, so we assume everything is ok. + return 0 + } + } +} + + +ad_proc -public db_name {{-dbn ""}} { + + @return the name of the database as reported by the driver. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + db_with_handle -dbn $dbn db { + set dbtype [ns_db dbtype $db] + } + return $dbtype +} + + +ad_proc -public db_get_username {{-dbn ""}} { + @return the username parameter from the driver section of the + first database pool for the dbn. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + return [ns_config "ns/db/pool/$pool" User] +} + +ad_proc -public db_get_password {{-dbn ""}} { + @return the password parameter from the driver section of the + first database pool for the dbn. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + return [ns_config "ns/db/pool/$pool" Password] +} + +ad_proc -public db_get_sql_user {{-dbn ""}} { + Oracle only. + +

+ @return a valid Oracle user@database/password string to access a + database through sqlplus. + +

+ This proc may well work for databases other than Oracle, + but its return value won't really be of any use. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + set datasource [ns_config "ns/db/pool/$pool" DataSource] + if { $datasource ne "" && ![string is space $datasource] } { + return "[ns_config ns/db/pool/$pool User]/[ns_config ns/db/pool/$pool Password]@$datasource" + } else { + return "[ns_config ns/db/pool/$pool User]/[ns_config ns/db/pool/$pool Password]" + } +} + +ad_proc -public db_get_pgbin {{-dbn ""}} { + PostgreSQL only. + +

+ @return the pgbin parameter from the driver section of the first database pool. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + set driver [ns_config "ns/db/pool/$pool" Driver] + return [ns_config "ns/db/driver/$driver" pgbin] +} + + +ad_proc -public db_get_port {{-dbn ""}} { + PostgreSQL only. + +

+ @return the port number from the first database pool. It assumes the + datasource is properly formatted since we've already verified that we + can connect to the pool. + It returns an empty string for an empty port value. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + set datasource [ns_config "ns/db/pool/$pool" DataSource] + set last_colon_pos [string last ":" $datasource] + if { $last_colon_pos == -1 } { + ns_log Error "datasource contains no \":\"? datasource = $datasource" + return "" + } + set first_colon_pos [string first ":" $datasource] + + if { $first_colon_pos == $last_colon_pos || ($last_colon_pos - $first_colon_pos) == 1 } { + # No port specified + return "" + } + + return [string range $datasource $first_colon_pos+1 $last_colon_pos-1] +} + + +ad_proc -public db_get_database {{-dbn ""}} { + PostgreSQL only. + +

+ @return the database name from the first database pool. It assumes the + datasource is properly formatted since we've already verified that we + can connect to the pool. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + set datasource [ns_config "ns/db/pool/$pool" DataSource] + set last_colon_pos [string last ":" $datasource] + if { $last_colon_pos == -1 } { + ns_log Error "datasource contains no \":\"? datasource = $datasource" + return "" + } + return [string range $datasource $last_colon_pos+1 end] +} + + +ad_proc -public db_get_dbhost { + {-dbn ""} +} { + PostgreSQL only. + +

+ @return the name of the database host from the first database pool. + It assumes the datasource is properly formatted since we've already + verified that we can connect to the pool. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set pool [lindex [db_available_pools $dbn] 0] + set datasource [ns_config "ns/db/pool/$pool" DataSource] + set first_colon_pos [string first ":" $datasource] + if { $first_colon_pos == -1 } { + ns_log Error "datasource contains no \":\"? datasource = $datasource" + return "" + } + return [string range $datasource 0 $first_colon_pos-1] +} + +ad_proc -public db_source_sql_file { + {-dbn ""} + {-callback apm_ns_write_callback} + file +} { + Sources a SQL file into Oracle (SQL*Plus format file) or + PostgreSQL (psql format file). + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set proc_name {db_source_sql_file} + set driverkey [db_driverkey $dbn] + + switch -- $driverkey { + + oracle { + set user_pass [db_get_sql_user -dbn $dbn] + cd [file dirname $file] + set fp [open "|[file join $::env(ORACLE_HOME) bin sqlplus] $user_pass @$file" "r+"] + fconfigure $fp -buffering line + puts $fp "exit" + + while { [gets $fp line] >= 0 } { + # Don't bother writing out lines which are purely whitespace. + if { ![string is space $line] } { + apm_callback_and_log $callback "[ns_quotehtml $line]\n" + } + } + close $fp + } + + postgresql { + set file_name [file tail $file] + + set pguser [db_get_username] + if { $pguser ne "" } { + set pguser "-U $pguser" + } + + set pgport [db_get_port] + if { $pgport ne "" } { + set pgport "-p $pgport" + } + + set pgpass [db_get_password] + if { $pgpass ne "" } { + set pgpass "<<$pgpass" + } + + # DRB: Submitted patch was in error - the driver opens a -h hostname connection + # unless the hostname is localhost. We need to do the same here. The submitted + # patch checked for a blank hostname, which fails in the driver. Arguably the + # driver's wrong but a lot of non-OpenACS folks use it, and even though I'm the + # maintainer we shouldn't break existing code over such trivialities... + # GN: windows requires $pghost "-h ..." + + if { ([db_get_dbhost] eq "localhost" || [db_get_dbhost] eq "") + && $::tcl_platform(platform) ne "windows" + } { + set pghost "" + } else { + set pghost "-h [db_get_dbhost]" + } + + set errno [catch { + cd [file dirname $file] + set fp [open "|[file join [db_get_pgbin] psql] $pghost $pgport $pguser -f $file [db_get_database] $pgpass" "r"] + } errorMsg] + + if {$errno > 0} { + set error_found 1 + set error_lines $errorMsg + } else { + while { [gets $fp line] >= 0 } { + # Don't bother writing out lines which are purely whitespace. + if { ![string is space $line] } { + apm_callback_and_log $callback "[ns_quotehtml $line]\n" + } + } + + # PSQL dumps errors and notice information on stderr, and has no option to turn + # this off. So we have to chug through the "error" lines looking for those that + # really signal an error. + + set errno [ catch { + close $fp + } error] + + if { $errno == 2 } { + return $error + } + + # Just filter out the "NOTICE" lines, so we get the stack dump along with real + # ERRORs. This could be done with a couple of opaque-looking regexps... + + set error_found 0 + foreach line [split $error "\n"] { + if { [string first NOTICE $line] == -1 } { + append error_lines "$line\n" + set error_found [expr { $error_found + || [string first ERROR $line] != -1 + || [string first FATAL $line] != -1 } ] + } + } + } + + if { $error_found } { + return -code error -errorinfo $error_lines -errorcode $::errorCode $error_lines + } + + } + + nsodbc { + error "$proc_name is not supported for this database." + } + default { + error "$proc_name is not supported for this database." + } + } +} + +ad_proc -public db_load_sql_data { + {-dbn ""} + {-callback apm_ns_write_callback} + file +} { + Loads a CSV formatted file into a table using PostgreSQL's COPY command or + Oracle's SQL*Loader utility. The file name format consists of a sequence + number used to control the order in which tables are loaded, and the table + name with "-" replacing "_". This is a bit of a kludge but greatly speeds + the loading of large amounts of data, such as is done when various "ref-*" + packages are installed. + + @param dbn The database name to use. If empty_string, uses the default database. + @param file Filename in the format dd-table-name.ctl where 'dd' is a sequence number + used to control the order in which data is loaded. This file is an + RDBMS-specific data loader control file. + +} { + + switch [db_driverkey $dbn] { + + oracle { + set user_pass [db_get_sql_user -dbn $dbn] + set tmpnam [ad_tmpnam] + + set fd [open $file r] + set file_contents [read $fd] + close $fd + + set file_contents [subst $file_contents] + + set fd1 [open "${tmpnam}.ctl" w] + puts $fd1 $file_contents + close $fd1 + + cd [file dirname $file] + + set fd [open "|[file join $::env(ORACLE_HOME) bin sqlldr] userid=$user_pass control=$tmpnam" "r"] + + while { [gets $fd line] >= 0 } { + # Don't bother writing out lines which are purely whitespace. + if { ![string is space $line] } { + apm_callback_and_log $callback "[ns_quotehtml $line]\n" + } + } + close $fd + } + + postgresql { + set pguser [db_get_username] + if { $pguser ne "" } { + set pguser "-U $pguser" + } + + set pgport [db_get_port] + if { $pgport ne "" } { + set pgport "-p $pgport" + } + + set pgpass [db_get_password] + if { $pgpass ne "" } { + set pgpass "<<$pgpass" + } + + if { [db_get_dbhost] eq "localhost" || [db_get_dbhost] eq "" } { + set pghost "" + } else { + set pghost "-h [db_get_dbhost]" + } + + set fd [open $file r] + set copy_command [subst -nobackslashes [read $fd]] + close $fd + set copy_file [ns_mktemp [ad_tmpdir]/psql-copyfile-XXXXXX] + set fd [open $copy_file "CREAT EXCL WRONLY" 0600] + puts $fd $copy_command + close $fd + + if { $::tcl_platform(platform) eq "windows" } { + set fp [open "|[file join [db_get_pgbin] psql] -f $copy_file $pghost $pgport $pguser [db_get_database]" "r"] + } else { + set fp [open "|[file join [db_get_pgbin] psql] -f $copy_file $pghost $pgport $pguser [db_get_database] $pgpass" "r"] + } + + while { [gets $fp line] >= 0 } { + # Don't bother writing out lines which are purely whitespace. + if { ![string is space $line] } { + apm_callback_and_log $callback "[ns_quotehtml $line]\n" + } + } + + # PSQL dumps errors and notice information on stderr, and has no option to turn + # this off. So we have to chug through the "error" lines looking for those that + # really signal an error. + + set errno [ catch { + close $fp + } error] + + # remove the copy file. + file delete -force -- $copy_file + + if { $errno == 2 } { + return $error + } + + # Just filter out the "NOTICE" lines, so we get the stack dump along with real + # ERRORs. This could be done with a couple of opaque-looking regexps... + + set error_found 0 + foreach line [split $error "\n"] { + if { [string first NOTICE $line] == -1 } { + append error_lines "$line\n" + set error_found [expr { $error_found + || [string first ERROR $line] != -1 + || [string first FATAL $line] != -1 } ] + } + } + + if { $error_found } { + return -code error -errorinfo $error_lines -errorcode $::errorCode $error_lines + } + + } + + nsodbc { + error "db_load_sql_data is not supported for this database." + } + default { + error "db_load_sql_data is not supported for this database." + } + } +} + +ad_proc -public db_source_sqlj_file { + {-dbn ""} + {-callback apm_ns_write_callback} + file +} { + Oracle only. +

+ Sources a SQLJ file using loadjava. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + set user_pass [db_get_sql_user -dbn $dbn] + set fp [open "|[file join $::env(ORACLE_HOME) bin loadjava] -verbose -user $user_pass $file" "r"] + + # Despite the fact that this works, the text does not get written to the stream. + # The output is generated as an error when you attempt to close the input stream as + # done below. + while { [gets $fp line] >= 0 } { + # Don't bother writing out lines which are purely whitespace. + if { ![string is space $line] } { + apm_callback_and_log $callback "[ns_quotehtml $line]\n" + } + } + if { [catch { + close $fp + } errmsg] } { + apm_callback_and_log $callback "[ns_quotehtml $errmsg]\n" + } +} + + +ad_proc -public db_tables { + -pattern + {-dbn ""} +} { + @return a Tcl list of all the tables owned by the connected user. + + @param pattern Will be used as LIKE 'pattern%' to limit the number of tables returned. + + @param dbn The database name to use. If empty_string, uses the default database. + + @author Don Baccus (dhogaza@pacifier.com) + @author Lars Pind (lars@pinds.com) + + @change-log yon@arsdigita.com 20000711 changed to return lower case table names +} { + set proc_name {db_tables} + set driverkey [db_driverkey $dbn] + + switch -- $driverkey { + oracle { + set sql_table_names_with_pattern { + select lower(table_name) as table_name + from user_tables + where table_name like upper(:pattern) + } + set sql_table_names_without_pattern { + select lower(table_name) as table_name + from user_tables + } + } + + postgresql { + set sql_table_names_with_pattern { + select relname as table_name + from pg_class + where relname like lower(:pattern) and + relname !~ '^pg_' and relkind = 'r' + } + set sql_table_names_without_pattern { + select relname as table_name + from pg_class + where relname !~ '^pg_' and relkind = 'r' + } + } + + nsodbc - + default { + error "$proc_name is not supported for this database." + } + } + + set tables [list] + if { [info exists pattern] } { + db_foreach -dbn $dbn table_names_with_pattern \ + $sql_table_names_with_pattern { + lappend tables $table_name + } + } else { + db_foreach -dbn $dbn table_names_without_pattern \ + $sql_table_names_without_pattern { + lappend tables $table_name + } + } + + return $tables +} + + +ad_proc -public db_table_exists {{-dbn ""} table_name } { + @return 1 if a table with the specified name exists in the database, otherwise 0. + + @param dbn The database name to use. If empty_string, uses the default database. + + @author Don Baccus (dhogaza@pacifier.com) + @author Lars Pind (lars@pinds.com) +} { + set proc_name {db_table_exists} + set driverkey [db_driverkey $dbn] + + switch -- $driverkey { + oracle { + set n_rows [db_string -dbn $dbn table_count { + select count(*) from user_tables + where table_name = upper(:table_name) + }] + } + + postgresql { + set n_rows [db_string -dbn $dbn table_count { + select count(*) from pg_class + where relname = lower(:table_name) and + relname !~ '^pg_' and relkind = 'r' + }] + } + + nsodbc - + default { + error "$proc_name is not supported for this database." + } + } + + return $n_rows +} + + +ad_proc -public db_columns {{-dbn ""} table_name } { + @return a Tcl list of all the columns in the table with the given name. + + @param dbn The database name to use. If empty_string, uses the default database. + + @author Lars Pind (lars@pinds.com) + + @change-log yon@arsdigita.com 20000711 changed to return lower case column names +} { + set columns [list] + + # Works for both Oracle and PostgreSQL: + db_foreach -dbn $dbn table_column_names { + select lower(column_name) as column_name + from user_tab_columns + where table_name = upper(:table_name) + } { + lappend columns $column_name + } + + return $columns +} + + +ad_proc -public db_column_exists {{-dbn ""} table_name column_name } { + @return 1 if the row exists in the table, 0 if not. + + @param dbn The database name to use. If empty_string, uses the default database. + + @author Lars Pind (lars@pinds.com) +} { + set columns [list] + + # Works for both Oracle and PostgreSQL: + set n_rows [db_string -dbn $dbn column_exists { + select count(*) + from user_tab_columns + where table_name = upper(:table_name) + and column_name = upper(:column_name) + }] + + return [expr {$n_rows > 0}] +} + + +ad_proc -public db_column_type {{-dbn ""} {-complain:boolean} table_name column_name } { + + @return the Oracle Data Type for the specified column. + @return -1 if the table or column doesn't exist. + @return an error if table or column doesn't exist and -complain flag was specified + + @param dbn The database name to use. If empty_string, uses the default database. + @param complain throw an error when datatype is not found + + @author Yon Feldman (yon@arsdigita.com) + + @change-log 10 July, 2000: changed to return error + if column name doesn't exist + (mdettinger@arsdigita.com) + + @change-log 11 July, 2000: changed to return lower case data types + (yon@arsdigita.com) + + @change-log 11 July, 2000: changed to return error using the db_string default clause + (yon@arsdigita.com) + +} { + # Works for both Oracle and PostgreSQL: + set datatype [db_string -dbn $dbn column_type_select " + select data_type as data_type + from user_tab_columns + where upper(table_name) = upper(:table_name) + and upper(column_name) = upper(:column_name) + " -default "-1"] + if {$complain_p && $datatype == -1} { + error "Datatype for $table_name.$column_name not found." + } else { + return $datatype + } +} + + +ad_proc -public ad_column_type {{-dbn ""} table_name column_name } { + + @return 'numeric' for number type columns, 'text' otherwise + Throws an error if no such column exists. + + @param dbn The database name to use. If empty_string, uses the default database. + + @author Yon Feldman (yon@arsdigita.com) + +} { + set column_type [db_column_type -dbn $dbn $table_name $column_name] + + if { $column_type == -1 } { + return "Either table $table_name doesn't exist or column $column_name doesn't exist" + } elseif {$column_type ne "NUMBER" } { + return "numeric" + } else { + return "text" + } +} + + +ad_proc -public db_write_clob {{-dbn ""} statement_name sql args } { + @param dbn The database name to use. If empty_string, uses the default database. +} { + ad_arg_parser { bind } $args + set proc_name {db_write_clob} + set driverkey [db_driverkey $dbn] + + # TODO: Below, is db_qd_get_fullname necessary? Why this + # difference between Oracle and Postgres code? + # --atp@piskorski.com, 2003/04/09 10:00 EDT + + switch -- $driverkey { + oracle { + set full_statement_name [db_qd_get_fullname $statement_name] + db_with_handle -dbn $dbn db { + db_exec write_clob $db $full_statement_name $sql + } + } + + postgresql { + db_with_handle -dbn $dbn db { + db_exec write_clob $db $statement_name $sql + } + } + + nsodbc - + default { + error "$proc_name is not supported for this database." + } + } +} + + +ad_proc -public db_write_blob {{-dbn ""} statement_name sql args } { + @param dbn The database name to use. If empty_string, uses the default database. +} { + ad_arg_parser { bind } $args + set full_statement_name [db_qd_get_fullname $statement_name] + db_with_handle -dbn $dbn db { + db_exec_lob write_blob $db $full_statement_name $sql + } +} + + +ad_proc -public db_blob_get_file {{-dbn ""} statement_name sql args } { + @param dbn The database name to use. If empty_string, uses the default database. + +

+ TODO: + This proc should probably be changed to take a final + file argument, only, rather than the current + args variable length argument list. Currently, it is + called only 4 places in OpenACS, and each place args, + if used at all, is always "-file $file". However, + such a change might break custom code... I'm not sure. + --atp@piskorski.com, 2003/04/09 11:39 EDT + +} { + ad_arg_parser { bind file args } $args + set proc_name {db_blob_get_file} + set driverkey [db_driverkey $dbn] + + set full_statement_name [db_qd_get_fullname $statement_name] + + switch -- $driverkey { + oracle { + db_with_handle -dbn $dbn db { + db_exec_lob blob_get_file $db $full_statement_name $sql $file + } + } + + postgresql { + db_with_handle -dbn $dbn db { + db_exec_lob blob_select_file $db $full_statement_name $sql $file + } + } + + nsodbc - + default { + error "$proc_name is not supported for this database." + } + } +} + + +ad_proc -public db_blob_get {{-dbn ""} statement_name sql args } { + PostgreSQL only. + + @param dbn The database name to use. If empty_string, uses the default database. +} { + ad_arg_parser { bind } $args + set proc_name {db_blob_get} + set driverkey [db_driverkey $dbn] + + switch -- $driverkey { + + postgresql { + set full_statement_name [db_qd_get_fullname $statement_name] + db_with_handle -dbn $dbn db { + set data [db_exec_lob blob_get $db $full_statement_name $sql] + } + return $data + } + + oracle { + set pre_sql $sql + set full_statement_name [db_qd_get_fullname $statement_name] + set sql [db_qd_replace_sql $full_statement_name $pre_sql] + + # insert Tcl variable values (borrowed from Dan W - olah) + if {$sql ne $pre_sql } { + set sql [uplevel 2 [list subst -nobackslashes $sql]] + } + + set data [db_string dummy_statement_name $sql] + return $data + } + + nsodbc - + default { + error "$proc_name is not supported for this database." + } + } +} + + +ad_proc -private db_exec_lob { + {-ulevel 2} + type + db + statement_name + pre_sql + {file ""} +} { + A helper procedure to execute a SQL statement, potentially binding + depending on the value of the $bind variable in the calling environment + (if set). +} { + set proc_name {db_exec_lob} + set driverkey [db_driverkey -handle_p 1 $db] + + # Note: db_exec_lob is marked as private and in the entire + # toolkit, is ONLY called from a few other procs defined in this + # same file. So we definitely could change it to take a -dbn + # switch and remove the passed in db handle altogether, and call + # 'db_driverkey -dbn' rather than 'db_driverkey -handle'. But, + # db_exec NEEDS 'db_driverkey -handle', so we might as well use it + # here too. --atp@piskorski.com, 2003/04/09 12:13 EDT + + # TODO: Using this as a wrapper for the separate _oracle and + # _postgresql versions of this proc is ugly. But also simplest + # and safest at this point, as it let me change as little as + # possible of those two relatively complex procs. + # --atp@piskorski.com, 2003/04/09 11:55 EDT + + switch -- $driverkey { + oracle { + set which_proc {db_exec_lob_oracle} + } + postgresql { + set which_proc {db_exec_lob_postgresql} + } + + nsodbc - + default { + error "$proc_name is not supported for this database." + } + } + + ns_log Debug "$proc_name: $which_proc -ulevel [expr {$ulevel +1}] $type $db $statement_name $pre_sql $file" + return [$which_proc -ulevel [expr {$ulevel +1}] $type $db $statement_name $pre_sql $file] +} + + +ad_proc -private db_exec_lob_oracle { + {-ulevel 2} + type + db + statement_name + pre_sql + {file ""} +} { + A helper procedure to execute a SQL statement, potentially binding + depending on the value of the $bind variable in the calling environment + (if set). +} { + set start_time [expr {[clock clicks -microseconds]/1000.0}] + + set sql [db_qd_replace_sql $statement_name $pre_sql] + + # insert Tcl variable values (OpenACS - Dan) + if {$sql ne $pre_sql } { + set sql [uplevel $ulevel [list subst -nobackslashes $sql]] + } + + set file_storage_p 0 + upvar $ulevel storage_type storage_type + + if {[info exists storage_type] && $storage_type eq "file"} { + set file_storage_p 1 + set original_type $type + set qtype 1row + ns_log Debug "db_exec_lob: file storage in use" + } else { + set qtype $type + ns_log Debug "db_exec_lob: blob storage in use" + } + + set errno [catch { + upvar bind bind + + # Below, note that 'ns_ora blob_get_file' takes 3 parameters, + # while 'ns_ora write_blob' takes only 2. So if file is empty + # string (which it always will/should be for $qtype + # write_blob), we must not pass any 3rd parameter to the + # ns_ora command: --atp@piskorski.com, 2003/04/09 15:10 EDT + + if { [info exists bind] && [llength $bind] != 0 } { + if { [llength $bind] == 1 } { + if { $file eq "" } { + # gn: not sure, why the eval was ever needed (4 times) + set selection [eval [list ns_ora $qtype $db -bind $bind $sql]] + } else { + set selection [eval [list ns_ora $qtype $db -bind $bind $sql $file]] + } + + } else { + set bind_vars [ns_set create] + foreach { name value } $bind { + ns_set put $bind_vars $name $value + } + if { $file eq "" } { + set selection [eval [list ns_ora $qtype $db -bind $bind_vars $sql]] + } else { + set selection [eval [list ns_ora $qtype $db -bind $bind_vars $sql $file]] + } + } + + } else { + if { $file eq "" } { + set selection [uplevel $ulevel [list ns_ora $qtype $db $sql]] + } else { + set selection [uplevel $ulevel [list ns_ora $qtype $db $sql $file]] + } + } + + if {$file_storage_p} { + set content [ns_set value $selection 0] + for {set i 0} {$i < [ns_set size $selection]} {incr i} { + set name [ns_set key $selection $i] + if {$name eq "content"} { + set content [ns_set value $selection $i] + } + } + + switch -- $original_type { + + blob_get_file { + if {[file exists $content]} { + file copy -- $content $file + return $selection + } else { + error "file: $content doesn't exist" + } + } + + write_blob { + + if {[file exists $content]} { + set ofp [open $content r] + fconfigure $ofp -encoding binary + ns_writefp $ofp + close $ofp + return $selection + } else { + error "file: $content doesn't exist" + } + } + } + } else { + return $selection + } + + } error] + + ds_collect_db_call $db $type $statement_name $sql $start_time $errno $error + if { $errno == 2 } { + return $error + } + + return -code $errno -errorinfo $::errorInfo -errorcode $::errorCode $error +} + + +ad_proc -private db_exec_lob_postgresql { + {-ulevel 2} + type + db + statement_name + pre_sql + {file ""} +} { + A helper procedure to execute a SQL statement, potentially binding + depending on the value of the $bind variable in the calling environment + (if set). + + Low level replacement for db_exec which emulates blob handling. + +} { + set start_time [expr {[clock clicks -microseconds]/1000.0}] + + # Query Dispatcher (OpenACS - ben) + set sql [db_qd_replace_sql $statement_name $pre_sql] + + # insert Tcl variable values (OpenACS - Dan) + if {$sql ne $pre_sql } { + set sql [uplevel $ulevel [list subst -nobackslashes $sql]] + } + # create a function definition statement for the inline code + # binding is emulated in tcl. (OpenACS - Dan) + + set errno [catch { + upvar bind bind + if { [info exists bind] && [llength $bind] != 0 } { + if { [llength $bind] == 1 } { + set bind_vars [list] + set len [ns_set size $bind] + for {set i 0} {$i < $len} {incr i} { + lappend bind_vars [ns_set key $bind $i] \ + [ns_set value $bind $i] + } + set lob_sql [db_bind_var_substitution $sql $bind_vars] + } else { + set lob_sql [db_bind_var_substitution $sql $bind] + } + } else { + set lob_sql [uplevel $ulevel [list db_bind_var_substitution $sql]] + } + + # get the content - asssume it is in column 0, or optionally it can + # be returned as "content" with the storage type indicated by the + # "storage_type" column. + + set selection [ns_db 1row $db $lob_sql] + set content [ns_set value $selection 0] + for {set i 0} {$i < [ns_set size $selection]} {incr i} { + set name [ns_set key $selection $i] + if {$name eq "storage_type"} { + set storage_type [ns_set value $selection $i] + } elseif {$name eq "content"} { + set content [ns_set value $selection $i] + } + } + + # this is an ugly hack, but it allows content to be written + # to a file/connection if it is stored as a lob or if it is + # stored in the content-repository as a file. (DanW - Openacs) + + switch -- $type { + + blob_get { + + if {[info exists storage_type]} { + switch -- $storage_type { + file { + if {[file exists $content]} { + set ifp [open $content r] + + # DRB: this could be made faster by setting the buffersize + # to the size of the file, but for very large files allocating + # that much more memory on top of that needed by Tcl for storage + # of the data might not be wise. + + fconfigure $ifp -translation binary + + set data [read $ifp] + close $ifp + return $data + } else { + error "file: $content doesn't exist" + } + } + + lob { + if {[regexp {^[0-9]+$} $content match]} { + return [ns_pg blob_get $db $content] + } else { + error "invalid lob_id: should be an integer" + } + } + + default { + error "invalid storage type" + } + } + } elseif {[file exists $content]} { + set ifp [open $content r] + fconfigure $ifp -translation binary + set data [read $ifp] + close $ifp + return $data + } elseif {[regexp {^[0-9]+$} $content match]} { + return [ns_pg blob_get $db $content] + } else { + error "invalid query" + } + } + + blob_select_file { + + if {[info exists storage_type]} { + switch -- $storage_type { + file { + if {[file exists $content]} { + file copy -- $content $file + } else { + error "file: $content doesn't exist" + } + } + + lob { + if {[regexp {^[0-9]+$} $content match]} { + ns_pg blob_select_file $db $content $file + } else { + error "invalid lob_id: should be an integer" + } + } + + default { + error "invalid storage type" + } + } + } elseif {[file exists $content]} { + file copy -- $content $file + } elseif {[regexp {^[0-9]+$} $content match]} { + ns_pg blob_select_file $db $content $file + } else { + error "invalid query" + + # TODO: Page /file-storage/download-archive/index + # fails here on cvs head both before and after my + # mult-db db_* API work, I don't know why. See bug: + # http://openacs.org/bugtracker/openacs/com/file-storage/bug?bug%5fnumber=427 + # --atp@piskorski.com, 2003/04/09 18:04 EDT + } + } + + write_blob { + + if {[info exists storage_type]} { + switch -- $storage_type { + file { + if {[file exists $content]} { + set ofp [open $content r] + fconfigure $ofp -encoding binary + ns_writefp $ofp + close $ofp + } else { + error "file: $content doesn't exist" + } + } + + text { + ns_write $content + } + + lob { + if {[regexp {^[0-9]+$} $content match]} { + ns_pg blob_write $db $content + } else { + error "invalid lob_id: should be an integer" + } + } + + default { + error "invalid storage type" + } + } + } elseif {[file exists $content]} { + set ofp [open $content r] + fconfigure $ofp -encoding binary + ns_writefp $ofp + close $ofp + } elseif {[regexp {^[0-9]+$} $content match]} { + ns_pg blob_write $db $content + } else { + ns_write $content + } + } + } + + return + + } error] + + set errinfo $::errorInfo + set errcode $::errorCode + + ds_collect_db_call $db 0or1row $statement_name $sql $start_time $errno $error + + if { $errno == 2 } { + return $error + } + + return -code $errno -errorinfo $errinfo -errorcode $errcode $error +} + +ad_proc -public db_flush_cache { + {-cache_key_pattern *} + {-cache_pool db_cache_pool} +} { + + Flush the given cache of entries with keys that match the given pattern. + + @param cache_key_pattern The "string match" pattern used to flush keys (default is to flush all entries) + @param cache_pool The pool to flush (default is to flush db_cache_pool) + @author Don Baccus (dhogasa@pacifier.com) + +} { + # + # If the key pattern has meta characters, iterate over the entries. + # Otherwise, make a direct lookup, without retrieving the all keys + # from the cache, which can cause large mutex lock times. + # + if {[regexp {[*\]\[]} $cache_key_pattern]} { + foreach key [ns_cache names $cache_pool $cache_key_pattern] { + ns_cache flush $cache_pool $key + } + } else { + ns_cache flush $cache_pool $cache_key_pattern + } +} + +ad_proc -public db_bounce_pools {{-dbn ""}} { + @return Call ns_db bouncepool on all pools for the named database. + @param dbn The database name to use. Uses the default database if not supplied. +} { + foreach pool [db_available_pools $dbn] { + ns_db bouncepool $pool + } +} + +# Local variables: +# mode: tcl +# tcl-indent-level: 4 +# indent-tabs-mode: nil +# End: