Store notes in SQLite

This commit is contained in:
jaina heartles 2022-07-15 00:27:27 -07:00
parent 9faf2a3c9b
commit b82600e8cd
5 changed files with 258 additions and 185 deletions

View file

@ -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);
}
};

View file

@ -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
View 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",
});
}

View file

@ -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,
};

View file

@ -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}", .{