using System; using System.Collections; using System.Collections.Specialized; using System.Data; using System.IO; using System.Security.Cryptography; using System.Text; using SemWeb.Util; namespace SemWeb.Stores { // TODO: It's not safe to have two concurrent accesses to the same database // because the creation of new entities will use the same IDs. public abstract class SQLStore : Store, SupportsPersistableBNodes { int dbformat = 1; string table; string guid; bool firstUse = true; bool isImporting = false; int cachedNextId = -1; Hashtable entityCache = new Hashtable(); Hashtable literalCache = new Hashtable(); ArrayList anonEntityHeldIds = new ArrayList(); bool statementsRemoved = false; static bool Debug = System.Environment.GetEnvironmentVariable("SEMWEB_DEBUG_SQL") != null; StringBuilder cmdBuffer = new StringBuilder(); // Buffer statements to process together. StatementList addStatementBuffer = null; string INSERT_INTO_LITERALS_VALUES, INSERT_INTO_STATEMENTS_VALUES, INSERT_INTO_ENTITIES_VALUES; char quote; object syncroot = new object(); Hashtable metaEntities; SHA1 sha = SHA1.Create(); int importAddBufferSize = 200, importAddBufferRotation = 0; TimeSpan importAddBufferTime = TimeSpan.MinValue; private class ResourceKey { public int ResId; public ResourceKey(int id) { ResId = id; } public override int GetHashCode() { return ResId.GetHashCode(); } public override bool Equals(object other) { return (other is ResourceKey) && ((ResourceKey)other).ResId == ResId; } } private static readonly string[] fourcols = new string[] { "subject", "predicate", "object", "meta" }; private static readonly string[] predcol = new string[] { "predicate" }; private static readonly string[] metacol = new string[] { "meta" }; protected SQLStore(string table) { this.table = table; INSERT_INTO_LITERALS_VALUES = "INSERT INTO " + table + "_literals VALUES "; INSERT_INTO_ENTITIES_VALUES = "INSERT INTO " + table + "_entities VALUES "; INSERT_INTO_STATEMENTS_VALUES = "INSERT " + (SupportsInsertIgnore ? "IGNORE " : "") + "INTO " + table + "_statements VALUES "; quote = GetQuoteChar(); } protected string TableName { get { return table; } } protected abstract bool SupportsNoDuplicates { get; } protected abstract bool SupportsInsertIgnore { get; } protected abstract bool SupportsInsertCombined { get; } protected virtual bool SupportsFastJoin { get { return true; } } protected abstract bool SupportsSubquery { get; } protected abstract void CreateNullTest(string column, System.Text.StringBuilder command); private void Init() { if (!firstUse) return; firstUse = false; CreateTable(); CreateIndexes(); CreateVersion(); } private void CreateVersion() { string verdatastr = RunScalarString("SELECT value FROM " + table + "_literals WHERE id = 0"); NameValueCollection verdata = ParseVersionInfo(verdatastr); if (verdatastr != null && verdata["ver"] == null) throw new InvalidOperationException("The SQLStore adapter in this version of SemWeb cannot read databases created in previous versions."); verdata["ver"] = dbformat.ToString(); if (verdata["guid"] == null) { guid = Guid.NewGuid().ToString("N"); verdata["guid"] = guid; } else { guid = verdata["guid"]; } string newverdata = SerializeVersionInfo(verdata); if (verdatastr == null) RunCommand("INSERT INTO " + table + "_literals (id, value) VALUES (0, " + Escape(newverdata, true) + ")"); else if (verdatastr != newverdata) RunCommand("UPDATE " + table + "_literals SET value = " + newverdata + " WHERE id = 0"); } NameValueCollection ParseVersionInfo(string verdata) { NameValueCollection nvc = new NameValueCollection(); if (verdata == null) return nvc; foreach (string s in verdata.Split('\n')) { int c = s.IndexOf(':'); if (c == -1) continue; nvc[s.Substring(0, c)] = s.Substring(c+1); } return nvc; } string SerializeVersionInfo(NameValueCollection verdata) { string ret = ""; foreach (string k in verdata.Keys) ret += k + ":" + verdata[k] + "\n"; return ret; } public override bool Distinct { get { return true; } } public override int StatementCount { get { Init(); RunAddBuffer(); return RunScalarInt("select count(subject) from " + table + "_statements", 0); } } public string GetStoreGuid() { return guid; } public string GetNodeId(BNode node) { ResourceKey rk = (ResourceKey)GetResourceKey(node); if (rk == null) return null; return rk.ResId.ToString(); } public BNode GetNodeFromId(string persistentId) { try { int id = int.Parse(persistentId); return (BNode)MakeEntity(id, null, null); } catch (Exception e) { return null; } } private int NextId() { if (isImporting && cachedNextId != -1) return ++cachedNextId; RunAddBuffer(); // The 0 id is not used. // The 1 id is reserved for Statement.DefaultMeta. int nextid = 2; CheckMax("select max(subject) from " + table + "_statements", ref nextid); CheckMax("select max(predicate) from " + table + "_statements", ref nextid); CheckMax("select max(object) from " + table + "_statements", ref nextid); CheckMax("select max(meta) from " + table + "_statements", ref nextid); CheckMax("select max(id) from " + table + "_literals", ref nextid); CheckMax("select max(id) from " + table + "_entities", ref nextid); cachedNextId = nextid; return nextid; } private void CheckMax(string command, ref int nextid) { int maxid = RunScalarInt(command, 0); if (maxid >= nextid) nextid = maxid + 1; } public override void Clear() { // Drop the tables, if they exist. try { RunCommand("DROP TABLE " + table + "_statements;"); } catch (Exception e) { } try { RunCommand("DROP TABLE " + table + "_literals;"); } catch (Exception e) { } try { RunCommand("DROP TABLE " + table + "_entities;"); } catch (Exception e) { } firstUse = true; Init(); if (addStatementBuffer != null) addStatementBuffer.Clear(); metaEntities = null; //RunCommand("DELETE FROM " + table + "_statements;"); //RunCommand("DELETE FROM " + table + "_literals;"); //RunCommand("DELETE FROM " + table + "_entities;"); } private string GetLiteralHash(Literal literal) { byte[] data = System.Text.Encoding.Unicode.GetBytes(literal.ToString()); byte[] hash = sha.ComputeHash(data); return Convert.ToBase64String(hash); } private int GetLiteralId(Literal literal, bool create, StringBuilder buffer, bool insertCombined) { // Returns the literal ID associated with the literal. If a literal // doesn't exist and create is true, a new literal is created, // otherwise 0 is returned. if (isImporting) { object ret = literalCache[literal]; if (ret != null) return (int)ret; } else { StringBuilder b = cmdBuffer; cmdBuffer.Length = 0; b.Append("SELECT id FROM "); b.Append(table); b.Append("_literals WHERE hash ="); b.Append("\""); b.Append(GetLiteralHash(literal)); b.Append("\""); b.Append(" LIMIT 1;"); object id = RunScalar(b.ToString()); if (id != null) return AsInt(id); } if (create) { int id = AddLiteral(literal, buffer, insertCombined); if (isImporting) literalCache[literal] = id; return id; } return 0; } private int AddLiteral(Literal literal, StringBuilder buffer, bool insertCombined) { int id = NextId(); StringBuilder b; if (buffer != null) { b = buffer; } else { b = cmdBuffer; cmdBuffer.Length = 0; } if (!insertCombined) { b.Append(INSERT_INTO_LITERALS_VALUES); } else { if (b.Length > 0) b.Append(','); } b.Append('('); b.Append(id); b.Append(','); EscapedAppend(b, literal.Value); b.Append(','); if (literal.Language != null) EscapedAppend(b, literal.Language); else b.Append("NULL"); b.Append(','); if (literal.DataType != null) EscapedAppend(b, literal.DataType); else b.Append("NULL"); b.Append(",\""); b.Append(GetLiteralHash(literal)); b.Append("\")"); if (!insertCombined) b.Append(';'); if (buffer == null) { if (Debug) Console.Error.WriteLine(b.ToString()); RunCommand(b.ToString()); } return id; } private int GetEntityId(string uri, bool create, StringBuilder entityInsertBuffer, bool insertCombined, bool checkIfExists) { // Returns the resource ID associated with the URI. If a resource // doesn't exist and create is true, a new resource is created, // otherwise 0 is returned. int id; if (isImporting) { object idobj = entityCache[uri]; if (idobj == null && !create) return 0; if (idobj != null) return (int)idobj; } else if (checkIfExists) { StringBuilder cmd = cmdBuffer; cmdBuffer.Length = 0; cmd.Append("SELECT id FROM "); cmd.Append(table); cmd.Append("_entities WHERE value ="); EscapedAppend(cmd, uri); id = RunScalarInt(cmd.ToString(), 0); if (id != 0 || !create) return id; } // If we got here, no such resource exists and create is true. if (uri.Length > 255) throw new NotSupportedException("URIs must be a maximum of 255 characters for this store due to indexing constraints (before MySQL 4.1.2)."); id = NextId(); StringBuilder b; if (entityInsertBuffer != null) { b = entityInsertBuffer; } else { b = cmdBuffer; cmdBuffer.Length = 0; } if (!insertCombined) { b.Append(INSERT_INTO_ENTITIES_VALUES); } else { if (b.Length > 0) b.Append(','); } b.Append('('); b.Append(id); b.Append(','); EscapedAppend(b, uri); b.Append(')'); if (!insertCombined) b.Append(';'); if (entityInsertBuffer == null) RunCommand(b.ToString()); // Add it to the URI map if (isImporting) entityCache[uri] = id; return id; } private int GetResourceId(Resource resource, bool create) { return GetResourceIdBuffer(resource, create, null, null, false); } private int GetResourceIdBuffer(Resource resource, bool create, StringBuilder literalInsertBuffer, StringBuilder entityInsertBuffer, bool insertCombined) { if (resource == null) return 0; if (resource is Literal) { Literal lit = (Literal)resource; return GetLiteralId(lit, create, literalInsertBuffer, insertCombined); } if (object.ReferenceEquals(resource, Statement.DefaultMeta)) return 1; ResourceKey key = (ResourceKey)GetResourceKey(resource); if (key != null) return key.ResId; int id; if (resource.Uri != null) { id = GetEntityId(resource.Uri, create, entityInsertBuffer, insertCombined, true); } else { // This anonymous node didn't come from the database // since it didn't have a resource key. If !create, // then just return 0 to signal the resource doesn't exist. if (!create) return 0; if (isImporting) { // Can just increment the counter. id = NextId(); } else { // We need to reserve an id for this resource so that // this function returns other ids for other anonymous // resources. To do that, we'll insert a record // into the entities table with a GUID for the entity. // Inserting something into the table also gives us // concurrency, I hope. Then, once the resource is // added to the statements table, this row can be // removed. string guid = "semweb-bnode-guid://taubz.for.net,2006/" + Guid.NewGuid().ToString("N"); id = GetEntityId(guid, create, entityInsertBuffer, insertCombined, false); anonEntityHeldIds.Add(id); } } if (id != 0) SetResourceKey(resource, new ResourceKey(id)); return id; } private int ObjectType(Resource r) { if (r is Literal) return 1; return 0; } private Entity MakeEntity(int resourceId, string uri, Hashtable cache) { if (resourceId == 0) return null; if (resourceId == 1) return Statement.DefaultMeta; ResourceKey rk = new ResourceKey(resourceId); if (cache != null && cache.ContainsKey(rk)) return (Entity)cache[rk]; Entity ent; if (uri != null) { ent = new Entity(uri); } else { ent = new BNode(); } SetResourceKey(ent, rk); if (cache != null) cache[rk] = ent; return ent; } public override void Add(Statement statement) { if (statement.AnyNull) throw new ArgumentNullException(); metaEntities = null; if (addStatementBuffer != null) { addStatementBuffer.Add(statement); // This complicated code here adjusts the size of the add // buffer dynamically to maximize performance. int thresh = importAddBufferSize; if (importAddBufferRotation == 1) thresh += 100; // experiment with changing if (importAddBufferRotation == 2) thresh -= 100; // the buffer size if (addStatementBuffer.Count >= thresh) { DateTime start = DateTime.Now; RunAddBuffer(); TimeSpan duration = DateTime.Now - start; // If there was an improvement in speed, per statement, on an // experimental change in buffer size, keep the change. if (importAddBufferRotation != 0 && duration.TotalSeconds/thresh < importAddBufferTime.TotalSeconds/importAddBufferSize && thresh >= 200 && thresh <= 4000) importAddBufferSize = thresh; importAddBufferTime = duration; importAddBufferRotation++; if (importAddBufferRotation == 3) importAddBufferRotation = 0; } return; } Init(); int subj = GetResourceId(statement.Subject, true); int pred = GetResourceId(statement.Predicate, true); int objtype = ObjectType(statement.Object); int obj = GetResourceId(statement.Object, true); int meta = GetResourceId(statement.Meta, true); StringBuilder addBuffer = cmdBuffer; addBuffer.Length = 0; addBuffer.Append(INSERT_INTO_STATEMENTS_VALUES); addBuffer.Append('('); addBuffer.Append(subj); addBuffer.Append(','); addBuffer.Append(pred); addBuffer.Append(','); addBuffer.Append(objtype); addBuffer.Append(','); addBuffer.Append(obj); addBuffer.Append(','); addBuffer.Append(meta); addBuffer.Append("); "); RunCommand(addBuffer.ToString()); // Remove any entries in the entities table // for anonymous nodes. if (anonEntityHeldIds.Count > 0) { addBuffer.Length = 0; addBuffer.Append("DELETE FROM "); addBuffer.Append(table); addBuffer.Append("_entities where id IN ("); bool first = true; foreach (int id in anonEntityHeldIds) { if (!first) addBuffer.Append(','); first = false; addBuffer.Append(id); } addBuffer.Append(')'); RunCommand(addBuffer.ToString()); anonEntityHeldIds.Clear(); } } private void RunAddBuffer() { if (addStatementBuffer == null || addStatementBuffer.Count == 0) return; bool insertCombined = SupportsInsertCombined; Init(); // Prevent recursion through NextId=>StatementCount StatementList statements = addStatementBuffer; addStatementBuffer = null; try { // Prefetch the IDs of all entities mentioned in statements. StringBuilder cmd = new StringBuilder(); cmd.Append("SELECT id, value FROM "); cmd.Append(table); cmd.Append("_entities WHERE value IN ("); Hashtable entseen = new Hashtable(); bool hasEnts = false; for (int i = 0; i < statements.Count; i++) { Statement s = (Statement)statements[i]; for (int j = 0; j < 4; j++) { Entity ent = s.GetComponent(j) as Entity; if (ent == null || ent.Uri == null) continue; if (entityCache.ContainsKey(ent.Uri)) continue; if (entseen.ContainsKey(ent.Uri)) continue; if (hasEnts) cmd.Append(" , "); EscapedAppend(cmd, ent.Uri); hasEnts = true; entseen[ent.Uri] = ent.Uri; } } if (hasEnts) { cmd.Append(");"); if (Debug) Console.Error.WriteLine(cmd.ToString()); using (IDataReader reader = RunReader(cmd.ToString())) { while (reader.Read()) { int id = reader.GetInt32(0); string uri = AsString(reader[1]); entityCache[uri] = id; } } } // Prefetch the IDs of all literals mentioned in statements. cmd.Length = 0; cmd.Append("SELECT id, hash FROM "); cmd.Append(table); cmd.Append("_literals WHERE hash IN ("); bool hasLiterals = false; Hashtable litseen = new Hashtable(); for (int i = 0; i < statements.Count; i++) { Statement s = (Statement)statements[i]; Literal lit = s.Object as Literal; if (lit == null) continue; if (literalCache.ContainsKey(lit)) continue; string hash = GetLiteralHash(lit); if (litseen.ContainsKey(hash)) continue; if (hasLiterals) cmd.Append(" , "); cmd.Append('"'); cmd.Append(hash); cmd.Append('"'); hasLiterals = true; litseen[hash] = lit; } if (hasLiterals) { cmd.Append(");"); using (IDataReader reader = RunReader(cmd.ToString())) { while (reader.Read()) { int id = reader.GetInt32(0); string hash = AsString(reader[1]); Literal lit = (Literal)litseen[hash]; literalCache[lit] = id; } } } StringBuilder entityInsertions = new StringBuilder(); StringBuilder literalInsertions = new StringBuilder(); cmd = new StringBuilder(); if (insertCombined) cmd.Append(INSERT_INTO_STATEMENTS_VALUES); for (int i = 0; i < statements.Count; i++) { Statement statement = (Statement)statements[i]; int subj = GetResourceIdBuffer(statement.Subject, true, literalInsertions, entityInsertions, insertCombined); int pred = GetResourceIdBuffer(statement.Predicate, true, literalInsertions, entityInsertions, insertCombined); int objtype = ObjectType(statement.Object); int obj = GetResourceIdBuffer(statement.Object, true, literalInsertions, entityInsertions, insertCombined); int meta = GetResourceIdBuffer(statement.Meta, true, literalInsertions, entityInsertions, insertCombined); if (!insertCombined) cmd.Append(INSERT_INTO_STATEMENTS_VALUES); cmd.Append('('); cmd.Append(subj); cmd.Append(','); cmd.Append(pred); cmd.Append(','); cmd.Append(objtype); cmd.Append(','); cmd.Append(obj); cmd.Append(','); cmd.Append(meta); if (i == statements.Count-1 || !insertCombined) cmd.Append(");"); else cmd.Append("),"); } if (literalInsertions.Length > 0) { if (insertCombined) { literalInsertions.Insert(0, INSERT_INTO_LITERALS_VALUES); literalInsertions.Append(';'); } RunCommand(literalInsertions.ToString()); } if (entityInsertions.Length > 0) { if (insertCombined) { entityInsertions.Insert(0, INSERT_INTO_ENTITIES_VALUES); entityInsertions.Append(';'); } RunCommand(entityInsertions.ToString()); } RunCommand(cmd.ToString()); } finally { // Clear the array and reuse it. statements.Clear(); addStatementBuffer = statements; entityCache.Clear(); literalCache.Clear(); } } public override void Remove(Statement template) { Init(); RunAddBuffer(); System.Text.StringBuilder cmd = new System.Text.StringBuilder("DELETE FROM "); cmd.Append(table); cmd.Append("_statements "); if (!WhereClause(template, cmd)) return; cmd.Append(';'); RunCommand(cmd.ToString()); statementsRemoved = true; metaEntities = null; } public override Entity[] GetEntities() { return GetAllEntities(fourcols); } public override Entity[] GetPredicates() { return GetAllEntities(predcol); } public override Entity[] GetMetas() { return GetAllEntities(metacol); } private Entity[] GetAllEntities(string[] cols) { RunAddBuffer(); ArrayList ret = new ArrayList(); Hashtable seen = new Hashtable(); foreach (string col in cols) { using (IDataReader reader = RunReader("SELECT " + col + ", value FROM " + table + "_statements LEFT JOIN " + table + "_entities ON " + col + "=id " + (col == "object" ? " WHERE objecttype=0" : "") + " GROUP BY " + col + ";")) { while (reader.Read()) { int id = reader.GetInt32(0); if (id == 1 && col == "meta") continue; // don't return DefaultMeta in meta column. if (seen.ContainsKey(id)) continue; seen[id] = seen; string uri = AsString(reader[1]); ret.Add(MakeEntity(id, uri, null)); } } } return (Entity[])ret.ToArray(typeof(Entity));; } private bool WhereItem(string col, Resource r, System.Text.StringBuilder cmd, bool and) { if (and) cmd.Append(" and "); if (r is MultiRes) { cmd.Append('('); cmd.Append(col); cmd.Append(" IN ("); if (!AppendMultiRes((MultiRes)r, cmd)) return false; cmd.Append(" ))"); } else { int id = GetResourceId(r, false); if (id == 0) return false; cmd.Append('('); cmd.Append(col); cmd.Append('='); cmd.Append(id); cmd.Append(')'); } return true; } private bool AppendMultiRes(MultiRes r, StringBuilder cmd) { bool first = true; for (int i = 0; i < r.items.Length; i++) { int id = GetResourceId(r.items[i], false); if (id == 0) continue; if (!first) cmd.Append(','); first = false; cmd.Append(id); } if (first) return false; // none are in the store return true; } private bool WhereClause(Statement template, System.Text.StringBuilder cmd) { bool ww; return WhereClause(template.Subject, template.Predicate, template.Object, template.Meta, cmd, out ww); } private bool WhereClause(Resource templateSubject, Resource templatePredicate, Resource templateObject, Resource templateMeta, System.Text.StringBuilder cmd, out bool wroteWhere) { if (templateSubject == null && templatePredicate == null && templateObject == null && templateMeta == null) { wroteWhere = false; return true; } wroteWhere = true; cmd.Append(" WHERE "); if (templateSubject != null) if (!WhereItem("subject", templateSubject, cmd, false)) return false; if (templatePredicate != null) if (!WhereItem("predicate", templatePredicate, cmd, templateSubject != null)) return false; if (templateObject != null) if (!WhereItem("object", templateObject, cmd, templateSubject != null || templatePredicate != null)) return false; if (templateMeta != null) if (!WhereItem("meta", templateMeta, cmd, templateSubject != null || templatePredicate != null || templateObject != null)) return false; return true; } private int AsInt(object r) { if (r is int) return (int)r; if (r is uint) return (int)(uint)r; if (r is long) return (int)(long)r; if (r is string) return int.Parse((string)r); throw new ArgumentException(r.ToString()); } private string AsString(object r) { if (r == null) return null; else if (r is System.DBNull) return null; else if (r is string) return (string)r; else if (r is byte[]) return System.Text.Encoding.UTF8.GetString((byte[])r); else throw new FormatException("SQL store returned a literal value as " + r.GetType()); } private static void AppendComma(StringBuilder builder, string text, bool comma) { if (comma) builder.Append(','); builder.Append(text); } internal struct SelectColumnFilter { public bool SubjectId, PredicateId, ObjectId, MetaId; public bool SubjectUri, PredicateUri, ObjectData, MetaUri; } private static void SelectFilterColumns(SelectColumnFilter filter, StringBuilder cmd) { bool f = true; if (filter.SubjectId) { cmd.Append("q.subject"); f = false; } if (filter.PredicateId) { AppendComma(cmd, "q.predicate", !f); f = false; } if (filter.ObjectId) { AppendComma(cmd, "q.object", !f); f = false; } if (filter.MetaId) { AppendComma(cmd, "q.meta", !f); f = false; } if (filter.SubjectUri) { AppendComma(cmd, "suri.value", !f); f = false; } if (filter.PredicateUri) { AppendComma(cmd, "puri.value", !f); f = false; } if (filter.ObjectData) { AppendComma(cmd, "q.objecttype, ouri.value, lit.value, lit.language, lit.datatype", !f); f = false; } if (filter.MetaUri) { AppendComma(cmd, "muri.value", !f); f = false; } } private void SelectFilterTables(SelectColumnFilter filter, StringBuilder cmd) { if (filter.SubjectUri) { cmd.Append(" LEFT JOIN "); cmd.Append(table); cmd.Append("_entities AS suri ON q.subject = suri.id"); } if (filter.PredicateUri) { cmd.Append(" LEFT JOIN "); cmd.Append(table); cmd.Append("_entities AS puri ON q.predicate = puri.id"); } if (filter.ObjectData) { cmd.Append(" LEFT JOIN "); cmd.Append(table); cmd.Append("_entities AS ouri ON q.object = ouri.id"); cmd.Append(" LEFT JOIN "); cmd.Append(table); cmd.Append("_literals AS lit ON q.object=lit.id"); } if (filter.MetaUri) { cmd.Append(" LEFT JOIN "); cmd.Append(table); cmd.Append("_entities AS muri ON q.meta = muri.id"); } } public override void Select(SelectFilter filter, StatementSink result) { if (result == null) throw new ArgumentNullException(); foreach (Entity[] s in SplitArray(filter.Subjects)) foreach (Entity[] p in SplitArray(filter.Predicates)) foreach (Resource[] o in SplitArray(filter.Objects)) foreach (Entity[] m in SplitArray(filter.Metas)) { Select( ToMultiRes(s), ToMultiRes(p), ToMultiRes(o), ToMultiRes(m), filter.LiteralFilters, result, filter.Limit); // hmm, repeated } } Resource[][] SplitArray(Resource[] e) { int lim = 1000; if (e == null || e.Length <= lim) { if (e is Entity[]) return new Entity[][] { (Entity[])e }; else return new Resource[][] { e }; } int overflow = e.Length % lim; int n = (e.Length / lim) + ((overflow != 0) ? 1 : 0); Resource[][] ret; if (e is Entity[]) ret = new Entity[n][]; else ret = new Resource[n][]; for (int i = 0; i < n; i++) { int c = lim; if (i == n-1 && overflow != 0) c = overflow; if (e is Entity[]) ret[i] = new Entity[c]; else ret[i] = new Resource[c]; Array.Copy(e, i*lim, ret[i], 0, c); } return ret; } Resource ToMultiRes(Resource[] r) { if (r == null || r.Length == 0) return null; if (r.Length == 1) return r[0]; return new MultiRes(r); } private class MultiRes : Resource { public MultiRes(Resource[] a) { items = a; } public Resource[] items; public override string Uri { get { return null; } } public bool ContainsLiterals() { foreach (Resource r in items) if (r is Literal) return true; return false; } } void CacheMultiObjects(Hashtable entMap, Resource obj) { if (!(obj is MultiRes)) return; foreach (Resource r in ((MultiRes)obj).items) entMap[GetResourceId(r, false)] = r; } public override void Select(Statement template, StatementSink result) { if (result == null) throw new ArgumentNullException(); Select(template.Subject, template.Predicate, template.Object, template.Meta, null, result, 0); } private void Select(Resource templateSubject, Resource templatePredicate, Resource templateObject, Resource templateMeta, LiteralFilter[] litFilters, StatementSink result, int limit) { if (result == null) throw new ArgumentNullException(); lock (syncroot) { Init(); RunAddBuffer(); // Don't select on columns that we already know from the template. // But grab the URIs and literal values for MultiRes selection. SelectColumnFilter columns = new SelectColumnFilter(); columns.SubjectId = (templateSubject == null) || templateSubject is MultiRes; columns.PredicateId = (templatePredicate == null) || templatePredicate is MultiRes; columns.ObjectId = (templateObject == null) || templateObject is MultiRes; columns.MetaId = (templateMeta == null) || templateMeta is MultiRes; columns.SubjectUri = templateSubject == null; columns.PredicateUri = templatePredicate == null; columns.ObjectData = templateObject == null || (templateObject is MultiRes && ((MultiRes)templateObject).ContainsLiterals()); columns.MetaUri = templateMeta == null; // Meta URIs tend to be repeated a lot, so we don't // want to ever select them from the database. // This preloads them, although it makes the first // select quite slow. if (templateMeta == null && SupportsSubquery) { LoadMetaEntities(); columns.MetaUri = false; } // Have to select something if (!columns.SubjectId && !columns.PredicateId && !columns.ObjectId && !columns.MetaId) columns.SubjectId = true; // SQLite has a problem with LEFT JOIN: When a condition is made on the // first table in the ON clause (q.objecttype=0/1), when it fails, // it excludes the row from the first table, whereas it should only // exclude the results of the join. System.Text.StringBuilder cmd = new System.Text.StringBuilder("SELECT "); if (!SupportsNoDuplicates) cmd.Append("DISTINCT "); SelectFilterColumns(columns, cmd); cmd.Append(" FROM "); cmd.Append(table); cmd.Append("_statements AS q"); SelectFilterTables(columns, cmd); cmd.Append(' '); bool wroteWhere; if (!WhereClause(templateSubject, templatePredicate, templateObject, templateMeta, cmd, out wroteWhere)) return; // Transform literal filters into SQL. if (litFilters != null) { foreach (LiteralFilter f in litFilters) { string s = FilterToSQL(f, "lit.value"); if (s != null) { if (!wroteWhere) { cmd.Append(" WHERE "); wroteWhere = true; } else { cmd.Append(" AND "); } cmd.Append(' '); cmd.Append(s); } } } if (limit >= 1) { cmd.Append(" LIMIT "); cmd.Append(limit); } cmd.Append(';'); if (Debug) { string cmd2 = cmd.ToString(); //if (cmd2.Length > 80) cmd2 = cmd2.Substring(0, 80); Console.Error.WriteLine(cmd2); } Hashtable entMap = new Hashtable(); // Be sure if a MultiRes is involved we hash the // ids of the entities so we can return them // without creating new ones. CacheMultiObjects(entMap, templateSubject); CacheMultiObjects(entMap, templatePredicate); CacheMultiObjects(entMap, templateObject); CacheMultiObjects(entMap, templateMeta); using (IDataReader reader = RunReader(cmd.ToString())) { while (reader.Read()) { int col = 0; int sid = -1, pid = -1, ot = -1, oid = -1, mid = -1; string suri = null, puri = null, ouri = null, muri = null; string lv = null, ll = null, ld = null; if (columns.SubjectId) { sid = reader.GetInt32(col++); } if (columns.PredicateId) { pid = reader.GetInt32(col++); } if (columns.ObjectId) { oid = reader.GetInt32(col++); } if (columns.MetaId) { mid = reader.GetInt32(col++); } if (columns.SubjectUri) { suri = AsString(reader[col++]); } if (columns.PredicateUri) { puri = AsString(reader[col++]); } if (columns.ObjectData) { ot = reader.GetInt32(col++); ouri = AsString(reader[col++]); lv = AsString(reader[col++]); ll = AsString(reader[col++]); ld = AsString(reader[col++]);} if (columns.MetaUri) { muri = AsString(reader[col++]); } Entity subject = GetSelectedEntity(sid, suri, templateSubject, columns.SubjectId, columns.SubjectUri, entMap); Entity predicate = GetSelectedEntity(pid, puri, templatePredicate, columns.PredicateId, columns.PredicateUri, entMap); Resource objec = GetSelectedResource(oid, ot, ouri, lv, ll, ld, templateObject, columns.ObjectId, columns.ObjectData, entMap); Entity meta = GetSelectedEntity(mid, muri, templateMeta, columns.MetaId, columns.MetaUri, templateMeta != null ? entMap : metaEntities); if (litFilters != null && !LiteralFilter.MatchesFilters(objec, litFilters, this)) continue; bool ret = result.Add(new Statement(subject, predicate, objec, meta)); if (!ret) break; } } } // lock } Entity GetSelectedEntity(int id, string uri, Resource given, bool idSelected, bool uriSelected, Hashtable entMap) { if (!idSelected) return (Entity)given; if (!uriSelected) { Entity ent = (Entity)entMap[id]; if (ent != null) return ent; // had a URI so was precached, or was otherwise precached else // didn't have a URI return MakeEntity(id, null, entMap); } return MakeEntity(id, uri, entMap); } Resource GetSelectedResource(int id, int type, string uri, string lv, string ll, string ld, Resource given, bool idSelected, bool uriSelected, Hashtable entMap) { if (!idSelected) return (Resource)given; if (!uriSelected) return (Resource)entMap[id]; if (type == 0) return MakeEntity(id, uri, entMap); else return new Literal(lv, ll, ld); } private string FilterToSQL(LiteralFilter filter, string col) { if (filter is SemWeb.Filters.StringCompareFilter) { SemWeb.Filters.StringCompareFilter f = (SemWeb.Filters.StringCompareFilter)filter; return col + FilterOpToSQL(f.Type) + Escape(f.Pattern, true); } if (filter is SemWeb.Filters.StringContainsFilter) { SemWeb.Filters.StringContainsFilter f = (SemWeb.Filters.StringContainsFilter)filter; return col + " LIKE " + quote + "%" + Escape(f.Pattern, false).Replace("%", "\\%") + "%" + quote; } if (filter is SemWeb.Filters.StringStartsWithFilter) { SemWeb.Filters.StringStartsWithFilter f = (SemWeb.Filters.StringStartsWithFilter)filter; return col + " LIKE " + quote + Escape(f.Pattern, false).Replace("%", "\\%") + "%" + quote; } if (filter is SemWeb.Filters.NumericCompareFilter) { SemWeb.Filters.NumericCompareFilter f = (SemWeb.Filters.NumericCompareFilter)filter; return col + FilterOpToSQL(f.Type) + f.Number; } return null; } private string FilterOpToSQL(LiteralFilter.CompType op) { switch (op) { case LiteralFilter.CompType.LT: return " < "; case LiteralFilter.CompType.LE: return " <= "; case LiteralFilter.CompType.NE: return " <> "; case LiteralFilter.CompType.EQ: return " = "; case LiteralFilter.CompType.GT: return " > "; case LiteralFilter.CompType.GE: return " >= "; default: throw new ArgumentException(op.ToString()); } } private void LoadMetaEntities() { if (metaEntities != null) return; metaEntities = new Hashtable(); // this misses meta entities that are anonymous, but that's ok using (IDataReader reader = RunReader("select id, value from " + table + "_entities where id in (select distinct meta from " + table + "_statements)")) { while (reader.Read()) { int id = reader.GetInt32(0); string uri = reader.GetString(1); metaEntities[id] = MakeEntity(id, uri, null); } } } private string Escape(string str, bool quotes) { if (str == null) return "NULL"; StringBuilder b = new StringBuilder(); EscapedAppend(b, str, quotes); return b.ToString(); } protected void EscapedAppend(StringBuilder b, string str) { EscapedAppend(b, str, true); } protected virtual char GetQuoteChar() { return '\"'; } protected virtual void EscapedAppend(StringBuilder b, string str, bool quotes) { if (quotes) b.Append(quote); for (int i = 0; i < str.Length; i++) { char c = str[i]; switch (c) { case '\n': b.Append("\\n"); break; case '\\': case '\"': case '*': b.Append('\\'); b.Append(c); break; default: b.Append(c); break; } } if (quotes) b.Append(quote); } /*internal static void Escape(StringBuilder b) { b.Replace("\\", "\\\\"); b.Replace("\"", "\\\""); b.Replace("\n", "\\n"); b.Replace("%", "\\%"); b.Replace("*", "\\*"); }*/ public override void Import(StatementSource source) { if (source == null) throw new ArgumentNullException(); if (isImporting) throw new InvalidOperationException("Store is already importing."); Init(); RunAddBuffer(); cachedNextId = -1; addStatementBuffer = new StatementList(); BeginTransaction(); try { isImporting = true; base.Import(source); } finally { RunAddBuffer(); EndTransaction(); addStatementBuffer = null; isImporting = false; entityCache.Clear(); literalCache.Clear(); } } public override void Replace(Entity a, Entity b) { Init(); RunAddBuffer(); int id = GetResourceId(b, true); foreach (string col in fourcols) { StringBuilder cmd = new StringBuilder(); cmd.Append("UPDATE "); cmd.Append(table); cmd.Append("_statements SET "); cmd.Append(col); cmd.Append('='); cmd.Append(id); if (!WhereItem(col, a, cmd, false)) return; cmd.Append(';'); RunCommand(cmd.ToString()); } metaEntities = null; } public override void Replace(Statement find, Statement replacement) { if (find.AnyNull) throw new ArgumentNullException("find"); if (replacement.AnyNull) throw new ArgumentNullException("replacement"); if (find == replacement) return; Init(); RunAddBuffer(); int subj = GetResourceId(replacement.Subject, true); int pred = GetResourceId(replacement.Predicate, true); int objtype = ObjectType(replacement.Object); int obj = GetResourceId(replacement.Object, true); int meta = GetResourceId(replacement.Meta, true); StringBuilder cmd = cmdBuffer; cmd.Length = 0; cmd.Append("UPDATE "); cmd.Append(table); cmd.Append("_statements SET subject="); cmd.Append(subj); cmd.Append(", predicate="); cmd.Append(pred); cmd.Append(", objecttype="); cmd.Append(objtype); cmd.Append(", object="); cmd.Append(obj); cmd.Append(", meta="); cmd.Append(meta); cmd.Append(' '); if (!WhereClause(find, cmd)) return; RunCommand(cmd.ToString()); metaEntities = null; } protected abstract void RunCommand(string sql); protected abstract object RunScalar(string sql); protected abstract IDataReader RunReader(string sql); private int RunScalarInt(string sql, int def) { object ret = RunScalar(sql); if (ret == null) return def; if (ret is int) return (int)ret; try { return int.Parse(ret.ToString()); } catch (FormatException e) { return def; } } private string RunScalarString(string sql) { object ret = RunScalar(sql); if (ret == null) return null; if (ret is string) return (string)ret; if (ret is byte[]) return System.Text.Encoding.UTF8.GetString((byte[])ret); throw new FormatException("SQL store returned a literal value as " + ret); } public override void Close() { if (statementsRemoved) { RunCommand("DELETE FROM " + table + "_literals where (select count(*) from " + table + "_statements where object=id) = 0 and id > 0"); RunCommand("DELETE FROM " + table + "_entities where (select count(*) from " + table + "_statements where subject=id) = 0 and (select count(*) from " + table + "_statements where predicate=id) = 0 and (select count(*) from " + table + "_statements where object=id) = 0 and (select count(*) from " + table + "_statements where meta=id) = 0 ;"); } } protected virtual void CreateTable() { foreach (string cmd in GetCreateTableCommands(table)) { try { RunCommand(cmd); } catch (Exception e) { if (Debug) Console.Error.WriteLine(e); } } } protected virtual void CreateIndexes() { foreach (string cmd in GetCreateIndexCommands(table)) { try { RunCommand(cmd); } catch (Exception e) { if (Debug) Console.Error.WriteLine(e); } } } protected virtual void BeginTransaction() { } protected virtual void EndTransaction() { } internal static string[] GetCreateTableCommands(string table) { return new string[] { "CREATE TABLE " + table + "_statements" + "(subject int UNSIGNED NOT NULL, predicate int UNSIGNED NOT NULL, objecttype int NOT NULL, object int UNSIGNED NOT NULL, meta int UNSIGNED NOT NULL);", "CREATE TABLE " + table + "_literals" + "(id INT NOT NULL, value BLOB NOT NULL, language TEXT, datatype TEXT, hash BINARY(28), PRIMARY KEY(id));", "CREATE TABLE " + table + "_entities" + "(id INT NOT NULL, value BLOB NOT NULL, PRIMARY KEY(id));" }; } internal static string[] GetCreateIndexCommands(string table) { return new string[] { "CREATE UNIQUE INDEX subject_full_index ON " + table + "_statements(subject, predicate, object, meta, objecttype);", "CREATE INDEX predicate_index ON " + table + "_statements(predicate);", "CREATE INDEX object_index ON " + table + "_statements(object);", "CREATE INDEX meta_index ON " + table + "_statements(meta);", "CREATE UNIQUE INDEX literal_index ON " + table + "_literals(hash);", "CREATE UNIQUE INDEX entity_index ON " + table + "_entities(value(255));" }; } } }