Updated 2012-07-09 10:43:47 by RLE


By Alex Caldwell.

This is a designer/viewer and editor for SQLite3 databases. It uses the Tablelist widget as the viewer/editor. It allows both viewing and editing data in SQLite tables. It allows viewing, but not editing SQLite views, since views are not editable in SQLite. Changes to the data in the cells in a table are updated in the database automatically. You can enter simple SQL queries and get the results back in a Tk text widget, a Tablelist or both side by side. It was part of a medical project for setting up a database of contacts with a mail merge set up. But I thought it would be potentially useful as a generalizable tool for working with SQLite databases from Tcl. I'm still working on it, so there are a lot of random comments, puts statements and commented out junk that didn't work in the code I posted. Sorry about that. In addition to the TclSqlite3 [1] [2] extension and the Tablelist extension, it uses an Iwidgets combobox and an an Iwidgets paned window, so it also requires the Iwidgets extension of Incr Tcl. Thanks to the combobox's built in methods, it can store the SQL queries you type in, both during and between sessions, so you can select them from a drop down list. If you have a table named "SpokanePhysicians" that has fields named FirstName, LastName and Address, you can try the simple "mail merge" demo feature by selecting multiple rows and clicking on the "Fax" button. Or, edit the code to use a different table name for your mail merging. It's only a demo of how the idea would work, and does not actually link to any email or fax merging program.

CAUTION!!! NOT FOR PRODUCTION USE!!!

See discussion below regarding various possible problems. It works for our simple database, but may cause problems with other more complex schema we have not considered. Do NOT attempt to open an important database and try to edit data without backup copies of your data, or it might irreversibly DAMAGE YOUR DATABASE! It is a preliminary, and I felt potentially useful tool, but has not been thoroughly tested, i.e. I just barely got it working at all yesterday! I was hoping to get expert Tcler help for improving it and fixing problems. But I was so excited that it was working, I couldn't resist posting it on the Wiki. It has a lot of redundant code that needs to be broken out and put in procs for reuse and efficiency. Also, why do I always seem to be needing to use all those commands built up with a bunch of backquotes? I know there must be a better way to do that. Also why can't I summarize a question or answer about Tcl in just a few cryptic lines like others on the Tcler's Wiki? Why do I need these run-on sentences? Is that why my code is so bloated and hard to understand? Why? Why? Why?

