What's an AutoIncrement field?
AutoIncrement, or AutoInc for short, is a new type of field, available in level 7 DBF tables. The values in AutoInc fields are integers, and they are read-only. As the name implies, these values are supplied automatically by the Borland Database Engine each time a record is appended to the table. The value for the AutoInc field in the next new record is stored in the header of the DBF file, and that value is only assigned to the field when the record is actually saved to the DBF. Therefore, no matter what kind of caching or buffering is going on, the AutoInc values in the records of a given table will all be unique.
Once an AutoInc value is assigned for a particular record, it is permanent. When a row is removed from a table, the AutoInc field values of the other rows do not change.
In the old days, if you had a table where a unique identifier for each record was required, you had two choices. You could identify the record by the record number, or you could create a special field that would acquire a unique number or code, via some home-grown mechanism.
Using record numbers proved to be a bad idea, because they are simply not permanent. When you PACK a table that has deleted records, the record numbers get rearranged.
Home-grown mechanisms that provide a permanent unique number or code for each record were always a little complicated, and seemed slightly precarious in busy multi-user environments. One of the best efforts at such a mechanism is Romain Strieff's SeqValue.CC, which was written for Visual dBASE 5.5x.
AutoInc fields provide us with a simple and elegant way to meet this need, with no fuss or muss.
What are the limits?
The first thing you'll notice when using the Table Designer is that an AutoInc field is only 4 bytes long. Does that mean that it can only count up to 9999? No, that would be pretty useless, wouldn't it? It's 4 bytes, not 4 digits. That means it can hold 2564 permutations of values, or 4,294,967,296 different possible values. The first value normally begins at 1, then counts up to 2,147,483,647, then rolls over to -2,147,483,647 and begins counting back up to zero. The practical limit is actually somewhat less - in fact it has to do with the limit of the DBF size, and therefore depends on the record structure. But the main thing is that in reality, the limit will never be reached in most tables.
How can we control the values?
Normally you don't need to. In most applications, AutoInc values are used only for relating data between tables, and the user never sees them. So it doesn't matter what the values are, as long as they are unique and permanent. However, there's nothing to stop you from making these values visible to the user - as invoice numbers, for example - and in this case you might want to be able to “reset” the AutoIncrement counter to some arbitrary value. For example, most companies would prefer to start with invoice number 10,001 rather than 1, which might give the impression that the company is on its first trip around the block. Visual dBASE 7 does not have any built-in functions to control the next AutoInc value, but we can mess with the header of the DBF with low-level file functions to control it. The wheel to achieve this has already been invented, and it's available as freeware, in Ken Mayer's dUFLP library. Look for the file called Dbf7File.CC. If you decide to mess with the AutoInc counter, you might want to create a DISTINCT index tag on the AutoInc field, to prevent the accidental creation of non-unique values. The BDE will issue a “key violation” error if you try to create a duplicate key - there's probably no way to handle this error gracefully, but it's certainly better than letting the problem go undetected.
Why use AutoIncs?
AutoInc fields provide us with an easy and secure way of relating the data in two or more tables. This is illustrated in the example that follows.
A caveat to remember: Because the value of the AutoInc is not determined until the row is saved, you cannot create a child row in a parent-child relationship until the parent row is saved.
Additionally, AutoIncs give us persistent references that we can use to instantly locate specific rows. If you're experienced with VdB7, you already know that a rowset.bookMark() is only useful within that instance of that rowset, and even then, bookmarks are unusable after certain actions, such as changing the Query's SQL string, or issuing a Requery(). To set the row pointer to a desired row, rowset.findKey(nAutoIncValue) is very reliable.
An example of programming with AutoIncs
Suppose we have a small database, consisting of three tables: Regions, Customers, and Invoices. Here are the exact structures of these tables:
Table Fields Type, size Regions.dbf Region_No AutoInc RegionName Character, 30 Customers.dbf Customer_No AutoInc CustomerName Character, 30 Region_No Long Invoices.dbf Invoice_No AutoInc InvoiceDate Date Customer_No Long InvoiceAmount Numeric, 9, 2
Our little database is very nicely normalized, meaning the duplication of data is kept to a minimum. This design allows us to easily provide our users with the views of the data they'll probably want. Here are some demands that a user is likely to make:
Show me the invoices for a specific customerI created a form that answers all of these needs - and I did it quickly, with a minimal amount of coding. I was able to develop this form very efficiently, because I took advantage of three very cool features of Visual dBASE 7 that work neatly together, hand in hand: AutoInc fields, MasterRowset/MasterFields, and LookupSQL. The form looks like this:Show me the customers in a specific region
Show me how much has been invoiced for a specific region, or for a specific customer
Give me a user interface that easily lets me get all the above information, and easily lets me enter data into the 3 tables.

