2 * Copyright (C) 2009 Jens Georg <mail@jensge.org>.
4 * Author: Jens Georg <mail@jensge.org>
6 * This file is part of Rygel.
8 * Rygel is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU Lesser General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * Rygel is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU Lesser General Public License for more details.
18 * You should have received a copy of the GNU Lesser General Public License
19 * along with this program; if not, write to the Free Software Foundation,
20 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
25 public errordomain Rygel.MediaExport.DatabaseError {
31 * This class is a thin wrapper around SQLite's database object.
33 * It adds statement preparation based on GValue and a cancellable exec
36 internal class Rygel.MediaExport.Database : Object {
37 private Sqlite.Database db;
40 * Callback to pass to exec
42 * @return true, if you want the query to continue, false otherwise
44 public delegate bool RowCallback (Sqlite.Statement stmt);
46 private static void utf8_like (Sqlite.Context context,
48 requires (args.length == 2) {
49 if (args[1].to_text() == null) {
50 context.result_int (0);
55 var pattern = Regex.escape_string (args[0].to_text ());
56 pattern = pattern.replace("%", ".*").replace ("_", ".");
57 if (Regex.match_simple (pattern,
59 RegexCompileFlags.CASELESS)) {
60 context.result_int (1);
62 context.result_int (0);
66 private static int utf8_collate (int alen, void* a, int blen, void* b) {
67 // unowned to prevent array copy
68 unowned uint8[] _a = (uint8[]) a;
71 unowned uint8[] _b = (uint8[]) b;
74 var str_a = ((string) _a).casefold ();
75 var str_b = ((string) _b).casefold ();
77 return str_a.collate (str_b);
81 * Open a database in the user's cache directory as defined by XDG
83 * @param name of the database, used to build full path
84 * (<cache-dir>/rygel/<name>.db)
86 public Database (string name) throws DatabaseError {
87 var dirname = Path.build_filename (Environment.get_user_cache_dir (),
89 DirUtils.create_with_parents (dirname, 0750);
90 var db_file = Path.build_filename (dirname, "%s.db".printf (name));
91 debug (_("Using database file %s"), db_file);
92 var rc = Sqlite.Database.open (db_file, out this.db);
93 if (rc != Sqlite.OK) {
94 throw new DatabaseError.IO_ERROR (
95 _("Failed to open database: %d (%s)"),
100 this.db.exec ("PRAGMA cache_size = 32768");
101 this.db.exec ("PRAGMA synchronous = OFF");
102 this.db.exec ("PRAGMA temp_store = MEMORY");
103 this.db.exec ("PRAGMA count_changes = OFF");
104 this.db.create_function ("like",
111 this.db.create_collation ("CASEFOLD",
113 Database.utf8_collate);
117 * Execute a cancellable SQL statement.
119 * The supplied values are bound to the SQL statement and the RowCallback
120 * is called on every row of the resultset.
122 * @param sql statement to execute
123 * @param values array of values to bind to the SQL statement or null if
125 * @param callback to call on each row of the result set or null if none
127 * @param cancellable to cancel the running query or null if none
130 public int exec (string sql,
131 GLib.Value[]? values = null,
132 RowCallback? callback = null,
133 Cancellable? cancellable = null) throws DatabaseError {
135 var t = new Timer ();
139 if (values == null && callback == null && cancellable == null) {
140 rc = this.db.exec (sql);
142 var statement = prepare_statement (sql, values);
143 while ((rc = statement.step ()) == Sqlite.ROW) {
144 if (cancellable != null && cancellable.is_cancelled ()) {
148 if (callback != null) {
149 if (!callback (statement)) {
158 if (rc != Sqlite.DONE && rc != Sqlite.OK) {
159 throw new DatabaseError.SQLITE_ERROR (db.errmsg ());
162 debug ("Query: %s, Time: %f", sql, t.elapsed ());
169 * Analyze triggers of database
171 public void analyze () {
172 this.db.exec ("ANALYZE");
176 * Special GValue to pass to exec or prepare_statement to bind a column to
179 public static GLib.Value @null () {
180 GLib.Value v = GLib.Value (typeof (void *));
181 v.set_pointer (null);
187 * Start a transaction
189 public void begin () throws DatabaseError {
190 this.single_statement ("BEGIN");
194 * Commit a transaction
196 public void commit () throws DatabaseError {
197 this.single_statement ("COMMIT");
201 * Rollback a transaction
203 public void rollback () {
205 this.single_statement ("ROLLBACK");
206 } catch (DatabaseError error) {
207 critical (_("Failed to roll back transaction: %s"),
213 * Execute a single SQL statement and throw an exception on error
215 * @param sql SQL statement to execute
216 * @throws DatabaseError if SQL statement fails
218 private void single_statement (string sql) throws DatabaseError {
219 if (this.db.exec (sql) != Sqlite.OK) {
220 throw new DatabaseError.SQLITE_ERROR (db.errmsg ());
225 * Prepare a SQLite statement from a SQL string
227 * This function uses the type of the GValue passed in values to determine
228 * which _bind function to use.
230 * Supported types are: int, long, int64, string and pointer.
231 * @note the only pointer supported is the null pointer as provided by
232 * Database.@null. This is a special value to bind a column to NULL
234 * @param sql statement to execute
235 * @param values array of values to bind to the SQL statement or null if
238 private Statement prepare_statement (string sql,
239 GLib.Value[]? values = null)
240 throws DatabaseError {
242 var rc = db.prepare_v2 (sql, -1, out statement, null);
243 if (rc != Sqlite.OK) {
244 throw new DatabaseError.SQLITE_ERROR (db.errmsg ());
247 if (values != null) {
248 for (int i = 0; i < values.length; i++) {
249 if (values[i].holds (typeof (int))) {
250 rc = statement.bind_int (i + 1, values[i].get_int ());
251 } else if (values[i].holds (typeof (int64))) {
252 rc = statement.bind_int64 (i + 1, values[i].get_int64 ());
253 } else if (values[i].holds (typeof (long))) {
254 rc = statement.bind_int64 (i + 1, values[i].get_long ());
255 } else if (values[i].holds (typeof (string))) {
256 rc = statement.bind_text (i + 1, values[i].get_string ());
257 } else if (values[i].holds (typeof (void *))) {
258 if (values[i].peek_pointer () == null) {
259 rc = statement.bind_null (i + 1);
261 assert_not_reached ();
264 var t = values[i].type ();
265 warning (_("Unsupported type %s"), t.name ());
266 assert_not_reached ();
268 if (rc != Sqlite.OK) {
269 throw new DatabaseError.SQLITE_ERROR (db.errmsg ());