A Guide for Writing


Sybase Stored Procedures for the World Wide Web


by


Denis D. Workman










SRI International
333 Ravenswood Aveunue
Menlo Park, California 94025









Updated on Sunday, March 29, 1998








Table of Contents

  1. Introduction
  2. What you will need
  3. Writing your stored procedure
  4. Formatting Result Sets
  5. Tips and Techniques
  6. Sending Partial Result Sets
  7. Handling Text and Image Data Types
  8. Advanced Topics
  9. Reference








Introduction:

This guide will attempt to show you how to create stored procedures for the World Wide Web. As I was in the middle of writing this guide, I realized it was about 10 times more lengthy than what I intended. The interface you will be using is slick, writing stored procedures is a snap, and you can do some marvelous things in a very short time. But, there is lots of good information in this guide, and I wanted to be as complete as possible.

There is a facility known as the Common Gateway Interface or CGI that extends the functionality of a Web Server. This feature makes it possible to pass information from the client to an external application (known as the CGI application) which processes the information and returns the result to the client through the Web Server. Sybernet is a CGI that talks to a Sybase database.

In his article on CGI applications, Jon Wiederspan writes:

When trying to understand how CGI applications work, the first thing to learn is the difference between the server and the client. WebSTAR is [a] server. Mosaic, MacWeb, and Netscape are all clients. The client software is smart. It knows how to interpret HTML, how to handle partial URL's and URL's with strange extra information like search strings, and how to find servers. The server software is stupid. It does fine as long as it is fed the right information, but if a client sends bad information, the server has no idea what to do. Therefore, in most transactions it is the client that's doing all the cool stuff and the server is just passing back a file or an error code."
One of the things you will be learning is how to tell Sybernet to invoke your stored procedure. Obviously, there are a lot of stored procedures in this world, and Sybernet needs to know the name of the stored procedure you want to invoke.

We can't possibly cover everything there is to know about HTML or stored procedures, but we will learn enough for you to go away and write your own. In this guide we will be creating two stored procedures: a form screen and a report screen.

  • The form screen procedure will prompt the user for some information. Our form screen will prompt the user for an author ID that we will pass to our report screen procedure.

  • The report screen procedure will receive this author ID as a parameter and generate a report by extracting information about this author from the the Pubs2 database.

One of the facilities of HTML form screens is the ability to pass hidden fields from your form to the Server. Of course, as far as Sybernet is concerned the field does not need to be hidden, but we hide the field because we don't want the user to see this: not because we have something to hide (no pun intended), but because it is information that they do not need to see. All they are interested in is entering the appropriate information and getting their report. Besides, hiding the field makes your form less cluttered.

We will be using input fields on our form screen to pass information to Sybernet and to our report screen procedure. Sybernet needs to know the name of the stored procedure and how to format the result set. The report screen needs to know the value of the author's ID.

Somewhere in this document there is a list of reserved words that have particular meaning to Sybernet: they do something special. Any identifier that Sybernet does not recognize, however, is considered a parameter to your stored procedure. Earlier versions of Sybernet required Sybase parameters to be preceded by an at-sign (@). For POST requests, this is no longer true. This change allows you to use your Sybase names with JavaScript

Parameters are passed to stored procedures explicitly by name; for example,

@EMPID="14568"

Thanks to Sybase, this is really cool because passing the parameter name along with the parameter value means you can pass the parameters in any order. And this is most helpful because you cannot always guarantee the order of fields passed from a Web client.

Features

Now that you know a little bit more about CGI's here is a list of features (in no particular order) that Sybernet provides:
  • Supports multiple user connections to Sybase.
  • Holds connections open until the user logs out or after a pre-defined idle time limit.
  • Allows "guest" access to a database (if desired).
  • Easy to install (simply copy Sybernet.cgi to the same folder as WebSTAR).
  • Can run in either synchronous or asynchronous mode.
  • Supports the Send Partial event of WebSTAR allowing unlimited output.
  • Can use cooperative, multi-tasking threads to service multiple requests simultaneously.
  • Automatically shuts down when no user connections are active.
  • Automatically starts up when a request is received.
  • Can retrieve and store both Text and Image data types.
  • Allows any number of parameters to a stored procedure.
  • Automatically precedes each parameter value by its parameter name (making order unimportant).
  • Can use the Direct and Search arguments in a URL request to pass parameters to a stored procedure.
  • Can pass internal values (such as client IP address or name of client software) to a stored procedure.
  • Does not use AppleScript.
  • Does not use Perl.
  • Does not use scripts.
  • Does use Open Client Client-Library.

What you will need:

Let's have some fun now!


Writing your stored procedure:

Well, you're actually going to write two stored procedures. The first procedure is your form screen that you will register with the Stored Procedure Register facility. The form screen is where you gather parameters for the report screen procedure. This is the procedure that creates the report.

Have I explained this clearly enough? Let's say someone wants you to create a report for them on their Web browser. They want to specify, for example, an employee number, and you are to create a report for that particular employee.

This is what's going to happen:

  1. They log into Sybase.

  2. They select your form screen from the main directory.

  3. They then enter an employee number in the space provided.

  4. They then press the submit button to process their report.

  5. The employee number is passed to your report screen procedure.

  6. The report magically appears.

Log on on Sybase and create the following two procedures: once this is done you can read the explanation that follows. Don't forget to grant the appropriate permissions on these stored procedures, however.

The FORM screen:

        create procedure sp_authors_form
        as
            select '<H1>Search Authors</H1>'
            select "Please enter author's ID in the space provided and press report."
            select '<P>'
            select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
            select 'Author: <INPUT TYPE="TEXT" NAME="au_id">'
            select '<P>'
            select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_authors_report">'
            select '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="NONE">'
            select '<INPUT TYPE="SUBMIT" VALUE="Report"><BR>'
            select '</FORM>'
            return

The REPORT screen:

        create procedure sp_authors_report(@au_id varchar(11) = NULL)
        as        
            select '<BR>' + au_fname + ' ' + au_lname
            ,      '<BR>' + address
            ,      '<BR>' + city + ', ' + state + ' ' + postalcode
            ,      '<BR>'
            ,      '<BR>' + 'Phone: ' + phone
            ,      '<BR>'
            from   pubs2..authors
            where  au_id = @au_id
            if @@rowcount = 0
            begin
                select '<STRONG>'
                select @au_id + ' not found!'
                select '</STRONG>'
            end
            return

Explaining the Form Screen:


Explaining the Report Screen:

After all that work and we've only covered the stored procedure that creates the form screen. Well, I'm just as tired as you are. But we're in luck: we've covered the difficult part. All that's left to do is select our author from the authors table and format it with HTML! How hard can that be?

Here is the Report Screen Procedure again:

        create procedure sp_authors_report(@au_id varchar(11) = NULL)
        as        
            select '<BR>' + au_fname + ' ' + au_lname
            ,      '<BR>' + address
            ,      '<BR>' + city + ', ' + state + ' ' + postalcode
            ,      '<BR>'
            ,      '<BR>' + 'Phone: ' + phone
            ,      '<BR>'
            from   pubs2..authors
            where  au_id = @au_id
            if @@rowcount = 0
            begin
                select '<STRONG>'
                select @au_id + ' not found!'
                select '</STRONG>'
            end
            return
Actually, there are only a few things to note about this procedure:

  • create procedure sp_authors_report

    sp_authors_report is the same name that we defined for the PROCEDURE value in sp_authors_form.

  • (@au_id varchar(11) = null)

    You want to make sure that the parameter names in your stored procedure match with the parameter names you declared on your form screen.

    I strongly suggest that you make all your parameters optional. You should check whether or not they are null and display an appropriate error message if this is not allowed. The error messages from Sybase (via Sybernet) may not be understood by your user; for example,

            if @au_id is null
            begin
                select '<H1>Error...</H1>'
                select "An author's ID is required!"
                return
            end
            ...
    
  • select '<BR>' + au_fname + ' ' + au_lname ... from pubs2..authors

    Here we actually select this information from Sybase and display the results in HTML.

    What appears to be a redundant <BR> tag will actually cause our result set to be separated by a blank line in case more than one author is found.

    This statement alone (if nothing else in this guide) should illustrate how powerful it is to get this information from Sybase as opposed to writing static HTML files.

  • if @@rowcount = 0

    If no row results were returned, display an appropriate message.

Even though our Report Screen procedure doesn't do much, this is where most of your work will be done. And although there are guide lines for the Form Screen procedure, there are none for the Report Screen. If you want to create sub-forms in this procedure, go right ahead. You can even have hypertext links that point to different areas on your report. You can call other stored procedures. Really, the possibilities are endless.


Registering your procedure:

The main menu screen is dynamically created each time a user logs on to Sybase with their Web browser. The sign-on screen on my home page instructs Sybernet to invoke the procedure SP_HTML_LOGIN if a successful connection is made. This procedure reads the HTTP database which contains a list of stored procedure names that it will use to create this menu. Before your form procedure can appear in this menu, however, it must be registered. And you must use your Web browser to do this! One of the menu options that you can select from the main menu screen is called
Stored Procedure Register. And yes, the Stored Procedure Register was registered the same way your procedure will be. Select this link to see the form screen for the Register program.

Here are the steps to register your Form Screen procedure:

  1. Log on to Sybase with your Web Browser.

  2. Click on the hypertext link for Stored Procedure Register.

  3. In the left-most frame is a list of all procedures in the current database. If you are not in the correct database, click the hypertext link at the top, and then select the database where your procedure resides.

  4. Click on your procedure name from the directory window.

  5. Enter Search Author's Table in the short description field.

  6. Leave the long description blank.

  7. Make sure the REGISTER radio is button is selected.

  8. Click on the SUBMIT button.

Your procedure is now registered! Go back to the main menu screen and run it. But remember, if you press the BACK button to return to the main menu, your procedure will not be there. This is the old menu that NetScape has been saving for you. Simply click on NetScape's Reload button, and your procedure will appear.


Formatting Result Sets:

FORMAT=NONE tells Sybernet to not perform any formatting on the result set of the stored procedure. This is not entirely correct because the NONE option attempts to format columnar data produced by a result set. This may or may not be what you desire. Column headings (if they exist) are suppressed, column widths are determined by the maximum of the column heading and column data, each column of a result set is separated by a single space, and numeric data is right-aligned.

FORMAT=SUPPRESSED tells Sybernet to stream all result set data without any formatting. The SUPPRESSED option may be a better choice when generating reports that use the PRE tag. It may also be a better choice because you do not have to remember any of the rules imposed by the NONE option. And although the results are usually identical, the SUPPRESSED option will usually generate less output than that produced by the NONE option.

Let's take a closer look at these two options and see how they compare. You decide which option you prefer to use.

Consider the following table:

        CREATE TABLE authors
        (
            au_id           varchar(11)         not null
        ,   au_lname        varchar(40)         not null
        ,   au_fname        varchar(20)         not null
        ,   phone           char(12)            not null
        ,   address         varchar(40)         null
        ,   city            varchar(20)         null
        ,   state           char(2)             null
        ,   country         varchar(12)         null
        ,   postalcode      char(10)            null
        )
When FORMAT=NONE and the result set is produced by the statements
        select '<PRE>' 
        select *, '<BR>' from pubs2..authors
        select '</PRE>'
The results is
        807-91-6654 Panteley                                 Sylvia               301 946-8853 1956 Arlington Pl.                       Rockville            MD    USA          20853     
        846-92-7186 Hunter                                   Sheryl               415 836-7128 3410 Blonde St.                          Palo Alto            CA    USA          94301     
When FORMAT=SUPPRESSED the result of this same expression is
        807-91-6654PanteleySylvia301 946-88531956 Arlington Pl.RockvilleMDUSA20853
        846-92-7186HunterSheryl415 836-71283410 Blonde St.Palo AltoCAUSA94301
These are very different results, but one of the reasons is that we are using the PRE tag, which the Web browser identifies as preformatted text.

In the first example (when FORMAT=NONE) notice that there is blank space between each column and numeric data is right-aligned. Also note that the column width for state is 5 positions: the actual column data is 2 positions, but the column heading takes up 5 positions. This may or may not be what you want, especially since no column heading is displayed when format is NONE.

One solution is to override the column heading name with the syntax provided for just this purpose. For example, if I wanted state to occupy exactly two positions on output, I could tell Sybase this by using the following statement:

        select state "ST" from pubs2..authors
A more elegant solution is to use the convert function. The convert function is primarily used for converting between data types, but you can also use it to truncate or expand column data. The same effect is achieved with the following statement:

        select convert(char(2),state) from pubs2..authors
The above rule does not apply when exactly one column is returned from your result set. In this case the results are identical to when FORMAT=SUPPRESSED.

When using the SUPPRESSED option, formatting is completely controlled by your stored procedure. Again, many times the interpretation of HTML makes these results identical. Here is how you might use the SUPPRESSED option to format your result set to resemble the results produced when format is NONE.

        select convert(char(11),au_id)
        ,      ' '
        ,      convert(char(40),au_lname)
        ,      ' '
        ,      convert(char(20),au_fname)
        ,      ' '
        ,      convert(char(12),phone)
        ,      ' '
        ,      convert(char(40),address)
        ,      ' '
        ,      convert(char(20),city)
        ,      ' '
        ,      convert(char(5),state)
        ,      ' '
        ,      convert(char(12),country)
        ,      ' '
        ,      convert(char(10),postalcode)
        ,      char(13)
        from   pubs2..authors
Another feature about the NONE and SUPPRESSED format options is that Sybernet does not put carriage-returns or line-feeds in the output. This is most desirable!

Ordered lists <OL> and Unordered lists <UL>, as you already know, separate each list item by preceding them with <LI>.

Here is how you would create an Unordered List from a result set:

        select '<UL>'
        select '<LI>', au_lname + ', ' + au_fname from pubs2..authors
        select '</UL>'
Select boxes precede each item with the <OPTION> tag. Here is how you might make one of those:

        select '<SELECT NAME="@name" SIZE=1>'
        select '<OPTION>', name from sysobjects where type = "P" order by name
        select '</SELECT>'
Textarea's are separated by carriage-returns (at the end of each line). Here is how to create a Textarea:

        select '<TEXTAREA ROWS=20 COLS=80>'
        select *, char(13) from pubs2..authors
        select '</TEXTAREA>'
