How to use
First, import Quill on your Zig source file.
Now, import common quill modules to use through out the examples.
const Dt = quill.Types;
const Uuid = quill.Uuid;
const Quill = quill.Quill;
const Qb = quill.QueryBuilder;
Initialize the General Propose Allocator (GPA) within the main
function.
var gpa_mem = std.heap.DebugAllocator(.{}).init;
defer std.debug.assert(gpa_mem.deinit() == .ok);
const heap = gpa_mem.allocator();
Initial Setup
Let's Initialize an on disk database with global configuration.
try Quill.init(.Serialized);
defer Quill.deinit();
var db = try Quill.open(heap, "hello.db");
defer db.close();
Schema Declaration
Now, declare some demo schema to use through out the examples.
const Gender = enum(u8) { Male = 1, Female = 2 };
const Social = struct { website: []const u8, username: [] const u8 };
pub const Model = struct {
uuid: Dt.CastInto(.Blob, Dt.Slice),
name1: Dt.CastInto(.Text, Dt.Slice),
name2: ?Dt.CastInto(.Text, Dt.Slice),
balance1: Dt.Float,
balance2: ?Dt.Float,
age1: Dt.Int,
age2: ?Dt.Int,
verified1: Dt.Bool,
verified2: ?Dt.Bool,
gender1: Dt.CastInto(.Int, Gender),
gender2: ?Dt.CastInto(.Int, Gender),
gender3: Dt.CastInto(.Text, Gender),
gender4: ?Dt.CastInto(.Text, Gender),
about1: Dt.CastInto(.Blob, Dt.Slice),
about2: ?Dt.CastInto(.Blob, Dt.Slice),
social1: Dt.CastInto(.Text, Social),
social2: ?Dt.CastInto(.Text, Social),
social3: Dt.CastInto(.Text, []const Social),
social4: ?Dt.CastInto(.Text, []const Social)
};
pub const View = struct {
uuid: Dt.Slice,
name1: Dt.Slice,
name2: ?Dt.Slice,
balance1: Dt.Float,
balance2: ?Dt.Float,
age1: Dt.Int,
age2: ?Dt.Int,
verified1: Dt.Bool,
verified2: ?Dt.Bool,
gender1: Dt.Any(Gender),
gender2: ?Dt.Any(Gender),
gender3: Dt.Any(Gender),
gender4: ?Dt.Any(Gender),
about1: Dt.Slice,
about2: ?Dt.Slice,
social1: Dt.Any(Social),
social2: ?Dt.Any(Social),
social3: Dt.Any([]const Social),
social4: ?Dt.Any([]const Social)
};
pub const FilterUser = struct { name1: Dt.Slice, age1: Dt.Int };
pub const ModelProfile = struct {
name2: ?Dt.CastInto(.Text, Dt.Slice),
age2: ?Dt.Int,
};
Container Operations
Create a New Container
Following example will create a new container based on a given record Model
structure.
const sql = comptime Qb.Container.create(Model, "users", .Uuid);
var result = try db.exec(sql);
result.destroy();
Remarks: For other container related operations please see - Builtin Modules
Record Operation
Let's declare a Callback function. Following function is used in case you want to capture exec()
and remove()
result for the INSERT
, UPDATE
, and DELETE
SQL statement.
fn resultCallback(result: Quill.Result, affected: i64) void {
const fmt_str = "Result: {any}\nAffected Records: {d}\n";
std.debug.print(fmt_str, .{result, affected});
}
Create a New Record
Following example will insert a new record on a given container.
const sql = comptime blk: {
var sql = Qb.Record.create(Model, "users", .Default);
break :blk sql.statement();
};
// Preparing Record Data
// Mixing both static and dynamic data for checking memory leaks
const name = "John Doe";
const msg = "This is the story about " ++ name;
const about = try heap.alloc(u8, msg.len);
defer heap.free(about);
const soc_dyn = try heap.create(Social);
soc_dyn.* = Social {.website = "example.one", .username = name };
defer heap.destroy(soc_dyn);
const soc = Social { .website = "example.one", .username = name };
const record_data = Model {
.uuid = .{.blob = &Uuid.new()},
.name1 = .{.text = name},
.name2 = null,
.balance1 = 10.50,
.balance2 = null,
.age1 = 31,
.age2 = 30,
.verified1 = true,
.verified2 = null,
.gender1 = .{.int = .Male},
.gender2 = null,
.gender3 = .{.text = .Male},
.gender4 = null,
.about1 = .{.blob = about},
.about2 = null,
.social1 = .{.text = soc_dyn.*},
.social2 = null,
.social3 = .{.text = &.{soc, soc_dyn.*}},
.social4 = null
};
var crud = try db.prepare(sql);
defer crud.destroy();
try crud.exec(record_data, null, resultCallback);
Find Record
Following example will retrieve a single record from a given container.
const sql = comptime blk: {
var sql = Qb.Record.find(View, void, "users");
break :blk sql.statement();
};
var crud = try db.prepare(sql);
defer crud.destroy();
const result = try crud.readOne(View, null);
defer crud.free(result);
if (result) |data| {
std.debug.print("Find Result For: {s}\n", .{data.name1});
} else {
std.debug.print("Found 0 Result!\n", .{});
}
Following example will retrieve a single record from a given container with filtered result.
const sql = comptime blk: {
var sql = Qb.Record.find(View, FilterUser, "users");
sql.when(&.{
sql.filter("name1", .@"=", null),
sql.chain(.AND),
sql.filter("age1", .@"!=", null)
});
break :blk sql.statement();
};
const filter = FilterUser {.name1 = "Jane Doe", .age1 = 20};
var crud = try db.prepare(sql);
defer crud.destroy();
const result = try crud.readOne(View, filter);
defer crud.free(result);
if (result) |data| {
std.debug.print("Find Result For: {s}\n", .{data.name1});
} else {
std.debug.print("Found 0 Result!\n", .{});
}
Find Records
Following example will retrieve a multiple record from a given container.
const sql = comptime blk: {
var sql = Qb.Record.find(View, FilterUser, "users");
break :blk sql.statement();
};
var crud = try db.prepare(sql);
defer crud.destroy();
const results = try crud.readMany(View, null);
defer crud.free(results);
std.debug.print("Found Records: {d}\n", .{results.len});
for (results) |result| {
std.debug.print("Find Result For: {s}\n", .{result.name1});
}
Following example will retrieve multiple records from a given container with filtered result.
const sql = comptime blk: {
var sql = Qb.Record.find(View, FilterUser, "users");
sql.when(&.{
sql.filter("name1", .@"=", null),
sql.chain(.OR),
sql.filter("age1", .@"!=", null)
});
break :blk sql.statement();
};
const filter = FilterUser { .name1 = "Jane Doe", .age1 = 35 };
var crud = try db.prepare(sql);
defer crud.destroy();
const results = try crud.readMany(View, filter);
defer crud.free(results);
std.debug.print("Found Records: {d}\n", .{results.len});
for (results) |result| {
std.debug.print("Find Result For: {s}\n", .{result.name1});
}
Count Record
Following example will return matching records count from a given container.
const sql = comptime blk: {
var sql = Qb.Record.count(void, "users");
break :blk sql.statement();
};
var crud = try db.prepare(sql);
defer crud.destroy();
const result = try crud.count(null);
std.debug.print("Record Count: {d}\n", .{result});
Following example will return matching records count from a given container with filtered result.
const sql = comptime blk: {
var sql = Qb.Record.count(FilterUser, "users");
sql.when(&.{
sql.filter("name1", .@"=", null),
sql.chain(.OR),
sql.filter("age1", .@"!=", null)
});
break :blk sql.statement();
};
const filter = FilterUser { .name1 = "Jane Doe", .age1 = 35 };
var crud = try db.prepare(sql);
defer crud.destroy();
const result = try crud.count(filter);
std.debug.print("Record Count: {d}\n", .{result});
Update Record
Following example will update all records to a given container.
const sql = comptime blk: {
var sql = Qb.Record.update(ModelProfile, void, "users", .All);
break :blk sql.statement();
};
const profile = ModelProfile {
.name2 = .{.text = "Custom Name"},
.age2 = 33
};
var crud = try db.prepare(sql);
defer crud.destroy();
try crud.exec(profile, null, resultCallback);
Following example will update all records to a given container based on filtered result.
const sql = comptime blk: {
var sql = Qb.Record.update(ModelProfile, FilterUser, "users", .Exact);
sql.when(&.{
sql.filter("name1", .@"=", null),
sql.chain(.AND),
sql.filter("age1", .@"=", null)
});
break :blk sql.statement();
};
const profile = ModelProfile {
.name2 = .{.text = "Another Name"},
.age2 = 23
};
const filter = FilterUser { .name1 = "John Doe", .age1 = 31 };
var crud = try db.prepare(sql);
defer crud.destroy();
try crud.exec(profile, filter, resultCallback);
Remove Record
Following example will remove all records to a given container.
const sql = comptime blk: {
var sql = Qb.Record.remove(void, "users", .All);
break :blk sql.statement();
};
var crud = try db.prepare(sql);
defer crud.destroy();
try crud.remove(null, resultCallback);
Following example will remove all records to a given container based on filtered result.
const sql = comptime blk: {
var sql = Qb.Record.remove(FilterUser, "users", .Exact);
sql.when(&.{
sql.filter("name1", .@"=", null),
sql.chain(.AND),
sql.filter("age1", .@"=", null)
});
break :blk sql.statement();
};
const filter = FilterUser { .name1 = "John Doe", .age1 = 31 };
var crud = try db.prepare(sql);
defer crud.destroy();
try crud.remove(filter, resultCallback);
ACID Session
ACID Session provides a SQLite transaction, to group one or more SQL statements into a single unit of work that is executed atomically — meaning all or nothing.
try Quill.AcidSession.start(&db, null);
errdefer Quill.AcidSession.end(&db, .Rollback, null) catch |err| {
std.debug.print("{s}\n", .{@errorName(err)});
};
// Your multiple database operations here..
try Quill.AcidSession.end(&db, .Commit, null);
Remarks: &db
argument is your database (*Quill
) instance.
Blob Stream
Provides a stream interface for progressive read, write on large Blob data.
Read form Stream
Following example will progressively read blob data as a chunked buffer.
const BlobStream = Quill.BlobStream;
var buffer: [1024 * 8]u8 = undefined;
var blob = try BlobStream.open(&db, "users", "data", 1, .Read);
defer blob.close();
while(try blob.read(&buffer)) |data| {
std.debug.print("Chunk: {s}\n", .{data});
}
Write to Stream
Following example will write a chunked buffer data to the given offset position.
const BlobStream = Quill.BlobStream;
var blob = try BlobStream.open(&db, "users", "data", 1, .ReadWrite);
defer blob.close();
const data = "World!";
const offset = blob.size() - 6;
try blob.write(data, offset);
Remarks: Make sure the .ReadWrite
permission is set on open()
.
Miscellaneous
Quill has some additional utility modules for repeated codes. Import following module to use through out the examples.
UUID
Provides an Universally Unique IDentifier module for managing primary keys.
Create a new slice of UUID v7.
Create an URN (Uniform Resource Name) string from a given UUID slice.
const id = Uuid.new();
const id_urn = try Uuid.toUrn(&id);
std.debug.print("URN: {s}\n", .{id_urn});
Generate an UUID slice from a given URN string.
const urn_string = "01956633-4BAB-7BD9-9209-7365F2DB6F2E";
const id = try Uuid.fromUrn(urn_string);
std.debug.print("{any}\n", .{id});
Timestamp
Provides an Epoch timestamp in millisecond. Needed for record's timekeeping.