2 // FileAttributesStore_Sqlite.cs
4 // Copyright (C) 2004 Novell, Inc.
8 // Permission is hereby granted, free of charge, to any person obtaining a copy
9 // of this software and associated documentation files (the "Software"), to deal
10 // in the Software without restriction, including without limitation the rights
11 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 // copies of the Software, and to permit persons to whom the Software is
13 // furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in all
16 // copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28 using System
.Collections
;
30 using System
.Threading
;
32 using Mono
.Data
.SqliteClient
;
36 namespace Beagle
.Daemon
{
38 public class FileAttributesStore_Sqlite
: IFileAttributesStore
{
41 // 1: Original version
42 // 2: Replaced LastIndexedTime with LastAttrTime
43 const int VERSION
= 2;
45 private SqliteConnection connection
;
46 private BitArray path_flags
;
47 private int transaction_count
= 0;
49 enum TransactionState
{
54 private TransactionState transaction_state
;
56 public FileAttributesStore_Sqlite (string directory
, string index_fingerprint
)
58 bool create_new_db
= false;
59 path_flags
= new BitArray (65536);
61 if (! File
.Exists (GetDbPath (directory
))) {
65 // Funky logic here to deal with sqlite versions.
67 // When sqlite 3 tries to open an sqlite 2 database,
68 // it will throw an SqliteException with SqliteError
69 // NOTADB when trying to execute a command.
71 // When sqlite 2 tries to open an sqlite 3 database,
72 // it will throw an ApplicationException when it
73 // tries to open the database.
76 connection
= Open (directory
);
77 } catch (ApplicationException
) {
78 Logger
.Log
.Warn ("Likely sqlite database version mismatch trying to open {0}. Purging.", GetDbPath (directory
));
82 if (! create_new_db
) {
83 SqliteCommand command
;
84 SqliteDataReader reader
= null;
85 int stored_version
= 0;
86 string stored_fingerprint
= null;
89 command
= new SqliteCommand ();
90 command
.Connection
= connection
;
92 "SELECT version, fingerprint FROM db_info";
94 reader
= ExecuteReaderOrWait (command
);
95 } catch (Exception ex
) {
96 Logger
.Log
.Warn ("Likely sqlite database version mismatch trying to read from {0}. Purging.", GetDbPath (directory
));
99 if (reader
!= null && ! create_new_db
) {
100 if (ReadOrWait (reader
)) {
101 stored_version
= reader
.GetInt32 (0);
102 stored_fingerprint
= reader
.GetString (1);
108 if (VERSION
!= stored_version
109 || (index_fingerprint
!= null && index_fingerprint
!= stored_fingerprint
))
110 create_new_db
= true;
115 if (connection
!= null)
116 connection
.Dispose ();
117 File
.Delete (GetDbPath (directory
));
118 connection
= Open (directory
);
120 DoNonQuery ("CREATE TABLE db_info ( " +
121 " version INTEGER NOT NULL, " +
122 " fingerprint STRING NOT NULL " +
125 DoNonQuery ("INSERT INTO db_info (version, fingerprint) VALUES ({0}, '{1}')",
126 VERSION
, index_fingerprint
);
128 DoNonQuery ("CREATE TABLE file_attributes ( " +
129 " unique_id STRING UNIQUE, " +
130 " directory STRING NOT NULL, " +
131 " filename STRING NOT NULL, " +
132 " last_mtime STRING NOT NULL, " +
133 " last_attrtime STRING NOT NULL, " +
134 " filter_name STRING NOT NULL, " +
135 " filter_version STRING NOT NULL " +
138 DoNonQuery ("CREATE INDEX file_path on file_attributes (" +
143 SqliteCommand command
;
144 SqliteDataReader reader
;
147 DateTime dt1
= DateTime
.Now
;
149 // Select all of the files and use them to populate our bit-vector.
150 command
= new SqliteCommand ();
151 command
.Connection
= connection
;
152 command
.CommandText
= "SELECT directory, filename FROM file_attributes";
154 reader
= ExecuteReaderOrWait (command
);
156 while (ReadOrWait (reader
)) {
158 string dir
= reader
.GetString (0);
159 string file
= reader
.GetString (1);
160 string path
= Path
.Combine (dir
, file
);
168 DateTime dt2
= DateTime
.Now
;
170 Logger
.Log
.Debug ("Loaded {0} records from {1} in {2:0.000}s",
171 count
, GetDbPath (directory
), (dt2
- dt1
).TotalSeconds
);
175 ///////////////////////////////////////////////////////////////////
177 private string GetDbPath (string directory
)
179 return Path
.Combine (directory
, "FileAttributesStore.db");
182 private SqliteConnection
Open (string directory
)
185 c
= new SqliteConnection ();
186 c
.ConnectionString
= "version=" + ExternalStringsHack
.SqliteVersion
187 + ",encoding=UTF-8,URI=file:" + GetDbPath (directory
);
192 private void DoNonQuery (string format
, params object [] args
)
194 SqliteCommand command
;
195 command
= new SqliteCommand ();
196 command
.Connection
= connection
;
197 command
.CommandText
= String
.Format (format
, args
);
201 command
.ExecuteNonQuery ();
203 } catch (SqliteBusyException ex
) {
211 private SqliteCommand
QueryCommand (string where_format
, params object [] where_args
)
213 SqliteCommand command
;
214 command
= new SqliteCommand ();
215 command
.Connection
= connection
;
216 command
.CommandText
=
217 "SELECT unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version " +
218 "FROM file_attributes WHERE " +
219 String
.Format (where_format
, where_args
);
223 static private SqliteDataReader
ExecuteReaderOrWait (SqliteCommand command
)
225 SqliteDataReader reader
= null;
226 while (reader
== null) {
228 reader
= command
.ExecuteReader ();
229 } catch (SqliteBusyException ex
) {
236 static private bool ReadOrWait (SqliteDataReader reader
)
240 return reader
.Read ();
241 } catch (SqliteBusyException ex
) {
247 private FileAttributes
GetFromReader (SqliteDataReader reader
)
249 FileAttributes attr
= new FileAttributes ();
251 attr
.UniqueId
= GuidFu
.FromShortString (reader
.GetString (0));
252 attr
.Path
= System
.IO
.Path
.Combine (reader
.GetString (1), reader
.GetString (2));
253 attr
.LastWriteTime
= StringFu
.StringToDateTime (reader
.GetString (3));
254 attr
.LastAttrTime
= StringFu
.StringToDateTime (reader
.GetString (4));
255 attr
.FilterName
= reader
.GetString (5);
256 attr
.FilterVersion
= int.Parse (reader
.GetString (6));
258 if (attr
.FilterName
== "")
259 attr
.FilterName
= null;
264 ///////////////////////////////////////////////////////////////////
266 private int GetPathHash (string path
)
268 uint hash
= 0xdeadbeef;
269 foreach (char c
in path
)
270 hash
= 17 * hash
+ (uint) c
;
271 // Fold the 32 bits in 16.
272 return (int) ((hash
& 0xffff) ^
(hash
>> 16));
275 private bool GetPathFlag (string path
)
277 int hash
= GetPathHash (path
);
278 return path_flags
[hash
];
281 private void SetPathFlag (string path
)
283 int hash
= GetPathHash (path
);
284 path_flags
[hash
] = true;
287 ///////////////////////////////////////////////////////////////////
289 public FileAttributes
Read (string path
)
291 SqliteCommand command
;
292 SqliteDataReader reader
;
294 if (! GetPathFlag (path
))
297 FileAttributes attr
= null;
298 bool found_too_many
= false;
300 // We need to quote any 's that appear in the strings
301 // (int particular, in the path)
302 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
303 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
305 command
= QueryCommand ("directory='{0}' AND filename='{1}'",
306 directory
, filename
);
307 reader
= ExecuteReaderOrWait (command
);
309 if (ReadOrWait (reader
)) {
310 attr
= GetFromReader (reader
);
312 if (ReadOrWait (reader
))
313 found_too_many
= true;
318 // If we found more than one matching record for a given
319 // directory and filename, something has gone wrong.
320 // Since we have no way of knowing which one is correct
321 // and which isn't, we delete them all and return
322 // null. (Which in most cases will force a re-index.
323 if (found_too_many
) {
324 DoNonQuery ("DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
325 directory
, filename
);
332 public bool Write (FileAttributes fa
)
334 SetPathFlag (fa
.Path
);
336 // We need to quote any 's that appear in the strings
337 // (in particular, in the path)
340 // If a transaction has been requested, start it now.
341 MaybeStartTransaction ();
344 filter_name
= fa
.FilterName
;
345 if (filter_name
== null)
347 filter_name
= filter_name
.Replace ("'", "''");
349 DoNonQuery ("INSERT OR REPLACE INTO file_attributes " +
350 " (unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version) " +
351 " VALUES ('{0}', '{1}', '{2}', '{3}', '{4}', '{5}', '{6}')",
352 GuidFu
.ToShortString (fa
.UniqueId
),
353 fa
.Directory
.Replace ("'", "''"), fa
.Filename
.Replace ("'", "''"),
354 StringFu
.DateTimeToString (fa
.LastWriteTime
),
355 StringFu
.DateTimeToString (fa
.LastAttrTime
),
362 public void Drop (string path
)
364 // We don't want to "UnSetPathFlag" here, since we have no way of knowing
365 // if another path hashes to the same value as this one.
367 // We need to quote any 's that appear in the strings
368 // (in particular, in the path)
369 string directory
= FileSystem
.GetDirectoryNameRootOk (path
).Replace ("'", "''");
370 string filename
= Path
.GetFileName (path
).Replace ("'", "''");
373 // If a transaction has been requested, start it now.
374 MaybeStartTransaction ();
376 DoNonQuery ("DELETE FROM file_attributes WHERE directory='{0}' AND filename='{1}'",
377 directory
, filename
);
381 private void MaybeStartTransaction ()
383 if (transaction_state
== TransactionState
.Requested
) {
384 DoNonQuery ("BEGIN");
385 transaction_state
= TransactionState
.Started
;
389 public void BeginTransaction ()
391 if (transaction_state
== TransactionState
.None
)
392 transaction_state
= TransactionState
.Requested
;
395 public void CommitTransaction ()
397 if (transaction_state
== TransactionState
.Started
) {
399 DoNonQuery ("COMMIT");
401 transaction_state
= TransactionState
.None
;
407 if (transaction_count
> 0) {
408 Logger
.Log
.Debug ("Flushing requested -- committing sqlite transaction");
409 DoNonQuery ("COMMIT");
410 transaction_count
= 0;
415 ///////////////////////////////////////////////////////////////////
417 // Return all attributes in the attributes database, used for merging
419 private ICollection
ReadAllAttributes ()
421 ArrayList attributes
= new ArrayList ();
423 SqliteCommand command
;
424 SqliteDataReader reader
;
427 command
= new SqliteCommand ();
428 command
.Connection
= connection
;
429 command
.CommandText
=
430 "SELECT unique_id, directory, filename, last_mtime, last_attrtime, filter_name, filter_version " +
431 "FROM file_attributes";
433 reader
= ExecuteReaderOrWait (command
);
435 while (ReadOrWait (reader
)) {
436 attributes
.Add (GetFromReader (reader
));
445 // FIXME: Might wanna do this a bit more intelligently
447 public void Merge (FileAttributesStore_Sqlite fa_sqlite_store_to_merge
)
449 ICollection attributes
= fa_sqlite_store_to_merge
.ReadAllAttributes ();
453 foreach (FileAttributes attribute
in attributes
)
456 CommitTransaction ();