media-export: Fix bgo#627243
[profile/ivi/rygel.git] / src / plugins / media-export / rygel-media-export-database.vala
1 /*
2  * Copyright (C) 2009 Jens Georg <mail@jensge.org>.
3  *
4  * Author: Jens Georg <mail@jensge.org>
5  *
6  * This file is part of Rygel.
7  *
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.
12  *
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.
17  *
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.
21  */
22
23 using Sqlite;
24
25 public errordomain Rygel.MediaExport.DatabaseError {
26     IO_ERROR,
27     SQLITE_ERROR
28 }
29
30 /**
31  * This class is a thin wrapper around SQLite's database object.
32  *
33  * It adds statement preparation based on GValue and a cancellable exec
34  * function.
35  */
36 internal class Rygel.MediaExport.Database : Object {
37     private Sqlite.Database db;
38
39     /**
40      * Callback to pass to exec
41      *
42      * @return true, if you want the query to continue, false otherwise
43      */
44     public delegate bool RowCallback (Sqlite.Statement stmt);
45
46     private static void utf8_like (Sqlite.Context context,
47                                    Sqlite.Value[] args)
48                                    requires (args.length == 2) {
49         if (args[1].to_text() == null) {
50            context.result_int (0);
51
52            return;
53         }
54
55         var pattern = Regex.escape_string (args[0].to_text ());
56         pattern = pattern.replace("%", ".*").replace ("_", ".");
57         if (Regex.match_simple (pattern,
58                                 args[1].to_text (),
59                                 RegexCompileFlags.CASELESS)) {
60             context.result_int (1);
61         } else {
62             context.result_int (0);
63         }
64     }
65
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;
69         _a.length = alen;
70
71         unowned uint8[] _b = (uint8[]) b;
72         _b.length = blen;
73
74         var str_a = ((string) _a).casefold ();
75         var str_b = ((string) _b).casefold ();
76
77         return str_a.collate (str_b);
78     }
79
80     /**
81      * Open a database in the user's cache directory as defined by XDG
82      *
83      * @param name of the database, used to build full path
84      * (<cache-dir>/rygel/<name>.db)
85      */
86     public Database (string name) throws DatabaseError {
87         var dirname = Path.build_filename (Environment.get_user_cache_dir (),
88                                            "rygel");
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)"),
96                                         rc,
97                                         db.errmsg ());
98         }
99
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",
105                                  2,
106                                  Sqlite.UTF8,
107                                  null,
108                                  Database.utf8_like,
109                                  null,
110                                  null);
111         this.db.create_collation ("CASEFOLD",
112                                   Sqlite.UTF8,
113                                   Database.utf8_collate);
114     }
115
116     /**
117      * Execute a cancellable SQL statement.
118      *
119      * The supplied values are bound to the SQL statement and the RowCallback
120      * is called on every row of the resultset.
121      *
122      * @param sql statement to execute
123      * @param values array of values to bind to the SQL statement or null if
124      * none
125      * @param callback to call on each row of the result set or null if none
126      * necessary
127      * @param cancellable to cancel the running query or null if none
128      * necessary
129      */
130     public int exec (string        sql,
131                      GLib.Value[]? values      = null,
132                      RowCallback?  callback    = null,
133                      Cancellable?  cancellable = null) throws DatabaseError {
134         #if RYGEL_DEBUG_SQL
135         var t = new Timer ();
136         #endif
137         int rc;
138
139         if (values == null && callback == null && cancellable == null) {
140             rc = this.db.exec (sql);
141         } else {
142             var statement = prepare_statement (sql, values);
143             while ((rc = statement.step ()) == Sqlite.ROW) {
144                 if (cancellable != null && cancellable.is_cancelled ()) {
145                     break;
146                 }
147
148                 if (callback != null) {
149                     if (!callback (statement)) {
150                         rc = Sqlite.DONE;
151
152                         break;
153                     }
154                 }
155             }
156         }
157
158         if (rc != Sqlite.DONE && rc != Sqlite.OK) {
159             throw new DatabaseError.SQLITE_ERROR (db.errmsg ());
160         }
161         #if RYGEL_DEBUG_SQL
162         debug ("Query: %s, Time: %f", sql, t.elapsed ());
163         #endif
164
165         return rc;
166     }
167
168     /**
169      * Analyze triggers of database
170      */
171     public void analyze () {
172         this.db.exec ("ANALYZE");
173     }
174
175     /**
176      * Special GValue to pass to exec or prepare_statement to bind a column to
177      * NULL
178      */
179     public static GLib.Value @null () {
180         GLib.Value v = GLib.Value (typeof (void *));
181         v.set_pointer (null);
182
183         return v;
184     }
185
186     /**
187      * Start a transaction
188      */
189     public void begin () throws DatabaseError {
190         this.single_statement ("BEGIN");
191     }
192
193     /**
194      * Commit a transaction
195      */
196     public void commit () throws DatabaseError {
197         this.single_statement ("COMMIT");
198     }
199
200     /**
201      * Rollback a transaction
202      */
203     public void rollback () {
204         try {
205             this.single_statement ("ROLLBACK");
206         } catch (DatabaseError error) {
207             critical (_("Failed to roll back transaction: %s"),
208                       error.message);
209         }
210     }
211
212     /**
213      * Execute a single SQL statement and throw an exception on error
214      *
215      * @param sql SQL statement to execute
216      * @throws DatabaseError if SQL statement fails
217      */
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 ());
221         }
222     }
223
224     /**
225      * Prepare a SQLite statement from a SQL string
226      *
227      * This function uses the type of the GValue passed in values to determine
228      * which _bind function to use.
229      *
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
233      *
234      * @param sql statement to execute
235      * @param values array of values to bind to the SQL statement or null if
236      * none
237      */
238     private Statement prepare_statement (string        sql,
239                                          GLib.Value[]? values = null)
240                                          throws DatabaseError {
241         Statement statement;
242         var rc = db.prepare_v2 (sql, -1, out statement, null);
243         if (rc != Sqlite.OK) {
244             throw new DatabaseError.SQLITE_ERROR (db.errmsg ());
245         }
246
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);
260                     } else {
261                         assert_not_reached ();
262                     }
263                 } else {
264                     var t = values[i].type ();
265                     warning (_("Unsupported type %s"), t.name ());
266                     assert_not_reached ();
267                 }
268                 if (rc != Sqlite.OK) {
269                     throw new DatabaseError.SQLITE_ERROR (db.errmsg ());
270                 }
271             }
272         }
273
274         return statement;
275     }
276 }