I have a "network" version of this program that connects to a Tcl server using the Tcl socket library, so you can connect to your SQLite database remotely. The Tablelist widget is a natural fit with the network version, since it uses the Tcl list data structure, which is easy to send over a socket. The Tktable widget uses the Tcl array data structure, which has to be "serialized" before it can be sent over the socket and then reassembled into arrays on the other end. My network version has no encryption or authentication, so it is not secure. But if you want that version, send me an email. It requires another program that functions as the server that I did not post here.
 # by Alex Caldwell M.D
 # [email protected]
 # with much help from
 # Dr. Jerry Park D.O.
 # [email protected]

 package require Tk
 package require Tablelist
 package require Iwidgets
 package require sqlite3

 #create some bitmaps for the fax and mail merge buttons
 #bitmaps were borrowed from addressbook-0.7 a Tcl/Tk program by Klemens Durka

 image create bitmap fax -data {
    #define fax_width 31
    #define fax_height 21
    static unsigned char fax_bits[] = {
        0xf0, 0xff, 0xff, 0x1f, 0x18, 0x00, 0x00, 0x10, 0x08, 0x00, 0x00, 0x18,
        0xe8, 0x39, 0x21, 0x0b, 0x28, 0x44, 0x12, 0x0b, 0x28, 0x44, 0x0c, 0x0b,
        0xe8, 0x7c, 0x0c, 0x0b, 0x2e, 0x44, 0x12, 0x38, 0x2a, 0x44, 0x21, 0x2b,
        0x0a, 0x00, 0x00, 0x28, 0x0a, 0x00, 0x00, 0x28, 0xfa, 0xff, 0xff, 0x2f,
        0x02, 0x00, 0x00, 0x20, 0xfa, 0xff, 0xff, 0x27, 0x0a, 0x30, 0xf2, 0x24,
        0xfa, 0xff, 0xff, 0x2f, 0x02, 0x30, 0x92, 0x2c, 0x02, 0xf0, 0xff, 0x2f,
        0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20, 0xfe, 0xff, 0xff, 0x3f};
 }
 image create bitmap mail -data {
    #define brief_width 31
    #define brief_height 21
    static unsigned char brief_bits[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0xff, 0x3f,
        0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20, 0x3a, 0x00, 0x00, 0x27,
        0x02, 0x00, 0x00, 0x25, 0xba, 0x01, 0x00, 0x25, 0x02, 0x00, 0x00, 0x27,
        0x02, 0x00, 0x00, 0x20, 0x02, 0xfc, 0x07, 0x20, 0x02, 0x00, 0x00, 0x20,
        0x02, 0x7c, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20, 0x02, 0xfc, 0x79, 0x20,
        0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20,
        0xfe, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
    
 }
 image create bitmap email -data {
    #define email_width 31
    #define email_height 21
    static unsigned char email_bits[] = {
        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xfe, 0xff, 0xff, 0x3f,
        0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x24,
        0x02, 0x00, 0x40, 0x24, 0x02, 0x00, 0x00, 0x24, 0x72, 0x2a, 0x43, 0x24,
        0x8a, 0xbe, 0x44, 0x24, 0x7a, 0xaa, 0x44, 0x24, 0x0a, 0xaa, 0x44, 0x24,
        0x72, 0x2a, 0xeb, 0x2e, 0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20,
        0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x20,
        0xfe, 0xff, 0xff, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
 }


 set types {
    {{SQLite} {.db}}
    {{SQLiteExplorer} {.db3}}
    {{All Files} {*.*}}
 }

 sqlite3 db  [set database_name [tk_getOpenFile -initialdir "./" -title \
        "Choose Sqlite Database File" -filetypes $types]]

 wm title . "[file tail $database_name] - Tables"


 #Eval_Remote $sock {sqlite3 db ./medrolodex.db}

 # get the names of all the tables
 set table_names [db eval {SELECT name FROM sqlite_master WHERE type='table' UNION ALL SELECT name  FROM sqlite_temp_master WHERE type='table' ORDER BY name;}]
 # check the no. of tables - used later to configure views in a green foreground color
 set no_tables [llength $table_names]
 append table_names " [db eval {SELECT name FROM sqlite_master WHERE type='view' UNION ALL SELECT  name FROM sqlite_temp_master WHERE type='view' ORDER BY name;}]"
 # create a button for each table which when clicked will create a tablelist widget and populate it  with data from the table
 #foreach table $table_names {
 #    button ._$table -text $table -command "createtablelist $table"
 #    pack ._$table -side left
 #}

 # try a listbox instead to see what works best
 frame .topframe
 pack .topframe -expand true -fill both
 frame .topframe.leftframe
 frame .topframe.rightframe
 pack .topframe.leftframe  -side left -expand true -fill both
 pack .topframe.rightframe -side right -expand true -fill y
 listbox .topframe.leftframe.list -width 115 -yscrollcommand {.topframe.rightframe.scroll set}
 pack .topframe.leftframe.list -expand true -fill both
 scrollbar .topframe.rightframe.scroll -command {.topframe.leftframe.list yview}
 pack .topframe.rightframe.scroll -expand true -fill y

 foreach table $table_names {
    .topframe.leftframe.list insert end $table
 }

 # configure the foreground color of the views in green to distinguish from tables in black
 for {set x $no_tables} {$x < [.topframe.leftframe.list index end]} {incr x} {
    .topframe.leftframe.list itemconfigure  $x  -foreground green
 }

 bind .topframe.leftframe.list <Double-Button-1> {
    #createtablelist [selection get]
    foreach i [selection get] {createtablelist $i}
 }
 set report_type text
 frame .bottomframe
 pack .bottomframe -side top -expand true -fill x
 button .bottomframe.button1 -text "New Table" -command newTable
 pack .bottomframe.button1 -side left -padx 2 -pady 4
 button .bottomframe.button2 -text "Delete Selected Table" -command {
    dropTable [selection get]
 }
 pack .bottomframe.button2 -side left -padx 2 -pady 4
 frame .sqlframe
 pack .sqlframe -side top -expand true -fill x
 #label .sqlframe.label -text "SQL Query"
 #pack .sqlframe.label -side left
 #entry .sqlframe.entry -width 80 -textvariable sqlquery


 iwidgets::combobox .sqlframe.entry -width 75 -editable true -unique true -labeltext "SQL Query"  -labelpos w -textvariable sqlquery -selectioncommand {
    #puts "selected: [.sqlframe.entry getcurselection]"
    set sqlquery [.sqlframe.entry getcurselection]
    #.sqlframe.entry insert list end $sqlquery
    set f [open ${database_name}_queries.tcl w]
    for {set x 0} {$x < [.sqlframe.entry index end]} {incr x} {
        puts $f [.sqlframe.entry get $x]
    }
    close $f
 }

 lappend query_list ""
 if {[file isfile ${database_name}_queries.tcl]} {
    set f [open ${database_name}_queries.tcl r]
    while {![eof $f]} {
        gets $f line
        if {$line != {}} {
            lappend query_list "$line"
        }
    }
    close $f
 }
 #.sqlframe.entry insert list end {SELECT * FROM SpokanePhysicians;}
 foreach query $query_list {
    .sqlframe.entry insert list end $query
 }


 #.sqlframe.entry selection set {}

 pack .sqlframe.entry -side left -expand true -fill both

 button .sqlframe.go_button -text "Go" -command {
    catch {
        destroy .result_text
        destroy .result_scroll
        destroy .result_scroll2
        destroy .query_results
        destroy .hsb
        destroy .vsb
        destroy .pw
    }
    iwidgets::panedwindow .pw -width 6i -height 2.5i -orient vertical
    pack .pw -expand true -fill both
    
    .pw add "left" -margin 2
    
    .pw add "right" -margin 2
    
    set left [.pw childsite "left"]
    set right [.pw childsite "right"]
    
    
    set no_columns 1
    set column_names ""
    
    set result [db eval "$sqlquery"]
    
    regexp {SELECT.+FROM} $sqlquery no_columns
    
    if {![regexp {\*} $no_columns]} {
        regsub -all {SELECT } $no_columns {} no_columns
        regsub -all { FROM} $no_columns {} no_columns
        #set no_columns [split $no_columns ", "]
        regsub -all {, } $no_columns { } no_columns
        set column_names [split $no_columns " "]
        set no_columns [llength [split $no_columns " "]]
        #puts $result
        if {$report_type == "text" || $report_type == "both"} {
            if {$report_type == "text"} {
                .pw fraction 100 0
            } else {
                .pw fraction 50 50
            }
            scrollbar .result_scroll2 -orient horizontal -command {.result_text xview}
            pack .result_scroll2 -in $left -expand true -fill x
            scrollbar .result_scroll -command {.result_text yview}
            pack .result_scroll -in $left -side left -anchor w -padx 0 -expand true -fill y
            
            
            text .result_text -width 125 -yscrollcommand {.result_scroll set} -wrap none
            pack .result_text -in $left -side left -anchor w -padx 0 -expand true -fill both
            
        }
        set initial_result_length [llength $result]
        
        for {set x $no_columns} {$x <= [llength $result]} {incr x [expr $no_columns + 1]} {
            set result [linsert $result $x  \n]
        }
        
        #for {set x 1} {$x <= [llength $result]} {incr x 1} {
        #    if {[expr $x % ($no_columns + 1)] eq 0} {
        #    set result [linsert $result $x  \n]
        #    } else {
        #    set result [linsert $result $x "\t"]
        #    }
        #}
        
        regsub -all {\{\n\}} $result "\n" result
        if {$report_type == "text" || $report_type == "both"} {
            .result_text insert end "$column_names \n"
            .result_text insert end $result
        }
        set new_column_names [list "0 [join $column_names "\n0 "]"]
        regsub -all {\{} $new_column_names {} new_column_names
        regsub -all {\}} $new_column_names {} new_column_names
        if {$report_type == "tablelist" || $report_type == "both"} {
            if {$report_type == "tablelist"} {
                .pw fraction 0 100
            } else {
                .pw fraction 50 50
            }
            
            tablelist::tablelist .query_results -columns $new_column_names \
                    -labelcommand tablelist::sortByColumn -sortcommand demo::compareAsSet \
                    -editendcommand applyValue -height 15 -width 120 -stretch all \
                    -xscrollcommand [list .hsb set] -yscrollcommand [list .vsb set] \
                    -stripebackground           #e0e8f0
            for {set x 0} {$x < [llength $column_names]} {incr x} {
                .query_results columnconfigure $x -maxwidth 30 -editable no
            }
            scrollbar .vsb -orient vertical   -command [list .query_results yview]
            scrollbar .hsb -orient horizontal -command [list .query_results xview]
            #grid .query_results -row 0 -column 0 -sticky news
            #grid .vsb -row 0 -column 1 -sticky ns
            #grid .hsb -row 1 -column 0 -sticky ew
            
            #grid rowconfigure    $tf 0 -weight 1
            #grid columnconfigure $tf 0 -weight 1
            pack .hsb -in $right -expand true -fill x
            pack .vsb -in $right -side left -fill y
            pack .query_results -in $right -side left
            foreach line [split $result "\n"] {
                regsub -all {'} $line {\\u0027} line
                regsub -all {"} $line {\\u0022} line
                #.query_results insert end [string map {' \'} $line]
                .query_results insert end $line
            }
        }
        
    } else {
        #toplevel .message
        #label .message.label -text "Sorry, cannot process the wildcard yet"
        #pack .message.label
        #label .message.label2 -text "column names"
        #pack .message.label2
        if {$report_type == "text" || $report_type == "both"} {
            if {$report_type == "text"} {
                .pw fraction 100 0
            } else {
                .pw fraction 50 50
            }
            
            scrollbar .result_scroll2 -orient horizontal -command {.result_text xview}
            pack .result_scroll2 -in $left -expand true -fill x
            
            scrollbar .result_scroll -command {.result_text yview}
            pack .result_scroll -in $left -side left -anchor w -padx 0 -expand true -fill y
            
            
            text .result_text -width 125 -xscrollcommand {.result_scroll2 set} -yscrollcommand {.result_scroll set} -wrap none
            pack .result_text -in $left -side left -anchor w -padx 0 -expand true -fill both
            
        }
        
        #need to get the table_name in order to find the column names when using a wildcard
        if {[regexp "WHERE" $sqlquery]} {
            regexp {FROM .+ WHERE} $sqlquery table_name
            regsub {FROM } $table_name {} table_name
            regsub { WHERE} $table_name {} table_name
        } else {
            regexp {FROM [^ ;]+[ ;]} $sqlquery table_name
            regsub {FROM } $table_name {} table_name
            set table_name [string trimright $table_name]
            set table_name [string trimright $table_name ";"]
        }
        #.message.label configure -text "$table_name"
        # need to get the names of all the columns in the selected table using SQL command on the  sqlite_master table
        set initial_column_names [db eval [subst {SELECT sql FROM (SELECT * FROM sqlite_master UNION  ALL SELECT * FROM sqlite_temp_master) WHERE tbl_name LIKE '$table_name' AND type!='meta' ORDER BY  type DESC, name;}]]
        puts "initial_column_names ==\n$initial_column_names"
        
        if {[regexp "CREATE TABLE" $initial_column_names]} {
            # get rid of some junk in the reply that we don't want
            regsub "CREATE TABLE $table_name" $initial_column_names {} initial_column_names
            #regsub   {((version\)|\(name),|(KE|PRIMAR)Y)|(version\)|\(name,|(KE|PRIMAR)Y)|(\(signature\)|UNIQUE)|(\(signature\),|UNIQUE)}  $initial_column_names {} initial_column_names
            regsub {PRIMARY KEY \((.+, .+)+\),} $initial_column_names {} initial_column_names
            regsub {UNIQUE \(.+\)} $initial_column_names {} initial_column_names
            regsub {PRIMARY KEY \((.+, .+)+\)} $initial_column_names {} initial_column_names
            puts "initial_column_names ==\n$initial_column_names"
            
            regsub -all {\(} $initial_column_names {} initial_column_names
            regsub -all {\)} $initial_column_names {} initial_column_names
            regsub -all {\{} $initial_column_names {} initial_column_names
            regsub -all {\}} $initial_column_names {} initial_column_names
            puts "initial_column_names ==\n$initial_column_names"
            
            # the reply still contains the column name followed by a comma and the type description
            # so we need to make a new list with only the first element - the name without the type  description
            set key_index_counter 0
            foreach name [split $initial_column_names ","] {
                if {[regexp "PRIMARY KEY" $name]} {
                    set primary_key $key_index_counter
                    set primary_key_name [lindex $name 0]
                }
                if {[lindex $name 0] != "" && [lindex $name 0] != "CREATE"} {
                    lappend column_names [lindex $name 0]
                }
                incr key_index_counter
            }
        }
        
        if {[regexp "CREATE VIEW" $initial_column_names] && ![regexp "\\*" $initial_column_names]} {
            # we need to get the names of the columns you want from between the SELECT and the FROM statements
            regexp "SELECT .+ FROM" $initial_column_names match
            puts "match == $match"
            regsub "SELECT " $match {} match
            regsub " FROM" $match {} match
            regsub -all {, } $match { } match
            puts "match == $match"
            
            # in this case, the initial_column_names is actually the table names - I know that is  confusing - just too lazy to change the code
            regsub "CREATE VIEW $table_name AS SELECT .+ FROM " $initial_column_names {} initial_column_names
            regsub { WHERE.+$} $initial_column_names {} initial_column_names
            set initial_column_names [split $initial_column_names ", "]
            regsub -all {\(} $initial_column_names {} initial_column_names
            regsub -all {\)} $initial_column_names {} initial_column_names
            regsub -all {\{} $initial_column_names {} initial_column_names
            regsub -all {\}} $initial_column_names {} initial_column_names
            regsub -all {\\} $initial_column_names {} initial_column_names
            
            set column_names $match
            
            
            
            
            
        }
        
        # this is the case where you use a wildcard for selecting the columnames when creating a view. So you will get all the column names in the tablelist widget.
        if {[regexp "CREATE VIEW" $initial_column_names] && [regexp "\\*" $initial_column_names]} {
            regsub "CREATE VIEW $table_name AS SELECT \\* FROM " $initial_column_names {} initial_column_names
            regsub { WHERE.+$} $initial_column_names {} initial_column_names
            set initial_column_names [split $initial_column_names ", "]
            regsub -all {\(} $initial_column_names {} initial_column_names
            regsub -all {\)} $initial_column_names {} initial_column_names
            regsub -all {\{} $initial_column_names {} initial_column_names
            regsub -all {\}} $initial_column_names {} initial_column_names
            regsub -all {\\} $initial_column_names {} initial_column_names
            puts $initial_column_names
            
            foreach view_table $initial_column_names {
                set initial_column_names2 [db eval [subst {SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE tbl_name LIKE '$view_table' AND type!='meta' ORDER BY type DESC, name;}]]
                regsub "CREATE TABLE $view_table" $initial_column_names2 {} initial_column_names2
                regsub -all {\(} $initial_column_names2 {} initial_column_names2
                regsub -all {\)} $initial_column_names2 {} initial_column_names2
                regsub -all {\{} $initial_column_names2 {} initial_column_names2
                regsub -all {\}} $initial_column_names2 {} initial_column_names2
                
                # the reply still contains the column name followed by a comma and the type description
                # so we need to make a new list with only the first element - the name without the type description
                set key_index_counter 0
                foreach name [split $initial_column_names2 ","] {
                    if {[regexp "PRIMARY KEY" $name]} {
                        set primary_key $key_index_counter
                        set primary_key_name [lindex $name 0]
                    }
                    
                    
                    lappend column_names [lindex $name 0]
                    
                    incr key_index_counter
                }
                
                
                
            }
            
            
        }
        
        
        
        set no_columns [llength [split $column_names " "]]
        for {set x $no_columns} {$x <= [llength $result]} {incr x [expr $no_columns + 1]} {
            set result [linsert $result $x  \n]
        }
        
        
        
        #.message.label configure -text "$initial_column_names"
        #.message.label2 configure -text "$column_names"
        
        if {$report_type == "text" || $report_type == "both"} {
            .result_text insert end "$column_names \n"
        }
        regsub -all {\{\n\}} $result "\n" result
        if {$report_type == "text" || $report_type == "both"} {
            .result_text insert end $result
        }
        set new_column_names [list "0 [join $column_names "\n0 "]"]
        regsub -all {\{} $new_column_names {} new_column_names
        regsub -all {\}} $new_column_names {} new_column_names
        if {$report_type == "tablelist" || $report_type == "both"} {
            if {$report_type == "tablelist"} {
                .pw fraction 0 100
            } else {
                .pw fraction 50 50
            }
            
            tablelist::tablelist .query_results -columns $new_column_names \
                    -labelcommand tablelist::sortByColumn -sortcommand demo::compareAsSet \
                    -editendcommand applyValue -height 15 -width 120 -stretch all \
                    -xscrollcommand [list .hsb set] -yscrollcommand [list .vsb set] \
                    -stripebackground           #e0e8f0
            for {set x 0} {$x < [llength $column_names]} {incr x} {
                .query_results columnconfigure $x -maxwidth 30 -editable no
            }
            scrollbar .vsb -orient vertical   -command [list .query_results yview]
            scrollbar .hsb -orient horizontal -command [list .query_results xview]
            #grid .query_results -row 0 -column 0 -sticky news
            #grid .vsb -row 0 -column 1 -sticky ns
            #grid .hsb -row 1 -column 0 -sticky ew
            
            #grid rowconfigure    $tf 0 -weight 1
            #grid columnconfigure $tf 0 -weight 1
            pack .hsb -in $right -expand true -fill x
            pack .vsb -in $right -side left -expand true -fill y
            pack .query_results -in $right -side left
            foreach line [split $result "\n"] {
                #regsub -all {\'} $line {\\\'} line
                .query_results insert end [string map {' \'} $line]
            }
        }
    }
 }
 pack .sqlframe.go_button -side left

 menubutton .sqlframe.report_type -relief raised -indicatoron true -text "Result Format" -menu  .sqlframe.report_type.menu
 pack .sqlframe.report_type -side left

 menu .sqlframe.report_type.menu
 .sqlframe.report_type.menu add radiobutton -label "Text        " -variable report_type -value "text"  -command {.sqlframe.report_type configure -text "Text         "}
 .sqlframe.report_type.menu add radiobutton -label "Tablelist   " -variable report_type -value  "tablelist" -command {.sqlframe.report_type configure -text "Tablelist    "}
 .sqlframe.report_type.menu add radiobutton -label "Both        " -variable report_type -value "both"  -command {.sqlframe.report_type configure -text "Both        "}

 label .sqlframe.column_label -text "No. Columns"
 pack .sqlframe.column_label -side left
 #set no_columns 1
 entry .sqlframe.columns -width 3 -textvariable no_columns
 pack .sqlframe.columns -side left -expand true -fill both

 focus -force .sqlframe.entry

 proc applyValue {tbl row col text} {
    global  primary_key primary_key_name
    #This proc gets called whenever you edit a value in a cell that is editable
    #The purpose is to then update the database with the change you made automatically
    
    # get the name of this table for the UPDATE sql command by introspection by querying the wm for  the top level title.
    # This way you can have multiple tablists open on different tables and each will know what table  it updates
    # without having to set any global variables.
    set table_name [wm title [winfo parent [winfo parent $tbl]]]
    puts $table_name
    
    # this inserts the change into the table cell from the entry box after user hits return key or  moves to another cell
    # it is set up to trim or the spaces on the right unless the data in the box is only a space, which is the default value.
    # a box has to have something in it in order to match the tablelist rows to rows in the SQLite table. This is because
    # if you have nothing in a box, the tablelist widget will output a list with that member removed from the list and then the
    # values going into the SQLIte table when it is updated do not match the right entry in the tablelist with the right column
    # in the SQLite table.
    if {![string compare $text " "]} {
        $tbl cellconfigure $row,$col -text [string trimleft [string trimright $text]]
    } else {
        $tbl cellconfigure $row,$col -text $text
    }
    # get the name of column by querying the tablelist widget for the title on the button at the top of the column
    # this will be passed to the SQL command for updating the database down below
    set columnname [$tbl columncget $col -title]
    
    #puts "columnname == $columnname"
    #set values "'"
    set changes [$tbl get $row]
    #puts "changes == $changes"
    
    set key [lindex $changes $primary_key]
    #puts "key == $key"
    #puts "primary_key_name == $primary_key_name"
    set changes2 [lindex $changes $col]
    regsub -all {'} $changes2 {\\u0027} changes2
    regsub -all {"} $changes2 {\\u0022} changes2
    #puts "changes2 == $changes2"
    
    
    #set changes [join $changes "','"]
    #regsub -all {\{} $changes {'} changes
    #regsub -all {\}} $changes {'} changes
    #regsub -all {@} $changes At changes
    
    #append values $changes
    #append values "'"
    
    # db eval [subst {INSERT INTO Names VALUES($values);}]
    db eval [subst {UPDATE $table_name SET $columnname = '$changes2' WHERE $primary_key_name =  '$key';}]
    return [string trimleft [string trimright $text]]
 }

 proc newRecord {tbl table_name} {
    global sock primary_key primary_key_name column_names
    set lastrow [expr [$tbl index end] -1]
    
    # this gets the highest value of the primary key, assuming that column is sorted in ascending  order in the tablelist.
    # that might not be a safe assumption, it might be better to get the key values with an SQL  command but this will do for now.
    # get the values in the last row in the table
    set lastindex [lindex [$tbl get $lastrow] $primary_key]
    
    # the primary_key variable has the column number of the primary key column.
    # this gets the value in that column in the last row and increments it by one.
    if {$lastindex != ""} {
        set lastindex [incr lastindex 1]
    } else {
        set lastindex 1
    }
    
    for {set x 0} {$x < [llength $column_names]} {incr x} {
        lappend new_row_data { }
    }
    
    # this inserts a new row in the table with the new index in the primary key column
    #$tbl insert end "{ } $lastindex { } { } { } { } { } { } { } { } { } { } { } { } { } { } { } { }"
    set new_row_data [lreplace $new_row_data $primary_key $primary_key $lastindex]
    $tbl insert end "$new_row_data"
    $tbl see end
    set new_row_data [join $new_row_data "\',\'"]
    set new_row_data "\'$new_row_data\'"
    puts $new_row_data
    #Eval_Remote $sock "db eval [subst {\{INSERT INTO Names VALUES(' ',$lastindex,' ',' ',' ',' ','  ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ');\}}]"
    db eval [subst {INSERT INTO $table_name VALUES($new_row_data);}]
 }

 # This is called whenever user clicks the delete selected row button when the cursor is focused on a  certain row
 proc deleteRecord {tbl table_name row primary_key} {
    
    set row_to_delete [eval $row]
    
    puts "table_name == $table_name"
    puts "row to delete == $row_to_delete"
    puts "primary_key == $primary_key"
    # get the value in the cell representing the primary key to pass later to the SQL DELETE command
    set primary_key_value [eval $tbl getcells $row_to_delete,$primary_key]
    puts "primary_key_value == $primary_key_value"
    # get the name of column with the primary key by querying the tablelist widget for the title on  the button at the top of the column
    # this will be passed to the SQL command for updating the database down below
    set columnname [$tbl columncget $primary_key -title]
    puts "columnname == $columnname"
    
    # Make a simple dialog to give user a chance to back out before it is too late.
    toplevel .are_you_sure
    label .are_you_sure.label -text "Are you sure you want to delte row $row_to_delete ?"
    pack .are_you_sure.label
    button .are_you_sure.yes -text "Yes, I AM sure" -command "
    destroy .are_you_sure
    # delete the row from the tablelist widget - has no actual effect on the SQLite database
    $tbl delete $row_to_delete
    # finally, delete the row from the Sqlite database itself.
    db eval {DELETE FROM $table_name WHERE $columnname = $primary_key_value}
    set sure_variable true
    "
    pack .are_you_sure.yes
    button .are_you_sure.no -text "No, I am NOT sure" -command {
        destroy .are_you_sure
        set sure_variable false
    }
    pack .are_you_sure.no
    tkwait variable sure_variable
 } 

 proc newTable {} {
    global table_names
    
    set newTableName ""
    toplevel .newTable
    label .newTable.label -text "Table Name"
    pack .newTable.label
    entry .newTable.entry -textvariable newTableName
    pack .newTable.entry
    button .newTable.newField -text "Add Field" -command {
        set newFieldName ""
        set newFieldType ""
        toplevel .newField
        label .newField.label -text "Field Name"
        pack .newField.label
        entry .newField.entry -textvariable newFieldName
        pack .newField.entry
        menubutton .newField.menubutton -relief raised -text {Type Field} -indicatoron true -menu  .newField.menubutton.menu
        menu .newField.menubutton.menu
        .newField.menubutton.menu add radiobutton -label "TEXT" -variable newFieldType -value "TEXT"
        .newField.menubutton.menu add radiobutton -label "numeric" -variable newFieldType -value  "numeric"
        .newField.menubutton.menu add radiobutton -label "BLOB" -variable newFieldType -value "BLOB"
        .newField.menubutton.menu add radiobutton -label "INTEGER PRIMARY KEY" -variable newFieldType -value "INTEGER PRIMARY KEY"
        pack .newField.menubutton
        button .newField.done -text Done -command {
            lappend new_field_list "$newFieldName $newFieldType ,"
            puts $new_field_list
            
            destroy .newField
        }
        pack .newField.done
        
    }
    pack .newTable.newField
    button .newTable.create -text "Create Table" -command {
        if {![regexp $newTableName $table_names]} {
            
            
            if {[catch {
                    regsub -all {\}} $new_field_list {} new_field_list
                    regsub -all {\{} $new_field_list {} new_field_list
                    regsub {,$} $new_field_list {} new_field_list
                    puts $new_field_list
                } err]} {
                tk_dialog .error Error "You have to have at least one new field for your new table." error 0 OK
                return
            }
            set command [list db eval [subst {CREATE TABLE $newTableName ($new_field_list);}]]
            lappend table_names $newTableName
            puts $command
            eval $command
            .topframe.leftframe.list insert end $newTableName
            destroy .newTable
        } else {
            tk_dialog .error Error "You already have a table by that name. Please select another." error 0 OK
        }
    }
    
    pack .newTable.create
 }

 proc dropTable {table} {
    global table_names
    toplevel .are_you_sure
    label .are_you_sure.label -text "Are you sure you want to delte table $table ?"
    pack .are_you_sure.label
    button .are_you_sure.yes -text "Yes, I AM sure" -command "
    destroy .are_you_sure
    set sure_variable true
    db eval {[subst {DROP TABLE $table ;}]}
    .topframe.leftframe.list delete [.topframe.leftframe.list index active]
    for \{set x 0\} \{\$x < \[llength \$table_names\]\} \{incr x\} \{
    if \{\[string compare $table  \[lindex \$table_names \$x\]\] == \"0\"\} \{
    set table_names \[lreplace \$table_names \$x \$x\]
    puts \"they match\"
    \}
    \}
    "
    pack .are_you_sure.yes
    button .are_you_sure.no -text "No, I am NOT sure" -command {
        destroy .are_you_sure
        set sure_variable false
    }
    pack .are_you_sure.no
    tkwait variable sure_variable
    
    
 }

 proc createtablelist {table_name} {
    global sock primary_key primary_key_name column_names
    set column_names {}
    set primary_key_name {}
    set primary_key {}
    # need to get the names of all the columns in the selected table using SQL command on the  sqlite_master table
    set initial_column_names [db eval [subst {SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL  SELECT * FROM sqlite_temp_master) WHERE tbl_name LIKE '$table_name' AND type!='meta' ORDER BY type  DESC, name;}]]
    #set column_names [Eval_Remote $sock db eval [subst {SELECT sql FROM (SELECT * FROM  sqlite_master) WHERE tbl_name LIKE '$table_name' AND type!='meta' ORDER BY type DESC, name;}]]
    puts "initial_column_names ==\n$initial_column_names"
    if {[regexp "CREATE TABLE" $initial_column_names]} {
        # get rid of some junk in the reply that we don't want
        regsub "CREATE TABLE $table_name" $initial_column_names {} initial_column_names
        #regsub {((version\)|\(name),|(KE|PRIMAR)Y)|(version\)|\(name,|(KE|PRIMAR)Y)|(\(signature\)|UNIQUE)|(\(signature\),|UNIQUE)}  $initial_column_names {} initial_column_names
        regsub {PRIMARY KEY \((.+, .+)+\),} $initial_column_names {} initial_column_names
        regsub {UNIQUE \(.+\)} $initial_column_names {} initial_column_names
        regsub {PRIMARY KEY \((.+, .+)+\)} $initial_column_names {} initial_column_names
        puts "initial_column_names ==\n$initial_column_names"
        
        regsub -all {\(} $initial_column_names {} initial_column_names
        regsub -all {\)} $initial_column_names {} initial_column_names
        regsub -all {\{} $initial_column_names {} initial_column_names
        regsub -all {\}} $initial_column_names {} initial_column_names
        puts "initial_column_names ==\n$initial_column_names"
        
        # the reply still contains the column name followed by a comma and the type description
        # so we need to make a new list with only the first element - the name without the type description
        set key_index_counter 0
        foreach name [split $initial_column_names ","] {
            if {[regexp "PRIMARY KEY" $name]} {
                set primary_key $key_index_counter
                set primary_key_name [lindex $name 0]
            }
            if {[lindex $name 0] != "" && [lindex $name 0] != "CREATE"} {
                lappend column_names [lindex $name 0]
            }
            incr key_index_counter
        }
    }
    
    # in this case you do not want all the column names
    if {[regexp "CREATE VIEW" $initial_column_names] && ![regexp "\\*" $initial_column_names]} {
        # we need to get the names of the columns you want from between the SELECT and the FROM statements
        regexp "SELECT .+ FROM" $initial_column_names match
        puts "match == $match"
        regsub "SELECT " $match {} match
        regsub " FROM" $match {} match
        regsub -all {, } $match { } match
        puts "match == $match"
        
        # in this case, the initial_column_names is actually the table names - I know that is confusing - just too lazy to change the code
        regsub "CREATE VIEW $table_name AS SELECT .+ FROM " $initial_column_names {} initial_column_names
        regsub { WHERE.+$} $initial_column_names {} initial_column_names
        set initial_column_names [split $initial_column_names ", "]
        regsub -all {\(} $initial_column_names {} initial_column_names
        regsub -all {\)} $initial_column_names {} initial_column_names
        regsub -all {\{} $initial_column_names {} initial_column_names
        regsub -all {\}} $initial_column_names {} initial_column_names
        regsub -all {\\} $initial_column_names {} initial_column_names
        #puts $initial_column_names
        # now loop through the selected tables and get the column names
        #foreach view_table $initial_column_names {
        #    set initial_column_names2 [db eval [subst {SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE tbl_name LIKE '$view_table' AND type!='meta' ORDER BY type DESC, name;}]]
        #    regsub "CREATE TABLE $view_table" $initial_column_names2 {} initial_column_names2
        #    regsub -all {\(} $initial_column_names2 {} initial_column_names2
        #    regsub -all {\)} $initial_column_names2 {} initial_column_names2
        #    regsub -all {\{} $initial_column_names2 {} initial_column_names2
        #    regsub -all {\}} $initial_column_names2 {} initial_column_names2
        
        # the reply still contains the column name followed by a comma and the type description
        # so we need to make a new list with only the first element - the name without the type description
        #    set key_index_counter 0
        #    foreach name [split $initial_column_names2 ","] {
        #        if {[regexp "PRIMARY KEY" $name]} {
        #            set primary_key $key_index_counter
        #            set primary_key_name [lindex $name 0]
        #        }
        #        lappend column_names [lindex $name 0]
        #        incr key_index_counter
        #    }
        #in this case the column names comes from the SELECT columnname1, columnname2 FROM
        # so we just set columnames equal to that
        set column_names $match
        
        
        #}
        
        
    }
    # this is the case where you use a wildcard for selecting the columnames when creating a view. So you will get all the column names in the tablelist widget.
    if {[regexp "CREATE VIEW" $initial_column_names] && [regexp "\\*" $initial_column_names]} {
        regsub "CREATE VIEW $table_name AS SELECT \\* FROM " $initial_column_names {} initial_column_names
        regsub { WHERE.+$} $initial_column_names {} initial_column_names
        set initial_column_names [split $initial_column_names ", "]
        regsub -all {\(} $initial_column_names {} initial_column_names
        regsub -all {\)} $initial_column_names {} initial_column_names
        regsub -all {\{} $initial_column_names {} initial_column_names
        regsub -all {\}} $initial_column_names {} initial_column_names
        regsub -all {\\} $initial_column_names {} initial_column_names
        puts $initial_column_names
        
        foreach view_table $initial_column_names {
            set initial_column_names2 [db eval [subst {SELECT sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE tbl_name LIKE '$view_table' AND type!='meta' ORDER BY type DESC, name;}]]
            regsub "CREATE TABLE $view_table" $initial_column_names2 {} initial_column_names2
            regsub -all {\(} $initial_column_names2 {} initial_column_names2
            regsub -all {\)} $initial_column_names2 {} initial_column_names2
            regsub -all {\{} $initial_column_names2 {} initial_column_names2
            regsub -all {\}} $initial_column_names2 {} initial_column_names2
            
            # the reply still contains the column name followed by a comma and the type description
            # so we need to make a new list with only the first element - the name without the type description
            set key_index_counter 0
            foreach name [split $initial_column_names2 ","] {
                if {[regexp "PRIMARY KEY" $name]} {
                    set primary_key $key_index_counter
                    set primary_key_name [lindex $name 0]
                }
                
                lappend column_names [lindex $name 0]
                
                incr key_index_counter
            }
            
            
            
        }
        
        
    }
    
    
    
    
    set top .configTop
    for {set n 2} {[winfo exists $top]} {incr n} {
        set top .configTop$n
    }
    toplevel $top -class DemoTop
    wm title $top $table_name
    
    
    set tf $top.tf
    frame $tf
    
    set tbl $tf.tbl
    set vsb $tf.vsb
    set hsb $tf.hsb
    set new_column_names [list "0 [join $column_names "\n0 "]"]
    #puts $new_column_names
    regsub -all {\{} $new_column_names {} new_column_names
    regsub -all {\}} $new_column_names {} new_column_names
    
    tablelist::tablelist $tbl -columns $new_column_names -selectmode multiple \
            -labelcommand tablelist::sortByColumn -sortcommand demo::compareAsSet \
            -editendcommand applyValue -height 15 -width 100 -stretch all \
            -xscrollcommand [list $hsb set] -yscrollcommand [list $vsb set] \
            -stripebackground           #e0e8f0
    # -editstartcommand enableDelete
    #$tbl columnconfigure 3 -maxwidth 30 -editable yes
    #$tbl columnconfigure 4 -maxwidth 30 -editable yes
    
    
    for {set x 0} {$x < [llength $column_names]} {incr x} {
        $tbl columnconfigure $x -maxwidth 30 -editable yes
        if {$x == $primary_key} {
            $tbl columnconfigure $x -foreground red -editable no
        }
    }
    
    
    scrollbar $vsb -orient vertical   -command [list $tbl yview]
    scrollbar $hsb -orient horizontal -command [list $tbl xview]
    
    #
    # Create three buttons within a frame child of the top-level widget
    #
    set bf $top.bf
    frame $bf
    set b1 $bf.b1
    set b2 $bf.b2
    set b3 $bf.b3
    set b4 $bf.b4
    
    set b5 $bf.b5
    set b6 $bf.b6
    set b7 $bf.b7
    
    button $b1 -text "Refresh"     -command [list demo::putConfig \$w $tbl] -state disabled
    button $b4 -text "New Record"  -command [list newRecord $tbl $table_name]
    #    button $b2 -text "Sort as set" -command [list $tbl sort] -state disabled
    
    # row delete button initially disabled because tablelist widget will set inital active row to 0 and you don't want to delete
    # until user selects row by putting the mouse on it at least.
    button $b2 -text "Delete Seleted Row" -command [list deleteRecord $tbl $table_name "$tbl index active" $primary_key] -state disabled
    button $b3 -text "Close"       -command [list destroy $top]
    
    if {$table_name == "SpokanePhysicians"} {
        button $b5 -image fax -command "
        foreach row \[$tbl curselection\] \{
        toplevel .fax
        text .fax.t
        pack .fax.t
        .fax.t insert 1.0 \"                              Jerry Park D.O.
        101 Main St.
        Spokane WA 88845
        \[clock format \[clock seconds\] -format \"%m/%d/%Y %R\"\]\\n\\n\\n\"
        set this_row  \[$tbl get \$row\]
        .fax.t insert end  \"\[lindex \$this_row 2\] \[lindex \$this_row 1\] \[lindex \$this_row 4\]\\n\"
        .fax.t insert end  \"\[lindex \$this_row 5\]\\n\"
        .fax.t insert end \"\[lindex \$this_row 6\] \[lindex \$this_row 7\] \[lindex \$this_row 8\]\\n\\n\\n\"
        .fax.t insert end \"Hi \[lindex \$this_row 2\]!\\n\\\n\\n\"
        .fax.t insert end \"     I was just wanting to let you know that our new tkfp_tablelist2.tcl program seems to be working.\\n\\n\\n\\n\"
        .fax.t insert end \"                              Yours truly,\\n\\n\"
        .fax.t insert end \"                              Jerry Park D.O.\"
        
        button .fax.done -text {Send} -command {set done_variable true;destroy .fax}
        pack .fax.done -side left
        button .fax.cancel -text {Cancel} -command {set done_variable true;destroy .fax}
        pack .fax.cancel -side left -padx 5
        tkwait variable done_variable
        \}
        "
        
        button $b6 -image email
        button $b7 -image mail
    }
    
    set bodyTag [$tbl bodytag]
    bind $bodyTag <FocusIn>   [list $b2 configure -state normal]
    
    #
    # Manage the widgets
    #
    grid $tbl -row 0 -column 0 -sticky news
    grid $vsb -row 0 -column 1 -sticky ns
    grid $hsb -row 1 -column 0 -sticky ew
    grid rowconfigure    $tf 0 -weight 1
    grid columnconfigure $tf 0 -weight 1
    pack $b1 $b2 $b4 $b3 -side left -expand yes -pady 10
    if {$table_name == "SpokanePhysicians"} {
        pack $b5 $b6 $b7 -side left -expand yes -pady 10
    }
    pack $bf -side bottom -fill x
    pack $tf -side top -expand yes -fill both
    
    #insert some data retrieved by sql from the database into the tablelist
    #the blank space at the end of each variable is because the tablelist seems
    #to ignor nulls and moves things over one unless you put a space in.
    set data [db eval [subst {Select * from $table_name;}]]
    
    #foreach {BusinessOrganizationType NameID Honorific FirstName LastName Degree ExtraDegrees Nickname SpecialtyID Specialty2ID BusinessOrganization Birthday Custom1_Name Custom2_Name Custom3_Name Custom4_Name Comments_Name DateLastUpdated_Name} $data {
    
    #    $tbl insert end " \"$BusinessOrganizationType \" \"$NameID\" \"$Honorific \" \"$FirstName \" \"$LastName \" \"$Degree \" \"$ExtraDegrees \" \"$Nickname \" \"$SpecialtyID \" \"$Specialty2ID \" \"$BusinessOrganization \" \"$Birthday \" \"$Custom1_Name \" \"$Custom2_Name \" \"$Custom3_Name \" \"$Custom4_Name \" \"$Comments_Name \" \"$DateLastUpdated_Name \""
    #}
    
    #set column_insert_command " \\\"\$"
    #set column_insert_list [join $column_names " \\\" \\\"\$"]
    #append column_insert_command $column_insert_list
    #append column_insert_command " \\\""
    
    append column_insert_command " \\\"\$\{"
    set key_counter 0
    foreach name $column_names {
        #if {[regexp {\.} $name]} {
        #    set name [lindex [split $name "."] 1]
        #}
        
        if {$key_counter != $primary_key && ![string compare $name " "] && ![string compare $name ""]} {
            append column_insert_command "$name\} \\\" \\\"\$\{"
        } else {
            append column_insert_command "$name\}\\\" \\\"\$\{"
        }
        incr key_counter
    }
    append column_insert_command "\} \\\""
    regsub {\$\{\}} $column_insert_command {} column_insert_command
    
    foreach $column_names $data {
        set command  "$tbl insert end \"$column_insert_command\""
        #puts $command
        eval $command
        #update idletasks
    }
 }

LES on 2006-03-24: This program causes "segmentation fault" in my Linux box with ActiveTcl 8.5a4.

Alex Caldwell Thanks, So far I have only tested it on Windows XP with Active Tcl 8.4.9, tclsqlite 3.3.4, and Tablelist 4.3

AK wrote (in email):

This application seems to have problems when a table has an index set on it.

I used it on a database I had here and it showed me additional columns which were not in the table. From some clues (like the title of one being PRIMARY) I guess they were for the index. However the app filled these columns with the data from the table, not from the index, causing the values to be shuffled around over several entries. Instead of, for example
 a b c d e
 a b c d e
 ...

I got shown
 a b c d e a
 b c d e a b
 c d e a b c
 ...

Alex Caldwell Email Discussion Regarding Display Problem

Hi again,

I think I remember something that applies here - With tclsqlite, if you have a table in the SQLite database and ask for a row back as a Tcl list, you can get a list like this if there is an empty value in the SQLite table (I may be using the wrong term there, is it the same as a Null value to SQLite?) in that row:
 e d f {} g

I found that when you then insert a list like that into a row in the Tablelist widget, the tablelist widget ignores the {} member of the list, and moves the other members of the list to the right of it one cell over to the left. Then the rows that come after that row in the table also all get shifted over to the left too, so the first member of the next row will be inserted in the last cell on the previous row's line in the tablelist widget. In our database, I substituted empty members of the table with a space or a _ so the list would look like:
 e d f {_} g

or
 e d f { } g

This seemed to keep the Tcl list lengths in sync with the SQLite table and the Tablelist widget's rows. I then trimmed off the extra space or _ when some data was actually finally added to that field in the tablelist widget by the user. I am thinking this might be where the shifting you are seeing is coming from. I should also mention that the SQL query combobox is not fully compliant with standard SQL. It is sensitive to the amount of whitespace - it only allows one space between keywords in the SQL statement. It is likely it may not understand complicated SQL queries with nested sub queries and stuff like that. It has only been tested with some simple queries that we used in our little project.

Csaba Nemethi on 2006-03-24: It is not correct that Tablelist ignores empty list elements when inserting a row. Here is a simple example:
 package require Tablelist
 tablelist::tableist .tbl -columns {0 A  0 B  0 C}
 grid .tbl
 .tbl insert end {a b c}
 .tbl insert end {a {} c}
 .tbl insert end {{} b c}

Alex Caldwell Thanks, I see that you are right. I think the problems was actually sort of the reverse of what I said. I think it comes when I collect the list from the row in the tablelist that is being edited and send it to SQLite for updating the row. If there is an empty cell, my list would shorten by one and that would shift the data over in the SQLite table. So I made the cells default to contain a space, and that seemed to be a work around for the problem I was running into. When the user enters some data into an "empty" cell, I trim off the extra whitespace on the ends.

AK. My guess would then be that the code simply creates a tablelist with the wrong number of columns:

Sqlite returns a list
 {a b c d e a b c d e ...}

i.e. groups of 5. And the application inserts this into a tablelist which is configured for n (n != 5) columns, and thus takes the input in groups of n, and this shifts everything around.

Alex Caldwell Through the use of some really ugly looking regsub expressions, I was able to get the program to handle the database schema sent to me by AK that was not displaying properly. The way it gets the column names is to query the database for the table schemas. Then it picks apart the table schema using regexp and regsub, trying to get the column names. But obviously, I had only done it on a simple table schema with no indexes on them as in this example, which it now seems to handle. I am sure there are more variations of table schema that it won't handle, but if I got some samples of schema that have problems like this, I may be able to modify the "schema parser" to become more robust:

From Email Discussion Regarding Display Problems:

This schema should show the troubles:
                CREATE TABLE objects
                (  name      TEXT NOT NULL,
                   version   TEXT NOT NULL,
                   signature         TEXT NOT NULL,
                   PRIMARY KEY (name, version),
                   UNIQUE (signature)
                   )
                ;
                CREATE TABLE attr
                (  signature     TEXT NOT NULL,
                   name  TEXT NOT NULL,
                   value TEXT NOT NULL,
                   file TEXT NOT NULL,
                   PRIMARY KEY (signature, name)
                   )
                ;
                CREATE INDEX attr_file
                ON           attr (file)
                ;

Andreas Kupries

AK: Alex, thank you very much for your responsiveness. I just retrieved the updated code of this application and tried again to view my database. Everything is now looking fine. Thanks again.

LV 2007 Sep 06 I wonder - perhaps correspondence with the SQLite developer might provide a more tcl compatible interface for getting schema information. In that way, you wouldn't have to mess with all the regular expressions.

I mean, unless the schema is kept in full ascii sql format, sqlite is generating the above from metadata. So it shouldn't be that bit of a deal to return the information in the form of a tcl list, for instance, or perhaps a dict.

NEM 2008-05-18: You can get the columns for a query if there is at least one row in it:
 proc cols {db table} {
     $db eval {SELECT * FROM $table LIMIT 1} row { return $row(*) }
     error "unable to get columns for '$table'"
 }

DKF: It's a really good idea to put a LIMIT 1 clause in there. Otherwise, you could be doing quite a bit of work which will never be required. NEM Done.

KBK 2008-05-19 Tbere is a way to get the column information, even though that part of the schema is, in fact, kept in unparsed SQL. Try the following:
proc cols {db table} {
    set result {}
    $db eval "PRAGMA TABLE_INFO($table)" row { lappend result $row(name) }
    return $result
}

The PRAGMA TABLE_INFO query returns other information about the column, including its index within the table, its data type, its default value, and indicators for whether nulls are allowed and whether the column is a part of the primary key.

Brian Theado 2008-05-19: NEM, when the table has no rows, the row(*) variable still gets populated with the column names, so the following works even on empty tables:
 proc cols {db table} {
     $db eval {SELECT * FROM $table} row break
     return $row(*)
 }

NEM Interesting, so it gets set before the loop executes. Is this behaviour guaranteed/documented or just a side-effect of the current implementation?

HJG 2007-09-05 There is no menu and no buttons, so how should saving a new database work ?