aboutsummaryrefslogtreecommitdiff
path: root/qlite/src
diff options
context:
space:
mode:
Diffstat (limited to 'qlite/src')
-rw-r--r--qlite/src/column.vala188
-rw-r--r--qlite/src/database.vala152
-rw-r--r--qlite/src/delete_builder.vala75
-rw-r--r--qlite/src/insert_builder.vala102
-rw-r--r--qlite/src/query_builder.vala196
-rw-r--r--qlite/src/row.vala79
-rw-r--r--qlite/src/statement_builder.vala53
-rw-r--r--qlite/src/table.vala84
-rw-r--r--qlite/src/update_builder.vala133
9 files changed, 1062 insertions, 0 deletions
diff --git a/qlite/src/column.vala b/qlite/src/column.vala
new file mode 100644
index 00000000..f7b3114f
--- /dev/null
+++ b/qlite/src/column.vala
@@ -0,0 +1,188 @@
+using Sqlite;
+
+namespace Qlite {
+
+public abstract class Column<T> {
+ public string name { get; private set; }
+ public string default { get; set; }
+ public int sqlite_type { get; private set; }
+ public bool primary_key { get; set; }
+ public bool auto_increment { get; set; }
+ public bool unique { get; set; }
+ public bool not_null { get; set; }
+ public long min_version { get; set; default = -1; }
+ public long max_version { get; set; default = long.MAX; }
+
+ public abstract T get(Row row);
+
+ public virtual bool is_null(Row row) {
+ return false;
+ }
+
+ public virtual void bind(Statement stmt, int index, T value) {
+ throw new DatabaseError.NOT_SUPPORTED(@"bind() was not implemented for field $name");
+ }
+
+ public string to_string() {
+ string res = name;
+ switch (sqlite_type) {
+ case INTEGER:
+ res += " INTEGER";
+ break;
+ case FLOAT:
+ res += " REAL";
+ break;
+ case TEXT:
+ res += " TEXT";
+ break;
+ default:
+ res += " UNKNOWN";
+ break;
+ }
+ if (primary_key) {
+ res += " PRIMARY KEY";
+ if (auto_increment) res += " AUTOINCREMENT";
+ }
+ if (not_null) res += " NOT NULL";
+ if (unique) res += " UNIQUE";
+ if (default != null) res += @" DEFAULT $default";
+
+ return res;
+ }
+
+ public Column(string name, int type) {
+ this.name = name;
+ this.sqlite_type = type;
+ }
+
+ public class Integer : Column<int> {
+ public Integer(string name) {
+ base(name, INTEGER);
+ }
+
+ public override int get(Row row) {
+ return (int) row.get_integer(name);
+ }
+
+ public override bool is_null(Row row) {
+ return !row.has_integer(name);
+ }
+
+ public override void bind(Statement stmt, int index, int value) {
+ stmt.bind_int(index, value);
+ }
+ }
+
+ public class Long : Column<long> {
+ public Long(string name) {
+ base(name, INTEGER);
+ }
+
+ public override long get(Row row) {
+ return (long) row.get_integer(name);
+ }
+
+ public override bool is_null(Row row) {
+ return !row.has_integer(name);
+ }
+
+ public override void bind(Statement stmt, int index, long value) {
+ stmt.bind_int64(index, value);
+ }
+ }
+
+ public class Real : Column<double> {
+ public Real(string name) {
+ base(name, FLOAT);
+ }
+
+ public override double get(Row row) {
+ return row.get_real(name);
+ }
+
+ public override bool is_null(Row row) {
+ return !row.has_real(name);
+ }
+
+ public override void bind(Statement stmt, int index, double value) {
+ stmt.bind_double(index, value);
+ }
+ }
+
+ public class Text : Column<string?> {
+ public Text(string name) {
+ base(name, TEXT);
+ }
+
+ public override string? get(Row row) {
+ return row.get_text(name);
+ }
+
+ public override bool is_null(Row row) {
+ return get(row) == null;
+ }
+
+ public override void bind(Statement stmt, int index, string? value) {
+ if (value != null) {
+ stmt.bind_text(index, value);
+ } else {
+ stmt.bind_null(index);
+ }
+ }
+ }
+
+ public class BoolText : Column<bool> {
+ public BoolText(string name) {
+ base(name, TEXT);
+ }
+
+ public override bool get(Row row) {
+ return row.get_text(name) == "1";
+ }
+
+ public override void bind(Statement stmt, int index, bool value) {
+ stmt.bind_text(index, value ? "1" : "0");
+ }
+ }
+
+ public class BoolInt : Column<bool> {
+ public BoolInt(string name) {
+ base(name, INTEGER);
+ }
+
+ public override bool get(Row row) {
+ return row.get_integer(name) == 1;
+ }
+
+ public override void bind(Statement stmt, int index, bool value) {
+ stmt.bind_int(index, value ? 1 : 0);
+ }
+ }
+
+ public class RowReference : Column<Row?> {
+ private Table table;
+ private Column<int> id_column;
+
+ public RowReference(string name, Table table, Column<int> id_column) throws DatabaseError {
+ base(name, INTEGER);
+ if (!table.is_known_column(id_column.name)) throw new DatabaseError.ILLEGAL_REFERENCE(@"$(id_column.name) is not a column in $(table.name)");
+ if (!id_column.primary_key && !id_column.unique) throw new DatabaseError.NON_UNIQUE(@"$(id_column.name) is not suited to identify a row, but used with RowReference");
+ this.table = table;
+ this.id_column = id_column;
+ }
+
+ public override Row? get(Row row) {
+ return table.row_with(id_column, (int)row.get_integer(name));
+ }
+
+ public override void bind(Statement stmt, int index, Row? value) {
+ if (value != null) {
+ stmt.bind_int(index, id_column.get(value));
+ } else {
+ stmt.bind_null(index);
+ }
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/database.vala b/qlite/src/database.vala
new file mode 100644
index 00000000..285e10a8
--- /dev/null
+++ b/qlite/src/database.vala
@@ -0,0 +1,152 @@
+using Sqlite;
+
+namespace Qlite {
+
+public errordomain DatabaseError {
+ ILLEGAL_QUERY,
+ NOT_SUPPORTED,
+ OPEN_ERROR,
+ PREPARE_ERROR,
+ EXEC_ERROR,
+ NON_UNIQUE,
+ ILLEGAL_REFERENCE,
+ NOT_INITIALIZED
+}
+
+public class Database {
+ private string file_name;
+ private Sqlite.Database db;
+ private long expected_version;
+ private Table[] tables;
+
+ private Column<string> meta_name = new Column.Text("name") { primary_key = true };
+ private Column<long> meta_int_val = new Column.Long("int_val");
+ private Column<string> meta_text_val = new Column.Text("text_val");
+ private Table meta_table;
+
+ public bool debug = false;
+
+ public Database(string file_name, long expected_version) {
+ this.file_name = file_name;
+ this.expected_version = expected_version;
+ meta_table = new Table(this, "_meta");
+ meta_table.init({meta_name, meta_int_val, meta_text_val});
+ }
+
+ public void init(Table[] tables) throws DatabaseError {
+ print(@"Intializing database at $file_name\n");
+ Sqlite.config(Config.SERIALIZED);
+ int ec = Sqlite.Database.open_v2(file_name, out db, OPEN_READWRITE | OPEN_CREATE | 0x00010000);
+ if (ec != Sqlite.OK) {
+ throw new DatabaseError.OPEN_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
+ }
+ this.tables = tables;
+ start_migration();
+ }
+
+ public void ensure_init() throws DatabaseError {
+ if (tables == null) throw new DatabaseError.NOT_INITIALIZED(@"Database $file_name was not initialized, call init()");
+ }
+
+ private void start_migration() throws DatabaseError {
+ meta_table.create_table_at_version(expected_version);
+ long old_version = 0;
+ try {
+ Row? row = meta_table.row_with(meta_name, "version");
+ old_version = row == null ? -1 : (long) row[meta_int_val];
+ } catch (DatabaseError e) {
+ old_version = -1;
+ }
+ foreach (Table t in tables) {
+ t.create_table_at_version(old_version);
+ }
+ if (expected_version != old_version) {
+ foreach (Table t in tables) {
+ t.add_columns_for_version(old_version, expected_version);
+ }
+ migrate(old_version);
+ foreach (Table t in tables) {
+ t.delete_columns_for_version(old_version, expected_version);
+ }
+ if (old_version == -1) {
+ meta_table.insert().value(meta_name, "version").value(meta_int_val, expected_version).perform();
+ } else {
+ meta_table.update().with(meta_name, "=", "version").set(meta_int_val, expected_version).perform();
+ }
+ }
+ }
+
+ internal int errcode() {
+ return db.errcode();
+ }
+
+ internal string errmsg() {
+ return db.errmsg();
+ }
+
+ internal int64 last_insert_rowid() {
+ return db.last_insert_rowid();
+ }
+
+ // To be implemented by actual implementation if required
+ // new table columns are added, outdated columns are still present and will be removed afterwards
+ public virtual void migrate(long old_version) throws DatabaseError {
+ }
+
+ public QueryBuilder select(Column[]? columns = null) throws DatabaseError {
+ ensure_init();
+ return new QueryBuilder(this).select(columns);
+ }
+
+ public InsertBuilder insert() throws DatabaseError {
+ ensure_init();
+ return new InsertBuilder(this);
+ }
+
+ public UpdateBuilder update(Table table) throws DatabaseError {
+ ensure_init();
+ return new UpdateBuilder(this, table);
+ }
+
+ public UpdateBuilder update_named(string table) throws DatabaseError {
+ ensure_init();
+ return new UpdateBuilder.for_name(this, table);
+ }
+
+ public DeleteBuilder delete() throws DatabaseError {
+ ensure_init();
+ return new DeleteBuilder(this);
+ }
+
+ public Row.RowIterator query_sql(string sql, string[]? args = null) throws DatabaseError {
+ ensure_init();
+ return new Row.RowIterator(this, sql, args);
+ }
+
+ public Statement prepare(string sql) throws DatabaseError {
+ ensure_init();
+ if (debug) print(@"prepare: $sql\n");
+ Sqlite.Statement statement;
+ if (db.prepare_v2(sql, sql.length, out statement) != OK) {
+ throw new DatabaseError.PREPARE_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
+ }
+ return statement;
+ }
+
+ public void exec(string sql) throws DatabaseError {
+ ensure_init();
+ if (db.exec(sql) != OK) {
+ throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
+ }
+ }
+
+ public bool is_known_column(string table, string field) throws DatabaseError {
+ ensure_init();
+ foreach (Table t in tables) {
+ if (t.is_known_column(field)) return true;
+ }
+ return false;
+ }
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/delete_builder.vala b/qlite/src/delete_builder.vala
new file mode 100644
index 00000000..5999dc40
--- /dev/null
+++ b/qlite/src/delete_builder.vala
@@ -0,0 +1,75 @@
+using Sqlite;
+
+namespace Qlite {
+
+public class DeleteBuilder : StatementBuilder {
+
+ // DELETE FROM [...]
+ private Table table;
+ private string table_name;
+
+ // WHERE [...]
+ private string selection;
+ private StatementBuilder.Field[] selection_args;
+
+ protected DeleteBuilder(Database db) {
+ base(db);
+ }
+
+ public DeleteBuilder from(Table table) {
+ if (table != null) throw new DatabaseError.ILLEGAL_QUERY("cannot use from() multiple times.");
+ this.table = table;
+ this.table_name = table.name;
+ return this;
+ }
+
+ public DeleteBuilder from_name(string table) {
+ this.table_name = table;
+ return this;
+ }
+
+ public DeleteBuilder where(string selection, string[]? selection_args = null) {
+ if (selection != null) throw new DatabaseError.ILLEGAL_QUERY("selection was already done, but where() was called.");
+ this.selection = selection;
+ if (selection_args != null) {
+ this.selection_args = new StatementBuilder.Field[selection_args.length];
+ for (int i = 0; i < selection_args.length; i++) {
+ this.selection_args[i] = new StatementBuilder.StringField(selection_args[i]);
+ }
+ }
+ return this;
+ }
+
+ public DeleteBuilder with<T>(Column<T> column, string comp, T value) {
+ if (selection == null) {
+ selection = @"$(column.name) $comp ?";
+ selection_args = { new StatementBuilder.Field<T>(column, value) };
+ } else {
+ selection = @"($selection) AND $(column.name) $comp ?";
+ StatementBuilder.Field[] selection_args_new = new StatementBuilder.Field[selection_args.length+1];
+ for (int i = 0; i < selection_args.length; i++) {
+ selection_args_new[i] = selection_args[i];
+ }
+ selection_args_new[selection_args.length] = new Field<T>(column, value);
+ selection_args = selection_args_new;
+ }
+ return this;
+ }
+
+ public override Statement prepare() {
+ Statement stmt = db.prepare(@"DELETE FROM $table_name $(selection != null ? @"WHERE $selection": "")");
+ for (int i = 0; i < selection_args.length; i++) {
+ selection_args[i].bind(stmt, i+1);
+ }
+ return stmt;
+ }
+
+ public void perform() {
+ if (prepare().step() != DONE) {
+ throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
+ }
+ }
+
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/insert_builder.vala b/qlite/src/insert_builder.vala
new file mode 100644
index 00000000..654935a6
--- /dev/null
+++ b/qlite/src/insert_builder.vala
@@ -0,0 +1,102 @@
+using Sqlite;
+
+namespace Qlite {
+
+public class InsertBuilder : StatementBuilder {
+
+ // INSERT [OR ...]
+ private bool replace_val;
+ private string or_val;
+
+ // INTO [...]
+ private Table table;
+ private string table_name;
+
+ // VALUES [...]
+ private StatementBuilder.Field[] fields;
+
+ protected InsertBuilder(Database db) {
+ base(db);
+ }
+
+ public InsertBuilder replace() {
+ this.replace_val = true;
+ return this;
+ }
+
+ public InsertBuilder or(string or) {
+ this.or_val = or;
+ return this;
+ }
+
+ public InsertBuilder into(Table table) {
+ this.table = table;
+ this.table_name = table.name;
+ return this;
+ }
+
+ public InsertBuilder into_name(string table) {
+ this.table_name = table;
+ return this;
+ }
+
+ public InsertBuilder value<T>(Column<T> column, T value) {
+ if (fields == null) {
+ fields = { new StatementBuilder.Field<T>(column, value) };
+ } else {
+ StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1];
+ for (int i = 0; i < fields.length; i++) {
+ fields_new[i] = fields[i];
+ }
+ fields_new[fields.length] = new Field<T>(column, value);
+ fields = fields_new;
+ }
+ return this;
+ }
+
+ public InsertBuilder value_null<T>(Column<T> column) {
+ if (column.not_null) throw new DatabaseError.ILLEGAL_QUERY(@"Can't set non-null column $(column.name) to null");
+ if (fields == null) {
+ fields = { new NullField<T>(column) };
+ } else {
+ StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1];
+ for (int i = 0; i < fields.length; i++) {
+ fields_new[i] = fields[i];
+ }
+ fields_new[fields.length] = new NullField<T>(column);
+ fields = fields_new;
+ }
+ return this;
+ }
+
+ public override Statement prepare() throws DatabaseError {
+ string fields_text = "";
+ string value_qs = "";
+ for (int i = 0; i < fields.length; i++) {
+ if (i != 0) {
+ value_qs += ", ";
+ fields_text += ", ";
+ }
+ fields_text += fields[i].column.name;
+ value_qs += "?";
+ }
+ string sql = replace_val ? "REPLACE" : "INSERT";
+ if (!replace_val && or_val != null) sql += @" OR $or_val";
+ sql += @" INTO $table_name ( $fields_text ) VALUES ($value_qs)";
+ Statement stmt = db.prepare(sql);
+ for (int i = 0; i < fields.length; i++) {
+ fields[i].bind(stmt, i+1);
+ }
+ return stmt;
+ }
+
+ public int64 perform() throws DatabaseError {
+ if (prepare().step() != DONE) {
+ throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
+ }
+ return db.last_insert_rowid();
+ }
+
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/query_builder.vala b/qlite/src/query_builder.vala
new file mode 100644
index 00000000..0c9f4d98
--- /dev/null
+++ b/qlite/src/query_builder.vala
@@ -0,0 +1,196 @@
+using Sqlite;
+
+namespace Qlite {
+
+public class QueryBuilder : StatementBuilder {
+ private bool finished;
+ private bool single_result;
+
+ // SELECT [...]
+ private string column_selector = "*";
+ private Column[] columns;
+
+ // FROM [...]
+ private Table table;
+ private string table_name;
+
+ // WHERE [...]
+ private string selection;
+ private StatementBuilder.Field[] selection_args;
+
+ // ORDER BY [...]
+ private OrderingTerm[] order_by_terms;
+
+ // LIMIT [...]
+ private int limit_val;
+
+ private Row[] result;
+
+ protected QueryBuilder(Database db) {
+ base(db);
+ }
+
+ public QueryBuilder select(Column[]? columns = null) {
+ this.columns = columns;
+ if (columns != null) {
+ for (int i = 0; i < columns.length; i++) {
+ if (column_selector == "*") {
+ column_selector = columns[0].name;
+ } else {
+ column_selector += ", " + columns[i].name;
+ }
+ }
+ } else {
+ column_selector = "*";
+ }
+ return this;
+ }
+
+ public QueryBuilder select_string(string column_selector) {
+ this.columns = null;
+ this.column_selector = column_selector;
+ return this;
+ }
+
+ public QueryBuilder from(Table table) throws DatabaseError {
+ if (this.table_name != null) throw new DatabaseError.ILLEGAL_QUERY("cannot use from() multiple times.");
+ this.table = table;
+ this.table_name = table.name;
+ return this;
+ }
+
+ public QueryBuilder from_name(string table) {
+ this.table_name = table;
+ return this;
+ }
+
+ public QueryBuilder where(string selection, string[]? selection_args = null) throws DatabaseError {
+ if (this.selection != null) throw new DatabaseError.ILLEGAL_QUERY("selection was already done, but where() was called.");
+ this.selection = selection;
+ if (selection_args != null) {
+ this.selection_args = new StatementBuilder.Field[selection_args.length];
+ for (int i = 0; i < selection_args.length; i++) {
+ this.selection_args[i] = new StatementBuilder.StringField(selection_args[i]);
+ }
+ }
+ return this;
+ }
+
+ public QueryBuilder with<T>(Column<T> column, string comp, T value) {
+ if ((column.unique || column.primary_key) && comp == "=") single_result = true;
+ if (selection == null) {
+ selection = @"$(column.name) $comp ?";
+ selection_args = { new StatementBuilder.Field<T>(column, value) };
+ } else {
+ selection = @"($selection) AND $(column.name) $comp ?";
+ StatementBuilder.Field[] selection_args_new = new StatementBuilder.Field[selection_args.length+1];
+ for (int i = 0; i < selection_args.length; i++) {
+ selection_args_new[i] = selection_args[i];
+ }
+ selection_args_new[selection_args.length] = new Field<T>(column, value);
+ selection_args = selection_args_new;
+ }
+ return this;
+ }
+
+ public QueryBuilder with_null<T>(Column<T> column) {
+ selection = @"($selection) AND $(column.name) ISNULL";
+ return this;
+ }
+
+ public QueryBuilder without_null<T>(Column<T> column) {
+ selection = @"($selection) AND $(column.name) NOT NULL";
+ return this;
+ }
+
+ private void add_order_by(OrderingTerm term) {
+ if (order_by_terms == null) {
+ order_by_terms = { term };
+ } else {
+ OrderingTerm[] order_by_terms_new = new OrderingTerm[order_by_terms.length+1];
+ for (int i = 0; i < order_by_terms.length; i++) {
+ order_by_terms_new[i] = order_by_terms[i];
+ }
+ order_by_terms_new[order_by_terms.length] = term;
+ order_by_terms = order_by_terms_new;
+ }
+ }
+
+ public QueryBuilder order_by(Column column, string dir = "ASC") {
+ add_order_by(new OrderingTerm(column, dir));
+ return this;
+ }
+
+ public QueryBuilder order_by_name(string name, string dir) {
+ add_order_by(new OrderingTerm.by_name(name, dir));
+ return this;
+ }
+
+ public QueryBuilder limit(int limit) {
+ this.limit_val = limit;
+ return this;
+ }
+
+ public int64 count() throws DatabaseError {
+ this.column_selector = @"COUNT($column_selector) AS count";
+ this.single_result = true;
+ return row().get_integer("count");
+ }
+
+ public Row? row() throws DatabaseError {
+ if (!single_result) throw new DatabaseError.NON_UNIQUE("query is not suited to return a single row, but row() was called.");
+ return iterator().next_value();
+ }
+
+ public T get<T>(Column<T> field) throws DatabaseError {
+ Row row = row();
+ if (row != null) {
+ return row[field];
+ }
+ return null;
+ }
+
+ public override Statement prepare() throws DatabaseError {
+ Statement stmt = db.prepare(@"SELECT $column_selector FROM $table_name $(selection != null ? @"WHERE $selection" : "") $(order_by_terms != null ? OrderingTerm.all_to_string(order_by_terms) : "") $(limit_val > 0 ? @" LIMIT $limit_val" : "")");
+ for (int i = 0; i < selection_args.length; i++) {
+ selection_args[i].bind(stmt, i+1);
+ }
+ return stmt;
+ }
+
+ public Row.RowIterator iterator() throws DatabaseError {
+ return new Row.RowIterator.from_query_builder(this);
+ }
+
+ class OrderingTerm {
+ Column column;
+ string column_name;
+ string dir;
+
+ public OrderingTerm(Column column, string dir) {
+ this.column = column;
+ this.column_name = column.name;
+ this.dir = dir;
+ }
+
+ public OrderingTerm.by_name(string column_name, string dir) {
+ this.column_name = column_name;
+ this.dir = dir;
+ }
+
+ public string to_string() {
+ return @"$column_name $dir";
+ }
+
+ public static string all_to_string(OrderingTerm[] terms) {
+ if (terms.length == 0) return "";
+ string res = "ORDER BY "+terms[0].to_string();
+ for (int i = 1; i < terms.length; i++) {
+ res += @", $(terms[i])";
+ }
+ return res;
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/row.vala b/qlite/src/row.vala
new file mode 100644
index 00000000..905d12a1
--- /dev/null
+++ b/qlite/src/row.vala
@@ -0,0 +1,79 @@
+using Gee;
+using Sqlite;
+
+namespace Qlite {
+
+public class Row {
+ private Map<string, string> text_map = new HashMap<string, string>();
+ private Map<string, long> int_map = new HashMap<string, long>();
+ private Map<string, double?> real_map = new HashMap<string, double?>();
+
+ public Row(Statement stmt) {
+ for (int i = 0; i < stmt.column_count(); i++) {
+ switch(stmt.column_type(i)) {
+ case TEXT:
+ text_map[stmt.column_name(i)] = stmt.column_text(i);
+ break;
+ case INTEGER:
+ int_map[stmt.column_name(i)] = (long) stmt.column_int64(i);
+ break;
+ case FLOAT:
+ real_map[stmt.column_name(i)] = stmt.column_double(i);
+ break;
+ }
+ }
+ }
+
+ public T get<T>(Column<T> field) {
+ return field[this];
+ }
+
+ public string? get_text(string field) {
+ if (text_map.contains(field)) {
+ return text_map[field];
+ }
+ return null;
+ }
+
+ public long get_integer(string field) {
+ return int_map[field];
+ }
+
+ public bool has_integer(string field) {
+ return int_map.contains(field);
+ }
+
+ public double get_real(string field) {
+ return real_map[field];
+ }
+
+ public bool has_real(string field) {
+ return real_map.contains(field) && real_map[field] != null;
+ }
+
+ public class RowIterator {
+ private Statement stmt;
+
+ public RowIterator.from_query_builder(QueryBuilder query) throws DatabaseError {
+ this.stmt = query.prepare();
+ }
+
+ public RowIterator(Database db, string sql, string[]? args = null) {
+ this.stmt = db.prepare(sql);
+ if (args != null) {
+ for (int i = 0; i < args.length; i++) {
+ stmt.bind_text(i, sql, sql.length);
+ }
+ }
+ }
+
+ public Row? next_value() {
+ if (stmt.step() == Sqlite.ROW) {
+ return new Row(stmt);
+ }
+ return null;
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/statement_builder.vala b/qlite/src/statement_builder.vala
new file mode 100644
index 00000000..8df069dd
--- /dev/null
+++ b/qlite/src/statement_builder.vala
@@ -0,0 +1,53 @@
+using Sqlite;
+
+namespace Qlite {
+
+public abstract class StatementBuilder {
+ protected Database db;
+
+ public StatementBuilder(Database db) {
+ this.db = db;
+ }
+
+ public abstract Statement prepare() throws DatabaseError;
+
+ protected class Field<T> {
+ public T value;
+ public Column<T>? column;
+
+ public Field(Column<T>? column, T value) {
+ this.column = column;
+ this.value = value;
+ }
+
+ public virtual void bind(Statement stmt, int index) {
+ if (column != null) {
+ column.bind(stmt, index, value);
+ } else {
+ throw new DatabaseError.NOT_SUPPORTED("binding was not implemented for this field.");
+ }
+ }
+ }
+
+ protected class NullField<T> : Field<T> {
+ public NullField(Column<T>? column) {
+ base(column, null);
+ }
+
+ public override void bind(Statement stmt, int index) {
+ stmt.bind_null(index);
+ }
+ }
+
+ protected class StringField : Field<string> {
+ public StringField(string value) {
+ base(null, value);
+ }
+
+ public override void bind(Statement stmt, int index) {
+ stmt.bind_text(index, value);
+ }
+ }
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/table.vala b/qlite/src/table.vala
new file mode 100644
index 00000000..209a5a96
--- /dev/null
+++ b/qlite/src/table.vala
@@ -0,0 +1,84 @@
+using Sqlite;
+
+namespace Qlite {
+
+public class Table {
+ protected Database db;
+ public string name { get; private set; }
+ protected Column[] columns;
+
+ public Table(Database db, string name) {
+ this.db = db;
+ this.name = name;
+ }
+
+ public void init(Column[] columns) {
+ this.columns = columns;
+ }
+
+ private void ensure_init() throws DatabaseError {
+ if (columns == null) throw new DatabaseError.NOT_INITIALIZED(@"Table $name was not initialized, call init()");
+ }
+
+ public QueryBuilder select(Column[]? columns = null) throws DatabaseError {
+ ensure_init();
+ return db.select(columns).from(this);
+ }
+
+ public InsertBuilder insert() throws DatabaseError {
+ ensure_init();
+ return db.insert().into(this);
+ }
+
+ public UpdateBuilder update() throws DatabaseError {
+ ensure_init();
+ return db.update(this);
+ }
+
+ public DeleteBuilder delete() throws DatabaseError {
+ ensure_init();
+ return db.delete().from(this);
+ }
+
+ public Row? row_with<T>(Column<T> column, T value) throws DatabaseError {
+ ensure_init();
+ if (!column.unique && !column.primary_key) throw new DatabaseError.NON_UNIQUE(@"$(column.name) is not suited to identify a row, but used with row_with()");
+ return select().with(column, "=", value).row();
+ }
+
+ public bool is_known_column(string column) throws DatabaseError {
+ ensure_init();
+ foreach (Column c in columns) {
+ if (c.name == column) return true;
+ }
+ return false;
+ }
+
+ public void create_table_at_version(long version) throws DatabaseError {
+ ensure_init();
+ string sql = @"CREATE TABLE IF NOT EXISTS $name (";
+ for(int i = 0; i < columns.length; i++) {
+ Column c = columns[i];
+ if (c.min_version <= version && c.max_version >= version) {
+ sql += @"$(i > 0 ? "," : "") $c";
+ }
+ }
+ sql += ")";
+ db.exec(sql);
+ }
+
+ public void add_columns_for_version(long old_version, long new_version) throws DatabaseError {
+ ensure_init();
+ foreach (Column c in columns) {
+ if (c.min_version <= new_version && c.max_version >= new_version && c.min_version > old_version && c.max_version < old_version) {
+ db.exec(@"ALTER TABLE $name ADD COLUMN $c");
+ }
+ }
+ }
+
+ public void delete_columns_for_version(long old_version, long new_version) throws DatabaseError {
+ // TODO: Rename old table, create table at new_version, transfer data
+ }
+}
+
+} \ No newline at end of file
diff --git a/qlite/src/update_builder.vala b/qlite/src/update_builder.vala
new file mode 100644
index 00000000..f6729772
--- /dev/null
+++ b/qlite/src/update_builder.vala
@@ -0,0 +1,133 @@
+using Sqlite;
+
+namespace Qlite {
+
+public class UpdateBuilder : StatementBuilder {
+
+ // UPDATE [OR ...]
+ private string or_val;
+
+ // [...]
+ private Table table;
+ private string table_name;
+
+ // SET [...]
+ private StatementBuilder.Field[] fields;
+
+ // WHERE [...]
+ private string selection;
+ private StatementBuilder.Field[] selection_args;
+
+ protected UpdateBuilder(Database db, Table table) {
+ base(db);
+ this.table = table;
+ this.table_name = table.name;
+ }
+
+ internal UpdateBuilder.for_name(Database db, string table) {
+ base(db);
+ this.table_name = table;
+ }
+
+ public UpdateBuilder or(string or) {
+ this.or_val = or;
+ return this;
+ }
+
+ public UpdateBuilder set<T>(Column<T> column, T value) {
+ if (fields == null) {
+ fields = { new StatementBuilder.Field<T>(column, value) };
+ } else {
+ StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1];
+ for (int i = 0; i < fields.length; i++) {
+ fields_new[i] = fields[i];
+ }
+ fields_new[fields.length] = new Field<T>(column, value);
+ fields = fields_new;
+ }
+ return this;
+ }
+
+ public UpdateBuilder set_null<T>(Column<T> column) {
+ if (column.not_null) throw new DatabaseError.ILLEGAL_QUERY(@"Can't set non-null column $(column.name) to null");
+ if (fields == null) {
+ fields = { new NullField<T>(column) };
+ } else {
+ StatementBuilder.Field[] fields_new = new StatementBuilder.Field[fields.length+1];
+ for (int i = 0; i < fields.length; i++) {
+ fields_new[i] = fields[i];
+ }
+ fields_new[fields.length] = new NullField<T>(column);
+ fields = fields_new;
+ }
+ return this;
+ }
+
+ public UpdateBuilder where(string selection, string[]? selection_args = null) {
+ if (selection != null) throw new DatabaseError.ILLEGAL_QUERY("selection was already done, but where() was called.");
+ this.selection = selection;
+ if (selection_args != null) {
+ this.selection_args = new StatementBuilder.Field[selection_args.length];
+ for (int i = 0; i < selection_args.length; i++) {
+ this.selection_args[i] = new StatementBuilder.StringField(selection_args[i]);
+ }
+ }
+ return this;
+ }
+
+ public UpdateBuilder with<T>(Column<T> column, string comp, T value) {
+ if (selection == null) {
+ selection = @"$(column.name) $comp ?";
+ selection_args = { new StatementBuilder.Field<T>(column, value) };
+ } else {
+ selection = @"($selection) AND $(column.name) $comp ?";
+ StatementBuilder.Field[] selection_args_new = new StatementBuilder.Field[selection_args.length+1];
+ for (int i = 0; i < selection_args.length; i++) {
+ selection_args_new[i] = selection_args[i];
+ }
+ selection_args_new[selection_args.length] = new Field<T>(column, value);
+ selection_args = selection_args_new;
+ }
+ return this;
+ }
+
+ public UpdateBuilder with_null<T>(Column<T> column) {
+ selection = @"($selection) AND $(column.name) ISNULL";
+ return this;
+ }
+
+ public UpdateBuilder without_null<T>(Column<T> column) {
+ selection = @"($selection) AND $(column.name) NOT NULL";
+ return this;
+ }
+
+ public override Statement prepare() throws DatabaseError {
+ string sql = "UPDATE";
+ if (or_val != null) sql += @" OR $or_val";
+ sql += @" $table_name SET ";
+ for (int i = 0; i < fields.length; i++) {
+ if (i != 0) {
+ sql += ", ";
+ }
+ sql += @"$(fields[i].column.name) = ?";
+ }
+ sql += @" WHERE $selection";
+ Statement stmt = db.prepare(sql);
+ for (int i = 0; i < fields.length; i++) {
+ fields[i].bind(stmt, i+1);
+ }
+ for (int i = 0; i < selection_args.length; i++) {
+ selection_args[i].bind(stmt, i + fields.length + 1);
+ }
+ return stmt;
+ }
+
+ public void perform() throws DatabaseError {
+ if (prepare().step() != DONE) {
+ throw new DatabaseError.EXEC_ERROR(@"SQLite error: $(db.errcode()) - $(db.errmsg())");
+ }
+ }
+
+}
+
+} \ No newline at end of file