  SQLSCREENS: a simple screen generator for MYSQL
  J.F. Dockes - CDKIT - dockes@cdkit.remcomp.fr

  25 May 1999
  ____________________________________________________________

  Table of Contents:

  1.      Preface

  2.      Quick description

  3.      Installation

  3.1.    External software needed

  3.2.    Installation procedure

  3.2.1.  Generating a wish interpreter with MySQL support:

  4.      Using sqlscreens

  4.1.    Environment variables

  4.2.    Buttons

  4.3.    Keyboard shortcuts

  4.4.    Special characters in fields

  5.      Programming Interface

  5.1.    Overview

  5.2.    Initialization and termination

  5.2.1.  sqlscreen

  5.2.2.  sqlscreendelete

  5.3.    Interface array entries

  5.3.1.  General parameters

  5.3.1.1.        window

  5.3.1.2.        database

  5.3.1.3.        sqlcpasswd, sqlschost, sqlscuser

  5.3.1.4.        table

  5.3.1.5.        joinclause

  5.3.1.6.        columns

  5.3.2.  Screen attributes

  5.3.2.1.        queryonly

  5.3.2.2.        allowdelete

  5.3.2.3.        notitle

  5.3.2.4.        nobuttons

  5.3.3.  Field attributes

  5.3.3.1.        Column type and length

  5.3.3.2.        autopercent

  5.3.3.3.        texts

  5.3.3.4.        choices

  5.3.3.5.        ordercols

  5.3.3.6.        updateindex

  5.3.3.7.        upshiftcols

  5.3.3.8.        noentry

  5.3.3.9.        nodisplay

  5.3.4.  Auxiliary list window

  5.3.4.1.        list_columns, list_window

  5.3.4.2.        list_colwidths:

  5.3.4.3.        list_lineproc:

  5.3.5.  Miscellaneous

  5.3.5.1.        hdl

  5.3.5.2.        initfocus

  5.3.5.3.        tabcolserial

  5.3.5.4.        querynum

  5.3.5.5.        sqlscshowstmts

  5.3.5.6.        sqlsc_colname_valsaved

  5.4.    Linking screens

  5.4.1.  sqlmasterslave

  5.4.2.  sqlslavemaster

  5.5.    Controlling the number of button sets

  5.6.    Callback routines:

  5.7.    Visible internal interfaces

  5.8.    Small utility routines

  6.      SQL generation

  6.1.    Query

  6.2.    Add

  6.3.    Update

  6.4.    Delete

  7.      Update issues

  8.      The tcsq low level database access layer

  8.1.    Environment variables

  8.2.    API calls

  8.2.1.  tcsqconnect

  8.2.2.  tcsquse

  8.2.3.  tcsqconuse

  8.2.4.  tcsqopensel

  8.2.5.  tcsqrew

  8.2.6.  tcsqclosel

  8.2.7.  tcsqnext

  8.2.8.  tcsqexec

  8.2.9.  tcsqdiscon

  8.2.10. tcsqtabinfo

  8.2.11. tcsqcolinfo

  8.2.12. tcsqinsertid

  8.2.13. tcsqquotequote

  8.2.14. tcsqquoteblob

  8.3.    Programming example

  9.      Sample scripts
  ______________________________________________________________________

  11..

  PPrreeffaaccee

  This document describes sqlscreens release 1.0.1

  With sqlscreens you can easily create screen forms, for querying and
  updating a relational database.

  Sqlscreens is based on TCL/TK. It is primarily designed to work with
  MySQL as a backend. It might still work with INFORMIX (untested for
  some time), MSQL, and ODBC.

  Sqlscreens is no match for commercial application development tools.
  It is a very simple tool to create ad-hoc query screens. We find it
  very handy for creating our data-entry utilities in CDKIT, which is
  why we decided to publish it, partly also because we use so much free
  software that we feel compelled to contribute a little.

  If you've struggled with (Y,N,Y,N,Y,Y,...) lists in the MySQL grant
  tables, you may find sqlscreens useful :-)

  Still interested ? Details ``follow'' , or if already know everything,
  here is the download page <sqlscdownload.html> .

  22..

  QQuuiicckk ddeessccrriippttiioonn

  A typical sqlscreens application will have a number of screens, each
  with several fields. Each screen will be linked to one or several
  database tables, each field to a column. Impatient people using a web
  browser can look at the screen dumps <sqlscdumps.html> right now.

  You create each screen by listing the column names you want.

  If you do not even list the column names, all columns are used, so
  that creating a screen to look at a table is 5 lines of TCL code.
  There is a sample program in the package to do just this: give it the
  table name and up comes the screen (tablescreen.tcl).

  Once the screens are created, you can query, insert, update, and
  delete records by entering data in the fields and clicking on the
  appropriate button (or using a keyboard shortcut).

  Sqlscreens provides an easy method to link the screens so that a
  change in one screen will trigger a query in another one (for master-
  detail relationships), or so that it will just update the join column.

  You can also:


  +o  Create multiline text widgets to edit text blobs.

  +o  Display query results as a list linked to a detail screen.

  +o  And do many other things described a little further.

  As all values for the fields are stored in an accessible TCL array, it
  is quite easy to add code for data validation or to show computed
  fields. There are provisions in the package for calling external
  routines before and after the database operations.

  Sqlscreens can be used to build standalone database access
  applications, or to embed a database-access screen in another
  application. For example, in CDKIT, we manage a big musical database.
  We use sqlscreens mainly for data-entry screens, but we also embed it
  in the audio-acquisition application, to establish the link between
  the database and the audio files.

  There are many other bells and whistles, but also a few drawbacks:


  +o  You have little control over field placement. Fields are placed in
     a row-column grid managed by Tk's grid geometry manager.

  +o  The generated SQL is very basic, and you have little control over
     it.

  +o  The package has mainly be used in CDKIT, and it may be dependant on
     assumptions that we make here, which may not match your
     environment. Please try on a test database, not your production one
     ! When there are no primary keys, the package is crippled.

  +o  There is no real support for structured fields like date/time (that
     is, you can use date fields, but the package will not check the
     format). As we mainly use text and number fields in CDKIT, there
     are probably more bugs with other types of fields (less testing).

  +o  You can't specify null values when querying (the fields with no
     data are just not used). Operators like '>' or '<' can only be used
     for non-text fields .

  +o  The screens are not pretty !

  +o  Etc... Etc...

  This said, the software is free and we are open to suggestions to
  improve it.

  If you want a quick idea of what it does, do the installation, have a
  look at the tablescreen.tcl file in the samples directory, set the
  host and user name, and point it to any table, like:


  ______________________________________________________________________
  tablescreen.tcl dbname tablename

  ______________________________________________________________________



  This will create a screen with fields for all columns in the table
  (you may need to adjust the host and user names in the script or the
  environment to get the right permissions).

  If you're reading this in a WEB browser, you can have a look at a few
  screen dumps <sqlscdumps.html> to get a better idea.

  33..

  IInnssttaallllaattiioonn

  33..11..

  EExxtteerrnnaall ssooffttwwaarree nneeeeddeedd

  To use sqlscreens, you will need a number of external software
  packages:


  +o  TCL/TK. We now mainly use version 8.0, but TCL 7.6 + TK 4.2 might
     still be ok for some time. Version 8.1 should be ok too. If you're
     running a recent FreeBSD or RedHat Linux, you just need to install
     the packages. Else, you can get the source distributions from the
     scriptics site <http://www.scriptics.com> .TCL and TK are very easy
     to compile.

  In order to access the databases, the basic TCL interpreter must be
  augmented with a database access module:


  +o  For MySQL, a modified version of the mmssqqllttccll package by Hakan
     Soderstrom is included in the distribution (mmyyssqqllttccll..cc).

  +o  The original mmssqqllttccll can be used for accessing MSQL databases.

  +o  For ODBC you will need tclodbc
     <http://www.solagem.fi/~rnurmi/tclodbc.html>, and an ODBC driver
     manager. Under UNIX, we tested iODBC
     <http://www.openlinksw.com/iodbc/>, but unixODBC
     <http://genix.net/unixODBC/> should be OK too. Of course, you'll
     also need the driver for your database.  For MMyySSQQLL, this would be
     myodbc <http://www.tcx.se/download.html>.. The exact versions that
     were used while developping the ssqqllssccrreeeennss ODBC interface were:
     iODBC 2.50.2, myodbc 2.50.23, tclodbc 2.1.  We also have a small
     patch to help compile tclodbc, you can find it on the the
     sqlscreens site
     <http://www.cdkit.com/sqlscreens/sqlscdownload.html>

  +o  For INFORMIX you will need the iissqqllttccll package by Srinivas Kumar.
     You can get it there
     <ftp://ftp.neosoft.com/languages/tcl/sorted/databases/isqltcl3.2> .

  If you're reading this in a browser, there is a download page
  <sqlscdownload.html> where you can get the sqlscreens package and
  which also points to the other pieces.

  33..22..

  IInnssttaallllaattiioonn pprroocceedduurree

  The package comes as a gzipped tar file named something like
  _s_q_l_s_c_r_e_e_n_s_-_X_._Y_._Z_._t_a_r_._g_z.


  Unpacking the file (gunzip < sqlscreens-X.Y.Z.tar.gz | tar xvf -) will
  create a top directory named like _s_q_l_s_c_r_e_e_n_s_-_X_._Y_._Z_.

  X is the major release number. Y is the minor release. Z is the bug
  fix release number. Don't make too much of it...

  First, if needed, compile and install TCL and TK (untar; cd
  tcl8.../unix;configure;make;make install, same for tk). tcl and wish
  will probably be installed as "/usr/local/bin/tclsh8.0" and
  "/usr/local/bin/wish8.0". The sqlscreens Makefile expects to find
  "tclsh" and "wish". Please make a link, and ensure that your PATH is
  setup to execute the correct copy of tclsh and wish.

  The next step is to add database-access capability to the standard
  TCL/TK wwiisshh interpreter. This can be done in two ways, either by
  statically linking the database access module (mmyyssqqllttccll or iissqqllttccll)
  with the interpreter or by using the TCL llooaadd facility and a shared
  library. The dynamic version sometimes need some manual tweaking to
  work.

  The Makefile in the ssqqllssccrreeeennss directory only supports MMyySSQQLL
  internally.  If you will be using iissqqllttccll and INFORMIX, follow the
  installation instructions in the iissqqllttccll package to generate the
  interpreter.

  For OODDBBCC, you should first install the driver manager, the driver(s)
  you need and the ttccllooddbbcc TCL extension. Follow the instructions in
  each package.

  33..22..11..

  GGeenneerraattiinngg aa wwiisshh iinntteerrpprreetteerr wwiitthh MMyySSQQLL ssuuppppoorrtt::

  Both the static and dynamic load methods are supported by the build
  tools in the ssqqllssccrreeeennss directory. Only Linux (RedHat 4.1/4.2),
  SOLARIS, and FreeBSD 2.2.x have been tested, it is not guaranteed to
  work on other systems. The Makefile generated by ccoonnffiigguurree is small,
  it should be easy to adjust if needed.

  The configuration script only looks for the MySQL library in
  /usr/local/mysql/lib and /usr/local/lib/mysql. If your installation is
  different, please make symbolic links or modify the scripts.

  When you're ready:



  1. "cd" to the sqlscreens directory, and type "./configure".

  2. Type "make" to compile and link the mmyyssqqllwwiisshh interpreter and the
     shared library. The shared library link may produce error messages,
     see below.

  3. Type "make install" to install the package. This will create a
     "/usr/local/lib/sqlsc" directory and copy the shared library and
     TCL code there. It will also copy mmyyssqqllwwiisshh to "/usr/local/bin. If
     the shared library link failed at the previous step, or if you get
     error messages about unfound symbols during installation, either
     type "make install-static" to just install the static version, or
     review the README-DYNAMIC file where there is some more information
     about dynamic libraries issues. If you're in a hurry or/and are not
     used to building shared libraries, you might just want to use the
     static version.

  If you use TCL with other statically linked extensions, and want to
  use the same interpreter with ssqqllssccrreeeennss, you will have to add the
  _M_y_s_q_l_t_c_l___I_n_i_tcall to your usual _t_k_A_p_p_I_n_i_t file and modify your
  Makefile to link with mmyyssqqllttccll..oo.  You've probably been through this
  already. Have a look at the included tkAppinit.c

  Because the dynamic version is sometimes a pain to build, the sample
  scripts are now set up to be executed by mmyyssqqllwwiisshh by default (for
  MySQL). If you managed to get the dynamic version to build, you'll
  certainly find it easy to change the first line in the scripts...

  44..

  UUssiinngg ssqqllssccrreeeennss

  The following is organized more like a reference manual, there is
  little tutorial material. You may want to look at the ``sample
  scripts'' to get a quick idea.

  44..11..

  EEnnvviirroonnmmeenntt vvaarriiaabblleess

  The SSQQLLDDBBTTYYPPEE environment variable decides what database code is going
  to be used. It has two possible values:


  +o  _M_Y_S_Q_L to access a MySQL database.

  +o  _I_N_F_O_R_M_I_X to access an INFORMIX database.

  +o  _M_S_Q_L to access an MSQL database.

  +o  ODBC to use an ODBC driver manager.

  The default if the variable is not set is to use MYSQL. Don't depend
  on it.

  This variable can be set inside the script, before the first call to
  ssqqllssccrreeeenn.  (It is set to MYSQL inside the sample scripts). Of course,
  the value of this variable must be consistent with what extensions are
  available to the TCL interpreter (See installation).

  The SSQQLLSSCCLLOOGG environment variable can be set to the name of a file
  where Sqlscreens will log the SQL statements it executes. The default
  is to log to stdout.

  The SSQQLLSSCCHHOOSSTT, SSQQLLSSCCUUSSEERR, and SSQQLLSSCCPPAASSSSWWOORRDD variables can be used to
  set the connection parameters. These are only used by the sample
  scripts, not the core package.

  MMYYSSQQLL__TTCCPP__PPOORRTT should be used if you need to change the default MySQL
  connection port.

  44..22..

  BBuuttttoonnss

  Each screen has a set of buttons to perform the following operations:


     QQuueerryy
        starts a SELECT. The WHERE clause is built with the values
        currently shown on the screen and the nnooddiissppllaayy fields).

     NNeexxtt
        fetches the next record in the current query

     RReewwiinndd
        gets back to the first record in the current query. This has
        different effects depending on the database: in INFORMIX this
        actually reruns the query so that changes in the database will
        be visible. With MySQL, this just rewinds the local result
        buffer.

     RReesseett
        clears all visible and hidden fields in the screen.

     AAdddd
        inserts the current values. There is special handling for
        auto_increment fields, see ``SQL generation''

     UUppddaattee
        updates the row according to the current values (how the where
        clause is built is described later, see ``updateindex'' and
        ``Sql generation'' ).

     DDeelleettee
        deletes the row(s) selected by the current values. It will
        prompt for confirmation if more than one row would be affected.

  The UUppddaattee and AAdddd buttons may not exist on all screens (some screens
  may be set up only for querying).

  DDeelleettee is not created by default (See ``allowdelete'' ).

  The ``SQL generation'' section describes how the SQL statements are
  generated for the different actions.

  44..33..

  KKeeyybbooaarrdd sshhoorrttccuuttss

  Often, when doing data entry, it is inconvenient to have to reach for
  the mouse to perform an action. A set of keyboard shortcuts is
  provided to make things smoother:


     <<TTAABB>>
        goes to the next field in the screen.

     <<CCRR>>
        in any field will start a SELECT.


     <<EESSCC>>nn
        will fetch the next row.

     <<EESSCC>>rr
        will rewind the query.

     <<EESSCC>>aa
        will start an INSERT.

     <<EESSCC>>uu
        will start an UPDATE.

     <<EESSCC>>ww
        will reset the current screen (clear all fields).

  The Update and Add shortcuts will have no effects in query only
  screens.  There is no Delete shortcut.

  44..44..

  SSppeecciiaall cchhaarraacctteerrss iinn ffiieellddss

  The '<' and '>' characters will be interpreted when entered as the
  first character in a non-string field. Actually, if such a field
  begins with clause when querying. For example:

  If you enter ">10" in a field named "quantity", a "quantity > 10"
  condition will be inserted in the WHERE clause. You could also enter
  ">10 and quantity < 20", or whatever condition you need. (See
  also``the paragraph about SQL generation'' ).

  55..  PPrrooggrraammmmiinngg IInntteerrffaaccee

  55..11..

  OOvveerrvviieeww

  All exchanges between the package and the user application are made
  through TCL arrays that hold all data and parameters. There is one
  such array for every screen. The array name is not significant except
  that it will be used for the screen title. We often use the table
  name, but this is not mandatory.

  The basic idea is that you set values in the array and then call
  "ssqqllssccrreeeenn _a_r_r_a_y_n_a_m_e_" to create the screen.

  The values set in the array define what tables/columns will be used,
  how the screen will look like, etc... You can also define callback
  functions which will be called before and after the database accesses,
  to give you an opportunity for checking what's happening, possibly
  modify values, or block the operation if something is wrong.

  In the following, we shall use the example of a database named
  "orderdb", with a table named "customers", with columns named
  "custid", "custname", and "custfirstname", and a table named "orders"
  with "orderid" and "ordercustid".

  ssqqllssccrreeeennss stores the values for the field corresponding to a column
  as "arrayname(sqlsc__c_o_l_u_m_n_value)" (Ex: customer(sqlsc_custid_value)).
  This makes collisions of other entries with your column names
  unlikely. You can access these variables to retrieve values into your
  application, and also to modify them (before an insert for example, if
  the user input needs processing, or if some values are automatically
  generated by the application).


  The first release used to store the values as arrayname(_c_o_l_u_m_n). If
  you have written code based on this, I would suggest that you modify
  it. If you do not want or can not, you can set the global variable
  "sqlsc_names_compat_old" to 1 before the first call to get a
  compatible behaviour (this will go away in the near future).

  The following paragraphs describe the function of the different array
  entries, beginning with the most basic and frequently used, then the
  different callback functions that you can use.

  I am sorry for the many naming inconsistencies (like using or not the
  sqlsc prefix for array entries), this came over time and would just be
  too much work to change.

  When you're finished with the screen, you can call ssqqllssccrreeeennddeelleettee
  _a_r_r_a_y_n_a_m_e to cleanup and release all resources (array, windows,
  database connections).  Most applications will exit instead.
  ssqqllssccrreeeennddeelleettee is mostly useful in case you want to recreate the
  screen with different options (most options can't be changed once a
  screen is created).

  55..22..

  IInniittiiaalliizzaattiioonn aanndd tteerrmmiinnaattiioonn

  55..22..11..

  ssqqllssccrreeeenn

  To create an sqlscreen, you set values inside a TCL array (See the
  following section: ``Interface array entries'' ), then call :


  ______________________________________________________________________
  sqlscreen yourarrayname

  ______________________________________________________________________



  to create the screen. Note that ssqqllssccrreeeenn will create and pack the
  screen's window, but not its parents, so that the screen will not be
  necessarily visible at this point. Ex:


  ______________________________________________________________________
  frame .f
  set myarray(window) .f.scr
  sqlscreen myarray
  # screen still not visible
  pack .f     #screen appears

  ______________________________________________________________________



  This can be useful if you do not want the screen to be visible at all
  times: you can use 'pack ' and 'pack forget' to make it appear and
  disappear as you wish.

  ssqqllssccrreeeenn optionnally takes a second parameter. If the value is 'h',
  the fields will be arranged horizontally instead of vertically. There
  are other ways to do this(see ``columns'') , but it can still be
  useful in some cases.


  55..22..22..

  ssqqllssccrreeeennddeelleettee

  This procedure will destroy all resources associated with an sqlscreen
  (windows, database connections and the array itself). Call it as:


  ______________________________________________________________________
  sqlscreendelete arrayname

  ______________________________________________________________________



  55..33..

  IInntteerrffaaccee aarrrraayy eennttrriieess

  55..33..11..

  GGeenneerraall ppaarraammeetteerrss

  55..33..11..11..

  wwiinnddooww

  This defines the TK frame name where the screen will be created.
  Example:


  ______________________________________________________________________
  frame .f1
  set customer(window) .f1.cust

  ______________________________________________________________________



  or just the following to create the window in the top one:


  ______________________________________________________________________
  set customer(window) .cust

  ______________________________________________________________________



  This entry must be a valid TK window name: for exemple it cannot start
  with an upper case character.

  The window must not exist before calling ssqqllssccrreeeenn, which will create
  it.  Its parents must exist.

  55..33..11..22..

  ddaattaabbaassee

  This defines the database name.






  ______________________________________________________________________
  set customer(database) orderdb

  ______________________________________________________________________



  55..33..11..33..

  ssqqllccppaasssswwdd,, ssqqllsscchhoosstt,, ssqqllssccuusseerr

  These define the user name, host and password for the connection to
  the database server. These are all optional.

  55..33..11..44..

  ttaabbllee

  This defines the table name. Example:


  ______________________________________________________________________
  set customer(table) customers

  ______________________________________________________________________



  It is also possible to display fields from several tables in one
  screen:


  ______________________________________________________________________
  set custorder(table) {customers orders}

  ______________________________________________________________________



  If you are using several tables, you'll also need a join clause (see
  the following paragraph), and you will not be able to modify data
  through the screen.  (You CAN update several tables in one
  application, but each table will need a separate screen, and the
  screen links will be through cascaded queries, not join clauses - See
  ``Linking screens'' ).

  55..33..11..55..

  jjooiinnccllaauussee

  In case fields from several tables are displayed in a screen,
  ssqqllssccrreeeennss needs to know how to join the tables when performing a
  SELECT. This is defined by the jjooiinnccllaauussee array entry. Exemple:


  ______________________________________________________________________
  set custorder(joinclause) \\
         {customers.custid = orders.ordercustid}

  ______________________________________________________________________






  55..33..11..66..

  ccoolluummnnss

  This is a list to define the columns that you want included. If it is
  not set, ssqqllssccrreeeenn will query the database for all the column names in
  the table, and build the screen with the result.

  Example for specifying the column names:


  ______________________________________________________________________
  set customer(columns) {custid custname}

  ______________________________________________________________________



  If several tables are used, it may be necessary to qualify the column
  names if they are not unique:


  ______________________________________________________________________
  set custorder(columns) {customers.custid customers.custname...}

  ______________________________________________________________________



  By default, all fields will be displayed in one column. You can get
  them to be displayed in one line by calling sqlscreen as "sqlscreen
  arrayname h" instead of "sqlscreen arrayname". You can also insert
  "line breaks" by inserting newline caracters in the column list, like:


  ______________________________________________________________________
  set arrayname(columns) {
      host "\\n"
      user db "\\n"
      select_priv insert_priv update_priv "\\n"
      delete_priv create_priv drop_priv
  }

  ______________________________________________________________________



  The field positions will be arranged by the ggrriidd geometry manager. In
  lines with less fields, the last field (and only the last) spans the
  remaining columns.  You'll probably need several tries to get it right
  (at least I do).

  55..33..22..

  SSccrreeeenn aattttrriibbuutteess

  55..33..22..11..

  qquueerryyoonnllyy

  If this is set, the screen will not have "add" and "update" buttons,
  you will only be able to select data. Example:




  ______________________________________________________________________
  set customer(queryonly) ""

  ______________________________________________________________________



  The value has no importance, just setting the array entry (even to
  "no") creates a screen for query only.

  55..33..22..22..

  aalllloowwddeelleettee

  If this entry is set, and queryonly is not set a DDeelleettee button will be
  created.

  55..33..22..33..

  nnoottiittllee

  Suppresses the screen title. This spares a little space if your screen
  is crowded.

  55..33..22..44..

  nnoobbuuttttoonnss

  If this is set, no buttons will be created in this particular screen.
  Note that this does not change what you can do in the screen, because
  the keyboard shortcuts are still available.

  55..33..33..

  FFiieelldd aattttrriibbuutteess

  55..33..33..11..

  CCoolluummnn ttyyppee aanndd lleennggtthh

  The ssqqllsscc___c_o_l_n_a_m_e__lleenn and ssqqllsscc___c_o_l_n_a_m_e__ttyyppee entries (like
  _s_q_l_s_c___c_u_s_t_i_d___l_e_n, _s_q_l_s_c___c_u_s_t_i_d___t_y_p_e) are normally created by the
  package, you do not need to set them. For character columns, you can
  set ssqqllsscc___c_o_l_n_a_m_e__lleenn if you want the entry field to be of a size
  different from the column width (for example if the column is very
  wide). Example:


  ______________________________________________________________________
  set customer(sqlsc_custname_len) 20

  ______________________________________________________________________



  would create a 20 characters field even if custname is actually 100
  characters wide. This does not constrain what you can enter because TK
  fields can scroll.

  Beware: ssqqllssccrreeeennss does NOT check that the input can fit in the
  database column. This can produce unexpected truncation.

  55..33..33..22..

  aauuttooppeerrcceenntt

  The aauuttooppeerrcceennttbbootthh, aauuttooppeerrcceennttlleefftt, aauuttooppeerrcceennttrriigghhtt lists can be
  set for character columns where you want '%' to be automatically added
  before a query (all char field queries are done with the LIKE
  operator). Example:


  ______________________________________________________________________
  set customer(autopercentright) {custname}

  ______________________________________________________________________



  would let you query by entering just the beginning of the name,
  without having to reach for the shift key to type '%'.

  55..33..33..33..

  tteexxttss

  This is a list of columns (typically text blobs) that should be
  displayed in multiline text widgets. Each entry is a triplet listing
  the column name, and the width and height of the text widget. Ex:


  ______________________________________________________________________
  set product(texts) {{description 20 70} {notice 10 70}}

  ______________________________________________________________________



  ssqqllssccrreeeennss will handle quoting and unquoting the blob contents.

  There is an exemple of tteexxttss use in the wwiinneess..ttccll sample application.

  55..33..33..44..

  cchhooiicceess

  This list defines columns where entries should come from a menu
  instead of being free form. It is very useful, but the interface could
  be nicer.

  The cchhooiicceess entry is a list. There are two list elements for every
  column.  The first element is the column name, the second element the
  name for the list of possible values. For example:


  ______________________________________________________________________
  set customer(choices) {
      custtype custtypelist
      custsex custsexlist
  }

  ______________________________________________________________________



  Would specify that the _c_u_s_t_t_y_p_e and _c_u_s_t_s_e_x columns will have values
  coming from _c_u_s_t_t_y_p_e_l_i_s_t and _c_u_s_t_s_e_x_l_i_s_t. These lists would typically
  have been created beforehand (possibly by querying another table). The
  list of values can in turn be of two types: either a simple list or a
  list of pairs.


  A simple list lists the possible values (would you believe this ?).
  Ex:


  ______________________________________________________________________
  set custtypelist {normal distributor internal}

  ______________________________________________________________________



  normal_, distributor and internal will be both displayed on the screen
  and used for querying or updating the database.

  In a list of pairs, each pair defines the value that should be shown
  and the value that should be entered in the database. Example:


  ______________________________________________________________________
  set custsexlist {{unknown 0} {female 1} {male 2}}

  ______________________________________________________________________



  With this list, the menu would display unknown, male, female, but the
  values used for the database would be 0, 1, 2.

  Note that when using MySQL, a choice menu will be automatically
  generated for 'enum' columns. You can still set your own list, which
  will override the automatically built one. This can be useful if the
  displayed values are different from the stored ones.

  There are exemples of use (both automatic and explicit) in the
  wwiinneess..ttccllsample program.

  55..33..33..55..

  oorrddeerrccoollss

  This list defines column names that will be added in an ORDER BY
  clause each time a query is run. It has the format of a normal ORDER
  BY column list.  Ex:


  ______________________________________________________________________
  set customer(ordercols) "custid desc, custname"

  ______________________________________________________________________




  55..33..33..66..

  uuppddaatteeiinnddeexx

  This defines a column name (or a list of column names) that will be
  used in the WHERE clause of an UPDATE statement. It should provide a
  way to uniquely identify a row.

  If neither ccoolluummnnss nor uuppddaatteeiinnddeexx are set before calling ssqqllssccrreeeenn,
  ssqqllssccrreeeennsswill try to make up an uuppddaatteeiinnddeexx by using a serial column
  or primary key if one is found.


  If ccoolluummnnss is set, uuppddaatteeiinnddeexx is automatically generated only if the
  primary key is completely included in the column list.

  If no uuppddaatteeiinnddeexx list finally exists, the screen will have no uuppddaattee
  button.

  See ``Sql generation'' for a more complete discussion of update row
  selection.

  55..33..33..77..

  uuppsshhiiffttccoollss

  This is a list of fields for which values should be automatically
  changed to upper case before inserting or querying. This is very
  useful with INFORMIX which is case-sensitive, not very useful with
  MySQL. Example:


  ______________________________________________________________________
  set customer(upshiftcols) {custname custfirstname}

  ______________________________________________________________________



  55..33..33..88..

  nnooeennttrryy

  This is a list of columns for which data entry is forbidden. They are
  displayed differently, and will not allow typing. This is sometimes
  useful for fields that should only be updated by the program or on
  which searching is forbidden.

  55..33..33..99..

  nnooddiissppllaayy

  This is a list of columns for which no fields will be shown. The
  corresponding values are present in the array. This is used for fields
  which link several screens, or which the application wants to use, but
  which don't need to be displayed.

  55..33..44..  AAuuxxiilliiaarryy lliisstt wwiinnddooww

  55..33..44..11..

  lliisstt__ccoolluummnnss,, lliisstt__wwiinnddooww

  If lliisstt__ccoolluummnnss and lliisstt__wwiinnddooww are set, ssqqllssccrreeeenn will create an
  auxiliary list for the screen, in the specified window. The list
  screen will display one line for each result row, the data displayed
  will be taken from the lliisstt__ccoolluummnnss columns. This is useful to get a
  compact display of a query's results.

  Clicking on a line in the list with mouse button 1 will display the
  corresponding row in the main screen.

  This capability will only be available if an updateindex list has been
  defined for the screen (either implicitely or explicitely), see ``the
  updateindex section''. The updateindex columns must be part of
  lliisstt__ccoolluummnnss, so that we can uniquely link back from the list to the
  detail screen.


  Example:


  ______________________________________________________________________
  set customers(list_columns) {custname custid}
  set customers(updateindex) custid
  toplevel .custlist
  set customers(list_window) .custlist

  ______________________________________________________________________



  55..33..44..22..  lliisstt__ccoollwwiiddtthhss::

  Sqlscreens will try its best to compute appropriate column widths for
  the list and to align the columns. You may force specific values for
  the column widths by specifying the lliisstt__ccoollwwiiddtthhss entry, as a list
  specifying the width in characters for each column. Exemple:


  ______________________________________________________________________
  set customers(list_colwidths) {40 5}

  ______________________________________________________________________



  The widths must be specified in the same order as the columns in
  lliisstt__ccoolluummnnss.

  55..33..44..33..  lliisstt__lliinneepprroocc::

  When displaying the list, Sqlscreens will alternate the line's
  background between white and light grey to facilitate reading. If
  defined, the lliisstt__lliinneepprroocc procedure will be called for each displayed
  line, with parameters allowing it to change the line's display (for
  exemple, this would allow showing special rows needing attention in
  red). Exemple:


  ______________________________________________________________________
  set customers(list_lineproc) custlineproc

  ______________________________________________________________________



  lliisstt__lliinneepprroocc will then be called for each line with 3 parameters:


  1. The name of the TTKK tteexxtt window where the line is displayed.

  2. The TTKK tteexxtt ttaagg name for the area associated with the line.

  3. The list of column values for this line.

  lliisstt__lliinneepprroocc can then test one or several entries in the value list,
  and use the window and tag names to set attributes. The following
  exemple sets the ugly colors in the wine list according to the bottle
  count (from wwiinneess..ttccll):





  ______________________________________________________________________
  proc setlinecolor {w tag res} {
      # Get the bottle count from the value list
      set botcnt [lindex $res 0]
      # Set the background color accordingly
      switch $botcnt {
        1 {$w tag configure $tag -background red}
        2 {$w tag configure $tag -background orange}
        3 {$w tag configure $tag -background yellow}
        default {$w tag configure $tag -background green}
      }
  }

  ______________________________________________________________________



  55..33..55..

  MMiisscceellllaanneeoouuss

  55..33..55..11..

  hhddll

  The package uses this entry to store the database handle.

  55..33..55..22..

  iinniittffooccuuss

  This is the name of the window where the focus should go when the
  screen is reset. This can be useful for repetitive entry when you
  don't want to use the mouse.

  55..33..55..33..

  ttaabbccoollsseerriiaall

  If there is a serial or auto_increment column, ssqqllssccrreeeenn sets its name
  in there.

  If the screen allows insertion, but this field is either not displayed
  or not modifiable (noentry), the value will be automatically reset to
  null before performing an insert, which will allow inserting a record
  by first querying for (and probably modifying) another one.

  If the field is modifiable by the user, no special action will be
  taken.

  If the bbeeffoorreeiinnsseerrtt procedure is defined for the screen, any
  modification is performed before calling it, to allow for a local
  value allocation scheme.

  55..33..55..44..

  qquueerryynnuumm

  This is the select result handle.

  55..33..55..55..

  ssqqllssccsshhoowwssttmmttss

  You can set this variable to 1 or 0 to print the SQL statements to
  stdout (or SSQQLLSSCCLLOOGG) when they are executed. This is not an array
  element but a global variable.

  55..33..55..66..

  ssqqllsscc__ccoollnnaammee__vvaallssaavveedd

  The package uses these entries to save the database values when a
  query is performed. This is used to compute the UPDATE statements (See
  the ``Sql generation'' section).

  55..44..

  LLiinnkkiinngg ssccrreeeennss

  It is possible to link two screens so that a change in one screen will
  update the other one. This can be done in two ways.

  55..44..11..

  ssqqllmmaasstteerrssllaavvee

  This links the first screen to the second one so that a query in the
  first will run a query in the second. Example:


  ______________________________________________________________________
  sqlmasterslave customer custid order ordercust

  ______________________________________________________________________



  would link the _c_u_s_t_o_m_e_r and _o_r_d_e_r screens so that the _o_r_d_e_r screen is
  reset, the _o_r_d_e_r_c_u_s_t field is set to the value of the _c_u_s_t_i_d field and
  a query is run every time a query is run in _c_u_s_t_o_m_e_r.

  Things are set up so that it is possible to have reciprocal links
  without creating an infinite loop. Example:


  ______________________________________________________________________
  sqlmasterslave customer custid order ordercustsqlmasterslave order ordercust customer custid

  ______________________________________________________________________



  is ok and would both show a customer's orders after querying in the
  _c_u_s_t_o_m_e_r screen and an order's customer after querying in the _o_r_d_e_r_s
  screen.

  55..44..22..

  ssqqllssllaavveemmaasstteerr

  This second type of link is used to just update a column in the target
  screen, without running a query there. It is useful to set the join
  column values. Example:


  ______________________________________________________________________
  sqlslavemaster customer custid order ordercust

  ______________________________________________________________________


  could be used to set the _o_r_d_e_r_c_u_s_t field by querying _c_u_s_t_o_m_e_r,
  typically while entering orders.

  55..55..

  CCoonnttrroolllliinngg tthhee nnuummbbeerr ooff bbuuttttoonn sseettss

  Each screen in an application normally has a set of control buttons.
  It is sometimes useful to use only one set of buttons for several
  screens. This is done with the ssqqccoommmmoonnbbuuttttoonnss routine. ssqqccoommmmoonnbbuuttoonnss
  will create a TK frame with a set of control buttons inside. This set
  of buttons will not be linked to a particular screen, but will apply
  to the screen which has the current keyboard focus. Example:


  ______________________________________________________________________
  sqcommonbuttons .f1.buttons

  ______________________________________________________________________



  Will create the _._f_1_._b_u_t_t_o_n_s frame and buttons inside there.

  It is possible to create several sets of buttons (useful when there
  are several top level frames in the application) by calling
  ssqqccoommmmoonnbbuuttttoonnss several times. Any of these sets will control the
  screen which currently has the keyboard focus.

  In practice, this facility has not proved very useful because it is to
  easy to make mistake about where the current keyboard focus actually
  is.

  It would be quite easy to use completely custom buttons for an
  application by setting the "usecommonbuttons" variable, and creating
  custom buttons with appropriate callbacks (look at the sqcommonbuttons
  code in sqlscreens.tcl).

  55..66..

  CCaallllbbaacckk rroouuttiinneess::

  The following callback routine names can be defined in the array:


  +o  afterinsert

  +o  afterquery

  +o  afterupdate

  +o  afterdelete

  +o  beforeinsert

  +o  beforequery

  +o  beforeupdate

  +o  beforedelete

  Example:




  ______________________________________________________________________
  set customer(beforeinsert) checkcustfields

  ______________________________________________________________________



  The different routines will be invoked in the following manner:

  For beforexxx routines:


  ______________________________________________________________________
  routinename optype arrayname

  ______________________________________________________________________



  For afterxxx routines:


  ______________________________________________________________________
  routinename optype txt arrayname

  ______________________________________________________________________



  Where optype defines what's happening (like beforeinsert, afternext,
  etc...), arrayname is the affected screen's array name, and txt is the
  SQL text for afterxxx routines. We can't pass the text to the
  beforexxx routines, because they may be responsible to modify some
  field values that will affect the statement !

  bbeeffoorreeiinnsseerrtt, aafftteerriinnsseerrtt, bbeeffoorreeuuppddaattee, aafftteerruuppddaattee,, bbeeffoorreeddeelleettee
  andaafftteerrddeelleettee will be called before and after inserting or updating
  data.

  bbeeffoorreeqquueerryy and aafftteerrqquueerryy will be called before and after doing a
  select, and aafftteerrqquueerryy will also be called after the user fetches the
  next record, rewinds the query, or resets the screen.

  If one of the beforexxx routines returns anything but 0 , the
  operation will be canceled (not run).

  55..77..

  VViissiibbllee iinntteerrnnaall iinntteerrffaacceess

  In some cases it may be useful to start a database operation by a
  program call (as opposed to a button press by the user). This is
  easily feasible by calling the following routines. They all take the
  array name as sole argument, and use the values that are currently
  stored/displayed in the screen.


  +o  ssqqllsscciinnsseerrtt: Generate and run an INSERT statement.

  +o  ssqqllssccuuppdd: Generate and run an UPDATE statement.

  +o  ssqqllssccqquueerryy: Generate and run a SELECT statement.

  +o  ssqqllssccddeelleettee: Generate and run a DELETE statement.


  +o  ssqqllssccnneexxtt: Fetch the next row in the current query.

  +o  ssqqllssccrreeooppeenn: Rewind the current query.

  +o  ssqqllssccrreesseett: Reset all data values for the screen.

  In all cases, the effect will be exactly the same as the corresponding
  button press.

  55..88..

  SSmmaallll uuttiilliittyy rroouuttiinneess


  +o  ssqqllsscc__eennttrryywwiiddggeett _a_r_n_m _c_o_l_n_a_m_e Return the name for the entry widget
     for _a_r_n_m and _c_o_l_n_a_m_e.

  +o  ssqqllsscc__llaabbeellwwiiddggeett _a_r_n_m _c_o_l_n_a_m_e Return the name for the label widget
     for _a_r_n_m and _c_o_l_n_a_m_e.

  66..

  SSQQLL ggeenneerraattiioonn

  Every time the user presses a button like qquueerryy or uuppddaattee, the program
  will generate a SQL statement to perform the appropriate operation on
  the database.  The following paragraphs describe how the statement is
  generated.

  66..11..

  QQuueerryy

  The QQuueerryy button generate a SELECT statement. The list of columns
  comes from the ccoolluummnnss entry in the input array (all the columns by
  default).

  The WHERE clause is built from all the fields that hold data
  (including the hidden ones if there are any).

  For non character columns, the comparison operator used is =. For
  character columns, it is LIKE.

  Example: for a screen with custid, custname, custfirstname,
  custsomenumber fields, where data was entered in custname (xxx) and
  custsomenumber (yyy), the statement would be:


  ______________________________________________________________________
  SELECT custid,custname,custfirstname,custsomenumber from customers
  WHERE custname LIKE 'xxx' AND custsomenumber = yyy

  ______________________________________________________________________



  If a numeric field begins with '<' or '>', whatever is entered in the
  field will be used as a condition in the WHERE clause, and AND'ed with
  the rest. (Ex: you could enter ">10" or "<>1234" , or ">10 and
  custsomenumber<20).

  66..22..

  AAdddd


  The AAdddd button generates an INSERT statement. All fields which hold
  data are used for the values, the others are not listed. Char fields
  are suitably quoted. With the same example as above, the SQL statement
  would be:


  ______________________________________________________________________
  INSERT INTO customers(custname,custsomenumber) VALUES('xxx',yyy)

  ______________________________________________________________________



  There is no explicitely provided way to insert a NULL value (and
  certainly none for a char field).

  If the table's primary key is a serial or auto_increment field, and
  the corresponding field is set as "noentry", the value for the field
  is reset to "" before inserting to let auto_increment do its job.

  There seems to be no way to retrieve the auto_increment attribute from
  a mysql client program, so that, when using MySQL, we make the
  assumption that if an integer field is a primary key, it also has the
  auto_increment attribute.  If the field is also set as noentry, it
  will be reset before inserting.

  66..33..

  UUppddaattee

  The UUppddaattee button generates an UPDATE statement. There are two issues:
  the WHERE clause and the values.

  The WHERE clause is built from the columns that were designated in the
  uuppddaatteeiinnddeexx list (if no such list was explicitely indicated,
  sqlscreens tries to use the primary index columns for the table. If
  there is no primary index, no updateindex list is built, and no Update
  button is created, neither can you run an update by typing Esc u).

  The values in the WHERE clause are taken from those that were saved
  when the last QQuueerryy (or NNeexxtt, RReewwiinndd, RReesseett) was performed, which
  means that it is possible to update the columns in the primary index.
  If you try an update without having performed some query before,
  you'll get strange error messages about missing array entries.

  The values for the update are taken from the screen fields (including
  the possible hidden ones). All fields whose value is different from
  the saved value are used. If no value changed, no update is performed
  (and an error dialog is shown).

  As opposed to what happens for SELECT and INSERT, even the fields with
  no data are used. For char fields, the columns are set to '', for
  other types, they are set to NULL. This is somewhat arbitrary, but we
  like it like this.

  66..44..

  DDeelleettee

  The DDeelleettee button generates a DELETE statement. The WHERE clause is
  built like the SELECTs, except that no LIKE operators are used.

  If some columns have NULL or zero-length string values, they will not
  be used in the WHERE clause. This means that more rows than expected
  could sometimes be affected by the statement.

  For this reason, the program will create a dialog screen and ask for
  confirmation if more than one row would be affected by a DELETE
  statement.

  77..

  UUppddaattee iissssuueess

  Any application that displays database values and allows the user to
  update them has two problems:


  1. Ensuring that the generated UPDATE statement will really update the
     row that was displayed and not many other rows in the database.

  2. Ensuring that the affected row has not changed since it was
     displayed.

  The first issue can be solved by ensuring that the values initially
  retrieved uniquely define the row (for example, it is the case if a
  complete primary key is included in the retrieved fields). That is why
  ssqqllssccrreeeennss insists on having an _u_p_d_a_t_e_i_n_d_e_x field list. This will be
  automaticaly generated from the primary key in some cases, or
  specified by the application in other cases.  There are also other
  ways, such as using rowids or server-side cursors, but they are
  database-dependant.

  For the second issue, ssqqllssccrreeeennss takes the approach to include all the
  screen's fields in the WHERE clause, not only the updateindex fields.
  This guarantees that the UPDATE will fail if one of the fields changed
  in the database.  It still might be possible that another field in the
  record (a field not used by the screen) would have changed since the
  query, but this change will not be affected by the new update. If this
  is still undesirable, you just need to include all the appropriate
  fields in the screen (possibly with the "nodisplay" attribute).

  88..

  TThhee ttccssqq llooww lleevveell ddaattaabbaassee aacccceessss llaayyeerr

  ssqqllssccrreeeennss uses an intermediate code layer to access the different
  databases in a consistent fashion. This intermediate layer is called
  ttccssqq.

  The ttccssqq calls which are documented here may be useful as a database
  access layer for non-ssqqllssccrreeeennss applications (for any TCL script
  accessing the supported databases), or in auxiliary routines inside an
  ssqqllssccrreeeennss application (for example, for building lists of values by
  querying a table).

  88..11..

  EEnnvviirroonnmmeenntt vvaarriiaabblleess

  ttccssqq uses the same SQLDBTYPE environment variable as the ssqqllssccrreeeennss
  layer to define the database type.

  88..22..

  AAPPII ccaallllss

  88..22..11..

  ttccssqqccoonnnneecctt


  ______________________________________________________________________
  tcsqconnect [host [user [passwd]]]

  ______________________________________________________________________



  Returns a server connection handle (hdl in the following). Depending
  on the database type, it may actually connect to a server, or do
  nothing (Ex: informix).

  88..22..22..

  ttccssqquussee


  ______________________________________________________________________
  tcsquse hdl dbname

  ______________________________________________________________________



  Associates the connection handle hdl with database dbname .

  88..22..33..

  ttccssqqccoonnuussee


  ______________________________________________________________________
  tcsqconuse database [host]

  ______________________________________________________________________



  Utility function: connect and use.

  88..22..44..

  ttccssqqooppeennsseell


  ______________________________________________________________________
  tcsqopensel hdl stmt

  ______________________________________________________________________



  Opens a query operation. stmt is a string holding an SQL SELECT
  statement.

  Returns a select handle (selhdl in the following).

  88..22..55..

  ttccssqqrreeww


  ______________________________________________________________________
  tcsqrew selhdl

  ______________________________________________________________________

  Rewinds the query associated with selhdl. This may actually rerun the
  query (INFORMIX) or be purely local (MYSQL).

  88..22..66..

  ttccssqqcclloosseell


  ______________________________________________________________________
  tcsqclosel selhdl

  ______________________________________________________________________



  Closes a query, and frees the associated resources.

  88..22..77..

  ttccssqqnneexxtt


  ______________________________________________________________________
  tcsqnext selhdl

  ______________________________________________________________________



  Returns the next row for the query, as a list of values, in the order
  of the columns in the SELECT statement. The last fetch returns an
  empty list.

  88..22..88..

  ttccssqqeexxeecc


  ______________________________________________________________________
  tcsqexec hdl stmt

  ______________________________________________________________________



  Executes a non-SELECT SQL statement (Ie, INSERT, DELETE, etc...)

  88..22..99..

  ttccssqqddiissccoonn


  ______________________________________________________________________
  tcsqdiscon hdl

  ______________________________________________________________________



  Disconnects and frees resources associated with hdl.

  88..22..1100..

  ttccssqqttaabbiinnffoo


  ______________________________________________________________________
  tcsqtabinfo hdl

  ______________________________________________________________________



  Returns a list of the user tables in the database referenced by hdl.

  88..22..1111..

  ttccssqqccoolliinnffoo


  ______________________________________________________________________
  tcsqcolinfo hdl tbl arnm

  ______________________________________________________________________



  Returns information about table tbl into the array the name of which
  is specified by arnm.

  88..22..1122..

  ttccssqqiinnsseerrttiidd


  ______________________________________________________________________
  tcsqinsertid hdl

  ______________________________________________________________________



  Returns the auto_increment value for the last inserted row.

  88..22..1133..

  ttccssqqqquuootteeqquuoottee


  ______________________________________________________________________
  tcsqquotequote s

  ______________________________________________________________________



  Returns a suitably escaped string, for use in sql statements.

  88..22..1144..

  ttccssqqqquuootteebblloobb


  ______________________________________________________________________
  tcsqquoteblob s

  ______________________________________________________________________



  Same for blobs.

  88..33..

  PPrrooggrraammmmiinngg eexxaammppllee

  The following shows is a small program to search for a name in a MYSQL


  ______________________________________________________________________
  #!/usr/local/cdkit/isqltcl
  package require tcsq
  set env(SQLDBTYPE) MYSQL

  set hdl [tcsqconuse mysql localhost]

  set uname [tcsqquotequote "John O'Connell"]
  set qry [tcsqopensel $hdl "SELECT host,user FROM user WHERE user LIKE '$uname'"]

  for {set res [tcsqnext $qry]} {$res != ""} {set res [tcsqnext $qry]} {

      set host [lindex $res 0]
      set user [lindex $res 1]

      puts "Host: $host User: $user"
  }

  tcsqclosel $qry
  tcsqdiscon $hdl
  exit 0

  ______________________________________________________________________



  99..

  SSaammppllee ssccrriippttss

  Three sample scripts are provided with the package (in the _s_a_m_p_l_e_s
  directory).  These are not real applications. For example you have to
  set the password in the environment or edit the scripts to change it.
  The goal was to keep things as simple as possible.


     ttaabblleessccrreeeenn..ttccll
        is a minimal application using all the automatic defaults to
        create a screen with all the columns in a given table. This can
        be a good skeleton for trying things.

     mmyyssqqllddbb..ttccll
        creates a screen to access the MySQL "db" table. The main goal
        is to show how you can arrange the fields in row-column.
        Otherwise, it does nothing more than ttaabblleessccrreeeenn..ttccll..

     wwiinneess..ttccll
        is something I use to manage my wine cellar. It demonstrates
        most of sqlscreen's features (and provides most of the
        incentives for new gadgets, by the way).

  To try wwiinneess..ttccll, you will have to create tables and load the sample
  data in a database named "wines". The "_c_r_e_a_t_e_l_o_a_d_w_i_n_e_s_._s_h" shell-
  script will do this for you.

  wwiinneess..ttccll is not the perfect cellar management application, but I'll
  be working on it, and it is useful enough for now. It is already
  better than my old Excel spreadsheet, and I don't need to reboot my PC
  under some strange Operating System to use it.
