You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2154 lines
72 KiB
2154 lines
72 KiB
# pgin.tcl - PostgreSQL Tcl Interface direct to protocol v3 backend |
|
# $Id: pgin.tcl 593 2017-11-12 23:16:54Z lbayuk $ |
|
# |
|
# Copyright (c) 1998-2017 L Bayuk |
|
# May be freely distributed with or without modification; must retain this |
|
# notice; provided with no warranties. |
|
# See the file COPYING for complete information on usage and redistribution |
|
# of this file, and for a disclaimer of all warranties. |
|
# |
|
# See the file INTERNALS in the source distribution for more information |
|
# about how this thing works, including namespace variables. |
|
# |
|
# Also includes: |
|
# md5.tcl - Compute MD5 Checksum |
|
|
|
# Some features require Tcl-8.5 but these are runtime detected. |
|
package require Tcl 8.4- |
|
|
|
# === Definition of the pgtcl namespace === |
|
|
|
namespace eval pgtcl { |
|
# Debug flag: |
|
variable debug 0 |
|
|
|
# Version number, also used in package provide at the bottom of this file: |
|
variable version 3.5.2 |
|
|
|
# Counter for making uniquely named result structures: |
|
variable rn 0 |
|
|
|
# Array mapping error field names to protocol codes: |
|
variable errnames |
|
array set errnames { |
|
SEVERITY S |
|
SQLSTATE C |
|
MESSAGE_PRIMARY M PRIMARY M |
|
MESSAGE_DETAIL D DETAIL D |
|
MESSAGE_HINT H HINT H |
|
STATEMENT_POSITION P POSITION P |
|
CONTEXT W |
|
SOURCE_FILE F FILE F |
|
SOURCE_LINE L LINE L |
|
SOURCE_FUNCTION R FUNCTION R |
|
SCHEMA_NAME s |
|
TABLE_NAME t |
|
COLUMN_NAME c |
|
DATATYPE_NAME d |
|
CONSTRAINT_NAME n |
|
} |
|
|
|
# For pg_escape_string, pg_quote, and pg_escape_bytea: need to keep the |
|
# value of standard_conforming_strings - both per-connection and global |
|
# default. The default is kept at "_default_" and the other elements |
|
# are indexed by connection handle. |
|
variable std_str |
|
set std_str(_default_) 0 |
|
} |
|
|
|
# === Internal Low-level I/O procedures for v3 protocol === |
|
|
|
# Internal procedure to send a packet to the backend with type and length. |
|
# Type can be empty - this is used for the startup packet. |
|
# The default is to flush the channel, since almost all messages generated |
|
# by pgin.tcl need to wait for a response. The exception is prepared queries. |
|
proc pgtcl::sendmsg {sock type data {noflush ""}} { |
|
puts -nonewline $sock \ |
|
$type[binary format I [expr {[string length $data]+4}]]$data |
|
if {$noflush == ""} { |
|
flush $sock |
|
} |
|
} |
|
|
|
# Read a message and return the message type byte: |
|
# This initializes the per-connection buffer too. |
|
# This has a special check for a v2 error message, which is needed at |
|
# startup in case of talking to v2 server. It assumes we will not |
|
# get a V3 error message longer than 0x20000000 bytes, which is pretty safe. |
|
# It fakes up a V3 error with severity ERROR, code (5 spaces), and the message. |
|
proc pgtcl::readmsg {sock} { |
|
upvar #0 pgtcl::buf_$sock buf pgtcl::bufi_$sock bufi pgtcl::bufn_$sock bufn |
|
set bufi 0 |
|
if {[binary scan [read $sock 5] aI type len] != 2} { |
|
set err "pgtcl: Unable to read message from database" |
|
if {[eof $sock]} { |
|
append err " - server closed connection" |
|
} |
|
error $err |
|
} |
|
if {$type == "E" && $len >= 0x20000000} { |
|
if {$pgtcl::debug} { puts "Warning: V2 error message received!" } |
|
# Build the start of the V3 error, including the 4 misread bytes in $len: |
|
set buf [binary format {a a*x a a*x a I} S ERROR C " " M $len] |
|
while {[set c [read $sock 1]] != ""} { |
|
append buf $c |
|
if {$c == "\000"} break |
|
} |
|
# This is 'code=0' to mark no more error options. |
|
append buf "\000" |
|
set bufn [string length $buf] |
|
} else { |
|
set bufn [expr {$len - 4}] |
|
set buf [read $sock $bufn] |
|
} |
|
return $type |
|
} |
|
|
|
# Return the next byte from the buffer: |
|
proc pgtcl::get_byte {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
set result [string index $buf $bufi] |
|
incr bufi |
|
return $result |
|
} |
|
|
|
# Return the next $n bytes from the buffer: |
|
proc pgtcl::get_bytes {db n} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
set obufi $bufi |
|
incr bufi $n |
|
return [string range $buf $obufi [expr {$obufi + $n - 1}]] |
|
} |
|
|
|
# Return the rest of the buffer. |
|
proc pgtcl::get_rest {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi pgtcl::bufn_$db bufn |
|
set obufi $bufi |
|
set bufi $bufn |
|
return [string range $buf $obufi end] |
|
} |
|
|
|
# Skip next $n bytes in the buffer. |
|
proc pgtcl::skip {db n} { |
|
upvar #0 pgtcl::bufi_$db bufi |
|
incr bufi $n |
|
} |
|
|
|
# Return next int32 from the buffer: |
|
proc pgtcl::get_int32 {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
if {[binary scan $buf "x$bufi I" i] != 1} { |
|
set i 0 |
|
} |
|
incr bufi 4 |
|
return $i |
|
} |
|
|
|
# Return next signed int16 from the buffer: |
|
proc pgtcl::get_int16 {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
if {[binary scan $buf "x$bufi S" i] != 1} { |
|
set i 0 |
|
} |
|
incr bufi 2 |
|
return $i |
|
} |
|
|
|
# Return next unsigned int16 from the buffer: |
|
proc pgtcl::get_uint16 {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
if {[binary scan $buf "x$bufi S" i] != 1} { |
|
set i 0 |
|
} |
|
incr bufi 2 |
|
return [expr {$i & 0xffff}] |
|
} |
|
|
|
# Return next signed int8 from the buffer: |
|
# (This is only used in 1 place in the protocol...) |
|
proc pgtcl::get_int8 {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
if {[binary scan $buf "x$bufi c" i] != 1} { |
|
set i 0 |
|
} |
|
incr bufi |
|
return $i |
|
} |
|
|
|
# Return the next null-terminated string from the buffer: |
|
# This decodes the UNICODE data. It is used for people-readable text like |
|
# messages, not query result data. |
|
proc pgtcl::get_string {db} { |
|
upvar #0 pgtcl::buf_$db buf pgtcl::bufi_$db bufi |
|
set end [string first "\000" $buf $bufi] |
|
if {$end < 0} { |
|
return "" |
|
} |
|
set obufi $bufi |
|
set bufi [expr {$end + 1}] |
|
return [encoding convertfrom identity \ |
|
[string range $buf $obufi [expr {$end - 1}]]] |
|
} |
|
|
|
# === Internal Mid-level I/O procedures for v3 protocol === |
|
|
|
# Parse a backend ErrorResponse or NoticeResponse message. The Severity |
|
# and Message parts are returned together with a trailing newline, like v2 |
|
# protocol did. If optional result_name is supplied, it is the name of |
|
# a result structure to store all error parts in, indexed as (error,$code). |
|
proc pgtcl::get_response {db {result_name ""}} { |
|
if {$result_name != ""} { |
|
upvar $result_name result |
|
} |
|
array set result {error,S ERROR error,M {}} |
|
while {[set c [pgtcl::get_byte $db]] != "\000" && $c != ""} { |
|
set result(error,$c) [pgtcl::get_string $db] |
|
} |
|
return "$result(error,S): $result(error,M)\n" |
|
} |
|
|
|
# Handle ParameterStatus and remember the name and value: |
|
proc pgtcl::get_parameter_status {db} { |
|
upvar #0 pgtcl::param_$db param |
|
set name [pgtcl::get_string $db] |
|
set param($name) [pgtcl::get_string $db] |
|
if {$pgtcl::debug} { puts "+server param $name=$param($name)" } |
|
|
|
# Special cases: |
|
# Remember per-connection and global default for standard_conforming_strings |
|
# for use by pg_escape_string, pg_quote, and pg_escape_bytea. |
|
if {$name eq "standard_conforming_strings"} { |
|
set is_on [expr {$param($name) eq "on"}] |
|
set pgtcl::std_str($db) $is_on |
|
set pgtcl::std_str(_default_) $is_on |
|
} |
|
} |
|
|
|
# Handle a notification ('A') message. |
|
# The notifying backend pid is read and passed to the callback if requested. |
|
# Starting with PostgreSQL-9.0, more_info (ignored) became 'payload' and can be |
|
# sent with SQL. To help avoid breaking exising code, the payload is only sent |
|
# as an argument to the notify command if it is non-empty. But if you send |
|
# a notification with payload to code that doesn't expect it, you will get |
|
# a background error from the 'after' code because of the extra argument. |
|
proc pgtcl::get_notification_response {db} { |
|
set notify_pid [pgtcl::get_int32 $db] |
|
set notify_rel [pgtcl::get_string $db] |
|
set payload [pgtcl::get_string $db] |
|
if {$pgtcl::debug} { |
|
puts "+pgtcl got notify from $notify_pid: $notify_rel $payload" |
|
} |
|
if {[info exists pgtcl::notify($db,$notify_rel)]} { |
|
set cmd $pgtcl::notify($db,$notify_rel) |
|
if {$pgtcl::notifopt($db,$notify_rel) == 1} { |
|
lappend cmd $notify_pid |
|
} |
|
if {$payload ne ""} { |
|
lappend cmd $payload |
|
} |
|
after idle $cmd |
|
} |
|
} |
|
|
|
# Handle a notice ('N') message. If no handler is defined, or the handler is |
|
# empty, do nothing, otherwise, call the handler with the message argument |
|
# appended. For backward compatibility with v2 protocol, the message is |
|
# assumed to end in a newline. |
|
proc pgtcl::get_notice {db} { |
|
set msg [pgtcl::get_response $db] |
|
if {[info exists pgtcl::notice($db)] && [set cmd $pgtcl::notice($db)] != ""} { |
|
eval $cmd [list $msg] |
|
} |
|
} |
|
|
|
# Internal procedure to read a tuple (row) from the backend. |
|
# Column count is redundant, but check it anyway. |
|
# Format code (text/binary) is used to do Unicode decoding on Text only. |
|
proc pgtcl::gettuple {db result_name} { |
|
upvar $result_name result |
|
if {$result(nattr) == 0} { |
|
unset result |
|
error "Protocol error, data before descriptor" |
|
} |
|
set irow $result(ntuple) |
|
set nattr [pgtcl::get_uint16 $db] |
|
if {$nattr != $result(nattr)} { |
|
unset result |
|
error "Expecting $result(nattr) columns, but data row has $nattr" |
|
} |
|
set icol 0 |
|
foreach format $result(formats) { |
|
set col_len [pgtcl::get_int32 $db] |
|
if {$col_len > 0} { |
|
if ($format) { |
|
set result($irow,$icol) [pgtcl::get_bytes $db $col_len] |
|
} else { |
|
set result($irow,$icol) [encoding convertfrom identity \ |
|
[pgtcl::get_bytes $db $col_len]] |
|
} |
|
} else { |
|
set result($irow,$icol) "" |
|
if {$col_len < 0} { |
|
set result(null,$irow,$icol) "" |
|
} |
|
} |
|
incr icol |
|
} |
|
incr result(ntuple) |
|
} |
|
|
|
# Internal procedure to handle common backend utility message types: |
|
# C : Completion status E : Error |
|
# N : Notice message A : Notification |
|
# S : ParameterStatus |
|
# This can be given any message type. If it handles the message, |
|
# it returns 1. If it doesn't handle the message, it returns 0. |
|
# |
|
proc pgtcl::common_message {msgchar db result_name} { |
|
upvar $result_name result |
|
switch -- $msgchar { |
|
A { pgtcl::get_notification_response $db } |
|
C { set result(complete) [pgtcl::get_string $db] } |
|
N { pgtcl::get_notice $db } |
|
S { pgtcl::get_parameter_status $db } |
|
E { |
|
set result(status) PGRES_FATAL_ERROR |
|
set result(error) [pgtcl::get_response $db result] |
|
} |
|
default { return 0 } |
|
} |
|
return 1 |
|
} |
|
|
|
# === Other internal support procedures === |
|
|
|
# Internal procedure to set a default value from the environment: |
|
proc pgtcl::default {default args} { |
|
global env |
|
foreach a $args { |
|
if {[info exists env($a)]} { |
|
return $env($a) |
|
} |
|
} |
|
return $default |
|
} |
|
|
|
# Internal procedure to parse a connection info string. |
|
# This has to handle quoting and escaping. See the PostgreSQL Programmer's |
|
# Guide, Client Interfaces, Libpq, Database Connection Functions. |
|
# The definitive reference is the PostgreSQL source code in: |
|
# interface/libpq/fe-connect.c:conninfo_parse() |
|
# One quirk to note: backslash escapes work in quoted values, and also in |
|
# unquoted values, but you cannot use backslash-space in an unquoted value, |
|
# because the space ends the value regardless of the backslash. |
|
# |
|
# Stores the results in an array $result(paramname)=value. It will not |
|
# create a new index in the array; if paramname does not already exist, |
|
# it means a bad parameter was given (one not defined by pg_conndefaults). |
|
# Returns an error message on error, else an empty string if OK. |
|
proc pgtcl::parse_conninfo {conninfo result_name} { |
|
upvar $result_name result |
|
while {[regexp {^ *([^=]*)= *(.+)} $conninfo unused name conninfo]} { |
|
set name [string trim $name] |
|
if {[regexp {^'(.*)} $conninfo unused conninfo]} { |
|
set value "" |
|
set n [string length $conninfo] |
|
for {set i 0} {$i < $n} {incr i} { |
|
if {[set c [string index $conninfo $i]] == "\\"} { |
|
set c [string index $conninfo [incr i]] |
|
} elseif {$c == "'"} break |
|
append value $c |
|
} |
|
if {$i >= $n} { |
|
return "unterminated quoted string in connection info string" |
|
} |
|
set conninfo [string range $conninfo [incr i] end] |
|
} else { |
|
regexp {^([^ ]*)(.*)} $conninfo unused value conninfo |
|
regsub -all {\\(.)} $value {\1} value |
|
} |
|
if {$pgtcl::debug} { puts "+parse_conninfo name=$name value=$value" } |
|
if {![info exists result($name)]} { |
|
return "invalid connection option \"$name\"" |
|
} |
|
set result($name) $value |
|
} |
|
if {[string trim $conninfo] != ""} { |
|
return "syntax error in connection info string '...$conninfo'" |
|
} |
|
return "" |
|
} |
|
|
|
# Internal helper for parse_connuri: URI character escape decoding. |
|
# Decode %XX hex escapes in a string and return the result: |
|
proc pgtcl::uri_unesc {s} { |
|
set result "" |
|
set re_hex {^([^%]*)%([0-9A-Za-z][0-9A-Za-z])(.*)} |
|
while {$s != "" && [regexp $re_hex $s unused before hex rest]} { |
|
append result $before [binary format a [subst "\\x$hex"]] |
|
set s $rest |
|
} |
|
return $result$s |
|
} |
|
|
|
# Internal procedure to parse a connection URI and merge the parameters |
|
# into a connection parameter array $result. $uri_rest is the part of the |
|
# URI after the scheme and delimiters (postgresql:// or postgres://). |
|
# NOTE: URI parameters (?param=value...) are currently ignored. |
|
# Returns an error message on error, else an empty string if OK. |
|
proc pgtcl::parse_connuri {uri_rest result_name} { |
|
upvar $result_name result |
|
|
|
set s $uri_rest |
|
# Note: Results are stored temporarily in a list $r for debug purposes. |
|
set r {} |
|
|
|
# Parse optional username or username:password, which ends in @ |
|
if {[regexp {^([A-Za-z0-9_%.~-]+)(:[A-za-z0-9_%.~-]+)?@} $s match p1 p2]} { |
|
lappend r user [pgtcl::uri_unesc $p1] |
|
# Remove the : from before the password |
|
if {$p2 != ""} { |
|
lappend r password [pgtcl::uri_unesc [string range $p2 1 end]] |
|
} |
|
set s [string range $s [string length $match] end] |
|
} |
|
|
|
# Parse hostname, port (port is not URI-encoded) |
|
if {[regexp {^([A-Za-z0-9_%.-]+)|(\[[0-9a-fA-F:]+])} $s match]} { |
|
lappend r host [pgtcl::uri_unesc $match] |
|
set s [string range $s [string length $match] end] |
|
if {[regexp {^:([0-9]+)} $s match p1]} { |
|
lappend r port $p1 |
|
set s [string range $s [string length $match] end] |
|
} |
|
} |
|
|
|
# The "path" part of the URI is the database name. |
|
if {[regexp {^/([^?#]*)} $s match p1] && $p1 != ""} { |
|
lappend r dbname [pgtcl::uri_unesc $p1] |
|
} |
|
if {$pgtcl::debug} { puts "+parse_connuri postgresql://$uri_rest\n => {$r}" } |
|
array set result $r |
|
return "" |
|
} |
|
|
|
# Internal procedure to merge connection options into the connection |
|
# options array. $mode is -conninfo or -connlist. conninfo mode accepts a |
|
# connection string (param=value ...) or a URI (postgresql://...) which are |
|
# handled by parse_conninfo and parse_connuri respectively. connlist mode |
|
# accepts a Tcl list of params and values, and is handled here. |
|
# Stores the results in an array $result(paramname)=value. |
|
# Returns an error message on error, else an empty string if OK. |
|
proc pgtcl::merge_connopts {mode arg result_name} { |
|
upvar $result_name result |
|
if {$mode eq "-conninfo"} { |
|
if {[regexp {^postgres(ql)?://(.*)} $arg ignored1 ignored2 uri_rest]} { |
|
return [pgtcl::parse_connuri $uri_rest result] |
|
} |
|
return [pgtcl::parse_conninfo $arg result] |
|
} |
|
foreach {name value} $arg { |
|
if {![info exists result($name)]} { |
|
return "invalid connection option \"$name\"" |
|
} |
|
set result($name) $value |
|
} |
|
return "" |
|
} |
|
|
|
# Internal procedure to check for valid result handle. This returns |
|
# the fully qualified name of the result array. |
|
# Usage: upvar #0 [pgtcl::checkres $res] result |
|
proc pgtcl::checkres {res} { |
|
if {![info exists pgtcl::result$res]} { |
|
error "Invalid result handle\n$res is not a valid query result" |
|
} |
|
return "pgtcl::result$res" |
|
} |
|
|
|
# Password encryption with MD5. This is used by pg_encrypt_password and |
|
# by pg_connect. It needs to be separate from pg_encrypt_password because |
|
# (like libpq's PQencryptPassword) that returns the prefix "md5" in front |
|
# but that doesn't work for the inner encryption in pg_connect. |
|
proc pgtcl::encrypt_password {part1 part2} { |
|
return [md5::digest "$part1$part2"] |
|
} |
|
|
|
# === Public procedures : Connecting and Disconnecting === |
|
|
|
# Return connection defaults as {optname label dispchar dispsize value}... |
|
proc pg_conndefaults {} { |
|
set user [pgtcl::default user PGUSER USER LOGNAME USERNAME] |
|
set result [list \ |
|
[list user Database-User {} 20 $user] \ |
|
[list password Database-Password * 20 [pgtcl::default {} PGPASSWORD]] \ |
|
[list host Database-Host {} 40 [pgtcl::default localhost PGHOST]] \ |
|
{hostaddr Database-Host-IP-Address {} 45 {}} \ |
|
[list port Database-Port {} 6 [pgtcl::default 5432 PGPORT]] \ |
|
[list dbname Database-Name {} 20 [pgtcl::default $user PGDATABASE]] \ |
|
[list tty Backend-Debug-TTY D 40 [pgtcl::default {} PGTTY]] \ |
|
[list options Backend-Debug-Options D 40 [pgtcl::default {} PGOPTIONS]] \ |
|
] |
|
if {$pgtcl::debug} { puts "+pg_conndefaults: $result" } |
|
return $result |
|
} |
|
|
|
# Connect to database. Two forms are supported: -conninfo (with connection |
|
# info string or URI), and -connlist (with connection keyword/value list). |
|
# The older form with dbname and separate option/value args is not supported. |
|
# We speak backend protocol v3, and only handle clear-text password and |
|
# MD5 authentication (messages R 3, and R 5). |
|
# A parameter is added to set client_encoding to UNICODE. This is due to |
|
# Tcl's way of representing strings. |
|
proc pg_connect {args} { |
|
|
|
if {[llength $args] != 2 || ([set mode [lindex $args 0]] ne "-conninfo" \ |
|
&& $mode ne "-connlist")} { |
|
error "wrong # args: should be \"pg_connect -conninfo conninfoString |\ |
|
-connlist conninfoList\"" |
|
} |
|
|
|
# Get connection defaults into an array opt(): |
|
foreach o [pg_conndefaults] { |
|
set opt([lindex $o 0]) [lindex $o 4] |
|
} |
|
|
|
# Merge in command-line options, as a connection list or connection string: |
|
if {[set msg [pgtcl::merge_connopts $mode [lindex $args 1] opt]] ne ""} { |
|
error "Connection to database failed\n$msg" |
|
} |
|
|
|
# Hostaddr overrides host, per documentation, and we need host below. |
|
if {$opt(hostaddr) != ""} { |
|
set opt(host) $opt(hostaddr) |
|
} |
|
|
|
if {$pgtcl::debug} { |
|
puts "+pg_connect to $opt(dbname)@$opt(host):$opt(port) as $opt(user)" |
|
} |
|
|
|
if {[catch {socket $opt(host) $opt(port)} sock]} { |
|
error "Connection to database failed\n$sock" |
|
} |
|
# Note: full buffering, socket must be flushed after write! |
|
fconfigure $sock -buffering full -translation binary |
|
|
|
# Startup packet: |
|
pgtcl::sendmsg $sock {} [binary format "I a*x a*x a*x a*x a*x a*x a*x a*x x" \ |
|
0x00030000 \ |
|
user $opt(user) database $opt(dbname) \ |
|
client_encoding UNICODE options $opt(options)] |
|
|
|
set msg {} |
|
while {[set c [pgtcl::readmsg $sock]] != "Z"} { |
|
switch -- $c { |
|
E { |
|
set msg [pgtcl::get_response $sock] |
|
break |
|
} |
|
R { |
|
set n [pgtcl::get_int32 $sock] |
|
if {$n == 3} { |
|
pgtcl::sendmsg $sock p "$opt(password)\000" |
|
} elseif {$n == 5} { |
|
set salt [pgtcl::get_bytes $sock 4] |
|
set md5_response [pg_encrypt_password \ |
|
[pgtcl::encrypt_password $opt(password) $opt(user)] $salt] |
|
if {$pgtcl::debug} { puts "+pg_connect MD5 sending: $md5_response" } |
|
pgtcl::sendmsg $sock p "$md5_response\000" |
|
} elseif {$n != 0} { |
|
set msg "Unknown database authentication request($n)" |
|
break |
|
} |
|
} |
|
K { |
|
set pid [pgtcl::get_int32 $sock] |
|
set key [pgtcl::get_int32 $sock] |
|
if {$pgtcl::debug} { puts "+server pid=$pid key=$key" } |
|
} |
|
S { |
|
pgtcl::get_parameter_status $sock |
|
} |
|
default { |
|
set msg "Unexpected reply from database: $c" |
|
break |
|
} |
|
} |
|
} |
|
if {$msg != ""} { |
|
close $sock |
|
error "Connection to database failed\n$msg" |
|
} |
|
# Initialize transaction status; should be get_byte but it better be I: |
|
set pgtcl::xstate($sock) I |
|
# Initialize action for NOTICE messages (see get_notice): |
|
set pgtcl::notice($sock) {puts -nonewline stderr} |
|
# Make sure there is a setting for standard_conforming_strings (should |
|
# have come back via get_parameter_status) |
|
if {![info exists pgtcl::std_str($sock)]} { |
|
set pgtcl::std_str($sock) $pgtcl::std_str(_default_) |
|
} |
|
# Save backend process ID. (Key isn't saved since it isn't usable) |
|
set pgtcl::bepid($sock) $pid |
|
|
|
return $sock |
|
} |
|
|
|
# Disconnect from the database. Free all result structures which are |
|
# associated with this connection, and other data for this connection, |
|
# including the buffer. |
|
proc pg_disconnect {db} { |
|
if {$pgtcl::debug} { puts "+Disconnecting $db from database" } |
|
pgtcl::sendmsg $db X {} |
|
catch {close $db} |
|
foreach v [info vars pgtcl::result*] { |
|
upvar #0 $v result |
|
if {$result(conn) == $db} { |
|
if {$pgtcl::debug} { puts "+Freeing left-over result structure $v" } |
|
unset result |
|
} |
|
} |
|
array unset pgtcl::notify $db,* |
|
array unset pgtcl::notifopt $db,* |
|
unset -nocomplain pgtcl::param_$db pgtcl::xstate($db) pgtcl::notice($db) \ |
|
pgtcl::buf_$db pgtcl::bufi_$db pgtcl::bufn_$db pgtcl::std_str($db) \ |
|
pgtcl::bepid($db) |
|
} |
|
|
|
# === Internal procedures: Query Result and supporting functions === |
|
|
|
# Read the backend reply to a query or other request, and build a |
|
# result structure. This implements most of the backend response protocol. |
|
# The $mode parameter is used to do some checks for expected message types. |
|
# mode "" : Basic query mode |
|
# mode "E" : Extended Query mode, e.g. pg_exec_prepared. |
|
# mode "D" : Describe Portal or Describe Prepared. |
|
# This table indicates which message types are expected in each mode: |
|
# Handled in Mode: |
|
# Message Type: Basic Extended Describe |
|
# -------------------------- ----- -------- -------- |
|
# common C N S E A * * * |
|
# 2 BindComplete * |
|
# G CopyInResponse * * |
|
# H CopyOutResponse * * |
|
# D DataRow * * |
|
# I EmptyQueryResponse * * |
|
# n NoData * * |
|
# t ParameterDescription * |
|
# 1 ParseComplete * |
|
# Z ReadyForQuery * * * |
|
# T RowDescription * * * |
|
# The 'common' types C N S E and A are handled by pgtcl::common_message. |
|
# Not every message/mode pair above is checked. For example, DataRow |
|
# is allowed in every mode although it should never appear in Describe, |
|
# just because it would unnecessarily slow things down. |
|
# |
|
# Note: In Describe mode, the status is PGRES_COMMAND_OK, not |
|
# PGRES_TUPLES_OK, when a RowDescription message is returned. This is |
|
# not easily distinguished from a query that returns no rows, so it is |
|
# special cased here. |
|
# |
|
# Returns a result handle (the number pgtcl::rn), or throws an error. |
|
|
|
proc pgtcl::getresult {db {mode ""}} { |
|
upvar #0 pgtcl::result[incr pgtcl::rn] result |
|
set result(conn) $db |
|
array set result { |
|
nattr 0 ntuple 0 |
|
attrs {} types {} sizes {} modifs {} formats {} |
|
error {} tbloids {} tblcols {} nparam 0 paramtypes {} |
|
complete {} |
|
status PGRES_COMMAND_OK |
|
} |
|
|
|
# Note: Each valid switch branch ends in continue or break. Invalid |
|
# falls through to error handling for an unexpected message type. |
|
# D is special case, no mode check and up top because of its frequency. |
|
while {1} { |
|
set c [pgtcl::readmsg $db] |
|
switch -- $c { |
|
D { |
|
pgtcl::gettuple $db result |
|
continue |
|
} |
|
T { |
|
if {$result(nattr) != 0} { |
|
unset result |
|
error "Protocol failure, multiple descriptors" |
|
} |
|
if {$mode eq "D"} { |
|
set result(status) PGRES_COMMAND_OK |
|
} else { |
|
set result(status) PGRES_TUPLES_OK |
|
} |
|
set nattr [pgtcl::get_uint16 $db] |
|
set result(nattr) $nattr |
|
for {set icol 0} {$icol < $nattr} {incr icol} { |
|
lappend result(attrs) [pgtcl::get_string $db] |
|
lappend result(tbloids) [pgtcl::get_int32 $db] |
|
lappend result(tblcols) [pgtcl::get_uint16 $db] |
|
lappend result(types) [pgtcl::get_int32 $db] |
|
lappend result(sizes) [pgtcl::get_int16 $db] |
|
lappend result(modifs) [pgtcl::get_int32 $db] |
|
lappend result(formats) [pgtcl::get_int16 $db] |
|
} |
|
continue |
|
} |
|
Z { |
|
set pgtcl::xstate($db) [pgtcl::get_byte $db] |
|
break |
|
} |
|
} |
|
|
|
if {[pgtcl::common_message $c $db result]} continue |
|
|
|
if {$mode eq "" || $mode eq "E"} { |
|
switch -- $c { |
|
I { |
|
set result(status) PGRES_EMPTY_QUERY |
|
continue |
|
} |
|
H { |
|
pgtcl::begincopy result OUT |
|
break |
|
} |
|
G { |
|
pgtcl::begincopy result IN |
|
break |
|
} |
|
} |
|
} |
|
|
|
if {$mode eq "E" && ($c eq "2" || $c eq "1")} continue |
|
|
|
if {($mode eq "E" || $mode eq "D") && $c eq "n"} continue |
|
|
|
if {$mode eq "D" && $c eq "t"} { |
|
set result(nparam) [set np [pgtcl::get_int16 $db]] |
|
for {set i 0} {$i < $np} {incr i} { |
|
lappend result(paramtypes) [pgtcl::get_int32 $db] |
|
} |
|
continue |
|
} |
|
unset result |
|
error "Unexpected reply from database: $c" |
|
} |
|
if {$pgtcl::debug > 1} { |
|
puts "+pgtcl::getresult $pgtcl::rn = " |
|
parray result |
|
} |
|
return $pgtcl::rn |
|
} |
|
|
|
# Process format code information for pg_exec_prepared. |
|
# fclist A list of BINARY (or B*) or TEXT (or T*) format code words. |
|
# ncodes_name The name of a variable to get the number of format codes. |
|
# codes_name The name of a variable to get a list of format codes in |
|
# the PostgreSQL syntax: 0=text 1=binary. |
|
proc pgtcl::crunch_fcodes {fclist ncodes_name codes_name} { |
|
upvar $ncodes_name ncodes $codes_name codes |
|
set ncodes [llength $fclist] |
|
set codes {} |
|
foreach k $fclist { |
|
if {[string match B* $k]} { |
|
lappend codes 1 |
|
} else { |
|
lappend codes 0 |
|
} |
|
} |
|
} |
|
|
|
# Return an error code field value for pg_result -error?Field? code. |
|
# For field names, it accepts either the libpq name (without PG_DIAG_) or the |
|
# single-letter protocol code. |
|
# For compatibility with changes made to the other pgtcl after this feature was |
|
# added here, it also accepts some names without the prefixes. |
|
# The $code is not case sensitive, but the protocol letter is. This was |
|
# changed because PostgreSQL-9.3.0 started using some lower case letters too. |
|
# If an unknown field name is used, or the field isn't part of the error |
|
# message, an empty string is substituted. |
|
|
|
proc pgtcl::error_fields {result_name code} { |
|
upvar $result_name result |
|
variable errnames |
|
set upcase_code [string toupper $code] |
|
if {[info exists errnames($upcase_code)]} { |
|
set code $errnames($upcase_code) |
|
} |
|
if {[info exists result(error,$code)]} { |
|
return $result(error,$code) |
|
} |
|
return "" |
|
} |
|
|
|
# === Public procedures : Query and Result === |
|
|
|
# Execute SQL and return a result handle. |
|
# If parameters are supplied, use pg_exec_params in all-text arg mode. |
|
# (Let pg_exec_params encode the query in that case.) |
|
|
|
proc pg_exec {db query args} { |
|
if {$pgtcl::debug} { puts "+pg_exec $query {$args}" } |
|
if {[llength $args] == 0} { |
|
pgtcl::sendmsg $db Q "[encoding convertto identity $query]\000" |
|
return [pgtcl::getresult $db] |
|
} |
|
return [eval [list pg_exec_params $db $query {} {} {}] $args] |
|
} |
|
|
|
# Extract data from a pg_exec result structure. |
|
# -cmdTuples, -list, and -llist are extensions to the baseline libpgtcl which |
|
# have appeared or will appear in beta or future versions. |
|
# -errorField, -lxAttributes and -getNull are proposed new for 7.4. |
|
# -cmdStatus is new with pgintcl-2.0.1 |
|
# -numParams and -paramTypes, for prepared statements, is new with pgintcl-3.1.0 |
|
# -dict for dictionary return, idea credit to pgfoundry/pgtcl developers, new |
|
# with pgintcl-3.3.0. |
|
|
|
proc pg_result {res option args} { |
|
upvar #0 [pgtcl::checkres $res] result |
|
set argc [llength $args] |
|
set ntuple $result(ntuple) |
|
set nattr $result(nattr) |
|
switch -- $option { |
|
-status { return $result(status) } |
|
-conn { return $result(conn) } |
|
-oid { |
|
if {[regexp {^INSERT +([0-9]*)} $result(complete) unused oid]} { |
|
return $oid |
|
} |
|
return 0 |
|
} |
|
-cmdTuples { |
|
if {[regexp {^INSERT +[0-9]* +([0-9]*)} $result(complete) x num] \ |
|
|| [regexp {^(UPDATE|DELETE|SELECT|FETCH|MOVE|COPY) +([0-9]*)} \ |
|
$result(complete) x y num]} { |
|
return $num |
|
} |
|
return "" |
|
} |
|
-cmdStatus { return $result(complete) } |
|
-numTuples { return $ntuple } |
|
-numAttrs { return $nattr } |
|
-assign { |
|
if {$argc != 1} { |
|
error "-assign option must be followed by a variable name" |
|
} |
|
upvar $args a |
|
set icol 0 |
|
foreach attr $result(attrs) { |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
set a($irow,$attr) $result($irow,$icol) |
|
} |
|
incr icol |
|
} |
|
} |
|
-assignbyidx { |
|
if {$argc != 1 && $argc != 2} { |
|
error "-assignbyidxoption requires an array name and optionally an\ |
|
append string" |
|
} |
|
upvar [lindex $args 0] a |
|
if {$argc == 2} { |
|
set suffix [lindex $args 1] |
|
} else { |
|
set suffix {} |
|
} |
|
set attr_first [lindex $result(attrs) 0] |
|
set attr_rest [lrange $result(attrs) 1 end] |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
set val_first $result($irow,0) |
|
set icol 1 |
|
foreach attr $attr_rest { |
|
set a($val_first,$attr$suffix) $result($irow,$icol) |
|
incr icol |
|
} |
|
} |
|
} |
|
-getTuple { |
|
if {$argc != 1} { |
|
error "-getTuple option must be followed by a tuple number" |
|
} |
|
set irow $args |
|
if {$irow < 0 || $irow >= $ntuple} { |
|
error "argument to getTuple cannot exceed number of tuples - 1" |
|
} |
|
set list {} |
|
for {set icol 0} {$icol < $nattr} {incr icol} { |
|
lappend list $result($irow,$icol) |
|
} |
|
return $list |
|
} |
|
-getNull { |
|
if {$argc != 1} { |
|
error "-getNull option must be followed by a tuple number" |
|
} |
|
set irow $args |
|
if {$irow < 0 || $irow >= $ntuple} { |
|
error "argument to getNull cannot exceed number of tuples - 1" |
|
} |
|
set list {} |
|
for {set icol 0} {$icol < $nattr} {incr icol} { |
|
lappend list [info exists result(null,$irow,$icol)] |
|
} |
|
return $list |
|
} |
|
-tupleArray { |
|
if {$argc != 2} { |
|
error "-tupleArray option must be followed by a tuple number and\ |
|
array name" |
|
} |
|
set irow [lindex $args 0] |
|
if {$irow < 0 || $irow >= $ntuple} { |
|
error "argument to tupleArray cannot exceed number of tuples - 1" |
|
} |
|
upvar [lindex $args 1] a |
|
set icol 0 |
|
foreach attr $result(attrs) { |
|
set a($attr) $result($irow,$icol) |
|
incr icol |
|
} |
|
} |
|
-list { |
|
set list {} |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
for {set icol 0} {$icol < $nattr} {incr icol} { |
|
lappend list $result($irow,$icol) |
|
} |
|
} |
|
return $list |
|
} |
|
-llist { |
|
set list {} |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
set sublist {} |
|
for {set icol 0} {$icol < $nattr} {incr icol} { |
|
lappend sublist $result($irow,$icol) |
|
} |
|
lappend list $sublist |
|
} |
|
return $list |
|
} |
|
-attributes { |
|
return $result(attrs) |
|
} |
|
-lAttributes { |
|
set list {} |
|
foreach attr $result(attrs) type $result(types) size $result(sizes) { |
|
lappend list [list $attr $type $size] |
|
} |
|
return $list |
|
} |
|
-lxAttributes { |
|
set list {} |
|
foreach attr $result(attrs) type $result(types) size $result(sizes) \ |
|
modif $result(modifs) format $result(formats) \ |
|
tbloid $result(tbloids) tblcol $result(tblcols) { |
|
lappend list [list $attr $type $size $modif $format $tbloid $tblcol] |
|
} |
|
return $list |
|
} |
|
-clear { |
|
unset result |
|
} |
|
-error - |
|
-errorField { |
|
if {$argc == 0} { |
|
return $result(error) |
|
} |
|
return [pgtcl::error_fields result $args] |
|
} |
|
-numParams { |
|
return $result(nparam) |
|
} |
|
-paramTypes { |
|
return $result(paramtypes) |
|
} |
|
-dict { |
|
if {[catch {dict create} d]} { |
|
error "pg_result -dict requires Tcl dictionary support" |
|
} |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
set icol 0 |
|
foreach attr $result(attrs) { |
|
dict set d $irow $attr $result($irow,$icol) |
|
incr icol |
|
} |
|
} |
|
return $d |
|
} |
|
default { error "Invalid option to pg_result: $option" } |
|
} |
|
} |
|
|
|
# Run a select query and iterate over the results. Uses pg_exec to run the |
|
# query and build the result structure, but we cheat and directly use the |
|
# result array rather than calling pg_result. |
|
# Each returned tuple is stored into the caller's array, then the caller's |
|
# proc is called. |
|
# If the caller's proc does "break", "return", or gets an error, get out |
|
# of the processing loop. Tcl codes: 0=OK 1=error 2=return 3=break 4=continue |
|
proc pg_select {db query var_name proc} { |
|
upvar $var_name var |
|
global errorCode errorInfo |
|
set res [pg_exec $db $query] |
|
upvar #0 pgtcl::result$res result |
|
if {$result(status) != "PGRES_TUPLES_OK"} { |
|
set msg $result(error) |
|
unset result |
|
error $msg |
|
} |
|
set code 0 |
|
set var(.headers) $result(attrs) |
|
set var(.numcols) $result(nattr) |
|
set ntuple $result(ntuple) |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
set var(.tupno) $irow |
|
set icol 0 |
|
foreach attr $result(attrs) { |
|
set var($attr) $result($irow,$icol) |
|
incr icol |
|
} |
|
set code [catch {uplevel 1 $proc} s] |
|
if {$code != 0 && $code != 4} break |
|
} |
|
unset result var |
|
if {$code == 1} { |
|
return -code error -errorinfo $errorInfo -errorcode $errorCode $s |
|
} elseif {$code == 2 || $code > 4} { |
|
return -code $code $s |
|
} |
|
return |
|
} |
|
|
|
# Register a listener for backend notification, or cancel a listener. |
|
# Usage: pg_listen db name - Cancel a listener |
|
# pg_listen db name command - Set a new listener |
|
# pg_listen -pid db name command - Set a new listener with PID arg |
|
proc pg_listen {args} { |
|
set nargs [llength $args] |
|
set narg -1 |
|
set options 0 |
|
if {$nargs > 0 && [lindex $args 0] == "-pid"} { |
|
set options 1 |
|
incr narg |
|
incr nargs -1 |
|
} |
|
if {$nargs < 2 || 3 < $nargs} { |
|
error "Wrong # args: should be \"pg_listen ?options? db name ?command?\"" |
|
} |
|
set db [lindex $args [incr narg]] |
|
set name [lindex $args [incr narg]] |
|
# If the name is quoted, strip quotes, else downcase - same as SQL does. |
|
if {![regexp {^"(.*)"$} $name unused ccname]} { |
|
set ccname [string tolower $name] |
|
} |
|
|
|
if {$nargs == 3} { |
|
set proc [lindex $args [incr narg]] |
|
set pgtcl::notify($db,$ccname) $proc |
|
set pgtcl::notifopt($db,$ccname) $options |
|
# Use the original argument here, not case corrected/quotes stripped. |
|
set r [pg_exec $db "listen $name"] |
|
pg_result $r -clear |
|
} elseif {[info exists pgtcl::notify($db,$ccname)]} { |
|
unset -nocomplain pgtcl::notify($db,$ccname) pgtcl::notifopt($db,$ccname) |
|
pg_result [pg_exec $db "unlisten $ccname"] -clear |
|
} |
|
} |
|
|
|
# pg_execute: Execute a query, optionally iterating over the results. |
|
# |
|
# Returns the number of tuples selected or affected by the query. |
|
# Usage: pg_execute ?options? connection query ?proc? |
|
# Options: -array ArrayVar |
|
# -oid OidVar |
|
# If -array is not given with a SELECT, the data is put in variables |
|
# named by the fields. This is generally a bad idea and could be dangerous. |
|
# |
|
# If there is no proc body and the query return 1 or more rows, the first |
|
# row is stored in the array or variables and we return (as does libpgtcl). |
|
# |
|
# Notes: Handles proc return codes of: |
|
# 0(OK) 1(error) 2(return) 3(break) 4(continue) |
|
# Uses pg_exec and pg_result, but also makes direct access to the |
|
# structures used by them. |
|
|
|
proc pg_execute {args} { |
|
global errorCode errorInfo |
|
|
|
set usage "pg_execute ?-array arrayname?\ |
|
?-oid varname? connection queryString ?loop_body?" |
|
|
|
# Set defaults and parse command arguments: |
|
set use_array 0 |
|
set set_oid 0 |
|
set do_proc 0 |
|
set last_option_arg {} |
|
set n_nonswitch_args 0 |
|
set conn {} |
|
set query {} |
|
set proc {} |
|
foreach arg $args { |
|
if {$last_option_arg != ""} { |
|
if {$last_option_arg == "-array"} { |
|
set use_array 1 |
|
upvar $arg data |
|
} elseif {$last_option_arg == "-oid"} { |
|
set set_oid 1 |
|
upvar $arg oid |
|
} else { |
|
error "Unknown option $last_option_arg\n$usage" |
|
} |
|
set last_option_arg {} |
|
} elseif {[regexp ^- $arg]} { |
|
set last_option_arg $arg |
|
} else { |
|
if {[incr n_nonswitch_args] == 1} { |
|
set conn $arg |
|
} elseif {$n_nonswitch_args == 2} { |
|
set query $arg |
|
} elseif {$n_nonswitch_args == 3} { |
|
set do_proc 1 |
|
set proc $arg |
|
} else { |
|
error "Wrong # of arguments\n$usage" |
|
} |
|
} |
|
} |
|
if {$last_option_arg != "" || $n_nonswitch_args < 2} { |
|
error "Bad arguments\n$usage" |
|
} |
|
|
|
set res [pg_exec $conn $query] |
|
upvar #0 pgtcl::result$res result |
|
|
|
# For non-SELECT query, just process oid and return value. |
|
# Let pg_result do the decoding. |
|
if {[regexp {^PGRES_(COMMAND_OK|COPY|EMPTY_QUERY)} $result(status)]} { |
|
if {$set_oid} { |
|
set oid [pg_result $res -oid] |
|
} |
|
set ntuple [pg_result $res -cmdTuples] |
|
pg_result $res -clear |
|
return $ntuple |
|
} |
|
|
|
if {$result(status) != "PGRES_TUPLES_OK"} { |
|
set status [list $result(status) $result(error)] |
|
pg_result $res -clear |
|
error $status |
|
} |
|
|
|
# Handle a SELECT query. This is like pg_select, except the proc is optional, |
|
# and the fields can go in an array or variables. |
|
# With no proc, store the first row only. |
|
set code 0 |
|
if {!$use_array} { |
|
foreach attr $result(attrs) { |
|
upvar $attr data_$attr |
|
} |
|
} |
|
set ntuple $result(ntuple) |
|
for {set irow 0} {$irow < $ntuple} {incr irow} { |
|
set icol 0 |
|
if {$use_array} { |
|
foreach attr $result(attrs) { |
|
set data($attr) $result($irow,$icol) |
|
incr icol |
|
} |
|
} else { |
|
foreach attr $result(attrs) { |
|
set data_$attr $result($irow,$icol) |
|
incr icol |
|
} |
|
} |
|
if {!$do_proc} break |
|
set code [catch {uplevel 1 $proc} s] |
|
if {$code != 0 && $code != 4} break |
|
} |
|
pg_result $res -clear |
|
if {$code == 1} { |
|
return -code error -errorinfo $errorInfo -errorcode $errorCode $s |
|
} elseif {$code == 2 || $code > 4} { |
|
return -code $code $s |
|
} |
|
return $ntuple |
|
} |
|
|
|
# Extended query protocol: Bind parameters and execute prepared statement. |
|
# This is modelled on libpq PQexecPrepared. Use pg_exec to send a PREPARE |
|
# first; when called externally it does not handle unnamed statements. |
|
# This is also used internally by pg_exec_params, with an unnamed statement. |
|
# Parameters: |
|
# db Connection handle |
|
# stmt Name of the prepared SQL statement to execute |
|
# res_formats A list describing results: B* => Binary, else Text. |
|
# arg_formats A list describing args: B* => Binary, else Text. |
|
# args Variable number of arguments to bind to the query params. |
|
proc pg_exec_prepared {db stmt res_formats arg_formats args} { |
|
set nargs [llength $args] |
|
|
|
if {$pgtcl::debug} { puts "+pg_exec_prepared stmt=$stmt nargs=$nargs" } |
|
# Calculate argument format information: |
|
pgtcl::crunch_fcodes $arg_formats nfcodes fcodes |
|
|
|
# Build the first part of the Bind message: |
|
set out [binary format {x a*x S S* S} \ |
|
[encoding convertto identity $stmt] $nfcodes $fcodes $nargs] |
|
|
|
# Expand fcodes so there is a text/binary flag for each argument: |
|
if {$nfcodes == 0} { |
|
set all_fcodes [string repeat "0 " $nargs] |
|
} elseif {$nfcodes == 1} { |
|
set all_fcodes [string repeat "$fcodes " $nargs] |
|
} else { |
|
set all_fcodes $fcodes |
|
} |
|
|
|
# Append parameter values as { int32 length or 0 or -1 for NULL; data} |
|
# Note: There is no support for NULLs as parameters. |
|
# Encode all text parameters, leave binary parameters alone. |
|
foreach arg $args fcode $all_fcodes { |
|
if {$fcode} { |
|
append out [binary format I [string length $arg]] $arg |
|
} else { |
|
set encoded_arg [encoding convertto identity $arg] |
|
append out [binary format I [string length $encoded_arg]] $encoded_arg |
|
} |
|
} |
|
|
|
# Append result parameter format information: |
|
pgtcl::crunch_fcodes $res_formats nrfcodes rfcodes |
|
append out [binary format {S S*} $nrfcodes $rfcodes] |
|
|
|
# Send it off. Don't wait for BindComplete or Error, because the protocol |
|
# says the BE will discard until Sync anyway. |
|
pgtcl::sendmsg $db B $out -noflush |
|
unset out |
|
# Send DescribePortal for the unnamed portal: |
|
pgtcl::sendmsg $db D "P\0" -noflush |
|
# Send Execute, unnamed portal, unlimited rows: |
|
pgtcl::sendmsg $db E "\0\0\0\0\0" -noflush |
|
# Send Sync |
|
pgtcl::sendmsg $db S {} |
|
|
|
# Fetch query result and return result handle: |
|
return [pgtcl::getresult $db E] |
|
} |
|
|
|
# Extended query protocol: Parse, Bind and execute statement. This is similar |
|
# to pg_exec_prepared, but doesn't use a pre-prepared statement, and if you |
|
# want to pass binary parameters you must also provide the type OIDs. |
|
# This is modelled on libpq PQexecParams. |
|
# Parameters: |
|
# db Connection handle |
|
# query Query to execute, may contain parameters $1, $2, ... |
|
# res_formats A list describing results: B* => binary, else text |
|
# arg_formats A list describing args: B* => Binary, else Text. |
|
# arg_types A list of type OIDs for each argument (if Binary). |
|
# args Variable number of arguments to bind to the query params. |
|
|
|
# Protocol note: Perhaps the right way to do this is to send Parse, |
|
# then Flush, and check for ParseComplete or ErrorResponse. But then |
|
# if there is an error, you need to send Sync and build a result structure. |
|
# Since the backend will ignore everything after error until Sync, this |
|
# is coded the easier way: Just send everything and let the lower-level code |
|
# report the errors, whether on Parse or Bind or Execute. |
|
|
|
proc pg_exec_params {db query res_formats arg_formats arg_types args} { |
|
if {$pgtcl::debug} { puts "+pg_exec_params query=$query" } |
|
# Build and send Parse message with the SQL command and list of arg types: |
|
set out [binary format {x a*x S} [encoding convertto identity $query] \ |
|
[llength $arg_types]] |
|
foreach type $arg_types { |
|
append out [binary format I $type] |
|
} |
|
pgtcl::sendmsg $db P $out -noflush |
|
# See note above regarding not checking for ParseComplete here. |
|
# Proceed as with pg_exec_prepared, but with an unnamed statement: |
|
return [eval [list pg_exec_prepared $db "" $res_formats $arg_formats] $args] |
|
} |
|
|
|
# Get information about cursor ("portal"). See libpq PQdescribePortal |
|
# Parameters: |
|
# db Connection handle |
|
# cursor Name of a cursor. |
|
# Returns a result structure with no data, only column information. |
|
# Note: This does not handle NoData. That is documented as a valid response |
|
# but I don't see how it is possible. |
|
|
|
proc pg_describe_cursor {db cursor} { |
|
if {$pgtcl::debug} { puts "+pg_describe_cursor $cursor" } |
|
# Build and send the Describe Portal message, then sync. |
|
pgtcl::sendmsg $db D "P[binary format {a*x} \ |
|
[encoding convertto identity $cursor]]" -noflush |
|
pgtcl::sendmsg $db S {} |
|
|
|
# Wait for result, Describe mode (D), return result handle. |
|
return [pgtcl::getresult $db D] |
|
} |
|
|
|
# Get information about a prepared statement. See libpq PQdescribePrepared |
|
# Parameters: |
|
# db Connection handle |
|
# statement Name of a prepared statement. |
|
# Returns a result structure with no data, only column information and |
|
# also parameter information. |
|
|
|
proc pg_describe_prepared {db statement} { |
|
if {$pgtcl::debug} { puts "+pg_describe_prepared $statement" } |
|
# Build and send the Describe Statement message, then sync. |
|
pgtcl::sendmsg $db D "S[binary format {a*x} \ |
|
[encoding convertto identity $statement]]" -noflush |
|
pgtcl::sendmsg $db S {} |
|
|
|
# Wait for result, Describe mode (D), return result handle. |
|
return [pgtcl::getresult $db D] |
|
} |
|
|
|
# === Public procedures : Miscellaneous === |
|
|
|
# pg_notice_handler: Set/get handler command for Notice/Warning |
|
# Usage: pg_notice_handler connection ?command? |
|
# Parameters: |
|
# command If supplied, the new handler command. The notice text |
|
# will be appended as a list element. |
|
# If supplied but empty, ignore notice/warnings. |
|
# If not supplied, just return the current value. |
|
# Returns the previous handler command. |
|
proc pg_notice_handler {db args} { |
|
set return_value $pgtcl::notice($db) |
|
if {[set nargs [llength $args]] == 1} { |
|
set pgtcl::notice($db) [lindex $args 0] |
|
} elseif {$nargs != 0} { |
|
error "Wrong # args: should be \"pg_notice_handler connection ?command?\"" |
|
} |
|
return $return_value |
|
} |
|
|
|
# pg_configure: Configure options for PostgreSQL connections |
|
# This is provided only for backward compatibility with earlier versions. |
|
# Do not use. |
|
proc pg_configure {db option args} { |
|
if {[set nargs [llength $args]] > 1} { |
|
error "Wrong # args: should be \"pg_configure connection option ?value?\"" |
|
} |
|
switch -- $option { |
|
debug { upvar pgtcl::debug var } |
|
notice { upvar pgtcl::notice($db) var } |
|
default { |
|
error "Bad option \"$option\": must be one of notice, debug" |
|
} |
|
} |
|
set return_value $var |
|
if {$nargs} { |
|
set var [lindex $args 0] |
|
} |
|
return $return_value |
|
} |
|
|
|
# pg_escape_string: Returns an escaped string for use as an SQL string. |
|
# An optional connection argument can be provided, which is used to |
|
# determine the setting of "standard_conforming_strings". In libpq, this also |
|
# affects handling of non-ASCII characters, but pgin.tcl does not support that. |
|
# Caution: There is an incompatible pg_escape_string in another Pgtcl |
|
# implementation, which makes pg_escape_string and pg_quote equivalent. |
|
# In pgintcl and pgtclng, only pg_quote includes quotes around the return. |
|
proc pg_escape_string {args} { |
|
if {[set argc [llength $args]] == 1} { |
|
set db _default_ |
|
set argi 0 |
|
} elseif {$argc == 2} { |
|
set db [lindex $args 0] |
|
set argi 1 |
|
} else { |
|
error "wrong # args: should be pg_escape_string ?conn? string" |
|
} |
|
if {![info exists pgtcl::std_str($db)]} { |
|
error "$db is not a valid postgresql connection" |
|
} |
|
if {$pgtcl::std_str($db)} { |
|
return [string map {' ''} [lindex $args $argi]] |
|
} |
|
return [string map {' '' \\ \\\\} [lindex $args $argi]] |
|
} |
|
|
|
# pg_quote: Returns a quoted, escaped string for use as an SQL string. |
|
# An optional connection argument can be provided, which is used to |
|
# determine the setting of "standard_conforming_strings". In libpq, this also |
|
# affects handling of non-ASCII characters, but pgin.tcl does not support that. |
|
proc pg_quote {args} { |
|
if {[set argc [llength $args]] == 1} { |
|
return "'[pg_escape_string [lindex $args 0]]'" |
|
} |
|
if {$argc == 2} { |
|
return "'[pg_escape_string [lindex $args 0] [lindex $args 1]]'" |
|
} |
|
error "wrong # args: should be pg_quote ?conn? string" |
|
} |
|
|
|
# pg_escape_identifier: Return a double-quoted, escaped identifier string |
|
# This is for table names, column names, etc. See libpq PQescapeIdentifier(). |
|
# Caution: Pgintcl ignores the connection handle, and does not account for |
|
# encoding. |
|
proc pg_escape_identifier {db_ignored s} { |
|
return "\"[string map {{"} {""}} $s]\"" |
|
} |
|
|
|
# pg_escape_literal: Return a single-quoted, escaped string for use in SQL. |
|
# See libpq PQescapeLiteral(). This is effectively equivalent to pg_quote, |
|
# but the result is independed of standard_conforming_strings - if the |
|
# string has any \, they are doubled and the PostgreSQL-specific Escape |
|
# String syntax E'...' is used. |
|
# Caution: Pgintcl ignores the connection handle, and does not account for |
|
# encoding. |
|
proc pg_escape_literal {db_ignored s} { |
|
if {[string first "\\" $s] >= 0} { |
|
return " E'[string map {' '' \\ \\\\} $s]'" |
|
} |
|
return "'[string map {' ''} $s]'" |
|
} |
|
|
|
# pg_escape_bytea: Escape a binary string for use as a quoted SQL string. |
|
# Returns the escaped string, which is safe for use inside single quotes |
|
# in an SQL statement. Note back-slashes are doubled due to double parsing |
|
# in the backend. Emulates libpq PQescapeBytea() or PQescapeByteaConn(), |
|
# except it always uses 'escape' encoding, never 'hex' encoding. |
|
# See also pg_unescape_bytea, but note that these functions are not inverses. |
|
# (I tried many versions to improve speed and this was fastest, although still |
|
# slow. The numeric constants 92=\ and 39=` were part of that optimization.) |
|
proc pg_escape_bytea {args} { |
|
if {[set argc [llength $args]] == 1} { |
|
set db _default_ |
|
set binstr [lindex $args 0] |
|
} elseif {$argc == 2} { |
|
set db [lindex $args 0] |
|
set binstr [lindex $args 1] |
|
} else { |
|
error "wrong # args: should be pg_escape_bytea ?conn? string" |
|
} |
|
if {![info exists pgtcl::std_str($db)]} { |
|
error "$db is not a valid postgresql connection" |
|
} |
|
if {$pgtcl::std_str($db)} { |
|
set backslash "\\" |
|
} else { |
|
set backslash "\\\\" |
|
} |
|
set result "" |
|
|
|
binary scan $binstr c* val_list |
|
foreach c [split $binstr {}] val $val_list { |
|
if {$val == 92} { |
|
append result $backslash$backslash |
|
} elseif {$val == 39} { |
|
append result '' |
|
} elseif {$val < 32 || 126 < $val} { |
|
append result $backslash [format %03o [expr {$val & 255}]] |
|
} else { |
|
append result $c |
|
} |
|
} |
|
return $result |
|
} |
|
|
|
# pg_unescape_bytea: Unescape a string returned from PostgreSQL as an |
|
# escaped bytea object and return a binary string. |
|
# Emulates libpq PQunescapeBytea(), and supports both 'hex' and 'escape' |
|
# coding, automatically detected, so it works through PostgreSQL-9.0. |
|
# See also pg_escape_bytea, but note that these functions are not inverses. |
|
# Implementation note: Iterative implementations perform very poorly. |
|
# The method used for 'escape' decoding is from Benny Riefenstahl via |
|
# Jerry Levan. It works much faster than looping or string map, and returns |
|
# the correct data on any value produced by the PostgreSQL backend from |
|
# converting a bytea data type to text (byteaout). |
|
# But it does not work the same as PQunescapeBytea() for all values. |
|
# For example, passing \a here returns 0x07, but PQunescapeBytea returns 'a'. |
|
# The method used for 'hex' decoding is also a compromise which should work |
|
# correctly within the range of results produced by PostgreSQL. For example, |
|
# this implementation ignores whitespace anywhere, but PostgreSQL only |
|
# allows whitespace between pairs of digits. |
|
proc pg_unescape_bytea {str} { |
|
if {[string range $str 0 1] != "\\x"} { |
|
# Escape mode |
|
return [subst -nocommands -novariables $str] |
|
} |
|
# Hex mode. Strip leading \x and whitespace, then decode as hex. |
|
return [binary format H* [regsub -all "\\s" [string range $str 2 end] ""]] |
|
} |
|
|
|
# pg_parameter_status: Return the value of a backend parameter value. |
|
# These are generally supplied by the backend during startup. |
|
proc pg_parameter_status {db name} { |
|
upvar #0 pgtcl::param_$db param |
|
if {[info exists param($name)]} { |
|
return $param($name) |
|
} |
|
return "" |
|
} |
|
|
|
# pg_transaction_status: Return the current transaction status. |
|
# Returns a string: IDLE INTRANS INERROR or UNKNOWN. |
|
proc pg_transaction_status {db} { |
|
if {[info exists pgtcl::xstate($db)]} { |
|
switch -- $pgtcl::xstate($db) { |
|
I { return IDLE } |
|
T { return INTRANS } |
|
E { return INERROR } |
|
} |
|
} |
|
return UNKNOWN |
|
} |
|
|
|
|
|
# pg_encrypt_password: Encrypt a password for commands that accept |
|
# a pre-encrypted password, like ALTER USER. |
|
# Returns a PostgreSQL-style encrypted password. |
|
# See pgtcl::encrypt_password for more information. |
|
proc pg_encrypt_password {password username} { |
|
return "md5[pgtcl::encrypt_password $password $username]" |
|
} |
|
|
|
# pg_backend_pid: Return the process ID (PID) of the backend process |
|
proc pg_backend_pid {db} { |
|
if {[info exists pgtcl::bepid($db)]} { |
|
return $pgtcl::bepid($db) |
|
} |
|
return 0 |
|
} |
|
|
|
# pg_server_version: Return the PostgreSQL server version as an integer. |
|
# This parses the server_version parameter sent on connect. |
|
proc pg_server_version {db} { |
|
switch [scan [pg_parameter_status $db server_version] %d.%d.%d x y z] { |
|
3 { return [expr {($x * 100 + $y) * 100 + $z}] } |
|
2 { return [expr {$x * 10000 + $y}] } |
|
} |
|
return 0 |
|
} |
|
|
|
# === Internal Procedure to support COPY === |
|
|
|
# Handle a CopyInResponse or CopyOutResponse message: |
|
proc pgtcl::begincopy {result_name direction} { |
|
upvar $result_name result |
|
set db $result(conn) |
|
if {[pgtcl::get_int8 $db]} { |
|
error "pg_exec: COPY BINARY is not supported" |
|
} |
|
set result(status) PGRES_COPY_$direction |
|
# Column count and per-column formats are ignored. |
|
set ncol [pgtcl::get_int16 $db] |
|
pgtcl::skip $db [expr {2*$ncol}] |
|
if {$pgtcl::debug} { puts "+pg_exec begin copy $direction" } |
|
} |
|
|
|
# === Public procedures: COPY === |
|
|
|
# I/O procedures to support COPY. No longer able to just read/write the |
|
# channel, due to the message procotol. |
|
|
|
# Read line from COPY TO. Returns the copy line if OK, else "" on end. |
|
# Note: The returned line does not end in a newline, so you can split it |
|
# on tab and get a list of column values. |
|
# At end of COPY, it takes the CopyDone only. pg_endcopy must be called to |
|
# get the CommandComplete and ReadyForQuery messages. |
|
proc pg_copy_read {res} { |
|
upvar #0 [pgtcl::checkres $res] result |
|
set db $result(conn) |
|
if {$result(status) != "PGRES_COPY_OUT"} { |
|
error "pg_copy_read called but connection is not doing a COPY OUT" |
|
} |
|
# Notice/Notify etc are not allowed during copy, so no loop needed. |
|
set c [pgtcl::readmsg $db] |
|
if {$pgtcl::debug} { puts "+pg_copy_read msg $c" } |
|
if {$c == "d"} { |
|
return [string trimright \ |
|
[encoding convertfrom identity [pgtcl::get_rest $db]] "\n\r"] |
|
} |
|
if {$c == "c"} { |
|
return "" |
|
} |
|
# Error or invalid response. |
|
if {$c == "E"} { |
|
set result(status) PGRES_FATAL_ERROR |
|
set result(error) [pgtcl::get_response $db result] |
|
return "" |
|
} |
|
error "pg_copy_read: procotol violation, unexpected $c in copy out" |
|
} |
|
|
|
# Write line for COPY FROM. This must represent a single record (tuple) with |
|
# values separated by tabs. Do not add a newline; pg_copy_write does this. |
|
proc pg_copy_write {res line} { |
|
upvar #0 [pgtcl::checkres $res] result |
|
pgtcl::sendmsg $result(conn) d "[encoding convertto identity $line]\n" |
|
} |
|
|
|
# End a COPY TO/FROM. This is needed to finish up the protocol after |
|
# reading or writing. On COPY TO, this needs to be called after |
|
# pg_copy_read returns an empty string. On COPY FROM, this needs to |
|
# be called after writing the last record with pg_copy_write. |
|
# Note: Do not write or expect to read "\." anymore. |
|
# When it returns, the result structure (res) will be updated. |
|
proc pg_endcopy {res} { |
|
upvar #0 [pgtcl::checkres $res] result |
|
set db $result(conn) |
|
if {$pgtcl::debug} { puts "+pg_endcopy end $result(status)" } |
|
|
|
# An error might have been sent during a COPY TO, so the result |
|
# status will already be FATAL and should not be disturbed. |
|
if {$result(status) != "PGRES_FATAL_ERROR"} { |
|
if {$result(status) == "PGRES_COPY_IN"} { |
|
# Send CopyDone |
|
pgtcl::sendmsg $db c {} |
|
} elseif {$result(status) != "PGRES_COPY_OUT"} { |
|
error "pg_endcopy called but connection is not doing a COPY" |
|
} |
|
set result(status) PGRES_COMMAND_OK |
|
} |
|
|
|
# We're looking for CommandComplete and ReadyForQuery here, but other |
|
# things can happen too. |
|
while {[set c [pgtcl::readmsg $db]] != "Z"} { |
|
if {![pgtcl::common_message $c $db result]} { |
|
error "Unexpected reply from database: $c" |
|
} |
|
} |
|
set pgtcl::xstate($db) [pgtcl::get_byte $db] |
|
if {$pgtcl::debug} { puts "+pg_endcopy returns, st=$result(status)" } |
|
} |
|
|
|
# === Internal producedures for Function Call (used by Large Object) === |
|
|
|
# Internal procedure to lookup, cache, and return a PostgreSQL function OID. |
|
# This assumes all connections have the same function OIDs, which might not be |
|
# true if you connect to servers running different versions of PostgreSQL. |
|
# Throws an error if the OID is not found by PostgreSQL. |
|
# To call overloaded functions, argument types must be specified in parentheses |
|
# after the function name, in the the exact same format as psql "\df". |
|
# This is a list of types separated by a comma and one space. |
|
# For example: fname="like(text, text)". |
|
# The return type cannot be specified. I don't think there are any functions |
|
# distinguished only by return type. |
|
proc pgtcl::getfnoid {db fname} { |
|
variable fnoids |
|
|
|
if {![info exists fnoids($fname)]} { |
|
|
|
# Separate the function name from the (arg type list): |
|
if {[regexp {^([^(]*)\(([^)]*)\)$} $fname unused fcn arglist]} { |
|
set amatch " and oidvectortypes(proargtypes)='$arglist'" |
|
} else { |
|
set fcn $fname |
|
set amatch "" |
|
} |
|
pg_select $db "select oid from pg_proc where proname='$fcn' $amatch" d { |
|
set fnoids($fname) $d(oid) |
|
} |
|
if {![info exists fnoids($fname)]} { |
|
error "Unable to get OID of database function $fname" |
|
} |
|
} |
|
return $fnoids($fname) |
|
} |
|
|
|
# Internal procedure to implement PostgreSQL "fast-path" function calls. |
|
# $fn_oid is the OID of the PostgreSQL function. See pgtcl::getfnoid. |
|
# $result_name is the name of the variable to store the backend function |
|
# result into. |
|
# $arginfo is a list of argument descriptors, each is I or S or a number. |
|
# I means the argument is an integer32. |
|
# S means the argument is a string, and its actual length is used. |
|
# A number means send exactly that many bytes (null-pad if needed) from |
|
# the argument. |
|
# (Argument type S is passed in Ascii format code, others as Binary.) |
|
# $arglist is a list of arguments to the PostgreSQL function. (This |
|
# is actually a pass-through argument 'args' from the wrappers.) |
|
# Throws Tcl error on error, otherwise returns size of the result |
|
# stored into the $result_name variable. |
|
|
|
proc pgtcl::callfn {db fn_oid result_name arginfo arglist} { |
|
upvar $result_name result |
|
|
|
set nargs [llength $arginfo] |
|
if {$pgtcl::debug} { |
|
puts "+callfn oid=$fn_oid nargs=$nargs info=$arginfo args=$arglist" |
|
} |
|
|
|
# Function call: oid nfcodes fcodes... nargs {arglen arg}... resultfcode |
|
set fcodes {} |
|
foreach k $arginfo { |
|
if {$k == "S"} { |
|
lappend fcodes 0 |
|
} else { |
|
lappend fcodes 1 |
|
} |
|
} |
|
set out [binary format {I S S* S} $fn_oid $nargs $fcodes $nargs] |
|
# Append each argument and its length: |
|
foreach k $arginfo arg $arglist { |
|
if {$k == "I"} { |
|
append out [binary format II 4 $arg] |
|
} elseif {$k == "S"} { |
|
append out [binary format I [string length $arg]] $arg |
|
} else { |
|
append out [binary format Ia$k $k $arg] |
|
} |
|
} |
|
# Append format code for binary result: |
|
append out [binary format S 1] |
|
pgtcl::sendmsg $db F $out |
|
|
|
set result {} |
|
set result_size 0 |
|
# Fake up a partial result structure for pgtcl::common_message : |
|
set res(error) "" |
|
|
|
# FunctionCall response. Also handles common messages (notify, notice). |
|
while {[set c [pgtcl::readmsg $db]] != "Z"} { |
|
if {$c == "V"} { |
|
set result_size [pgtcl::get_int32 $db] |
|
if {$result_size > 0} { |
|
set result [pgtcl::get_bytes $db $result_size] |
|
} else { |
|
set result "" |
|
} |
|
} elseif {![pgtcl::common_message $c $db res]} { |
|
error "Unexpected reply from database: $c" |
|
} |
|
} |
|
set pgtcl::xstate($db) [pgtcl::get_byte $db] |
|
if {$res(error) != ""} { |
|
error $res(error) |
|
} |
|
return $result_size |
|
} |
|
|
|
# === Public prodedures: Function Call === |
|
|
|
# Public interface to pgtcl::callfn. |
|
proc pg_callfn {db fname result_name arginfo args} { |
|
upvar $result_name result |
|
return [pgtcl::callfn $db [pgtcl::getfnoid $db $fname] result $arginfo $args] |
|
} |
|
|
|
# Public, simplified interface to pgtcl::callfn when an int32 return value is |
|
# expected. Returns the backend function return value. |
|
proc pg_callfn_int {db fname arginfo args} { |
|
set n [pgtcl::callfn $db [pgtcl::getfnoid $db $fname] result $arginfo $args] |
|
if {$n != 4} { |
|
error "Unexpected response size ($result_size) to pg function call $fname" |
|
} |
|
binary scan $result I val |
|
return $val |
|
} |
|
|
|
# === Internal procedure to support Large Object === |
|
|
|
# Convert a LO mode string into the value of the constants used by libpq. |
|
# Note: libpgtcl uses a mode like INV_READ|INV_WRITE for lo_creat, but |
|
# r, w, or rw for lo_open (which it translates to INV_READ|INV_WRITE). |
|
# This seems like a mistake. The code here accepts either form for either. |
|
proc pgtcl::lomode {mode} { |
|
set imode 0 |
|
if {[string match -nocase *INV_* $mode]} { |
|
if {[string match -nocase *INV_READ* $mode]} { |
|
set imode 0x40000 |
|
} |
|
if {[string match -nocase *INV_WRITE* $mode]} { |
|
set imode [expr {$imode + 0x20000}] |
|
} |
|
} else { |
|
if {[string match -nocase *r* $mode]} { |
|
set imode 0x40000 |
|
} |
|
if {[string match -nocase *w* $mode]} { |
|
set imode [expr {$imode + 0x20000}] |
|
} |
|
} |
|
if {$imode == 0} { |
|
error "Invalid large object mode $mode" |
|
} |
|
return $imode |
|
} |
|
|
|
# === Public prodedures: Large Object === |
|
|
|
# Create large object and return OID. |
|
# See note regarding mode above at pgtcl::lomode. |
|
proc pg_lo_creat {db mode} { |
|
if {[catch {pg_callfn_int $db lo_creat I [pgtcl::lomode $mode]} result]} { |
|
error "Large Object create failed\n$result" |
|
} |
|
if {$result == -1} { |
|
error "Large Object create failed" |
|
} |
|
return $result |
|
} |
|
|
|
# Open large object and return large object file descriptor. |
|
# See note regarding mode above at pgtcl::lomode. |
|
proc pg_lo_open {db loid mode} { |
|
if {[catch {pg_callfn_int $db lo_open "I I" $loid [pgtcl::lomode $mode]} \ |
|
result]} { |
|
error "Large Object open failed\n$result" |
|
} |
|
if {$result == -1} { |
|
error "Large Object open failed" |
|
} |
|
return $result |
|
} |
|
|
|
# Close large object file descriptor. |
|
proc pg_lo_close {db lofd} { |
|
if {[catch {pg_callfn_int $db lo_close I $lofd} result]} { |
|
error "Large Object close failed\n$result" |
|
} |
|
return $result |
|
} |
|
|
|
# Delete large object: |
|
proc pg_lo_unlink {db loid} { |
|
if {[catch {pg_callfn_int $db lo_unlink I $loid} result]} { |
|
error "Large Object unlink failed\n$result" |
|
} |
|
return $result |
|
} |
|
|
|
# Read from large object. |
|
# Note: The original PostgreSQL documentation says it returns -1 on error, |
|
# which is a bad idea since you can't get to the error message. But it's |
|
# probably too late to change it, so we remain bug compatible. |
|
proc pg_lo_read {db lofd buf_name maxlen} { |
|
upvar $buf_name buf |
|
if {[catch {pg_callfn $db loread buf "I I" $lofd $maxlen} result]} { |
|
return -1 |
|
} |
|
return $result |
|
} |
|
|
|
# Write to large object. At most $len bytes are written. |
|
# See note above on pg_lo_read error return. |
|
proc pg_lo_write {db lofd buf len} { |
|
if {[set buflen [string length $buf]] < $len} { |
|
set len $buflen |
|
} |
|
if {[catch {pg_callfn_int $db lowrite "I $len" $lofd $buf} result]} { |
|
return -1 |
|
} |
|
return $result |
|
} |
|
|
|
# Seek to offset inside large object: |
|
proc pg_lo_lseek {db lofd offset whence} { |
|
if {[set iwhence [lsearch {SEEK_SET SEEK_CUR SEEK_END} $whence]] < 0} { |
|
error "'whence' must be SEEK_SET, SEEK_CUR, or SEEK_END" |
|
} |
|
if {[catch {pg_callfn_int $db lo_lseek "I I I" $lofd $offset $iwhence} \ |
|
result]} { |
|
error "Large Object seek failed\n$result" |
|
} |
|
return $result |
|
} |
|
|
|
# Seek to offset inside large object, using 64-bit offset: |
|
proc pg_lo_lseek64 {db lofd offset whence} { |
|
if {[set iwhence [lsearch {SEEK_SET SEEK_CUR SEEK_END} $whence]] < 0} { |
|
error "'whence' must be SEEK_SET, SEEK_CUR, or SEEK_END" |
|
} |
|
set off64 [binary format W $offset] |
|
if {[catch {pg_callfn $db lo_lseek64 newoff "I 8 I" $lofd $off64 $iwhence} \ |
|
result] || $result != 8} { |
|
error "Large Object seek failed\n$result" |
|
} |
|
binary scan $newoff W off64 |
|
return $off64 |
|
} |
|
|
|
# Return location of file offset in large object: |
|
proc pg_lo_tell {db lofd} { |
|
if {[catch {pg_callfn_int $db lo_tell I $lofd} result]} { |
|
error "Large Object tell offset failed\n$result" |
|
} |
|
return $result |
|
} |
|
|
|
# Return location of file offset in large object, using 64-bit offset: |
|
proc pg_lo_tell64 {db lofd} { |
|
if {[catch {pg_callfn $db lo_tell64 offset I $lofd} result] || $result != 8} { |
|
error "Large Object tell offset failed\n$result" |
|
} |
|
binary scan $offset W offset_int64 |
|
return $offset_int64 |
|
} |
|
|
|
# Truncate large object (or extend) to given size: |
|
proc pg_lo_truncate {db lofd len} { |
|
if {[catch {pg_callfn_int $db lo_truncate "I I" $lofd $len} result]} { |
|
error "Large Object truncate failed\n$result" |
|
} |
|
return $result |
|
} |
|
|
|
# Truncate large object (or extend) to given size, using 64-bit offset |
|
proc pg_lo_truncate64 {db lofd len} { |
|
set size64 [binary format W $len] |
|
if {[catch {pg_callfn_int $db lo_truncate64 "I 8" $lofd $size64} result]} { |
|
error "Large Object truncate failed\n$result" |
|
} |
|
return $result |
|
} |
|
|
|
# Import large object. Wrapper for lo_creat, lo_open, lo_write. |
|
# Returns Large Object OID, which should be stored in a table somewhere. |
|
proc pg_lo_import {db filename} { |
|
if {[catch {open $filename} f]} { |
|
error "Large object import of $filename failed\n$f" |
|
} |
|
fconfigure $f -translation binary |
|
if {[catch {pg_lo_creat $db INV_READ|INV_WRITE} loid]} { |
|
close $f |
|
error "Large Object import of $filename failed\n$loid" |
|
} |
|
if {[catch {pg_lo_open $db $loid w} lofd]} { |
|
close $f |
|
set error "Large Object import of $filename failed\n$lofd" |
|
} |
|
|
|
while {1} { |
|
set buf [read $f 32768] |
|
if {[set len [string length $buf]] == 0} break |
|
if {[pg_lo_write $db $lofd $buf $len] != $len} { |
|
close $f |
|
# Based on comments in libpq source, do not do pg_lo_close here because |
|
# it is already in a failed transaction and will overwrite any error. |
|
error "Large Object import failed to write $len bytes" |
|
} |
|
} |
|
close $f |
|
pg_lo_close $db $lofd |
|
return $loid |
|
} |
|
|
|
# Export large object. Wrapper for lo_open, lo_read. |
|
proc pg_lo_export {db loid filename} { |
|
if {[catch {pg_lo_open $db $loid r} lofd]} { |
|
error "Large Object export to $filename failed\n$lofd" |
|
} |
|
if {[catch {open $filename w} f]} { |
|
pg_lo_close $db $lofd |
|
error "Large object export to $filename failed\n$f" |
|
} |
|
fconfigure $f -translation binary |
|
while {[set len [pg_lo_read $db $lofd buf 32768]] > 0} { |
|
puts -nonewline $f $buf |
|
} |
|
close $f |
|
if {$len < 0} { |
|
# Based on comments in libpq source, do not do pg_lo_close here because |
|
# it is already in a failed transaction and will overwrite any error. |
|
error "Large Object export to $filename failed\nLarge object read error" |
|
} |
|
pg_lo_close $db $lofd |
|
} |
|
|
|
# === MD5 Checksum procedures for password authentication === |
|
|
|
# Coded in Tcl by L Bayuk, using these sources: |
|
# RFC1321 |
|
# PostgreSQL: src/backend/libpq/md5.c |
|
# If you want a better/faster MD5 implementation, see tcllib. |
|
|
|
namespace eval md5 { } |
|
|
|
# Round 1 helper, e.g.: |
|
# a = b + ROT_LEFT((a + F(b, c, d) + X[0] + 0xd76aa478), 7) |
|
# p1 p2 p1 p3 p4 p5 p6 p7 |
|
# Where F(x,y,z) = (x & y) | (~x & z) |
|
# |
|
proc md5::round1 {p1 p2 p3 p4 p5 p6 p7} { |
|
set r [expr {$p2 + ($p1 & $p3 | ~$p1 & $p4) + $p5 + $p6}] |
|
return [expr {$p1 + ($r << $p7 | (($r >> (32 - $p7)) & ((1 << $p7) - 1)))}] |
|
} |
|
|
|
# Round 2 helper, e.g.: |
|
# a = b + ROT_LEFT((a + G(b, c, d) + X[1] + 0xf61e2562), 5) |
|
# p1 p2 p1 p3 p4 p5 p6 p7 |
|
# Where G(x,y,z) = (x & z) | (y & ~z) |
|
# |
|
proc md5::round2 {p1 p2 p3 p4 p5 p6 p7} { |
|
set r [expr {$p2 + ($p1 & $p4 | $p3 & ~$p4) + $p5 + $p6}] |
|
return [expr {$p1 + ($r << $p7 | (($r >> (32 - $p7)) & ((1 << $p7) - 1)))}] |
|
} |
|
|
|
# Round 3 helper, e.g.: |
|
# a = b + ROT_LEFT((a + H(b, c, d) + X[5] + 0xfffa3942), 4) |
|
# p1 p2 p1 p3 p4 p5 p6 p7 |
|
# Where H(x, y, z) = x ^ y ^ z |
|
# |
|
proc md5::round3 {p1 p2 p3 p4 p5 p6 p7} { |
|
set r [expr {$p2 + ($p1 ^ $p3 ^ $p4) + $p5 + $p6}] |
|
return [expr {$p1 + ($r << $p7 | (($r >> (32 - $p7)) & ((1 << $p7) - 1)))}] |
|
} |
|
|
|
# Round 4 helper, e.g.: |
|
# a = b + ROT_LEFT((a + I(b, c, d) + X[0] + 0xf4292244), 6) |
|
# p1 p2 p1 p3 p4 p5 p6 p7 |
|
# Where I(x, y, z) = y ^ (x | ~z) |
|
# |
|
proc md5::round4 {p1 p2 p3 p4 p5 p6 p7} { |
|
set r [expr {$p2 + ($p3 ^ ($p1 | ~$p4)) + $p5 + $p6}] |
|
return [expr {$p1 + ($r << $p7 | (($r >> (32 - $p7)) & ((1 << $p7) - 1)))}] |
|
} |
|
|
|
# Do one set of rounds. Updates $state(0:3) with results from $x(0:16). |
|
proc md5::round {x_name state_name} { |
|
upvar $x_name x $state_name state |
|
set a $state(0) |
|
set b $state(1) |
|
set c $state(2) |
|
set d $state(3) |
|
|
|
# Round 1, steps 1-16 |
|
set a [round1 $b $a $c $d $x(0) 0xd76aa478 7] |
|
set d [round1 $a $d $b $c $x(1) 0xe8c7b756 12] |
|
set c [round1 $d $c $a $b $x(2) 0x242070db 17] |
|
set b [round1 $c $b $d $a $x(3) 0xc1bdceee 22] |
|
set a [round1 $b $a $c $d $x(4) 0xf57c0faf 7] |
|
set d [round1 $a $d $b $c $x(5) 0x4787c62a 12] |
|
set c [round1 $d $c $a $b $x(6) 0xa8304613 17] |
|
set b [round1 $c $b $d $a $x(7) 0xfd469501 22] |
|
set a [round1 $b $a $c $d $x(8) 0x698098d8 7] |
|
set d [round1 $a $d $b $c $x(9) 0x8b44f7af 12] |
|
set c [round1 $d $c $a $b $x(10) 0xffff5bb1 17] |
|
set b [round1 $c $b $d $a $x(11) 0x895cd7be 22] |
|
set a [round1 $b $a $c $d $x(12) 0x6b901122 7] |
|
set d [round1 $a $d $b $c $x(13) 0xfd987193 12] |
|
set c [round1 $d $c $a $b $x(14) 0xa679438e 17] |
|
set b [round1 $c $b $d $a $x(15) 0x49b40821 22] |
|
|
|
# Round 2, steps 17-32 |
|
set a [round2 $b $a $c $d $x(1) 0xf61e2562 5] |
|
set d [round2 $a $d $b $c $x(6) 0xc040b340 9] |
|
set c [round2 $d $c $a $b $x(11) 0x265e5a51 14] |
|
set b [round2 $c $b $d $a $x(0) 0xe9b6c7aa 20] |
|
set a [round2 $b $a $c $d $x(5) 0xd62f105d 5] |
|
set d [round2 $a $d $b $c $x(10) 0x02441453 9] |
|
set c [round2 $d $c $a $b $x(15) 0xd8a1e681 14] |
|
set b [round2 $c $b $d $a $x(4) 0xe7d3fbc8 20] |
|
set a [round2 $b $a $c $d $x(9) 0x21e1cde6 5] |
|
set d [round2 $a $d $b $c $x(14) 0xc33707d6 9] |
|
set c [round2 $d $c $a $b $x(3) 0xf4d50d87 14] |
|
set b [round2 $c $b $d $a $x(8) 0x455a14ed 20] |
|
set a [round2 $b $a $c $d $x(13) 0xa9e3e905 5] |
|
set d [round2 $a $d $b $c $x(2) 0xfcefa3f8 9] |
|
set c [round2 $d $c $a $b $x(7) 0x676f02d9 14] |
|
set b [round2 $c $b $d $a $x(12) 0x8d2a4c8a 20] |
|
|
|
# Round 3, steps 33-48 |
|
set a [round3 $b $a $c $d $x(5) 0xfffa3942 4] |
|
set d [round3 $a $d $b $c $x(8) 0x8771f681 11] |
|
set c [round3 $d $c $a $b $x(11) 0x6d9d6122 16] |
|
set b [round3 $c $b $d $a $x(14) 0xfde5380c 23] |
|
set a [round3 $b $a $c $d $x(1) 0xa4beea44 4] |
|
set d [round3 $a $d $b $c $x(4) 0x4bdecfa9 11] |
|
set c [round3 $d $c $a $b $x(7) 0xf6bb4b60 16] |
|
set b [round3 $c $b $d $a $x(10) 0xbebfbc70 23] |
|
set a [round3 $b $a $c $d $x(13) 0x289b7ec6 4] |
|
set d [round3 $a $d $b $c $x(0) 0xeaa127fa 11] |
|
set c [round3 $d $c $a $b $x(3) 0xd4ef3085 16] |
|
set b [round3 $c $b $d $a $x(6) 0x04881d05 23] |
|
set a [round3 $b $a $c $d $x(9) 0xd9d4d039 4] |
|
set d [round3 $a $d $b $c $x(12) 0xe6db99e5 11] |
|
set c [round3 $d $c $a $b $x(15) 0x1fa27cf8 16] |
|
set b [round3 $c $b $d $a $x(2) 0xc4ac5665 23] |
|
|
|
# Round 4, steps 49-64 |
|
set a [round4 $b $a $c $d $x(0) 0xf4292244 6] |
|
set d [round4 $a $d $b $c $x(7) 0x432aff97 10] |
|
set c [round4 $d $c $a $b $x(14) 0xab9423a7 15] |
|
set b [round4 $c $b $d $a $x(5) 0xfc93a039 21] |
|
set a [round4 $b $a $c $d $x(12) 0x655b59c3 6] |
|
set d [round4 $a $d $b $c $x(3) 0x8f0ccc92 10] |
|
set c [round4 $d $c $a $b $x(10) 0xffeff47d 15] |
|
set b [round4 $c $b $d $a $x(1) 0x85845dd1 21] |
|
set a [round4 $b $a $c $d $x(8) 0x6fa87e4f 6] |
|
set d [round4 $a $d $b $c $x(15) 0xfe2ce6e0 10] |
|
set c [round4 $d $c $a $b $x(6) 0xa3014314 15] |
|
set b [round4 $c $b $d $a $x(13) 0x4e0811a1 21] |
|
set a [round4 $b $a $c $d $x(4) 0xf7537e82 6] |
|
set d [round4 $a $d $b $c $x(11) 0xbd3af235 10] |
|
set c [round4 $d $c $a $b $x(2) 0x2ad7d2bb 15] |
|
set b [round4 $c $b $d $a $x(9) 0xeb86d391 21] |
|
|
|
incr state(0) $a |
|
incr state(1) $b |
|
incr state(2) $c |
|
incr state(3) $d |
|
} |
|
|
|
# Pad out buffer per MD5 spec: |
|
proc md5::pad {buf_name} { |
|
upvar $buf_name buf |
|
|
|
# Length in bytes: |
|
set len [string length $buf] |
|
# Length in bits as 2 32 bit words: |
|
set len64hi [expr {$len >> 29 & 7}] |
|
set len64lo [expr {$len << 3}] |
|
|
|
# Append 1 special byte, then append 0 or more 0 bytes until |
|
# (length in bytes % 64) == 56 |
|
set pad [expr {64 - ($len + 8) % 64}] |
|
append buf [binary format a$pad "\x80"] |
|
|
|
# Append the length in bits as a 64 bit value, low bytes first. |
|
append buf [binary format i1i1 $len64lo $len64hi] |
|
|
|
} |
|
|
|
# Calculate MD5 Digest over a string, return as 32 hex digit string. |
|
proc md5::digest {buf} { |
|
# This is 0123456789abcdeffedcba9876543210 in byte-swapped order: |
|
set state(0) 0x67452301 |
|
set state(1) 0xEFCDAB89 |
|
set state(2) 0x98BADCFE |
|
set state(3) 0x10325476 |
|
|
|
# Pad buffer per RFC to exact multiple of 64 bytes. |
|
pad buf |
|
|
|
# Calculate digest in 64 byte chunks: |
|
set nwords 0 |
|
set nbytes 0 |
|
set word 0 |
|
binary scan $buf c* bytes |
|
# Unclear, but the data seems to get byte swapped here. |
|
foreach c $bytes { |
|
set word [expr {$c << 24 | ($word >> 8 & 0xffffff) }] |
|
if {[incr nbytes] == 4} { |
|
set nbytes 0 |
|
set x($nwords) $word |
|
set word 0 |
|
if {[incr nwords] == 16} { |
|
round x state |
|
set nwords 0 |
|
} |
|
} |
|
} |
|
|
|
# Result is state(0:3), but each word is taken low byte first. |
|
set result {} |
|
for {set i 0} {$i <= 3} {incr i} { |
|
set w $state($i) |
|
append result [format %02x%02x%02x%02x \ |
|
[expr {$w & 255}] \ |
|
[expr {$w >> 8 & 255}] \ |
|
[expr {$w >> 16 & 255}] \ |
|
[expr {$w >> 24 & 255}]] |
|
} |
|
return $result |
|
} |
|
package provide pgintcl $pgtcl::version
|
|
|