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.
880 lines
27 KiB
880 lines
27 KiB
# $Id: tkpsql.tcl 515 2011-09-17 19:18:53Z lbayuk $ |
|
# tkpsql - Interactive PostgreSQL SQL Interface |
|
# Copyright 2003-2008 by 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. |
|
|
|
# Global variables: |
|
# version - Our version string. |
|
# widgets() - Main widget pathnames : input output status |
|
# n_history - Number of history elements |
|
# history() - History array 1:n_history |
|
# history_p - Index in history where next command will be stored |
|
# history_q - Index in history where next command will be recalled from |
|
# db - Handle to open database, if empty there is no connection. |
|
# dbinfo() - Remembers db conection info: host, user, dbname, port, password |
|
# dbinfo(has_schema) Flag: Database has schemas (PostgreSQL >=7.3) |
|
# form_status - Temporary flag for waiting on a popup |
|
# pwd - Starting directory for file open/save |
|
# option() - Array of options |
|
# " (outstyle) - Output style, "wide" or "narrow" |
|
# " (debug) - Debug flag, 0 for none |
|
# " (maxlook) - Max. result rows to examine for column widths |
|
# " (clear) - Clear output pad before each command results |
|
# special() - SQL for special database queries, index by code. |
|
# special_title() - Titles for special queries, indexed by code. |
|
# special_codes - A list of special*() indexes, ordered as they should |
|
# be displayed in the popup. |
|
|
|
set version 1.2.1 |
|
package require Tk |
|
|
|
# ===== Utility Routines ===== |
|
|
|
# Initialization: |
|
proc initialize {} { |
|
global n_history history history_p history_q |
|
global db pwd option |
|
|
|
array set option { |
|
debug 0 |
|
outstyle wide |
|
maxlook 20 |
|
clear 1 |
|
} |
|
|
|
# Initialize the history list: |
|
set n_history 25 |
|
for {set i 1} {$i <= $n_history} {incr i} { |
|
set history($i) {} |
|
} |
|
set history_p 1 |
|
set history_q 1 |
|
|
|
set db {} |
|
set pwd [pwd] |
|
dbms_load |
|
font create monofont -family Courier |
|
font create boldfont -family Courier -weight bold |
|
} |
|
|
|
# Initialize the array of special database queries. |
|
# This has to be done after connecting to the database, so we know if |
|
# the schema-aware versions should be used. It can be called again as needed. |
|
# special(c) contains the SQL for code 'c'. |
|
# special_title(c) contains the displayed title for code 'c'. |
|
# The index values 'c' are arbitrary codes. |
|
# The list special_codes contains the ordered list of indexes. |
|
# |
|
# I mostly copied the SQL queries from psql. The 'schema-aware' queries are |
|
# based on PostgreSQL-7.3.4; the 'non-schema' versions are from some older |
|
# version. But in some cases, I took advantage of the special views. |
|
# |
|
# Note: The pre-7.3 queries are no longer updated/maintained because I don't |
|
# have pre-7.3 server to test them on. |
|
# |
|
proc init_special {} { |
|
global dbinfo special special_title special_codes |
|
catch {unset special_codes special_title special} |
|
|
|
if {$dbinfo(has_schema)} init_special_new init_special_old |
|
} |
|
|
|
# Initialize special queries for PostgreSQL-7.3 and higher. |
|
# See comments for init_special |
|
proc init_special_new {} { |
|
global special special_title special_codes |
|
|
|
# This is the ordered list of codes whose titles will be displayed. |
|
set special_codes { dbs tables views index rules seqs rights user group } |
|
|
|
set special_title(dbs) "List Databases" |
|
set special(dbs) { |
|
select datname as "Database Name", usename as "Owner" |
|
from pg_database, pg_user |
|
where datdba=usesysid order by datname |
|
} |
|
|
|
set special_title(tables) "List Tables" |
|
set special(tables) { |
|
select schemaname as "Schema", tablename as "Table", tableowner as "Owner" |
|
from pg_catalog.pg_tables |
|
where schemaname not in ('pg_catalog', 'pg_toast', 'information_schema') |
|
order by 1,2 |
|
} |
|
|
|
set special_title(views) "List Views" |
|
set special(views) { |
|
select schemaname as "Schema", viewname as "View", viewowner as "Owner", |
|
definition as "Definition" |
|
from pg_catalog.pg_views |
|
where schemaname not in ('pg_catalog', 'pg_toast', 'information_schema') |
|
order by 1,2 |
|
} |
|
|
|
set special_title(index) "List Indexes" |
|
set special(index) { |
|
select schemaname as "Schema", indexname as "Index-Name", |
|
tablename as "Base-Table", indexdef as "Definition" |
|
from pg_catalog.pg_indexes |
|
where schemaname not in ('pg_catalog', 'pg_toast', 'information_schema') |
|
order by 1,2 |
|
} |
|
|
|
set special_title(rules) "List Rules" |
|
set special(rules) { |
|
select schemaname as "Schema", rulename as "Rule", |
|
definition as "Definition" |
|
from pg_catalog.pg_rules |
|
where schemaname not in ('pg_catalog', 'pg_toast', 'information_schema') |
|
order by 1,2 |
|
} |
|
|
|
# Sequences - no special view, so do it manually. |
|
set special_title(seqs) "List Sequences" |
|
set special(seqs) { |
|
select n.nspname as "Schema", c.relname as "Sequence", |
|
u.usename as "Owner" |
|
from pg_namespace n, pg_class c, pg_user u |
|
where n.oid = c.relnamespace and c.relowner = u.usesysid |
|
and relkind = 'S' |
|
and n.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') |
|
order by 1, 2 |
|
} |
|
|
|
set special_title(rights) "Show Permissions" |
|
set special(rights) { |
|
select n.nspname as "Schema", c.relname as "Relation", |
|
u.usename as "Owner", c.relacl as "Access Control List" |
|
from pg_class c, pg_user u, pg_namespace n |
|
where c.relowner = u.usesysid and c.relnamespace = n.oid |
|
and c.relkind in ('r', 'v', 'S') |
|
and pg_catalog.pg_table_is_visible(c.oid) |
|
and n.nspname not in ('pg_catalog', 'pg_toast', 'information_schema') |
|
order by 1, 2 |
|
} |
|
|
|
set special_title(user) "List Users" |
|
set special(user) { |
|
select usename as "Username", usesysid as "User-ID", |
|
trim (leading ' ' from |
|
trim (trailing ',' from |
|
case when usesuper then ' Superuser,' else '' end |
|
|| case when usecreatedb then ' Create Database,' else '' end |
|
|| case when usecatupd then ' Update Catalogs,' else '' end)) |
|
as "Rights" |
|
from pg_user order by usename |
|
} |
|
|
|
set special_title(group) "List Groups" |
|
set special(group) { |
|
select groname as "Groupname", grosysid as "Group-ID", |
|
grolist as "Member-IDs" |
|
from pg_group order by groname |
|
} |
|
} |
|
|
|
# Initialize special queries for PostgreSQL older than 7.3. |
|
# See comments for init_special. This is UNMAINTAINED. |
|
proc init_special_old {} { |
|
global special special_title special_codes |
|
|
|
# This is the ordered list of codes whose titles will be displayed. |
|
set special_codes { dbs tables index rights user group } |
|
|
|
set special_title(dbs) "List Databases" |
|
set special(dbs) { |
|
select datname as "Database Name", usename as "Owner" |
|
from pg_database, pg_user |
|
where datdba=usesysid order by datname |
|
} |
|
|
|
set special_title(tables) "List Tables" |
|
set special(tables) { |
|
select usename as username, relname as table, relkind as kind |
|
from pg_class, pg_user where relkind = 'r' and relname !~ '^pg_' |
|
and usesysid=relowner order by relname |
|
} |
|
|
|
set special_title(index) "List Indexes/Sequences" |
|
set special(index) { |
|
select usename as username, relname as name, relkind as kind |
|
from pg_class, pg_user where (relkind='i' or relkind='S') and |
|
relname !~ '^pg_' and usesysid=relowner order by relname |
|
} |
|
|
|
set special_title(rights) "Show Table/Sequence Rights" |
|
set special(rights) { |
|
select relname as table, usename as owner, relacl as acl from |
|
pg_class, pg_user where (relkind = 'r' or relkind = 'S') and |
|
relname !~ '^pg_' and usesysid=relowner order by relname |
|
} |
|
|
|
set special_title(user) "List Users" |
|
set special(user) { |
|
select usename as "Username", usesysid as "User-ID", |
|
usecreatedb as "Create-DB?", |
|
usesuper as "Superuser?", |
|
usecatupd as "Update-Catalogs?" |
|
from pg_user order by usename |
|
} |
|
|
|
set special_title(group) "List Groups" |
|
set special(group) { |
|
select groname as "Groupname", grosysid as "Group-ID", |
|
grolist as "Member-IDs" |
|
from pg_group order by groname |
|
} |
|
} |
|
|
|
|
|
# Initialize after connecting to a database |
|
# If an error occurs querying the database, ignore the error and don't |
|
# report it. (Will only report errors from user-issued queries.) |
|
# This also inializes the special queries. |
|
proc init_post_connect {} { |
|
global db dbinfo |
|
|
|
# Determine if the database supports schemas. |
|
set dbinfo(has_schema) 0 |
|
if {![catch {pg_exec $db "select nspname from pg_namespace limit 1"} r]} { |
|
if {[pg_result $r -status] == "PGRES_TUPLES_OK"} { |
|
set dbinfo(has_schema) 1 |
|
} |
|
pg_result $r -clear |
|
} |
|
debug_puts "has_schema: $dbinfo(has_schema)" |
|
init_special |
|
} |
|
|
|
# Pluralization |
|
proc plural {n {s "s"}} { |
|
if {$n == 1} { return ""} else { return $s } |
|
} |
|
|
|
# Assign respective list elements to named variables: |
|
proc setlist {list args} { |
|
foreach val $list var $args { |
|
upvar $var v |
|
set v $val |
|
} |
|
} |
|
|
|
# Output some text if debugging is on: |
|
proc debug_puts {s} { |
|
global option |
|
if {$option(debug)} { |
|
puts "+debug: $s" |
|
} |
|
} |
|
|
|
# Load PostgreSQL support with library or emulator: |
|
proc dbms_load {} { |
|
# If it is already loaded; e.g. running under pgtksh, nothing to do. |
|
if {[info commands pg_connect] != ""} return |
|
# Use my pgin.tcl interface library from the same directory: |
|
set cmd [list source [file join [file dirname [info script]] pgin.tcl]] |
|
if {[catch {uplevel #0 $cmd} msg]} { |
|
error "Error: Unable to load database support. $msg" |
|
} |
|
} |
|
|
|
# ===== GUI / Window Utilities ===== |
|
|
|
# Center a window over another window. |
|
# $win : Window to center |
|
# $over : What to center it over: |
|
# "ROOT" => center over the screen. |
|
# "PARENT" => center over $win's parent window. |
|
# Otherwise $over is the name of a window to center $win over. |
|
# On return, the window will be mapped (de-iconified). |
|
proc center_window {win over} { |
|
wm withdraw $win |
|
update |
|
if {$over == "ROOT"} { |
|
set base_x 0 |
|
set base_y 0 |
|
set base_w [winfo screenwidth $win] |
|
set base_h [winfo screenheight $win] |
|
} else { |
|
if {$over == "PARENT"} { |
|
set overwin [winfo parent $win] |
|
} else { |
|
set overwin $over |
|
} |
|
set base_x [winfo rootx $overwin] |
|
set base_y [winfo rooty $overwin] |
|
set base_w [winfo width $overwin] |
|
set base_h [winfo height $overwin] |
|
} |
|
set win_w [winfo reqwidth $win] |
|
set win_h [winfo reqheight $win] |
|
if {[set win_x [expr {$base_x + int(($base_w - $win_w) / 2)}]] < 0} { |
|
set win_x 0 |
|
} |
|
if {[set win_y [expr {$base_y + int(($base_h - $win_h) / 2)}]] < 0} { |
|
set win_y 0 |
|
} |
|
wm geometry $win +$win_x+$win_y |
|
wm deiconify $win |
|
} |
|
|
|
# Make a top-level window and return its name: |
|
proc mk_window {name title} { |
|
catch {destroy $name} |
|
toplevel $name |
|
wm title $name $title |
|
wm transient $name . |
|
return $name |
|
} |
|
|
|
# Position and wait for grabbed popup window. |
|
# Change with care; MS-Win is very sensitive to the command order. |
|
proc window_wait {win focus_to varname} { |
|
global $varname |
|
set save_focus [focus] |
|
center_window $win PARENT |
|
focus $focus_to |
|
grab set $win |
|
tkwait variable $varname |
|
destroy $win |
|
catch {focus $save_focus} |
|
} |
|
|
|
# Build a button with key binding(s) and command. Returns widget name. |
|
proc mk_button {widget label key command} { |
|
button $widget -text "$label $key" -command $command |
|
bind . $key "$widget invoke" |
|
return $widget |
|
} |
|
|
|
# Make a 'buttons' frame with OK and Cancel buttons. |
|
proc mk_buttons {toplevel {ok_action {set form_status 1}}} { |
|
set f $toplevel.buttons |
|
frame $f |
|
button $f.ok -text OK -default active -command $ok_action |
|
bind $toplevel <Return> "$f.ok invoke" |
|
button $f.cancel -text Cancel -default normal -command {set form_status 0} |
|
bind $toplevel <Escape> "$f.cancel invoke" |
|
pack $f.ok $f.cancel -side left -padx 3 -pady 3 |
|
} |
|
|
|
# ===== UI Support Routines ===== |
|
|
|
# Append a line to the output window: |
|
proc oputs {s {tag ""}} { |
|
global widgets |
|
$widgets(output) insert end "$s\n" $tag |
|
$widgets(output) see end |
|
} |
|
|
|
# Clear the output window: |
|
proc clear_output {} { |
|
global widgets |
|
$widgets(output) delete 1.0 end |
|
} |
|
|
|
# Display some text in the status window: |
|
proc show_status {s} { |
|
global widgets |
|
$widgets(status) configure -text $s |
|
update |
|
} |
|
|
|
# Clear the input window and put the focus there; also clears the status. |
|
# This is used when returning from a command so no update is needed. |
|
proc clear_input {} { |
|
global widgets |
|
$widgets(input) delete 1.0 end |
|
focus $widgets(input) |
|
$widgets(status) configure -text {} |
|
} |
|
|
|
# Utility function used by build_format to update max lengths |
|
proc max_list {max_name list} { |
|
upvar $max_name max |
|
set i 0 |
|
foreach s $list { |
|
set slen [string length $s] |
|
if {$slen > $max($i)} { |
|
set max($i) $slen |
|
} |
|
incr i |
|
} |
|
} |
|
|
|
# Create a format for output of query results. This decides how much space |
|
# should be given to each column, and builds a format for {format} and |
|
# returns it. $qr is the pgtcl query result handle. We look at the column |
|
# headers and up to $option(maxlook) rows to find the longest field values. |
|
# The result is a format string like {%-ns %-ns... %s}. |
|
proc build_format {nrow ncol qr} { |
|
global option |
|
|
|
if {$nrow > $option(maxlook)} { |
|
set nrow $option(maxlook) |
|
} |
|
for {set i 0} {$i < $ncol} {incr i} { |
|
set max($i) 0 |
|
} |
|
max_list max [pg_result $qr -attributes] |
|
for {set i 0} {$i < $nrow} {incr i} { |
|
max_list max [pg_result $qr -getTuple $i] |
|
} |
|
# Don't use the last column's width, just do "%s" for it. |
|
set fmt {} |
|
for {set i 0} {$i < $ncol-1} {incr i} { |
|
append fmt "%-$max($i)s " |
|
} |
|
append fmt "%s" |
|
debug_puts "build_format=$fmt" |
|
return $fmt |
|
} |
|
|
|
# Display query results in "narrow" format (one field per line): |
|
proc show_results_narrow {nrow ncol qr} { |
|
set headers [pg_result $qr -attributes] |
|
for {set r 0} {$r < $nrow} {incr r} { |
|
foreach name $headers value [pg_result $qr -getTuple $r] { |
|
oputs "$name: $value" |
|
} |
|
if {$r % 10 == 0} { |
|
show_status "Reading reply...$r" |
|
} |
|
oputs "" |
|
} |
|
} |
|
|
|
# Display query results in "wide" format (one record per line): |
|
proc show_results_wide {nrow ncol qr} { |
|
# Calculate field widths for output: |
|
set fmt [build_format $nrow $ncol $qr] |
|
|
|
# Output the column headers: |
|
oputs [eval format {$fmt} [pg_result $qr -attributes]] under |
|
|
|
# Output all of the rows: |
|
for {set r 0} {$r < $nrow} {incr r} { |
|
oputs [eval format {$fmt} [pg_result $qr -getTuple $r]] |
|
if {$r % 10 == 0} { |
|
show_status "Reading reply...$r" |
|
} |
|
} |
|
} |
|
|
|
# Send SQL to the backend and display the results. Optional title is |
|
# displayed instead of the actual SQL (used for special queries). |
|
proc run_sql {sql {title ""}} { |
|
global db option |
|
|
|
if {$db == ""} { |
|
tk_messageBox -title tkpsql -icon error -type ok \ |
|
-message "Not connected to database" |
|
return |
|
} |
|
debug_puts "SQL: $sql" |
|
|
|
if {$option(clear)} clear_output |
|
|
|
if {$title != ""} { |
|
oputs $title bold |
|
} else { |
|
oputs $sql bold |
|
} |
|
show_status "Sending query..." |
|
# Run the SQL, catch a backend or connection failure. |
|
if {[catch {pg_exec $db $sql} reply]} { |
|
oputs "ERROR executing SQL:" bold |
|
oputs $reply bold |
|
return |
|
} |
|
set status [pg_result $reply -status] |
|
debug_puts "Query status $status" |
|
show_status "" |
|
if {$status == "PGRES_COMMAND_OK"} { |
|
# Command completed with no tuples (e.g. insert, update, etc.). |
|
# Show the OID, if available. (Not available should be 0, but there was |
|
# some confusion early about this and it might be an empty string.) |
|
set show OK |
|
if {[set oid [pg_result $reply -oid]] != 0 && $oid != ""} { |
|
append show ", OID=$oid" |
|
} |
|
# Show affected tuple count. Not all pgtcl's support this. |
|
if {![catch {pg_result $reply -cmdTuples} n] && $n != ""} { |
|
append show ", $n row[plural $n] affected" |
|
} |
|
oputs $show bold |
|
clear_input |
|
} elseif {$status != "PGRES_TUPLES_OK"} { |
|
# Generally this will be PGRES_FATAL_ERROR, but any other status |
|
# is also considered an error. |
|
set errmsg [pg_result $reply -error] |
|
oputs "ERROR ($status):" bold |
|
oputs $errmsg bold |
|
} else { |
|
# Result was PGRES_TUPLES_OK, so there are tuples to show. |
|
set ncol [pg_result $reply -numAttrs] |
|
set nrow [pg_result $reply -numTuples] |
|
oputs "OK with $nrow row[plural $nrow] and $ncol column[plural $ncol]." bold |
|
oputs "" |
|
show_status "Reading reply..." |
|
show_results_$option(outstyle) $nrow $ncol $reply |
|
clear_input |
|
show_status "" |
|
} |
|
pg_result $reply -clear |
|
oputs "" |
|
} |
|
|
|
# Return the string properly escaped for conninfo quoting: |
|
proc conninfo_quote {s} { |
|
regsub -all {\\} $s {\\\\} s |
|
regsub -all {'} $s {\\'} s |
|
return $s |
|
} |
|
|
|
# Call-back for do_connect on OK. Check the form values and try to connect. |
|
# If it worked, set form_status to 1 to finish window_wait; else raise an |
|
# error and return with the connection dialog still up. |
|
proc do_connect_done {toplevel} { |
|
global form_status dbinfo db |
|
if {$dbinfo(dbname) == "" || $dbinfo(user) == "" || $dbinfo(password) == ""} { |
|
tk_messageBox -title tkpsql -icon error -type ok \ |
|
-parent $toplevel \ |
|
-message "Missing information: must supply dbname, user, password" |
|
return |
|
} |
|
|
|
# Connect to the database: |
|
# Only password can contain spaces, and only strings with spaces must |
|
# be escape-quoted. |
|
set conninfo "dbname=$dbinfo(dbname) user=$dbinfo(user)\ |
|
password='[conninfo_quote $dbinfo(password)]'" |
|
# Host is optional, because blank host means use localhost. |
|
# Apply port only if host is used, although technically it can be used |
|
# without a host over UDS. |
|
if {$dbinfo(host) != ""} { |
|
append conninfo " host=$dbinfo(host) port=$dbinfo(port)" |
|
} |
|
show_status "Connecting to $dbinfo(dbname)@$dbinfo(host)..." |
|
|
|
if {[catch {pg_connect -conninfo $conninfo} result]} { |
|
show_status "" |
|
tk_messageBox -title tkpsql -icon error -type ok \ |
|
-parent $toplevel \ |
|
-message "Failed to connect to database: $result" |
|
return |
|
} |
|
set db $result |
|
show_status "Connected to database $dbinfo(dbname)@$dbinfo(host)" |
|
init_post_connect |
|
set form_status 1 |
|
} |
|
|
|
# Run special queries. See do_special and init_special. |
|
proc run_special {code} { |
|
global form_status special special_title |
|
# Close the special query popup: |
|
set form_status 1 |
|
update |
|
run_sql $special($code) $special_title($code) |
|
} |
|
|
|
# ===== Menu Command Routines ===== |
|
|
|
# Manage the history list. |
|
# If op is + or -, step the history pointer, and replace the input |
|
# window contents with the history value (if not empty). If op is |
|
# something else, enter it into the history table. |
|
# When storing into the history list, synchronize the read and write |
|
# indexes. |
|
proc do_history {op} { |
|
global history history_p history_q n_history |
|
global widgets |
|
if {$op == ""} return |
|
debug_puts "do_history '$op' p=$history_p q=$history_q" |
|
if {$op == "+"} { |
|
set n $history_q |
|
incr n |
|
if {$n > $n_history} { |
|
set n 1 |
|
} |
|
if {$history($n) == ""} return |
|
set history_q $n |
|
clear_input |
|
$widgets(input) insert 1.0 $history($history_q) |
|
} elseif {$op == "-"} { |
|
set n $history_q |
|
incr n -1 |
|
if {$n < 1} { |
|
set n $n_history |
|
} |
|
if {$history($n) == ""} return |
|
set history_q $n |
|
clear_input |
|
$widgets(input) insert 1.0 $history($history_q) |
|
} else { |
|
# Delete trailing newlines to keep it neat. |
|
set history($history_p) [string trimright $op] |
|
incr history_p |
|
if {$history_p > $n_history} { |
|
set history_p 1 |
|
} |
|
set history_q $history_p |
|
} |
|
} |
|
|
|
# Connect to database: |
|
proc do_connect {} { |
|
global db dbinfo form_status |
|
|
|
if {$db != ""} do_disconnect |
|
|
|
# Initialize if never done. pg_conndefaults returns list of {key - - - value} |
|
if {![info exists dbinfo(user)]} { |
|
array set dbinfo {user {} host {} dbname {} port {} password {}} |
|
foreach default [pg_conndefaults] { |
|
setlist $default key unused1 unused2 unused3 value |
|
if {[info exists dbinfo($key)]} { |
|
set dbinfo($key) $value |
|
} |
|
} |
|
} |
|
# Build the Connect to Database popup: |
|
set t [mk_window .dbconnect "Connect to DBMS"] |
|
set f $t.entry |
|
frame $f |
|
label $f.host_l -text "Database Host:" |
|
entry $f.host -width 24 -textvariable dbinfo(host) |
|
label $f.port_l -text "Database Port:" |
|
entry $f.port -width 12 -textvariable dbinfo(port) |
|
label $f.dbname_l -text "Database Name:" |
|
entry $f.dbname -width 16 -textvariable dbinfo(dbname) |
|
label $f.user_l -text "Username:" |
|
entry $f.user -width 12 -textvariable dbinfo(user) |
|
label $f.password_l -text "Password:" |
|
entry $f.password -width 24 -textvariable dbinfo(password) -show * |
|
foreach field {host port dbname user password} { |
|
grid $f.${field}_l $f.$field |
|
grid configure $f.${field}_l -sticky e |
|
grid configure $f.${field} -sticky w |
|
} |
|
mk_buttons $t "do_connect_done $t" |
|
pack $t.entry $t.buttons -side top -fill x |
|
set form_status -1 |
|
window_wait $t $t.entry.host form_status |
|
# At this point $form_status is 1 on OK, 0 on Cancel, but we really |
|
# don't care because do_connect_done did all the work on OK. |
|
} |
|
|
|
# Disconnect from the database: |
|
proc do_disconnect {} { |
|
global db dbinfo |
|
if {$db == ""} return |
|
pg_disconnect $db |
|
show_status "Disconnected from database $dbinfo(dbname)@$dbinfo(host)" |
|
set db {} |
|
} |
|
|
|
# Load a file into the input window: |
|
proc do_loadin {} { |
|
global widgets pwd |
|
|
|
set fname [tk_getOpenFile -initialdir $pwd -title "Load input window"] |
|
if {$fname == ""} return |
|
set pwd [file dirname $fname] |
|
if {[catch {open $fname} f]} { |
|
tk_messageBox -title tkpsql -icon error -type ok \ |
|
-message "Failed to open $fname: $f" |
|
return |
|
} |
|
clear_input |
|
$widgets(input) insert end [read -nonewline $f] |
|
close $f |
|
} |
|
|
|
# Save Input or Output text areas to a file. |
|
proc do_save {which} { |
|
global widgets pwd |
|
|
|
set fname [tk_getSaveFile -initialdir $pwd -title "Save $which window"] |
|
if {$fname == ""} return |
|
set pwd [file dirname $fname] |
|
if {[catch {open $fname w} f]} { |
|
tk_messageBox -title tkpsql -icon error -type ok \ |
|
-message "Failed to open $fname: $f" |
|
return |
|
} |
|
show_status "Saving text..." |
|
puts -nonewline $f [$widgets($which) get 1.0 end] |
|
close $f |
|
show_status "" |
|
} |
|
|
|
# Exit the program: |
|
proc do_exit {} { |
|
do_disconnect |
|
exit |
|
} |
|
|
|
# Run the SQL in the input window. First, remove any trailing newlines, |
|
# spaces and ';' chars. |
|
proc do_run {} { |
|
global widgets |
|
set sql [string trimright [$widgets(input) get 1.0 end] " \n;"] |
|
do_history $sql |
|
run_sql $sql |
|
} |
|
|
|
# Clear the input and output boxes: |
|
proc do_clear {} { |
|
clear_input |
|
clear_output |
|
} |
|
|
|
# Display options dialog: |
|
proc do_options {} { |
|
global form_status option |
|
# Save the current options to be restored if the form is Cancelled. |
|
array set copy_option [array get option] |
|
# Build the Options popup: |
|
set t [mk_window .options "Set Options"] |
|
set f $t.opt |
|
frame $f |
|
label $f.outstyle -text "Output Style:" |
|
radiobutton $f.outstyle1 -text Narrow -variable option(outstyle) -value narrow |
|
radiobutton $f.outstyle2 -text Wide -variable option(outstyle) -value wide |
|
label $f.maxlook_l -text "Max rows to look at for column widths:" |
|
entry $f.maxlook -width 5 -textvariable option(maxlook) |
|
checkbutton $f.clear -text "Clear output before results" -variable option(clear) |
|
checkbutton $f.debug -text Debug -variable option(debug) |
|
|
|
grid $f.outstyle $f.outstyle1 $f.outstyle2 |
|
grid $f.maxlook_l - $f.maxlook |
|
grid $f.clear - x |
|
grid $f.debug x x |
|
mk_buttons $t |
|
|
|
pack $t.opt $t.buttons -side top -fill x |
|
set form_status -1 |
|
window_wait $t $t.buttons.ok form_status |
|
|
|
# Restore the options on Cancel: |
|
if {!$form_status} { |
|
array set option [array get copy_option] |
|
} |
|
if {$option(debug)} { |
|
parray option |
|
} |
|
} |
|
|
|
# Special queries. See init_special for the data which drives this. |
|
proc do_special {} { |
|
global form_status special special_title special_codes |
|
|
|
set t [mk_window .special "Special Queries"] |
|
set packme {} |
|
# Generate all the special query buttons: |
|
foreach code $special_codes { |
|
button $t.$code -text $special_title($code) -command "run_special $code" |
|
lappend packme $t.$code |
|
} |
|
button $t.cancel -text Cancel -command "set form_status 0" |
|
bind $t <Escape> "set form_status 0" |
|
eval pack $packme $t.cancel -side top -fill x -padx 2 -pady 2 |
|
set form_status -1 |
|
window_wait $t $t.cancel form_status |
|
} |
|
|
|
# ===== Main Window UI ===== |
|
|
|
# Build the main user interface: |
|
proc build_ui {} { |
|
global widgets version |
|
|
|
set f .buttons |
|
frame $f |
|
set buttons [list \ |
|
[mk_button $f.run Run <F1> do_run] \ |
|
[mk_button $f.clear Clear <F2> do_clear] \ |
|
[mk_button $f.next_hist {History Next} <F3> {do_history +}] \ |
|
[mk_button $f.prev_hist {History Prev} <F4> {do_history -}] \ |
|
[mk_button $f.loadin {Load Input} <F5> do_loadin] \ |
|
[mk_button $f.savein {Save Input} <F6> {do_save input}] \ |
|
[mk_button $f.saveout {Save Output} <F7> {do_save output}] \ |
|
[mk_button $f.connect Connect <F8> do_connect] \ |
|
[mk_button $f.disconn Disconnect <F9> do_disconnect] \ |
|
[mk_button $f.options Options <F10> do_options] \ |
|
[mk_button $f.special Special <F11> do_special] \ |
|
[mk_button $f.quit Exit <F12> do_exit] \ |
|
] |
|
eval pack $buttons -side top -fill x -padx 2 -pady 4 |
|
|
|
# Alternate bindings for keyboard without F11 or F12: |
|
bind . <Alt-s> do_special |
|
bind . <Alt-q> do_exit |
|
# Forget bogus binding of F10 on unix platforms to traverse menus: |
|
bind all <F10> {} |
|
|
|
# Frame .r holds the right-hand side with input, output, and status. |
|
set f .r |
|
frame $f |
|
|
|
# Output text area with horizontal and vertical scrollers: |
|
# Must use monospace font so columns line up. |
|
set widgets(output) $f.output |
|
text $widgets(output) -relief sunken -borderwidth 2 -height 16 -width 64 \ |
|
-wrap none -setgrid 1 -font monofont \ |
|
-yscrollcommand "$f.oyscroll set" -xscrollcommand "$f.oxscroll set" |
|
scrollbar $f.oyscroll -command "$f.output yview" |
|
scrollbar $f.oxscroll -orient horizontal -command "$f.output xview" |
|
# Tags for output area for special text display: |
|
$widgets(output) tag configure under -underline on |
|
$widgets(output) tag configure bold -font boldfont |
|
|
|
# Input text area: vertical scroller only, word wrap. |
|
set widgets(input) $f.input |
|
text $widgets(input) -relief sunken -borderwidth 2 -height 5 -width 64 \ |
|
-wrap word \ |
|
-yscrollcommand "$f.iyscroll set" |
|
scrollbar $f.iyscroll -command "$f.input yview" |
|
|
|
# Status area: |
|
set widgets(status) $f.status |
|
label $widgets(status) -relief sunken -borderwidth 1 -anchor w |
|
|
|
# Grid up the output, input, and status with scroll bars: |
|
grid $f.output $f.oyscroll |
|
grid $f.oxscroll x |
|
grid $f.input $f.iyscroll |
|
grid $f.status - |
|
# ... Set stickiness: |
|
grid configure $f.input $f.output -sticky nsew |
|
grid configure $f.oxscroll $f.status -sticky ew |
|
grid configure $f.oyscroll $f.iyscroll -sticky ns |
|
# ... Indicate that the output and input rows expand: |
|
grid rowconfigure $f 0 -weight 3 |
|
grid rowconfigure $f 2 -weight 1 |
|
grid columnconfigure $f 0 -weight 1 |
|
|
|
pack .buttons .r -side left -fill both |
|
pack configure .r -expand 1 |
|
|
|
# Main window setup: |
|
wm title . "tkpsql $version" |
|
wm iconname . tkpsql |
|
wm protocol . WM_DELETE_WINDOW do_exit |
|
center_window . ROOT |
|
|
|
focus $widgets(input) |
|
# Needed on Windows, for some strange reason: |
|
raise . |
|
} |
|
|
|
# ===== Main program ===== |
|
|
|
initialize |
|
build_ui |
|
do_connect
|
|
|