MODULE SqlObxDriv;
(**

   project   = "BlackBox"
   organization   = "www.oberon.ch"
   contributors   = "Oberon microsystems"
   version   = "System/Rsrc/About"
   copyright   = "System/Rsrc/About"
   license   = "Docu/BB-License"
   changes   = ""
   issues   = ""

**)

   (* this is a template to be filled out *)

   IMPORT Dates, Dialog, Services, SqlDrivers;

   CONST

      (* general error code *)
      connectionsExceeded = 4;
      outOfTables = 5;
      notExecutable = 6;
      cannotOpenDB = 7;
      wrongIdentification = 8;
      
      (* table read error codes *)
      converted = 1;
      truncated = 2;
      overflow = 3;
      incompatible = 4;
      
      (* states *)
      connecting = 1;
      connected = 2;
      executing = 3;
      closed = 4;
      
   TYPE
      Table = POINTER TO RECORD (SqlDrivers.Table)
         rows, columns: INTEGER;
         (* additional resultset related data *)
      END;
      Driver = POINTER TO RECORD (SqlDrivers.Driver)

         (* the state variable is not strictly necessary, it is used here to show the conceptual states *)
         state: INTEGER;   (* connecting, connected, executing, closed *)
         (* additional database related data *)
      END;
   

   (* Driver *)
   PROCEDURE (d: Driver) Ready (): BOOLEAN;

   BEGIN
      (* check for completion if asynchronous operation in progress *)
      RETURN (d.state # closed) & <asynchronous operation terminated>
   END Ready;
   PROCEDURE (d: Driver) EndOpen (OUT res: INTEGER);

   (* Pre: database connection was initiated by Open   20
            d.Ready()   21 *)
   BEGIN
      ASSERT(d.state = connecting, 20);
      ASSERT(d.Ready(), 21);
      (* deliver result of asynchronous database connection request *)
      (* return res = 0 if open is synchronous *)
      d.state := connected; res := 0
   END EndOpen;
   PROCEDURE (d: Driver) BeginExec (IN statement: ARRAY OF CHAR; data: SqlDrivers.Blob;

                                             async, showErr: BOOLEAN; OUT res: INTEGER);
   (* asynchronous execution is optional, a driver may legally execute synchronously if async is TRUE *)
   (* Pre: statement # ""   20
            no other execution started   21 *)
   (* Post: res = 0 <=> execution has started and will be completed later with EndExec
            ~async => d.Ready() *)
   BEGIN
      ASSERT(statement # "", 20);
      ASSERT(d.state = connected, 21);
      WHILE data # NIL DO
         (* pass blob data in database specific way *)
         data := data.next
      END;
      IF async THEN
         (* try to start asynchronous execution of statement *)
      ELSE
         (* try to execute statement *)
      END;
      IF <successful> THEN
         d.state := executing; res := 0
      ELSE
         res := notExecutable
      END
   END BeginExec;
   PROCEDURE (d: Driver) EndExec (VAR t: SqlDrivers.Table; OUT rows, columns, res: INTEGER);

   (* Pre: execution was started successfully with BeginExec   20
            d.Ready()   21 *)
   (* Post: res = 0<=>(t # NIL) OR (rows = 0) & (columns = 0) *)
      VAR h: Table;
   BEGIN
      ASSERT(d.state = executing, 20);
      ASSERT(d.Ready(), 21);
      d.state := connected;
      IF <successful execution returning a result set> THEN
         NEW(h); h.Init(d);
         (* initialize table data *)
         h.columns := <number of columns in result set>;   (* h.columns > 0 *)
         h.rows := <number of rows in result set>;      (* h.rows >= 0 *)
         t := h; columns := h.columns; rows := h.rows; res := 0
      ELSIF <successful execution without result set> THEN
         t := NIL; columns := 0; rows := 0; res := 0
      ELSE (* execution failed *)
         t := NIL; columns := 0; rows := 0; res := notExecutable
      END
   END EndExec;
   PROCEDURE (d: Driver) Commit (accept: BOOLEAN; OUT res: INTEGER);

   BEGIN
      IF accept THEN
         (* commit actual transaction *)
         res := 0
      ELSE
         IF <rollback possible> THEN
            (* rollback actual transaction *)
            res := 0
         ELSE res := notExecutable
         END
      END
   END Commit;
   PROCEDURE (d: Driver) Cleanup;

   BEGIN
      (* cleanup database data *)
      (* release database specific resources *)
      d.state := closed
   END Cleanup;
   
   (* Table *)

   
   PROCEDURE (t: Table) ReadInteger (row, column: INTEGER; OUT val: INTEGER);
   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read an integer from table *)
      IF <table value is not an integer> THEN val := 0; t.res := incompatible
      ELSIF <table value is out of range> THEN val := 0; t.res := overflow
      ELSE val := <value>
      END
   END ReadInteger;
   PROCEDURE (t: Table) ReadReal (row, column: INTEGER; OUT val: LONGREAL);

   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a real from table *)
      IF <table value is not a real> THEN val := 0; t.res := incompatible
      ELSIF <table value is out of range> THEN val := 0; t.res := overflow
      ELSE val := <value>
      END
   END ReadReal;
   PROCEDURE (t: Table) ReadDate (row, column: INTEGER; OUT val: Dates.Date);

   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a date from table *)
      IF <table value is not a date> THEN
         val.year := 0; val.month := 0; val.day := 0; t.res := incompatible
      ELSE
         val.year := <value>; val.month := <value>; val.day := <value>
      END
   END ReadDate;
   PROCEDURE (t: Table) ReadTime (row, column: INTEGER; OUT val: Dates.Time);

   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a time from table *)
      IF <table value is not a time> THEN
         val.hour := 0; val.minute := 0; val.second := 0; t.res := incompatible
      ELSE
         val.hour := <value>; val.minute := <value>; val.second := <value>
      END
   END ReadTime;
   PROCEDURE (t: Table) ReadCurrency (row, column: INTEGER; OUT val: Dialog.Currency);

   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a currency from table *)
      IF <table value is not a currency> THEN
         val.val := 0; val.scale := 0; t.res := incompatible
      ELSE
         val.val := <value>; val.scale := <value>
      END
   END ReadCurrency;
   PROCEDURE (t: Table) ReadString (row, column: INTEGER; OUT str: ARRAY OF CHAR);

      (* any value should be readable as a string *)
   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a string from table *)
      IF <table value is not a string> THEN
         (* convert table value to a string *)
         t.res := converted
      END;
      IF <length of string (incl 0X)> > LEN(str) THEN
         (* copy truncated string to str *)
         t.res := truncated
      ELSE
         (* copy string to str *)
      END
   END ReadString;
   PROCEDURE (t: Table) ReadVarString (row, column: INTEGER; OUT str: SqlDrivers.String);

      (* any value should be readable as a string *)
   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a string from table *)
      IF <table value is not a string> THEN
         (* convert table value to a string *)
         t.res := converted
      END;
      IF (str = NIL) OR (<length of string (incl 0X)> > LEN(str^)) THEN
         NEW(str, <length of string (incl 0X)>)
      END;
      (* copy string to str^ *)
   END ReadVarString;
   
   PROCEDURE (t: Table) ReadBlob (row, column: INTEGER; VAR len: INTEGER;
                                          OUT data: POINTER TO ARRAY OF BYTE);
      (* blobs are not zero terminated *)
   BEGIN
      ASSERT(row >= 0, 20); ASSERT(row < t.rows, 21);
      ASSERT(column >= 0, 22); ASSERT(column < t.columns, 23);
      (* try to read a blob from table *)
      IF <table value is not a blob> THEN
         len := 0; t.res := incompatible
      ELSE
         len := <length of blob>;
         IF (data = NIL) OR (len > LEN(data^)) THEN NEW(str, len) END;
         (* copy blob data to data^ *)
      END
   END ReadBlob;
   PROCEDURE (t: Table) ReadName (column: INTEGER; OUT str: SqlDrivers.String);

   BEGIN
      ASSERT(column >= 0, 20); ASSERT(column < t.columns, 21);
      (* get column name *)
      IF (str = NIL) OR (<length of name (incl 0X)> > LEN(str^)) THEN
         NEW(str, <length of name (incl 0X)>)
      END;
      (* copy name to str^ *)
   END ReadName;
   PROCEDURE (t: Table) ReadType (column: INTEGER; OUT str: SqlDrivers.String);

   BEGIN
      ASSERT(column >= 0, 20); ASSERT(column < t.columns, 21);
      (* get column type *)
      IF (str = NIL) OR (<length of type (incl 0X)> > LEN(str^)) THEN
         NEW(str, <length of type (incl 0X)>)
      END;
      (* copy type to str^ *)
   END ReadType;
   PROCEDURE (t: Table) Cleanup;

   BEGIN
      (* cleanup resultset data *)
      (* release resultset specific resources *)
   END Cleanup;
   PROCEDURE Open* (id, password, datasource: ARRAY OF CHAR;


                                    async, showErr: BOOLEAN; VAR d: SqlDrivers.Driver; OUT res: INTEGER);
   (* asynchronous operation is optional, a driver may legally open synchronously if async is TRUE *)
   (* Post: res = 0 <=> connection has started and will be completed later with EndOpen
            res = 0 <=> d # NIL
            ~async & (res = 0) => d.Ready() *)
      VAR h: Driver;
   BEGIN
      (* check id & password *)
      IF <valid> THEN
         IF async THEN
            (* try to start connect request to datasource *)
         ELSE
            (* try to connect to datasource *)
         END;
         IF <successful> THEN
            NEW(h); h.Init("<blob representation in statements>");
            (* Initialize driver data *)
            h.state := connecting; d := h; res := 0
         ELSE
            d := NIL; res := cannotOpenDB
         END
      ELSE
         d := NIL; res := wrongIdentification
      END
   END Open;
END SqlObxDriv.