Sybase Programming Basics

Transact SQL is a very neat language which provides a simple and elegant way of splitting jobs into maneagable chunks (batching), local variable declaration and simple but effective programme control constructs.



Batches.


So far as Sybase is concerned, anything that preceeds the word 'go' in a script is part of the same batch. If there is anything wrong in any part of the batch, the whole thing is rejected and no changes take place on the server.

Just to clarify: Go is not actually a Transact SQL keyword. It is, by convention, the batch seperator that most front ends to Sybase recognise. It's actually up to the front-end to split long scripts into batches - Sybase itself doesn't do so. The convention is so deeply entrenched, though, that most people think of it as a Sybase feature.

Here's an example of a script that contains three batches...

/* -----------------------------------
   Main run
   --------
   initialise the input feed tables
   -------------------------------- */
delete from feed_master where runresult < 0
exec initialisefeedproc
go
-- --------------
-- start the feed
-- --------------
exec runfeedproc
go
-- ------------------
-- check the feed ran
-- ------------------
select feedresult from feedmaster where runresult < 0
go

Note that there's no error handling evident in this script other than at the end. I leave it as an excercise for the reader to decide if and what error handling may be appropriate.



Comments.


Sybase allows both the multi-line (/* comment */) and single line (-- comment) forms, in common with most modern versions of SQL, as shown in the above sample.



Local Variables.


Sybase has both local and global variables but only permits the user to define local variables, which must always start with '@'. Local variables exist only for the life of the current batch. This is important, too many people spend time looking for bugs caused by assuming that variables will span an entire script.

Local variables are declared and then loaded with a select statement, as in the following example...

-- ----------------------
-- Create the variable...
-- ----------------------
declare @NewSalesman varchar(40)

-- ----------------------------
-- then load it from a table...
-- ----------------------------
select @NewSalesman = Salesman
 from update
 where NextProcess = 'update'
go

-- ---------------------------------------
-- ...a waste of time because @NewSalesman
-- has now ceased to exist!
-- ---------------------------------------

Note that you can't use the same query both to retrieve data and set a variable. You may also load a variable with a constant value as in...

-- -----------------
-- Create test value
-- -----------------
declare @TestValue
select @TestValue = 'A345321'
etc....



Global Variables.

These start with '@@' and are read only. There's a fairly long list of which some of the more useful are...

@@rowcount    The number of rows returned by the last query.
              Everything (except declare) changes this so store it
              in an int if you'll need it later.
@@error       The last error number raised. More or less the same
              applies to this as to @@rowcount.
@@trancount   How deep the transaction nest is currently. If you have
              any uncommitted transactions, this will be greater than
              zero. Conversely, if it's zero, there are no outstanding
              transactions.
@@servername  The name of the server the query is being run against.
              Commonly used by DBAs, this can also be usefull to stop
              code being run against the wrong database.
@@version     The server and O/S version numbers.
@@spid        The ID of the current process.
@@identity    The last identity value used by an insert.
@@nestlevel   How many levels down the current procedure or trigger is.
@@sqlstatus   The result of the last fetch on a cursor.



The Print Command.


This is very simplistic. Basically, it just passes a message to the client programme's message handler. Messages are limited to 255 characters and must be either a single string or a single variable...

print 'Number of orders in transaction exceeds number on file'
print @SalesOrderErrorMessage




Raising Errors.


If the default error messages are insufficient for your needs, Sybase allows you to create your own with the raiserror command. Sybase Errors are identified by a number in the range 50,001 to 2,147,483,647 so the chances of running out of codes are fairly remote. Associated with each error number is a severity level, state  and message.

Error messages need to be stored in sysmessages before use as in...

sp_admessage 57001, 16, "Customer code %s is currently on credit hold - not updated"

The %s in the above example is a placeholder, similar to the variables in the Unix printf command. If a suitable value is supplied when the error is raised, it will replace the %s.

You can either raise an existing error or you can generate one of your own, as in the following example.

raiserror(57001, 16, 1, @CustCode)

will result in the following message being returned to the client...

MSG 57001, Level 16, State 1
Customer Code GHB31987 is currently on credit hold - not updated



if..else


This can be used anywhere in a batch and works pretty well as you might expect. The following example shows how you can compare the result of a query to a constant. Note that a query must be enclosed in brackets...

if (select max(invoice)
      from sales
     where office = 'Swindon') > 2000.00
   @SwindonCovered = 'Yes'
else
   @SwindonCovered = 'No'

and this illustrates the use of the exists test...

if exists (select CustCode
             from Customer
            where Status = 'Live'
              and Salesman is null)
 print 'Orphaned customer found - run orphans report before month end'

Sybase permits you to nest if statements up to 150 levels, which should be enough for most purposes! Note also that only one statement or block is triggered by if and the same for else, which brings us neatly onto...



Statement Blocks.


We can alter the example above to look something more like the real world by doing the following...

if exists (select CustCode
             from Customer
            where Status = 'Live'
              and Salesman is null)
   begin
      print 'Orphaned customer found - running orphans report...'
      exec orphans_report
      exec clear_orphans
   end

begin and end may enclose any number of statements.



The Case Statement.


Sybase also supports a case statement which is clearer in use than Oracle's decode and can, usefully, also support code blocks...

case @Office
   when 'Swindon' then exec SwindonMonthEnd
   when 'Slough' then exec SloughMonthEnd
   when 'Devizes' then exec DevizesMonthEnd
   else exec OfficeNotRecognised @Office
 


Coalesce


We just mentioned the Oracle decode statement and here's another take on the same process...

select @Office = coalesce(@MainOffice, @BranchOffice, @RepAlone)

This will select the first item in the list which is not NULL. All succeeding values are ignored. If all the values in the list are NULL, coalesce returns a NULL.




