ofdl provides interfaces for both C and lua. Since the languages are different, the interfaces are also a little different. However, the general idea is the same.
I currently am working on a couple of projects and they require functions you would find in a relational db. However, these programs need to be very transportable (i.e. users shouldn't be forced to run db software such as MySQL or Oracle). So, a single-file solution is the best.
Why not just use Berkeley DB library? As far I have read, the standard db library only provides a 'key' column and a 'data' column for the table and is not able to store more than one table in a file. Although it seems to go to great depths in optimizing the access and memory usage like a real db server does, it doesn't provide much functionality.
Another alternative is to embed a MySQL server into your application. I certainly would use it for a big project but I think that it's overkill for the job ofdl was designed for.
Introduces a new table to the database.
Adds a new field to the current table being defined. Currently only three types of data are available in ofdl databases. These are integers, fixed-length strings and variable-length strings. More types will follow later as the driver programs require them.
Field type is 1 for integers, 2 for variable length strings (or TEXTs) and (length<<8)|4 for a string whose maximum length is length. Field types are written as ascii strings.
Ends the definition block for the current table.
Marks the beginning of a record belonging to the given table. The table must be defined before any related RECORD instructions.
Sets the data for the mentioned field of the current record. The format of the data is:
| Field Type | Format |
| integer | 4 bytes representing the integer in LSB format |
| string with a maximum length | Contents of the string, padded with '\0' characters to the maximum length. |
| variable length string | Length of the string in ascii, followed by exactly one whitespace, followed by contents of the string. The length of the string includes all characters in the contents, including a null character if there is one. |
There must be exactly one whitespace character between the field name and the data.
Ends the current record definition block.
Ends the definition of the database file. The rest of the file is ignored.
When you want to manipulate/query data in an ofdl db, you do so by using a query. In other SQL APIs, a query is usually a string that is constructed by the user which then gets sent to the server for execution. ofdl uses functional programming techniques to let the user express very powerful predicates and operations. This relieves the user from constructing query strings, and re-encoding of data.
All ofdl functions can operate on a single table at a time. Although ofdl does not support joins, you can emulate them by executing a new select query inside a where clause.
There are 3 steps involved in executing an ofdl query.
This step compiles the given query and fetches the values provided from the C environment. No expression evaluation occurs at this point. The compiler returns a cursor which contains the necessary code for the query.
Also called iteration in this document, this step performs the action stored in the cursor. All interaction with the C environment (such as function calls and referencing pointers) occur in this step.
When executing an operation, a cursor is used for holding the context of the operation. This allows the library to return multiple rows for a query or repeat an operation using different data. Cursors also allow you to execute multiple operations at the same time, providing different contexts for each of them. You get a cursor handle when you prepare the statement for execution.
This step de-allocates the memory associated with the query.
All ofdl functions and macros are defined in <db/db.h> . If you want errors to terminate your application with a SIGSEGV, then don't
#include <db/error.h>
..
main() {
...
errstream= memsopen();
...
}
in your main file. I personally use it
without initializing the error stream and catch errors as soon as
they occur.
To link with ofdl, just adding libdb.a to your objects should be enough. ofdl doesn't depend on anything else.
Queries may be composed of three different elements: string literals that generally contain the code to execute, C values that contain the values to be passed to ofdl, and addresses of variables in which you expect values to be returned. The latter two have to be rewritten in special forms which allows ofdl functions to identify them. Since ofdl is only a library and not a preprocessor system, these conventions must be followed.
A query is a list of elements mentioned above seperated by commas. This structure makes it possible to define ofdl functions that take a query argument as variadic functions and to make the C compiler pass the values to the functions, as opposed to translating the query to another form before the C compiler gets its hands on the code. Here are the rules for writing a valid query:
If you just want to write some code, you write it as a C string literal. Example:
db_insert(db,"into items itemno=55;");
Note that you must use C lexemes for all code you are going to execute. So, all identifiers (table and colum names), string literals, character constants and integers must be valid C lexemes of the corresponding kind. The keywords 'from' , 'into' and 'where' are only recognized when they are expected. Since the SQL part of the syntax is basicly an r.e. a column called say, 'where' is perfectly valid.
Expressions in a query can use almost all C operators. The only ones left out are the array-reference operator [] and the ternary operator ?: . Only C forms of logical operators && || and ! are recognized. 'and' or 'or' are just identifiers.
When you want to pass a C value to be used in an expression, you must pass it through the cv() macro. Example usage:
db_delete(db,"from employee where empid==",cv(empid),";");There is no difference between using int and char * values. Arithmetic and comparision operators are defined for string values as well.
If you want a value to be returned in a C variable, you just write the assignment as you would do in C, but you must take care to pass the variable's name through the ca() macro. Example usage:
char *name;
..
db_select(db, ca(name),"=name ,",
ca(id),"=id from employee where salary>=",
cv(5000),";");
There are two ways to fetch string values into C variables. One of them,
using the ca() macro as shown above, creates a fresh copy of the
string and stores the address of the new string in the given variable.
Although this a dynamically allocated string, you should not try to
de-allocate or to resize it: ofdl manages that string.
The other method is better suited to fetching strings that have a fixed maximum length. If the name field had a maximum size, the above code would look like:
char name[40];
..
db_select(db, cv(name),"=name ,",
ca(id),"=id from employee where salary>=",
cv(5000),";");
This method of using the cv() macro copies the contents of the
field named 'name' into the buffer pointed by the C variable 'name'. Actually
the ca() macro is defined as #define ca(x) "*",cv(&(x))Note the use of the pointer reference operator in this definition.
Every query must end with a string literal element which contains a semicolon (;) character as its last non-whitespace character. Above examples all demonstrate this rule.
Expressions in a query can also contain function calls. A function called from ofdl can take any number and type of arguments but each argument must be 32-bits long when promoted for function call. When used as a boolean expression, a function must return an integer value. If an arithmetic operator is to be applied to the return value, then it should return a value matching the other operands of the operator. Example usage of a function call:
int isgood(char *name,int salary) {
if ..some_condition..
return 1;
else
return 0;
}
char *strtoupper(char *txt, allocator *context) {
char *r = context_alloc(strlen(txt)+1,context);
..uppercase txt into r..
return r;
}
...
db_select(db,ca(upname),"=",cv(strtoupper), "(name,",cv(context),")",
"from employee where", cv(isgood),"( name, salary );");
...
All this probably looks like gibberish, but it's the easiest way I could think of, without resorting to a preprocessor.
| Data Type | Description |
| DB | Handle for an ofdl database. Variables of this type are always used by value, since the type is a pointer. |
| DBCURSOR | Handle for an ofdl operation. Again, this is a pointer type. |
| char * | The usual C strings are used for communicating string values between ofdl and your program. You should not store a string returned by ofdl since it may be deallocated by ofdl functions later. |
| int | C ints are used for communicating integers. You must always use the correct type for assigning to/from db fields. So, passing a string value when an integer is required or vice-versa will mess things up. |
| FSTREAM | There is only one variable with this type. See error handling below. |
Reads a database from the given file and returns the handle for it. Returns NULL if it fails for some reason.
Writes the db to the given file.
Returns a cursor handle that can be used to iterate the given query over the db. QUERY has the form :
<expression> from <tablename> [ where <where-expr> ] ;
When iterated, a cursor returned from db_select will calculate <expression> for each record that match the where expression (or for each record in the table if it is missing). <expression> may be any valid C expression. You may use it to assign values to C variables, call functions, access values through pointers etc.
An iteration of the select statement terminates when a record that matches the where expression is found. So, you should use it in a loop like
while (db_iterate(cursor)) ;in order to make it operate on all records.
DBCURSOR db_update(DB db, <QUERY>);
Returns a cursor handle that can be used to update the db using the given query. The query has the form:
<tablename> set <expr> [ where <where-expr> ] ;
When iterated, a cursor returned from db_update will calculate <expr> for each record that matches the where expression.
An iteration of the update statement updates all records in the database. See the discussion in the delete function below.
Returns a handle that can be used to delete records from the db using the given query. The query has the form:
from <tablename> [ where <where-expr> ] ;When iterated, a cursor returned by this function will delete all records in the table that matches the where expression. An iteration deletes all records as opposed to each iteration deleting one matching record. In general, only select needs a cursor, since it may return multiple rows that must be processed seperately. Other operations are not generally expected to return any values. However, having cursors for other operations may also be handy: you could run the same query multiple times with different C values without the need for re-compiling the query. This is both faster (doesn't matter much though) and more convenient to use. You would have one instance of the cursor for all operations that do the same thing, which is easier to maintain.
DBCURSOR db_insert(DB db, <QUERY>);
Returns a cursor handle that can be used to insert new records into the specified table. The query has the form:
into <tablename> <expr>When iterated, a cursor returned by this function will insert a record in to the specified table and evaluate <expr> on that record.
int db_iterate(DBCURSOR c);
This function will execute the action associated with the cursor. For a cursor created by db_select, the return value is 1 if a record matching the where clause was found, otherwise it is zero. For a cursor created by db_insert, the return value is nonzero unless there was a run time error. For cursors created by db_delete or db_update, the return value is the number of records that matched the where clause.
int db_close(DBCURSOR c);
This function will close the cursor and deallocate all memory associated with it. Return value is 1, unless the cursor is NULL in which case the return value is 0.
int db_once(DBCURSOR c);
This function iterates the given cursor once and then closes it. It's primarily intended for use with the db_insert, db_update and db_delete functions. In order to execute a query and not deal with the cursor stuff at all, one could write:
db_once(db_insert(db,"into employee name=",cv(name),",
"department= ",cv(dept),";"));
If the cursor is NULL, the return value is zero. Otherwise the return
value is the return value of db_iterate(c).
There are 5 classes of errors that can occur while using ofdl:
When an error is caught, the reason for it is written to the global variable FSTREAM *errstream, using the functions in io.h. Also, the called ofdl function will return a zero. This will change in the future to return a negative numeric constant identifying the error.
As it can be seen from the list above, almost all errors can be checked
for at compile time, were a preprocessor/checker was implemented for
ofdl functions. This may be done in future, depending on how many programs
use ofdl and how badly it is needed.
You will also need to link with dblua.o in addition to libdb.a
For a function used as a where clause, the prototype is:
Reads the database from the file named by string s and returns the db
handle as a string. Returns nil if unsuccessful.
Writes the database db to the file named fn.
selectorf is a function to return values from a record. The single argument
is a luatable including the fields of the current record. The function returns
the necessary fields and/or any computed fields based on the argument.
db_select returns a cursor handle (encoded as a string). Example:
Iterates the given cursor and returns the results the selector function
returns. Unlike the C interface, only select operations may be iterated.
Return value is a single nil if there are no more records that match the
where clause given in db_select
Example: Closes the given cursor. It's no
longer valid.
fields is a luatable containing the fields of a record to
be inserted in the given table. This function returns nothing. Example: Deletes
all records from the table.
updatef is a function that modifies the record given to it as it's
only argument. This function returns nothing. Example:
Well, there is almost no error handling. See the associated section.
This library should be used only with small data sets,
which fit in the memory because it's the way it works. Files are just
for storing the stuff for reading later as opposed to being used
as a medium from which to access data directly. If you are dealing
with so much data that ofdl can't handle well, you are better off using a
real db server anyway. ofdl was written in a couple of days and will
never reach the maturity of a real server.
An interface for creating and modifying databases should be created. ofdl misses functions to
create a db and create/modify/destroy tables in it.
Currently there is no way to include a null character inside a variable-length
text field. This prohibits storing binary data (BLOBs) inside these fields.
The Lua Interface
DB handles and DBCURSOR's are represented as strings inside lua.
These strings should not be used for anything else. The proper way to do
it would be to use userdata types but this works and is implemented easily.
The lua interface is not pure. It is designed for an application that embeds lua.
So you will have to do a little preparation before you can use
ofdl from lua.
Compiling and Linking
Before you can use ofdl functions, you need to let ofdl register it's
functions with the lua interpreter. The function to do this is:
void db_register_lua_functions(lua_State *l);
which is defined in <db/dblua.h> . Queries
Lua predicates and operations are just lambda functions. Unlike C counterparts,
ofdl functions for lua always take a fixed number of arguments. ofdl does no
parsing on these arguments.
function (R)
The argument is a record from the table being filtered. Each field in the record
is stored inside the luatable R as key-value pairs. The return value is non-nil
if this record should be included in the set, nil otherwise. Note that where clauses
are not optional, you should use function (R) return 1 end
when
you want all records to be operated on.
Lua Functions
In all of the function declarations below, db is a string holding the database handle.
tablename is the name of a table inside the database. This name should always
be valid. It's likely that you will get a SIGSEGV if not so. whereclauses
are predicates as explained in the queries section.
x= db_select(db, function (R) return R.id,R.name end, "employee",
function (R) return R.salary >= 2000 )
id,name= db_iterate(x)
if id then
print(id,name)
end
db_insert(db, "employee", { id= 555, name="John Smith", salary= 2500 } )
db_update(db,function (R) R.salary=R.salary*1.1 end, "employee",
function (R) return R.name=="John Smith" end )
Bugs and Limitations
Download and Installation
You can download the latest version from my
homepage. You will also need
tlib. You need to modify the Makefile
in order to build it. Needless to say, you need the lua
library if you want to use the lua interface.
Included are two test programs for the two interfaces.
cinarus at yahoo dot com