Store notes in SQLite
This commit is contained in:
parent
9faf2a3c9b
commit
b82600e8cd
5 changed files with 258 additions and 185 deletions
|
@ -1,11 +1,11 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const sql = @import("sql");
|
||||
|
||||
const memdb = @import("./memdb.zig");
|
||||
const db = @import("./db.zig");
|
||||
|
||||
pub const models = @import("./models.zig");
|
||||
pub const free = db.free;
|
||||
pub const free = memdb.free;
|
||||
pub const Uuid = util.Uuid;
|
||||
|
||||
pub fn CreateInfo(comptime T: type) type {
|
||||
|
@ -38,18 +38,18 @@ fn reify(comptime T: type, id: Uuid, val: CreateInfo(T)) T {
|
|||
}
|
||||
|
||||
pub const ApiServer = struct {
|
||||
db: db.Database,
|
||||
memdb: memdb.Database,
|
||||
prng: std.rand.DefaultPrng,
|
||||
last_id: u64 = 0,
|
||||
|
||||
db2: sql.Sqlite,
|
||||
db: db.Database,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !ApiServer {
|
||||
return ApiServer{
|
||||
.db = try db.Database.init(alloc),
|
||||
.memdb = try memdb.Database.init(alloc),
|
||||
.prng = std.rand.DefaultPrng.init(1998),
|
||||
|
||||
.db2 = try sql.Sqlite.open("./test.db"),
|
||||
.db = try db.Database.init(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -63,20 +63,21 @@ pub const ApiServer = struct {
|
|||
// TODO: check for dupes
|
||||
|
||||
const note = reify(models.Note, id, info);
|
||||
try self.db.notes.put(note);
|
||||
try self.db.insertNote(note);
|
||||
//try self.memdb.notes.put(note);
|
||||
|
||||
return note;
|
||||
}
|
||||
|
||||
pub fn createUser(self: *ApiServer, info: CreateInfo(models.User)) !models.User {
|
||||
try self.db.users.lock();
|
||||
defer self.db.users.unlock();
|
||||
try self.memdb.users.lock();
|
||||
defer self.memdb.users.unlock();
|
||||
// TODO; real queries
|
||||
|
||||
const id = Uuid.randV4(self.prng.random());
|
||||
|
||||
// check for handle dupes
|
||||
var iter = self.db.users.data.iterator();
|
||||
var iter = self.memdb.users.data.iterator();
|
||||
while (iter.next()) |it| {
|
||||
if (std.mem.eql(u8, it.value_ptr.handle, info.handle)) {
|
||||
return error.DuplicateHandle;
|
||||
|
@ -85,12 +86,13 @@ pub const ApiServer = struct {
|
|||
// TODO: check for id dupes
|
||||
|
||||
const user = reify(models.User, id, info);
|
||||
try self.db.users.put(user);
|
||||
try self.memdb.users.put(user);
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
pub fn getNote(self: *ApiServer, id: Uuid, alloc: std.mem.Allocator) !?models.Note {
|
||||
return self.db.notes.get(id, alloc);
|
||||
return self.db.getNoteById(id, alloc);
|
||||
//return self.memdb.notes.get(id, alloc);
|
||||
}
|
||||
};
|
||||
|
|
203
src/main/db.zig
203
src/main/db.zig
|
@ -1,170 +1,57 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const Uuid = util.Uuid;
|
||||
|
||||
const sql = @import("sql");
|
||||
const models = @import("./models.zig");
|
||||
|
||||
// Clones a struct and its fields to a single layer of depth.
|
||||
// Caller owns memory, can be freed using free below
|
||||
// TODO: check that this is a struct, etc etc
|
||||
fn clone(alloc: std.mem.Allocator, val: anytype) !@TypeOf(val) {
|
||||
var result: @TypeOf(val) = undefined;
|
||||
|
||||
errdefer {
|
||||
@panic("memory leak in deep clone, fix this");
|
||||
}
|
||||
|
||||
inline for (std.meta.fields(@TypeOf(val))) |f| {
|
||||
// TODO
|
||||
if (f.field_type == []u8 or f.field_type == []const u8) {
|
||||
@field(result, f.name) = try cloneString(alloc, @field(val, f.name));
|
||||
} else if (f.field_type == Uuid) {
|
||||
@field(result, f.name) = @field(val, f.name);
|
||||
} else {
|
||||
@compileError("unsupported field type " ++ @typeName(f.field_type));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn cloneString(alloc: std.mem.Allocator, str: []const u8) ![]const u8 {
|
||||
var result = try alloc.alloc(u8, str.len);
|
||||
std.mem.copy(u8, result, str);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Frees a struct and its fields returned by clone
|
||||
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
||||
inline for (std.meta.fields(@TypeOf(val))) |f| {
|
||||
// TODO
|
||||
if (f.field_type == []u8 or f.field_type == []const u8) {
|
||||
alloc.free(@field(val, f.name));
|
||||
} else if (f.field_type == Uuid) {
|
||||
// nothing
|
||||
} else {
|
||||
@compileError("unsupported field type " ++ @typeName(f.field_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Table(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
internal_alloc: std.mem.Allocator,
|
||||
data: std.AutoHashMap(Uuid, T),
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Self {
|
||||
return Self{
|
||||
.internal_alloc = alloc,
|
||||
.data = std.AutoHashMap(Uuid, T).init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
var iter = self.data.iterator();
|
||||
while (iter.next()) |it| {
|
||||
free(self.internal_alloc, it.value_ptr.*);
|
||||
}
|
||||
|
||||
self.data.deinit();
|
||||
}
|
||||
|
||||
pub fn contains(self: *Self, id: Uuid) !bool {
|
||||
return self.data.contains(id);
|
||||
}
|
||||
|
||||
// returns a copy of the note data from storage. memory is allocated with the provided
|
||||
// allocator. can be freed using free() above
|
||||
pub fn get(self: *Self, id: Uuid, alloc: std.mem.Allocator) !?T {
|
||||
const data = self.data.get(id) orelse return null;
|
||||
return try clone(alloc, data);
|
||||
}
|
||||
|
||||
pub fn put(self: *Self, data: T) !void {
|
||||
const copy = try clone(self.internal_alloc, data);
|
||||
errdefer free(self.internal_alloc, copy);
|
||||
|
||||
const key = copy.id;
|
||||
|
||||
if (self.data.fetchRemove(key)) |e| {
|
||||
free(self.internal_alloc, e.value);
|
||||
}
|
||||
try self.data.put(key, copy);
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn lock(_: *Self) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn unlock(_: *Self) void {
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
const Uuid = @import("util").Uuid;
|
||||
|
||||
pub const Database = struct {
|
||||
internal_alloc: std.mem.Allocator,
|
||||
notes: Table(models.Note),
|
||||
users: Table(models.User),
|
||||
db: sql.Sqlite,
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Database {
|
||||
var db = Database{
|
||||
.internal_alloc = alloc,
|
||||
.notes = try Table(models.Note).init(alloc),
|
||||
.users = try Table(models.User).init(alloc),
|
||||
};
|
||||
pub fn init() !Database {
|
||||
var db = try sql.Sqlite.open("./test.db");
|
||||
errdefer db.close();
|
||||
|
||||
return db;
|
||||
var stmt = try db.prepare(
|
||||
\\CREATE TABLE IF NOT EXISTS
|
||||
\\note(
|
||||
\\ id TEXT PRIMARY KEY,
|
||||
\\ content TEXT NOT NULL
|
||||
\\) STRICT;
|
||||
\\
|
||||
);
|
||||
defer stmt.finalize();
|
||||
|
||||
while (try stmt.step()) |_| {}
|
||||
|
||||
return Database{ .db = db };
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Database) void {
|
||||
self.notes.deinit();
|
||||
self.users.deinit();
|
||||
self.db.close();
|
||||
}
|
||||
|
||||
pub fn getNoteById(self: *Database, id: Uuid, alloc: std.mem.Allocator) !?models.Note {
|
||||
var stmt = try self.db.prepare("SELECT content FROM note WHERE id = ? LIMIT 1;");
|
||||
defer stmt.finalize();
|
||||
|
||||
const id_str = id.toCharArray();
|
||||
try stmt.bindText(1, &id_str);
|
||||
|
||||
const row = (try stmt.step()) orelse return null;
|
||||
return models.Note{
|
||||
.id = id,
|
||||
.content = try row.getTextAlloc(0, alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn insertNote(self: *Database, note: models.Note) !void {
|
||||
var stmt = try self.db.prepare("INSERT INTO note (id, content) VALUES(?, ?);");
|
||||
defer stmt.finalize();
|
||||
|
||||
const id_str = note.id.toCharArray();
|
||||
try stmt.bindText(1, &id_str);
|
||||
try stmt.bindText(2, note.content);
|
||||
|
||||
if ((try stmt.step()) != null) return error.UnexpectedError;
|
||||
}
|
||||
};
|
||||
|
||||
test "clone" {
|
||||
const T = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
const copy = try clone(std.testing.allocator, T{ .name = "myName", .value = "myValue" });
|
||||
free(std.testing.allocator, copy);
|
||||
}
|
||||
|
||||
test "db" {
|
||||
var db = try Database.init(std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
|
||||
const note = (try db.getNote("100", std.testing.allocator)).?;
|
||||
free(std.testing.allocator, note);
|
||||
}
|
||||
|
||||
test "db" {
|
||||
var db = try Database.init(std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
}
|
||||
|
|
170
src/main/memdb.zig
Normal file
170
src/main/memdb.zig
Normal file
|
@ -0,0 +1,170 @@
|
|||
const std = @import("std");
|
||||
const util = @import("util");
|
||||
const Uuid = util.Uuid;
|
||||
|
||||
const models = @import("./models.zig");
|
||||
|
||||
// Clones a struct and its fields to a single layer of depth.
|
||||
// Caller owns memory, can be freed using free below
|
||||
// TODO: check that this is a struct, etc etc
|
||||
fn clone(alloc: std.mem.Allocator, val: anytype) !@TypeOf(val) {
|
||||
var result: @TypeOf(val) = undefined;
|
||||
|
||||
errdefer {
|
||||
@panic("memory leak in deep clone, fix this");
|
||||
}
|
||||
|
||||
inline for (std.meta.fields(@TypeOf(val))) |f| {
|
||||
// TODO
|
||||
if (f.field_type == []u8 or f.field_type == []const u8) {
|
||||
@field(result, f.name) = try cloneString(alloc, @field(val, f.name));
|
||||
} else if (f.field_type == Uuid) {
|
||||
@field(result, f.name) = @field(val, f.name);
|
||||
} else {
|
||||
@compileError("unsupported field type " ++ @typeName(f.field_type));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
fn cloneString(alloc: std.mem.Allocator, str: []const u8) ![]const u8 {
|
||||
var result = try alloc.alloc(u8, str.len);
|
||||
std.mem.copy(u8, result, str);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Frees a struct and its fields returned by clone
|
||||
pub fn free(alloc: std.mem.Allocator, val: anytype) void {
|
||||
inline for (std.meta.fields(@TypeOf(val))) |f| {
|
||||
// TODO
|
||||
if (f.field_type == []u8 or f.field_type == []const u8) {
|
||||
alloc.free(@field(val, f.name));
|
||||
} else if (f.field_type == Uuid) {
|
||||
// nothing
|
||||
} else {
|
||||
@compileError("unsupported field type " ++ @typeName(f.field_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn Table(comptime T: type) type {
|
||||
return struct {
|
||||
const Self = @This();
|
||||
|
||||
internal_alloc: std.mem.Allocator,
|
||||
data: std.AutoHashMap(Uuid, T),
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Self {
|
||||
return Self{
|
||||
.internal_alloc = alloc,
|
||||
.data = std.AutoHashMap(Uuid, T).init(alloc),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Self) void {
|
||||
var iter = self.data.iterator();
|
||||
while (iter.next()) |it| {
|
||||
free(self.internal_alloc, it.value_ptr.*);
|
||||
}
|
||||
|
||||
self.data.deinit();
|
||||
}
|
||||
|
||||
pub fn contains(self: *Self, id: Uuid) !bool {
|
||||
return self.data.contains(id);
|
||||
}
|
||||
|
||||
// returns a copy of the note data from storage. memory is allocated with the provided
|
||||
// allocator. can be freed using free() above
|
||||
pub fn get(self: *Self, id: Uuid, alloc: std.mem.Allocator) !?T {
|
||||
const data = self.data.get(id) orelse return null;
|
||||
return try clone(alloc, data);
|
||||
}
|
||||
|
||||
pub fn put(self: *Self, data: T) !void {
|
||||
const copy = try clone(self.internal_alloc, data);
|
||||
errdefer free(self.internal_alloc, copy);
|
||||
|
||||
const key = copy.id;
|
||||
|
||||
if (self.data.fetchRemove(key)) |e| {
|
||||
free(self.internal_alloc, e.value);
|
||||
}
|
||||
try self.data.put(key, copy);
|
||||
}
|
||||
|
||||
// TODO
|
||||
pub fn lock(_: *Self) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn unlock(_: *Self) void {
|
||||
return;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub const Database = struct {
|
||||
internal_alloc: std.mem.Allocator,
|
||||
notes: Table(models.Note),
|
||||
users: Table(models.User),
|
||||
|
||||
pub fn init(alloc: std.mem.Allocator) !Database {
|
||||
var db = Database{
|
||||
.internal_alloc = alloc,
|
||||
.notes = try Table(models.Note).init(alloc),
|
||||
.users = try Table(models.User).init(alloc),
|
||||
};
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Database) void {
|
||||
self.notes.deinit();
|
||||
self.users.deinit();
|
||||
}
|
||||
};
|
||||
|
||||
test "clone" {
|
||||
const T = struct {
|
||||
name: []const u8,
|
||||
value: []const u8,
|
||||
};
|
||||
|
||||
const copy = try clone(std.testing.allocator, T{ .name = "myName", .value = "myValue" });
|
||||
free(std.testing.allocator, copy);
|
||||
}
|
||||
|
||||
test "db" {
|
||||
var db = try Database.init(std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
|
||||
const note = (try db.getNote("100", std.testing.allocator)).?;
|
||||
free(std.testing.allocator, note);
|
||||
}
|
||||
|
||||
test "db" {
|
||||
var db = try Database.init(std.testing.allocator);
|
||||
defer db.deinit();
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
|
||||
try db.putNote(.{
|
||||
.id = "100",
|
||||
.content = "content",
|
||||
});
|
||||
}
|
|
@ -17,38 +17,39 @@ pub const Sqlite = struct {
|
|||
}
|
||||
|
||||
pub fn close(self: *Sqlite) void {
|
||||
return c.sqlite3_close_v2(self.db);
|
||||
_ = c.sqlite3_close_v2(self.db);
|
||||
}
|
||||
|
||||
pub fn prepare(self: *Sqlite, sql: []const u8) !PreparedStmt {
|
||||
var stmt: [*c]c.sqlite3_stmt = undefined;
|
||||
const err = c.sqlite3_prepare_v2(self.db, sql.ptr, sql.len, &stmt, null);
|
||||
var stmt: ?*c.sqlite3_stmt = undefined;
|
||||
const err = c.sqlite3_prepare_v2(self.db, sql.ptr, @intCast(c_int, sql.len), &stmt, null);
|
||||
if (err != c.SQLITE_OK) return error.UnknownError;
|
||||
|
||||
return PreparedStmt{ .stmt = stmt };
|
||||
return PreparedStmt{ .stmt = stmt.?, .db = self.db };
|
||||
}
|
||||
};
|
||||
|
||||
pub const Row = struct {
|
||||
stmt: *c.sqlite3_stmt,
|
||||
db: *c.sqlite3,
|
||||
|
||||
pub fn getI64(self: *Row, idx: u15) !i64 {
|
||||
pub fn getI64(self: Row, idx: u15) !i64 {
|
||||
return @intCast(i64, c.sqlite3_column_int64(self.stmt, idx));
|
||||
}
|
||||
|
||||
pub fn getText(self: *Row, idx: u15, buf: []u8) ![]u8 {
|
||||
pub fn getText(self: Row, idx: u15, buf: []u8) ![]u8 {
|
||||
const ptr = c.sqlite3_column_text(self.stmt, idx);
|
||||
|
||||
const size = c.sqlite3_column_bytes(self.stmt, idx);
|
||||
const size = @intCast(usize, c.sqlite3_column_bytes(self.stmt, idx));
|
||||
if (size > buf.len) return error.StreamTooLong;
|
||||
for (ptr[0..size]) |ch, i| buf[i] = ch;
|
||||
|
||||
return buf[0..size];
|
||||
}
|
||||
|
||||
pub fn getTextAlloc(self: *Row, idx: u15, alloc: std.mem.Allocator) ![]u8 {
|
||||
pub fn getTextAlloc(self: Row, idx: u15, alloc: std.mem.Allocator) ![]u8 {
|
||||
const size = c.sqlite3_column_bytes(self.stmt, idx);
|
||||
var buf = try alloc.alloc(u8, size);
|
||||
var buf = try alloc.alloc(u8, @intCast(usize, size));
|
||||
errdefer alloc.free(buf);
|
||||
|
||||
return self.getText(idx, buf);
|
||||
|
@ -57,32 +58,33 @@ pub const Row = struct {
|
|||
|
||||
pub const PreparedStmt = struct {
|
||||
stmt: *c.sqlite3_stmt,
|
||||
db: *c.sqlite3,
|
||||
|
||||
pub fn bindNull(self: *PreparedStmt, idx: u15) !void {
|
||||
return switch (c.sqlite3_bind_null(self.stmt, idx)) {
|
||||
.SQLITE_OK => void,
|
||||
.SQLITE_OK => {},
|
||||
else => error.UnknownError,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn bindText(self: *PreparedStmt, idx: u15, str: []const u8) !void {
|
||||
return switch (c.sqlite3_bind_text(self.stmt, idx, str.ptr, str.len, c.SQLITE_TRANSIENT)) {
|
||||
.SQLITE_OK => void,
|
||||
return switch (c.sqlite3_bind_text(self.stmt, idx, str.ptr, @intCast(c_int, str.len), c.SQLITE_TRANSIENT)) {
|
||||
c.SQLITE_OK => {},
|
||||
else => error.UnknownError,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn bindI64(self: *PreparedStmt, idx: u15, val: i64) !void {
|
||||
return switch (c.sqlite3_bind_int64(self.stmt, idx, val)) {
|
||||
.SQLITE_OK => void,
|
||||
.SQLITE_OK => {},
|
||||
else => error.UnknownError,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn step(self: *PreparedStmt) !?Row {
|
||||
return switch (c.sqlite3_step(self.stmt)) {
|
||||
.SQLITE_ROW => Row{ .stmt = self.stmt },
|
||||
.SQLITE_DONE => null,
|
||||
c.SQLITE_ROW => Row{ .stmt = self.stmt, .db = self.db },
|
||||
c.SQLITE_DONE => null,
|
||||
|
||||
else => error.UnknownError,
|
||||
};
|
||||
|
|
|
@ -11,6 +11,18 @@ pub fn eql(lhs: Uuid, rhs: Uuid) bool {
|
|||
return lhs.data == rhs.data;
|
||||
}
|
||||
|
||||
pub fn toCharArray(value: Uuid) [StringLen]u8 {
|
||||
var buf: [StringLen]u8 = undefined;
|
||||
_ = std.fmt.bufPrint(&buf, "{}", .{value}) catch unreachable;
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn toCharArrayZ(value: Uuid) [StringLen + 1:0]u8 {
|
||||
var buf: [StringLen + 1:0]u8 = undefined;
|
||||
_ = std.fmt.bufPrintZ(&buf, "{}", .{value}) catch unreachable;
|
||||
return buf;
|
||||
}
|
||||
|
||||
pub fn format(value: Uuid, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||
const arr = @bitCast([16]u8, value.data);
|
||||
try std.fmt.format(writer, "{x:0>2}{x:0>2}{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}-{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}{x:0>2}", .{
|
||||
|
|
Loading…
Reference in a new issue