** Summary ** [Richard Suchenwirth] 2004-03-22: In [A simple database], I showed how databases may be implemented with Tcl [array]s. This take here is closer to traditional relational databases, with ''tables'' of pre-defined columns (modeled here as a list of lists, the first being the column heading, the rest the "records"), and mimicks the [SQL] SELECT statement a bit. ** See Also ** [editRecord]: a tiny dialog to edit records in such a database. ** Description ** Tables are "pure values", and the result of ''select'' is a valid table again: ====== proc select {fields "from" tbl "where" condition} { set infields [lindex $tbl 0] if {$fields eq "*"} {set fields $infields} set res [list $fields] foreach inrow [lrange $tbl 1 end] { foreach $infields $inrow break if $condition { set row {} foreach field $fields {lappend row [set $field]} lappend res $row } } set res } #-- Test data, assuming a little inventory control system: set table { {number description pieces} {1234 Foo 100} {2345 Bar 50} {3456 Grill 2} } ====== Testing: ======none % select * from $table where {$pieces < 100} {number description pieces} {2345 Bar 50} {3456 Grill 2} % select {pieces description} from $table where {$number != 1234} {pieces description} {50 Bar} {2 Grill} ====== Cute, ain't it? There is a danger though, if you happen to name a "database" column ''condition, row, fields, res'' or so... because the column names are used as variables, and would overwrite the working variables, possibly causing syntax errors. [AM]: If you use namespace variables (like ::db::row - not "variable row"), you should be able to avoid even such clashes ... Adding a "record" to this database is trivial: ====== lappend table {1234 "another Item" 1} ====== Editing a value in place goes well with [lset], where you for now need to specify the record number, but can address a column by its name: ====== proc col {table field} { lsearch [lindex $table 0] $field } ====== ====== lset table 4 [col $table description] "Item, another" ====== ---- Another frequent operation is sorting a table on a column, with options like ''-increasing'' or ''-integer''. We only have to make sure that the header list stays always in front:} ====== proc sort {table field args} { set res [list [lindex $table 0]] eval lappend res [eval lsort -index [col $table $field] $args \ [list [lrange $table 1 end]]] } ====== ======none % sort $table pieces -integer {number description pieces} {3456 Grill 2} {2345 Bar 50} {1234 Foo 100} % sort $table description -decreasing {number description pieces} {3456 Grill 2} {1234 Foo 100} {2345 Bar 50} ====== And as fashionable these days, here's a simple sketch how to export a table as [XML] (with entity escaping of ''cell''): ====== proc toXML {table {type table}} { set fields [lindex $table 0] set res <$type>\n foreach row [lrange $table 1 end] { append res foreach field $fields cell $row { set cell [string map {< "<" & "&" > ">"} $cell] append res <$field>$cell } append res \n } append res } ====== ---- [RS] 2004-09-30: Another terribly simple piece of code generates a full '''index''' of the table, where keys are ''field=value'', and the "row number(s)" of applicable records are the value. It returns it as a serialized array (which could be turned to [dict] later): ====== proc mkindex table { set fields [lindex $table 0] array set a {} set i 0 foreach row [lrange $table 1 end] { incr i foreach field $fields value $row { lappend a($field=$value) $i } } array get a } ====== Testing: ======none % mkindex $table pieces=2 3 number=3456 3 number=2345 2 number=1234 1 description=Foo 1 description=Bar 2 description=Grill 3 pieces=50 2 pieces=100 1 ====== To query this index, dump it into an array again, and search for all things whose description starts with "G", with ====== array get arr description=G* ====== ----- [unperson]: Very impressive work! Well done, Richard! May I ask you a question? When one opens the file where the data is stored to have a look at the contents, (say we open the file with Notepad), what do we see? Text + {{Opening and closing braces}}? When we are talking about entering the data with fields, in a table, we are talking about seing '''all''' the records in the database and not only one. Some people (including yours truly) need a global view of a database. Is there a way to have a table view of the records? I wonder if it would be easy to adapt your database from the opening-closing-brace data structure to the CSV data structure. ---- [RS] The only data structure involved here is a list of lists. How to serialize (stringify) that in a text file is left to the user (Tcl format, [CSV], whatever). For displaying the whole table, one might look at [mclistbox] or such, but if there are several 100 000 records, putting all in listboxes may not be the best idea :^) Then again, one could also put extracts (as delivered by ''select'' above) in an [mclistbox]. [ZB] 2009-11-15: Comparing this solution with that described in [a simple database] I've got a feeling, that array-based solution - while being older - is still superior to this one. Having a set of basic array functions - completed with list- and dict-related - one just doesn't need to iterate, to fetch any amount of data, whether there's a need for rows, or for columns; a good example is "how to get column", just by: array get db *,title (of course, there must be some iteration anyway, but not at TCL-level). Storing the "records" in lists - or dictionaries - will force, at some point, the need for iteration. (After a while) from the other side: iteration can be used f.e. to fill treeview widget without a need to buffer a large amount of data (one "record" at a time will do) - so actually it depends on the need... <> Category Database | Arts and Crafts of Tcl-Tk Programming | Arts and Crafts of Tcl-Tk Programming | RS