# SQLite3 wrapper # Copyright (C) 2023 Nguyễn Gia Phong # # This file if part of hybring. # # Hybring is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Hybring is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with hybring. If not, see . @[Link("sqlite3")] lib SQLite OK = 0 BUSY = 5 ROW = 100 DONE = 101 enum UpdateAction : LibC::Int Delete = 9 Insert = 18 Update = 23 end type Database = Void* type Statement = Void* fun errstr = sqlite3_errstr(rc : LibC::Int) : LibC::Char* fun mprintf = sqlite3_mprintf(format : LibC::Char*, ...) : LibC::Char* fun free = sqlite3_free(format : Void*, ...) fun open = sqlite3_open(filename : LibC::Char*, db : Database*) : LibC::Int fun close = sqlite3_close_v2(db : Database) : LibC::Int fun update_hook = sqlite3_update_hook(db : Database, f : (Void*, UpdateAction, LibC::Char*, LibC::Char*, Int64 ->), arg : Void*) fun prepare = sqlite3_prepare_v2(db : Database, query : LibC::Char*, length : LibC::Int, stmt : Statement*, query_tail : LibC::Char**) : LibC::Int fun finalize = sqlite3_finalize(stmt : Statement) : LibC::Int fun step = sqlite3_step(stmt : Statement) : Int32 fun column_int = sqlite3_column_int(stmt : Statement, col : LibC::Int) : LibC::Int fun column_int64 = sqlite3_column_int64(stmt : Statement, col : LibC::Int) : Int64 fun column_text = sqlite3_column_text(stmt : Statement, col : LibC::Int) : LibC::Char* end MIGRATIONS = [ ["CREATE TABLE member (nick TEXT NOT NULL UNIQUE, -- de facto primary opennic TEXT NOT NULL UNIQUE, icann TEXT NOT NULL UNIQUE, official INTEGER NOT NULL DEFAULT 0, left REFERENCES member(nick), right REFERENCES member(nick))", "CREATE TABLE change (type INTEGER, -- enum nick REFERENCES member(nick) ON DELETE CASCADE, time TEXT)"], ] class Database alias UpdateAction = SQLite::UpdateAction class Statement def initialize(db, query) Database.check SQLite.prepare db, query, LibC.strlen(query), out @ref, out _ end def step : LibC::Int SQLite.step @ref end def row Row.new @ref end def finalize Database.check SQLite.finalize @ref end end class Column def initialize(stmt : SQLite::Statement, i : LibC::Int) @stmt = stmt @i = i end def int SQLite.column_int @stmt, @i end def int64 SQLite.column_int64 @stmt, @i end def text String.new SQLite.column_text @stmt, @i end end class Row def initialize(stmt : SQLite::Statement) @stmt = stmt end def [](i : LibC::Int) Column.new @stmt, i end end def initialize(path) Dir.mkdir_p path.parent Database.check SQLite.open path.to_s, out @ref self.exec "PRAGMA foreign_keys = ON" do end end def initialize(path, opennic, icann) Dir.mkdir_p path.parent Database.check SQLite.open path.to_s, out @ref self.exec "PRAGMA foreign_keys = ON" do end self.exec "PRAGMA user_version" do |row| version = row[0].int raise "negative schema version" if version < 0 raise "schema version newer than supported" if version > MIGRATIONS.size self.transact do MIGRATIONS[version..].each do |migration| migration.each do |sql| self.exec sql do end end end self.exec "PRAGMA user_version = %u", MIGRATIONS.size do end self.exec "INSERT INTO member (nick, opennic, icann, official, left, right) VALUES (%Q, %Q, %Q, 1, %Q, %Q)", "self", opennic, icann, "self", "self" do end if version == 0 # avoid out-of-bound when looking for neighbors end rescue ex self.finalize raise ex end end def update_hook(callback, arg) SQLite.update_hook @ref, callback, arg end def exec(query : String, *values) sql = SQLite.mprintf query, *values stmt = Statement.new @ref, sql SQLite.free sql loop do rc = stmt.step case rc when SQLite::ROW yield stmt.row when SQLite::DONE break when SQLite::BUSY next # FIXME: rollback transaction else Database.check rc end end end def transact self.exec "BEGIN" do end yield self.exec "COMMIT" do end end def add_applicant(nick, opennic, icann) self.exec "INSERT INTO member (nick, opennic, icann, left, right) SELECT %Q, %Q, %Q, nick, left FROM member WHERE official = 1 ORDER BY rowid ASC LIMIT 1", nick, opennic, icann do end end def finalize Database.check SQLite.close @ref end end def Database.check(rc : LibC::Int) raise String.new SQLite.errstr rc if rc != SQLite::OK end