Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
QueryMetadataProvider.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections.Generic;
5 using System.Data;
6 using System.Data.SQLite;
7 using System.IO;
8 using System.Linq;
9 using System.Threading.Tasks;
10 
11 using SiliconStudio.Core.Extensions;
12 
13 namespace SiliconStudio.BuildEngine
14 {
15  // TODO: sanitize SQL queries, ensure keys are valid and no forbidden char are passed
17  {
18  public const string DefaultDatabaseFilename = "Metadata.db";
19 
20  private readonly Dictionary<string, long> objectUrlIds = new Dictionary<string, long>();
21  private readonly Dictionary<MetadataKey, long> keyIds = new Dictionary<MetadataKey, long>();
22 
23  private SQLiteConnection connection;
24 
25  private readonly object lockObject = new object();
26 
27  public bool Open(string path, bool create)
28  {
29  if (!File.Exists(path))
30  {
31  return create && Create(path);
32  }
33 
34  string connectionString = String.Format("Data Source={0}", path);
35  lock (lockObject)
36  {
37  // Connection already opened
38  if (connection != null)
39  throw new InvalidOperationException("Connection to the metadata database already opened");
40 
41  connection = new SQLiteConnection(connectionString);
42  // TODO: check for exception, set connection to null in case of failure
43  connection.Open();
44  return true;
45  }
46  }
47 
48  public Task<bool> OpenAsync(string path, bool create)
49  {
50  return Task.Run(() => Open(path, create));
51  }
52 
53  /// <summary>
54  /// Create and open
55  /// </summary>
56  /// <param name="path"></param>
57  /// <returns></returns>
58  public bool Create(string path)
59  {
60  try
61  {
62  SQLiteConnection.CreateFile(path);
63  }
64  catch (IOException)
65  {
66  return false;
67  }
68 
69  if (!Open(path, false))
70  return false;
71 
72  const string Query = @"
73  CREATE TABLE `Keys` (
74  `KeyId` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
75  `TypeId` INTEGER NOT NULL,
76  `Name` char(255) NOT NULL
77  );
78  CREATE TABLE `ObjectUrls` (
79  `ObjectUrlId` INTEGER NOT NULL PRIMARY KEY,
80  `Location` char(255) NOT NULL
81  );
82  CREATE TABLE `Metadata` (
83  `ObjectUrlId` INTEGER NOT NULL,
84  `KeyId` INTEGER NOT NULL,
85  `Value`,
86  PRIMARY KEY (`ObjectUrlId`, `KeyId`),
87  FOREIGN KEY(ObjectUrlId) REFERENCES ObjectUrls(ObjectUrlId),
88  FOREIGN KEY(KeyId) REFERENCES Keys(KeyId)
89  );
90  ";
91 
92  ExecuteNonQuery(Query);
93  return true;
94  }
95 
96  public Task<bool> CreateAsync(string path)
97  {
98  return Task.Run(() => Create(path));
99  }
100 
101 
102  public void Close()
103  {
104  lock (lockObject)
105  {
106  if (connection != null)
107  connection.Close();
108 
109  connection = null;
110  }
111  }
112 
113  public Task CloseAsync()
114  {
115  return Task.Run(() => Close());
116  }
117 
119  {
120  var keysToRemove = new List<MetadataKey>(keyIds.Keys);
121  const string Query = @"SELECT * FROM `Keys`";
122 
123  DataTable dataTable = ExecuteReader(Query);
124  foreach (DataRow row in dataTable.Rows)
125  {
126  var typeId = (int)(long)row["TypeId"];
127  var keyId = (long)row["KeyId"];
128  if (typeId >= 0 && typeId < Enum.GetValues(typeof(MetadataKey.DatabaseType)).Length)
129  {
130  var key = new MetadataKey((string)row["Name"], (MetadataKey.DatabaseType)typeId);
131  keyIds[key] = keyId;
132  keysToRemove.Remove(key);
133  }
134  }
135  // Also remove keys that has been removed
136  foreach (var key in keysToRemove)
137  {
138  keyIds.Remove(key);
139  }
140  return keyIds.Keys.ToArray();
141  }
142 
144  {
145  return Task.Run(() => FetchAllKeys());
146  }
147 
149  {
150  var urlsToRemove = new List<string>();
151  const string Query = @"SELECT * FROM `ObjectUrls`";
152  DataTable dataTable = ExecuteReader(Query);
153  foreach (DataRow row in dataTable.Rows)
154  {
155  var url = (string)row["Location"];
156  var urlId = (long)row["ObjectUrlId"];
157  objectUrlIds[url] = urlId;
158  urlsToRemove.SwapRemove(url);
159  }
160  // Also remove keys that has been removed
161  foreach (var url in urlsToRemove)
162  {
163  objectUrlIds.Remove(url);
164  }
165  return objectUrlIds.Keys.ToArray();
166  }
167 
169  {
170  return Task.Run(() => FetchAllObjectUrls());
171  }
172 
173  public IEnumerable<IObjectMetadata> Fetch(string objectUrl)
174  {
175  string query = String.Format(@"SELECT * FROM `Metadata` INNER JOIN ObjectUrls ON `ObjectUrls`.`ObjectUrlId` = `Metadata`.`ObjectUrlId` INNER JOIN Keys ON `Keys`.`KeyId` = `Metadata`.`KeyId` WHERE `ObjectUrls`.`Location` = '{0}'", FormatUrl(objectUrl));
176  return ParseResult(ExecuteReader(query));
177  }
178 
180  {
181  return Task.Run(() => Fetch(objectUrl));
182  }
183 
185  {
186  string query = String.Format(@"SELECT * FROM `Metadata` INNER JOIN ObjectUrls ON `ObjectUrls`.`ObjectUrlId` = `Metadata`.`ObjectUrlId` INNER JOIN Keys ON `Keys`.`KeyId` = `Metadata`.`KeyId` WHERE `Keys`.`Name` = '{0}' AND `Keys`.`TypeId` = '{1}'", key.Name, (int)key.Type);
187  return ParseResult(ExecuteReader(query));
188  }
189 
191  {
192  return Task.Run(() => Fetch(key));
193  }
194 
195  public IObjectMetadata Fetch(string objectUrl, MetadataKey key)
196  {
197  string query = String.Format(@"SELECT * FROM `Metadata` INNER JOIN ObjectUrls ON `ObjectUrls`.`ObjectUrlId` = `Metadata`.`ObjectUrlId` INNER JOIN Keys ON `Keys`.`KeyId` = `Metadata`.`KeyId` WHERE `ObjectUrls`.`Location` = '{0}' AND `Keys`.`Name` = '{1}' AND `Keys`.`TypeId` = '{2}'", FormatUrl(objectUrl), key.Name, (int)key.Type);
198  return ParseResult(ExecuteReader(query)).SingleOrDefault();
199  }
200 
201  public Task<IObjectMetadata> FetchAsync(string objectUrl, MetadataKey key)
202  {
203  return Task.Run(() => Fetch(objectUrl, key));
204  }
205 
207  {
208  return Fetch(data.ObjectUrl, data.Key);
209  }
210 
212  {
213  return Task.Run(() => Fetch(data.ObjectUrl, data.Key));
214  }
215 
217  {
218  string query = String.Format(@"SELECT * FROM `Metadata` INNER JOIN ObjectUrls ON `ObjectUrls`.`ObjectUrlId` = `Metadata`.`ObjectUrlId` INNER JOIN Keys ON `Keys`.`KeyId` = `Metadata`.`KeyId`");
219 
220  return ParseResult(ExecuteReader(query));
221  }
222 
224  {
225  return Task.Run(() => FetchAll());
226  }
227 
228  private IEnumerable<IObjectMetadata> ParseResult(DataTable dataTable)
229  {
230  var result = new List<IObjectMetadata>();
231  foreach (DataRow row in dataTable.Rows)
232  {
233  var url = (string)row["Location"];
234  var name = (string)row["Name"];
235  var type = (MetadataKey.DatabaseType)(long)row["TypeId"];
236  var key = new MetadataKey(name, type);
237  var keyId = (long)row["KeyId"];
238  var objectUrlId = (long)row["ObjectUrlId"];
239  keyIds[key] = keyId;
240  objectUrlIds[FormatUrl(url)] = objectUrlId;
241  object value = key.ConvertValue(row["Value"].ToString());
242  result.Add(new ObjectMetadata(url, key, value));
243  }
244  return result;
245  }
246 
247  public bool AddKey(MetadataKey key)
248  {
249  if (key == null) throw new ArgumentNullException("key");
250  if (!key.IsValid()) throw new ArgumentException(@"Key is invalid.", "key");
251  var typeId = (int)key.Type;
252  if (typeId != -1)
253  {
254  lock (lockObject)
255  {
256  if (connection != null)
257  {
258  // TODO/Benlitz: a transaction that first try to fetch the key. If it exists, it should return false
259  string query = String.Format(@"INSERT INTO `Keys` (`TypeId`, `Name`) VALUES ('{0}', '{1}')", typeId, key.Name);
260  return ExecuteNonQuery(query) == 1;
261  }
262  }
263  }
264  return false;
265  }
266 
267  public bool RemoveKey(MetadataKey key)
268  {
269  if (key == null) throw new ArgumentNullException("key");
270  if (!key.IsValid()) throw new ArgumentException(@"Key is invalid.", "key");
271  var typeId = (int)key.Type;
272  if (typeId != -1)
273  {
274  lock (lockObject)
275  {
276  if (connection != null)
277  {
278  string query = String.Format(@"DELETE FROM `Keys` WHERE `Keys`.`TypeId` = '{0}' AND `Keys`.`Name` = '{1}'", typeId, key.Name);
279  return ExecuteNonQuery(query) == 1;
280  }
281  }
282  }
283  return false;
284  }
285 
286  public bool Write(IObjectMetadata data)
287  {
288  if (data == null) throw new ArgumentNullException("data");
289 
290  long keyId, objectUrlId;
291  keyIds.TryGetValue(data.Key, out keyId);
292  objectUrlIds.TryGetValue(FormatUrl(data.ObjectUrl), out objectUrlId);
293 
294  IObjectMetadata previousData;
295  if (keyId != 0 && objectUrlId != 0)
296  {
297  previousData = Fetch(objectUrlId, keyId);
298  }
299  else
300  {
301  previousData = Fetch(data);
302  }
303 
304  // Insert
305  if (previousData == null)
306  {
307  keyId = keyId == 0 ? GetKeyId(data.Key) : keyId;
308  objectUrlId = objectUrlId == 0 ? GetOrCreateObjectUrlId(data.ObjectUrl) : objectUrlId;
309 
310  if (keyId == 0)
311  throw new InvalidOperationException(String.Format("The key {0} does not exist in database.", data.Key));
312 
313  return InsertMetadata(objectUrlId, keyId, data.Value.ToString());
314  }
315 
316  // Update
317  return UpdateMetadata(objectUrlId, keyId, data.Value.ToString());
318  }
319 
320  public bool Delete(IObjectMetadata data)
321  {
322  long keyId, objectUrlId;
323  keyIds.TryGetValue(data.Key, out keyId);
324  objectUrlIds.TryGetValue(FormatUrl(data.ObjectUrl), out objectUrlId);
325  keyId = keyId == 0 ? GetKeyId(data.Key) : keyId;
326  objectUrlId = objectUrlId == 0 ? GetOrCreateObjectUrlId(data.ObjectUrl) : objectUrlId;
327 
328  string query = String.Format(@"DELETE FROM `Metadata` WHERE `Metadata`.`ObjectUrlId` = '{0}' AND `Metadata`.`KeyId` = '{1}'", objectUrlId, keyId);
329  return ExecuteNonQuery(query) == 1;
330  }
331 
332  public void Dispose()
333  {
334  Close();
335  }
336 
337  /// <summary>
338  /// Escape single quotes and convert the string to lower invariant.
339  /// </summary>
340  /// <param name="url">The url to format.</param>
341  /// <returns>The url formatted accordingly.</returns>
342  private static string FormatUrl(string url)
343  {
344  return url.Replace("'", "''").ToLowerInvariant();
345  }
346 
347  private long GetKeyId(MetadataKey key)
348  {
349  if (key == null) throw new ArgumentNullException("key");
350  string query = String.Format(@"SELECT `KeyId` FROM `Keys` WHERE `Name` = '{0}' AND `TypeId` = '{1}'", key.Name, (int)key.Type);
351  var result = ExecuteScalar(query);
352  if (result != null)
353  keyIds[key] = (long)result;
354  return result != null ? (long)result : 0;
355  }
356 
357  private long GetObjectUrlId(string url)
358  {
359  if (url == null) throw new ArgumentNullException("url");
360  string query = String.Format(@"SELECT `ObjectUrlId` FROM `ObjectUrls` WHERE `Location` = '{0}'", FormatUrl(url));
361  var result = ExecuteScalar(query);
362  if (result != null)
363  objectUrlIds[FormatUrl(url)] = (long)result;
364  return result != null ? (long)result : 0;
365  }
366 
367  private bool CreateObjectUrlId(string url)
368  {
369  if (url == null) throw new ArgumentNullException("url");
370  string query = String.Format(@"INSERT INTO `ObjectUrls` (`Location`) VALUES ('{0}')", FormatUrl(url));
371  return ExecuteNonQuery(query) == 1;
372  }
373 
374  private long GetOrCreateObjectUrlId(string url)
375  {
376  if (url == null) throw new ArgumentNullException("url");
377  long objectUrlId = GetObjectUrlId(url);
378  if (objectUrlId == 0)
379  {
380  if (CreateObjectUrlId(url))
381  objectUrlId = GetObjectUrlId(url);
382  }
383  return objectUrlId;
384  }
385 
386  private IObjectMetadata Fetch(long objectUrlId, long keyId)
387  {
388  string query = String.Format(@"SELECT * FROM `Metadata` INNER JOIN ObjectUrls ON `ObjectUrls`.`ObjectUrlId` = `Metadata`.`ObjectUrlId` INNER JOIN Keys ON `Keys`.`KeyId` = `Metadata`.`KeyId` WHERE `Metadata`.`ObjectUrlId` = '{0}' AND `Metadata`.`KeyId` = '{1}'", objectUrlId, keyId);
389  return ParseResult(ExecuteReader(query)).SingleOrDefault();
390  }
391 
392  private bool InsertMetadata(long objectUrlId, long keyId, string value)
393  {
394  string query = String.Format(@"INSERT INTO `Metadata` (`ObjectUrlId`, `KeyId`, `Value`) VALUES ('{0}', '{1}', '{2}')", objectUrlId, keyId, value);
395  return ExecuteNonQuery(query) == 1;
396  }
397 
398  private bool UpdateMetadata(long objectUrlId, long keyId, string value)
399  {
400  string query = String.Format(@"UPDATE `Metadata` SET `Value` = '{0}' WHERE `ObjectUrlId` = '{1}' AND `KeyId` = '{2}'", value, objectUrlId, keyId);
401  return ExecuteNonQuery(query) == 1;
402  }
403 
404  private int ExecuteNonQuery(string query)
405  {
406  lock (lockObject)
407  {
408  var command = new SQLiteCommand(connection) { CommandText = query };
409  return command.ExecuteNonQuery();
410  }
411  }
412 
413  private object ExecuteScalar(string query)
414  {
415  lock (lockObject)
416  {
417  var command = new SQLiteCommand(connection) { CommandText = query };
418  return command.ExecuteScalar();
419  }
420  }
421 
422  private DataTable ExecuteReader(string query)
423  {
424  lock (lockObject)
425  {
426  var command = new SQLiteCommand(connection) { CommandText = query };
427  SQLiteDataReader reader = command.ExecuteReader();
428  var dataTable = new DataTable();
429  dataTable.Load(reader);
430  return dataTable;
431  }
432  }
433  }
434 }
Keys
Enumeration for keys.
Definition: Keys.cs:8
Task< IEnumerable< IObjectMetadata > > FetchAsync(string objectUrl)
object Value
The value of the metadata. Its type must match the Key type.
Represent a metadata key. This object is immutable.
Definition: MetadataKey.cs:11
System.IO.File File
IEnumerable< IObjectMetadata > Fetch(string objectUrl)
Task< IEnumerable< IObjectMetadata > > FetchAllAsync()
Task< IEnumerable< MetadataKey > > FetchAllKeysAsync()
MetadataKey Key
The key of the metadata
Task< IObjectMetadata > FetchAsync(string objectUrl, MetadataKey key)
Interface for object metadata
IObjectMetadata Fetch(string objectUrl, MetadataKey key)
IObjectMetadata Fetch(IObjectMetadata data)
Task< IEnumerable< string > > FetchAllObjectUrlsAsync()
Task< IObjectMetadata > FetchAsync(IObjectMetadata data)
Task< IEnumerable< IObjectMetadata > > FetchAsync(MetadataKey key)
Object metadata created by user in order to inject them in the database
Task< bool > OpenAsync(string path, bool create)
IEnumerable< IObjectMetadata > Fetch(MetadataKey key)
string ObjectUrl
The url of the object which uses this metadata