This is a module for Jai that allows you to interface with SQLite however works best for you.
However, this module is still in early development and lacks rigorous testing & some features.
The example is out of date and broken -- look here for a more up-to-date example instead!
By #import
ing SQLite
, you get access to bindings for the SQLite C API. These
work exactly as you'd expect.
Unless you #import "SQLite"(ONLY_C_BINDINGS=true)
, you also get a more Jai-friendly set of wrapper functions.
(note that the latter is not an exhaustive port of all SQLite functions, at least, yet)
The database handle parameter is omitted from every wrapping procedure signature, as it is stored in
the context
instead.
Instead of wrapping sqlite3_exec()
as exec()
, a more Jai-friendly exec()
is provided:
exec :: (
sql: string, // the SQL statement
params: ..Any, // parameters to bind
$code: Code = #code,null, // run this code for each returned row
$ignore_returned_rows := false // ignore returned rows
) -> Result
This allows the user to provide a Code
that should be run for each row returned from the database,
instead of a callback function. Additionally, it bind()
s each of params
, so your sql
statement can include parameter notation.
In addition, several procedures that wrap exec()
are provided to make common tasks easy:
exec_row(T, sql, ..params) -> result, T, success
returns a single row of typeT
exec_rows(T, sql, ..params, allocator=temp) -> result, [..] T
returns an array of rows of typeT
exec_column(T, sql, ..params, allocator=temp) -> result, [..] T)
returns an array of values of typeT
from a single columnexec_value(T, sql, ..params) -> result, T, success
returns a single value of typeT
To execute a query that doesn't return any rows, you can just write:
some_id := 4;
result := SQLite.exec("DELETE FROM User WHERE id = ?", 4);
exec_row()
and exec_rows()
work with Jai's anonymous structs, too:
min_author_id := 2;
result, rows := SQLite.exec_rows(
struct { author_name: string; count: int; total_score: int; },
#string __SQL__
SELECT
User.name AS author_name,
COUNT(*) AS count,
SUM(Post.score) AS total_score
FROM Post
LEFT JOIN User ON User.id = Post.author_id
WHERE
author_id >= ? AND
parent_id IS NULL
GROUP BY author_id
__SQL__,
min_author_id
);
assert(result == .OK);
for rows log("%1\n posts: %2\n total score: %3", it.author_name, it.count, it.total_score);
In the above example, all of the column names in the SQL statement match the member names of the
struct. This seems like a pretty good idea, so by default we check this at runtime (unless a given
column is unnamed). Thus, exec_row()
and exec_rows()
have an additional optional
check_column_names
parameter, which is true
by default, but can be set to false
, if, for
whatever reason, you don't want this behavior.
You can also #import "SQLite"(USE_MODEL_CACHE=true);
, which, if you've set up your metaprogram
correctly (see the “overview” example), allows you to more easily create
Jai structs that can automatically interface with SQLite, like this:
using SQLite;
My_Struct :: struct { using #as model: Model;
name: string;
boolean: bool;
number: int;
big_num: s64;
real: float;
big_real: float64;
other: Cached(My_Other_Struct);
private: int; @do_not_serialize;
}
All of the members in the above struct--except for the last one--will be automatically de/serialized to/from your SQLite database, when using the model cache.
Cached(My_Other_Struct)
is used for foreign-key references. in the example above, other
will
serialize to SQL as other_id
, which will contain the ID of the My_Other_Struct
that other
points to.
There's no need to include your own ID member, as that's included for you in Model
, along with
created
and modified
timestamps. (The timestamps are all handled in SQLite, and, for now,
returned to you in Jai as UNIX timestamps.)
See the “overview” example for a sample use of this module. note that overview.jai
contains the metaprogram, and src/Main.jai contains the actual
program.
There are various notes (@xxx
) you can annotate the members of your model structs with, that make
things easy for you:
Indicates that this member should not be stored as a field in the SQLite table
Indicates that this member should be automatically fetched when an instance of this model struct is retrieved from the database.
If a model Model_Name
has a member member_name
that is both @autofetch
and
@do_not_serialize
, then you must provide a procedure called fetch_member_name
within the body
of the model struct, that takes in a *Cached(Model_Name)
and returns nothing. This procedure
will be automatically executed when the instance is retrieved from the database.
If the member is not @do_not_serialize
, then the member type must be a Cached(T)
, and it
will be fetched automatically every time an instance of this model type is retrieved from the
database.
Here's an example:
Foo :: struct { using #as model: Model;
// bar will be fetched automatically (if not NULL)
bar: Cached(Bar); @autofetch
// baz will be "fetched" automatically, using the procedure below
baz: int; @autofetch @do_not_serialize
fetch_baz :: (using foo: *Cached(Foo)) { baz = 42; }
}
Bar :: struct { using #as model: Model; name: string; }
// ...
bar := insert(Bar.{name="BAR"});
insert(Foo.{bar=bar});
reset_model_cache(); // reset the cache so we can prove autofetching does in fact work
foo := select_by_id(Foo, 1);
assert(foo.bar.name == "BAR"); // proving bar was autofetched
assert(foo.baz == 42 ); // proving baz was autofetched
Caution should be exercised when using @autofetch
! Only @autofetch
if you're absoutely
certain that:
- you will in fact use that member every time you retrieve a model instance from the database
- your models'
@autofetch
members do not have any circular dependencies
The basic SQLite wrapper will add a sqlite: SQLIte_Info
to the context
, which contains the
current database connection and other such bookkeeping.
Using the model cache will add an additional sqlite_model_cache
struct to the context
, which is a cache of rows
retrieved from the database. You can flush this cache at any time using reset_model_cache();
.
- fix the
overview
example - massively cull the
assert_ok()
s, with something like anautomatically_assert_ok
field inSQLite_Info
- delete
a ton ofeven moreyet even more code fromModel_Cache
now that we have a betterexec()
and such - more compile-time checking of things to make usage more pleasant, e.g. help the user if they forget to
#as
theusing #as model: Model;
in their models - actually try this out with slightly non-trivial things like threading and such and see if it actually is good at all lmao
- finish wrapping the rest of the SQLite API
- test the code, like, at all
- write documentation
set(T, field_name, value, where, ..params)
delete_from(T, where, ..params)
insert(objs)
- work on the automatic migration system -- we can do SQLite stuff at compile time, so why not store it in a SQLite database?
- eventually -- in the distant future -- we could resolve conflicts between migration dbs, creating a new, "merged" migration db
@default=value
for model fields- figure out what to do about
NULL
values. consider something like@null_if_default
/@null_if=value
/@not_null
/...?Nullable(T)
...? ...?
- maybe just huck the SQLite error message into the
context
? - maybe have the wrapper functions return enums that are subsets of
Result
? (#must
...?) - more generally, figure out if there's a more ergonomic way to handle SQLite result and/or error passing