331 lines
13 KiB
Vala
331 lines
13 KiB
Vala
/*
|
|
Copyright (C) 2013 Christian Dywan <christian@twotoats.de>
|
|
|
|
This library is free software; you can redistribute it and/or
|
|
modify it under the terms of the GNU Lesser General Public
|
|
License as published by the Free Software Foundation; either
|
|
version 2.1 of the License, or (at your option) any later version.
|
|
|
|
See the file COPYING for the full license text.
|
|
*/
|
|
|
|
namespace Midori {
|
|
/*
|
|
* Since: 0.5.6
|
|
*/
|
|
public errordomain DatabaseError {
|
|
OPEN,
|
|
NAMING,
|
|
FILENAME,
|
|
EXECUTE,
|
|
COMPILE,
|
|
TYPE,
|
|
}
|
|
|
|
/*
|
|
* Since: 0.5.8
|
|
*/
|
|
public delegate bool DatabaseCallback () throws DatabaseError;
|
|
|
|
/*
|
|
* Since: 0.5.7
|
|
*/
|
|
public class DatabaseStatement : GLib.Object, GLib.Initable {
|
|
public Sqlite.Statement? stmt { get { return _stmt; } }
|
|
protected Sqlite.Statement _stmt = null;
|
|
public Database? database { get; set construct; }
|
|
public string? query { get; set construct; }
|
|
private int64 last_row_id = -1;
|
|
|
|
public DatabaseStatement (Database database, string query) throws DatabaseError {
|
|
Object (database: database, query: query);
|
|
init ();
|
|
}
|
|
|
|
public virtual bool init (GLib.Cancellable? cancellable = null) throws DatabaseError {
|
|
int result = database.db.prepare_v2 (query, -1, out _stmt, null);
|
|
if (result != Sqlite.OK)
|
|
throw new DatabaseError.COMPILE ("Failed to compile statement: %s".printf (database.db.errmsg ()));
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Bind values to named parameters.
|
|
* SQL: "SELECT foo FROM bar WHERE id = :session_id"
|
|
* Vala: statement.bind(":session_id", typeof (int64), 12345);
|
|
* Supported types: string, int64, double
|
|
*/
|
|
public void bind (string pname, ...) throws DatabaseError {
|
|
int pindex = stmt.bind_parameter_index (pname);
|
|
if (pindex <= 0)
|
|
throw new DatabaseError.TYPE ("No such parameter '%s' in statement: %s".printf (pname, query));
|
|
var args = va_list ();
|
|
Type ptype = args.arg ();
|
|
if (ptype == typeof (string)) {
|
|
string text = args.arg ();
|
|
stmt.bind_text (pindex, text);
|
|
if (database.trace)
|
|
stdout.printf ("%s=%s ", pname, text);
|
|
} else if (ptype == typeof (int64)) {
|
|
int64 integer = args.arg ();
|
|
stmt.bind_int64 (pindex, integer);
|
|
if (database.trace)
|
|
stdout.printf ("%s=%s ", pname, integer.to_string ());
|
|
} else if (ptype == typeof (double)) {
|
|
double stuntman = args.arg ();
|
|
stmt.bind_double (pindex, stuntman);
|
|
if (database.trace)
|
|
stdout.printf ("%s=%s ", pname, stuntman.to_string ());
|
|
} else
|
|
throw new DatabaseError.TYPE ("Invalid type '%s' for '%s' in statement: %s".printf (ptype.name (), pname, query));
|
|
}
|
|
|
|
/*
|
|
* Execute the statement, it's an error if there are more rows.
|
|
*/
|
|
public bool exec () throws DatabaseError {
|
|
if (step ())
|
|
throw new DatabaseError.EXECUTE ("More rows available - use step instead of exec");
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Proceed to the next row, returns false when the end is nigh.
|
|
*/
|
|
public bool step () throws DatabaseError {
|
|
int result = stmt.step ();
|
|
if (result != Sqlite.DONE && result != Sqlite.ROW)
|
|
throw new DatabaseError.EXECUTE (database.db.errmsg ());
|
|
last_row_id = database.db.last_insert_rowid ();
|
|
return result == Sqlite.ROW;
|
|
}
|
|
|
|
/*
|
|
* Returns the id of the last inserted row.
|
|
* It is an error to ask for an id without having inserted a row.
|
|
* Since: 0.5.8
|
|
*/
|
|
public int64 row_id () throws DatabaseError {
|
|
if (last_row_id == -1)
|
|
throw new DatabaseError.EXECUTE ("No row id");
|
|
return last_row_id;
|
|
}
|
|
|
|
private int column_index (string name) throws DatabaseError {
|
|
for (int i = 0; i < stmt.column_count (); i++) {
|
|
if (name == stmt.column_name (i))
|
|
return i;
|
|
}
|
|
throw new DatabaseError.TYPE ("No such column '%s' in row: %s".printf (name, query));
|
|
}
|
|
|
|
/*
|
|
* Get a string value by its named parameter, for example ":uri".
|
|
* Returns null if not found.
|
|
*/
|
|
public string? get_string (string name) throws DatabaseError {
|
|
int index = column_index (name);
|
|
int type = stmt.column_type (index);
|
|
if (stmt.column_type (index) != Sqlite.TEXT && type != Sqlite.NULL)
|
|
throw new DatabaseError.TYPE ("Getting '%s' with wrong type in row: %s".printf (name, query));
|
|
return stmt.column_text (index);
|
|
}
|
|
|
|
/*
|
|
* Get an integer value by its named parameter, for example ":day".
|
|
* Returns 0 if not found.
|
|
*/
|
|
public int64 get_int64 (string name) throws DatabaseError {
|
|
int index = column_index (name);
|
|
int type = stmt.column_type (index);
|
|
if (type != Sqlite.INTEGER && type != Sqlite.NULL)
|
|
throw new DatabaseError.TYPE ("Getting '%s' with value '%s' of wrong type %d in row: %s".printf (
|
|
name, stmt.column_text (index), type, query));
|
|
return stmt.column_int64 (index);
|
|
}
|
|
|
|
/*
|
|
* Get a double value by its named parameter, for example ":session_id".
|
|
* Returns double.NAN if not found.
|
|
*/
|
|
public double get_double (string name) throws DatabaseError {
|
|
int index = column_index (name);
|
|
int type = stmt.column_type (index);
|
|
if (type != Sqlite.FLOAT && type != Sqlite.NULL)
|
|
throw new DatabaseError.TYPE ("Getting '%s' with wrong type in row: %s".printf (name, query));
|
|
return type == Sqlite.NULL ? double.NAN : stmt.column_double (index);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Since: 0.5.6
|
|
*/
|
|
public class Database : GLib.Object, GLib.Initable {
|
|
internal bool trace = false;
|
|
public Sqlite.Database? db { get { return _db; } }
|
|
protected Sqlite.Database? _db = null;
|
|
public string path { get; protected set; default = ":memory:"; }
|
|
|
|
/*
|
|
* A new database successfully opened for the first time.
|
|
* Old or additional data should be opened if this is true.
|
|
*/
|
|
public bool first_use { get; protected set; default = false; }
|
|
|
|
/*
|
|
* If a filename is passed it's assumed to be in the config folder.
|
|
* Otherwise the database is in memory only (useful for private browsing).
|
|
*/
|
|
public Database (string path=":memory:") throws DatabaseError {
|
|
Object (path: path);
|
|
init ();
|
|
}
|
|
|
|
string resolve_path (string path) {
|
|
if (path.has_prefix (":memory:"))
|
|
return ":memory:";
|
|
else if (!Path.is_absolute (path))
|
|
return Midori.Paths.get_config_filename_for_writing (path);
|
|
return path;
|
|
}
|
|
|
|
public virtual bool init (GLib.Cancellable? cancellable = null) throws DatabaseError {
|
|
string real_path = resolve_path (path);
|
|
bool exists = exists(real_path);
|
|
|
|
if (Sqlite.Database.open_v2 (real_path, out _db) != Sqlite.OK)
|
|
throw new DatabaseError.OPEN ("Failed to open database %s".printf (real_path));
|
|
|
|
string token = Environment.get_variable ("MIDORI_DEBUG") ?? "";
|
|
string basename = Path.get_basename (path);
|
|
string[] parts = basename.split (".");
|
|
trace = ("db:" + parts[0]) in token;
|
|
if (trace) {
|
|
stdout.printf ("§§ Tracing %s\n", path);
|
|
db.profile ((sql, nanoseconds) => {
|
|
/* sqlite as of this writing isn't more precise than ms */
|
|
string milliseconds = (nanoseconds / 1000000).to_string ();
|
|
stdout.printf ("§§ %s: %s (%sms)\n", path, sql, milliseconds);
|
|
});
|
|
}
|
|
|
|
if (db.exec ("PRAGMA journal_mode = WAL; PRAGMA cache_size = 32100;") != Sqlite.OK)
|
|
db.exec ("PRAGMA synchronous = NORMAL; PRAGMA temp_store = MEMORY;");
|
|
db.exec ("PRAGMA count_changes = OFF;");
|
|
|
|
if (real_path == ":memory:")
|
|
return true;
|
|
|
|
int64 user_version;
|
|
Sqlite.Statement stmt;
|
|
if (db.prepare_v2 ("PRAGMA user_version;", -1, out stmt, null) != Sqlite.OK)
|
|
throw new DatabaseError.EXECUTE ("Failed to compile statement %s".printf (db.errmsg ()));
|
|
if (stmt.step () != Sqlite.ROW)
|
|
throw new DatabaseError.EXECUTE ("Failed to get row %s".printf (db.errmsg ()));
|
|
user_version = stmt.column_int64 (0);
|
|
|
|
if (user_version == 0) {
|
|
exec_script ("Create");
|
|
user_version = 1;
|
|
exec ("PRAGMA user_version = " + user_version.to_string ());
|
|
}
|
|
|
|
while (true) {
|
|
try {
|
|
exec_script ("Update" + user_version.to_string ());
|
|
} catch (DatabaseError error) {
|
|
if (error is DatabaseError.FILENAME)
|
|
break;
|
|
throw error;
|
|
}
|
|
user_version = user_version + 1;
|
|
exec ("PRAGMA user_version = " + user_version.to_string ());
|
|
}
|
|
|
|
first_use = !exists;
|
|
return true;
|
|
}
|
|
|
|
public bool exists (string path) {
|
|
bool exists;
|
|
#if !HAVE_WIN32
|
|
exists = Posix.access (path, Posix.F_OK) == 0;
|
|
#else
|
|
var folder = File.new_for_path (path);
|
|
exists = folder.query_exists();
|
|
#endif
|
|
return exists;
|
|
}
|
|
|
|
|
|
/*
|
|
* Since: 0.5.8
|
|
*/
|
|
|
|
public bool attach (string path, string alias) throws DatabaseError {
|
|
string real_path = resolve_path (path);
|
|
if (!exists (real_path))
|
|
throw new DatabaseError.OPEN ("Failed to attach database %s".printf (path));
|
|
return exec ("ATTACH DATABASE '%s' AS '%s';".printf (real_path, alias));
|
|
}
|
|
|
|
public bool exec_script (string filename) throws DatabaseError {
|
|
string basename = Path.get_basename (path);
|
|
string[] parts = basename.split (".");
|
|
if (!(parts != null && parts[0] != null && parts[1] != null))
|
|
throw new DatabaseError.NAMING ("Failed to deduce schema filename from %s".printf (path));
|
|
string schema_filename = Midori.Paths.get_res_filename (parts[0] + "/" + filename + ".sql");
|
|
string schema;
|
|
try {
|
|
FileUtils.get_contents (schema_filename, out schema, null);
|
|
} catch (Error error) {
|
|
throw new DatabaseError.FILENAME ("Failed to open schema: %s".printf (schema_filename));
|
|
}
|
|
transaction (()=> { return exec (schema); });
|
|
return true;
|
|
}
|
|
|
|
public bool transaction (DatabaseCallback callback) throws DatabaseError {
|
|
exec ("BEGIN TRANSACTION;");
|
|
callback ();
|
|
exec ("COMMIT;");
|
|
return true;
|
|
}
|
|
|
|
public bool exec (string query) throws DatabaseError {
|
|
if (db.exec (query) != Sqlite.OK)
|
|
throw new DatabaseError.EXECUTE (db.errmsg ());
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Prepare a statement with optionally binding parameters by name.
|
|
* See also DatabaseStatement.bind().
|
|
* Since: 0.5.7
|
|
*/
|
|
public DatabaseStatement prepare (string query, ...) throws DatabaseError {
|
|
var statement = new DatabaseStatement (this, query);
|
|
var args = va_list ();
|
|
unowned string? pname = args.arg ();
|
|
while (pname != null) {
|
|
Type ptype = args.arg ();
|
|
if (ptype == typeof (string)) {
|
|
string pvalue = args.arg ();
|
|
statement.bind (pname, ptype, pvalue);
|
|
} else if (ptype == typeof (int64)) {
|
|
int64 pvalue = args.arg ();
|
|
statement.bind (pname, ptype, pvalue);
|
|
} else if (ptype == typeof (double)) {
|
|
double pvalue = args.arg ();
|
|
statement.bind (pname, ptype, pvalue);
|
|
} else
|
|
throw new DatabaseError.TYPE ("Invalid type '%s' in statement: %s".printf (ptype.name (), query));
|
|
pname = args.arg ();
|
|
}
|
|
if (trace)
|
|
stdout.printf ("\n");
|
|
return statement;
|
|
}
|
|
}
|
|
}
|