This single form is really a complete mini-application. The user can add or edit the rows of data directly in the grids. The LookupSQL properties of the Customers->Region_No and Invoices->Customer_No fields cause these fields be automatically morphed in the grids so that names are shown instead of the numeric values, and the user can choose the looked-up RegionName or CustomerName from comboboxes, right in the grids.
With the right index tags, it's easy to use MasterRowset/MasterFields to establish a parent-child-grandchild behavior, so that the user only sees the invoices belonging to the current customer, in reverse chronological order, and (optionally, with the checkbox) only sees the clients in the current region.
I should mention that in real-world applications, it's probably a better idea to make your grids read-only, and let the user add or edit rows via a dialog that has OK and Cancel buttons. There's just less chance that the user will mess things up that way. But for the purposes of this article, I decided to keep things simple and use the grids for both viewing and data entry.
Note that the user can change the spelling of a customer's name without breaking any referential integrity - in other words the customer's invoices don't get “lost”. The same can be said of regions: try changing “USA” to “United States”, and you'll find that doing so will not break any of the links between Customers and Regions (though you'll have to scroll the Customers grid to refresh it, so that “United States” is displayed for the American customers). No special programming is required to ensure this integrity. It all happens automatically, because our references are based on AutoInc values that never change.
Here is the complete source code for the sample form. Paste everything below into a new file called AutoInc.wfm (in the Command window, type: Modify Command Autoinc.wfm), and run it by pressing F2.
// AutoInc.wfm
// If the tables don't exist yet, create them, build the indexes, // and add a few records containing sample data:
if not file('Regions.dbf') // If the tables aren't found...
create table Regions (Region_No autoinc,; RegionName char(30) ) use Regions exclusive index on Region_No tag Region_No index on upper(RegionName) tag RegionName distinct
create table Customers (Customer_No autoinc,; CustomerName char(30),; Region_No integer ) use Customers exclusive index on Customer_No tag Customer_No index on upper(CustomerName) tag CustomerName distinct index on str(Region_No,7)+upper(CustomerName) tag RegionCustomerName distinct
create table Invoices (Invoice_No autoinc,; Customer_No integer,; InvoiceDate date,; InvoiceAmount numeric(9,2) ) use Invoices exclusive index on str(Customer_No,7)+dtos(InvoiceDate) tag Customer descending // Why did we create that funny index tag? Because it lets us view the // invoices for a specific customer in reverse chronological order // (the most recent invoice will be at the top of the list), by applying // SetRange(str(Some_Customer_No,7)) or MasterRowset/MasterFields. // // An index on Invoice_No is not needed by this form. However, if we // had an InvoiceDetails table, we would certainly need it.
// Append sample data:
use Regions append blank replace RegionName with 'Canada' append blank replace RegionName with 'USA'
use Customers append blank replace CustomerName with 'Smith & Co.',; Region_No with 1 // Canada append blank replace CustomerName with 'Weston Washers.',; Region_No with 1 // Canada append blank replace CustomerName with 'Big Money Investments.',; Region_No with 2 // USA append blank replace CustomerName with 'The Malarkey Corp.',; Region_No with 2 // USA
use Invoices append blank replace Customer_No with 1,; // Smith & Co. InvoiceDate with date()-1,; InvoiceAmount with 100 append blank replace Customer_No with 1,; // Smith & Co. InvoiceDate with date()-2,; InvoiceAmount with 200 append blank replace Customer_No with 2,; // Weston Washers InvoiceDate with date()-3,; InvoiceAmount with 300 append blank replace Customer_No with 2,; // Weston Washers InvoiceDate with date()-4,; InvoiceAmount with 400 append blank replace Customer_No with 2,; // Weston Washers InvoiceDate with date()-5,; InvoiceAmount with 500 append blank replace Customer_No with 3,; // Big Money Investments InvoiceDate with date()-6,; InvoiceAmount with 600 append blank replace Customer_No with 3,; // Big Money Investments InvoiceDate with date()-7,; InvoiceAmount with 700 append blank replace Customer_No with 4,; // The Malarkey Corp. InvoiceDate with date()-8,; InvoiceAmount with 800
use // Close the table
endif
*----------------------------------------------
** END HEADER -- do not remove this line
//
// Generated on 12/06/1999
//
parameter bModal
local f
f = new autoincForm()
if (bModal)
f.mdi = false // ensure not MDI
f.readModal()
else
f.open()
endif
class autoincForm of FORM
with (this)
height = 18.1818
width = 77.2857
text = "AutoInc sample form"
autoCenter = true
endwith
this.REGIONS1 = new QUERY()
this.REGIONS1.parent = this
with (this.REGIONS1)
onOpen = class::REGIONS1_ONOPEN
left = 44.4286
top = 1.7273
sql = 'select * from "REGIONS.DBF"'
active = true
endwith
this.CUSTOMERS1 = new QUERY()
this.CUSTOMERS1.parent = this
with (this.CUSTOMERS1)
onOpen = class::CUSTOMERS1_ONOPEN
left = 48.8571
top = 1.7273
sql = 'select * from "CUSTOMERS.DBF"'
active = true
endwith
this.INVOICES1 = new QUERY()
this.INVOICES1.parent = this
with (this.INVOICES1)
onOpen = class::INVOICES1_ONOPEN
left = 53.1429
top = 1.7273
sql = 'select * from "INVOICES.DBF"'
active = true
endwith
this.RECTANGLE3 = new RECTANGLE(this)
with (this.RECTANGLE3)
left = 0.8571
top = 16.8636
width = 75
height = 1
text = ""
border = false
fontSize = 8
fontBold = true
borderStyle = 1 // Raised
endwith
this.RECTANGLE2 = new RECTANGLE(this)
with (this.RECTANGLE2)
left = 0.8571
top = 5.2273
width = 75
height = 6.4091
text = ""
border = false
fontSize = 8
fontBold = true
borderStyle = 2 // Lowered
endwith
this.RECTANGLE1 = new RECTANGLE(this)
with (this.RECTANGLE1)
left = 0.8571
top = 0.2727
width = 75.1429
height = 4.6364
text = ""
border = false
fontSize = 8
fontBold = true
borderStyle = 2 // Lowered
endwith
this.REGIONSGRID = new GRID(this)
with (this.REGIONSGRID)
onRightMouseDown = {; this.datalink.beginAppend()}
dataLink = form.regions1.rowset
columns["COLUMN1"] = new GRIDCOLUMN(form.REGIONSGRID)
columns["COLUMN1"].dataLink = form.regions1.rowset.fields["RegionName"]
columns["COLUMN1"].editorType = 1 // EntryField
columns["COLUMN1"].width = 42.8571
with (columns["COLUMN1"].headingControl)
value = "RegionName "
endwith
bgColor = "white"
cellHeight = 0.8
hScrollBar = 0 // Off
height = 4.0455
left = 1.5714
top = 0.5455
width = 41.7143
endwith
this.CHECKBOX1 = new CHECKBOX(this)
with (this.CHECKBOX1)
onChange = class::CHECKBOX1_ONCHANGE
height = 1.0909
left = 44
top = 3.7273
width = 31.5714
text = "Constrain customers to this region"
value = false
endwith
this.CUSTOMERSGRID = new GRID(this)
with (this.CUSTOMERSGRID)
onRightMouseDown = {; this.datalink.beginAppend()}
dataLink = form.customers1.rowset
columns["COLUMN1"] = new GRIDCOLUMN(form.CUSTOMERSGRID)
columns["COLUMN1"].dataLink = form.customers1.rowset.fields["CustomerName"]
columns["COLUMN1"].editorType = 1 // EntryField
columns["COLUMN1"].width = 42.8571
columns["COLUMN2"] = new GRIDCOLUMN(form.CUSTOMERSGRID)
columns["COLUMN2"].dataLink = form.customers1.rowset.fields["Region_No"]
columns["COLUMN2"].editorType = 4 // ComboBox
columns["COLUMN2"].width = 24
with (columns["COLUMN1"].headingControl)
value = "CustomerName "
endwith
with (columns["COLUMN2"].editorControl)
dropDownHeight = 9.9545
endwith
with (columns["COLUMN2"].headingControl)
value = "Region "
endwith
bgColor = "white"
cellHeight = 0.8
hScrollBar = 0 // Off
height = 4.5455
left = 1.7143
top = 5.5455
width = 73.1429
endwith
this.INVOICESGRID = new GRID(this)
with (this.INVOICESGRID)
onRightMouseDown = {; this.datalink.beginAppend()}
dataLink = form.invoices1.rowset
columns["COLUMN1"] = new GRIDCOLUMN(form.INVOICESGRID)
columns["COLUMN1"].dataLink = form.invoices1.rowset.fields["Customer_No"]
columns["COLUMN1"].editorType = 4 // ComboBox
columns["COLUMN1"].width = 30
columns["COLUMN2"] = new GRIDCOLUMN(form.INVOICESGRID)
columns["COLUMN2"].dataLink = form.invoices1.rowset.fields["InvoiceDate"]
columns["COLUMN2"].editorType = 3 // SpinBox
columns["COLUMN2"].width = 17.1429
columns["COLUMN3"] = new GRIDCOLUMN(form.INVOICESGRID)
columns["COLUMN3"].dataLink = form.invoices1.rowset.fields["InvoiceAmount"]
columns["COLUMN3"].editorType = 3 // SpinBox
columns["COLUMN3"].width = 18.5714
with (columns["COLUMN1"].editorControl)
dropDownHeight = 9.9545
endwith
with (columns["COLUMN1"].headingControl)
value = "Customer "
endwith
with (columns["COLUMN2"].headingControl)
value = "InvoiceDate "
endwith
with (columns["COLUMN3"].headingControl)
value = "InvoiceAmount "
endwith
bgColor = "white"
cellHeight = 0.8
hScrollBar = 0 // Off
height = 4.5455
left = 1.5714
top = 12
width = 72
endwith
this.TEXT1 = new TEXT(this)
with (this.TEXT1)
height = 0.8182
left = 23.7143
top = 16.9545
width = 29.7143
text = "Right-click on the grids to add rows"
endwith
this.BTNTOTALFORREGION = new PUSHBUTTON(this)
with (this.BTNTOTALFORREGION)
onClick = class::BTNTOTALFORREGION_ONCLICK
enabled = false
height = 1
left = 44
top = 0.5455
width = 31.1429
text = "Total $ invoiced for this region"
value = false
endwith
this.PUSHBUTTON1 = new PUSHBUTTON(this)
with (this.PUSHBUTTON1)
onClick = class::BTNTOTALFORCUSTOMER_ONCLICK
height = 1
left = 5
top = 10.3182
width = 43.2857
text = "Total $ invoiced for this customer"
value = false
endwith
this.rowset = this.regions1.rowset
function regions1_onOpen
local r
r = this.rowset
r.indexName := "RegionName"
r.first()
// Create a calculated field, to be used as the MasterField
// in the parent-child relationship between Regions and Customers:
local f
f = new field()
f.fieldName := 'cRegion_No'
f.beforeGetValue := {|| str(this.parent['Region_No'].value,7) }
r.fields.add(f)
return
function customers1_onOpen
local r
r = this.rowset
r.indexName := "CustomerName"
r.first()
r.fields['Region_No'].lookupSql := 'Select * from Regions'
// Now create a calculated field, to be used as the MasterField
// in the parent-child relationship between customers and invoices:
local f
f = new field()
f.fieldName := 'cCustomer_No'
f.beforeGetValue := {|| str(this.parent['Customer_No'].value,7) }
r.fields.add(f)
return
function invoices1_onOpen local r r = this.rowset r.fields['Customer_No'].lookupSql := 'Select * from Customers' // Now make the invoices rowset a constrained child of Customers, // so that at any given time only the invoices belonging to // a single customer will be visible. r.indexName := 'Customer' r.MasterRowset := form.Customers1.rowset r.MasterFields := 'cCustomer_No' // The 7-character calculated field r.first() return
function btnTotalForCustomer_onClick
local r, bm, nTotal
nTotal = 0
r = form.invoices1.rowset
r.notifyControls := false // Prevent screen flicker.
bm = r.bookMark()
r.first() // Move to the first invoice for this customer.
do while not r.endOfSet
nTotal += r.fields['InvoiceAmount'].value
r.next()
enddo
r.goto(bm)
r.notifyControls := true
msgBox('Total for this customer is $ '+nTotal)
return
function btnTotalForRegion_onClick
local rCustomers, rInvoices, bmCustomers, bmInvoices, nTotal
nTotal = 0
rCustomers = form.Customers1.rowset
rInvoices = form.invoices1.rowset
rCustomers.notifyControls := false // Prevent screen flicker.
rInvoices.notifyControls := false
bmCustomers = rCustomers.bookMark()
bmInvoices = rInvoices.bookMark()
rCustomers.first() // Move to the first customer in this region.
do while not rCustomers.endOfSet
rInvoices.first() // First invoice for this customer
do while not rInvoices.endOfSet
nTotal += rInvoices.fields['InvoiceAmount'].value
rInvoices.next()
enddo
rCustomers.next()
enddo
// Now restore everything to the way we found it:
rCustomers.goto(bmCustomers)
rInvoices.goto(bmInvoices)
rCustomers.notifyControls := true
rInvoices.notifyControls := true
msgBox('Total for this region is $ '+nTotal)
return
function checkbox1_onChange // Turn ON or OFF the constraint of Customers to a single region: local r r = form.Customers1.rowset if this.value // If the constraint is being turned ON... r.indexName := 'RegionCustomerName' r.MasterRowset := form.Regions1.rowset r.MasterFields := 'cRegion_No' // The 7-character calculated field else // Turn off the constraint... r.indexName := 'CustomerName' r.MasterRowset := null r.MasterFields := null endif r.first() form.btnTotalForRegion.enabled := this.value return
endclass