While Loops.


One of the nicest features in any language is the ability to continue an activity only so long as a particular condition is true, Transact SQL provides the while construct for this purpose. The following example shows the construct in use...

-- ---------------------------------------------------
-- This loop keeps going so long as it can find a sale
-- for the current office. UpdateSales removes each
-- transaction from SalesList as it processes it and
-- sets up the next one to be processed at the same
-- time...
-- ---------------------------------------------------
while (select Office
         from SalesList
        where ProcessThis = 'yes') = @ThisOffice
   begin
      exec UpdateSales @ThisOffice
      if (select ProductType
            from SalesLedger
           where Office = @ThisOffice
             and Posted = @ThisDate) != 'SERVICE ONLY'
         -- ----------------------------------------
         -- Ordinary sale, so processing finished
         -- Go back to beginning and do next sale...
         -- ----------------------------------------
         continue
      else
         -- ----------------------------------
         -- This is a non-product transaction.
         -- Set up the timesheet charging...
         -- ----------------------------------
         exec BuildTimesheet @ThisOffice
         if (select Status
               from TimeSheetControl
              where Office = @ThisOffice) = 'DUPLICATE'
             -- -----------------------------------------
             -- Oops! That shouldn't occur in real life
             -- Report the problem and quit processing...
             -- -----------------------------------------
             raiseerror(51097, 16, 1, @ThisOffice)
             break
   end

This is an idealised example of while. The condition attached to the while is evaluated and if TRUE, the code block following is executed. During the execution of the code block, a further test decides if further processing within the block should be ignored. If that is the case, continue is invoked, which restarts processing at the top of the loop with the while statement itself. If processing continues, the code then tests for an error condition and if this is detected, invokes break. The break command falls out of the loop and continues execution at the next statement which is not part of the loop's block, in this case, the first statement after end.



Go To, Thou..
.

Transact SQL supports a goto command which, the screams of structured programming purists apart, can be extremely useful for solving certain types of problem. To use goto, you need to define labels and, when invoked, goto will pass execution to the next statement following the label. Labels may be any string followed by a colon (:).

You need to be careful how you use goto because there are certain types of backwards references that Sybase cannot resolve. For this reason, use goto only as a weapon of last resort when no other programming construct will do and test, test, test to make sure it works as you expect...

LoadSalesTableStart:
   -- -------------------------------------------------
   -- If an error is detected, this will loop
   -- continuously unless interrupted by an operator...
   -- -------------------------------------------------
   exec LoadSalesTable
   exec UpdateSalesman
   exec WriteInvoices
   if (select status
         from RunControl
        where RunID = @ThisRun) != 'Success'
      begin
         raiserror(58932, 16, 1)
         goto LoadSalesTableStart
      end

The above example may seem a bit contrived but we've actually done it in real life when dealing with feeds that may not arrive exactly on time but without which further processing cannot continue.



Waiting for...


The Sybase waitfor command is one of those useful little facilities that no-one knows how to use unless they've got a problem only it can solve. The waitfor command operates, effectively, in two different ways. The first is as a simple timer. You give it either a length of delay or a time of day and Sybase will hold your process until the sepecified delay is over or the specified time of day is reached.

The other way it works is as an event handler. Used this way, you give waitfor an event to watch for and when the event occurs, execution will resume. The events you can wait for are errorexit (a process terminates abnormally) processexit (a process terminates for any reason) and mirrorexit (a read or write to a mirrored device fails)

For obvious reasons, the second use of waitfor is largely confined to DBAs who want to check for problems on the system. We've never yet found a reason to use waitfor in this way ourselves. The first version, though, is extremely useful for dealing with feeds from remote systems as in the following alternative version of the previous example...

-- ------------------------------------------------------------
-- Don't start this until 2:30PM to allow for feed to arrive...
-- ------------------------------------------------------------
waitfor time "14:30:00"

-- --------------------------
-- Feed should be here now...
-- --------------------------
LoadSalesTableStart:
   -- -------------------------------------------------
   -- If an error is detected, this will loop
   -- continuously unless interrupted by an operator...
   -- -------------------------------------------------
   exec LoadSalesTable
   exec UpdateSalesman
   exec WriteInvoices
   if (select status
         from RunControl
        where RunID = @ThisRun) != 'Success'
      begin
         -- ------------------------------------
         -- Probably the feed's late - log it...
         -- ------------------------------------
         raiserror(58932, 16, 1)

         -- ------------------------------------
         -- Give it 1/2 an hour and try again...
         -- ------------------------------------
         waitfor delay "00:30:00"
         goto LoadSalesTableStart
      end

Note that waitfor will ONLY accept constants, as shown in the example, and not variables. The way round it is shown in the next section.



Executive decisions...


The execute command takes any string and passes it to Sybase as a command. This allows you to effectively create programmes on the fly and to get around limitations in Sybase such as waitfor's inability to accept variables as arguments. Here's a rather contrived example...

declare @Command char(255),
        @CurrentTime datetime,
        @Hour tinyint
select @CurrentTime = getdate(),
       @Hour = datepart(hh, getdate())
select @Command = "waitfor time '" + str(@Hour) + ":45:00'"
execute @Command




Time to Return...


Finally, we come to the return command which terminates the current batch and optionally gives a value to the calling routine.

begin
   -- --------------------------------------------------
   -- If an error is detected, fall out straight away...
   -- --------------------------------------------------
   exec LoadSalesTable
   if (select status
         from RunControl
        where RunID = @ThisRun) != 'Success'
      return
   exec UpdateSalesman
   exec WriteInvoices
end




Sybase offers a surprisingly powerful set of programming tools within the confines of Transact SQL. It is worth exploring them before turning to additional and more complex programming systems.







Find out more by searching Google here...

Google