Btrieve is
a transactional database product based on Indexed Sequential Access Method (ISAM), which is a way of storing data for fast retrieval.
It's also the backend for Pervasive Software's Pervasive.SQL product, which is a combination of the Micro-Kernel Database Engine and Scalable SQL relational database. At work, our Service Business Management Suite stores data in a Pervasive.SQL database, which is actually a collection of Btrieve data files and the DDFs (Data Definition Files) that describe their layout.
If you're a Btrieve user, then all of that made perfect sense.
Admittedly, I was only introduced to Btrieve and Pervasive.SQL eight months ago, so I don't claim to be an expert by any means. But, after using it for a while, I've begun to see how interesting Btrieve actually is. It's incredibly easy to use when interfacing via Pervasive's ADO.NET provider, but I wanted to bypass the relational engine and use the transactional engine instead (cause I like to make things difficult for myself).
The only .NET wrapper for the transactional Btrieve API that I could find was Btrieve Classes for .NET from TechKnowledge. It looks cool, but I don't really want to spend that kind of cash. So, I decided to write my own, which wasn't as difficult as I thought it would be.
I might upload a very, very, very early alpha version soon, but until then, here's a little sneak peek at what my Btrieve .NET library looks like.
At the moment, there are no classes to read DDFs, mostly because I can't find any documentation of their structure. If anyone has any information about that, please let me know in the comments. Anyway, until a DDF reader is implemented, you have to define record layouts in code. Even after a DDF reader is added, though, manual record layout construction will still be possible. Here's an example of a simple record:
public static class Note {
public static BtrieveRecordDefinition Record = BtrieveRecordDefinition {
Fields = new BtrieveFieldDefinitionList {
new BtrieveFieldDefinition("ID", 0, BtrieveDataTypes.INTEGER),
new BtrieveFieldDefinition("BODY", 4, 255, BtrieveDataTypes.CHAR)
}
}
}
This record contains two fields: ID and BODY. ID begins at offset 0 of the data buffer and is an INTEGER data type. BODY begins at offset 4, is up to 255 bytes in length, and is a CHAR data type. Now, how do we actually get some data?
Btrieve data files are read using the BtrieveFile class. Here's a quick example:
BtrieveFile notesFile = BtrieveFile.Open(@"C:\Data\NOTES.DAT", Note.Record);
As you can see, we're opening the Btrieve data file containing notes and telling it to read the records according to the record definition that we defined in our static Note class. The BtrieveFile class is enumerable, so reading records is incredibly easy:
foreach (BtrieveRecord note in notesFile) {
Console.WriteLine(note.Get<string>("BODY"));
}
You can also navigate through records manually:
notesFile.MoveNext();
notesFile.MovePrevious();
notesFile.MoveFirst();
notesFile.MoveLast();
Btrieve data types are implemented as classes derived from the abstract BtrieveDataType class. Each data type class handles conversion from data in the buffer to the appropriate managed data type. For example, the BtrieveCharType class converts the value of CHAR fields to a .NET string and back again. A single static class called BtrieveDataTypes (notice the "s" on the end) contains static instances of each common data type, so you don't have to create a new instance for every field definition. Some data types, such as INTEGER are fixed-length. Others, such as CHAR, are variable-length. If you attempt to assign a variable-length data type to a field without specifying it's Size parameter, you will get an exception.
You can also create your own data types, if you need to convert a value in your records to a custom class. All you have to do is derive from BtrieveDataType and tell the wrapper how to read and convert your data from the buffer. Then just specify your custom class in your field definition.
I've been talking about the data buffer, but I haven't actually explained what it is. The Btrieve API passes data to applications via a data buffer, which is an allocated block of memory. A class called BtrieveDataBuffer handles the allocation of memory and the conversion of common types to and from the buffer. It's unlikely that you will need to create your own buffer, but you may need to interact with it directly (for example, when creating a custom Btrieve data type).
BtrieveDataBuffer buffer = new BtrieveDataBuffer(255);
string helloWorld = "Hello, world";
buffer.WriteString(helloWorld, 0);
Console.WriteLine(buffer.ReadString(0, helloWorld.Length));
Above, we created a new 255 byte buffer, wrote the strong "Hello, world" to offset 0, and read it back again. It's important to point out that both BtrieveFile and BtrieveDataBuffer implement IDisposable. Both classed will handle the freeing of memory and the closing of Btrieve files automatically during garbage collection. This also means that both can be used in "using" blocks.
Certain data types may require a little bit of configuration on your part. For example, the LOGICAL data type doesn't define a specific value for true and false. It's up to your application to define them (it's an annoying Btrieve quirk, but I suppose it could be useful in some cases). By default, my wrapper defines true as 1 and false as 0. You can change these values like this:
BtrieveLogicalType.TrueValue = "Y";
BtrieveLogicalType.FalseValue = "N";
Now when reading a LOGICAL field, the wrapper will convert "Y" to a .NET boolean with a value of true and "N" to a .NET boolean with a value of false. If it encounters a LOGICAL field whose value does not match either of those values, an exception will be thrown. You can, however, specify that you want all unknown values automatically converted to false:
BtrieveLogicalType.ExceptionOnValueMismatch = false;
If you really want to get your hands dirty, you can call up Btrieve operations yourself using a handy little wrapper method around the BTRCALL Win32 function called BtrieveAPI.Call(). This method does some extra things to make your life a little easier and also raises a BtrieveException when the returned status code is anything but NO_ERROR.
Anyway, that's just a little sneak peek at what you will be able to do. Things will likely change in the future, but I think this is a good starting point, at least. For my closing act, here's a list of features implemented and not implemented...
Implemented
- Open and close Btrieve data files
- Define records using BtrieveRecordDefinition and BtrieveFieldDefinition
- Enumerate records or move through them manually using MoveNext, MovePrevious, MoveFirst, and MoveLast
- Read and write data in an unmanaged Btrieve data buffer
- Data Types: CHAR, INTEGER, LOGICAL, DATE
- Define your own custom Btrieve data types
- Retrieve client/server version information
Not Implemented
- Floating-point data types and others not mentioned in the Implemented section
- Insert and update records
- Generate record definitions from DDFs
- Null values
- Index/key information retrieval
- Extended operations
- Chunk operations
- and a bunch of other stuff that I probably missed
If there are any major Btrieve geeks out there who would be interested in giving some pointers from time to time, please let me know. I have to admit that data type conversion is getting pretty complicated.
Thanks for reading!