A tool for building queries.
        /// <summary>
        /// Removes a Navigation Path.
        /// </summary>
        /// <param name="connection">A database connection.</param>
        /// <param name="path">The navigation path to remove.</param>
        /// <returns><c>true</c> if the path is removed, <c>false</c> otherwise.</returns>
        private bool RemoveNavigationPath(DbConnection connection, NavigationPath path)
        {
            string nspace, name;
            NameTools.ExpandFullName(path.FullName, out nspace, out name);
            if(nspace == null) nspace = "";

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.DeleteFrom("NavigationPath");
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", name));
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));

            DbCommand command = builder.GetCommand(connection, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            return rows > 0;
        }
        /// <summary>
        /// Retrieves a Page Attachment.
        /// </summary>
        /// <param name="pageInfo">The Page Info that owns the Attachment.</param>
        /// <param name="name">The name of the Attachment, for example "myfile.jpg".</param>
        /// <param name="destinationStream">A Stream object used as <b>destination</b> of a byte stream,
        /// i.e. the method writes to the Stream the file content.</param>
        /// <param name="countHit">A value indicating whether or not to count this retrieval in the statistics.</param>
        /// <returns><c>true</c> if the Attachment is retrieved, <c>false</c> otherwise.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="pageInfo"/>, <paramref name="name"/> or <paramref name="destinationStream"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="name"/> is empty or if <paramref name="destinationStream"/> does not support writing,
        /// or if the page does not have attachments or if the attachment does not exist.</exception>
        public bool RetrievePageAttachment(PageInfo pageInfo, string name, System.IO.Stream destinationStream, bool countHit)
        {
            if(pageInfo == null) throw new ArgumentNullException("pageInfo");
            if(name == null) throw new ArgumentNullException("name");
            if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name");
            if(destinationStream == null) throw new ArgumentNullException("destinationStream");
            if(!destinationStream.CanWrite) throw new ArgumentException("Cannot write into Destination Stream", "destinationStream");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            if(!AttachmentExists(transaction, pageInfo, name)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("Attachment does not exist", "name");
            }

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("Attachment", new string[] { "Size", "Data" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", name));
            parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                bool done = false;

                if(reader.Read()) {
                    int read = ReadBinaryColumn(reader, "Data", destinationStream);
                    done = (long)read == (long)reader["Size"];
                }

                CloseReader(reader);

                if(!done) {
                    RollbackTransaction(transaction);
                    return false;
                }
            }
            else {
                RollbackTransaction(transaction);
                return false;
            }

            if(countHit) {
                // Update download count
                query = queryBuilder.UpdateIncrement("Attachment", "Downloads", 1);
                query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
                query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

                parameters = new List<Parameter>(2);
                parameters.Add(new Parameter(ParameterType.String, "Name", name));
                parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));

                command = builder.GetCommand(transaction, query, parameters);

                int rows = ExecuteNonQuery(command, false);
                if(rows != 1) {
                    RollbackTransaction(transaction);
                    return false;
                }
            }

            CommitTransaction(transaction);

            return true;
        }
        /// <summary>
        /// Sets the number of times a page attachment was retrieved.
        /// </summary>
        /// <param name="pageInfo">The page.</param>
        /// <param name="name">The name of the attachment.</param>
        /// <param name="count">The count to set.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="pageInfo"/> or <paramref name="name"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="name"/> is empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">If <paramref name="count"/> is less than zero.</exception>
        public void SetPageAttachmentRetrievalCount(PageInfo pageInfo, string name, int count)
        {
            if(pageInfo == null) throw new ArgumentNullException("pageInfo");
            if(name == null) throw new ArgumentNullException("name");
            if(name.Length == 0) throw new ArgumentException("Name cannot be empty");
            if(count < 0) throw new ArgumentOutOfRangeException("Count must be greater than or equal to zero", "count");

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.Update("Attachment", new string[] { "Downloads" }, new string[] { "Downloads" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", name));
            parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));
            parameters.Add(new Parameter(ParameterType.Int32, "Downloads", count));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            ExecuteNonQuery(command);
        }
        /// <summary>
        /// Lists the Files in the specified Directory.
        /// </summary>
        /// <param name="directory">The full directory name, for example "/my/directory". Null, empty or "/" for the root directory.</param>
        /// <returns>The list of Files in the directory.</returns>
        /// <exception cref="ArgumentException">If <paramref name="directory"/> does not exist.</exception>
        public string[] ListFiles(string directory)
        {
            directory = PrepareDirectory(directory);

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);

            if(!DirectoryExists(connection, directory)) {
                CloseConnection(connection);
                throw new ArgumentException("Directory does not exist", "directory");
            }

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("File", new string[] { "Name" });
            query = queryBuilder.Where(query, "Directory", WhereOperator.Equals, "Directory");
            query = queryBuilder.OrderBy(query, new [] { "Name" }, new[] { Ordering.Asc });

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Directory", directory));

            DbCommand command = builder.GetCommand(connection, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<string> result = new List<string>(20);

                while(reader.Read()) {
                    result.Add(directory + reader["Name"] as string);
                }

                CloseReader(command, reader);

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Renames or moves a File.
        /// </summary>
        /// <param name="oldFullName">The old full name of the File.</param>
        /// <param name="newFullName">The new full name of the File.</param>
        /// <returns><c>true</c> if the File is renamed, <c>false</c> otherwise.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="oldFullName"/> or <paramref name="newFullName"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="oldFullName"/> or <paramref name="newFullName"/> are empty, or if the old file does not exist, or if the new file already exist.</exception>
        public bool RenameFile(string oldFullName, string newFullName)
        {
            if(oldFullName == null) throw new ArgumentNullException("oldFullName");
            if(oldFullName.Length == 0) throw new ArgumentException("Old Full Name cannot be empty", "oldFullName");
            if(newFullName == null) throw new ArgumentNullException("newFullName");
            if(newFullName.Length == 0) throw new ArgumentException("New Full Name cannot be empty", "newFullName");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            if(!FileExists(transaction, oldFullName)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("File does not exist", "oldFullName");
            }
            if(FileExists(transaction, newFullName)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("File already exists", "newFullPath");
            }

            string oldDirectory, newDirectory, oldFilename, newFilename;
            SplitFileFullName(oldFullName, out oldDirectory, out oldFilename);
            SplitFileFullName(newFullName, out newDirectory, out newFilename);

            string query = queryBuilder.Update("File", new string[] { "Name" }, new string[] { "NewName" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "OldName");
            query = queryBuilder.AndWhere(query, "Directory", WhereOperator.Equals, "OldDirectory");

            List<Parameter> parameters = new List<Parameter>(3);
            parameters.Add(new Parameter(ParameterType.String, "NewName", newFilename));
            parameters.Add(new Parameter(ParameterType.String, "OldName", oldFilename));
            parameters.Add(new Parameter(ParameterType.String, "OldDirectory", oldDirectory));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);
            if(rows == 1) CommitTransaction(transaction);
            else RollbackTransaction(transaction);

            return rows == 1;
        }
        /// <summary>
        /// Renames or moves a Directory.
        /// </summary>
        /// <param name="transaction">The current transaction to use.</param>
        /// <param name="oldFullPath">The old full path of the Directory.</param>
        /// <param name="newFullPath">The new full path of the Directory.</param>
        /// <returns><c>true</c> if the Directory is renamed, <c>false</c> otherwise.</returns>
        private bool RenameDirectory(DbTransaction transaction, string oldFullPath, string newFullPath)
        {
            string[] directories = ListDirectories(transaction, oldFullPath);
            foreach(string dir in directories) {
                string trimmed = dir.Trim('/');
                string name = trimmed.Substring(trimmed.LastIndexOf("/") + 1);

                string newFullPathSub = PrepareDirectory(newFullPath + name);

                RenameDirectory(dir, newFullPathSub);
            }

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.Update("Directory", new string[] { "FullPath" }, new string[] { "NewDirectory1" });
            query = queryBuilder.Where(query, "FullPath", WhereOperator.Equals, "OldDirectory1");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "NewDirectory1", newFullPath));
            parameters.Add(new Parameter(ParameterType.String, "OldDirectory1", oldFullPath));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            return rows > 0;
        }
        /// <summary>
        /// Gets the details of a page attachment.
        /// </summary>
        /// <param name="pageInfo">The page that owns the attachment.</param>
        /// <param name="name">The name of the attachment, for example "myfile.jpg".</param>
        /// <returns>The details of the attachment, or <c>null</c> if the attachment does not exist.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="pageInfo"/> or <paramref name="name"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="name"/> is empty.</exception>
        public FileDetails GetPageAttachmentDetails(PageInfo pageInfo, string name)
        {
            if(pageInfo == null) throw new ArgumentNullException("pageInfo");
            if(name == null) throw new ArgumentNullException("name");
            if(name.Length == 0) throw new ArgumentException("Name cannot be empty");

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("Attachment", new string[] { "Size", "Downloads", "LastModified" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", name));
            parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                FileDetails details = null;

                if(reader.Read()) {
                    details = new FileDetails((long)reader["Size"],
                        (DateTime)reader["LastModified"], (int)reader["Downloads"]);
                }

                CloseReader(command, reader);

                return details;
            }
            else return null;
        }
        /// <summary>
        /// Gets all the categories of a page.
        /// </summary>
        /// <param name="page">The page.</param>
        /// <returns>The categories, sorted by name.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception>
        public CategoryInfo[] GetCategoriesForPage(PageInfo page)
        {
            if(page == null) throw new ArgumentNullException("page");

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string nspace, pageName;
            NameTools.ExpandFullName(page.FullName, out nspace, out pageName);
            if(nspace == null) nspace = "";

            string query = queryBuilder.SelectFrom("Category", "CategoryBinding", new string[] { "Name", "Namespace" }, new string[] { "Category", "Namespace" }, Join.LeftJoin,
                new string[] { "Name", "Namespace" }, new string[] { "Page" });
            query = queryBuilder.Where(query, "CategoryBinding", "Namespace", WhereOperator.Equals, "Namespace");
            query = queryBuilder.AndWhere(query, "CategoryBinding", "Page", WhereOperator.Equals, "Page");
            query = queryBuilder.OrderBy(query, new[] { "Category_Name" }, new[] { Ordering.Asc });

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
            parameters.Add(new Parameter(ParameterType.String, "Page", pageName));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<CategoryInfo> result = new List<CategoryInfo>(20);
                List<string> pages = new List<string>(50);

                string prevName = "|||";
                string name = null;

                while(reader.Read()) {
                    name = reader["Category_Name"] as string;

                    if(name != prevName) {
                        if(prevName != "|||") {
                            result[result.Count - 1].Pages = pages.ToArray();
                            pages.Clear();
                        }

                        result.Add(new CategoryInfo(NameTools.GetFullName(reader["Category_Namespace"] as string, name), this));
                    }

                    prevName = name;
                    if(!IsDBNull(reader, "CategoryBinding_Page")) {
                        pages.Add(NameTools.GetFullName(reader["Category_Namespace"] as string, reader["CategoryBinding_Page"] as string));
                    }
                }

                CloseReader(command, reader);

                if(result.Count > 0) result[result.Count - 1].Pages = pages.ToArray();

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Gets the total number of Messages in a Page Discussion.
        /// </summary>
        /// <param name="page">The Page.</param>
        /// <returns>The number of messages.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception>
        public int GetMessageCount(PageInfo page)
        {
            if(page == null) throw new ArgumentNullException("page");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);

            if(GetPage(connection, page.FullName) == null) {
                CloseConnection(connection);
                return -1;
            }

            string nspace, name;
            NameTools.ExpandFullName(page.FullName, out nspace, out name);
            if(nspace == null) nspace = "";

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectCountFrom("Message");
            query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
            query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Page", name));
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));

            DbCommand command = builder.GetCommand(connection, query, parameters);

            int count = ExecuteScalar<int>(command, 0);

            return count;
        }
        /// <summary>
        /// Tries to load all data related to a word from the database.
        /// </summary>
        /// <param name="text">The word text.</param>
        /// <param name="word">The returned word.</param>
        /// <param name="connection">An open database connection.</param>
        /// <returns><c>true</c> if the word is found, <c>false</c> otherwise.</returns>
        private bool TryFindWord(string text, out Word word, DbConnection connection)
        {
            // 1. Find word - if not found, return
            // 2. Read all raw word mappings
            // 3. Read all documents (unique)
            // 4. Build result data structure

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("IndexWord", new string[] { "Id" });
            query = queryBuilder.Where(query, "Text", WhereOperator.Equals, "Text");

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Text", text));

            DbCommand command = builder.GetCommand(connection, query, parameters);

            int wordId = ExecuteScalar<int>(command, -1, false);

            if(wordId == -1) {
                word = null;
                return false;
            }

            // Read all raw mappings
            query = queryBuilder.SelectFrom("IndexWordMapping");
            query = queryBuilder.Where(query, "Word", WhereOperator.Equals, "WordId");

            parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.Int32, "WordId", wordId));

            command = builder.GetCommand(connection, query, parameters);

            DbDataReader reader = ExecuteReader(command, false);

            List<DumpedWordMapping> mappings = new List<DumpedWordMapping>(2048);
            while(reader != null && reader.Read()) {
                mappings.Add(new DumpedWordMapping((uint)wordId,
                    (uint)(int)reader["Document"],
                    (ushort)(short)reader["FirstCharIndex"], (ushort)(short)reader["WordIndex"],
                    (byte)reader["Location"]));
            }
            CloseReader(reader);

            if(mappings.Count == 0) {
                word = null;
                return false;
            }

            // Find all documents
            query = queryBuilder.SelectFrom("IndexDocument");
            query = queryBuilder.Where(query, "Id", WhereOperator.Equals, "DocId");

            parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.Int32, "DocId", 0));

            Dictionary<uint, IDocument> documents = new Dictionary<uint, IDocument>(64);
            foreach(DumpedWordMapping map in mappings) {
                uint docId = map.DocumentID;
                if(documents.ContainsKey(docId)) continue;

                parameters[0].Value = (int)docId;
                command = builder.GetCommand(connection, query, parameters);

                reader = ExecuteReader(command, false);

                if(reader != null && reader.Read()) {
                    DumpedDocument dumpedDoc = new DumpedDocument(docId,
                        reader["Name"] as string, reader["Title"] as string,
                        reader["TypeTag"] as string,
                        (DateTime)reader["DateTime"]);

                    IDocument document = BuildDocument(dumpedDoc);

                    if(document != null) documents.Add(docId, document);
                }
                CloseReader(reader);
            }

            OccurrenceDictionary occurrences = new OccurrenceDictionary(mappings.Count);
            foreach(DumpedWordMapping map in mappings) {
                if(!occurrences.ContainsKey(documents[map.DocumentID])) {
                    occurrences.Add(documents[map.DocumentID], new SortedBasicWordInfoSet(2));
                }

                occurrences[documents[map.DocumentID]].Add(new BasicWordInfo(
                    map.FirstCharIndex, map.WordIndex, WordLocation.GetInstance(map.Location)));
            }

            word = new Word((uint)wordId, text, occurrences);
            return true;
        }
        /// <summary>
        /// Deletes the Backups of a Page, up to a specified revision.
        /// </summary>
        /// <param name="page">The Page to delete the backups of.</param>
        /// <param name="revision">The newest revision to delete (newer revision are kept) o -1 to delete all the Backups.</param>
        /// <returns><c>true</c> if the deletion succeeded, <c>false</c> otherwise.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="page"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentOutOfRangeException">If <paramref name="revision"/> is less than -1.</exception>
        public bool DeleteBackups(PageInfo page, int revision)
        {
            if(page == null) throw new ArgumentNullException("page");
            if(revision < -1) throw new ArgumentOutOfRangeException("revision", "Invalid Revision");

            // 1. Retrieve target content (revision-1 = first kept revision)
            // 2. Replace the current content (delete, store)
            // 3. Delete all older revisions up to the specified on (included) "N-m...N"
            // 4. Re-number remaining revisions starting from FirstRevision (zero) to revision-1 (don't re-number revs -1, -100)

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            if(GetPage(transaction, page.FullName) == null) {
                RollbackTransaction(transaction);
                return false;
            }

            int[] baks = GetBackups(transaction, page);
            if(baks.Length > 0 && revision > baks[baks.Length - 1]) {
                RollbackTransaction(transaction);
                return true;
            }

            string nspace, name;
            NameTools.ExpandFullName(page.FullName, out nspace, out name);
            if(nspace == null) nspace = "";

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.DeleteFrom("PageContent");
            query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
            query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
            if(revision != -1) query = queryBuilder.AndWhere(query, "Revision", WhereOperator.LessThanOrEqualTo, "Revision");
            query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "FirstRevision");

            List<Parameter> parameters = new List<Parameter>(4);
            parameters.Add(new Parameter(ParameterType.String, "Page", name));
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
            if(revision != -1) parameters.Add(new Parameter(ParameterType.Int16, "Revision", revision));
            parameters.Add(new Parameter(ParameterType.Int16, "FirstRevision", FirstRevision));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            if(rows == -1) {
                RollbackTransaction(transaction);
                return false;
            }

            if(revision != -1) {
                int revisionDelta = revision + 1;

                query = queryBuilder.UpdateIncrement("PageContent", "Revision", -revisionDelta);
                query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
                query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
                query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "FirstRevision");

                parameters = new List<Parameter>(3);
                parameters.Add(new Parameter(ParameterType.String, "Page", name));
                parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
                parameters.Add(new Parameter(ParameterType.Int16, "FirstRevision", FirstRevision));

                command = builder.GetCommand(transaction, query, parameters);

                rows = ExecuteNonQuery(command, false);

                if(rows > 0) CommitTransaction(transaction);
                else RollbackTransaction(transaction);

                return rows >= 0;
            }
            else {
                CommitTransaction(transaction);
                return true;
            }
        }
        /// <summary>
        /// Stores the content for a revision.
        /// </summary>
        /// <param name="transaction">A database transaction.</param>
        /// <param name="content">The content.</param>
        /// <param name="revision">The revision.</param>
        /// <returns><c>true</c> if the content is stored, <c>false</c> otherwise.</returns>
        private bool SetContent(DbTransaction transaction, PageContent content, int revision)
        {
            string name, nspace;
            NameTools.ExpandFullName(content.PageInfo.FullName, out nspace, out name);
            if(nspace == null) nspace = "";

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.InsertInto("PageContent",
                new string[] { "Page", "Namespace", "Revision", "Title", "User", "LastModified", "Comment", "Content", "Description" },
                new string[] { "Page", "Namespace", "Revision", "Title", "User", "LastModified", "Comment", "Content", "Description" });

            List<Parameter> parameters = new List<Parameter>(9);
            parameters.Add(new Parameter(ParameterType.String, "Page", name));
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
            parameters.Add(new Parameter(ParameterType.Int16, "Revision", revision));
            parameters.Add(new Parameter(ParameterType.String, "Title", content.Title));
            parameters.Add(new Parameter(ParameterType.String, "User", content.User));
            parameters.Add(new Parameter(ParameterType.DateTime, "LastModified", content.LastModified));
            if(!string.IsNullOrEmpty(content.Comment)) parameters.Add(new Parameter(ParameterType.String, "Comment", content.Comment));
            else parameters.Add(new Parameter(ParameterType.String, "Comment", DBNull.Value));
            parameters.Add(new Parameter(ParameterType.String, "Content", content.Content));
            if(!string.IsNullOrEmpty(content.Description)) parameters.Add(new Parameter(ParameterType.String, "Description", content.Description));
            else parameters.Add(new Parameter(ParameterType.String, "Description", DBNull.Value));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            if(rows != 1) return false;

            if(content.Keywords.Length > 0) {
                parameters = new List<Parameter>(content.Keywords.Length * 4);
                string fullQuery = "";
                int count = 0;
                string countString;
                foreach(string kw in content.Keywords) {
                    countString = count.ToString();

                    query = queryBuilder.InsertInto("PageKeyword", new string[] { "Page", "Namespace", "Revision", "Keyword" },
                        new string[] { "Page" + countString, "Namespace" + countString, "Revision" + countString, "Keyword" + countString });
                    fullQuery = queryBuilder.AppendForBatch(fullQuery, query);

                    parameters.Add(new Parameter(ParameterType.String, "Page" + countString, name));
                    parameters.Add(new Parameter(ParameterType.String, "Namespace" + countString, nspace));
                    parameters.Add(new Parameter(ParameterType.Int16, "Revision" + countString, revision));
                    parameters.Add(new Parameter(ParameterType.String, "Keyword" + countString, kw));

                    count++;
                }

                command = builder.GetCommand(transaction, fullQuery, parameters);

                rows = ExecuteNonQuery(command, false);

                return rows == content.Keywords.Length;
            }
            else return true;
        }
        /// <summary>
        /// Saves data for a new document.
        /// </summary>
        /// <param name="document">The document.</param>
        /// <param name="content">The content words.</param>
        /// <param name="title">The title words.</param>
        /// <param name="keywords">The keywords.</param>
        /// <param name="state">A state object passed from the index (can be <c>null</c> or a <see cref="T:DbTransaction" />).</param>
        /// <returns>The number of stored occurrences.</returns>
        private int SaveDataForDocument(IDocument document, WordInfo[] content, WordInfo[] title, WordInfo[] keywords, object state)
        {
            // 1. Insert document
            // 2. Insert all new words
            // 3. Load all word IDs
            // 4. Insert mappings

            // On error, return without rolling back if state != null, rollback otherwise
            // On completion, commit if state == null

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            DbTransaction transaction = null;
            if(state != null) transaction = (DbTransaction)state;
            else {
                DbConnection connection = builder.GetConnection(connString);
                transaction = BeginTransaction(connection);
            }

            uint freeDocumentId = GetFreeElementId(IndexElementType.Documents, transaction);
            uint freeWordId = GetFreeElementId(IndexElementType.Words, transaction);

            // Insert the document
            string query = queryBuilder.InsertInto("IndexDocument",
                new string[] { "Id", "Name", "Title", "TypeTag", "DateTime" },
                new string[] { "Id", "Name", "Title", "TypeTag", "DateTime" });

            List<Parameter> parameters = new List<Parameter>(5);
            parameters.Add(new Parameter(ParameterType.Int32, "Id", (int)freeDocumentId));
            parameters.Add(new Parameter(ParameterType.String, "Name", document.Name));
            parameters.Add(new Parameter(ParameterType.String, "Title", document.Title));
            parameters.Add(new Parameter(ParameterType.String, "TypeTag", document.TypeTag));
            parameters.Add(new Parameter(ParameterType.DateTime, "DateTime", document.DateTime));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            if(ExecuteNonQuery(command, false) != 1) {
                if(state == null) RollbackTransaction(transaction);
                return -1;
            }
            document.ID = freeDocumentId;

            List<WordInfo> allWords = new List<WordInfo>(content.Length + title.Length + keywords.Length);
            allWords.AddRange(content);
            allWords.AddRange(title);
            allWords.AddRange(keywords);

            List<WordInfo> existingWords = new List<WordInfo>(allWords.Count / 2);

            Dictionary<string, uint> wordIds = new Dictionary<string, uint>(1024);

            // Try to blindly insert all words (assumed to be lowercase and clean from diacritics)

            query = queryBuilder.InsertInto("IndexWord", new string[] { "Id", "Text" }, new string[] { "Id", "Text" });

            parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.Int32, "Id", 0));
            parameters.Add(new Parameter(ParameterType.String, "Text", ""));

            foreach(WordInfo word in allWords) {
                parameters[0].Value = (int)freeWordId;
                parameters[1].Value = word.Text;

                command = builder.GetCommand(transaction, query, parameters);

                if(ExecuteNonQuery(command, false, false) == 1) {
                    wordIds.Add(word.Text, freeWordId);
                    freeWordId++;
                }
                else {
                    existingWords.Add(word);
                }
            }

            // Load IDs of all existing words
            query = queryBuilder.SelectFrom("IndexWord", new string[] { "Id" });
            query = queryBuilder.Where(query, "Text", WhereOperator.Equals, "Text");

            parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Text", ""));

            foreach(WordInfo word in existingWords) {
                parameters[0].Value = word.Text;

                command = builder.GetCommand(transaction, query, parameters);

                int id = ExecuteScalar<int>(command, -1, false);
                if(id == -1) {
                    if(state == null) RollbackTransaction(transaction);
                    return -1;
                }

                if(!wordIds.ContainsKey(word.Text)) {
                    wordIds.Add(word.Text, (uint)id);
                }
                else if(wordIds[word.Text] != (uint)id) throw new InvalidOperationException("Word ID mismatch");
            }

            // Insert all mappings
            query = queryBuilder.InsertInto("IndexWordMapping",
                new string[] { "Word", "Document", "FirstCharIndex", "WordIndex", "Location" },
                new string[] { "Word", "Document", "FirstCharIndex", "WordIndex", "Location" });

            parameters = new List<Parameter>(5);
            parameters.Add(new Parameter(ParameterType.Int32, "Word", 0));
            parameters.Add(new Parameter(ParameterType.Int32, "Document", (int)freeDocumentId));
            parameters.Add(new Parameter(ParameterType.Int16, "FirstCharIndex", 0));
            parameters.Add(new Parameter(ParameterType.Int16, "WordIndex", 0));
            parameters.Add(new Parameter(ParameterType.Byte, "Location", 0));

            foreach(WordInfo word in allWords) {
                parameters[0].Value = (int)wordIds[word.Text];
                parameters[1].Value = (int)freeDocumentId;
                parameters[2].Value = (short)word.FirstCharIndex;
                parameters[3].Value = (short)word.WordIndex;
                parameters[4].Value = word.Location.Location;

                command = builder.GetCommand(transaction, query, parameters);

                if(ExecuteNonQuery(command, false) != 1) {
                    if(state == null) RollbackTransaction(transaction);
                    return -1;
                }
            }

            if(state == null) CommitTransaction(transaction);

            return allWords.Count;
        }
        /// <summary>
        /// Removes a new Snippet.
        /// </summary>
        /// <param name="connection">A database connection.</param>
        /// <param name="name">The Name of the Snippet to remove.</param>
        /// <returns><c>true</c> if the snippet is removed, <c>false</c> otherwise.</returns>
        private bool RemoveSnippet(DbConnection connection, string name)
        {
            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.DeleteFrom("Snippet");
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Name", name));

            DbCommand command = builder.GetCommand(connection, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            return rows == 1;
        }
        /// <summary>
        /// Deletes a File.
        /// </summary>
        /// <param name="fullName">The full name of the File.</param>
        /// <returns><c>true</c> if the File is deleted, <c>false</c> otherwise.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="fullName"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="fullName"/> is empty or it does not exist.</exception>
        public bool DeleteFile(string fullName)
        {
            if(fullName == null) throw new ArgumentNullException("fullName");
            if(fullName.Length == 0) throw new ArgumentException("Full Name cannot be empty", "fullName");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            if(!FileExists(transaction, fullName)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("File does not exist", "fullName");
            }

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string directory, filename;
            SplitFileFullName(fullName, out directory, out filename);

            string query = queryBuilder.DeleteFrom("File");
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Directory", WhereOperator.Equals, "Directory");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", filename));
            parameters.Add(new Parameter(ParameterType.String, "Directory", directory));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);
            if(rows == 1) CommitTransaction(transaction);
            else RollbackTransaction(transaction);

            return rows == 1;
        }
        /// <summary>
        /// Gets all the sub-namespaces.
        /// </summary>
        /// <returns>The sub-namespaces, sorted by name.</returns>
        public NamespaceInfo[] GetNamespaces()
        {
            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            // select ... from Namespace left join Page on Namespace.DefaultPage = Page.Name where Namespace.Name <> '' and (Namespace.DefaultPage is null or Page.Namespace <> '')
            string query = queryBuilder.SelectFrom("Namespace", "Page", "DefaultPage", "Name", Join.LeftJoin, new string[] { "Name", "DefaultPage" }, new string[] { "CreationDateTime" });
            query = queryBuilder.Where(query, "Namespace", "Name", WhereOperator.NotEquals, "Empty1");
            query = queryBuilder.AndWhere(query, "Namespace", "DefaultPage", WhereOperator.IsNull, null, true, false);
            query = queryBuilder.OrWhere(query, "Page", "Namespace", WhereOperator.NotEquals, "Empty2", false, true);
            query = queryBuilder.OrderBy(query, new[] { "Namespace_Name" }, new[] { Ordering.Asc });

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Empty1", ""));
            parameters.Add(new Parameter(ParameterType.String, "Empty2", ""));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<NamespaceInfo> result = new List<NamespaceInfo>(10);

                while(reader.Read()) {
                    string realName = reader["Namespace_Name"] as string;
                    string page = GetNullableColumn<string>(reader, "Namespace_DefaultPage", null);
                    PageInfo defaultPage = string.IsNullOrEmpty(page) ? null :
                        new PageInfo(NameTools.GetFullName(realName, page), this, (DateTime)reader["Page_CreationDateTime"]);

                    // The query returns duplicate entries if the main page of two or more namespaces have the same name
                    if(result.Find(n => { return n.Name.Equals(realName); }) == null) {
                        result.Add(new NamespaceInfo(realName, this, defaultPage));
                    }
                }

                CloseReader(command, reader);

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Returns the names of the Attachments of a Page.
        /// </summary>
        /// <param name="connection">A database connection.</param>
        /// <param name="pageInfo">The Page Info object that owns the Attachments.</param>
        /// <returns>The names, or an empty list.</returns>
        private string[] ListPageAttachments(DbConnection connection, PageInfo pageInfo)
        {
            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("Attachment", new string[] { "Name" });
            query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));
            query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { Ordering.Asc });

            DbCommand command = builder.GetCommand(connection, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<string> result = new List<string>(10);

                while(reader.Read()) {
                    result.Add(reader["Name"] as string);
                }

                CloseReader(reader);

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Gets all the Navigation Paths in a Namespace.
        /// </summary>
        /// <param name="nspace">The Namespace.</param>
        /// <returns>All the Navigation Paths, sorted by name.</returns>
        public NavigationPath[] GetNavigationPaths(NamespaceInfo nspace)
        {
            string nspaceName = nspace != null ? nspace.Name : "";

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("NavigationPath", new string[] { "Name", "Namespace", "Page" });
            query = queryBuilder.Where(query, "Namespace", WhereOperator.Equals, "Namespace");
            query = queryBuilder.OrderBy(query, new string[] { "Namespace", "Name", "Number" }, new Ordering[] { Ordering.Asc, Ordering.Asc, Ordering.Asc });

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspaceName));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<NavigationPath> result = new List<NavigationPath>(10);

                string prevName = "|||";
                string name;
                string actualNamespace = "";
                List<string> pages = new List<string>(10);

                while(reader.Read()) {
                    name = reader["Name"] as string;

                    if(name != prevName) {
                        actualNamespace = reader["Namespace"] as string;

                        if(prevName != "|||") {
                            result[result.Count - 1].Pages = pages.ToArray();
                            pages.Clear();
                        }

                        result.Add(new NavigationPath(NameTools.GetFullName(actualNamespace, name), this));
                    }

                    prevName = name;
                    pages.Add(NameTools.GetFullName(actualNamespace, reader["Page"] as string));
                }

                if(result.Count > 0) {
                    result[result.Count - 1].Pages = pages.ToArray();
                }

                CloseReader(command, reader);

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Gets the details of a file.
        /// </summary>
        /// <param name="fullName">The full name of the file.</param>
        /// <returns>The details, or <c>null</c> if the file does not exist.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="fullName"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="fullName"/> is empty.</exception>
        public FileDetails GetFileDetails(string fullName)
        {
            if(fullName == null) throw new ArgumentNullException("fullName");
            if(fullName.Length == 0) throw new ArgumentException("Full Name cannot be empty", "fullName");

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string directory, filename;
            SplitFileFullName(fullName, out directory, out filename);

            string query = queryBuilder.SelectFrom("File", new string[] { "Size", "Downloads", "LastModified" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Directory", WhereOperator.Equals, "Directory");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", filename));
            parameters.Add(new Parameter(ParameterType.String, "Directory", directory));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                FileDetails details = null;

                if(reader.Read()) {
                    details = new FileDetails((long)reader["Size"],
                        (DateTime)reader["LastModified"], (int)reader["Downloads"]);
                }

                CloseReader(command, reader);

                return details;
            }
            else return null;
        }
        /// <summary>
        /// Determines whether a page attachment exists.
        /// </summary>
        /// <param name="transaction">A database transaction.</param>
        /// <param name="page">The page.</param>
        /// <param name="name">The attachment.</param>
        /// <returns><c>true</c> if the attachment exists, <c>false</c> otherwise.</returns>
        private bool AttachmentExists(DbTransaction transaction, PageInfo page, string name)
        {
            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectCountFrom("Attachment");
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", name));
            parameters.Add(new Parameter(ParameterType.String, "Page", page.FullName));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int count = ExecuteScalar<int>(command, -1, false);

            return count == 1;
        }
        /// <summary>
        /// The the names of the pages with attachments.
        /// </summary>
        /// <returns>The names of the pages with attachments.</returns>
        public string[] GetPagesWithAttachments()
        {
            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("Attachment", new string[] { "Page" });
            query = queryBuilder.GroupBy(query, new[] { "Page" });
            query = queryBuilder.OrderBy(query, new[] { "Page" }, new[] { Ordering.Asc });

            DbCommand command = builder.GetCommand(connString, query, new List<Parameter>());

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<string> result = new List<string>(100);

                while(reader.Read()) {
                    result.Add(reader["Page"] as string);
                }

                CloseReader(command, reader);

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Deletes a directory and all its contents.
        /// </summary>
        /// <param name="transaction">The current transaction to use.</param>
        /// <param name="fullPath">The full path of the directory.</param>
        /// <returns><c>true</c> if the directory is deleted, <c>false</c> otherwise.</returns>
        private bool DeleteDirectory(DbTransaction transaction, string fullPath)
        {
            string[] dirs = ListDirectories(transaction, fullPath);
            foreach(string dir in dirs) {
                if(!DeleteDirectory(transaction, dir)) {
                    return false;
                }
            }

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.DeleteFrom("Directory");
            query = queryBuilder.Where(query, "FullPath", WhereOperator.Equals, "FullPath");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "FullPath", fullPath));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            return rows > 0;
        }
        /// <summary>
        /// Notifies the Provider that a Page has been renamed.
        /// </summary>
        /// <param name="oldPage">The old Page Info object.</param>
        /// <param name="newPage">The new Page Info object.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="oldPage"/> or <paramref name="newPage"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If the new page is already in use.</exception>
        public void NotifyPageRenaming(PageInfo oldPage, PageInfo newPage)
        {
            if(oldPage == null) throw new ArgumentNullException("oldPage");
            if(newPage == null) throw new ArgumentNullException("newPage");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            if(ListPageAttachments(transaction, newPage).Length > 0) {
                RollbackTransaction(transaction);
                throw new ArgumentException("New Page already exists", "newPage");
            }

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.Update("Attachment", new string[] { "Page" }, new string[] { "NewPage" });
            query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "OldPage");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "NewPage", newPage.FullName));
            parameters.Add(new Parameter(ParameterType.String, "OldPage", oldPage.FullName));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            if(rows != -1) CommitTransaction(transaction);
            else RollbackTransaction(transaction);
        }
        /// <summary>
        /// Determines whether a directory exists.
        /// </summary>
        /// <param name="transaction">A database transaction.</param>
        /// <param name="directory">The directory, for example "/my/directory".</param>
        /// <returns><c>true</c> if the directory exists, <c>false</c> otherwise.</returns>
        /// <remarks>The root directory always exists.</remarks>
        private bool DirectoryExists(DbTransaction transaction, string directory)
        {
            directory = PrepareDirectory(directory);

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectCountFrom("Directory");
            query = queryBuilder.Where(query, "FullPath", WhereOperator.Equals, "FullPath");

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "FullPath", directory));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int count = ExecuteScalar<int>(command, -1, false);

            return count == 1;
        }
        /// <summary>
        /// Renames a Page Attachment.
        /// </summary>
        /// <param name="pageInfo">The Page Info that owns the Attachment.</param>
        /// <param name="oldName">The old name of the Attachment.</param>
        /// <param name="newName">The new name of the Attachment.</param>
        /// <returns><c>true</c> if the Attachment is renamed, false otherwise.</returns>
        /// <exception cref="ArgumentNullException">If <paramref name="pageInfo"/>, <paramref name="oldName"/> or <paramref name="newName"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="pageInfo"/>, <paramref name="oldName"/> or <paramref name="newName"/> are empty,
        /// or if the page or old attachment do not exist, or the new attachment name already exists.</exception>
        public bool RenamePageAttachment(PageInfo pageInfo, string oldName, string newName)
        {
            if(pageInfo == null) throw new ArgumentNullException("pageInfo");
            if(oldName == null) throw new ArgumentNullException("oldName");
            if(oldName.Length == 0) throw new ArgumentException("Old Name cannot be empty", "oldName");
            if(newName == null) throw new ArgumentNullException("newName");
            if(newName.Length == 0) throw new ArgumentException("New Name cannot be empty", "newName");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            if(!AttachmentExists(transaction, pageInfo, oldName)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("Attachment does not exist", "name");
            }
            if(AttachmentExists(transaction, pageInfo, newName)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("Attachment already exists", "name");
            }

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.Update("Attachment", new string[] { "Name" }, new string[] { "NewName" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "OldName");
            query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

            List<Parameter> parameters = new List<Parameter>(3);
            parameters.Add(new Parameter(ParameterType.String, "NewName", newName));
            parameters.Add(new Parameter(ParameterType.String, "OldName", oldName));
            parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);
            if(rows == 1) CommitTransaction(transaction);
            else RollbackTransaction(transaction);

            return rows == 1;
        }
        /// <summary>
        /// Determines whether a file exists.
        /// </summary>
        /// <param name="transaction">A database transaction.</param>
        /// <param name="fullName">The file full name, for example "/file.txt" or "/directory/file.txt".</param>
        /// <returns><c>true</c> if the file exists, <c>false</c> otherwise.</returns>
        private bool FileExists(DbTransaction transaction, string fullName)
        {
            string directory, file;
            SplitFileFullName(fullName, out directory, out file);

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectCountFrom("File");
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Directory", WhereOperator.Equals, "Directory");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", file));
            parameters.Add(new Parameter(ParameterType.String, "Directory", directory));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int count = ExecuteScalar<int>(command, -1, false);

            return count == 1;
        }
        /// <summary>
        /// Sets the number of times a file was retrieved.
        /// </summary>
        /// <param name="fullName">The full name of the file.</param>
        /// <param name="count">The count to set.</param>
        /// <exception cref="ArgumentNullException">If <paramref name="fullName"/> is <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="fullName"/> is empty.</exception>
        /// <exception cref="ArgumentOutOfRangeException">If <paramref name="count"/> is less than zero.</exception>
        public void SetFileRetrievalCount(string fullName, int count)
        {
            if(fullName == null) throw new ArgumentNullException("fullName");
            if(fullName.Length == 0) throw new ArgumentException("Full Name cannot be empty", "fullName");
            if(count < 0) throw new ArgumentOutOfRangeException("count", "Count must be greater than or equal to zero");

            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string directory, filename;
            SplitFileFullName(fullName, out directory, out filename);

            string query = queryBuilder.Update("File", new string[] { "Downloads" }, new string[] { "Downloads" });
            query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
            query = queryBuilder.AndWhere(query, "Directory", WhereOperator.Equals, "Directory");

            List<Parameter> parameters = new List<Parameter>(2);
            parameters.Add(new Parameter(ParameterType.String, "Name", filename));
            parameters.Add(new Parameter(ParameterType.String, "Directory", directory));
            parameters.Add(new Parameter(ParameterType.Int32, "Downloads", count));

            DbCommand command = builder.GetCommand(connString, query, parameters);

            ExecuteNonQuery(command);
        }
        /// <summary>
        /// Lists the Directories in the specified directory.
        /// </summary>
        /// <param name="transaction">A database transaction.</param>
        /// <param name="directory">The full directory name, for example "/my/directory". Null, empty or "/" for the root directory.</param>
        /// <returns>The list of Directories in the Directory.</returns>
        private string[] ListDirectories(DbTransaction transaction, string directory)
        {
            directory = PrepareDirectory(directory);

            ICommandBuilder builder = GetCommandBuilder();

            if(!DirectoryExists(transaction, directory)) {
                RollbackTransaction(transaction);
                throw new ArgumentException("Directory does not exist", "directory");
            }

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.SelectFrom("Directory", new string[] { "FullPath" });
            query = queryBuilder.Where(query, "Parent", WhereOperator.Equals, "Parent");

            List<Parameter> parameters = new List<Parameter>(1);
            parameters.Add(new Parameter(ParameterType.String, "Parent", directory));
            query = queryBuilder.OrderBy(query, new[] { "FullPath" }, new[] { Ordering.Asc });

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            DbDataReader reader = ExecuteReader(command);

            if(reader != null) {
                List<string> result = new List<string>(20);

                while(reader.Read()) {
                    result.Add(reader["FullPath"] as string);
                }

                CloseReader(reader);

                return result.ToArray();
            }
            else return null;
        }
        /// <summary>
        /// Stores a Page Attachment.
        /// </summary>
        /// <param name="pageInfo">The Page Info that owns the Attachment.</param>
        /// <param name="name">The name of the Attachment, for example "myfile.jpg".</param>
        /// <param name="sourceStream">A Stream object used as <b>source</b> of a byte stream,
        /// i.e. the method reads from the Stream and stores the content properly.</param>
        /// <param name="overwrite"><c>true</c> to overwrite an existing Attachment.</param>
        /// <returns><c>true</c> if the Attachment is stored, <c>false</c> otherwise.</returns>
        /// <remarks>If <b>overwrite</b> is <c>false</c> and Attachment already exists, the method returns <c>false</c>.</remarks>
        /// <exception cref="ArgumentNullException">If <paramref name="pageInfo"/>, <paramref name="name"/> or <paramref name="sourceStream"/> are <c>null</c>.</exception>
        /// <exception cref="ArgumentException">If <paramref name="name"/> is empty or if <paramref name="sourceStream"/> does not support reading.</exception>
        public bool StorePageAttachment(PageInfo pageInfo, string name, System.IO.Stream sourceStream, bool overwrite)
        {
            if(pageInfo == null) throw new ArgumentNullException("pageInfo");
            if(name == null) throw new ArgumentNullException("name");
            if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name");
            if(sourceStream == null) throw new ArgumentNullException("sourceStream");
            if(!sourceStream.CanRead) throw new ArgumentException("Cannot read from Source Stream", "sourceStream");

            ICommandBuilder builder = GetCommandBuilder();
            DbConnection connection = builder.GetConnection(connString);
            DbTransaction transaction = BeginTransaction(connection);

            bool attachmentExists = AttachmentExists(transaction, pageInfo, name);

            if(attachmentExists && !overwrite) {
                RollbackTransaction(transaction);
                return false;
            }

            // To achieve decent performance, an UPDATE query is issued if the attachment exists,
            // otherwise an INSERT query is issued

            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query;
            List<Parameter> parameters;

            byte[] attachmentData = null;
            int size = Tools.ReadStream(sourceStream, ref attachmentData, MaxFileSize);
            if(size < 0) {
                RollbackTransaction(transaction);
                throw new ArgumentException("Source Stream contains too much data", "sourceStream");
            }

            if(attachmentExists) {
                query = queryBuilder.Update("Attachment", new string[] { "Size", "LastModified", "Data" }, new string[] { "Size", "LastModified", "Data" });
                query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "Name");
                query = queryBuilder.AndWhere(query, "Page", WhereOperator.Equals, "Page");

                parameters = new List<Parameter>(5);
                parameters.Add(new Parameter(ParameterType.Int64, "Size", (long)size));
                parameters.Add(new Parameter(ParameterType.DateTime, "LastModified", DateTime.Now));
                parameters.Add(new Parameter(ParameterType.ByteArray, "Data", attachmentData));
                parameters.Add(new Parameter(ParameterType.String, "Name", name));
                parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));
            }
            else {
                query = queryBuilder.InsertInto("Attachment", new string[] { "Name", "Page", "Size", "Downloads", "LastModified", "Data" },
                    new string[] { "Name", "Page", "Size", "Downloads", "LastModified", "Data" });

                parameters = new List<Parameter>(6);
                parameters.Add(new Parameter(ParameterType.String, "Name", name));
                parameters.Add(new Parameter(ParameterType.String, "Page", pageInfo.FullName));
                parameters.Add(new Parameter(ParameterType.Int64, "Size", (long)size));
                parameters.Add(new Parameter(ParameterType.Int32, "Downloads", 0));
                parameters.Add(new Parameter(ParameterType.DateTime, "LastModified", DateTime.Now));
                parameters.Add(new Parameter(ParameterType.ByteArray, "Data", attachmentData));
            }

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);
            if(rows == 1) CommitTransaction(transaction);
            else RollbackTransaction(transaction);

            return rows == 1;
        }
        /// <summary>
        /// Removes a Message.
        /// </summary>
        /// <param name="transaction">A database transaction.</param>
        /// <param name="page">The Page.</param>
        /// <param name="id">The ID of the Message to remove.</param>
        /// <param name="removeReplies">A value specifying whether or not to remove the replies.</param>
        /// <returns>True if the Message is removed successfully.</returns>
        private bool RemoveMessage(DbTransaction transaction, PageInfo page, int id, bool removeReplies)
        {
            string nspace, name;
            NameTools.ExpandFullName(page.FullName, out nspace, out name);
            if(nspace == null) nspace = "";

            Message[] messages = GetMessages(transaction, page);
            if(messages == null) return false;
            Message message = FindMessage(messages, id);
            if(message == null) return false;
            Message parent = FindAnchestor(messages, id);
            int parentId = parent != null ? parent.ID : -1;

            UnindexMessage(page, message.ID, message.Subject, message.DateTime, message.Body, transaction);

            if(removeReplies) {
                // Recursively remove all replies BEFORE removing parent (depth-first)
                foreach(Message reply in message.Replies) {
                    if(!RemoveMessage(transaction, page, reply.ID, true)) return false;
                }
            }

            // Remove this message
            ICommandBuilder builder = GetCommandBuilder();
            QueryBuilder queryBuilder = new QueryBuilder(builder);

            string query = queryBuilder.DeleteFrom("Message");
            query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
            query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
            query = queryBuilder.AndWhere(query, "Id", WhereOperator.Equals, "Id");

            List<Parameter> parameters = new List<Parameter>(3);
            parameters.Add(new Parameter(ParameterType.String, "Page", name));
            parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
            parameters.Add(new Parameter(ParameterType.Int16, "Id", (short)id));

            DbCommand command = builder.GetCommand(transaction, query, parameters);

            int rows = ExecuteNonQuery(command, false);

            if(!removeReplies && rows == 1) {
                // Update replies' parent id

                query = queryBuilder.Update("Message", new string[] { "Parent" }, new string[] { "NewParent" });
                query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
                query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
                query = queryBuilder.AndWhere(query, "Parent", WhereOperator.Equals, "OldParent");

                parameters = new List<Parameter>(4);
                if(parentId != -1) parameters.Add(new Parameter(ParameterType.Int16, "NewParent", parentId));
                else parameters.Add(new Parameter(ParameterType.Int16, "NewParent", DBNull.Value));
                parameters.Add(new Parameter(ParameterType.String, "Page", name));
                parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
                parameters.Add(new Parameter(ParameterType.Int16, "OldParent", (short)id));

                command = builder.GetCommand(transaction, query, parameters);

                rows = ExecuteNonQuery(command, false);
            }

            return rows > 0;
        }