/// <summary>
		/// Retrieves the value of a Setting.
		/// </summary>
		/// <param name="name">The name of the Setting.</param>
		/// <returns>The value of the Setting, or null.</returns>
		/// <exception cref="ArgumentNullException">If <b>name</b> is <c>null</c>.</exception>
		/// <exception cref="ArgumentException">If <b>name</b> is empty.</exception>
		public string GetSetting(string name) {
			if(name == null) throw new ArgumentNullException("name");
			if(name.Length == 0) throw new ArgumentException("Name cannot be empty", "name");

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

			string query = queryBuilder.SelectFrom("Setting");
			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(connString, query, parameters);

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				string result = null;

				if(reader.Read()) {
					result = reader["Value"] as string;
				}

				CloseReader(command, reader);

				// HACK: this allows to correctly initialize a fully Oracle-based wiki instance without any user intervention
				if(string.IsNullOrEmpty(result)) {
					if(name == "DefaultUsersProvider") result = DefaultUsersStorageProvider;
					if(name == "DefaultPagesProvider") result = DefaultPagesStorageProvider;
					if(name == "DefaultFilesProvider") result = DefaultFilesStorageProvider;
				}

				return result;
			}
			else return null;
		}
		/// <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>
		/// Retrieves a plugin's assembly.
		/// </summary>
		/// <param name="filename">The file name of the assembly.</param>
		/// <returns>The assembly content, or <c>null</c>.</returns>
		/// <exception cref="ArgumentNullException">If <b>filename</b> is <c>null</c>.</exception>
		/// <exception cref="ArgumentException">If <b>filename</b> is empty.</exception>
		public byte[] RetrievePluginAssembly(string filename) {
			if(filename == null) throw new ArgumentNullException("filename");
			if(filename.Length == 0) throw new ArgumentException("Filename cannot be empty", "filename");

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

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

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

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

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				byte[] result = null;

				if(reader.Read()) {
					result = GetBinaryColumn(reader, "Assembly", MaxAssemblySize);
				}

				CloseReader(command, reader);

				return result;
			}
			else return null;
		}
		/// <summary>
		/// Retrieves all ACL entries for a subject.
		/// </summary>
		/// <param name="subject">The subject.</param>
		/// <returns>The ACL entries for the subject.</returns>
		private AclEntry[] RetrieveAclEntriesForSubject(string subject) {
			ICommandBuilder builder = GetCommandBuilder();

			QueryBuilder queryBuilder = new QueryBuilder(builder);

			// Sort order is not relevant
			string query = queryBuilder.SelectFrom("AclEntry");
			query = queryBuilder.Where(query, "Subject", WhereOperator.Equals, "Subject");

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

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

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				List<AclEntry> result = new List<AclEntry>(50);

				while(reader.Read()) {
					result.Add(new AclEntry(reader["Resource"] as string, reader["Action"] as string, reader["Subject"] as string,
						AclEntryValueFromChar(((string)reader["Value"])[0])));
				}

				CloseReader(command, reader);

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Gets a meta-data item's content.
		/// </summary>
		/// <param name="item">The item.</param>
		/// <param name="tag">The tag that specifies the context (usually the namespace).</param>
		/// <returns>The content.</returns>
		public string GetMetaDataItem(MetaDataItem item, string tag) {
			if(string.IsNullOrEmpty(tag)) tag = " ";

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

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

			List<Parameter> parameters = new List<Parameter>(2);
			parameters.Add(new Parameter(ParameterType.String, "Name", item.ToString()));
			parameters.Add(new Parameter(ParameterType.String, "Tag", tag));

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

			string value = ExecuteScalar<string>(command, " ");

			return value;
		}
		/// <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>
		/// Gets the outgoing links of a page.
		/// </summary>
		/// <param name="page">The full name of the page.</param>
		/// <returns>The outgoing links.</returns>
		/// <exception cref="ArgumentNullException">If <b>page</b> is <c>null</c>.</exception>
		/// <exception cref="ArgumentException">If <b>page</b> is empty.</exception>
		public string[] GetOutgoingLinks(string page) {
			if(page == null) throw new ArgumentNullException("page");
			if(page.Length == 0) throw new ArgumentException("Page cannot be empty", "page");

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

			string query = queryBuilder.SelectFrom("OutgoingLink", new string[] { "Destination" });
			query = queryBuilder.Where(query, "Source", WhereOperator.Equals, "Source");
			query = queryBuilder.OrderBy(query, new[] { "Destination" }, new[] { Ordering.Asc });

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

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

			DbDataReader reader = ExecuteReader(command);

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

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

				CloseReader(command, reader);

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Gets all the pages in a namespace that are bound to zero categories.
		/// </summary>
		/// <param name="nspace">The namespace (<c>null</c> for the root).</param>
		/// <returns>The pages, sorted by name.</returns>
		public PageInfo[] GetUncategorizedPages(NamespaceInfo nspace) {
			string nspaceName = nspace != null&&!string.IsNullOrEmpty(nspace.Name) ? nspace.Name : " ";

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

			string query = queryBuilder.SelectFrom("Page", "CategoryBinding", "Name", "Page", Join.LeftJoin);
			query = queryBuilder.Where(query, "CategoryBinding", "Category", WhereOperator.IsNull, null);
			query = queryBuilder.AndWhere(query, "Page", "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.OrderBy(query, new[] { "Name" }, new[] { 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<PageInfo> result = new List<PageInfo>(100);

				while(reader.Read()) {
					result.Add(new PageInfo(NameTools.GetFullName((reader["Namespace"] as string).Trim(), reader["Name"] as string),
						this, (DateTime)reader["CreationDateTime"]));
				}

				CloseReader(command, reader);

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Gets the content of a specific revision of a page.
		/// </summary>
		/// <param name="transaction">A database transaction.</param>
		/// <param name="page">The page.</param>
		/// <param name="revision">The revision.</param>
		/// <returns>The content.</returns>
		private PageContent GetContent(DbTransaction transaction, PageInfo page, int revision) {
			// Internal version to work with GetContent, GetBackupContent, GetDraft

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

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

			string query = queryBuilder.SelectFrom("PageContent", "PageKeyword", new string[] { "Page", "Namespace", "Revision" }, new string[] { "Page", "Namespace", "Revision" }, Join.LeftJoin,
				new string[] { "Title", "User", "LastModified", "Comment", "Content", "Description" }, new string[] { "Keyword" });
			query = queryBuilder.Where(query, "PageContent", "Page", WhereOperator.Equals, "Page");
			query = queryBuilder.AndWhere(query, "PageContent", "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.AndWhere(query, "PageContent", "Revision", WhereOperator.Equals, "Revision");

			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, "Revision", (short)revision));

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

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				PageContent result = null;

				string title = null, user = null, comment = null, content = null, description = null;
				DateTime dateTime = DateTime.MinValue;
				List<string> keywords = new List<string>(10);

				while(reader.Read()) {
					if(title == null) {
						title = reader["PageContent_Title"] as string;
						user = reader["PageContent_User"] as string;
						dateTime = (DateTime)reader["PageContent_LastModified"];
						comment = GetNullableColumn<string>(reader, "PageContent_Comment", " ");
						content = reader["PageContent_Content"] as string;
						description = GetNullableColumn<string>(reader, "PageContent_Description", null);
					}

					if(!IsDBNull(reader, "PageKeyword_Keyword")) {
						keywords.Add(reader["PageKeyword_Keyword"] as string);
					}
				}

				if(title != null) {
					result = new PageContent(page, title, user, dateTime, comment, content, keywords.ToArray(), description);
				}

				CloseReader(reader);

				return result;
			}
			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( string.IsNullOrEmpty(nspace)||string.IsNullOrEmpty(nspace.Trim())) 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).Trim(), name), this));
					}

					prevName = name;
					if(!IsDBNull(reader, "CategoryBinding_Page")) {
						pages.Add(NameTools.GetFullName((reader["Category_Namespace"] as string).Trim(), 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 a page.
		/// </summary>
		/// <param name="connection">A database connection.</param>
		/// <param name="fullName">The full name of the page.</param>
		/// <returns>The <see cref="T:PageInfo" />, or <c>null</c> if no page is found.</returns>
		private PageInfo GetPage(DbConnection connection, string fullName) {
			string nspace, name;
			NameTools.ExpandFullName(fullName, out nspace, out name);
			if( string.IsNullOrEmpty(nspace)||string.IsNullOrEmpty(nspace.Trim())) nspace = " ";

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

			string query = queryBuilder.SelectFrom("Page");
			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);

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				PageInfo result = null;

				if(reader.Read()) {
					result = new PageInfo(NameTools.GetFullName((reader["Namespace"] as string).Trim(), reader["Name"] as string),
						this, (DateTime)reader["CreationDateTime"]);
				}

				CloseReader(reader);

				return result;
			}
			else return null;
		}
		/// <summary>
		/// Gets all the Categories in a namespace.
		/// </summary>
		/// <param name="connection">A database connection.</param>
		/// <param name="nspace">The namespace.</param>
		/// <returns>All the Categories in the namespace. The array is not sorted.</returns>
		private CategoryInfo[] GetCategories(DbConnection connection, NamespaceInfo nspace) {
			string nspaceName = nspace != null&&!string.IsNullOrEmpty(nspace.Name) ? nspace.Name : " ";

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

			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, "Category", "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.OrderBy(query, new string[] { "Category_Name", "CategoryBinding_Page" }, new Ordering[] { Ordering.Asc, Ordering.Asc });

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

			DbCommand command = builder.GetCommand(connection, 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).Trim(), name), this));
					}

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

				CloseReader(reader);

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

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Gets a category.
		/// </summary>
		/// <param name="connection">A database connection.</param>
		/// <param name="fullName">The full name of the category.</param>
		/// <returns>The <see cref="T:CategoryInfo" />, or <c>null</c> if no category is found.</returns>
		private CategoryInfo GetCategory(DbConnection connection, string fullName) {
			string nspace = null;
			string name = null;
			NameTools.ExpandFullName(fullName, out nspace, out name);
			if( string.IsNullOrEmpty(nspace)||string.IsNullOrEmpty(nspace.Trim())) nspace = " ";

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

			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, "Category", "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.AndWhere(query, "Category", "Name", WhereOperator.Equals, "Name");

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

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

			DbDataReader reader = ExecuteReader(command);

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

				while(reader.Read()) {
					if(result == null) result = new CategoryInfo(NameTools.GetFullName(((reader["Category_Namespace"] as string).Trim()).Trim(), reader["Category_Name"] as string), this);

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

				CloseReader(reader);

				if(result != null) result.Pages = pages.ToArray();

				return result;
			}
			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>
		/// 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)Convert.ChangeType(reader["Document"], typeof(int)),
                    (ushort)(short)Convert.ChangeType(reader["FirstCharIndex"], typeof(short)),
                    (ushort)(short)Convert.ChangeType(reader["WordIndex"], typeof(short)),
                    (byte)Convert.ChangeType(reader["Location"], typeof(byte))));
            }
			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>
		/// Gets the Backup/Revision numbers of a Page.
		/// </summary>
		/// <param name="connection">A database connection.</param>
		/// <param name="page">The Page to get the Backups of.</param>
		/// <returns>The Backup/Revision numbers.</returns>
		private int[] GetBackups(DbConnection connection, PageInfo page) {
			if(GetPage(connection, page.FullName) == null) {
				return null;
			}

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

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

			string query = queryBuilder.SelectFrom("PageContent", new string[] { "Revision" });
			query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
			query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.AndWhere(query, "Revision", WhereOperator.GreaterThanOrEqualTo, "Revision");
			query = queryBuilder.OrderBy(query, new[] { "Revision" }, new[] { Ordering.Asc });

			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, "Revision", FirstRevision));

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

			DbDataReader reader = ExecuteReader(command);

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

				while(reader.Read()) {
					result.Add((short)Convert.ChangeType(reader["Revision"],typeof(short)));
				}

				CloseReader(reader);

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Gets a namespace.
		/// </summary>
		/// <param name="connection">A database connection.</param>
		/// <param name="name">The name of the namespace (cannot be <c>null</c> or empty).</param>
		/// <returns>The <see cref="T:NamespaceInfo" />, or <c>null</c> if no namespace is found.</returns>
		private NamespaceInfo GetNamespace(DbConnection connection, string name) {
			ICommandBuilder builder = GetCommandBuilder();
			QueryBuilder queryBuilder = new QueryBuilder(builder);

			// select ... from Namespace left join Page on Namespace.DefaultPage = Page.Name where Namespace.Name = <name> and (Namespace.DefaultPage is null or Page.Namespace = <name>)
			string query = queryBuilder.SelectFrom("Namespace", "Page", "DefaultPage", "Name", Join.LeftJoin, new string[] { "Name", "DefaultPage" }, new string[] { "CreationDateTime" });
			query = queryBuilder.Where(query, "Namespace", "Name", WhereOperator.Equals, "Name1");
			query = queryBuilder.AndWhere(query, "Namespace", "DefaultPage", WhereOperator.IsNull, null, true, false);
			query = queryBuilder.OrWhere(query, "Page", "Namespace", WhereOperator.Equals, "Name2", false, true);

			List<Parameter> parameters = new List<Parameter>(2);
			parameters.Add(new Parameter(ParameterType.String, "Name1", name));
			parameters.Add(new Parameter(ParameterType.String, "Name2", name));

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

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				NamespaceInfo result = null;

				if(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"]);

					result = new NamespaceInfo(realName, this, defaultPage);
				}

				CloseReader(reader);

				return result;
			}
			else return null;
		}
		/// <summary>
		/// Deletes all data associated to a document.
		/// </summary>
		/// <param name="document">The document.</param>
		/// <param name="state">A state object passed from the index (can be <c>null</c> or a <see cref="T:DbTransaction" />).</param>
		private void DeleteDataForDocument(IDocument document, object state) {
			// 1. Delete all data related to a document
			// 2. Delete all words that have no more mappings

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

			string query = queryBuilder.DeleteFrom("IndexDocument");
			query = queryBuilder.Where(query, "Name", WhereOperator.Equals, "DocName");
			List<Parameter> parameters = new List<Parameter>(1);
			parameters.Add(new Parameter(ParameterType.String, "DocName", document.Name));

			string subQuery = queryBuilder.SelectFrom("IndexWordMapping", new string[] { "Word" });
			subQuery = queryBuilder.GroupBy(subQuery, new string[] { "Word" });
			string query2 = queryBuilder.DeleteFrom("IndexWord");
			query2 = queryBuilder.WhereNotInSubquery(query2, "IndexWord", "Id", subQuery);

			query = queryBuilder.AppendForBatch(query, query2);
            query = queryBuilder.BuildBatchCommand(query);
			DbCommand command = null;
			if(state != null) command = builder.GetCommand((DbTransaction)state, query, parameters);
			else command = builder.GetCommand(connString, query, parameters);

			// Close only if state is null
			ExecuteNonQuery(command, state == null);
		}
		/// <summary>
		/// Gets the configuration of a plugin.
		/// </summary>
		/// <param name="typeName">The Type name of the plugin.</param>
		/// <returns>The plugin configuration, or <b>String.Empty</b>.</returns>
		/// <exception cref="ArgumentNullException">If <b>typeName</b> is <c>null</c>.</exception>
		/// <exception cref="ArgumentException">If <b>typeName</b> is empty.</exception>
		public string GetPluginConfiguration(string typeName) {
			if(typeName == null) throw new ArgumentNullException("typeName");
			if(typeName.Length == 0) throw new ArgumentException("Type Name cannot be empty", "typeName");

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

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

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

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

			string result = ExecuteScalar<string>(command, "");

            return result.Trim() ;
		}
		/// <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>
		/// Gets all the outgoing links stored.
		/// </summary>
		/// <returns>The outgoing links, in a dictionary in the form page-&gt;outgoing_links.</returns>
		public IDictionary<string, string[]> GetAllOutgoingLinks() {
			ICommandBuilder builder = GetCommandBuilder();
			QueryBuilder queryBuilder = new QueryBuilder(builder);

			// Explicit columns in order to allow usage of GROUP BY
			string query = queryBuilder.SelectFrom("OutgoingLink", new string[] { "Source", "Destination" });
			query = queryBuilder.GroupBy(query, new string[] { "Source", "Destination" });

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

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				Dictionary<string, string[]> result = new Dictionary<string, string[]>(100);

				string prevSource = "|||";
				string source = null;
				List<string> destinations = new List<string>(20);

				while(reader.Read()) {
					source = reader["Source"] as string;

					if(source != prevSource) {
						if(prevSource != "|||") {
							result.Add(prevSource, destinations.ToArray());
							destinations.Clear();
						}
					}

					prevSource = source;
					destinations.Add(reader["Destination"] as string);
				}

				result.Add(prevSource, destinations.ToArray());
				
				CloseReader(command, reader);

				return result;
			}
			else return null;
		}
		/// <summary>
		/// Gets the Page Messages.
		/// </summary>
		/// <param name="connection">A database connection.</param>
		/// <param name="page">The Page.</param>
		/// <returns>The list of the <b>first-level</b> Messages, containing the replies properly nested, sorted by date/time.</returns>
		private Message[] GetMessages(DbConnection connection, PageInfo page) {
			if(GetPage(connection, page.FullName) == null) return null;

			// 1. Load all messages in memory in a dictionary id->message
			// 2. Build tree using ParentID

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

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

			string query = queryBuilder.SelectFrom("Message", new string[] { "Id", "Parent", "Username", "Subject", "DateTime", "Body" });
			query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
			query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.OrderBy(query, new string[] { "DateTime", "Id" }, new Ordering[] { Ordering.Asc, Ordering.Asc });

			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);

			DbDataReader reader = ExecuteReader(command);

			if(reader != null) {
				Dictionary<short, Message> allMessages = new Dictionary<short, Message>(50);
				List<short> ids = new List<short>(50);
				List<short?> parents = new List<short?>(50);

				while(reader.Read()) {
					Message msg = new Message((short)Convert.ChangeType( reader["Id"],typeof(short)), reader["Username"] as string, reader["Subject"] as string,
						(DateTime)reader["DateTime"], reader["Body"] as string);

					ids.Add((short)msg.ID);

					// Import from V2: parent = -1, otherwise null
					if(!IsDBNull(reader, "Parent")) {
						short par = (short)Convert.ChangeType(reader["Parent"],typeof(short));
						if(par >= 0) parents.Add(par);
						else parents.Add(null);
					}
					else parents.Add(null);

					allMessages.Add((short)msg.ID, msg);
				}

				CloseReader(reader);

				// Add messages to their parents and build the top-level messages list
				List<Message> result = new List<Message>(20);

				for(int i = 0; i < ids.Count; i++) {
					short? currentParent = parents[i];
					short currentId = ids[i];

					if(currentParent.HasValue) {
						List<Message> replies = new List<Message>(allMessages[currentParent.Value].Replies);
						replies.Add(allMessages[currentId]);
						allMessages[currentParent.Value].Replies = replies.ToArray();
					}
					else result.Add(allMessages[currentId]);
				}

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Gets all the Log Entries, sorted by date/time (oldest to newest).
		/// </summary>
		/// <returns>The Log Entries.</returns>
		public LogEntry[] GetLogEntries() {
			ICommandBuilder builder = GetCommandBuilder();
			QueryBuilder queryBuilder = new QueryBuilder(builder);

			string query = queryBuilder.SelectFrom("Log", new string[] { "DateTime", "EntryType", "User", "Message" });
			query = queryBuilder.OrderBy(query, new string[] { "DateTime" }, new Ordering[] { Ordering.Asc });

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

			DbDataReader reader = ExecuteReader(command);

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

				while(reader.Read()) {
					result.Add(new LogEntry(EntryTypeFromChar((reader["EntryType"] as string)[0]),
						(DateTime)reader["DateTime"], reader["Message"] as string, reader["User"] as string));
				}

				CloseReader(command, reader);

				return result.ToArray();
			}
			else return null;
		}
		/// <summary>
		/// Adds a new Message to a Page.
		/// </summary>
		/// <param name="page">The Page.</param>
		/// <param name="username">The Username.</param>
		/// <param name="subject">The Subject.</param>
		/// <param name="dateTime">The Date/Time.</param>
		/// <param name="body">The Body.</param>
		/// <param name="parent">The Parent Message ID, or -1.</param>
		/// <returns>True if the Message is added successfully.</returns>
		/// <exception cref="ArgumentNullException">If <paramref name="page"/>, <paramref name="username"/>, <paramref name="subject"/> or <paramref name="body"/> are <c>null</c>.</exception>
		/// <exception cref="ArgumentException">If <paramref name="username"/> or <paramref name="subject"/> are empty.</exception>
		/// <exception cref="ArgumentOutOfRangeException">If <paramref name="parent"/> is less than -1.</exception>
		public bool AddMessage(PageInfo page, string username, string subject, DateTime dateTime, string body, int parent) {
			if(page == null) throw new ArgumentNullException("page");
			if(username == null) throw new ArgumentNullException("username");
			if(username.Length == 0) throw new ArgumentException("Username cannot be empty", "username");
			if(subject == null) throw new ArgumentNullException("subject");
			if(subject.Length == 0) throw new ArgumentException("Subject cannot be empty", "subject");
			if(body == null) throw new ArgumentNullException("body"); // body can be empty
			if(parent < -1) throw new ArgumentOutOfRangeException("parent", "Invalid Parent Message ID");

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

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

			if(parent != -1 && FindMessage(GetMessages(transaction, page), parent) == null) {
				RollbackTransaction(transaction);
				return false;
			}

			QueryBuilder queryBuilder = new QueryBuilder(builder);

			short freeId = -1;

			string query = queryBuilder.SelectFrom("Message", new string[] { "Id" });
			query = queryBuilder.Where(query, "Page", WhereOperator.Equals, "Page");
			query = queryBuilder.AndWhere(query, "Namespace", WhereOperator.Equals, "Namespace");
			query = queryBuilder.OrderBy(query, new string[] { "Id" }, new Ordering[] { Ordering.Desc });

			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(transaction, query, parameters);

			freeId = ExecuteScalar<short>(command, -1, false);

			if(freeId == -1) freeId = 0;
			else freeId++;

			query = queryBuilder.InsertInto("Message", new string[] { "Page", "Namespace", "Id", "Parent", "Username", "Subject", "DateTime", "Body" },
				new string[] { "Page", "Namespace", "Id", "Parent", "Username", "Subject", "DateTime", "Body" });

			parameters = new List<Parameter>(8);
			parameters.Add(new Parameter(ParameterType.String, "Page", name));
			parameters.Add(new Parameter(ParameterType.String, "Namespace", nspace));
			parameters.Add(new Parameter(ParameterType.Int16, "Id", freeId));
			if(parent != -1) parameters.Add(new Parameter(ParameterType.Int16, "Parent", parent));
			else parameters.Add(new Parameter(ParameterType.Int16, "Parent", DBNull.Value));
			parameters.Add(new Parameter(ParameterType.String, "Username", username));
			parameters.Add(new Parameter(ParameterType.String, "Subject", subject));
			parameters.Add(new Parameter(ParameterType.DateTime, "DateTime", dateTime));
			parameters.Add(new Parameter(ParameterType.String, "Body", body));

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

			int rows = ExecuteNonQuery(command, false);

			if(rows == 1) {
				IndexMessage(page, freeId, subject, dateTime, body, transaction);

				CommitTransaction(transaction);
				return true;
			}
			else {
				RollbackTransaction(transaction);
				return false;
			}
		}
		/// <summary>
		/// Gets the recent changes of the Wiki.
		/// </summary>
		/// <returns>The recent Changes, oldest to newest.</returns>
		public RecentChange[] GetRecentChanges() {
			ICommandBuilder builder = GetCommandBuilder();
			QueryBuilder queryBuilder = new QueryBuilder(builder);

			string query = queryBuilder.SelectFrom("RecentChange", new string[] { "Page", "Title", "MessageSubject", "DateTime", "User", "Change", "Description" });
			query = queryBuilder.OrderBy(query, new string[] { "DateTime" }, new Ordering[] { Ordering.Asc });

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

			DbDataReader reader = ExecuteReader(command);

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

				while(reader.Read()) {
					result.Add(new RecentChange(reader["Page"] as string, reader["Title"] as string,
						GetNullableColumn<string>(reader, "MessageSubject", " "),
						(DateTime)reader["DateTime"], reader["User"] as string, RecentChangeFromChar(((string)reader["Change"])[0]),
						GetNullableColumn<string>(reader, "Description", "")));
				}

				CloseReader(command, 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&&!string.IsNullOrEmpty(nspace.Name) ? 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).Trim();
                        actualNamespace = actualNamespace.Trim();

						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>
		/// Cuts the recent changes if necessary.
		/// </summary>
		private void CutRecentChangesIfNecessary() {
			ICommandBuilder builder = GetCommandBuilder();
			DbConnection connection = builder.GetConnection(connString);
			DbTransaction transaction = BeginTransaction(connection);

			QueryBuilder queryBuilder = new QueryBuilder(builder);

			string query = queryBuilder.SelectCountFrom("RecentChange");

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

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

			int maxChanges = int.Parse(host.GetSettingValue(SettingName.MaxRecentChanges));

			if(rows > maxChanges) {
				// Remove 10% of old changes to avoid 1-by-1 deletion every time a change is made
				int entriesToDelete = maxChanges / 10;
				if(entriesToDelete > rows) entriesToDelete = rows;
				//entriesToDelete += entriesToDelete / 10;

				// This code is not optimized, but it surely works in most DBMS
				query = queryBuilder.SelectFrom("RecentChange", new string[] { "Id" });
				query = queryBuilder.OrderBy(query, new string[] { "Id" }, new Ordering[] { Ordering.Asc });

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

				DbDataReader reader = ExecuteReader(command);

				List<int> ids = new List<int>(entriesToDelete);

				if(reader != null) {
					while(reader.Read() && ids.Count < entriesToDelete) {
						ids.Add((int)Convert.ChangeType(reader["Id"],typeof(int)));
					}

					CloseReader(reader);
				}

				if(ids.Count > 0) {
					// Given that the IDs to delete can be many, the query is split in many chunks, each one deleting 50 items
					// This works-around the problem of too many parameters in a RPC call of Oracle
					// See also CutLog

					for(int chunk = 0; chunk <= ids.Count / MaxParametersInQuery; chunk++) {
						query = queryBuilder.DeleteFrom("RecentChange");
						List<string> parms = new List<string>(MaxParametersInQuery);
						List<Parameter> parameters = new List<Parameter>(MaxParametersInQuery);

						for(int i = chunk * MaxParametersInQuery; i < Math.Min(ids.Count, (chunk + 1) * MaxParametersInQuery); i++) {
							parms.Add("P" + i.ToString());
							parameters.Add(new Parameter(ParameterType.Int32, parms[parms.Count - 1], ids[i]));
						}

						query = queryBuilder.WhereIn(query, "Id", parms.ToArray());

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

						if(ExecuteNonQuery(command, false) < 0) {
							RollbackTransaction(transaction);
							return;
						}
					}
				}
			}

			CommitTransaction(transaction);
		}
		/// <summary>
		/// Gets a free element ID from the database.
		/// </summary>
		/// <param name="element">The element type.</param>
		/// <param name="transaction">The current database transaction.</param>
		/// <returns>The free element ID.</returns>
		private uint GetFreeElementId(IndexElementType element, DbTransaction transaction) {
			if(element == IndexElementType.Occurrences) throw new ArgumentException("Element cannot be Occurrences", "element");

			string table = element == IndexElementType.Documents ? "IndexDocument" : "IndexWord";

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

			string query = queryBuilder.SelectFrom(table, new string[] { "Id" });
			query = queryBuilder.OrderBy(query, new string[] { "Id" }, new Ordering[] { Ordering.Desc });

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

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

			if(id == -1) return 0;
			else return (uint)id + 1;
		}
		/// <summary>
		/// Gets the status of a plugin.
		/// </summary>
		/// <param name="typeName">The Type name of the plugin.</param>
		/// <returns>The status (<c>false</c> for disabled, <c>true</c> for enabled), or <c>true</c> if no status is found.</returns>
		/// <exception cref="ArgumentNullException">If <b>typeName</b> is <c>null</c>.</exception>
		/// <exception cref="ArgumentException">If <b>typeName</b> is empty.</exception>
		public bool GetPluginStatus(string typeName) {
			if(typeName == null) throw new ArgumentNullException("typeName");
			if(typeName.Length == 0) throw new ArgumentException("Type Name cannot be empty", "typeName");

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

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

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

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

			DbDataReader reader = ExecuteReader(command);

			bool? enabled = null;

			if(reader != null && reader.Read()) {
				if(!IsDBNull(reader, "Enabled")) enabled = Convert.ToBoolean(reader["Enabled"]);
			}
			CloseReader(command, reader);

			if(enabled.HasValue) return enabled.Value;
			else {
				if(typeName == "ScrewTurn.Wiki.UsersStorageProvider" ||
					typeName == "ScrewTurn.Wiki.PagesStorageProvider" ||
					typeName == "ScrewTurn.Wiki.FilesStorageProvider") return false;
				else return true;
			}
		}
		/// <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)Convert.ChangeType(reader["Size"],typeof(long)),
						(DateTime)reader["LastModified"], (int)Convert.ChangeType(reader["Downloads"],typeof(int)));
				}

				CloseReader(command, reader);

				return details;
			}
			else return null;
		}