home *** CD-ROM | disk | FTP | other *** search
/ Collection of Hack-Phreak Scene Programs / cleanhpvac.zip / cleanhpvac / WINER.ZIP / CHAP7.TXT < prev    next >
Text File  |  1994-09-01  |  72KB  |  1,705 lines

  1.                                 CHAPTER 7
  2.  
  3.                     NETWORK AND DATABASE PROGRAMMING
  4.  
  5.    In Chapter 6 you learned the principles of accessing files with BASIC,
  6. and saw the advantages and disadvantages of each of the various methods. 
  7. This chapter continues the coverage of file handling in BASIC by discussing
  8. the concepts of database application programming.  In particular, this
  9. chapter will cover database file structures--including fixed and variable
  10. length records--as well as the difference between code- and data-driven
  11. applications.
  12.    This chapter also provides an in-depth look at the steps needed to write
  13. applications that can run on a network.  This is an important topic that
  14. is fast becoming even more important, and very little information is
  15. available for programmers using BASIC.  I will discuss the various file
  16. access schemes and record locking techniques, and also how to determine if
  17. a program is currently running on a network and if so which one.
  18.    This chapter examines common database file formats including the one
  19. used by dBASE III Plus, and utility programs are provided showing how to
  20. access these files.  I will explain some of the fundamental issues of
  21. database design, including relationships between files.  Also presented is
  22. a discussion of the common indexing techniques available, and a comparison
  23. of the relative advantages and disadvantages of each.  You will also learn
  24. about the Structured Query Language (SQL) data access method, and
  25. understand the advantages it offers in an application programming context. 
  26. Finally, several third-party add-on products that facilitate database
  27. application programming will be described.
  28.  
  29.  
  30. DATA FILES VERSUS DATA MANAGEMENT
  31. =================================
  32.  
  33. Almost every application you create will require some sort of file access,
  34. if only to store configuration information.  Over time, programmers have
  35. developed hundreds of methods for storing information including sequential
  36. files, random files, and so forth.  However, this type of data file
  37. management must not be confused with database management in the strict
  38. sense.  Database management implies repeated data structures and
  39. relationships, with less importance given to the actual data itself.
  40.    In Chapter 6 you learned two common methods for defining the structure
  41. of a random access data file.  But whether you use FIELD or TYPE, those
  42. examples focused on defining a record layout that is known in advance. 
  43. When the data format will not change, defining a file structure within your
  44. program as FIELD or TYPE statements makes the most sense--a single
  45. statement can directly read or write any record in the file very quickly. 
  46. But this precludes writing a general purpose database program such as
  47. dBASE, DataEase, or Paradox.  In programs such as these, the user must be
  48. allowed to define each field and thus the record structure.
  49.    The key to the success of these commercial programs is therefore in
  50. their flexibility.  If you need to write routines for forms processing,
  51. expression evaluation, file sorting, reports, and so forth, you should
  52. strive to make them reusable.  For example, if you intend to print a report
  53. from a data file whose records have 100 fields, do you really want to use
  54. 100 explicit PRINT statements?  The ideal approach is to create a generic
  55. report module that uses a loop to print each selected field in each of the
  56. selected records.  This is where the concept of data-driven programming
  57. comes into play.
  58.  
  59.  
  60. DATA-DRIVEN PROGRAMMING
  61.  
  62. Data-driven programming, as its name implies, involves storing your data
  63. definitions as files, rather then as explicit statements in the program's
  64. BASIC source code.  The advantage to this method of database programming
  65. lies in its flexibility and reusability.  By storing the data definitions
  66. on disk, you can use one block of code to perform the same operations on
  67. completely different sets of data.
  68.    There are two general methods of storing data definitions on a disk--in
  69. the same file as the actual data or in a separate file.  Storing the record
  70. definition in a separate file is the simplest approach, because it allows
  71. the main data file to be comprised solely of identical-length records. 
  72. Keeping both the record layout and the data itself in a single file
  73. requires more work on your part, but with the advantage of slightly less
  74. disk clutter.  In either case, some format must be devised to identify the
  75. number of fields in each data record and their type.
  76.    The example below shows a typical field layout definition, along with
  77. code to determine the number of fields in each record.  Please understand
  78. that the random access file considered here is a file of field definitions,
  79. and not actual record data.
  80.  
  81. TYPE FldRec
  82.   FldName AS STRING * 15
  83.   FldType AS STRING * 1
  84.   FldOff  AS INTEGER
  85.   FldLen  AS INTEGER
  86. END TYPE
  87.  
  88. OPEN "CUST.FLD" FOR BINARY AS #1
  89. TotalFields% = LOF(1) \ 20
  90. DIM FldStruc(1 TO TotalFields%) AS FldRec
  91.  
  92. RecLength% = 0
  93. FOR X% = 1 TO TotalFields%
  94.   GET #1, , FldStruc(X%)
  95.   RecLength% = RecLength% + FldStruc(X%).FldLen
  96. NEXT
  97. CLOSE #1
  98.  
  99.  
  100. In this program fragment, 15 characters are set aside for each field's
  101. name, a single byte is used to hold a field type code (1 = string, 2 =
  102. currency, or whatever), and integer offset and length values show how far
  103. into the record each field is located and how long it is.  Once the field
  104. definitions file has been opened, the number of fields is easily determined
  105. by dividing the file size by the known 20-byte length of each entry.  From
  106. the number of you fields you can then dimension an array and read in the
  107. parameters of each field as shown here.
  108.    Notice that the record length is accumulated as each field description
  109. in read from the field definition file.  In a real program, two field
  110. lengths would probably be required: the length of the field as it appears
  111. on the screen and the number of bytes it will actually require in the
  112. record.  For example, a single precision number is stored on disk in only
  113. four bytes, even though as many as seven digits plus a decimal point could
  114. be displayed on the data entry screen.  Therefore, the method shown in this
  115. simple example to accumulate the record lengths would be slightly more
  116. involved in practice.
  117.    Once the number and size of each field is known, it is a simple matter
  118. to assign a string to the correct length to hold a single data record.  Any
  119. record could then be retrieved from the file, and its contents displayed
  120. as shown following.
  121.  
  122.  
  123. OPEN "CUST.DAT" FOR RANDOM AS #1 LEN = RecLength%
  124. Record$ = SPACE$(RecLength%)
  125. GET #1, 1, Record$
  126. CLOSE #1
  127.  
  128. FOR X% = 1 TO TotalFields%
  129.   FldText$ = MID$(Record$, FldStruc(X%).FldOff, FldStruc(X%).FldLen)
  130.   PRINT FldStruc(X%).FldName; ": "; FldText$
  131. NEXT
  132.  
  133.  
  134. Here, the first record in the file is read, and then the function form of
  135. MID$ is used to extract each data field from that record.  Assigning
  136. individual fields is just as easy, using the complementary statement form
  137. of MID$:
  138.  
  139.  
  140. MID$(Record$, FldStruc(FldNum).FldOff, FldStruc(FldNum).FldLen) = NewText$
  141.  
  142.  
  143. Understand that the entire point of this exercise is to show how a generic
  144. routine to access files can be written, and without having to establish the
  145. record structure when you write the program.  Although you could use FIELD
  146. instead of MID$ to assign and retrieve the information from each field,
  147. that works only when the field information is kept in a separate file.  If
  148. the field definitions are in the same file as the data, you will have to
  149. use purely binary file access, to account for the fixed header offset at
  150. the start of the file.
  151.    When you tell BASIC to open a file for random access, it uses the record
  152. length to determine where each record begins in the file.  But if a header
  153. portion is at the beginning of the file, a fixed offset must be added to
  154. skip over the header.  Since BASIC does not accommodate specifying an
  155. offset this way, it is up to you to handle that manually.  However, the
  156. added complexity is not really that difficult, as you will see shortly in
  157. the routines that create and access dBASE files.
  158.    dBASE--and indeed, most commercial database products--store the field
  159. information in the same file that contains the data.  This has the primary
  160. advantage of consolidating information for distribution purposes.  [For
  161. example, if your company sells a database of financial information, this
  162. minimizes the number of separate files your users will have to deal with.] 
  163. Modern header structures are variable length, which allows for a greater
  164. optimization of disk space.  In fact, most header structures mimic the
  165. record array shown above, but also store information such as the length
  166. of the header and the number of fields.  This is needed because the number
  167. of fields cannot be determined from the file size alone, when the file also
  168. holds the data.
  169.  
  170.  
  171. THE DBASE III FILE STRUCTURE
  172.  
  173. The description of the dBASE file structure that follows serves two
  174. important purposes:  First, it shows you how such a data file is
  175. constructed using a real world example.  Second, this information allows
  176. you to directly access dBASE files in programs of your own.  If you
  177. presently write commercial software--or if you aspire to--being compatible
  178. with the dBASE standard can give your product a definite advantage in the
  179. marketplace.  Table 7-1 identifies each component of the dBASE file header.
  180.  
  181. Offset  Contents
  182. ------  --------------------------------------------------
  183.  
  184.     1   dBASE version (3, or &H83 if there's a memo file)
  185.     2   Year of last update
  186.     3   Month of last update
  187.     4   Day of last update
  188.   5-8   Total number of records in the file (long integer)
  189.  9-10   Number of bytes in the header (integer)
  190. 11-12   Length of records in the file (integer)
  191. 13-32   Reserved
  192.  
  193.    The remainder of the header holds the field definitions, built from a
  194.    repeating group of 32-byte blocks structured as follows:
  195.  
  196. 33-42   Field name, padded with CHR$(0) null bytes
  197.    43   Always zero
  198.    44   Field type (C, D, L, M, or N)
  199. 45-48   Reserved
  200.    49   Field width
  201.    50   Number of decimal places (Numeric fields only)
  202. 51-64   Reserved
  203.  
  204. Notes:
  205.  
  206. 1. The end of the header is marked with a byte value of 13.
  207. 2. The possible field types at byte 44 are Character, Date, Yes/No, Memo,
  208. and Numeric.
  209.  
  210. Table 7.1: The Structure of a dBASE III File Header
  211.  
  212. To obtain any item of information from the header you will use the binary
  213. form of GET #.  For example, to read the number of data records in the file
  214. you would do this:
  215.  
  216.  
  217. OPEN "CUST.DBF" FOR BINARY AS #1
  218. GET #1, 5, NumRecords&
  219. CLOSE #1
  220.  
  221.  
  222. And to determine the length of each data record you will instead use this:
  223.  
  224.  
  225. OPEN "CUST.DBF" FOR BINARY AS #1
  226. GET #1, 1, RecordLength%
  227. CLOSE #1
  228. PRINT "The length of each record is "; RecordLength%
  229.  
  230.  
  231. In the first example, GET # is told to seek to the fifth byte in the file
  232. and read the four-byte long integer stored there.  The second example is
  233. similar, except it seeks to the 11th byte in the file and reads the integer
  234. record length field.  One potential limitation you should be aware of is
  235. BASIC does not offer a byte-sized variable type.  Therefore, to read a byte
  236. value such as the month you must create a one-character string, read the
  237. byte with GET #, and finally use the ASC function to obtain its value:
  238.  
  239.  
  240. Month$ = " "
  241. GET #1, 3, Month$
  242. PRINT "The month is "; ASC(Month$)
  243.  
  244.  
  245. Likewise, you will use CHR$ to assign a new byte value prior to writing a
  246. one-character string:
  247.  
  248.  
  249. Month$ = CHR$(NewMonth%)
  250. PUT #1, 3, Month$
  251.  
  252.  
  253. With this information in hand, it is a simple matter to open a dBASE file,
  254. and by reading the header determine everything your program needs to know
  255. about the structure of the data in that file.  The simplest way to do this
  256. is by defining a TYPE variable for the first portion of the header, and a
  257. TYPE array to hold the information about each field.  Since both the record
  258. and field header portions are each 32 bytes in length, you can open the
  259. file for Random access.  A short program that does this is shown below.
  260.  
  261. TYPE HeadInfo
  262.   Version  AS STRING * 1
  263.   Year     AS STRING * 1
  264.   Month    AS STRING * 1
  265.   Day      AS STRING * 1
  266.   TRecs    AS LONG
  267.   HLen     AS INTEGER
  268.   RecLen   AS INTEGER
  269.   Padded   AS STRING * 20
  270. END TYPE
  271.  
  272. TYPE FieldInfo
  273.   FName AS STRING * 10
  274.   Junk1 AS STRING * 1
  275.   FType AS STRING * 1
  276.   Junk2 AS STRING * 4
  277.   FLen  AS STRING * 1
  278.   Dec   AS STRING * 1
  279.   Junk3 AS STRING * 14
  280. END TYPE
  281.  
  282. DIM Header AS HeadInfo
  283.  
  284. OPEN "CUST.DBF" FOR RANDOM AS #1 LEN = 32
  285. GET #1, 1, Header
  286. TFields% = (Header.HLen - 32) \ 32
  287. REDIM FInfo(1 TO TFields%) AS FieldInfo
  288.  
  289. FOR X% = 2 TO TFields%
  290.   GET #1, X%, FInfo(X%)
  291. NEXT
  292. CLOSE #1
  293.  
  294. DBASE FILE ACCESS TOOLS
  295.  
  296. The programs that follow are intended as a complete set of toolbox
  297. subroutines that you can add to your own programs.  The first program
  298. contains the core routines that do all of the work, and the remaining
  299. programs illustrate their use in context.  Routines are provided to create,
  300. open, and close dBASE files, as well as read and write data records. 
  301. Additional functions are provided to read the field information from the
  302. header, and also determine if a record has been marked as deleted.
  303.    The main file that contains the dBASE access routines is DBACCESS.BAS,
  304. and several demonstration programs are included that show the use of these
  305. routines in context.  In particular, DBEDIT.BAS exercises all of the
  306. routines, and you should study that program very carefully.
  307.    There are two other example programs that illustrate the use of the
  308. dbAccess routines.  DBCREATE.BAS creates an empty dBASE file containing a
  309. header with field information only, DBEDIT.BAS lets you browse, edit, and
  310. add records to a file, and DBSTRUCT.BAS displays the structure of an
  311. existing file.  There is also a program to pack a database file to remove
  312. deleted records named, appropriately enough, DBPACK.BAS.
  313.    When you examine these subroutines, you will notice that all of the
  314. data--regardless of the field type--is stored as strings.  As you learned
  315. in earlier chapters, storing data as strings instead of in their native
  316. format usually bloats the file size, and always slows down access to the
  317. field values.  This is but one of the fundamental limitations of the dBASE
  318. file format.  Note that using strings alone is not the problem; rather, it
  319. is storing the numeric values as ASCII data.
  320.  
  321. '********** DBACCESS.BAS, module for access to DBF files
  322.  
  323. 'Copyright (c) 1991 Ethan Winer
  324.  
  325. DEFINT A-Z
  326.  
  327. '$INCLUDE: 'dbf.bi'
  328. '$INCLUDE: 'dbaccess.bi'
  329.  
  330. SUB CloseDBF (FileNum, TRecs&) STATIC
  331.  
  332.   Temp$ = PackDate$
  333.   PUT #FileNum, 2, Temp$
  334.   PUT #FileNum, 5, TRecs&
  335.   CLOSE #FileNum
  336.  
  337. END SUB
  338.  
  339. SUB CreateDBF (FileName$, FieldArray() AS FieldStruc) STATIC
  340.  
  341.   TFields = UBOUND(FieldArray)
  342.   HLen = TFields * 32 + 33
  343.   Header$ = SPACE$(HLen + 1)
  344.   Memo = 0
  345.  
  346.   FldBuf$ = STRING$(32, 0)
  347.   ZeroStuff$ = FldBuf$
  348.   FldOff = 33
  349.   RecLen = 1
  350.  
  351.   FOR X = 1 TO TFields
  352.     MID$(FldBuf$, 1) = FieldArray(X).FName
  353.     MID$(FldBuf$, 12) = FieldArray(X).FType
  354.     MID$(FldBuf$, 17) = CHR$(FieldArray(X).FLen)
  355.     MID$(FldBuf$, 18) = CHR$(FieldArray(X).Dec)
  356.     MID$(Header$, FldOff) = FldBuf$
  357.     LSET FldBuf$ = ZeroStuff$
  358.     FldOff = FldOff + 32
  359.     IF FieldArray(X).FType = "M" THEN Memo = -1
  360.     RecLen = RecLen + FieldArray(X).FLen
  361.   NEXT
  362.  
  363.   IF Memo THEN Version = 131 ELSE Version = 3
  364.   MID$(Header$, 1) = CHR$(Version)
  365.   Today$ = DATE$
  366.   Year = VAL(RIGHT$(Today$, 2))
  367.   Day = VAL(MID$(Today$, 4, 2))
  368.   Month = VAL(LEFT$(Today$, 2))
  369.  
  370.   MID$(Header$, 2) = PackDate$
  371.   MID$(Header$, 5) = MKL$(0)
  372.   MID$(Header$, 9) = MKI$(HLen)
  373.   MID$(Header$, 11, 2) = MKI$(RecLen)
  374.   MID$(Header$, FldOff) = CHR$(13)
  375.   MID$(Header$, FldOff + 1) = CHR$(26)
  376.  
  377.   OPEN FileName$ FOR BINARY AS #1
  378.   PUT #1, 1, Header$
  379.   CLOSE #1
  380. END SUB
  381.  
  382.  
  383. FUNCTION Deleted% (Record$) STATIC
  384.   Deleted% = 0
  385.   IF LEFT$(Record$, 1) = "*" THEN Deleted% = -1
  386. END FUNCTION
  387.  
  388.  
  389. FUNCTION GetField$ (Record$, FldNum, FldArray() AS FieldStruc) STATIC
  390.   GetField$ = MID$(Record$, FldArray(FldNum).FOff, FldArray(FldNum).FLen)
  391. END FUNCTION
  392.  
  393.  
  394. FUNCTION GetFldNum% (FieldName$, FldArray() AS FieldStruc) STATIC
  395.   FOR X = 1 TO UBOUND(FldArray)
  396.     IF FldArray(X).FName = FieldName$ THEN
  397.       GetFldNum% = X
  398.       EXIT FUNCTION
  399.     END IF
  400.   NEXT
  401. END FUNCTION
  402.  
  403.  
  404. SUB GetRecord (FileNum, RecNum&, Record$, Header AS DBFHeadStruc) STATIC
  405.   RecOff& = ((RecNum& - 1) * Header.RecLen) + Header.FirstRec
  406.   GET FileNum, RecOff&, Record$
  407. END SUB
  408.  
  409.  
  410. SUB OpenDBF (FileNum, FileName$, Header AS DBFHeadStruc, FldArray() AS _
  411.   FieldStruc) STATIC
  412.  
  413.   OPEN FileName$ FOR BINARY AS FileNum
  414.   GET FileNum, 9, HLen
  415.   Header.FirstRec = HLen + 1
  416.   Buffer$ = SPACE$(HLen)
  417.  
  418.   GET FileNum, 1, Buffer$
  419.   Header.Version = ASC(Buffer$)
  420.   IF Header.Version = 131 THEN
  421.     Header.Version = 3
  422.     Header.Memo = -1
  423.   ELSE
  424.     Header.Memo = 0
  425.   END IF
  426.  
  427.   Header.Year = ASC(MID$(Buffer$, 2, 1))
  428.   Header.Month = ASC(MID$(Buffer$, 3, 1))
  429.   Header.Day = ASC(MID$(Buffer$, 4, 1))
  430.   Header.TRecs = CVL(MID$(Buffer$, 5, 4))
  431.   Header.RecLen = CVI(MID$(Buffer$, 11, 2))
  432.   Header.TFields = (HLen - 33) \ 32
  433.  
  434.   REDIM FldArray(1 TO Header.TFields) AS FieldStruc
  435.   OffSet = 2
  436.   BuffOff = 33
  437.   Zero$ = CHR$(0)
  438.  
  439.   FOR X = 1 TO Header.TFields
  440.     FTerm = INSTR(BuffOff, Buffer$, Zero$)
  441.     FldArray(X).FName = MID$(Buffer$, BuffOff, FTerm - BuffOff)
  442.     FldArray(X).FType = MID$(Buffer$, BuffOff + 11, 1)
  443.     FldArray(X).FOff = OffSet
  444.     FldArray(X).FLen = ASC(MID$(Buffer$, BuffOff + 16, 1))
  445.     FldArray(X).Dec = ASC(MID$(Buffer$, BuffOff + 17, 1))
  446.     OffSet = OffSet + FldArray(X).FLen
  447.     BuffOff = BuffOff + 32
  448.   NEXT
  449. END SUB
  450.  
  451.  
  452. FUNCTION PackDate$ STATIC
  453.   Today$ = DATE$
  454.   Year = VAL(RIGHT$(Today$, 2))
  455.   Day = VAL(MID$(Today$, 4, 2))
  456.   Month = VAL(LEFT$(Today$, 2))
  457.   PackDate$ = CHR$(Year) + CHR$(Month) + CHR$(Day)
  458. END FUNCTION
  459.  
  460.  
  461. FUNCTION Padded$ (Fld$, FLen) STATIC
  462.   Temp$ = SPACE$(FLen)
  463.   LSET Temp$ = Fld$
  464.   Padded$ = Temp$
  465. END FUNCTION
  466.  
  467.  
  468. SUB SetField (Record$, FText$, FldNum, FldArray() AS FieldStruc) STATIC
  469.   FText$ = Padded$(FText$, FldArray(FldNum).FLen)
  470.   MID$(Record$, FldArray(FldNum).FOff, FldArray(FldNum).FLen) = FText$
  471. END SUB
  472.  
  473.  
  474. SUB SetRecord (FileNum, RecNum&, Record$, Header AS DBFHeadStruc) STATIC
  475.   RecOff& = ((RecNum& - 1) * Header.RecLen) + Header.FirstRec
  476.   PUT FileNum, RecOff&, Record$
  477. END SUB
  478.  
  479. Each of the routines listed above performs a different useful service to
  480. assist you in accessing dBASE files, and the following section describes
  481. the operation and use of each routine.  Please understand that these
  482. routines are intended to be loaded as a module, along with your own main
  483. program.  To assist you, a file named DBACCESS.BI is provided, which
  484. contains appropriate DECLARE statements for each routine.  You should
  485. therefore include this file in your programs that use these routines.
  486.    A second include file named DBF.BI is also provided, and it contains
  487. TYPE definitions for the header and field information.  You may notice that
  488. these definitions vary slightly from the actual format of a dBASE file. 
  489. For efficiency, the OpenDBF routine calculates and saves key information
  490. about the file to use later.  As an example, the offset of the first
  491. record's field information is needed by GetRecord and SetRecord.  Rather
  492. than require those procedures to calculate the information repeatedly each
  493. time, OpenDBF does it once and stores the result in the Header TYPE
  494. variable.
  495.    Similarly, the field definition header used by these routines does not
  496. parallel exactly the format of the information in the file.  The modified
  497. structures defined in DBF.BI are as follows:
  498.  
  499. '********** DBF.BI - Record declarations for the dbAccess routines
  500.  
  501. TYPE DBFHeadStruc
  502.   Version  AS INTEGER
  503.   Memo     AS INTEGER
  504.   Year     AS INTEGER
  505.   Month    AS INTEGER
  506.   Day      AS INTEGER
  507.   FirstRec AS INTEGER
  508.   TRecs    AS LONG
  509.   RecLen   AS INTEGER
  510.   TFields  AS INTEGER
  511. END TYPE
  512.  
  513. TYPE FieldStruc
  514.   FName AS STRING * 10
  515.   FType AS STRING * 1
  516.   FOff  AS INTEGER
  517.   FLen  AS INTEGER
  518.   Dec   AS INTEGER
  519. END TYPE
  520.  
  521. CreateDBF
  522.  
  523. CreateDBF accepts the name of the file to create and a field definition
  524. array, and then creates the header portion of a dBASE file based on the
  525. field information in the array.  The file that is created has no data
  526. records in it, but all of the header information is in place.  The calling
  527. program must have dimensioned the field information TYPE array, and filled
  528. it with appropriate information that describes the structure of the records
  529. in the file.  The DBCREATE.BAS program shows an example of how to set up
  530. and call CreateDBF.
  531.  
  532.  
  533. OpenDBF And CloseDBF
  534.  
  535. OpenDBF is used to open a DBF file, and to make information about its
  536. structure available to the calling program.  It fills a TYPE variable with
  537. information from the data file header, and also fills the field definition
  538. array with information about each field.  When you call it you will pass
  539. a BASIC file number you want to be used for later access, the full name of
  540. the file, a TYPE variable that receives the header information, and a TYPE
  541. array.  The array is redimensioned within OpenDBF, and then filled with
  542. information about each field in the file.
  543.    CloseDBF is called when you want to close the file, and it is also
  544. responsible for updating the date and number of records information in the
  545. file header.
  546.  
  547.  
  548. GetRecord And SetRecord
  549.  
  550. GetRecord and SetRecord retrieve and write individual records respectively. 
  551. The calling program must specify the file and record numbers, and also pass
  552. a string that will receive the actual record data.  GetRecord assumes that
  553. you have already created the string that is to receive data from the file. 
  554. A Header variable is also required, so GetRecord and SetRecord will know
  555. the length of each record.  Both GetRecord and SetRecord require the file
  556. to have already been opened using OpenDBF.
  557.  
  558.  
  559. GetField, GetFldNum, SetField, and Padded
  560.  
  561. These routines are used to retrieve and assign the actual field data within
  562. a record string.  The dbAccess routines cannot use a TYPE variable to
  563. define the records, since they must be able to accommodate any type of
  564. file.  Therefore, the Record$ variable is created dynamically, and assigned
  565. and read as necessary.  However, this also means that you may not refer to
  566. the fields by name as would be possible with a TYPE variable.
  567.    GetField returns the contents of the specified field, based on the field
  568. number; the complementary function GetFldName returns the field number
  569. based on the field name.  SetField is the opposite of GetField, and it
  570. assigns a field into the Record$ variable.  Padded$ serves as an assistant
  571. to SetField, and it ensures that the field contents are padded to the
  572. correct length with trailing blanks.
  573.  
  574.  
  575. Deleted
  576.  
  577. Deleted is an integer function that returns a value of -1 to indicate that
  578. the record string passed to it holds a deleted record, or 0 if the record
  579. is not deleted.  The very first byte in each dBASE record is reserved just
  580. to indicate if the record has been deleted.  An asterisk (*) in that
  581. position means the record is deleted; otherwise the field is blank.  Using
  582. a function for this purpose lets you directly test a record using code such
  583. as IF Deleted%(Record$) THEN or IF NOT Deleted%(Record$) THEN.
  584.    Marking deleted records is a common technique in database programming,
  585. because the amount of overhead needed to actually remove a record from a
  586. file is hardly ever justified.  The lost space is recovered in one of two
  587. ways: the most common is to copy the data from one file to another. 
  588. Another, more sophisticated method instead keeps track of which records
  589. have been deleted.  Then as new data is added, it is stored in the space
  590. that was marked as abandoned, thus overwriting the old data.  The
  591. DBPACK.BAS program described later in this chapter uses the copy method,
  592. but uses a trick to avoid having to create a second file.
  593.  
  594.  
  595. DBASE UTILITY PROGRAMS
  596.  
  597. Several programs are presented to show the various dbAccess routines in
  598. context, and each is described individually below.  DBSTRUCT.BAS displays
  599. the header structure of any dBASE file, DBCREATE.BAS creates an empty
  600. database file with header information only, and DBEDIT.BAS lets you browse,
  601. edit, and add records to an existing data file.  These programs are simple
  602. enough to understand, even without excessive comments.  However, highlights
  603. of each program's operation is given.
  604.  
  605.  
  606. DBSTRUCT.BAS
  607.  
  608. DBSTRUCT.BAS begins by including the DBF.BI file which defines the Header
  609. TYPE variable and the FldStruc() TYPE array.  A short DEF FN-style function
  610. is used to simplify formatting when the file date is printed later in the
  611. program.  Once you enter the name of the dBASE file to be displayed, a call
  612. is made to OpenDBF.  OpenDBF accepts the incoming file number and name, and
  613. returns information about the file in Header and FldStruc().  The remainder
  614. of the program simply reports that information on the display screen.
  615.  
  616. '********* DBSTRUCT.BAS, displays a dBASE file's structure
  617.  
  618. DEFINT A-Z
  619. '$INCLUDE: 'dbf.bi'
  620. '$INCLUDE: 'dbaccess.bi'
  621.  
  622. DEF FnTrim$ (DateInfo) = LTRIM$(STR$(DateInfo))
  623. DIM Header AS DBFHeadStruc
  624. REDIM FldStruc(1 TO 1) AS FieldStruc
  625.  
  626. CLS
  627. LINE INPUT "Enter the DBF file name: ", DBFName$
  628. IF INSTR(DBFName$, ".") = 0 THEN
  629.   DBFName$ = DBFName$ + ".DBF"
  630. END IF
  631.  
  632. CALL OpenDBF(1, DBFName$, Header, FldStruc())
  633. CLOSE #1
  634.  
  635. PRINT "Structure of " + DBFName$
  636. PRINT
  637.  
  638. PRINT "Version:     "; Header.Version
  639. PRINT "Last Update: "; FnTrim$(Header.Month);
  640. PRINT "/" + FnTrim$(Header.Day);
  641. PRINT "/" + FnTrim$(Header.Year)
  642. PRINT "# Records:   "; Header.TRecs
  643. PRINT "Rec Length:  "; Header.RecLen
  644. PRINT "# Fields:    "; Header.TFields
  645. PRINT
  646. PRINT "Name", "Type", "Offset", "Length", "# Decimals"
  647. PRINT "----", "----", "------", "------", "----------"
  648.  
  649. FOR X = 1 TO Header.TFields
  650.   PRINT FldStruc(X).FName,
  651.   PRINT FldStruc(X).FType,
  652.   PRINT FldStruc(X).FOff,
  653.   PRINT FldStruc(X).FLen,
  654.   PRINT FldStruc(X).Dec
  655. NEXT
  656. END
  657.  
  658. DBCREATE.BAS
  659.  
  660. The DBCREATE.BAS program accepts the name of a data file to create, and
  661. then asks how many fields it is to contain.  Once the number of fields is
  662. known, a TYPE array is dimensioned to hold the information, and you are
  663. prompted for each field's characteristics one by one.  As you can see by
  664. examining the program source listing, the information you enter is
  665. validated to prevent errors such as illegal field lengths, more decimal
  666. digits than the field can hold, and so forth.
  667.    As each field is defined in the main FOR/NEXT loop, the information you
  668. enter is stored directly into the FldStruc TYPE array.  At the end of the
  669. loop, CreateDBF is called to create an empty .DBF data file.
  670.  
  671. '********** DBCREATE.BAS, creates a DBF file
  672.  
  673. DEFINT A-Z
  674.  
  675. '$INCLUDE: 'dbf.bi'
  676. '$INCLUDE: 'dbaccess.bi'
  677.  
  678. CLS
  679. LOCATE , , 1
  680.  
  681. LINE INPUT "Enter DBF name: "; DBFName$
  682. IF INSTR(DBFName$, ".") = 0 THEN
  683.   DBFName$ = DBFName$ + ".DBF"
  684. END IF
  685.  
  686. DO
  687.   INPUT "Enter number of fields"; TFields
  688.   IF TFields <= 128 THEN EXIT DO
  689.   PRINT "Only 128 fields are allowed"
  690. LOOP
  691.  
  692. REDIM FldStruc(1 TO TFields) AS FieldStruc
  693.  
  694. FOR X = 1 TO TFields
  695.   CLS
  696.   DO
  697.     PRINT "Field #"; X
  698.     LINE INPUT "Enter field name: ", Temp$
  699.     IF LEN(Temp$) <= 10 THEN EXIT DO
  700.     PRINT "Field names are limited to 10 characters"
  701.   LOOP
  702.   FldStruc(X).FName = Temp$
  703.  
  704.   PRINT "Enter field type (Char, Date, Logical, Memo, ";
  705.   PRINT "Numeric (C,D,L,M,N): ";
  706.   DO
  707.     Temp$ = UCASE$(INKEY$)
  708.   LOOP UNTIL INSTR(" CDLMN", Temp$) > 1
  709.   PRINT
  710.   FldStruc(X).FType = Temp$
  711.   FldType = ASC(Temp$)
  712.  
  713.   SELECT CASE FldType
  714.     CASE 67                     'character
  715.       DO
  716.         INPUT "Enter field length: ", FldStruc(X).FLen
  717.         IF FldStruc(X).FLen <= 255 THEN EXIT DO
  718.         PRINT "Character field limited to 255 characters"
  719.       LOOP
  720.  
  721.     CASE 78                     'numeric
  722.       DO
  723.         INPUT "Enter field length: ", FldStruc(X).FLen
  724.         IF FldStruc(X).FLen <= 19 THEN EXIT DO
  725.         PRINT "Numeric field limited to 19 characters"
  726.       LOOP
  727.       DO
  728.         INPUT "Number of decimal places: ", FldStruc(X).Dec
  729.         IF FldStruc(X).Dec < FldStruc(X).FLen THEN EXIT DO
  730.         PRINT "Too many decimal places"
  731.       LOOP
  732.  
  733.     CASE 76                     'logical
  734.         FldStruc(X).FLen = 1
  735.  
  736.     CASE 68                     'date
  737.         FldStruc(X).FLen = 8
  738.      
  739.     CASE 77
  740.         FldStruc(X).FLen = 10
  741.  
  742.     END SELECT
  743. NEXT
  744.  
  745. CALL CreateDBF(DBFName$, FldStruc())
  746. PRINT DBFName$; " created"
  747. END
  748.  
  749. DBEDIT.BAS
  750.  
  751. DBEDIT.BAS is the main demonstration program for the dbAccess subroutines. 
  752. It prompts you for the name of the dBASE file to work with, and then calls
  753. OpenFile to open it.  Once the file has been opened you may view records
  754. forward and backward, edit existing records, add new records, and delete
  755. and undelete records.  Each of these operations is handled by a separate
  756. CASE block, making the code easy to understand.
  757.  
  758. '********** DBEDIT.BAS, edits a record in a DBF file
  759.  
  760. DEFINT A-Z
  761. '$INCLUDE: 'dbf.bi'
  762. '$INCLUDE: 'dbaccess.bi'
  763.  
  764. DIM Header AS DBFHeadStruc
  765. REDIM FldStruc(1 TO 1) AS FieldStruc
  766.  
  767. CLS
  768. LINE INPUT "Enter .DBF file name: ", DBFName$
  769. IF INSTR(DBFName$, ".") = 0 THEN
  770.   DBFName$ = DBFName$ + ".DBF"
  771. END IF
  772.  
  773. CALL OpenDBF(1, DBFName$, Header, FldStruc())
  774.  
  775. Record$ = SPACE$(Header.RecLen)
  776. RecNum& = 1
  777. RecChanged = 0
  778.  
  779. GOSUB GetTheRecord
  780.  
  781. DO
  782.   PRINT "What do you want to do (Next, Prior, Edit, ";
  783.   PRINT "Delete, Undelete, Add, Quit)? ";
  784.   SELECT CASE UCASE$(INPUT$(1))
  785.     CASE "N"
  786.       IF RecChanged THEN
  787.         CALL SetRecord(1, RecNum&, Record$, Header)
  788.       END IF
  789.       RecNum& = RecNum& + 1
  790.       IF RecNum& > Header.TRecs THEN
  791.         RecNum& = 1
  792.       END IF
  793.       GOSUB GetTheRecord
  794.       
  795.     CASE "P"
  796.       IF RecChanged THEN
  797.         CALL SetRecord(1, RecNum&, Record$, Header)
  798.       END IF
  799.       RecNum& = RecNum& - 1
  800.       IF RecNum& < 1 THEN
  801.         RecNum& = Header.TRecs
  802.       END IF
  803.       GOSUB GetTheRecord
  804.       
  805.     CASE "E"
  806. Edit:
  807.       PRINT
  808.       INPUT "Enter the field number:"; Fld
  809.       DO
  810.         PRINT "New "; FldStruc(Fld).FName;
  811.         INPUT Text$
  812.         IF LEN(Text$) <= FldStruc(Fld).FLen THEN EXIT DO
  813.         PRINT "Too long, only "; FldStruc(Fld).FLen
  814.       LOOP
  815.       CALL SetField(Record$, Text$, Fld, FldStruc())
  816.       RecChanged = -1
  817.       GOSUB DisplayRec
  818.       
  819.     CASE "D"
  820.       MID$(Record$, 1) = "*"
  821.       RecChanged = -1
  822.       GOSUB DisplayRec
  823.       
  824.     CASE "U"
  825.       MID$(Record$, 1, 1) = " "
  826.       RecChanged = -1
  827.       GOSUB DisplayRec
  828.  
  829.     CASE "A"
  830.       Header.TRecs = Header.TRecs + 1
  831.       RecNum& = Header.TRecs
  832.       LSET Record$ = ""
  833.       GOTO Edit
  834.       
  835.     CASE ELSE
  836.       EXIT DO
  837.   END SELECT
  838. LOOP
  839.  
  840. IF RecChanged THEN
  841.   CALL SetRecord(1, RecNum&, Record$, Header)
  842. END IF
  843.  
  844. CALL CloseDBF(1, Header.TRecs)
  845. END
  846.  
  847.  
  848. GetTheRecord:
  849.   CALL GetRecord(1, RecNum&, Record$, Header)
  850.  
  851. DisplayRec:
  852.   CLS
  853.   PRINT "Record "; RecNum&; " of "; Header.TRecs;
  854.   IF Deleted%(Record$) THEN PRINT " (Deleted)";
  855.  
  856.   PRINT
  857.   PRINT
  858.   FOR Fld = 1 TO Header.TFields
  859.     FldText$ = GetField$(Record$, Fld, FldStruc())
  860.     PRINT FldStruc(Fld).FName, FldText$
  861.   NEXT
  862.   PRINT
  863.  
  864. RETURN
  865.  
  866. DBPACK.BAS
  867.  
  868. DBPACK.BAS is the final dBASE utility, and it shows how to write an
  869. optimized packing program.  Since there is no reasonable way to actually
  870. erase a record from the middle of a file, dBASE (and indeed, most database
  871. programs) reserve a byte in each record solely to show if it has been
  872. deleted.  The DBPACK.BAS utility program is intended to be run
  873. periodically, to actually remove the deleted records.
  874.    Most programs perform this maintenance by creating a new file, copying
  875. only the valid records to that file, and then deleting the original data
  876. file.  In fact, this is what dBASE does.  The approach taken by DBPACK is
  877. much more intelligent in that it works through the file copying good
  878. records on top of deleted ones.  When all that remains at the end of the
  879. file is data that has been deleted or abandoned copies of records, the file
  880. is truncated to a new, shorter length.  The primary advantage of this
  881. approach is that it saves disk space.  This is superior to the copy method
  882. that of course requires you to have enough free space for both the original
  883. data and the copy.  Because the actual data file is manipulated instead of
  884. a copy, be sure to have a recent backup in case a power failure occurs
  885. during the packing process.
  886.    DBPACK.BAS is fairly quick, but it could be improved if records were
  887. processed in groups, rather than one at a time.  This would allow more of
  888. the swapping to take place in memory, rather than on the disk.  However,
  889. DBPACK was kept simple on purpose, to make its operation clearer.
  890.    There is no BASIC or DOS command that specifically truncates a file, so
  891. this program uses a little-known trick.  If a program calls DOS telling it
  892. to write zero bytes to a file, DOS truncates the file at the current seek
  893. location.  Since BASIC does not allow you to write zero bytes, CALL
  894. Interrupt must be used to perform the DOS call.  Note that you can also use
  895. this technique to extend a file beyond its current length.  This will be
  896. described in more detail in Chapter 11, which describes using CALL
  897. Interrupt to access DOS and BIOS services.
  898.  
  899. '********* DBPACK.BAS, removes deleted records from a file
  900.  
  901. 'NOTE: Please make a copy of your DBF file before running this program.
  902. '      Unlike dBASE that works with a copy of the data file, this program
  903. '      packs, swaps records, and then truncates the original data file.
  904.  
  905. DEFINT A-Z
  906. '$INCLUDE: 'dbf.bi'
  907. '$INCLUDE: 'dbaccess.bi'
  908. '$INCLUDE: 'regtype.bi'
  909.  
  910. DIM Registers AS RegType
  911. DIM Header AS DBFHeadStruc
  912. REDIM FldStruc(1 TO 1) AS FieldStruc
  913.  
  914. LINE INPUT "Enter the dBASE file name: ", DBFName$
  915. IF INSTR(DBFName$, ".") = 0 THEN
  916.   DBFName$ = DBFName$ + ".DBF"
  917. END IF
  918.  
  919. CALL OpenDBF(1, DBFName$, Header, FldStruc())
  920.  
  921. Record$ = SPACE$(Header.RecLen)
  922. GoodRecs& = 0
  923.  
  924. FOR Rec& = 1 TO Header.TRecs
  925.   CALL GetRecord(1, Rec&, Record$, Header)
  926.   IF NOT Deleted%(Record$) THEN
  927.     CALL SetRecord(1, GoodRecs& + 1, Record$, Header)
  928.     GoodRecs& = GoodRecs& + 1
  929.   END IF
  930. NEXT
  931.  
  932. 'This trick truncates the file
  933. RecOff& = (GoodRecs& * Header.RecLen) + Header.FirstRec
  934. Eof$ = CHR$(26)
  935. PUT #1, RecOff&, Eof$
  936. SEEK #1, RecOff& + 1
  937.  
  938. Registers.AX = &H4000          'service to write to a file
  939. Registers.BX = FILEATTR(1, 2)  'get the DOS handle
  940. Registers.CX = 0               'write 0 bytes to truncate
  941. CALL Interrupt(&H21, Registers, Registers)
  942. CALL CloseDBF(1, GoodRecs&)
  943.  
  944. PRINT "All of the deleted records were removed from "; 
  945. PRINT DBFName$
  946. PRINT GoodRecs&; "remaining records"
  947.  
  948. LIMITATIONS OF THE DBASE III STRUCTURE
  949.  
  950. The primary limitation of the DBF file format is it does not allow complex
  951. data types.  With support for only five basic field types--Character, Date,
  952. Logical, Memo, and Numeric--it is very limited when compared to what BASIC
  953. allows.  However, you can easily add new data types to the programs you
  954. write using extensions to the standard field format.  Since a byte is used
  955. to store the field type in the dBASE file header, as many as 256 different
  956. types are possible (0 through 255).  You would simply define additional
  957. code numbers for field types such as Money or Time, or perhaps other
  958. Logical field types such as M and F (Male and Female).
  959.    Another useful enhancement would be to store numeric values in their
  960. native fixed-length format, instead of using the much slower ASCII format
  961. that dBASE uses.  You could also modify the header structure itself, to
  962. improve the performance of your programs.  Since BASIC does not offer a
  963. single byte numeric data type, it would make sense to replace the STRING
  964. * 1 variables with integers.  This would eliminate repeated use of ASC and
  965. CHR$ when reading and assigning single byte strings.  You could also change
  966. the date storage method to pack the date fields to three characters--one
  967. for the year, one for the month, and another for the day.  Of course, if
  968. you do change the header or data format, then your files will no longer be
  969. compatible with the dBASE standard.
  970.  
  971.  
  972. INDEXING TECHNIQUES
  973. ===================
  974.  
  975. At some point, the number of records in a database file will grow to the
  976. point where it takes longer and longer to locate information in the file. 
  977. This is where indexing can help.  Some of the principles of indexed file
  978. access were already described in Chapter 5, in the section that listed the
  979. BASIC PDS ISAM compiler switches.  In this section I will present more
  980. details on how indexing works, and also show some simple methods you can
  981. create yourself.  Although there are nearly as many indexing systems as
  982. there are programmers, one of the most common is the sorted list.
  983.  
  984.  
  985. SORTED LISTS
  986.  
  987. A sorted list is simply a parallel TYPE array that holds the key field and
  988. a record number that corresponds to the data in the main file.  By
  989. maintaining the array in sorted order based on the key field information,
  990. the entire database may be accessed in sorted, rather than sequential
  991. order.  A typical TYPE array used as a sorted list for indexing would look
  992. like this:
  993.  
  994.  
  995. TYPE IndexType
  996.   LastName AS STRING * 15
  997.   RecNum   AS LONG
  998. END TYPE
  999. REDIM IArray(1 TO TotalRecords) AS IndexType
  1000.  
  1001.  
  1002. Assuming each record in the data file has a corresponding element in the
  1003. TYPE array, locating a given record is as simple as searching the array for
  1004. a match.  Since array searches in memory are much faster than reading a
  1005. disk file, this provides an enormous performance boost when compared to
  1006. reading each record sequentially.  To conserve memory and also further
  1007. improve searching speed, you might use a shorter string portion for the
  1008. last name.
  1009.    The following short program shows how such an index array could be
  1010. sorted.
  1011.  
  1012.  
  1013. FOR X% = MaxEls TO 1 STEP -1
  1014.   FOR Y% = 1 TO X% - 1
  1015.     IF IArray(Y%).LastName > IArray(Y% + 1).LastName THEN
  1016.       SWAP IArray(Y%), IArray(Y% + 1)
  1017.     END IF
  1018.   NEXT
  1019. NEXT
  1020.  
  1021.  
  1022. Here, the sorting is based on the last name portion of the TYPE elements. 
  1023. Once the array is sorted, the data file may be accessed in order by walking
  1024. through the record numbers contained in the RecNum portion of each element:
  1025.  
  1026.  
  1027. DIM RecordVar AS IndexType
  1028. FOR X% = 1 TO MaxEls
  1029.   GET #1, IArray(X%).RecNum, RecordVar
  1030.   PRINT RecordVar.LastName
  1031. NEXT
  1032.  
  1033.  
  1034. Likewise, to find a given name you would search the index array based on
  1035. the last name, and then use the record number from the same element once
  1036. it is found:
  1037.  
  1038.  
  1039. Search$ = "Cramer"
  1040. FOR X% = 1 TO MaxEls
  1041.   IF IArray(X%).LastName = Search$ THEN
  1042.     Record% = IArray(X%).RecNum
  1043.     GET #1, Record%, RecordVar
  1044.     PRINT "Found "; Search$; " at record number"; Record%
  1045.     EXIT FOR
  1046.   END IF
  1047. NEXT
  1048.  
  1049.  
  1050. Chapter 8 will discuss sorting and searching in detail using more
  1051. sophisticated algorithms than those shown here, and you would certainly
  1052. want to use those for your program.  However, one simple improvement you
  1053. could make is to reduce the number of characters in each index entry.  For
  1054. example, you could keep only the first four characters of each last name. 
  1055. Although this might seem to cause a problem--searching for Jackson would
  1056. also find Jack--you would have the same problem if there were two Jacksons. 
  1057. The solution, therefore, is to retrieve the entire record if a partial
  1058. match is found, and compare the complete information in the record with the
  1059. search criteria.
  1060.    Inserting an entry into a sorted list requires searching for the first
  1061. entry that is greater than or equal to the one you wish to insert, moving
  1062. the rest of the entries down one notch and inserting the new entry.  The
  1063. code for such a process might look something like this:
  1064.  
  1065.  
  1066. FOR X% = 2 TO NumRecs%
  1067.   IF Item.LastName <= Array(X%).LastName THEN
  1068.     IF Item.LastName >= Array(X% - 1).LastName THEN
  1069.       FOR Y% = NumRecs% TO X% STEP -1
  1070.         SWAP Array(Y%), Array(Y% + 1)
  1071.       NEXT
  1072.       Array(X%) = Item
  1073.       EXIT FOR
  1074.     END IF
  1075.   END IF
  1076. NEXT
  1077.  
  1078.  
  1079. Understand that this code is somewhat simplified.  For example, it will
  1080. not correctly handle inserting an element before the first existing entry
  1081. or after the last.  Equally important, unless you are dealing with less
  1082. than a few hundred entries, this code will be extremely slow.  The loop
  1083. that inserts an element by swapping all of the elements that lie beyond the
  1084. insertion point will never be as efficient as a dedicated subroutine
  1085. written in assembly language.  Commercial toolbox products such as Crescent
  1086. Software's QuickPak Professional include memory moving routines that are
  1087. much faster than one written using BASIC.
  1088.    Finally, you must have dimensioned the array to at least one more
  1089. element than there are records, to accommodate the inserted element.  Many
  1090. programs that use in-memory arrays for indexing dimension the arrays to
  1091. several hundred extra elements to allow new data to be entered during the
  1092. course of the session.  Since BASIC 7.1 offers the REDIM PRESERVE command,
  1093. that too could be used to extend an array as new data is added.
  1094.  
  1095.  
  1096. EXPRESSION EVALUATION
  1097.  
  1098. Expression evaluation, in the context of data management, is the process
  1099. of evaluating a record on the basis of some formula.  Its uses include the
  1100. creation of index keys, reports, and selection criteria.  This is where the
  1101. application of independent file structures such as the dBASE example shows
  1102. a tremendous advantage.  For example, if the user wants to be able to view
  1103. the file sorted first by zip code and then by last name, some means of
  1104. performing a multi-key sort is required.
  1105.    Another example of expression evaluation is when multiple conditions
  1106. using AND and OR logic are needed.  You may want to select only those
  1107. records where the balance due is greater than $100 *and* the date of last
  1108. payment is more than 30 days prior to the current date.  Admittedly,
  1109. writing an expression parser is not trivial; however, the point is that
  1110. data-driven programming is much more suitable than code-driven programming
  1111. in this case.
  1112.    Without some sort of look-up table in which you can find the field names
  1113. and byte offsets, you are going to have a huge number of SELECT CASE
  1114. statements, none of which are reusable in another application.  Indeed,
  1115. one of the most valuable features of AJS Publishing's db/LIB add-on
  1116. database library is the expression evaluator it includes.  This routine
  1117. lets you maintain the data structure in a file, and the same code can be
  1118. used to process all file search operations.
  1119.  
  1120.  
  1121. RELATIONAL DATABASES
  1122. ====================
  1123.  
  1124. Most programmers are familiar with traditional random access files, where
  1125. a fixed amount of space is set aside in each record to hold a fixed amount
  1126. of information.  For very simple applications this method is sensible, and
  1127. allows for fast access to each record provided you know the record number. 
  1128. As you learned earlier in this chapter, indexing systems can eliminate the
  1129. need to deal with record numbers, instead letting you locate records based
  1130. on the information they contain.  Relational databases take this concept
  1131. one step further, and let you locate records in one file based on
  1132. information contained in another file.  As you will see, this lets you
  1133. create applications that are much more powerful than those created using
  1134. standard file handling methods.
  1135.    Imagine you are responsible for creating an order entry program for an
  1136. auto parts store.  At the minimum, three sets of information must be
  1137. retained in such a system: the name, address, and phone number of each
  1138. customer; a description of each item that is stocked and its price; and the
  1139. order detail for each individual sale.  A simplistic approach would be to
  1140. define the records in a single database with fields to hold the customer
  1141. information and the products purchased, with a new record used for each
  1142. transaction.  A TYPE definition for these records might look like this:
  1143.  
  1144. TYPE RecordType
  1145.   InvoiceNum AS INTEGER
  1146.   CustName   AS STRING * 32
  1147.   CustStreet AS STRING * 32
  1148.   CustCity   AS STRING * 15
  1149.   CustState  AS STRING * 2
  1150.   CustZip    AS STRING * 5
  1151.   CustPhone  AS STRING * 10
  1152.   Item1Desc  AS STRING * 15
  1153.   Item1Price AS SINGLE
  1154.   Quantity1  AS INTEGER
  1155.   Item2Desc  AS STRING * 15
  1156.   Item2Price AS SINGLE
  1157.   Quantity2  AS INTEGER
  1158.   Item3Desc  AS STRING * 15
  1159.   Item3Price AS SINGLE
  1160.   Quantity3  AS INTEGER
  1161.   Item4Desc  AS STRING * 15
  1162.   Item4Price AS SINGLE
  1163.   Quantity4  AS INTEGER
  1164.   TaxPercent AS SINGLE
  1165.   InvoiceTot AS SINGLE
  1166. END TYPE
  1167.  
  1168. As sensible as this may seem at first glance, there are a number of
  1169. problems with this record structure.  The primary limitation is that each
  1170. record can hold only four purchase items.  How could the sales clerk
  1171. process an order if someone wanted to buy five items?  While room could be
  1172. set aside for ten or more items, that would waste disk space for sales of
  1173. fewer items.  Worse, that still doesn't solve the inevitable situation when
  1174. someone needs to buy eleven or more items at one time.
  1175.    Another important problem is that the customer name and address will be
  1176. repeated for each sale, further wasting space when the same customer comes
  1177. back a week later.  Yet another problem is that the sales personnel are
  1178. responsible for knowing all of the current prices for each item.  If they
  1179. have to look up the price in a printout each time, much of the power and
  1180. appeal of a computerized system is lost.  Solving these and similar
  1181. problems is therefore the purpose of a relational database.
  1182.    In a relational database, three separate files would be employed.  One
  1183. file will hold only the customer names and addresses, a second will hold
  1184. just the item information, and a third is used to store the details of each
  1185. invoice.  In order to bind the three files together, a unique number must
  1186. be assigned in each record.  This is shown as a list of field names in
  1187. Figure 7-1 below.
  1188.  
  1189.           CUSTOMER.DAT                   PRODUCTS.DAT
  1190.    ╔═════════════════════════╗    ╔════════════════════════╗
  1191. ┌──╫─> Customer Number       ║    ║   Product Number <─────╫─┐
  1192. │  ║   Customer Name         ║    ║   Product Name         ║ │
  1193. │  ║   Customer Address      ║    ║   Product Price        ║ │
  1194. │  ║   Customer Zip          ║    ║   Quantity on Hand     ║ │
  1195. │  ║   Customer Phone        ║    ╚════════════════════════╝ │
  1196. │  ║   Available Credit      ║                               │
  1197. │  ╚═════════════════════════╝                               │
  1198. │                                                            │
  1199. │                                                            │
  1200. │                         INVOICE.DAT                        │
  1201. │               ╔════════════════════════════╗               │
  1202. └───────────────╫─> Customer Number          ║               │
  1203.                 ║   Invoice Number           ║               │
  1204.                 ║   Product Number <─────────╫───────────────┘
  1205.                 ║   Product Quantity         ║
  1206.                 ║   Product Price            ║
  1207.                 ║   Tax Percent              ║
  1208.                 ╚════════════════════════════╝
  1209.  
  1210. Figure 7-1: How a relational database ties related data in separate files
  1211. using a unique value in each record.
  1212.  
  1213. Now, when Bob Jones goes into the store to buy a radiator cap and a case
  1214. of motor oil, the clerk can enter the names Jones and see if Bob is already
  1215. a customer.  If so, the order entry program will retrieve Bob's full name
  1216. and address from the customer file and display it on the screen.  Otherwise
  1217. it would prompt the clerk to enter Bob's name and address.  When Bob tells
  1218. the clerk what he wants to buy, the clerk would enter the part number or
  1219. name, and the program will automatically look up the price in the products
  1220. file.  (A smart program would even subtract the number of radiator caps
  1221. from the "Quantity on Hand" field, so a report run at the end of each day
  1222. can identify items that need to be ordered.)  Once the sale is finalized,
  1223. two new records will be written to the invoice file--one for the radiator
  1224. cap and one for the motor oil.
  1225.    Each invoice record would store Bob's customer number, a program-
  1226. generated sequential invoice number, the product number, the quantity of
  1227. this product sold, and the unit price.  There's no need to store the
  1228. subtotal, since that information could be recreated at any time from the
  1229. quantity and unit price fields.  If sales tax is charged, that field could
  1230. hold just the rate.  Again the actual tax amount could be computed at any
  1231. time.  The beauty of this organization is that there is never a need to
  1232. store duplicated information, and thus there is no wasted disk space.
  1233.    The relational aspect of this system becomes clear when it is time to
  1234. produce a report.  To print an invoice, the program searches the invoice
  1235. file for every record with the unique invoice number.  From the customer
  1236. number field the customer's name and address are available, by searching
  1237. for a match between the customer number in the invoice record and that same
  1238. unique number in the customer file.  And from the part number field the
  1239. part name can be retrieved, based on finding the same part number in the
  1240. products file.  Thus, the term relational is derived from the ability to
  1241. relate information in one file to information in a different file, based
  1242. on unique identifying values.  In this case, those values are the invoice
  1243. number, the customer number, and the part number.
  1244.  
  1245.  
  1246. SQL: THE BLACK BOX
  1247.  
  1248. An important current trend in data processing is the use of Structured
  1249. Query Language (SQL).  The appeal of SQL is that it eliminates explicit
  1250. coding in a conventional high-level language such as BASIC.  Instead, SQL
  1251. is an even higher-level language that performs most of the low-level
  1252. details for you.  SQL is based on passing SQL commands--called requests--
  1253. as strings, which are evaluated by the SQL engine.  The short example
  1254. program below shows some typical SQL commands in context.
  1255.  
  1256.  
  1257. select lastname, firstname, accountcode, phone
  1258. from customers
  1259. where unpaid > credit * .75
  1260.   and today - duedate > 30
  1261. order by accountcode
  1262.  
  1263.  
  1264. When these commands are sent to the SQL server, the server responds by
  1265. filling in an array with the resultant data.  The beauty of SQL, therefore,
  1266. is that it eliminates the SELECT CASE statements that you would have to
  1267. write, and that would be specific to a given data file.  In SQL, the data
  1268. fields are accessed by name instead of by numeric offsets.  The SQL program
  1269. does not have to specify which data is double precision, and which is text,
  1270. and so forth.  Rather, all that is needed is the name of the data being
  1271. reported on, the selection criteria, and the order in which the data is to
  1272. be returned.
  1273.    This program asks to report on the lastname, firstname, accountcode, and
  1274. phone fields of the data set (file) named customers.  It then specifies
  1275. that only those customers who owe more than 75 percent of their available
  1276. credit and are more than 30 days overdue should be listed.  Finally, the
  1277. customers are to be listed in order based on their customer account code
  1278. number.
  1279.    As a further example of the power of the SQL language, imagine you have
  1280. written an application to manage a publishing business.  In this
  1281. hypothetical situation, three of the tables in your database are Stores,
  1282. Titles, and Sales, which hold the names of each retail store, the book
  1283. titles offered for sale, and the details of each sale.
  1284.    Now, consider the problem of producing a report showing the total sales
  1285. in dollars, with individual subtotals for each store.  This would first
  1286. require you generate a list of stores from the Stores table.  You would
  1287. then have to examine each sale in the Sales table, and each entry there
  1288. would refer to a title which must be looked up in the Titles file to
  1289. determine the price.  You would then multiply this price by the quantity
  1290. and add that to a running total being kept for each store, perhaps storing
  1291. the result in a multi-dimensional array.
  1292.    As you can see, this is potentially a lot of coding if you attempt to
  1293. tackle the job using BASIC.  While the sequence of SQL commands necessary
  1294. to retrieve this information is not trivial either, it is certainly less
  1295. work than writing an equivalent report in BASIC.  Here are the SQL commands
  1296. that perform the store sales report described above:
  1297.  
  1298.  
  1299. select stores.storename, sum(sales.qty * titles.price)
  1300. from stores, titles, sales
  1301. where stores.store_id = sales.store_id
  1302.   and titles.title_id = sales.title_id
  1303. group by storename
  1304.  
  1305.  
  1306. As you can see from these short examples, SQL is a simple and intuitive
  1307. language, and it may well be worth your effort to learn if you specialize
  1308. in database programming or plan to.  One excellent product you may wish to
  1309. become familiar with is DataEase, a popular PC database product.  One of
  1310. the earliest adopters of SQL-style methods, DataEase lets even the novice
  1311. user create sophisticated data entry forms and reports in a very short
  1312. time.  Contrast that with procedural languages such as that used by dBASE
  1313. which require as much effort as programming in BASIC.
  1314.    There are several good books that go into far greater detail about SQL
  1315. than can possibly be offered here.  One I recommend is "The Practical SQL
  1316. Handbook: Using Structured Query Language" by Emerson, Darnovsky, and
  1317. Bowman; Addison-Wesley Publishing Company; 1989.  This book is clearly
  1318. written, avoids the use of jargon, and contains numerous good explanations
  1319. of what SQL is all about without getting bogged down in esoteric details.
  1320.  
  1321.  
  1322. PROGRAMMING FOR A NETWORK
  1323. =========================
  1324.  
  1325. Although network file access has been supported since QuickBASIC version
  1326. 1.0, many programmers do not fully understand how to use this important
  1327. feature.  However, the concepts are simple once you know the commands.  In
  1328. the earlier auto parts store example, it was assumed that only one computer
  1329. would be used to enter sales information.  But when there are many sales
  1330. people entering information all at once, some means is needed to let each
  1331. computer access simultaneously a single group of files from a remote file
  1332. server.
  1333.    In this section I will discuss two methods for sharing files--one which
  1334. is supported by BASIC, and the other supported only indirectly.  I will
  1335. also discuss methods for protecting data across the network and detecting
  1336. which type of network is being used.
  1337.  
  1338.  
  1339. FILE SHARING AND LOCKING
  1340.  
  1341. BASIC offers three commands to allow multiple programs to share files from
  1342. a central, remote computer: OPEN, LOCK, and UNLOCK.  Chapter 6 discussed
  1343. the OPEN command in great detail, but mentioned the various file sharing
  1344. options only briefly.  OPEN provides four variations that let you specify
  1345. what other processes have access to the file being opened.  For simplicity,
  1346. the discussions that follow assume the files are being opened for random
  1347. access; this is the most common access method when writing databases.  But
  1348. only very slight changes are needed to adapt this information for use with
  1349. binary file access as shown in the earlier dBASE examples.
  1350.    When you add SHARED to the list of OPEN arguments, you are telling the
  1351. operating system that any other program may also open the file while you
  1352. are using it.  [Without SHARED, another program that tries to open a file
  1353. you have opened will receive an "Access denied" error message.]  Once the
  1354. other programs have opened the file they may freely read from it or write
  1355. to it.  If you need to restrict what operations other programs may perform,
  1356. you would replace SHARED with either LOCK READ, LOCK WRITE, or LOCK READ
  1357. WRITE.  LOCK READ prevents other program from reading the file while you
  1358. have it open, although they could write to it.  Likewise, LOCK WRITE lets
  1359. another process read from the file but not write to it.  LOCK READ WRITE
  1360. of course prevents another program from either reading or writing the file.
  1361.    Because of these complications and limitations, you will most likely use
  1362. SHARED to allow full file sharing.  Then, the details of who writes what
  1363. and when can be handled by logic in your program, or by locking individual
  1364. records.
  1365.    Note that with most networks you cannot open a file for shared access,
  1366. unless you have previously loaded SHARE.EXE that comes with DOS 3.0 and
  1367. later versions.  SHARE.EXE is a TSR (terminate and stay resident) program
  1368. that manages *lock tables* for your machine.  These tables comprise a list
  1369. showing which portions of what files are currently locked.  A short utility
  1370. that reports if SHARE.EXE is installed is presented later in this chapter. 
  1371. Some networks, however, require SHARE to be installed only on the computer
  1372. that is acting as the file server.
  1373.  
  1374.  
  1375. RECORD LOCKING
  1376.  
  1377. The most difficult problem you will encounter when writing a program that
  1378. runs on a network is arbitrating when each user will be allowed to read and
  1379. write data.  Since more than one operator may call up a given record at the
  1380. same time, it is possible--even likely--that changes made by one person
  1381. will be overwritten later by another.  Imagine that two operators have just
  1382. called up the same customer record on their screens.  Further, one operator
  1383. has just changed the customer's address and the other has just changed the
  1384. phone number.  Then the first operator then saves the record with the new
  1385. address, but two seconds later the second operator saves the same record
  1386. with a new phone number.  In this case, the second disk write stores the
  1387. old address on top of the same record that was saved two seconds earlier!
  1388.    To prevent this from happening requires some type of file locking,
  1389. whereby the second operator is prevented from even loading the record; the
  1390. program instead gives them a message saying the record is already in use. 
  1391. There are two primary ways to do this.  A *hard lock* is implemented using the
  1392. BASIC LOCK statement, and it causes the network operating system to deny
  1393. access to the record if the first program has locked it.  A *soft lock* is
  1394. similar, except it uses program logic that you design to determine if the
  1395. file is already in use.  Let's take a closer at each of these locking
  1396. methods.
  1397.  
  1398.  
  1399. Hard Locks
  1400.  
  1401. A hard lock is handled by the network software, and is controlled by the
  1402. BASIC LOCK and UNLOCK statements.  Hard locks may be specified for all or
  1403. just a part of a file.  When a program imposes a hard lock, all other
  1404. programs are prevented from either reading or writing that portion of the
  1405. file.  You may lock either one record or a range of records: LOCK #1, 3
  1406. locks record 3, and UNLOCK #1, 1 TO 10 unlocks records 1 through 10.  Files
  1407. that have been opened for binary access may also be locked, by specifying
  1408. a range of bytes instead of one or more record numbers.
  1409.    Because access to the specified record or range of records is denied to
  1410. all other applications, it is important to unlock the records as soon as
  1411. you are done with them.  A code fragment that shows how to manipulate a
  1412. record using hard locking would look like this:
  1413.  
  1414.  
  1415. OPEN "CUST.DAT" SHARED AS #1 LEN = RecordLength%
  1416. LOCK #1, RecNum%
  1417. GET #1, RecNum%, RecData
  1418.  
  1419. 'allow the user to edit the record here
  1420.  
  1421. PUT #1, RecNum%, RecData
  1422. UNLOCK #1, RecNum%
  1423. CLOSE #1
  1424.  
  1425.  
  1426. There are several fundamental problems with hard locks you must be aware
  1427. of.  First, they prevent another application from even looking at the data
  1428. that is locked.  If a record is tied up for a long period of time, this
  1429. prevents another program from reporting on that data.  Another is that all
  1430. locks must be removed before the file is closed.  The BASIC PDS language
  1431. reference manual warns, "Be sure to remove all locks with an UNLOCK
  1432. statement before closing a file or terminating your program.  Failing to
  1433. remove locks produces unpredictable results."  [As in "Yo, get out the
  1434. Norton disk doctor".]
  1435.    Yet another problem is that each LOCK must have an exactly corresponding
  1436. UNLOCK statement.  It is therefore up to your program to know exactly which
  1437. record or range of records were locked earlier, and unlock the exact same
  1438. records later on.
  1439.    Finally, the last problem with hard locking is that it requires you to
  1440. use ON ERROR.  If someone else has locked a record and you attempt to read
  1441. it, BASIC will generate a "Permission denied" error that must be trapped. 
  1442. Since there's no way for you to know ahead of time if a record is available
  1443. or locked you must be prepared to handle the inevitable errors.  Similarly,
  1444. if you attempt to lock a record when it has already been locked by another
  1445. program, BASIC will create an error.  It is possible to lock and unlock
  1446. records behind BASIC's back using CALL Interrupt and detect those errors
  1447. manually; however, soft locks often provide an even better solution.
  1448.  
  1449.  
  1450. Soft Locks
  1451.  
  1452. A soft lock is implemented using logic you design, which has the decided
  1453. advantage of letting you customize that logic to your exact needs.  Most
  1454. programs implement a soft lock by reserving a single byte at the beginning
  1455. of each data record.  This is similar to the method dBASE uses to identify
  1456. deleted records.  Understand that the one important limitation of soft
  1457. locks is that all programs must agree on the method being used.  Unless you
  1458. wrote (or at least control) all of the other programs that are sharing the
  1459. file, soft locks will probably not be possible.
  1460.    One way to implement a soft lock is to use a special character--perhaps
  1461. the letter "L"--to indicate that a record is in use and may not be written
  1462. to.  Therefore, to lock a record you would first retrieve it, and then
  1463. check to be sure it isn't already locked.  If it is not currently locked
  1464. you would assign an "L" to the field reserved for that purpose, and finally
  1465. write the record back to disk.  Thereafter, any other program can tell that
  1466. the record is locked by simply examining that first byte.
  1467.    If someone tries to access a record that is locked, the program can
  1468. display the message "Record in use" or something along those lines.  A
  1469. simple enhancement to this would store a user identification number in the
  1470. lock field, rather than just a locked identifier.  This way the program
  1471. could also report who is using the record, and not just that it is locked. 
  1472. This is shown in context below.
  1473.  
  1474. GET #1, RecNum%, RecData$
  1475. Status$ = LEFT$(RecData$, 1)
  1476. SELECT CASE Status$
  1477.   CASE " "          'Record is okay to write, lock it now
  1478.     MID$(RecData$, 1) = CHR$(UserID)
  1479.     PUT #1, RecNum%, RecData$
  1480.     GOTO EditRecord
  1481.   CASE "*"          'Record is deleted, say so
  1482.     PRINT "Record number"; RecNum%; " is deleted."
  1483.     GOTO SelectAnotherRecord
  1484.   CASE ELSE         'Status$ contains the user number
  1485.     PRINT "Record already in use by user: "; Status$
  1486.     GOTO ReadOnly
  1487. END SELECT
  1488.   ...
  1489.   ...
  1490. SaveRecord:
  1491.   MID$ (RecData$, 1) = " "     'clear the lock status
  1492.   PUT #1, RecNum%, RecData$    'save the new data to disk
  1493.  
  1494. ADDITIONAL NETWORK CONSIDERATIONS
  1495.  
  1496. Many networks require that SHARE.EXE be installed before a file may be
  1497. opened for shared access, you can avoid runtime errors by being able to
  1498. determine ahead of time if this file is loaded.  The following short
  1499. function and example returns either -1 or 0 to indicate if SHARE is
  1500. currently loaded or not, respectively.
  1501.  
  1502. DEFINT A-Z
  1503. DECLARE FUNCTION ShareThere% ()
  1504.  
  1505. '$INCLUDE: 'regtype.bi'
  1506.  
  1507. FUNCTION ShareThere% STATIC
  1508.  
  1509.   DIM Registers AS RegType
  1510.   ShareThere% = -1              'assume Share is loaded
  1511.   Registers.AX = &H1000         'service 10h
  1512.   CALL Interrupt(&H2F, Registers, Registers)
  1513.   AL = Registers.AX AND 255     'isolate the result in AL
  1514.   IF AL <> &HFF THEN ShareThere% = 0
  1515.  
  1516. END FUNCTION
  1517.  
  1518. Then, at the start of your program you would invoke ShareThere, and display
  1519. an error message if SHARE has not been run:
  1520.  
  1521.  
  1522. IF NOT ShareThere% () THEN
  1523.   PRINT "SHARE.EXE is not installed"
  1524.   END
  1525. END IF
  1526.  
  1527.  
  1528. OPERATING SYSTEM CONFIRMATION
  1529.  
  1530. Another feature of a well-behaved network application is to determine if
  1531. the correct network operating system is installed.  In most cases, unless
  1532. you are writing a commercial application for others to use, you'll already
  1533. know which operating system is expected.  However, it is possible to
  1534. determine with reasonable certainty what network software is currently
  1535. running.  The three functions that follow must be invoked in the order
  1536. shown, and they help you determine the brand of network your program is
  1537. running under.
  1538.  
  1539. '********** NETCHECK.BAS, identifies the network brand
  1540.  
  1541. DEFINT A-Z
  1542. '$INCLUDE: 'regtype.bi'
  1543.  
  1544. DECLARE FUNCTION NWThere% ()
  1545. DECLARE FUNCTION BVThere% ()
  1546. DECLARE FUNCTION MSThere% ()
  1547. DIM SHARED Registers AS RegType
  1548.  
  1549. PRINT "I think the network is ";
  1550. IF NWThere% THEN
  1551.   PRINT "Novell Netware"
  1552. ELSEIF BVThere% THEN
  1553.   PRINT "Banyon Vines"
  1554. ELSEIF MSThere% THEN
  1555.   PRINT "Lantastic or other MS compatible"
  1556. ELSE
  1557.   PRINT "Something I don't recognize, or no network"
  1558. END IF
  1559. END
  1560.  
  1561.  
  1562. FUNCTION BVThere% STATIC
  1563.      BVThere% = -1
  1564.      Registers.AX = &HD701
  1565.      CALL Interrupt(&H2F, Registers, Registers)
  1566.      AL = Registers.AX AND 255
  1567.      IF AL <> 0 THEN BVThere% = 0
  1568. END FUNCTION
  1569.  
  1570. FUNCTION MSThere% STATIC
  1571.   MSThere% = -1
  1572.   Registers.AX = &HB800
  1573.   CALL Interrupt(&H2F, Registers, Registers)
  1574.   AL = Registers.AX AND 255
  1575.   IF AL = 0 THEN MSThere% = 0
  1576. END FUNCTION
  1577.  
  1578. FUNCTION NWThere% STATIC
  1579.   NWThere% = -1
  1580.   Registers.AX = &H7A00
  1581.   CALL Interrupt(&H2F, Registers, Registers)
  1582.   AL = Registers.AX AND 255
  1583.   IF AL <> &HFF THEN NWThere% = 0
  1584. END FUNCTION
  1585.  
  1586. THIRD-PARTY DATABASE TOOLS
  1587. ==========================
  1588.  
  1589. There are several tools on the market that can help you to write database
  1590. applications.  Although BASIC includes many of the primitive services
  1591. necessary for database programming, there are several limitations.  Four
  1592. such products are described briefly below, and all are written in assembly
  1593. language for fast performance and small code size.  You should contact the
  1594. vendors directly for more information on these products.
  1595.  
  1596.  
  1597. AJS Publishing's db/LIB
  1598.  
  1599. This is one of the most popular database add-on products for use with
  1600. BASIC, and rightfully so.  db/LIB comes in both single- and multi-user
  1601. versions, and handles all aspects of creating, updating, and indexing
  1602. relational database files.  db/LIB uses the dBASE III+ file format which
  1603. lets you access files from many different applications.  Besides its
  1604. database handling routines, db/LIB includes a sophisticated expression
  1605. evaluator that lets you select records based on multiple criteria. 
  1606. Compared to many other database libraries, db/LIB is extremely fast, and
  1607. is also very easy to use.
  1608.  
  1609. db/LIB
  1610. AJS Publishing, Inc.
  1611. P.O. Box 83220
  1612. Los Angeles, CA  90083
  1613. 213-215-9145
  1614.  
  1615.  
  1616. Novell's Btrieve
  1617.  
  1618. Btrieve has been around for a very long time, and like db/LIB it lets you
  1619. easily manipulate all aspects of a relational database.  Unlike db/LIB,
  1620. however, Btrieve can be used with nearly any programming language.  The
  1621. downside is that Btrieve is more complicated to use with BASIC.  Also, a
  1622. special TSR program must be run before your program can call its routines,
  1623. further complicating matters for your customers.  But Btrieve has a large
  1624. and loyal following, and if you write programs using more than one language
  1625. it is certainly a product to consider.
  1626.  
  1627. Btrieve
  1628. Novell, Inc.
  1629. 122 East 1700 SOuth
  1630. Provo, UT  84606
  1631. 801-429-7000
  1632.  
  1633.  
  1634. CDP Consultants' Index Manager
  1635.  
  1636. Index Manager is an interesting and unique product, because it handles
  1637. only the indexing portion of a database program.  Where most of the other
  1638. database add-ons take over all aspects of file creation and updating, Index
  1639. Manager lets you use any file format you want.  Each time a record is to
  1640. be retrieved based on a key field, a single call obtains the appropriate
  1641. record number.  Index Manager is available in single- and multi-user
  1642. versions, and is designed to work with compiled BASIC only.
  1643.  
  1644. Index Manager
  1645. CDP Consultants
  1646. 1700 Circo del Cielo Drive
  1647. El Cajon, CA  92020
  1648. 619-440-6482
  1649.  
  1650.  
  1651.  
  1652. Ocelot
  1653.  
  1654. Ocelot is unique in that it uses SQL commands instead of the more
  1655. traditional approach used by the other products mentioned.  Ocelot supports
  1656. both standalone and networked access, and it is both fast and flexible. 
  1657. Although Ocelot is meant for use with several different programming
  1658. languages, the company provides full support for programmers using BASIC.
  1659.  
  1660. Ocelot
  1661. Ocelot Computer Services
  1662. #1502, 10025-106 Street
  1663. Edmonton, Alberta
  1664. Canada  T5J 1G7
  1665. 403-421-4187
  1666.  
  1667.  
  1668. SUMMARY
  1669. =======
  1670.  
  1671. In this chapter you learned the principles of data-driven programming, and
  1672. the advantages this method offers.  Unlike the TYPE definition method that
  1673. Microsoft recommends, storing record and field information as variables
  1674. allows your programs to access any type of data using the same set of
  1675. subroutines.
  1676.    You also learned how to create and access data using the popular dBASE
  1677. file format, which has the decided advantage of being compatible with a
  1678. large number of already successful commercial products.  A complete set of
  1679. dBASE file access tools was presented, which may be incorporated directly
  1680. into your own programs.
  1681.    This chapter also explained indexing methods, to help you quickly locate
  1682. information stored in your data files.  Besides providing fast access,
  1683. indexes help to maintain your data in sorted order, facilitating reports
  1684. on that data.  Relational databases were described in detail, using
  1685. examples to show the importance of maintaining related information in
  1686. separate files.  As long as a unique key value is stored in each record,
  1687. the information can be joined together at any time for reporting and
  1688. auditing purposes.  SQL was also mentioned, albeit briefly, to provide a
  1689. glimpse into the future direction that database programming is surely
  1690. heading.
  1691.    In the section about programming for a network, a comparison of the
  1692. various file sharing and locking methods was given.  You learned the
  1693. importance of preventing one program from overwriting data from another,
  1694. and examined specific code fragments showing two different locking
  1695. techniques.
  1696.    Finally, several third-party library products were mentioned.  In many
  1697. situations it is more important to get the job done than to write all of
  1698. the code yourself.  When the absolute fastest performance is necessary, a
  1699. well written add-on product can often be the best solution to a complex
  1700. data management problem.
  1701.    The next chapter discusses searching and sorting data both in memory and
  1702. on disk, and provides a logical extension to the information presented
  1703. here.  In particular, there are a number of ways that you can speed up
  1704. index searches using either smarter algorithms, assembly language, or both.
  1705.