In this section, a number of run time analysis tools are applied to the QueryDbase program presented earlier.
Although the QueryDbase program performs little error checking, most problems will be detected immediately by the run time checks (NIL pointers, incorrect array indexing...). The program then stops and the problem location may be identified quickly using the debugger.
For instance, trying to fetch an entry without first opening a database will cause an access through a NIL database object.
cassis>LINUX/QueryDbase Enter command fetch key10 *** *** runtime error: *** attempt to dereference NIL *** IOT trap cassis>
Modula-3 programs may be compiled to extract line frequency execution data. This information may then be used to determine which portions were tested adequately or where most of the execution time is spent. The proper option has to be specified in the m3makefile and the program recompiled. The program is then run and creates a file named coverage.out with the desired information.
cassis>head -5 src/m3makefile m3_option("-Z") % database is located in our private package collection override("database","../..") import("database") cassis>rm -R LINUX cassis>m3build ... cassis>QueryDbase <example2.in >/dev/null cassis>ls LINUX/ coverage.out example2.in src/
This coverage information is then displayed using analyze_coverage.
cassis>analyze_coverage -S src -L *************** COVERAGE OF Query.m3 ... PROCEDURE PrintDbase(db: Dbase.T) = 1 VAR key, tmp: TEXT; cursor: CARDINAL := 0; BEGIN 1 key := Dbase.FirstKey(db); 1 WHILE key # NIL DO 100 IF cursor > LAST(keyArray^) THEN 4 oldKeyArray := keyArray; 4 keyArray := NEW(REF ARRAY OF TEXT,2 * NUMBER(oldKeyArray^)); 4 SUBARRAY(keyArray^,0,NUMBER(oldKeyArray^)) := oldKeyArray^; END; 100 keyArray[cursor] := key; 100 INC(cursor); 100 key := Dbase.NextKey(db); END; (* Use an inefficient sorting algorithm *) 1 FOR i := cursor - 1 TO 0 BY -1 DO 100 FOR j := 0 TO i - 1 DO 4950 IF Text.Compare(keyArray[j], keyArray[i]) = 1 THEN 890 tmp := keyArray[i]; 890 keyArray[i] := keyArray[j]; 890 keyArray[j] := tmp; END; END; END; (* Print all the keys *) 1 FOR i := 0 TO cursor - 1 DO 100 Wr.PutText(Stdio.stdout,keyArray[i] & "\n"); END; END PrintDbase; ... 2 LOOP 105 Wr.PutText(Stdio.stdout,"Enter command\n"); 105 Wr.Flush(Stdio.stdout); 105 Lex.Skip(Stdio.stdin); 105 command := Lex.Scan(Stdio.stdin); 105 IF Text.Equal(command,"exit") THEN EXIT; ELSIF Text.Equal(command,"create") THEN 0 Lex.Skip(Stdio.stdin); 0 name := Lex.Scan(Stdio.stdin); 0 db := Dbase.Create(name); ELSIF Text.Equal(command,"open") THEN 1 Lex.Skip(Stdio.stdin); 1 name := Lex.Scan(Stdio.stdin); 1 db := Dbase.Open(name); ELSIF Text.Equal(command,"close") THEN 1 Dbase.Close(db); ELSIF Text.Equal(command,"fetch") THEN 0 Lex.Skip(Stdio.stdin); 0 key := Lex.Scan(Stdio.stdin); 0 Wr.PutText(Stdio.stdout,Dbase.Fetch(db,key) & "\n"); ELSIF Text.Equal(command,"store") THEN 100 Lex.Skip(Stdio.stdin); 100 key := Lex.Scan(Stdio.stdin); 100 Lex.Skip(Stdio.stdin); 100 content := Lex.Scan(Stdio.stdin); 100 Dbase.Store(db,key,content); ELSIF Text.Equal(command,"delete") THEN 0 Lex.Skip(Stdio.stdin); 0 key := Lex.Scan(Stdio.stdin); 0 Dbase.Delete(db,key); ELSIF Text.Equal(command,"print") THEN 1 PrintDbase(db); ELSE 0 Wr.PutText(Stdio.stdout,"Incorrect command\n"); END; END; END Query.
This data reveals that some portions (fetching and deleting entries) were not tested (executed frequency is 0) and that most of the time is spent in the inefficient sorting performed in the PrintDbase procedure.
The usual profiling tools, for instance gprof, may also be used for finding where the execution time is spent in the call tree of a large program. These tools produce more complete information when all the modules and libraries involved have been compiled with the profiling flag. This may be achieved with the correct m3_option entry in the m3makefile or with a special m3build template which builds the program in a different directory. A special template is interesting because this way the profiled and ordinary versions of programs and libraries may coexist in separate directories.
The procedure is similar to coverage analysis. The program is recompiled and then run creating a file named gmon.out. This data is then processed and displayed using gprof. The explanations have been annotated directly on the output listing portions which follow.
cassis>m3build -b LINUX-PROF ... cassis>LINUX-PROF/QueryDbase <example2.in >/dev/null cassis>gprof LINUX-PROF/QueryDbase ... granularity: each sample hit covers 2 byte(s) for 0.09% of 11.49 seconds called/total parents index %time self descendents called+self name index called/total children ... # Each module initialization code is called from RunMainBodies. # __INITM_Query is the body of the main module (Query.m3). [4] 97.0 0.00 11.14 1 _RTLinker__RunMainBodies 0.01 11.13 1/1 __INITM_Query [5] 0.00 0.00 1/1 __INITM_ProcessPosix [112] 0.00 0.00 1/1 __INITM_Stdio [123] 0.00 0.00 1/1 __INITM_TimePosix [129] ... ----------------------------------------------- # __INITM_Query is called by RunMainBodies and calls a number # of procedures. On a total execution time of 11.13s, PrintDbase # uses 7.34s. 0.01 11.13 1/1 _RTLinker__RunMainBodies [5] 97.0 0.01 11.13 1 __INITM_Query [5] 1.14 7.34 1/1 _Query__PrintDbase [6] 0.06 0.97 1004/1004 _Wr__Flush [9] 0.03 0.71 3005/3005 _Lex__Scan [13] 0.02 0.63 1000/1000 _Dbase__Store [15] 0.01 0.13 3005/3005 _Lex__Skip [24] 0.01 0.04 1004/2004 _Wr__PutText [31] 0.04 0.00 6015/6015 _Text__Equal [48] 0.00 0.00 1/1 _Dbase__Create [119] 0.00 0.00 1/5044 _RTHooks__AllocateOpenArray <cycle 1> [28] 0.00 0.00 1/1 _Dbase__Close [584] ----------------------------------------------- # The bulk of the time spent in PrintDbase is consumed by Text__Compare. 1.14 7.34 1/1 __INITM_Query [5] [6] 73.8 1.14 7.34 1 _Query__PrintDbase [6] 7.06 0.00 499500/499500 _Text__Compare [7] 0.00 0.22 1000/1000 _Dbase__NextKey [22] 0.00 0.04 1000/2004 _Wr__PutText [31] 0.00 0.02 1000/4005 _RTHooks__Concat [33] 0.00 0.00 7/5044 _RTHooks__AllocateOpenArray <cycle 1> [28] 0.00 0.00 1/1 _Dbase__FirstKey [130] 0.00 0.00 7/7016 _memmove [61] ----------------------------------------------- # Text__Compare consumes the execution time itself, it does not # call other procedures. 7.06 0.00 499500/499500 _Query__PrintDbase [6] [7] 61.4 7.06 0.00 499500 _Text__Compare [7] ----------------------------------------------- # _syscall is called from a number of procedures, _write and # _fcntl being the most frequent. It does not however consume # too much execution time. 0.00 0.00 2/5993 _gettimeofday [128] 0.00 0.00 3/5993 _open [122] 0.00 0.00 5/5993 _fstat [118] 0.00 0.00 16/5993 _getrusage [101] 0.19 0.00 863/5993 _read [23] 0.45 0.00 2089/5993 _write [17] 0.65 0.00 3015/5993 _fcntl [14] [8] 11.3 1.30 0.00 5993 _syscall [8] ... # Summary of the time spent per procedure. # % cumulative self self total time seconds seconds calls ms/call ms/call name 55.2 7.06 7.06 499500 0.01 0.01 _Text__Compare [7] 10.2 8.36 1.30 5993 0.22 0.22 _syscall [8] 10.2 9.66 1.30 mcount (425) 8.9 10.80 1.14 1 1140.01 8481.51 _Query__PrintDbase [6] 2.1 11.07 0.27 29821 0.01 0.01 _Rd__GetChar [18] 1.0 11.20 0.13 1000 0.13 0.14 _dbm_do_nextkey [26] 0.8 11.30 0.10 38843 0.00 0.00 _RTHooks__LockMutex [30] ...
The profiling information is useful to see which procedure, called from where, consumes most of the execution time. These measures are nevertheless somewhat imprecise since a statistical sampling of the stack is used to evaluate the CPU usage. Furthermore, the profiling information does not tell much about the influence of a number of important factors such as cache memory misses, virtual memory page faults and input-output delays.
Although any debugger may be used to trace the execution through a Modula-3 program, it is preferable to have a Modula-3 aware debugger. This way, the Modula-3 syntax for specifying modules, procedures, variables, types and references may be used.
The debugger is used to understand the behavior of a program, thus finding errors, by tracing the execution and the content of variables. It may also be used to call data gathering procedures at certain points in the execution. One application of this is to study the memory allocation.
A session under the Modula-3 aware debugger m3gdb is presented and annotated below.
cassis>m3gdb LINUX/QueryDbase GDB is free software and you are welcome to distribute copies of it under certain conditions; type "show copying" to see the conditions. There is absolutely no warranty for GDB; type "show warranty" for details. GDB 940920 (EPC-M2 & SRC-M3) (i486-unknown-linux), Copyright 1994 Free Software Foundation, Inc... # The path to locate the source code is specified. # An initial break point is specified to examine the PrintDbase procedure (gdb) dir src (gdb) dir ../database/src (gdb) list Query.m3:0 1 MODULE Query EXPORTS Main; 2 3 (* This program accepts commands to create or open ndbm databases 4 and to store, fetch or delete elements. A sorted list of keys (gdb) break Query.PrintDbase Breakpoint 1 at 0xddf: file Query.m3, line 22. (gdb) run @M3novm <example2.in >/dev/null Starting program: LINUX/QueryDbase @M3novm <example2.in >/dev/null Breakpoint 1, Query.PrintDbase (db=16_e50f4) at Query.m3:22 22 key, tmp: TEXT; Current language: auto; currently m3 (gdb) where #0 Query.PrintDbase (db=16_e50f4) at Query.m3:22 #1 16_1945 in Query.m3.MAIN (phase=3) at Query.m3:111 #2 16_6799 in RTLinker.RunInitPhase (phase=3) at RTLinker.m3:25 #3 16_6969 in RTLinker.m3.MAIN (phase=3) at RTLinker.m3:67 #4 16_d85 in main (argc=1, argv=16_bffff960, envp=16_bffff968) at _m3main.c:2305 # RTutils.Heap prints the number of each object type currently allocated. # Pathname.T which is the same as TEXT (because of structural equivalence) # is the most common type here. (gdb) set lang c (gdb) print RTutils__Heap() Code Count TotalSize AvgSize Name ---- --------- --------- --------- -------------------------- 1 2235 38736 17 Pathname.T 8 2 24 12 RegularFile.T 10 1 12 12 Terminal.T 18 2 32 16 RTutils.Visitor 26 1 16 16 RefSeq.T 41 1 28 28 Atom.NewAtomTbl 50 3 36 12 Thread.Mutex 54 3 120 40 FileWr.T 60 1 40 40 FileRd.T 61 1 148 148 Thread.T 62 1 8 8 Thread.Condition ... 145 1 40 40 TextSeq.RefArray --------- --------- 2276 60372 $1 = void # The PrintDbase function is run to completion and the # Heap usage examined again. (gdb) finish Run till exit from #0 Query.PrintDbase () at Query.m3:22 0x1945 in Query.m3.MAIN () at Query.m3:111 111 PrintDbase(db); (gdb) print RTutils__Heap() Code Count TotalSize AvgSize Name ---- --------- --------- --------- -------------------------- 1 1929 31484 16 Pathname.T 8 2 24 12 RegularFile.T 10 1 12 12 Terminal.T 18 3 48 16 RTutils.Visitor 26 1 16 16 RefSeq.T 41 1 28 28 Atom.NewAtomTbl 50 3 36 12 Thread.Mutex 54 3 120 40 FileWr.T 60 1 40 40 FileRd.T 61 1 148 148 Thread.T 62 1 8 8 Thread.Condition 145 8 8224 1028 TextSeq.RefArray --------- --------- 1980 63716 # RTHeapStats.ReportReachable is called to determine the number # of active, reachable, objects (i.e. those which would not be # garbage collected). Of the 1980 objects on the heap, only slightly # more than half are still alive. (gdb) print RTHeapStats__ReportReachable() HEAP: 0xde000 .. 0x120000 => 0.2 Mbytes Module globals: # objects # bytes unit --------- -------- ----------------- 1007 26260 Query.m3 11 16656 Stdio.i3 2 2012 RTutils.m3 3 440 RTHeapPosix.m3 ... Global variable roots: # objects # bytes ref type location --------- -------- ---------- ----------------- ------------------------ 1001 24108 0x11e004 TextSeq.RefArray Query.m3 + 32 513 12300 0xe2004 TextSeq.RefArray Query.m3 + 36 3 4168 0xdea10 FileRd.T Stdio.i3 + 28 3 4168 0xdfa48 FileWr.T Stdio.i3 + 32 ... 1 24 0xf2aec Pathname.T Query.m3 + 56 1 20 0xf2b04 Pathname.T Query.m3 + 44 1 20 0xe50e0 Pathname.T Query.m3 + 48 1 20 0xf2ad8 Pathname.T Query.m3 + 52 1 16 0xde09c Thread.Mutex RTHeapPosix.m3 + 464 ... # KeyArray and oldKeyArray point to 1001 and 513 objects respectively # # The program is rerun. PrintDbase is executed one line at a time (gdb) run Starting program: LINUX/QueryDbase @M3novm <example2.in >/dev/null Breakpoint 1, Query.PrintDbase () at Query.m3:22 22 key, tmp: TEXT; (gdb) set lang m3 (gdb) n 23 cursor: CARDINAL := 0; (gdb) n 25 key := Dbase.FirstKey(db); (gdb) n 26 WHILE key # NIL DO (gdb) n 27 IF cursor > LAST(keyArray^) THEN (gdb) n 32 keyArray[cursor] := key; (gdb) n 33 INC(cursor); # The type and content of variables may be displayed. (gdb) print key $5 = "key515" (gdb) ptype key type = BRANDED "Text 1.0" REF Convert.Buffer (gdb) ptype key^ type = ARRAY OF CHAR (gdb) # This text object (key) is noted to check if it is free later on. (gdb) print RTHeapDebug.Free(key) $6 = 0 (gdb) finish Run till exit from #0 Query.PrintDbase (db=16_e50f4) at Query.m3:33 16_1945 in Query.m3.MAIN (phase=3) at Query.m3:111 111 PrintDbase(db); (gdb) print RTHeapDebug__CheckHeap() Path to 'free' object: Ref in root at address 0x48644... Object of type TextSeq.RefArray at address 11e34c... Free object of type Pathname.T at address 11ea90... $9 = void (gdb) set lang m3 (gdb) print keyArray $10 = 16_11e34c # Even though the PrintDbase procedure is finished, keyArray still # exists and maintains alive the keys printed.
In the above example, two allocation problems are identified. For this, the number of expected active objects may be checked against the information produced by RTHeapStats.ReportReachable. Another lead is to assert that a certain object is free and let RTHeapDebug.CheckHeap determine if a path still reaches the object and keeps it from being garbage collected.
A first problem is that the oldKeyArray should be released with oldKeyArray := NIL; once it has been used to initialize the new keyArray. A second problem is that the keyArray is not released once PrintDbase() is finished. It may be set to NIL, or if it is to be reused, its content should be set to NIL (keyArray[0] := NIL, keyArray[1]...).
It is interesting to note that keyArray reaches 1001 objects and oldKeyArray 513 even though the Query module as a whole reaches only 1007 objects. The explanation is that the 512 keys stored in oldKeyArray are also stored in keyArray and thus are not distinct objects.
Three tools are available to graphically display statistics about Modula-3 programs.
The QueryDbase example accepts simple commands from the user. Storing intermediate results for further queries, defining new commands and other similar enhancements are not supported and would require a significant programming effort to incorporate.
This is where using an existing embeddable interpreter provides a much enhanced functionality with little efforts. An embeddable interpreter for the simple yet powerful Obliq language is available. It is written in Modula-3 and is easy to interface to Modula-3 programs.
The QueryDbase program may thus be replaced by an Obliq interpreter where access functions to the database package have been added. The new program is named OblQuery. Its m3makefile and implementation Main.m3 follow.
import("synloc") import("obliqrt") import("obliqlibm3") import("obliq") override("database","../..") import("database") implementation("Main") Program("OblQuery")
MODULE Main; IMPORT ObliqOnline; IMPORT ObLib, ObLibM3, ObLibM3Help, Text, SynWr, SynLocation, ObCommand, ObValue, Dbase; (* Obliq objects are defined for the Database objects and commands *) TYPE ValDbase = ObValue.ValAnything BRANDED OBJECT value: Dbase.T; OVERRIDES Is := IsDbase; Copy := CopyDbase; END; OpCodes = ARRAY OF ObLib.OpCode; QueryCode = {Error, Create, Open, Close, Fetch, Store, Delete, First, Next}; QueryOpCode = ObLib.OpCode OBJECT code: QueryCode; END; PackageQuery = ObLib.T OBJECT OVERRIDES Eval:=EvalQuery; END; (* Support for Help messages *) CONST Greetings = " OblQuery (obliq with database libraries) (say \'help;\' for help)"; HelpSummary = " db (the built-in database library)\n"; HelpText = " create(file: TEXT): Dbase.T\n" & " open(file: TEXT): Dbase.T\n" & " close(db: Dbase.T)\n" & " fetch(db: Dbase.T; key: TEXT): TEXT\n" & " store(db: Dbase.T; key, content: TEXT)\n" & " delete(db: Dbase.T; key: TEXT)\n" & " first(db: Dbase.T): TEXT\n" & " next(db: Dbase.T): TEXT\n"; VAR queryException: ObValue.ValException; PROCEDURE HelpQuery(self: ObCommand.T; arg: TEXT; <*UNUSED*>data: REFANY:=NIL) = BEGIN IF Text.Equal(arg, "!") THEN SynWr.Text(SynWr.out,HelpSummary); ELSIF Text.Equal(arg, "?") THEN SynWr.Text(SynWr.out, HelpText); SynWr.NewLine(SynWr.out); ELSE SynWr.Text(SynWr.out, "Command " & self.name & ": bad argument: " & arg); SynWr.NewLine(SynWr.out); END; END HelpQuery; PROCEDURE IsDbase(self: ValDbase; other: ObValue.ValAnything): BOOLEAN = BEGIN TYPECASE other OF ValDbase(oth)=> RETURN self.value = oth.value; ELSE RETURN FALSE END; END IsDbase; PROCEDURE CopyDbase(<*UNUSED*>self: ObValue.ValAnything; <*UNUSED*>tbl: ObValue.Tbl; loc: SynLocation.T): ObValue.ValAnything RAISES {ObValue.Error} = BEGIN ObValue.RaiseError("Cannot copy db database", loc); END CopyDbase; PROCEDURE NewQueryOC(name: TEXT; arity: INTEGER; code: QueryCode) : QueryOpCode = BEGIN RETURN NEW(QueryOpCode, name:=name, arity:=arity, code:=code); END NewQueryOC; (* Define and register the new commands in the db package *) PROCEDURE PackageSetup() = VAR opCodes: REF OpCodes; BEGIN opCodes := NEW(REF OpCodes, NUMBER(QueryCode)); opCodes^ := OpCodes{ NewQueryOC("failure", -1, QueryCode.Error), NewQueryOC("create", 1, QueryCode.Create), NewQueryOC("open", 1, QueryCode.Open), NewQueryOC("close", 1, QueryCode.Close), NewQueryOC("fetch", 2, QueryCode.Fetch), NewQueryOC("store", 3, QueryCode.Store), NewQueryOC("delete", 2, QueryCode.Delete), NewQueryOC("first", 1, QueryCode.First), NewQueryOC("next",1, QueryCode.Next) }; ObLib.Register( NEW(PackageQuery, name:="db", opCodes:=opCodes)); queryException := NEW(ObValue.ValException, name:="db_failure"); ObLib.RegisterHelp("db", HelpQuery); END PackageSetup; (* For each database command, check the arguments, call the relevant procedure and wrap the result in Obliq objects. *) PROCEDURE EvalQuery(self: PackageQuery; opCode: ObLib.OpCode; <*UNUSED*>arity: ObLib.OpArity; READONLY args: ObValue.ArgArray; loc: SynLocation.T) : ObValue.Val RAISES {ObValue.Error, ObValue.Exception} = VAR text1, text2: TEXT; dbase: Dbase.T; BEGIN CASE NARROW(opCode, QueryOpCode).code OF | QueryCode.Error => RETURN queryException; | QueryCode.Create => TYPECASE args[1] OF | ObValue.ValText(node) => text1:=node.text; ELSE ObValue.BadArgType(1, "text", self.name, opCode.name, loc); END; dbase := Dbase.Create(text1); IF dbase = NIL THEN ObValue.RaiseException(queryException, opCode.name, loc); END; RETURN NEW(ValDbase, what:="<a database>", picklable:=FALSE, value:=dbase); | QueryCode.Open => TYPECASE args[1] OF | ObValue.ValText(node) => text1:=node.text; ELSE ObValue.BadArgType(1, "text", self.name, opCode.name, loc); END; dbase := Dbase.Open(text1); IF dbase = NIL THEN ObValue.RaiseException(queryException, opCode.name, loc); END; RETURN NEW(ValDbase, what:="<a database>", picklable:=FALSE, value:=dbase); | QueryCode.Close => TYPECASE args[1] OF | ValDbase(node) => dbase:=node.value; ELSE ObValue.BadArgType(1, "database", self.name, opCode.name, loc); END; Dbase.Close(dbase); RETURN ObValue.valOk; | QueryCode.Fetch => TYPECASE args[1] OF | ValDbase(node) => dbase:=node.value; ELSE ObValue.BadArgType(1, "database", self.name, opCode.name, loc); END; TYPECASE args[2] OF | ObValue.ValText(node) => text1:=node.text; ELSE ObValue.BadArgType(2, "text", self.name, opCode.name, loc); END; text2 := Dbase.Fetch(dbase,text1); IF text2 = NIL THEN ObValue.RaiseException(queryException, opCode.name, loc); END; RETURN NEW(ObValue.ValText, text:=text2); | QueryCode.Store => TYPECASE args[1] OF | ValDbase(node) => dbase:=node.value; ELSE ObValue.BadArgType(1, "database", self.name, opCode.name, loc); END; TYPECASE args[2] OF | ObValue.ValText(node) => text1:=node.text; ELSE ObValue.BadArgType(2, "text", self.name, opCode.name, loc); END; TYPECASE args[3] OF | ObValue.ValText(node) => text2:=node.text; ELSE ObValue.BadArgType(3, "text", self.name, opCode.name, loc); END; Dbase.Store(dbase,text1,text2); RETURN ObValue.valOk; | QueryCode.Delete => TYPECASE args[1] OF | ValDbase(node) => dbase:=node.value; ELSE ObValue.BadArgType(1, "database", self.name, opCode.name, loc); END; TYPECASE args[2] OF | ObValue.ValText(node) => text1:=node.text; ELSE ObValue.BadArgType(2, "text", self.name, opCode.name, loc); END; Dbase.Delete(dbase,text1); RETURN ObValue.valOk; | QueryCode.First => TYPECASE args[1] OF | ValDbase(node) => dbase:=node.value; ELSE ObValue.BadArgType(1, "database", self.name, opCode.name, loc); END; text1 := Dbase.FirstKey(dbase); IF text1 = NIL THEN ObValue.RaiseException(queryException, opCode.name, loc); END; RETURN NEW(ObValue.ValText, text:=text1); | QueryCode.Next => TYPECASE args[1] OF | ValDbase(node) => dbase:=node.value; ELSE ObValue.BadArgType(1, "database", self.name, opCode.name, loc); END; text1 := Dbase.NextKey(dbase); IF text1 = NIL THEN ObValue.RaiseException(queryException, opCode.name, loc); END; RETURN NEW(ObValue.ValText, text:=text1); ELSE ObValue.BadOp(self.name, opCode.name, loc); END; END EvalQuery; BEGIN ObliqOnline.Setup(); ObLibM3.PackageSetup(); ObLibM3Help.Setup(); PackageSetup(); ObliqOnline.Interact(ObliqOnline.New(Greetings)); END Main.
The full Obliq language now becomes accessible to interact with the database procedures, and to store and process the results.
cassis>LINUX/OblQuery OblQuery (obliq with database libraries) (say 'help;' for help) - help db; create(file: TEXT): Dbase.T open(file: TEXT): Dbase.T close(db: Dbase.T) fetch(db: Dbase.T; key: TEXT): TEXT store(db: Dbase.T; key, content: TEXT) delete(db: Dbase.T; key: TEXT) first(db: Dbase.T): TEXT next(db: Dbase.T): TEXT - let d = db_create("testdb"); let d = <a database> - db_store(d,"key1","content1"); ok - db_store(d,"key2","content2"); ok - db_store(d,"key3","content3"); ok - db_fetch(d,"key2"); "content2" - db_fetch(d,"bad key"); Uncaught exception (input line 8, char 10) db_failure (fetch) Error detected (last input line, char 10) - db_first(d); "key1" - ^D cassis>