From 56bc45ce4d07a7a9a415e9dc8ad2f7c3f3c9e48d Mon Sep 17 00:00:00 2001 From: fiaxh Date: Thu, 2 Mar 2017 15:37:32 +0100 Subject: Initial commit --- qlite/src/column.vala | 188 +++++++++++++++++++++++++++++++++++++ qlite/src/database.vala | 152 ++++++++++++++++++++++++++++++ qlite/src/delete_builder.vala | 75 +++++++++++++++ qlite/src/insert_builder.vala | 102 ++++++++++++++++++++ qlite/src/query_builder.vala | 196 +++++++++++++++++++++++++++++++++++++++ qlite/src/row.vala | 79 ++++++++++++++++ qlite/src/statement_builder.vala | 53 +++++++++++ qlite/src/table.vala | 84 +++++++++++++++++ qlite/src/update_builder.vala | 133 ++++++++++++++++++++++++++ 9 files changed, 1062 insertions(+) create mode 100644 qlite/src/column.vala create mode 100644 qlite/src/database.vala create mode 100644 qlite/src/delete_builder.vala create mode 100644 qlite/src/insert_builder.vala create mode 100644 qlite/src/query_builder.vala create mode 100644 qlite/src/row.vala create mode 100644 qlite/src/statement_builder.vala create mode 100644 qlite/src/table.vala create mode 100644 qlite/src/update_builder.vala (limited to 'qlite/src') 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + private Table table; + private Column id_column; + + public RowReference(string name, Table table, Column 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 meta_name = new Column.Text("name") { primary_key = true }; + private Column meta_int_val = new Column.Long("int_val"); + private Column 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(Column column, string comp, T value) { + if (selection == null) { + selection = @"$(column.name) $comp ?"; + selection_args = { new StatementBuilder.Field(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(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(Column column, T value) { + if (fields == null) { + fields = { new StatementBuilder.Field(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(column, value); + fields = fields_new; + } + return this; + } + + public InsertBuilder value_null(Column 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(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(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(Column 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(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(column, value); + selection_args = selection_args_new; + } + return this; + } + + public QueryBuilder with_null(Column column) { + selection = @"($selection) AND $(column.name) ISNULL"; + return this; + } + + public QueryBuilder without_null(Column 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(Column 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 text_map = new HashMap(); + private Map int_map = new HashMap(); + private Map real_map = new HashMap(); + + 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(Column 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 { + public T value; + public Column? column; + + public Field(Column? 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 : Field { + public NullField(Column? column) { + base(column, null); + } + + public override void bind(Statement stmt, int index) { + stmt.bind_null(index); + } + } + + protected class StringField : Field { + 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(Column 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(Column column, T value) { + if (fields == null) { + fields = { new StatementBuilder.Field(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(column, value); + fields = fields_new; + } + return this; + } + + public UpdateBuilder set_null(Column 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(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(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(Column column, string comp, T value) { + if (selection == null) { + selection = @"$(column.name) $comp ?"; + selection_args = { new StatementBuilder.Field(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(column, value); + selection_args = selection_args_new; + } + return this; + } + + public UpdateBuilder with_null(Column column) { + selection = @"($selection) AND $(column.name) ISNULL"; + return this; + } + + public UpdateBuilder without_null(Column 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 -- cgit v1.2.3-54-g00ecf