Tables are slightly more complicated, but you can create some incredible results from just a few select statements. Here is how you might create a table:

        select '<TABLE BORDER=6 WIDTH=90%>'
        select '<TR>'
        select '<TH>soc-sec-num</TH>'
        select '<TH>Last Name</TH>'
        select '<TH>First Name</TH>'
        select '</TR>'
        select '<TR>'
        ,      '<TD>' + au_id + '</TD>'
        ,      '<TD>' + au_lname + '</TD>'
        ,      '<TD>' + au_fname + '</TD>'
        ,      '</TR>'
        from   pubs2..authors
        select '</TABLE>'
The Table tag is a good example where the results produced from FORMAT=NONE and FORMAT=SUPPRESSED are the same; however, while the result appears to be the same, the SUPPRESSED option generates less output.

And finally, an anomaly about string expressions whose lengths are calculated at run-time.

For some reason (and none that I know), Sybase sets the column heading of a string expression to 255 if that expression must be calculated at run-time. Consider the following example:

        declare @width int
        select @width = 10
        select '***'
        ,      space(@width)
        ,      '***'
When format is NONE, the result of these statements produce the following:

  • 3 asterisks
  • 1 space (because each column is separated by a single space)
  • 255 spaces (because the column heading's length is 255)
  • 1 space (between each column)
  • 3 asterisks
If space(10) were used in lieu of space(@width), the results would be as expected. When format is SUPPRESSED, this anomaly does not occur and the results are as follows:

  • 3 asterisks
  • 10 spaces
  • 3 asterisks
Note that no space is inserted between result set columns when format is SUPPRESSED.


Tips and Techniques

This section concentrates on the different ways you can combine HTML and stored procedures. You want to stop thinking of HTML as a static language (a simple text file), and concentrate on how you can go about using both to their best advantage. Even a simple greeting can be made flexible as illustrated in the following:
        if datepart(hour,getdate()) < 12
            select 'Good morning'
        else
        if datepart(hour,getdate()) < 18
            select 'Good afternoon'
        else
            select 'Good evening'
One thing you might want to think about is combining your form and report screens into one procedure. The obvious benefit is that all of your code and logic is concentrated in a single stored procedure.

        create procedure sp_authors(@au_id varchar(11) = NULL)
        as
            if @au_id is NULL
            begin
                select '<H1>Search Authors</H1>'
                select "Please enter author's ID in the space provided and press report."
                select '<P>'
                select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
                select 'Author: <INPUT TYPE="TEXT" NAME="au_id" VALUE="' + @au_id + '">'
                select '<P>'
                select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_authors">'
                select '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="NONE">'
                select '<INPUT TYPE="SUBMIT" VALUE="Report"><BR>'
                select '</FORM>'
            end else
            begin
                select '<BR>' + au_fname + ' ' + au_lname
                ,      '<BR>' + address
                ,      '<BR>' + city + ', ' + state + ' ' + postalcode
                ,      '<BR>'
                ,      '<BR>' + 'Phone: ' + phone
                from   pubs2..authors
                where  au_id = @au_id
                if @@rowcount = 0
                begin
                    select '<STRONG>'
                    select @au_id + ' not found!'
                    select '</STRONG>'
                end
            end
            return
The sp_authors_form and sp_authors_report procedures (from our tutorial) are now combined into a single procedure called sp_authors. If @au_id is null, we build the form, and if it's not null we build the report. Also notice that the PROCEDURE value is sp_authors so that we call our self when the user presses the Report Button.

The problem with this design is that the user must constantly click the BACK button if they want to generate another report for a different author. One solution is to always generate the form screen.

        create procedure sp_authors(@au_id varchar(11) = NULL)
        as
            select '<H1>Search Authors</H1>'
            select "Please enter author's ID in the space provided and press report."
            select '<P>'
            select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
            select 'Author: <INPUT TYPE="TEXT" NAME="au_id" VALUE="' + @au_id + '">'
            select '<P>'
            select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_authors">'
            select '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="NONE">'
            select '<INPUT TYPE="SUBMIT" VALUE="Report"><BR>'
            select '</FORM>'

            select '<BR>' + au_fname + ' ' + au_lname
            ,      '<BR>' + address
            ,      '<BR>' + city + ', ' + state + ' ' + postalcode
            ,      '<BR>'
            ,      '<BR>' + 'Phone: ' + phone
            from   pubs2..authors
            where  au_id = @au_id

            if @@rowcount = 0 and @au_id != NULL
            begin
                select '<STRONG>'
                select @au_id + ' not found!'
                select '</STRONG>'
            end
            return
Notice that the input field for au_id now contains the value of @au_id. For input fields of type TEXT (as in this example), the desired effect is achieved by simply including its value, but for input fields of type SELECT (where a list is displayed), you'll want to make sure that the current value of @au_id is selected and that its location in this list is where the user would expected it.

        select '<SELECT NAME="@au_id" SIZE=1>'
        if @au_id is null
        begin
            select '<OPTION>', au_id from pubs2..authors order by au_id
        end else
        begin
            select '<OPTION>', au_id from pubs2..authors where au_id < @au_id order by au_id
            select '<OPTION SELECTED>', @au_id
            select '<OPTION>', au_id from pubs2..authors where au_id > @au_id order by au_id
        end
        select '</SELECT>'
If @au_id is null, we simply retrieve the entire list of author ID's from the authors table and order them by au_id; otherwise, we first retrieve those IDs less than the value of @au_id, then the value for @au_id which we also indicate as SELECTED, then those IDs greater than the value of @au_id. And the whole thing is ordered, just as the user would want it to be!

Frames

Our last example for sp_authors solved one problem and introduced another. Although the user no longer has to press the BACK button to enter a new author ID, they may not like the fact that our form appears in the same window as the report, especially when they go to print it. Frames (available with NetScape 2.0 and above) can be used to rectify this problem.

Frames certainly add another level of complexity, but I will show how their use can be simplified by using stored procedures. One reading the documentation for Frames would be inclined to think they need several stored procedures: one for the basic layout and Frameset and one for each frame that you declare. And if you were not using stored procedures, this is the correct path (except the stored procedures would be static text files).

The following example expands on our original procedure sp_authors, but creates two frames: one for the form and one for the report. To do this, we've added another parameter to our stored procedure called @WINDOW. This parameter will be used to determine the state of our stored procedure. When it is null, we build the layout for our frames, and when it is not null, its value corresponds to the frame we are to build. Here we go.

    create procedure sp_authors (@WINDOW varchar(16) = null,@au_id varchar(11) = null)
    as
        if @WINDOW is NULL
        begin
            select '<HTML>'
            select '<HEAD>'
            select '<FRAMESET ROWS="30%,70%">'
            select '</HEAD>'
            select '<FRAME SRC="Sybernet.cgi$sp_authors?INPUTWINDOW" NAME="INPUTWINDOW">'
            select '<FRAME SRC="Sybernet.cgi$sp_authors?OUTPUTWINDOW" NAME="OUTPUTWINDOW">'
            select '</FRAMESET>'
            select '</BODY>'
            select '</HTML>'
            return
        end
        if @WINDOW = "INPUTWINDOW"
        begin
            select '<H1>Search Authors</H1>'
            select '<FORM METHOD="POST" ACTION="Sybernet.cgi" TARGET="OUTPUTWINDOW">'
            select '<INPUT TYPE="hidden" NAME="@WINDOW" VALUE="OUTPUTWINDOW">'
            select 'Author: <INPUT NAME="@au_id" VALUE="' + @au_id + '"><P>'
            select '<INPUT TYPE="hidden" NAME="procedure" VALUE="sp_authors">'
            select '<INPUT TYPE="hidden" NAME="format" VALUE="none">'
            select 'Press here: <INPUT TYPE="submit" VALUE="Report">'
            select '</FORM>'
            return
        end
        if @WINDOW = "OUTPUTWINDOW" and @au_id != NULL
        begin
            select '<BR>' + au_fname + ' ' + au_lname
            ,      '<BR>' + address
            ,      '<BR>' + city + ', ' + state + ' ' + postalcode
            ,      '<BR>'
            ,      '<BR>' + 'Phone: ' + phone
            from   pubs2..authors
            where  au_id = @au_id
            if @@rowcount = 0
            begin
                select '<STRONG>'
                select @au_id + ' not found!'
                select '</STRONG>'
            end
            return
        end
Notice the select statements for FRAME SRC when @WINDOW is NULL. In those we are saying that the source is this procedure (sp_authors) and we are passing to this procedure non-null values for @WINDOW. Those values are INPUTWINDOW and OUTPUTWINDOW. They are appended to the procedure name by preceding them with a question-mark. They happen to be the same name we also define for these frames.

The other change worth noting is to our FORM declaration. We've added the parameter @WINDOW as a hidden field and assigned it the value OUTPUTWINDOW. When the form is submitted, this value is passed to our procedure. The expression TARGET="OUTPUTWINDOW" which has been added to our FORM tag instructs NetScape to send the output results to the window named OUTPUTWINDOW.

I purposely declared @WINDOW before @au_id in our procedure heading so I could talk about it here. If you allow your procedure to be invoked with a URL request that specifies a default author ID, then you'll want to reverse this order (just to make the invocation as natural as possible); for example, a URL request of

            http://Sybernet.sri.com/Sybernet.cgi$sp_authors?846-92-7186
should generate the report for @au_id 846-92-7186. In this example (because it was not specified), @WINDOW will be null. We need to modify our frame source when we build the frames to pass the value for @au_id to our input and output windows.
            select '<FRAME SRC="Sybernet.cgi$sp_authors?'
            select '@au_id=%22' + @au_id + '%22%2C@WINDOW=%22INPUTWINDOW%22" '
            select 'NAME="INPUTWINDOW">'

            select '<FRAME SRC="Sybernet.cgi$sp_authors?'
            select '@au_id=%22' + @au_id + '%22%2C@WINDOW=%22OUTPUTWINDOW%22" '
            select 'NAME="OUTPUTWINDOW">'
You don't have to name each parameter as long as they are in the correct order, but I did so here to be as explicit as possible. Ideally, we should also make sure that NULL values for @au_id are NULL when they arrive at our frame. The expression '@au_id=%22' + @au_id + '%22' becomes @au_id="" when @au_id is null. Sybase interprets this as a space, which is neither null nor what we desire. One solution is to pass the keyword NULL as illustrated by the following:
            if @au_id is null
                select '@au_id=NULL'
            else
                select '@au_id=%22' + @au_id + '%22'
            select '%2C@WINDOW=%22OUTPUTWINDOW%22" '
The
Advanced Topics section of this guide explains how to pass multiple parameters to a stored procedure in greater detail. You will normally not need to construct such complicated expressions, but we are doing so here because our stored procedure can be invoked directly with a URL request.

Tables and Forms

Something else you might want to consider adding to your forms is making them look more like a form screen rather than just a bunch of badly misplaced input fields on your screen. This too adds still another layer of complexity, but I think the results are well worth the effort, and your users will probably thank you as well.

You can put input fields, radio buttons, check boxes and buttons in a table using the TABLE tag. In most cases, NetScape will make everything pretty, but you will probably need to do some tweaking to get everything just the way you want. Tweaking is beyond the scope of this manual, and I leave it to you the reader to explore this at your own leisure.

        select '<TABLE BORDER=6>'
        select '<FORM METHOD="POST" ACTION="Sybernet.cgi" TARGET="OUTPUTWINDOW">'
        select '<INPUT TYPE="hidden" NAME="@WINDOW" VALUE="OUTPUTWINDOW">'
        select '<INPUT TYPE="hidden" NAME="procedure" VALUE="sp_authors">'
        select '<INPUT TYPE="hidden" NAME="format" VALUE="none">'
        select '<TR><TH COLSPAN=2 ALIGN=CENTER><FONT SIZE=4>Search Authors</TH></TR>'
        select '<TR>'
        select '<TD>Author: <INPUT NAME="@au_id"><TD>'
        select '<TD><INPUT TYPE="SUBMIT" VALUE="Report"></TD>'
        select '</TR>'
        select '</FORM>'
        select '</TABLE>'
Never terminate your form inside of the <TD> and </TD> tags. This has the unpleasant effect of causing a break inside of the table data cell. Instead of
        select '<TD><INPUT TYPE="SUBMIT" VALUE="Report"></FORM></TD>'
do this
        select '<TD><INPUT TYPE="SUBMIT" VALUE="Report"></TD></FORM>'

Buttons

Since HTML allows more than one pair of form tags in a document, you can take advantage of this by adding "buttons" to your input frame. When an input field has a type of SUBMIT, a button is drawn which contains the label you specified in its VALUE clause.

        select '<INPUT TYPE="SUBMIT" VALUE="REPORT">'
We can create many buttons in this manner and make them do something if each is bound by corresponding FORM tags. Here is an example that adds a NEXT button to our frame:

        select '<FORM METHOD="POST" ACTION="Sybernet.cgi" TARGET="INPUTWINDOW">'
        if @au_id is null
            select @NEXT = min(au_id) from pubs2..authors
        else
            select @NEXT = min(au_id) from pubs2..authors where au_id > @au_id
        if @NEXT is null
            select @NEXT = min(au_id) from pubs2..authors        
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_authors">'
        select '<INPUT TYPE="HIDDEN" NAME="@WINDOW" VALUE="INPUTWINDOW">'
        select '<INPUT TYPE="HIDDEN" NAME="@au_id" VALUE="' + @NEXT + '">'
        select '<INPUT TYPE="SUBMIT" VALUE="&nbsp;NEXT&nbsp;">'
        select '</FORM>'
Notice that we are sending the results of this button to our INPUTWINDOW frame. The theory for this is that our INPUTWINDOW would grab the value for @au_id and display other useful information corresponding to that author ID, say their name. Our example does not, but hopefully you get the idea.

The evaluation for @NEXT is simply retrieving the next author ID from the authors table. If @au_id is null or the last author ID in this table, @NEXT is assigned to the first author ID. In other words, it wraps around to the beginning.

The VALUE for our SUBMIT button is &nbsp;NEXT&nbsp;. &nbsp; is an HTML construct that forces our browser to place a space there. This makes all of our buttons the same size, just in case you were wondering.

Tables are a good way to control exactly where on the screen your buttons appear, and if you don't want the border, just set it to zero. The following example creates a button on the far right of your screen:

        select '<TABLE BORDER=0 WIDTH=100%>'
        select '<TR>'
        select '<TD ALIGN=RIGHT>'
        select '<FORM>'
        select '<INPUT TYPE="SUBMIT" VALUE="REPORT">'
        select '</TD>'
        select '</FORM>'
        select '</TABLE>'
And remember, table cells can contain other tables. So, if you have an entire weekend to blow, you can probably create an input frame where every every button is just where you want them.

More Buttons

SUBMIT buttons can also be named, and that name can be passed to your stored procedure.
        select '<INPUT TYPE="SUBMIT" NAME="@BUTTON" VALUE="NEXT">'
        select '<INPUT TYPE="SUBMIT" NAME="@BUTTON" VALUE="SAVE">'
        select '<INPUT TYPE="SUBMIT" NAME="@BUTTON" VALUE="PREV">'
Your stored procedure not only gets the entire form that was submitted, but also the name of the button that your user clicked. This is much simpler to code for than our first example which defined an entire form to handle our NEXT button. In practice, you will find it necessary to use both methods.

Still More Buttons

JavaScript allows you to validate your form data on the client side before your form is sent to Sybase; for example, you may wish to validate an input field for numeric data before it is submitted. One of my favorite uses is illustrated by the following:
        select '<INPUT'
        +      ' TYPE="SUBMIT"'
        +      ' NAME="@BUTTON"'
        +      ' VALUE="DROP"'
        +      ' OnClick="return confirm(''Are you sure want to delete this entry?'')"'
        +      '>'
This button causes a JavaScript alert to be displayed on the user's web browser warning them that they are about to delete something. This makes our stored procedure simpler to write because we do not have to code for this when the form is submitted.

User Preferences and the Menu Bar

This section discusses how your stored procedure can invoke user defined preferences (such as background and foreground colors) and the menu bar that you see on some of the HTML pages. Both of these facilities are enabled by calling stored procedures.

The procedure that invokes user defined preferences is called sp_html_preferences. This procedure (if you use it) sets the <BODY> tag to invoke these preferences, so it must be called directly after the <TITLE> tag; for example,

        select '<HTML>'
        select '<TITLE>Hello World</TITLE>'
        exec http..sp_html_preferences
        ...
        select '</BODY>'
        select '</HTML>'
A new version of sp_html_preferences now allows an optional parameter to be passed that can set TABLE and FONT preferences. The parameter name is @TAG and can be either TABLE or FONT. The TABLE value creates the beginning table tag. You must supply the ending table tag as illustrated in the following example:
        exec http..sp_html_preferences "TABLE"
        select '<TR><TH>Hello!</TH></TR>'
        select '</TABLE>'
The FONT value to sp_html_preferences sets the user-define font size. Actually, this is done automatically when sp_html_preferences is called to generate the <BODY> tag, but there are places (like inside of a table) where the font size is ignored.

The procedure for creating the menu bar is called sp_html_menubar and can be called from anywhere within the <BODY> tag; for example,

        select '<HTML>'
        select '<TITLE>Hello World</TITLE>'
        select '<BODY>'
        ...
        exec http..sp_html_menubar
        ...
        select '</BODY>'
        select '</HTML>'
It is not required that you call these procedures, and in some cases (such as for reports) they may not be suitable.

Putting It All Together

Here is our finished product using frames to build the input and output windows, a table to build the form screen, and @au_id declared before @WINDOW so the procedure can be invoked with a URL request that specifies an author ID.

    create procedure sp_authors (@au_id varchar(11) = null,@WINDOW varchar(16) = null)
    as
        if @WINDOW is NULL
        begin
            select '<HTML>'
            select '<HEAD>'
            select '<FRAMESET ROWS="40%,60%">'
            select '</HEAD>'
            select '<FRAME SRC="Sybernet.cgi$sp_authors?'
            if @au_id is null
                select '@au_id=NULL'
            else
                select '@au_id=%22' + @au_id + '%22'
            select '%2C@WINDOW=%22INPUTWINDOW%22" '
            select 'NAME="INPUTWINDOW">'
            select '<FRAME SRC="Sybernet.cgi$sp_authors?'
            if @au_id is null
                select '@au_id=NULL'
            else
                select '@au_id=%22' + @au_id + '%22'
            select '%2C@WINDOW=%22OUTPUTWINDOW%22" '
            select 'NAME="OUTPUTWINDOW">'
            select '</FRAMESET>'
            select '</BODY>'
            select '</HTML>'
            return
        end
        if @WINDOW = "INPUTWINDOW"
        begin
            select '<HTML>'
            exec http..sp_html_preferences
            select '<TABLE BORDER=6>'
            select '<FORM METHOD="POST" ACTION="Sybernet.cgi" TARGET="OUTPUTWINDOW">'
            select '<INPUT TYPE="hidden" NAME="@WINDOW" VALUE="OUTPUTWINDOW">'
            select '<INPUT TYPE="hidden" NAME="procedure" VALUE="sp_authors">'
            select '<INPUT TYPE="hidden" NAME="format" VALUE="none">'
            select '<TR><TH COLSPAN=2 ALIGN=CENTER><FONT SIZE=4>Search Authors</TH></TR>'
            select '<TR>'
            select '<TD>Author: '
            select '<SELECT NAME="@au_id" SIZE=1>'
            if @au_id is null
            begin
                select '<OPTION>', au_id from pubs2..authors order by au_id
            end else
            begin
                select '<OPTION>', au_id from pubs2..authors where au_id < @au_id order by au_id
                select '<OPTION SELECTED>', @au_id
                select '<OPTION>', au_id from pubs2..authors where au_id > @au_id order by au_id
            end
            select '</SELECT>'
            select '</TD>'
            select '<TD><INPUT TYPE="SUBMIT" VALUE="Report"></TD>'
            select '</TR>'
            select '</FORM>'
            select '</TABLE>'
            exec http..sp_html_menubar
            select '</BODY>'
            select '</HTML>'
            return
        end
        if @WINDOW = "OUTPUTWINDOW"
        begin
            select '<HTML>'
            exec http..sp_html_preferences
            if @au_id != NULL
            begin
                select '<BR>' + au_fname + ' ' + au_lname
                ,      '<BR>' + address
                ,      '<BR>' + city + ', ' + state + ' ' + postalcode
                ,      '<BR>'
                ,      '<BR>' + 'Phone: ' + phone
                from   pubs2..authors
                where  au_id = @au_id
                if @@rowcount = 0
                begin
                    select '<STRONG>'
                    select @au_id + ' not found!'
                    select '</STRONG>'
                end
            end
            select '</BODY>'
            select '</HTML>'
            return
        end
The examples presented in this section were meant only as that, and hopefully I gave you some ideas on how to write your own stored procedures.

Advanced Form Input

When we think of the web, we naturally think of how easy it is to retrieve information with our browser. You certainly wouldn't use the web or HTML to create honest to goodness applications, now would you?

And of course this is what we shall do in this section. The example we are about to discuss creates a form screen for the authors table in the Pubs2 database. Buttons on this form will allow you to update or delete existing entries, add new entries, and scroll forwards and backwards through this table. Our concentration here will be how these buttons and this procedure interact. The actual display is left as simple as possible so that we don't get too hung up with the HTML part of this application, though that is very important too.

Our procedure heading will contain a parameter called @BUTTON which tells us which function the user is requesting. The remaining parameters correspond to the column names in the authors table.

    create procedure sp_html_authors
    (
        @BUTTON         varchar(12)         = null
    ,   @au_id          varchar(11)         = null
    ,   @au_lname       varchar(40)         = null
    ,   @au_fname       varchar(20)         = null
    ,   @phone          char(12)            = null
    ,   @address        varchar(40)         = null
    ,   @city           varchar(20)         = null
    ,   @state          char(20)            = null
    ,   @country        varchar(12)         = null
    ,   @postalcode     varchar(10)         = null
    )
    as
The column names are passed as parameters and not declared as local variables because these values are going to be passed to this procedure when the form is submitted. If they were declared locally, we could not capture any changes the user might have made. This is an important point.

As the procedure starts to execute the first thing we want to do is check the value of our @BUTTON. You might possibly think we should build our form screen first, but this is not correct. If the value for our button is to delete a row, it wouldn't make any sense showing this row first and then deleting it. A more natural approach is to delete it first and then (possibly) show the next row in the table.

Our button will be NULL when the procedure is first executed. Detect this condition and set it to something that makes more sense: perhaps retrieving the first row in the table?

    if @BUTTON is NULL
        /*
        ** Just display the first entry in the list if there is one.
        */
        select @BUTTON = "NEXT"
If the button value is DROP, we can go ahead and delete this row, but change the button value (again) to something that makes sense.

    if @BUTTON = "DROP"
    begin
        delete pubs2..authors
        where  au_id = @au_id
        select @BUTTON = "NEXT"
    end
If the button value is SAVE, we can go ahead and save this information; however, we don't know whether or not we are suppose to update an existing row or insert a new row into our table. This is easily determined by asking Sybase.
    if @BUTTON = "SAVE"
    begin
        /*
        ** Is this an update or insert?
        */
        if exists(select * from pubs2..authors where au_id = @au_id)
        begin
            update  pubs2..authors
            set     au_lname        = @au_lname
            ,       au_fname        = @au_fname
            ,       phone           = @phone
            ,       address         = @address
            ,       city            = @city
            ,       state           = @state
            ,       postalcode      = @postalcode
            ,       country         = @country
            where   au_id = @au_id
        end else
        begin
            insert  pubs2..authors
            (       au_id
            ,       au_lname
            ,       au_fname
            ,       phone
            ,       address
            ,       city
            ,       state
            ,       postalcode
            ,       country
            )       
            values
            (       @au_id
            ,       @au_lname
            ,       @au_fname
            ,       @phone
            ,       @address
            ,       @city
            ,       @state
            ,       @postalcode
            ,       @country
            )
            select @BUTTON = "NEW"
        end
    end
Notice that when a row is inserted we change the button value to NEW. We could just as easily retrieve this new row from Sybase by leaving the button alone. In fact, this is a good idea if other columns in a table are filled in automatically when the row is inserted, perhaps by a trigger.

Now we want to handle the case when our button is either NEXT or PREV.

    if @BUTTON = "NEXT"
    begin
        select @au_id = min(au_id)
        from   pubs2..authors
        where  au_id > @au_id
        
        if @au_id is NULL
        select @au_id = min(au_id)
        from   pubs2..authors
    end   
    if @BUTTON = "PREV"
    begin
        select @au_id = max(au_id)
        from   pubs2..authors
        where  au_id < @au_id
        
        if @au_id is NULL
        select @au_id = max(au_id)
        from   pubs2..authors
    end
If the button's value is NEW, we want to set the parameters to NULL or give them a default value. If it's anything else, we want to retrieve this information from Sybase.

    if @BUTTON = "NEW"
    begin
        select @au_id       = null
        select @au_lname    = null
        select @au_fname    = null
        select @phone       = null
        select @address     = null
        select @city        = null
        select @state       = "CA"
        select @postalcode  = null
        select @country     = "USA"
    end else    
    begin
        select @au_lname    = au_lname
        ,      @au_fname    = au_fname
        ,      @phone       = phone
        ,      @address     = address
        ,      @city        = city
        ,      @state       = state
        ,      @postalcode  = postalcode
        ,      @country     = country
        from   pubs2..authors
        where  au_id = @au_id
    end
Finally, we are ready to build our screen. By this time we've handled all cases for our button, even the case when our button was NULL.

    select '<HTML>'
    select '<HEAD><TITLE>authors</TITLE></HEAD>'
    select '<BODY>'
    select '<H2 ALIGN=CENTER>Pubs2 Authors</H2>'
    /*
    ** Start the form.
    */
    select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
    select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_html_authors">'
    select '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="SUPPRESSED">'
    /*
    ** Nothing fancy here, just our input fields with a simple label.
    */
    select '<INPUT TYPE="TEXT" NAME="au_id"      VALUE="' + @au_id      + '" SIZE=20 MAXLENGTH=20>au_id<BR>'
    select '<INPUT TYPE="TEXT" NAME="au_lname"   VALUE="' + @au_lname   + '" SIZE=20 MAXLENGTH=40>au_lname<BR>'
    select '<INPUT TYPE="TEXT" NAME="au_fname"   VALUE="' + @au_fname   + '" SIZE=20 MAXLENGTH=20>au_fname<BR>'
    select '<INPUT TYPE="TEXT" NAME="phone"      VALUE="' + @phone      + '" SIZE=12 MAXLENGTH=12>phone<BR>'
    select '<INPUT TYPE="TEXT" NAME="address"    VALUE="' + @address    + '" SIZE=20 MAXLENGTH=40>address<BR>'
    select '<INPUT TYPE="TEXT" NAME="city"       VALUE="' + @city       + '" SIZE=20 MAXLENGTH=20>city'
    select '<INPUT TYPE="TEXT" NAME="state"      VALUE="' + @state      + '" SIZE= 2 MAXLENGTH= 2>state'
    select '<INPUT TYPE="TEXT" NAME="postalcode" VALUE="' + @postalcode + '" SIZE= 5 MAXLENGTH=10>postalcode<BR>'
    select '<INPUT TYPE="TEXT" NAME="country"    VALUE="' + @country    + '" SIZE=12 MAXLENGTH=12>country<P>'    
And now the buttons. This is about as simple as it's going to get. Too simple, in fact. Ideally, we should add some JavaScript to these buttons so that the user doesn't accidentally change something they don't mean to change; for example, a JavaScript confirm on the DROP button is a good idea here.

    select '<INPUT TYPE="SUBMIT" NAME="BUTTON" VALUE="SAVE">'
    select '<INPUT TYPE="SUBMIT" NAME="BUTTON" VALUE="DROP">'
    select '<INPUT TYPE="SUBMIT" NAME="BUTTON" VALUE="NEXT">'
    select '<INPUT TYPE="SUBMIT" NAME="BUTTON" VALUE="PREV">'
    select '<INPUT TYPE="SUBMIT" NAME="BUTTON" VALUE="NEW">'
    /*
    ** Finish our form and HTML.
    */
    select '</FORM>'
    select '</BODY>'
    select '</HTML>'
    return
There are several things missing from this application that you will definitely want to add. The application should check @@error after any table changes to make sure it worked. You would also want to add code to make sure that a new entry doesn't actually update an existing entry. This is possible because we allowed the user to enter any au_id. Since au_id is NULL after the user clicks the NEW button, I could change the button value for SAVE to something like CREATE. I would then know whether the user was creating or updating a row in this table.

You might also think the code for implementing the NEXT and PREV button's is quite clever. Hardly! In fact I usually try to stay away from the aggregate functions, but this is not always possible.

What are we suppose to do when multiple columns form our key? How do we make these buttons perform then? The answer has nothing to do with HTML or Sybernet, but it does rely on Sybase to do the work. With the proper WHERE condition, ORDER BY, and some way to limit the number of rows returned, this can be accomplished easily.

We can control the number of rows returned using a cursor. The examples here, however, will use the SET ROWCOUNT statement to accomplish this.

The code for one key column is obvious. Let's assume that our key columns is called KEY1. Here is the code for our NEXT button with one key column:

    set rowcount 1
    select @KEY1 = KEY1
    from   authors
    where  KEY1 > @KEY1
    order  by KEY1
    set rowcount 0
Here is the code for two key columns:

    set rowcount 1
    select @KEY1 = KEY1
    ,      @KEY2 = KEY2
    from   authors
    where  KEY1 = @KEY1 and KEY2 > @KEY2
    or     KEY1 > @KEY1
    order  by KEY1, KEY2
    set rowcount 0
And here is the code for three key columns:

    set rowcount 1
    select @KEY1 = KEY1
    ,      @KEY2 = KEY2
    ,      @KEY3 = KEY3
    from   authors
    where  KEY1 = @KEY1 and KEY2 = @KEY2 and KEY3 > @KEY3
    or     KEY1 = @KEY1 and KEY2 > @KEY2
    or     KEY1 > @KEY1
    order  by KEY1, KEY2, KEY3
    set rowcount 0
There's a pattern to this madness which hopefully you see. The BACK button is almost identical except we test for less than instead of greater than and order the keys columns in a descending fashion. Here is the BACK button for three key columns:

    set rowcount 1
    select @KEY1 = KEY1
    ,      @KEY2 = KEY2
    ,      @KEY3 = KEY3
    from   authors
    where  KEY1 = @KEY1 and KEY2 = @KEY2 and KEY3 < @KEY3
    or     KEY1 = @KEY1 and KEY2 < @KEY2
    or     KEY1 < @KEY1
    order  by KEY1 desc, KEY2 desc, KEY3 desc
    set rowcount 0
Make sure you check @@rowcount. @@rowcount tells you whether or not there were any more rows found. Eventually you'll either reach the end or beginning of your table. You might want display an alert or just wrap around to the beginning again.

Now this is clever, but unfortunately it does not work when our key columns can be null. In that case we can easily account for nulls with the ISNULL function, but its use is not so straight-forward because you will need to be familiar with your data to use it.

Suppose KEY3 is an int that can be null, but will never have a real value less than 1. The code for our NEXT button for this case is as follows:

    set rowcount 1
    select @KEY1 = KEY1
    ,      @KEY2 = KEY2
    ,      @KEY3 = KEY3
    from   authors
    where  KEY1 = @KEY1 and KEY2 = @KEY2 and isnull(KEY3,0) > isnull(@KEY3,0)
    or     KEY1 = @KEY1 and KEY2 > @KEY2
    or     KEY1 > @KEY1
    order  by KEY1, KEY2, KEY3
    set rowcount 0
Notice we ISNULL both the column and our local variable. This is most subtle, but it is correct since @KEY3 can be null.

Form Input and Line Items

The example above is typical of how a stored procedure combined with HTML can be used for form input. When dealing with a single row, the code to do this is fairly straight forward, but dealing with an unknown number of rows (such a line items) can be very difficult since parameter names to Sybase stored procedures must be unique.

This limitation--in a moment we will conclude there is no such limit--usually means we append a unique number to each column of each row that is passed to our procedure as well as impose a limit on how many rows we are actually going to handle in our form. Consider the following declaration:

    create table children
    (
        surname                   varchar(30)
    ,   givenname                 varchar(30)
    ,   birthdate                 datetime
    )
The theory here is that a parent (based on surname) can have zero or more children. Our goal is to create a form screen that allows any number of children to be edited. We might also want the ability to add additional children should the need arise.

I'm certainly not going to write code for each child, though one would be inclined to think this is required. Instead, I would write another procedure that would handle the update or insert of each child separately.

    create procedure update_child
    (
        @surname                  varchar(30)
    ,   @givenname                varchar(30)
    ,   @birthdate                datetime
    )
    as
        if @surname is null
            /*
            ** nothing to update.
            */
            return
        if exists(select * from children where surname = @surname and givenname = @givenname)
            /*
            ** update the birthdate.
            */
            update children
            set    birthdate = @birthdate
            where  surname = @surname and givenname = @givenname
        else
            /*
            ** It's a new arrival.
            */
            insert children
            (
                surname
            ,   givenname
            ,   birthdate
            )
            values
            (
                @surname
            ,   @givenname
            ,   @birthdate
            )
        return
You might image that the procedure that creates our form is a bit on the clumsy side, and you would be correct. Since there is a limit to the number of parameters I can pass to a stored procedure, I would be restricted to the number of rows I could edit. I might even be compelled to limit the number of rows simply because each parameter name must be unique. Ten seems a reasonable number, but for the sake of brevity we will limit the number of children we can edit to four.

Here is our first attempt at writing this procedure:

    create procedure update_child_form
    (
        @surname            varchar(30) = "Workman"

    ,   @surname_1          varchar(30) = NULL -- child 1.
    ,   @givenname_1        varchar(30) = NULL -- child 1.
    ,   @birthdate_1        datetime    = NULL -- child 1.
    ,   @surname_2          varchar(30) = NULL -- child 2.
    ,   @givenname_2        varchar(30) = NULL -- child 2.
    ,   @birthdate_2        datetime    = NULL -- child 2.
    ,   @surname_3          varchar(30) = NULL -- child 3.
    ,   @givenname_3        varchar(30) = NULL -- child 3.
    ,   @birthdate_3        datetime    = NULL -- child 3.
    ,   @surname_4          varchar(30) = NULL -- child 4.
    ,   @givenname_4        varchar(30) = NULL -- child 4.
    ,   @birthdate_4        datetime    = NULL -- child 4.
    )   
    as
    declare
        @givenname          varchar(30)
    ,   @birthdate          datetime
    ,   @number_of_children int
We probably need a cursor to step through our table so we can not only append a unique number to each variable name, but also limit the number of children displayed.

    declare child cursor for
        select givenname
        ,      birthdate
        from   children
        where  surname = @surname
        order  by birthdate

        select @number_of_children = 0
Since our procedure is self contained--it calls itself--and our update procedure can handle null values, here is a good time to call that procedure.

        exec update_child

            @surname = @surname_1
        ,   @givenname = @givenname_1
        ,   @birthdate = @birthdate_1

        exec update_child

            @surname = @surname_2
        ,   @givenname = @givenname_2
        ,   @birthdate = @birthdate_2

        exec update_child

            @surname = @surname_3
        ,   @givenname = @givenname_3
        ,   @birthdate = @birthdate_3

        exec update_child

            @surname = @surname_4
        ,   @givenname = @givenname_4
        ,   @birthdate = @birthdate_4
Fairly straight-forward, but can you now image what this procedure would look like if I were to allow 10 or more editable rows?

Now we can start building the form.

        select '<HTML>'
        ,      '<HEAD><TITLE>Children</TITLE></HEAD>'
        ,      '<BODY>'

        select '<FORM ACTION="POST" ACTION="Sybernet.cgi">
        ,      '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="update_child_form">'
        ,      '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="SUPPRESSED">'

Here is where it gets ugly. Fortunately, our cursor is going to do the dirty work, but creating unique names just so we can edit more than one row is very annoying.

Consider too that if we also included JavaScript to validate this form before it is submitted, we would have to deal with these unique names as well.

        open child
        fetch child into @givenname, @birthdate
        while @@sqlstatus = 0 and @number_of_children < 4
        begin
            select '<INPUT TYPE="TEXT" NAME="surname_'
            ,      @number_of_children
            ,      '" VALUE="'
            ,      @surname
            ,      '">'
            ,      '<INPUT TYPE="TEXT" NAME="givenname_'
            ,      @number_of_children
            ,      '" VALUE="'
            ,      @givenname
            ,      '">'
            ,      '<INPUT TYPE="TEXT" NAME="birthdate_'
            ,      @number_of_children
            ,      '" VALUE="'
            ,      @birthdate
            ,      '"><BR>'
            fetch child into @givenname, @birthdate
            select @number_of_children = @number_of_children + 1
        end
        close child
        deallocate cursor child

        select '<INPUT TYPE="SUBMIT" VALUE="Submit">'

        select '</FORM>'
        ,      '</BODY>'
        ,      '</HTML>'
        return
As you can see none of this is very clean except for our update_child procedure which is concerned only with updating a single child. And to account for those prolific parents having more than 4 children I may have to throw in a NEXT button to handle the next batch.

This was a lot of work to show you how this used to be accomplished. With Sybernet version 1.73 and above the ability to handle line items has been greatly improved by allowing a single form screen to call (or recurse) on more than one stored procedure.

In particular, when our form is submitted, what we'd like to have happen is to call update_child automatically for each child on our form, and to do so all you need to do is assign multiple invocations to that procedure with the Sybernet reserved word PROCEDURE. Sybernet now detects multiple invocations and builds the batch accordingly. In fact, you are not limited to just procedure calls: any Transact-SQL statement is valid for the PROCEDURE value.

Here is how our procedure looks now that we can call a procedure multiple times in the same form:

    create procedure update_child_form
    (
        @surname varchar(30) = "Workman"
    )
    as
        select '<HTML>'
        ,      '<HEAD><TITLE>Update Children</TITLE></HEAD>'
        ,      '<BODY>'

        select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
  
        select '<TABLE BORDER=0>'
        ,      '<TR>'
        ,      '<TH>Surname</TH>'
        ,      '<TH>Givenname</TH>'
        ,      '<TH>Birthdate</TH>'
        ,      '</TR>'      
        select '<TR>'
        ,      '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="update_child">'
        ,      '<TD>'
        ,      '<INPUT TYPE="TEXT" NAME="surname" VALUE="'
        ,      surname
        ,      '"></TD><TD>'
        ,      '<INPUT TYPE="TEXT" NAME="givenname" VALUE="'
        ,      givenname
        ,      '"></TD><TD>'
        ,      '<INPUT TYPE="TEXT" NAME="birthdate" VALUE="'
        ,      birthdate
        ,      '"></TD></TR>'
        from   children
        where  surname = @surname
        order  by birthdate

        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="update_child_form">'
        ,      '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="SUPPRESSED">'
        ,      '<INPUT TYPE="HIDDEN" NAME="surname" VALUE="' + @surname + '">'
        ,      '</TABLE>'
        ,      '<INPUT TYPE="SUBMIT" VALUE="Submit">'
        ,      '</FORM>'
        ,      '</BODY>'
        ,      '</HTML>'
        return
Everything is nicely aligned in a table, and I don't care how many children you have. The procedure turns around and calls itself as the very last thing since our update procedure is not normally going to generate a result set.

It's a very elegant solution to a very nasty problem. My only regret is that it is a solution I did not think of myself.


Sending Partial Result Sets

It is now possible to send partial result sets through the Sybernet CGI using WebStar's Send Partial event. In essence, this means stored procedures can now use the facility known as "Server Push" which is supported by NetScape version 1.1 and above. A description of Server Push and Client Pull is described in the document
An Exploration of Dynamic Documents.

This change means that Sybernet now runs as an asynchronous CGI application (though its name remains unchanged). I suspect we will have to change this name in the future (to add the acgi extension), but for now we will stick with the old name.

To invoke the Send Partial facility, simply return the text <SEND_PARTIAL> . If this 14 character string is matched exactly (it is case sensitive), Sybernet flushes the current result set output to WebStar who then flushes it to the client while keeping the connection open. Here are some examples that will work:

        select "<SEND_PARTIAL>"
        select "</TABLE>, "<SEND_PARTIAL>", "<TABLE>"
Here are some examples that won't work:
        select "<send_partial>"
        select "</TABLE>" + "<SEND_PARTIAL>" + "<TABLE>"
In the last example, you cannot embed this string within another string.

TIP: If your browser is timing out because your stored procedure is not returning results fast enough, issuing a <SEND_PARTIAL> could alleviate this. This option, however, is not an excuse for writing poorly designed or inefficient stored procedures.


Handling Text and Image Data Types

This section describes how stored procedures can be used to store and retrieve BLOB's (Binary Large OBjects) into and from a Sybase database. In Sybase, a BLOB is a data type of TEXT or IMAGE. The sections that follow will discuss how BLOB's are selected (this won't hurt), how to store them (this will hurt a little), and how to write a stored procedure that will retrieve and display a real image.

Selecting Text and Image Data.

The retrieval of BLOB's from a stored procedure is very easy because all you have to do is select that column the same way as you would select any column type; however, BLOB's that occur in a multi-column selection must occur after the last non-BLOB data type. Consider the following table declaration:
        create table template
        (   id              int         not null
        ,   status          char(6)     null
        ,   blob            text        null
        ,   picture         image       null
        )
Here are some examples of selections that are valid:
        select id, blob from template where id = 14568
        select blob from template
        select status, id, blob from template
Here is an example of a selection that is invalid:
        select blob, id from template where id = 14568
In this last and invalid example if you must select the blob column before the id column, you want to break your select into two separate statements; for example,
        select blob from template where id = 14568
        select id from template where id = 14568
is a legal way to select a BLOB type before a non-BLOB type.

Storing Text and Image Data

Sybernet now handles the ability to insert data into a table directly. A stored procedure can perform this task, but stored procedures are limited by their inability of accepting parameters whose length is greater than 255 characters. For non-BLOB data types stored procedures work very well; however, for data types of TEXT and IMAGE you can use the Transact-SQL INSERT statement which can accept up to 100K bytes of data for each column.

The following example illustrates an HTML form page that will generate an INSERT statement. Sybernet determines this by interrogating the PROCEDURE value for your submitted form:

    create procedure sp_html_blob_form (@id int = null)
    as
        select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
        select '<INPUT TYPE="hidden" NAME="procedure" VALUE="insert template">'
        select '<INPUT TYPE="hidden" NAME="format" VALUE="SUPPRESSED">'
        select '<INPUT TYPE="radio" NAME="status" VALUE="update">Update'
        select '<INPUT TYPE="radio" NAME="status" VALUE="delete">Delete'
        select '<INPUT TYPE="radio" NAME="status" VALUE="select">Select'
        select 'What name: <INPUT NAME="id">'
        select 'What text: <TEXTAREA NAME="blob" ROWS=10 COLS=80>'
        select blob from template where id = @id
        select '</TEXTAREA>'
        select 'Press here to submit form: '
        select '<INPUT TYPE="submit" VALUE="GO"><P>'
        return
The problem that exists with this method is that an insert statement does not generate a result set. How do you intercept the insertion and generate an HTML page for the user? The answer, of course, is a trigger. Triggers (if they exist) are fired automatically by the Sybase SQL Server whenever a row is inserted, deleted or updated on a table. A simple (if inelegant) trigger that might be defined for the above table might resemble the following:
    create trigger sp_html_blob_trigger on template for insert
    as
        select '<H1>Thank you for your input!</H1>
        return
This is not very exciting and the results are unimpressive; however, this simple example does illustrate how an insert statement combined with a trigger can generate a response.

Here is an example of a slightly more sophisticated trigger that can determine if the user is updating, deleting, or selecting a blob:

    create trigger sp_html_blob_trigger on template for insert
    as
    declare
        @id int
    ,   @status char(6)

        select @id = id, @status = status from inserted
        if @status = "update"
        begin
            delete template where id = @id and status = null
            update template set status = null where id = @id and status = "update"
        end else
        if @status = "delete"
        begin
            rollback transaction
            delete template where id = @id and status = null
        end else
            rollback transaction
        exec sp_html_blob_form @id
        return
This example assumes that permanent rows are stored with a null value in their status column. When an update occurs, the row corresponding to this id and whose status column is null is deleted; the row is then updated to set the status field to null. When a delete occurs, the transaction is rolled back (to delete the record just inserted) and the permanent row (if it exists) is also deleted. The select option simply rolls back the current transaction so that we don't store the row just created by this insert. And finally, a new form page is created by calling sp_html_blob_form.

Yes, that was quite a bit of code to swallow. Give it a second look to see what's really going on and I think you'll appreciate it as much as I. And remember, it's only an example of how you might go about creating your own trigger.

Retrieving Images

Ah, now we've come to the fun part. I've already explained that all you have to do to retrieve an image or text data type is to select it. But how do you retrieve an image from Sybase and have it appear as a real picture? Selecting it alone will not do the trick because the default Content-type is HTML. Selecting an image in this mode will simply produce a stream of funny looking characters on your screen. Here is an example of a stored procedure that will override the default Content-type and produce a real image from the above table:
    create procedure sp_html_picture (@id char(5) = null)
    as
    declare
        @CRLF char(2)
        select @CRLF = char(13)+char(10)	
        select "HTTP/1.0 200 OK"
        select @CRLF	
        select "Server: WebSTAR/1.2 Sybernet/CGI"
        select @CRLF	
        select "MIME-Version: 1.0"
        select @CRLF	
        select "Content-Length: "
        select datalength(picture) from template where id = @id
        select @CRLF	
        select "Content-type: image/gif"
        select @CRLF
        select @CRLF
        select picture from template where id = @id
        return
I won't go into the details of this procedure, but bear in mind this stored procedure cannot be mixed with other Content-types. Suffice it to say that it will generate the desired GIF from the table template, but you won't be able to bound this image with HTML directly. Read on, read on.

An image retrieved from a stored procedure can be bound by HTML in the usual way. Here is an example that does just that:

    select '<H3>This is your picture</H3>'
    select '<IMG SRC="Sybernet.cgi$sp_html_picture?' + @id + '">'
    select 'There it is!<P>'
In this example we are declaring our image in the usual way; however, instead of a filename that has the suffix GIF, we are invoking the stored procedure sp_html_picture and passing it @ID as the parameter. Pretty cool or what?


Advanced Topics

This section discusses some of the more advanced features that are available with Sybernet. In it we will discuss such things as URL's (and how they are used in this environment), redirection, and image maps. All of which can be performed with Sybase stored procedures. You'll want to check this section regularly as new features are added.

URL's

A URL (Universal Resource Locator) tells a Web browser where and how to fetch some information. I don't plan to describe all of its flavors (primarily because I am not capable of doing so), but I do want to describe how URL's relate to this environment and how you can use them to your advantage. Of the many forms of URL's that exist, the one you are most familiar with is the HTTP URL. The general syntax of an HTTP URL that invokes the Sybernet CGI is described by the following:
        http://Sybernet.sri.com/Sybernet.cgi$Direct Argument?Search Argument
Sybernet.sri.com represents the IP address or host where this information is found. Sybernet.cgi indicates the name of the CGI application. The Direct Argument (if specified) represents the name of the Sybase stored procedure that is to be executed (sans one special name), and the Search Argument (if specified) is interpreted as a single (unnamed) string parameter or a combination of named and unnamed parameters to that procedure. Consider the following URL:

        http://Sybernet.sri.com/Sybernet.cgi$sp_html_property?ORG,024
This example instructs the Web browser to invoke the CGI called Sybernet.cgi at Sybernet.sri.com and execute the procedure sp_html_property and pass the string parameter "ORG,024."

If the aforementioned URL is specified, the Sybernet.cgi will attempt to execute this procedure. If the user has not connected, they are automatically connected as "guest." If the invocation of this procedure does not cause a security error, it is executed and the string "ORG,024" is passed as a parameter. In this case it does not matter what the parameter name to this procedure is called, but it is important (at least in this example because one is specified) that a parameter is declared.

URL's and URI Encoding

Unfortunately, some characters in the Direct and Search arguments are not allowed unless they are URI (Universal Resource Identifier) encoded. This encoding is not unique to Sybernet. In fact, Sybernet has little to do with all of this until your extra path information is actually received and decoded. Getting that string to Sybernet, however, is going to make you scratch your head wondering why things aren't working.

Let's start with the basics of URI encoding. Anything in the Direct or Search argument that is not a letter, digit, or underscore should probably be encoded. URI Encoding simply replaces that character with a percent sign followed by its two character Hexadecimal representation; for example, the Hexadecimal value for an Ascii [double] quote is 22. The URI Encoding of an Ascii quote is therefore "%22."

Although spaces can be represented by %20 (since a space is denoted by a Hexadecimal 20), a plus-sign can be used instead. So, The URI encoding of the string

"Apples and Oranges"

becomes

%22Apples+and+Oranges%22

Your browser would complain vehementally at the former and gladly pass the latter to your web server. There are other exceptions as to which characters need to be encoded, by the way. Commas and at-signs (we use those a lot in stored procedures) can safely be left as they are, but encoding won't hurt here either.

We can see an example of this in action by entering the following in the Site Location of your browser:

http://Sybernet.sri.com/Sybernet.cgi$select @@servername

This won't work because the space was not encoded. Change it to

http://Sybernet.sri.com/Sybernet.cgi$select+@@servername

and you will see how URI encoding works.

There are lots of ways to URI encode your parameters to Sybase, but not until version 1.87 of Sybernet has there been a safe or easy way to do this that would work on database columns. You could call a stored procedure to do the encoding, but the maximum string length that can be passed to a stored procedure is 255 characters as you already know.

Version 1.87 of Sybernet introduced a new tag that will do the encoding automatically for you. URI Encoding is turned on with the <URLEncode> tag, and URI encoding is turned of with the </URLEncode> tag. This case-sensitive tag is unique to Sybernet and will not work in normal HTML files. It may only be used with FORMAT=SUPPRESSED.

Here is an example that creates a list of hyper-text links that invokes a stored procedure which passes au_fname and au_lname from the authors table in pubs2. It is extremely important to encode these name in case there are special characters in someone's name:

    select '<BR>'
    ,      '<A HREF="Sybernet.cgi$sp_html_list?'
    ,      '<URLEncode>'                            -- Turn on Encoding
    ,      '@au_fname="'
    ,      au_fname
    ,      '"'
    ,      ','
    ,      '@au_lname="'
    ,      au_lname
    ,      '"'
    ,      '</URLEncode>'                          -- Turn off Encoding
    ,      '">'
    ,      au_fname + ' ' + au_lname
    ,      '</A>'
    from   pubs2..authors
Notice that au_fname and au_lname are enclosed in quotes because we are passing strings to our stored procedure. The quotes conflict with the quotes that bound our HREF string and are thus URI Encoded. The end result after URI decoding is something that looks like this:

    exec sp_html_list @au_fname="Michelle", @au_lname="O'Leary"
As mentioned, the <URLEncode> and </URLEncode> tags are case-sensitive. They may not be embedded inside of other strings; for example,
    select '<URLEncode>This will not work!</URLEncode>'
but
    select '<URLEncode>'
    ,      'This will work!'
    ,      '</URLEncode>'
Version 1.87 also introduced two additional tags to aid you in creating other string expressions that require some form of encoding. They are the <JSEncode> and <HTMLEncode> tags. They are similarly disabled by including a forward slash in the usual manner.

The JSEncode Tag

<JSEncode> encodes a JavaScript string. In a JavaScript string, for example, a line-feed or carriage-return must be escaped. <JSEncode> encodes every character that requires escaping in a JavaScript string including forward slashes. Forward slashes are escaped because there is one particular phrase that JavaScript would be confused by. Here is an example that assigns a text field in Sybase to a JavaScript variable:

    select '<SCRIPT>'
    ,      'var s="'
    ,      '<JSEncode>'
    select blob from somewhere
    select '</JSEncode>'
    ,      '";'
    ,      '</SCRIPT>'
I'm not suggesting that this string expression is not going to be too large for your browser, but I am suggesting that the string will be correct and you will not get a JavaScript error!

Notice too that the blob was selected separately. Blobs must be the last column of a multi-column select statement, so I just put it on a line by itself.

The HTMLEncode Tag

<HTMLEncode> encodes an HTML string. Normally you don't need to worry about such a function because you are hopefully already familiar with the data you are sending to the web, but consider the following example:

    select '<TR>'
    ,      '<TD>'
    ,      au_lname
    ,      '</TD>'
    ,      '<TD>'
    ,      address
    ,      '</TD>'
    ,      '</TR>'
    from   pubs2..authors
What could possibly go wrong with this? Nothing as long as au_lname and address do not contain a less-than sign. An address of
    1234 Elm Street <apt 4
would display only 1234 Elm Street. Like I said, you know what your data looks like and you don't want to start embedding the <HTMLEncode> tag everytime you select data from a column, but for those cases where this is required, this tag will be most handy. So, if address is a real candidate for this option, this is how your select statement would look.

    select '<TR>'
    ,      '<TD>'
    ,      au_lname
    ,      '</TD>'
    ,      '<TD>'
    ,      '<HTMLEncode>'
    ,      address
    ,      '</HTMLEncode>'
    ,      '</TD>'
    ,      '</TR>'
    from   pubs2..authors
Finally, all of these special encoding tags may be nested; however, only encoding for the deepest tag will be performed. If you attempt to turn off encoding that is not active, the tag is ignored, subject to any encoding that might be taking place.

Passing parameters using the Search Argument

As I mentioned, parameters in the Search argument may be named, and you do this exactly like you would pass named parameters to a Sybase stored procedure. In fact, this facility allows you to pass numeric and hexadecimal constants in the usual way, but non-numeric data will be explicitly quoted by Sybernet. It is recommended, therefore, that you quote non-numeric data yourself to avoid any confusion. A Search argument of
        @number=123, @mask=0xff, @option=null, @name=Hunter
is translated to
        @number=123, @mask=0xff, @option=null, @name="Hunter"
This appears to make sense (and note that "null" is unquoted), but a Search argument of
        Hello World, @name=Hunter
will be translated to
        "Hello World, @name=Hunter"
which is probably not what you want (but it could be). To produce more predictable results, you should explicitly quote each parameter value; for example,
        "Hello World", @name="Hunter"
will produce the desired affect. Of course, the reason why Sybernet interprets strings this way is that it makes it very easy to pass a single (unnamed) string parameter to a stored procedure.

Sybernet will also perform command substitution and assignment against internal CGI variables if that variable is named as a parameter value in the Search argument; for example,

        @address, @method=@method
could be expanded into
        "128.18.20.143", @method="POST"
Somewhere in this guide is a list of Sybernet internal variables that may be retrieved and/or assigned in this manner.

What you are suppose to learn and what I am trying to convey is that you can construct hypertext links that use this method; for example, your stored procedure might generate a HTML page that contains a link (using this syntax) to another stored procedure. Consider the following example:

        select 'If you need more information, please press '
        select '<A HREF="Sybernet.cgi$sp_html_info">here</A>.
In this example, a hypertext link is constructed that will execute the stored procedure sp_html_info when it is selected. Notice that the IP address was not specified. This is allowed and will default to Sybernet.sri.com because that is the name of the server that generated this link.

Redirection

Redirection is a method whereby the Web Client is redirected to another location. This location, however, must have a server that handles the request; for example, your stored procedure might want to issue a redirect when the user requests information that is not available through the Sybernet CGI (i.e., it is not available by calling Sybase stored procedures). The following stored procedure will cause such a redirection to the MIS.SRI.COM home page.

        create procedure sp_html_redirect
        as
        declare
            @CRLF char(2)
            select @CRLF = char(13)+char(10)
            select 'HTTP/1.0 302 FOUND"
            select @CRLF
            select 'Server: WebSTAR/1.2 Sybernet/cgi'
            select @CRLF
            select 'Location: http://mis.sri.com/'
            select @CRLF
            select @CRLF
        return
A more general example of this procedure would accept a string parameter (e.g., a varchar) that would be inserted at the desired location. Also note that redirection can not be embedded within any other context: you are not allowed to bound this procedure call with HTML. An example of a hypertext link that invokes this procedure might resemble the following:

        select 'To return to the <STRONG>MIS</STRONG> home page select '
        select '<A HREF="Sybernet.cgi$sp_html_redirect">this</A>.
Remember that to pass a string parameter to a stored procedure using a URL, that parameter would occur after the Direct argument and preceded by a question mark.

Image Maps

Image maps are those entities you've probably seen that allow you to click anywhere on an image. What results occur are dependent upon exactly where on that image you actually clicked. The coordinates of your click are passed to an imagemapping CGI which determines where in this image you clicked and returns information based on that information; for example, an image of a face might produce detailed information about the eye-ball assuming you clicked on the part of the image.

You can write your own stored procedure to handle image maps, and I will show you an example of an imagemap stored procedure that does exactly that. The procedure is not very sophisticated, but it does illustrates the basics of creating such a function. If you come up with anything more elaborate, please share it with me.

The first thing we need is an image that is defined as an image map that also invokes our imagemap stored procedure when the user selects it.

        select 'This picture is a clickable map.<P>'
        select '<A HREF="Sybernet.cgi$sp_html_map">'
        select '<IMG SRC="Sybernet.cgi$sp_html_picture?' + @id + '" ISMAP>'
        select '</A>'
In this example we are retrieving a GIF image stored in Sybase. The image is declared as an image map by the ISMAP attribute. The procedure invoked by the Web client is called sp_html_map.

The client software passes the coordinates of the click to this procedure in the Search argument. The Search argument contains an X and Y coordinate separated by a comma. Here is a simple procedure that parses this argument and displays some relevant information to the client.

        create procedure sp_html_map (@coordinates varchar(10) =  null)
        as
        declare
            @X int
        ,   @Y int
        ,   @i int

            select @i = charindex(",",@coordinates)
            select @Y = convert(int,substring(@coordinates,1,@i-1)
            select @X = convert(int,substring(@coordinates,@i+1,datalength(@coordinates)-@i))

            select 'You selected the '			
            if @X < 122
                select 'top '
            else
                select 'bottom '
            if @Y < 162
                select 'left.'
            else
                select 'right.'
        return
This stored procedure simply tells you where in the picture you clicked. A more elaborate mapping could be devised where each individual element of the picture is bound by rectangular coordinates (or circular ones, it doesn't matter) and these coordinates along with some key are stored in a table. A select statement with an appropriate where clause could select that key and perform the required action.

HTTP Cookies

Cookies are a method that allows a CGI (or in our case a Sybase stored procedure) to store and retrieve state information on the client side of a connection. This section will discuss how cookies are stored and retrieved with a stored procedure. For a complete description of HTTP Cookies I recommend reading Persistent Client State HTTP Cookies.

COOKIE is a read-only reserved word in Sybernet. You can retrieve the cookies associated with a connection by including this name in a POST or GET request. First, let's take a look at a stored procedure that receives this string.

    create procedure show_cookies(@COOKIE varchar(255) = null)
    as
        select '<HTML>'
        select '<BODY>'
        select '<H1>Here are your cookies</H1>'
        select @COOKIE
        select '</BODY>'
        select '</HTML>'
        return
Notice that @COOKIE is declared as a VARCHAR(255) string. 255 is the maximum number of characters that Sybase will accept in a parameter. If you attempt to set so many cookies that this limit is exceeded, only the first 255 characters is returned.

The following stored procedure uses a method of POST to retrieve your cookies:

    create procedure get_cookie
    as
        select '<HTML>'
        select '<BODY>'
        select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="show_cookies">'
        select '<INPUT TYPE="HIDDEN" NAME="@COOKIE" VALUE="@COOKIE">'
        select '<INPUT TYPE="SUBMIT" VALUE="GET COOKIES">'
        select '</FORM>'
        select '</BODY>'
        select '</HTML>'
        return
Here is a URL request in the form of a hypertext link that accomplishes the same thing:
    create procedure get_cookie
    as
        select '<HTML>'
        select '<BODY>'
        select '<A HREF="Sybernet.cgi$show_cookies?@COOKIE">GET COOKIES</A>'
        select '</BODY>'
        select '</HTML>'
        return
And finally we also want the ability to assign cookies, but the COOKIE reserved word is read-only. To store an HTTP Cookie you must create a valid HTTP header in your stored procedure. Here is a stored procedure that creates a valid HTTP cookie.
    create procedure set_cookie
    as
    declare
        @CRLF char(2)
        select @CRLF = char(13)+char(10)
        select 'HTTP/1.0 200 OK'
        select @CRLF
        select 'Server: WebSTAR/1.2 Sybernet/cgi'
        select @CRLF
        select 'Content-type: text/html'
        select @CRLF
        select 'Set-cookie: INVOICE=1114'
        select @CRLF
        select @CRLF
        select '<HTML>'
        select '<BODY>'
        select '<H1>Thank you for your order!</H1>'
        select '</BODY>'
        select '</HTML>'
        return
As I mentioned all cookies are returned as one long string. To actually determine the value of all cookies in your @COOKIE parameter you need to parse this string in your stored procedure. This string will contain zero or more NAME=VALUE pairs and NAME=VALUE pairs are separated by semicolons.

    create procedure get_invoice(@COOKIE varchar(255) = null)
    as
    declare
        @i int
    ,   @NAME_VALUE varchar(255)
    ,   @NAME varchar(30)
    ,   @VALUE varchar(128)

        select '<HTML>'
        select '<BODY>'
        while datalength(@COOKIE) > 0
        begin
            select @i = charindex(@COOKIE,";")
            if @i > 0
            begin
                select @NAME_VALUE = substring(@COOKIE,1,@i-1)
                select @i = @i + 1
                select @COOKIE = substring(@COOKIE,@i+1,datalength(@COOKIE)-@i)
            end else
            begin
                select @NAME_VALUE = @COOKIE
                select @COOKIE = NULL
            end
            select @i = charindex(@NAME_VALUE,"=")
            select @NAME = substring(@NAME_VALUE,1,@i-1)
            select @VALUE = substring(@NAME_VALUE,@i+1,datalength(@NAME_VALUE)-@i)
            if @NAME = "INVOICE"
            begin
                select '<H1>Here is your order:</H1>'
                select '<PRE>'
                select * from invoices where invoice = @VALUE
                select '</PRE>'
                select '</BODY>'
                select '</HTML>'
                return
            end
        end
        select '<H1>Error...</H1>'
        select 'Invoice not found!'
        select '</BODY>'
        select '</HTML>'
        return
Cookies certainly can be helpful to save state information between multiple clients, but there are other mechanisms that will accomplish the same thing. One of the easier methods is to use "HIDDEN" fields in your form to pass this information between your clients and your stored procedure; however, Cookies are "persistent" in the sense that the client's browser will retain their value. You can even specify an expiration date for your cookies which persist even if the client disconnects from Sybase and shuts-down their Mac!

HTTP Headers

You have seen some examples in this guide where a stored procedure creates its own HTTP header. We did this when we wanted to change the content-type from text/html to image/gif and when we wanted to "redirect" a client to another URL. In general, your stored procedure will not create HTTP headers unless you need to specify something other than the default, but if you do, this section may be helpful understanding what happens "behind the scenes."

Sybernet creates its own HTTP header for every page generated by a stored procedure. Before delivering this page, however, it scans the result sets of your stored procedure to see if your stored procedure is building its own HTTP header. If another HTTP header is detected, Sybernet strips the HTTP header that it built before delivering your HTML page.

The following stored procedure creates its own HTTP header that is nearly identical to the HTTP header built by Sybernet.

    create procedure hello_world
    as
    declare
        @CRLF char(2)
        select @CRLF = char(13)+char(10)
        select 'HTTP/1.0 200 OK'
        select @CRLF
        select 'Server: WebSTAR/1.2 Sybernet/cgi'
        select @CRLF
        select 'Content-type: text/html'
        select @CRLF
        select @CRLF
        select '<HTML>'
        select '<BODY>'
        select '<H1>Hello World!</H1>'
        select '</BODY>'
        select '</HTML>'
        return
Sybernet does not scan your entire result set and does not look for more than two pairs of HTTP headers. Only the first HTTP header--the one created by Sybernet--is stripped when a second HTTP header is detected!

Have you noticed that sometimes your browser displays a running percentage of how much of your HTML has been retrieved? It can do this when a "Content-length" entry has been included in the HTTP header. Sybernet does not include Content-length in the HTTP headers that it builds because it has no way of knowing how large your result set will be. You probably do not know either, but if you did, you could create your own HTTP header and include a Content-length. This is hardly worth the effort!

Filters

Filters are independent of format and content-type. Filters are built-in preprocessors that do something with your result set before sending them to your browser; for example, the CGI filter sends your result sets back to Sybase before processing your output results.

The CGI Filter

This is one of the newer formatting options recently added to Sybernet. This option will cause Sybernet to resubmit the results of your stored procedure to Sybase.

Consider the following example:

    create procedure bounce
    as
        select 'SELECT "<H1>Hello World</H1>"'
        return
When FILTER=CGI, the result set produced by this procedure (SELECT "<H1>Hello World</H1>") are intercepted by the Sybernet CGI and resent to Sybase. The result is Hello World in big, bold text!

I bet you're still not impressed, am I right? Here's another example which exploits the full power of the CGI filtering option:

    create procedure get_table(@TABLENAME varchar(30))
    as
        select 'SELECT * from ' + @TABLENAME
        return
In this example we are selecting a table name dynamically. This may not seem like a big deal, except that this is something that is not allowed by a stored procedure. In a stored procedure all objects (tables, databases, views, et al) must be hard-coded at compile-time. The CGI filter completely eliminates this restriction.

Writing stored procedures with this method is fairly straight-forward, but things get complicated when dealing with quotes. Consider an HTML table that sets its width to 100%. When using FORMAT=NONE or FORMAT=SUPPRESSED, this table tag would be created with the following statement:

    select '<TABLE WIDTH="100%">'
But when building this same expression at run-time using the CGI formatting option, you need to separate the use of single-quotes embedded in double-quotes and vice versa. The expression to generate this same expression using FILTER=CGI would be similar to the following:
    SELECT "select '<TABLE WIDTH=" + '"100%"' + ">'"
The last programmer to use this option was attempting to build a complex selection condition in their WHERE clause. I first suggested fooling Sybase by doing multiple ANDS that would return TRUE using LIKE comparisons. The idea worked fine except it took the procedure 9 minutes to finish. With the CGI option, the selection condition was built dynamically (specifying only those columns that required testing) and the time to finish was reduced to a few seconds. Stunning, to say the least!

Example

One of the most clever uses of this option is to insert your HTML page into a text column (rather than sending it to the browser) and then select that column out of the table and give that to your user instead. The user gets their report, and you have the original safely stored away for easy retrieval.

    create procedure sp_html_deposit(@row_id int)
    as
        select 'insert safe'
        ,      '('
        ,      '    row_id'
        ,      ',   html'
        ,      ')'
        ,      'values'
        ,      '('
        ,      '    ' + convert(varchar,@row_id)
        ,      ',   "'

        select '<HTML>'
        ,      '<HEAD><TITLE>Web Page</TITLE></HEAD>'
        ,      '<BODY>'
        ,      'This HTML page is being inserted into a table'
        ,      '</BODY>'
        ,      '</HTML>'

        select '")'

        select '<GO>'
        ,      'select html from safe where row_id = ' + convert(varchar,@row_id)
        ,      '<GO>'
        return
The procedure (using FILTER=CGI and FORMAT=SUPPRESSED) creates an insert statement, the result of which resembles the following:
        insert safe
        (
            row_id
        ,   html
        )
        values
        (
            123
        ,   "<HTML><HEAD><TITLE>Web Page</TITLE></HEAD><BODY>This HTML page is being inserted into a table</BODY></HTML>"
        )
It then turns around and selects the row it just inserted:
        select html from safe where row_id = 123
And yes, writing stored procedures this way is not for the faint of heart. It takes a while getting used to, and you will spend count-less hours wondering why your quotes are in the wrong place, but the flexibility of this option makes it all worth while. Trust me!

The ECHO Filter

This filter option is similar to the CGI option except that results are sent back to your stored procedure. I added this option several months ago to solve a problem with BLOB's which could not be handled by stored procedures. We needed the ability to "parse" a Text field and replace certain tokens in that BLOB with other values. The use of this option, however, is not just limited to BLOB's.

When FILTER=ECHO, Sybernet will save the entire result set stream created by your stored procedure. Then your stored procedure is recalled repeatedly with chunks of 254 characters until the original result set has been sent. The data is passed to your procedure in its last parameter.

Let's take a look at a procedure that uses the ECHO option.

    create procedure echo(@ID varchar(20) = null,@timestamp varchar(26),@blob varchar(254) = null)
    as
        if @blob is null
        begin
            select Text_Column from Table_Name where id = @id
            return
        end
        if datalength(@blob) = 254
        begin
            insert Scratch_Table values (@id,@timestamp,@blob)
            return
        end
        insert Scratch_Table values (@id,@timestamp,@blob)

        select '<HTML>'
        select '<BODY>'
        select blob from Scratch_Table
        select '</BODY>'
        select '</HTML>'
        return
This procedure runs in three states:

When @BLOB is null, this is the first time it has been called and it selects the data that it needs. Although this procedure is selecting a Text field, you can select any field type and any number of fields before the procedure returns.

When the datalength of @BLOB is exactly equal to 254 characters, the procedure saves this information to a table and returns. The last chunk will always be less than 254 characters so the procedure knows there is more to come.

The third state is when @BLOB is neither NULL nor 254 characters in length. This is the last chunk being sent, so the procedure saves this as well and processes the table it just populated.

I now have the ability to select the information from the table as VARCHAR's (as opposed to Text or Image) and perform such functions on it as SUBSTRING and CHARINDEX. These functions are not available with BLOB's.


Filters, Formats, and Content-Types

This section attempts to explain how Filters, Formats, and Content-types interact with each other.

Filters are independent of format and content-type. Filters are built-in preprocessors that do something with your result set before sending them to your browser; for example, the CGI filter sends your result sets back to Sybase before processing your output results.

Format determines how Sybernet will format your output results; for example, the SUPPRESSED option streams all result set data to your browser without any translation or formatting.

Content-type is ignored by Sybernet. Content-type is a directive for your browser that tells it what to do with this data; for example, Content-type "application/rtf" might spawn a copy of MicroSoft Word or Claris Works, depending upon how this is defined in your browser options.

Setting Content-type to "application/rtf" will itself not produce a valid RTF document (since Sybernet ignores Content-type). Instead, you would either specify Content-type as "application/rtf" and then generate RTF in your stored procedure, or, more simply, specify format as RTF. The RTF format option automatically sets Content-type to "application/rtf."

Filters and Content-type never determine format; however, some values of Format may specify either or both of these options, overriding any values you may have specified. The following table explains this further:

Format FilterContent-type Format
CGI CGI SUPPRESSED
ECHO ECHO SUPPRESSED
EXCEL application/excelTABULAR
RTF application/rtfRTF
NONE NONE
SUPPRESSED SUPPRESSED
TABULAR TABULAR


To filter your result sets with the CGI filter and then create an RTF document you would specify FILTER=CGI and FORMAT=RTF.

FORMAT=CGI and FORMAT=ECHO are now obsolete options. They will be removed on a future release. Instead you should use the appropriate filter option and format to accomplish the same thing.

More About Content-type

When your stored procedure is concerned with delivering only a single content-type (normally, "text/html"), things are fairly straight-forward. Things start to get complicated when you attempt to handle many different content-types in the same request, but with some explanation this is something you can do.

Does it make sense to you that we need to tell Sybernet when your initial form is submitted that executes a stored procedure what the content-type is suppose to be? This is true, by the way, for all internal Sybernet options that we want to set. Consider the following example which allows the user to choose between HTML or RTF:

    create procedure sp_gig
    as
        select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_report">'
        select '<INPUT TYPE="RADIO" NAME="FORMAT" VALUE="SUPPRESSED">HTML'
        select '<INPUT TYPE="RADIO" NAME="FORMAT" VALUE="RTF">'
        select '<INPUT TYPE="SUBMIT">'
        select '</FORM>'
        return
Although content-type is never mentioned in this example, it does play an important role. When this form is submitted, Sybernet interrogates the value for FORMAT and automatically sets the content-type to "text/html" when set to SUPPRESSED and "application/rtf" when set to RTF.

Simple enough so far because Sybernet already knows how to handle these formats, but if you start adding other content-types (content-types that Sybernet does not recognize), things can slowly start getting out of hand. Suppose now we want to add another option to our radio button to send our output to Excel. Well fine, but there is no format that does this for us. Actually there is, but let's pretend there isn't.

Let's take a closer look at what we want to do in our stored procedure when output is being directed to EXCEL. We will obviously need to set CONTENTTYPE to "application/excel," and since we don't want Sybernet to format our output results, we would probably set FORMAT to SUPPRESSED. Do you see the problem? There are various combinations of both FORMAT and CONTENTTYPE that a single radio button (without help) cannot perform. To solve this problem we can use JavaScript to set both CONTENTTYPE and FORMAT appropriately.

In the following example, our radio button is called (surprise) @RADIO. FORMAT and CONTENTTYPE are both hidden fields that are set when the user clicks on the corresponding radio button.

    create procedure sp_gig
    as
        select '<FORM NAME="Form" METHOD="POST" ACTION="Sybernet.cgi">'
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_report">'
        select '<INPUT TYPE="RADIO" NAME="RADIO" VALUE="HTML" CHECKED'
        select ' onClick="document.Form.FORMAT.value=''SUPPRESSED'';document.Form.CONTENTTYPE.value=''text/rtf'';return true;"'
        select '>HTML'
        select '<INPUT TYPE="RADIO" NAME="RADIO" VALUE="RTF"'
        select ' onClick="document.Form.FORMAT.value=''RTF''; document.Form.CONTENTTYPE.value=''''; return true;"'
        select '>RTF'
        select '<INPUT TYPE="RADIO" NAME="RADIO" VALUE="EXCEL"'
        select '  onClick="document.Form.FORMAT.value=''SUPPRESSED''; document.Form.CONTENTTYPE.value=''application/excel''; return true;"'
        select '>EXCEL'

        select '<INPUT TYPE="HIDDEN" NAME="FORMAT" VALUE="SUPPRESSED">'
        select '<INPUT TYPE="HIDDEN" NAME="CONTENTTYPE" VALUE="text/html">'
        select '<INPUT TYPE="SUBMIT">'

        select '</FORM>'
        return
As a result now, clicking any button will set FORMAT and CONTENTTYPE appropriately. As an important side-effect, our radio button (@RADIO) is also passed to our stored procedure so that we can determine what to do:

    create procedure sp_report(@RADIO varchar(30) = NULL)
    as
        if @RADIO = "EXCEL"
        begin
            select 'au_id'
            ,      char(9)
            ,      'au_fname'
            ,      char(9)
            ,      'au_lname'
            ,      char(13)
            select au_id
            ,      char(9)
            ,      au_fname
            ,      char(9)
            ,      au_lname
            ,      char(13)
            from   pubs2..authors
        end else
        if @RADIO = "HTML" or @RADIO = "RTF"
        begin
            select '<TABLE BORDER=1>'
            select '<TR>'
            ,      '<TH>' + 'au_id'    + '</TH>'
            ,      '<TH>' + 'au_fname' + '</TH>'
            ,      '<TH>' + 'au_lname' + '</TH>'
            ,      '</TR>'
            select '<TR>'
            ,      '<TD>' + au_id      + '</TD>'
            ,      '<TD>' + au_fname   + '</TD>'
            ,      '<TD>' + au_lname   + '</TD>'
            ,      '</TR>'
            from   pubs2..authors
            select '</TABLE>'
         end
         return
Note that the reserved word PROCEDURE could also be assigned with JavaScript if I wanted to call a separate procedure for each choice. I could also bound each radio button by its own form tag and eliminate the use of JavaScript completely.


Rich Text Format

The Rich Text Format (RTF) standard is a method of encoding formatted text and graphics for easy transfer between applications. Sybernet can translate your HTML output to RTF automatically, though as of this writing there are some limitations which are explained below.

Every attempt has been made to generate an RTF document that is as close as possible to its HTML counter-part, but subtle differences will no doubt occur; for example, NetScape is very good about absorbing redundant <P> and <BR> tags. Sybernet attempts to do like-wise, but perhaps not as well. In general, though, the two documents should be nearly identical.

A stored procedure can easily specify RTF translation with the FORMAT keyword:

    create procedure report_form
    as
        select '<FORM METHOD="POST" ACTION="Sybernet.cgi">'
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="sp_report">'
        select '<INPUT TYPE="RADIO" NAME="FORMAT" VALUE="NONE" CHECKED>HTML<BR>'
        select '<INPUT TYPE="RADIO" NAME="FORMAT" VALUE="RTF">RTF<BR>'
        select '<INPUT TYPE="SUBMIT" VALUE="REPORT">'
        select '</FORM>'
        return
The example above creates two radio buttons for the FORMAT keyword. This makes it possible for your users to specify which format (HTML or RTF) they prefer.

When FORMAT=RTF is specified, Sybernet automatically sets the content-type to "application/rtf" and proceeds to translate all HTML to RTF format. Although the format is now RTF, your stored procedure should continue to produce valid HTML. In fact, if you attempt to insert RTF control codes in your document, Sybernet will suppress them.

Things to know about RTF encoding

The RTF translator was added to Sybernet because SRI had a real need to edit HTML documents. While this could be done with an HTML editor, there was currently no way to insert page breaks. And RTF is a popular format with several word processors. The Sybernet RTF translator is not going to translate any pictures, frames or forms, and you will be disappointed if you expect it to do so. But for generic HTML reports, the translation to RTF should be a handy facility.

Your stored procedure must contain an HTML <BODY> tag. If this tag is omitted, no translation will occur and your document will contain an appropriate error message.

        select '<HTML>'
        select '<HEAD>'
        select '<TITLE>Report</TITLE>'
        select '</HEAD>'
        select 'This line will not be translated to RTF!<P>'
        select '</BODY>'
        select '</HTML>'
Although your NetScape browser is capable of figuring this out, Sybernet will generate the following warning:

        I'm so sorry, but you forgot to supply a <BODY> tag!
All information between <HEAD> and </HEAD> is ignored by Sybernet with the exception of information in a <META> tag. In the example above, <TITLE>Report</TITLE> is completely ignored.

The META tag

The <META> tag allows you to specify information not normally found in an HTML document, but which you may wish to specify in an RTF document. Your browser will also ignore this information so it is safe to use in this context.

The META tag is composed of NAME=VALUE pairs, separated by space; for example,

        select '<META NAME="AUTHOR" CONTENT="Denis D. Workman">'
is how would specify the author of an HTML document. Coincidentally, this same expression would specify the author of your RTF document. The following META names are recognized by the Sybernet RTF translator:

  • AUTHOR

    The AUTHOR option allows you to specify the author of your RTF document.

  • TITLE

    The TITLE option allows you to specify the title of your RTF document, separate from the title of your HTML document; for example,

    select '<META NAME="TITLE" CONTENT="Sybernet.rtf">'

  • FOOTER

    The FOOTER option allows you to specify footer information that appears on the bottom of each page in a document. There is no default footer.

    Prior to version 1.73 of Sybernet, the footer was automatically formatted by placing it in a box, with small caps, in the lower right hand corner of the page. With versions 1.73 and above, the header and footer may now contain HTML which is (of course) translated to RTF.

    An example of HTML that creates a footer identical to earlier versions of this option is presented below.

  • HEADER

    The HEADER option allows you to specify header information that appears on the top of each page in a document. There is not default header.

  • PAPERW

    PAPERW indicates the paper width in twips. The default is 12,240 (8.5 inches).

  • PAPERH

    PAPERW represents the paper height in twips. The default is 15,840 (11 inches).

  • MARGL

    The MARGL option specifies the left margin of your RTF document in twips. The default is 1,440 twips, or one inch.

  • MARGR

    The MARGR option specifies the right margin in twips. The default is 1,440 twips.

  • MARGT

    The MARGT option specifies the top margin in twips. The default is 1,440 twips.

  • MARGB

    The MARGB option specifies the bottom margin in twips. The default is 1,440 twips.

  • ORIENTATION

    This meta tag allows you set the orientation of your RTF document to either LANDSCAPE or PORTRAIT. The default is PORTRAIT.

Multiple RTF options may be specified in one META tag; for example,
        select '<META NAME="MARGL" CONTENT=720 NAME="MARGR" CONTENT=720>'
would set the left and right margins in a single meta tag.

The FF tag

The <FF> tag causes a page break in your RTF document.

The Landscape tag

The <LANDSCAPE> and </LANDSCAPE> tags allows you to switch orientation between portrait and landscape in the middle of your RTF document. This option can only be used if the default orientation is portrait. There is no corresponding <PORTRAIT> tag.

The Paragraph tag

The <P> tag may include a directive for indenting the first line of a paragraph; for example, <P INDENT=5>. This value resets to zero if not specified.

Footers and Headers

Prior to version 1.73 of Sybernet, the footer had to be a simple text string that was automatically formatted for you. This is no longer true. To allow more control over how the footer and header is going to appear, you are now allowed (even encouraged) to create these with HTML. The following example illustrates how you would create a footer that is identical to the footer that Sybernet used to make:
        select '<META NAME=FOOTER CONTENT="'
        ,      '<TABLE BORDER=1 WIDTH=160 ALIGN=RIGHT>'
        ,      '<TR>'
        ,      '<TD ALIGN=JUSTIFIED>'
        ,      '<FONT SIZE=1>'
        ,      'THIS IS MY FOOTER TEXT THAT GOES ON THE BOTTOM OF EACH PAGE'
        ,      '</TD>'
        ,      '</TR>'
        ,      '</TABLE>'
        ,      '">'
Notice that the value of CONTENT is enclosed in double quotes. Thus, you do not want embed double quotes in this string. Alternatively, you can enclose the string in single quotes, but then the rule would be don't use single quotes in your string.

Limitations

When preformatted text is displayed in your browser, text exceeding the current width of your browser's window is truncated. This is not true for a word processor such as Claris Works, which will wrap preformatted text to the next line if the current line width is exceeded. If this is unacceptable, you may want to try decreasing the left and right margins with the META tag described above. Alternatively, you may need to change your preformatted text.

RTF does not permit embedded tables, and if you attempt to create embedded tables in your HTML document, they will be ignored when translated to RTF. The un-nested portion of a table is always printed subject to the following limitations:

Tables may not contain more than 33 columns. If you attempt to exceed this amount, the table is not printed.

COLSPAN is ignored. Actually RTF is capable of doing this, but the RTF translator in Sybernet is not. All columns have a COLSPAN of 1.

The default table width is 100%. This is different from NetScape which sizes the table dynamically. If you do not want your table to span the entire width of a page, you can specify table width in the usual way (using the WIDTH attribute of the TABLE tag).

Although Sybernet does make an attempt to calculate the ideal width of a table column, it is not nearly as good at this as your Web browser, though I can't say I have agreed with every calculation done with some of my tables. You can facilitate this calculation by using the WIDTH option of the <TH> and <TD> tags; for example,

        select '<TH WIDTH="60%">Description</TH><TH>Name</TH><TH>Default</TH>
will cause the Sybernet RTF translator to reserve 60% of the table's width to the first column. The remaining columns are allocated appropriately.

Note that absolute values for WIDTH are specified in pixels. The Sybernet RTF translator converts pixels to twips automatically.


Sybernet Reserved Words

The following list of reserved words are known internally to Sybernet. Some are read-only (meaning you can interrogate their value, but you can't assign them); some are write-only (meaning you can set their value, but you can't interrogate them); and others are both readable and writeable (meaning you can assign or interrogate their value).

Sybernet Reserved Words
Name Description Comment
PROCEDURE The name of the stored procedure to invoke Write-Only
USERCODE The Sybase Login Name Read-Write
PASSWORD The Sybase password Write-Only
HOSTNAME The hostname of the Sybase dataserver Read-Write
FORMAT Type of formatting to perform on result set Read-Write
DIRECT The Direct argument Read-Only
SEARCH Arguments to the URL after a ? Read-Only
METHOD Tells whether GET or POST was specified Read-Only
ADDRESS Name or IP address of the client Read-Only
EMAIL Optional E-Mail address of this document Read-Write
SERVERNAME Name or IP address of this server Read-Only
SERVERPORT TCP/IP port number being used by this server Read-Only
SCRIPTNAME Optional filename of this document Read-Write
REFERRER The URL of the page referencing this document Read-Only
CLIENT The name and version of the WWW Client softwareRead-Only
CONTENTTYPEMIME content type Read-Write
DATABASE Performs an implicit USE database statement Read-Write
PROCESSNAMEThe actual process name of the Sybernet CGI Read-Only
COOKIE NetScape Cookies Read-Only
FILTER Sybernet preprocessor option (ECHO or CGI) Read-Write
FROM Sender of message if EMAIL is specified Read-Write
SUBJECT Subject of message if EMAIL is specified Read-Write
FILLER Filler values are absorbed and do nothing Read-Write


You have already seen an example of how some of these values are assigned when you designed the form screen stored procedure. In that procedure you assigned values for PROCEDURE and FORMAT. These are the two values you will set most often. It is also possible to assign the values for username, password and hostname.

Values can also be retrieved from Sybernet when included as the value of any INPUT field or as a parameter value that is passed in the Search argument; for example,

        select '<INPUT NAME="@CLIENT_ADDRESS" VALUE="@ADDRESS">'
causes the address of the requesting client to be passed as the value for @CLIENT_ADDRESS. In all cases, the variable must be preceded by an at-sign ("@").

Note that the spelling of these variables is case-insensitive.


Security

This section discusses security and how it relates to stored procedures, and in particular, stored procedures for the World Wide Web. In it we discuss how the main menu is built (and why I keep insisting that "guest" must be added to your database), where your procedures should be located, and other nuances which should save you some time and grief.

The Main Menu Screen

The main menu screen is created by the stored procedure SP_HTML_LOGIN which resides in SYBSYSTEMPROCS. This procedure calls SP_HTML_PERMISSIONS for every registered procedure to determine whether or not execute permission has been granted on that procedure. If access is allowed, and entry for that procedure is built on the main menu screen, and if access it not allowed, the procedure does not even appear.

Although SP_HTML_PERMISSIONS resides in SYBSYSTEMPROCS, it actually executes from inside of the database where the procedure was created. And it is for this reason why "guest" must be a valid user when you attempt to register your procedures. Do not confuse username "guest" with guest access to your database via the Sybernet CGI. The former is a special username that can be added to a database with SP_ADDUSER. The latter allows an anonymous user using either "guest" or "anonymous" to sign on to your Sybase SQL server.

Where you should create your procedures

While you can create your procedures in any database (and the HTTP database seems as good a place as any), stored procedures should really be created in the database for which they refer.

Let's say I want to access information from the PUBS2 database. If I create that procedure in the HTTP database, I must not only grant execute permission on the procedure, but also on the tables this procedure accesses. On the other hand, if I create the procedure in PUBS2, I need only grant execute on the procedure, not the tables!

The other problem with this scenario is to whom am I going to grant access. If the procedure is in HTTP and I want to limit access to certain individuals or a "group" of individuals, then those individuals or groups must be valid users in both HTTP and PUBS2. But if the procedure was declared in PUBS2, I need only concern myself with the users and groups in that database.

Why you should qualify procedure names

You should also get in the habit of qualifying every procedure name that you reference. Consider the following:
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="SP_HTML_REPORT">'
Let's say that the procedure that is creating this form is in PUBS2, that it has been registered, and that execute permission to "public" has been granted. Someone executing this procedure from a database different from PUBS2 will get the form all right, but when the form is submitted, Sybase will look for the procedure SP_HTML_REPORT in the wrong database. Qualifying the name eliminates this problem.

        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="PUBS2..SP_HTML_REPORT">'
And if there is a possibility you need the exact same procedure in more than one database, you might want to consider doing something like this which determines the database name dynamically.
        select '<INPUT TYPE="HIDDEN" NAME="PROCEDURE" VALUE="' db_name() + '..SP_HTML_REPORT">'
Stored procedures run in the context of the database in which they were declared. The exception to this rule are stored procedures created in SYBSYSTEMPROCS. So even if a users current database is not PUBS2, for example, and they execute a stored procedure that is in PUBS2, they are actually in PUBS2 while that procedure runs. And this explains why you do not need to explicitly qualify table names in your procedure, although I think this is a good idea too.


The Stored Procedure Editor

The Stored Procedure Editor allows you to edit and create Sybase stored procedure on the World Wide Web. The Stored Procedure Editor is a Sybase stored procedure. This section will describe its use and the frames that are built when the editor is invoked.

Most of the editing you will do is accomplished by your browser. Functions such as cut, copy, and paste are used in the usual way. My browser does not have a replace command so this feature is not available to me.

When the Stored Procedure Editor is invoked, three windows are built on your screen. They are the Directory Window, the Input Window, and the Output Window:

The Directory Window

The Directory Window contains a list of hypertext links of all procedures, views and triggers in your database. This list is sorted by name and grouped by owner.

At the top of this list is a hypertext link that identifies the current database. To change the current database simply click on this link. A menu of all databases on the system is displayed in this window. Just click on the database you want and a new directory will be created.

Clicking on a procedure, view or trigger will load that object into the input window.

The Input Window

The Input Window is where your procedure is edited. Most of the editing you will do is accomplished by your browser. Functions such as cut, copy, and paste are used in the usual way by selecting these commands from the Edit command on the menu bar.

When the window is created, no procedure is loaded until you select one from the directory window; however, you can still create procedures in this window and submit them in a manner described below.

The buttons in the Input Window are described by the following:

  • submit

    Use the SUBMIT button to compile your stored procedure. If the procedure already exists, it must first be dropped.

  • undo

    The UNDO button resets all changes made to your edit window since the procedure was last retrieved.

  • load

    The LOAD button causes the directory window to be rebuilt. This is especially useful when you have created a new procedure and you want to enable the SAVE, DROP, and EXEC buttons. Those buttons are disabled--they cause an error message to be displayed on the Output Window--when no procedure has been selected from the directory window.

  • isql

    The ISQL button invokes the Interactive SQL function. This feature makes it very easy to issue adhoc SQL statements while in the middle of editing a procedure; for example, you may wish to change the security on a procedure after it has been changed and compiled.

    Use the right-mouse button in Windows or hold the mouse down for MacOS and press Back In Frame to return to your edit window.

  • save

    If a procedure has been selected from the directory window, this button will rename your procedure. The rename is such that the text "_save" is appended to your procedure's name. This button is especially useful for saving your work while in the middle of editing a procedure.

  • drop

    The DROP button drops your procedure from Sybase. You must drop a procedure, view, or trigger before it can be submitted if it already exists in Sybase.

  • exec

    The EXEC button will execute your stored procedure. This is a very crude way to test your procedure because it does not pass parameters. The results are also sent to the output window. This may or may not be a problem if you are also declaring frames in your procedure.

The Output Window

All output is sent to the Output Window. This includes syntax errors from Sybase or error messages generated by the Edit Window; for example, if you attempt to drop a procedure that does not exist, the output window will display a message saying that the procedure has already been dropped.

All output from the EXEC button is also sent to this window. This may or may not be a problem if your procedure declares frames.

When you submit a procedure that compiles successfully, the output window will be cleared (if you are using a WebSTAR server) or contain the text OK (if you are using a NetScape, NCSA, or CERN server).


The Stored Procedure Register

The main menu screen is dynamically created each time a user logs on to Sybase with their Web browser. The sign-on screen on my home page instructs Sybernet to invoke the procedure SP_HTML_LOGIN if a successful connection is made. This procedure reads the HTTP database which contains a list of stored procedure names that it will use to create this menu. Before your form procedure can appear in this menu, however, it must be registered. And you must use your Web browser to do this! One of the menu options that you can select from the main menu screen is called Stored Procedure Register. And yes, the Stored Procedure Register program was registered the same way your procedure will be. Select this link to see the form screen for the Register program.

When the Stored Procedure Register is invoked, three windows are built on your screen. They are the Directory Window, the Input Window, and the Output Window:

The Directory Window

The Directory Window contains a list of hypertext links of all procedures in your database. This list is sorted by name and grouped by owner.

At the top of this list is a hypertext link that identifies the current database. To change the current database simply click on this link. A menu of all databases on the system is displayed in this window. Just click on the database you want and a new directory will be created.

Clicking on a procedure will load that object into the input window. If the procedure has previously been registered, the SHORT DESCRIPTION and LONG DESCRIPTION fields will be filled in; otherwise, these fields are blank.

The Input Window

The Input Window is where you register your procedure. You need to select a procedure name from the Directory Window before it can be registered. That name should appear on the input form. The fields on this form as described as follows:
  • Short Description

    This description will appear in the main directory. This text is also used as a hypertext link to your form screen, and it is required.

  • Long Description

    The next field is a long description of what your application does. This too appears on the main directory (right next to your short description). This field is optional, however. You may also use HTML syntax in this field if you desire, but try not to be too obnoxious. We don't want to see no blinking, for example.

  • Four Radio Buttons

    • Register

      Use this button to register your procedure!

    • Change

      Use this button if you want to change either the short or long description of a procedure that you have previously registered.

    • Delete

      Use this button to delete your procedure from the HTTP database.

    • Retrieve

      Use this button to see what your procedure is going to look like on the Main Menu. The procedure must already be registered to use this button.

  • SUBMIT

    Click this button to submit this form.

The Output Window

The Output Window contains information about your request. If you have successfully registered your procedure, this window will say so and create an image of how your procedure will look on the Main Menu. Any other useful information (like errors) are written to this window as well.


Interactive SQL

Interactive SQL allows you to enter ad-hoc SQL statements. It is similar to the Sybase ISQL program, but the editing is slightly more intuitive and a wide and useful variety of formatting gives you more control on how your results are going to look.

Most of the editing you will do is accomplished by your browser. Functions such as cut, copy, and paste are used in the usual way. My browser does not have a replace command so this feature is not available to me.

When Interactive SQL is invoked, three windows are built on your screen. They are the Directory Window, the Input Window, and the Output Window:

The Directory Window

The Directory Window contains a list of hypertext links of all tables and views in your database. This list is sorted by name and grouped by owner.

At the top of this list is a hypertext link that identifies the current database. To change the current database simply click on this link. A menu of all databases on the system is displayed in this window. Just click on the database you want and a new directory will be created.

Clicking on a table or view will create a valid select statement for that object in your input window where it can be edited. Pressing the SUBMIT button will select every row and every column for that object.

The Input Window

The Input Window is where your SQL statements are entered and edited. Most of the editing you will do is accomplished by your browser. Functions such as cut, copy, and paste are used in the usual way by selecting these commands from the Edit command on the menu bar.

When this window is created, the area where you edit your statements is empty. You can either enter your SQL statements here or click on the desired object in the directory window which builds your select statement automatically.

Use the PREFERENCES button at the bottom of this window to control the size of your text area. You can set the number of rows and columns. Your preferences are saved in Sybase so you don't need to change these unless you desire new values.

The radio buttons at the bottom-left of this window determine how your result sets are formatted and are explained by the following:

  • ISQL

    ISQL displays output in a tabular format similar to the Sybase ISQL facility. Numeric data is right-aligned, money is comma-separated, and so forth.

  • Window

    Window places each result set in its own text area, similar to your input window, which can be edited and resubmitted to Sybase. You would want to use this particular format when your original request creates a valid transact SQL statement; for example, select 'GRANT EXECUTE ON ' + name + ' TO PUBLIC ' + char(13) FROM SYSOBJECTS WHERE TYPE = "P"'

    Be careful to remember that each result set goes in its own text area.

  • Table

    Table places each result set in its own HTML table complete with table headers. It's about the fastest way to produce something of report quality, but its use is very limited.

  • None

    The None option is tantamount to FORMAT=SUPPRESSED in your stored procedures. This option is useful for testing out select statements that are destined for the web. Other than that reason, you probably would not use this.

  • Excel

    Excel sends your result sets to Excel. Well, this is not entirely true. Actually, the content-type is set to application/excel, columns are separated by a tab character, and each row is terminated by a carriage-return.

    It doesn't matter if you have Excel or not. You can configure your NetScape browser to use any spread-sheet application (or even a word processor). The steps to do this are outlined by the following:

    1. Select Options/General Preferences...
    2. Select the tab for Helpers.
    3. Select New.
    4. In the space for MIME Type enter application/excel.
    5. In Handled By select Application.
    6. Select Browse....
    7. Find the application for Excel and click on Open.
    8. Click OK.

    This format option will also create column headings, and if they are not desired, you can delete them yourself.

The Output Window

All output is sent to the Output Window. This includes syntax errors from Sybase or error messages generated by the Edit Window.


Limitations

  • The maximum data that a client can send to Sybernet via a WebSTAR server is documented at 24K. The actual value is somewhat less than this, however. WebSTAR does seem to handle an ENCTYPE of "multipart/form-data" very well.

    If you want to use the Stored Procedure Editor, you better keep your procedures under this limit. If your procedure is that large, however, maybe you should consider breaking it up into smaller chunks.

  • The maximum data that a client can send to Sybernet via a Unix server is configurable. The default is 32K.

  • The maximum data that Sybernet can send to a client is limited only by the amount of memory available on the client's browser. Exceeding this limit can sometimes crash a PC. The preferences file in Sybernet allows you to set an internal threshold (HTMLLIMIT)that will prevent such catastrophes. This option is available in Sybernet version 1.70 and above.

  • The largest text or image field that can be retrieved in one select statement is (by default) 100K. This too can be changed in your preferences file by setting the value for TEXTLIMIT. This option is available in Sybernet version 1.70 and above.

  • Older versions of Navigator could not populate a <TEXTAREA> greater than 32K. The newer versions (3.0 and above) seem to deal with this correctly now. Thank you!

  • Sybase limits parameter names to 30 characters.

  • Sybase limits parameters to stored procedures to 255 characters. You can get around this limitation by storing into text or image data types using an insert statement.

  • Parameter names to Sybase must be preceded by an at-sign (@). This limitation, however, is NOT required by Sybernet when issuing POST requests. Sybernet will supply the at-sign automatically!


Bibliography

Wiederspan, Jon. "CGI Applications and WebSTAR." MacTech Magazine. Vol 11, No 7(1995), p. 15.






Links

Some useful links:








Sybernet is a trademark of SRI International. Copyright © 1996-2002 SRI International. All Rights Reserved.