/* db-reader.vala * * Copyright © 2011-2012 Collabora Ltd. * By Siegfried-Angel Gevatter Pujals * By Seif Lotfy * Copyright © 2011 Canonical Ltd. * By Michal Hruby * * Based upon a Python implementation (2009-2011) by: * Markus Korn * Mikkel Kamstrup Erlandsen * Seif Lotfy * Siegfried-Angel Gevatter Pujals * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 2.1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . * */ using Zeitgeist; using Zeitgeist.SQLite; using Zeitgeist.Utils; namespace Zeitgeist { public class DbReader : Object { public Zeitgeist.SQLite.Database database { get; construct; } protected unowned Sqlite.Database db; protected TableLookup interpretations_table; protected TableLookup manifestations_table; protected TableLookup mimetypes_table; protected TableLookup actors_table; public DbReader () throws EngineError { Object (database: new Zeitgeist.SQLite.Database.read_only ()); } construct { database.set_deletion_callback (delete_from_cache); db = database.database; try { interpretations_table = new TableLookup (database, "interpretation"); manifestations_table = new TableLookup (database, "manifestation"); mimetypes_table = new TableLookup (database, "mimetype"); actors_table = new TableLookup (database, "actor"); } catch (EngineError err) { // FIXME: propagate this properly? critical ("TableLookup initialization failed: %s", err.message); } database.set_cache_size (128); } protected Event get_event_from_row (Sqlite.Statement stmt, uint32 event_id) throws EngineError { Event event = new Event (); event.id = event_id; event.timestamp = stmt.column_int64 (EventViewRows.TIMESTAMP); event.interpretation = interpretations_table.get_value ( stmt.column_int (EventViewRows.INTERPRETATION)); event.manifestation = manifestations_table.get_value ( stmt.column_int (EventViewRows.MANIFESTATION)); event.actor = actors_table.get_value ( stmt.column_int (EventViewRows.ACTOR)); event.origin = stmt.column_text ( EventViewRows.EVENT_ORIGIN_URI); // Load payload unowned uint8[] data = (uint8[]) stmt.column_blob( EventViewRows.PAYLOAD); data.length = stmt.column_bytes(EventViewRows.PAYLOAD); if (data != null) { event.payload = new ByteArray(); event.payload.append(data); } return event; } protected Subject get_subject_from_row (Sqlite.Statement stmt) throws EngineError { Subject subject = new Subject (); subject.uri = stmt.column_text (EventViewRows.SUBJECT_URI); subject.text = stmt.column_text (EventViewRows.SUBJECT_TEXT); subject.storage = stmt.column_text (EventViewRows.SUBJECT_STORAGE); subject.origin = stmt.column_text (EventViewRows.SUBJECT_ORIGIN_URI); subject.current_uri = stmt.column_text ( EventViewRows.SUBJECT_CURRENT_URI); subject.current_origin = stmt.column_text ( EventViewRows.SUBJECT_CURRENT_ORIGIN_URI); subject.interpretation = interpretations_table.get_value ( stmt.column_int (EventViewRows.SUBJECT_INTERPRETATION)); subject.manifestation = manifestations_table.get_value ( stmt.column_int (EventViewRows.SUBJECT_MANIFESTATION)); subject.mimetype = mimetypes_table.get_value ( stmt.column_int (EventViewRows.SUBJECT_MIMETYPE)); return subject; } public GenericArray get_events(uint32[] event_ids, BusName? sender=null) throws EngineError { // TODO: Consider if we still want the cache. This should be done // once everything is working, since it adds unneeded complexity. // It'd also benchmark it again first, we may have better options // to enhance the performance of SQLite now, and event processing // will be faster now being C. if (event_ids.length == 0) return new GenericArray (); var sql_event_ids = database.get_sql_string_from_event_ids (event_ids); string sql = """ SELECT * FROM event_view WHERE id IN (%s) """.printf (sql_event_ids); Sqlite.Statement stmt; int rc = db.prepare_v2 (sql, -1, out stmt); database.assert_query_success (rc, "SQL error"); var events = new HashTable (direct_hash, direct_equal); // Create Events and Subjects from rows while ((rc = stmt.step ()) == Sqlite.ROW) { uint32 event_id = (uint32) stmt.column_int64 (EventViewRows.ID); Event? event = events.lookup (event_id); if (event == null) { event = get_event_from_row(stmt, event_id); events.insert (event_id, event); } Subject subject = get_subject_from_row(stmt); event.add_subject(subject); } database.assert_query_success (rc, "Error", Sqlite.DONE); // Sort events according to the sequence of event_ids var results = new GenericArray (); results.length = event_ids.length; int i = 0; foreach (var id in event_ids) { results.set(i++, events.lookup (id)); } return results; } public uint32[] find_event_ids_for_clause (WhereClause where, uint max_events, uint result_type) throws EngineError { string sql = "SELECT id FROM event_view "; string where_sql = ""; if (!where.is_empty ()) { where_sql = "WHERE " + where.get_sql_conditions (); } switch (result_type) { case ResultType.MOST_RECENT_EVENTS: sql += where_sql + " ORDER BY "; break; case ResultType.LEAST_RECENT_EVENTS: sql += where_sql + " ORDER BY "; break; case ResultType.MOST_RECENT_EVENT_ORIGIN: sql = group_and_sort ("origin", where); break; case ResultType.LEAST_RECENT_EVENT_ORIGIN: sql = group_and_sort ("origin", where); break; case ResultType.MOST_POPULAR_EVENT_ORIGIN: sql = group_and_sort ("origin", where, false); break; case ResultType.LEAST_POPULAR_EVENT_ORIGIN: sql = group_and_sort ("origin", where, true); break; case ResultType.MOST_RECENT_SUBJECTS: sql = group_and_sort ("subj_id", where); break; case ResultType.LEAST_RECENT_SUBJECTS: sql = group_and_sort ("subj_id", where); break; case ResultType.MOST_POPULAR_SUBJECTS: sql = group_and_sort ("subj_id", where, false); break; case ResultType.LEAST_POPULAR_SUBJECTS: sql = group_and_sort ("subj_id", where, true); break; case ResultType.MOST_RECENT_CURRENT_URI: sql = group_and_sort ("subj_id_current", where); break; case ResultType.LEAST_RECENT_CURRENT_URI: sql = group_and_sort ("subj_id_current", where); break; case ResultType.MOST_POPULAR_CURRENT_URI: sql = group_and_sort ("subj_id_current", where, false); break; case ResultType.LEAST_POPULAR_CURRENT_URI: sql = group_and_sort ("subj_id_current", where, true); break; case ResultType.MOST_RECENT_ACTOR: sql = group_and_sort ("actor", where); break; case ResultType.LEAST_RECENT_ACTOR: sql = group_and_sort ("actor", where); break; case ResultType.MOST_POPULAR_ACTOR: sql = group_and_sort ("actor", where, false); break; case ResultType.LEAST_POPULAR_ACTOR: sql = group_and_sort ("actor", where, true); break; case ResultType.OLDEST_ACTOR: sql = group_and_sort ("actor", where, null, "min"); break; case ResultType.MOST_RECENT_ORIGIN: sql = group_and_sort ("subj_origin", where); break; case ResultType.LEAST_RECENT_ORIGIN: sql = group_and_sort ("subj_origin", where); break; case ResultType.MOST_POPULAR_ORIGIN: sql = group_and_sort ("subj_origin", where, false); break; case ResultType.LEAST_POPULAR_ORIGIN: sql = group_and_sort ("subj_origin", where, true); break; case ResultType.MOST_RECENT_CURRENT_ORIGIN: sql = group_and_sort ("subj_origin_current", where); break; case ResultType.LEAST_RECENT_CURRENT_ORIGIN: sql = group_and_sort ("subj_origin_current", where); break; case ResultType.MOST_POPULAR_CURRENT_ORIGIN: sql = group_and_sort ("subj_origin_current", where, false); break; case ResultType.LEAST_POPULAR_CURRENT_ORIGIN: sql = group_and_sort ("subj_origin_current", where, true); break; case ResultType.MOST_RECENT_SUBJECT_INTERPRETATION: sql = group_and_sort ("subj_interpretation", where); break; case ResultType.LEAST_RECENT_SUBJECT_INTERPRETATION: sql = group_and_sort ("subj_interpretation", where); break; case ResultType.MOST_POPULAR_SUBJECT_INTERPRETATION: sql = group_and_sort ("subj_interpretation", where, false); break; case ResultType.LEAST_POPULAR_SUBJECT_INTERPRETATION: sql = group_and_sort ("subj_interpretation", where, true); break; case ResultType.MOST_RECENT_MIMETYPE: sql = group_and_sort ("subj_mimetype", where); break; case ResultType.LEAST_RECENT_MIMETYPE: sql = group_and_sort ("subj_mimetype", where); break; case ResultType.MOST_POPULAR_MIMETYPE: sql = group_and_sort ("subj_mimetype", where, false); break; case ResultType.LEAST_POPULAR_MIMETYPE: sql = group_and_sort ("subj_mimetype", where, true); break; default: string error_message = "Invalid ResultType."; warning (error_message); throw new EngineError.INVALID_ARGUMENT (error_message); } // complete the sort rule bool time_asc = ResultType.is_sort_order_asc ((ResultType) result_type); sql += " timestamp %s".printf ((time_asc) ? "ASC" : "DESC"); if (where.get_is_simple ()) sql = sql.replace ("FROM event_view", "FROM event"); int rc; Sqlite.Statement stmt; rc = db.prepare_v2 (sql, -1, out stmt); database.assert_query_success(rc, "SQL error"); var arguments = where.get_bind_arguments (); for (int i = 0; i < arguments.length; ++i) stmt.bind_text (i + 1, arguments[i]); #if EXPLAIN_QUERIES database.explain_query (stmt); #endif uint32[] event_ids = {}; while ((rc = stmt.step()) == Sqlite.ROW) { var event_id = (uint32) uint64.parse( stmt.column_text (EventViewRows.ID)); // Events are supposed to be contiguous in the database if (event_ids.length == 0 || event_ids[event_ids.length-1] != event_id) { event_ids += event_id; if (event_ids.length == max_events) break; } } if (rc != Sqlite.DONE && rc != Sqlite.ROW) { string error_message = "Error in find_event_ids: %d, %s".printf ( rc, db.errmsg ()); warning (error_message); database.assert_not_corrupt (rc); throw new EngineError.DATABASE_ERROR (error_message); } return event_ids; } public uint32[] find_event_ids (TimeRange time_range, GenericArray event_templates, uint storage_state, uint max_events, uint result_type, BusName? sender=null) throws EngineError { WhereClause where = get_where_clause_for_query (time_range, event_templates, storage_state); //if (!where.may_have_results ()) // return new uint32[0]; return find_event_ids_for_clause (where, max_events, result_type); } public GenericArray find_events (TimeRange time_range, GenericArray event_templates, uint storage_state, uint max_events, uint result_type, BusName? sender=null) throws EngineError { return get_events (find_event_ids (time_range, event_templates, storage_state, max_events, result_type)); } public WhereClause get_where_clause_for_query (TimeRange time_range, GenericArray event_templates, uint storage_state) throws EngineError { WhereClause where = new WhereClause (WhereClause.Type.AND); if (time_range.start != 0) where.add (("timestamp >= %" + int64.FORMAT).printf( time_range.start)); if (time_range.end != 0) where.add (("timestamp <= %" + int64.FORMAT).printf( time_range.end)); if (storage_state == StorageState.AVAILABLE || storage_state == StorageState.NOT_AVAILABLE) { where.add ("(subj_storage_state=? OR subj_storage_state IS NULL)", storage_state.to_string ()); where.set_is_simple (false); } else if (storage_state != StorageState.ANY) { throw new EngineError.INVALID_ARGUMENT( "Unknown storage state '%u'".printf(storage_state)); } WhereClause tpl_conditions = get_where_clause_from_event_templates ( event_templates); where.extend (tpl_conditions); return where; } private struct RelatedUri { public uint32 id; public int64 timestamp; public string uri; public int32 counter; } public string[] find_related_uris (TimeRange time_range, GenericArray event_templates, GenericArray result_event_templates, uint storage_state, uint max_results, uint result_type, BusName? sender=null) throws EngineError { /** * Return a list of subject URIs commonly used together with events * matching the given template, considering data from within the * indicated timerange. * Only URIs for subjects matching the indicated `result_event_templates` * and `result_storage_state` are returned. */ if (result_type == RelevantResultType.RECENT || result_type == RelevantResultType.RELATED) { // We pick out the ids for relational event so we can set them as // roots the ids are taken from the events that match the // events_templates uint32[] ids = find_event_ids (time_range, event_templates, storage_state, 0, ResultType.LEAST_RECENT_EVENTS); if (event_templates.length > 0 && ids.length == 0) { throw new EngineError.INVALID_ARGUMENT ( "No results found for the event_templates"); } // Pick out the result_ids for the filtered results we would like to // take into account the ids are taken from the events that match // the result_event_templates if no result_event_templates are set we // consider all results as allowed uint32[] result_ids; result_ids = find_event_ids (time_range, result_event_templates, storage_state, 0, ResultType.LEAST_RECENT_EVENTS); // From here we create several graphs with the maximum depth of 2 // and push all the nodes and vertices (events) in one pot together uint32[] pot = new uint32[ids.length + result_ids.length]; for (uint32 i=0; i < ids.length; i++) pot[i] = ids[i]; for (uint32 i=0; i < result_ids.length; i++) pot[ids.length + i] = result_ids[ids.length + i]; Sqlite.Statement stmt; var sql_event_ids = database.get_sql_string_from_event_ids (pot); string sql = """ SELECT id, timestamp, subj_uri FROM event_view WHERE id IN (%s) ORDER BY timestamp ASC """.printf (sql_event_ids); int rc = db.prepare_v2 (sql, -1, out stmt); database.assert_query_success(rc, "SQL error"); // FIXME: fix this ugly code var temp_related_uris = new GenericArray(); while ((rc = stmt.step()) == Sqlite.ROW) { RelatedUri ruri = RelatedUri(){ id = (uint32) uint64.parse(stmt.column_text (0)), timestamp = stmt.column_int64 (1), uri = stmt.column_text (2), counter = 0 }; temp_related_uris.add (ruri); } // RelatedUri[] related_uris = new RelatedUri[temp_related_uris.length]; // for (int i=0; i( str_hash, str_equal); for (int i = 0; i < temp_related_uris.length; i++) { var window = new GenericArray(); bool count_in_window = false; for (int j = int.max (0, i - 5); j < int.min (i, temp_related_uris.length); j++) { window.add(temp_related_uris[j]); if (temp_related_uris[j].id in ids) { count_in_window = true; break; } } if (count_in_window) { for (int j = 0; j < window.length; j++) { if (window[j].id in result_ids) { if (uri_counter.lookup (window[j].uri) == null) { RelatedUri ruri = RelatedUri () { id = window[j].id, timestamp = window[j].timestamp, uri = window[j].uri, counter = 0 }; uri_counter.insert (window[j].uri, ruri); } uri_counter.lookup (window[j].uri).counter++; if (uri_counter.lookup (window[j].uri).timestamp < window[j].timestamp) { uri_counter.lookup (window[j].uri).timestamp = window[j].timestamp; } } } } } // We have the big hashtable with the structs, now we sort them by // most used and limit the result then sort again List temp_ruris = new List(); List values = new List(); foreach (var uri in uri_counter.get_values()) values.append(uri); values.sort ((a, b) => a.counter - b.counter); values.sort ((a, b) => { int64 delta = a.timestamp - b.timestamp; if (delta < 0) return 1; else if (delta > 0) return -1; else return 0; }); foreach (RelatedUri ruri in values) { if (temp_ruris.length() < max_results) temp_ruris.append(ruri); else break; } // Sort by recency if (result_type == RelevantResultType.RECENT) temp_ruris.sort ((a, b) => { int64 delta = a.timestamp - b.timestamp; if (delta < 0) return 1; else if (delta > 0) return -1; else return 0;}); string[] results = new string[temp_ruris.length()]; int i = 0; foreach (var uri in temp_ruris) { results[i] = uri.uri; #if EXPLAIN_QUERIES stdout.printf("%i %lld %s\n", uri.counter, uri.timestamp, uri.uri); #endif i++; } return results; } else { throw new EngineError.DATABASE_ERROR ("Unsupported RelevantResultType"); } } /** * Clear all resources Engine is using (close database connection, etc.). * * After executing this method on an instance, no other function * may be called. */ public virtual void close () { database.close (); } // Used by find_event_ids private string group_and_sort (string field, WhereClause where, bool? count_asc=null, string aggregation_type="max") { string aggregation_sql = ""; string order_sql = ""; string where_sql = where.get_sql_conditions (); if (count_asc != null) { aggregation_sql = ", COUNT(%s) AS num_events".printf (field); order_sql = "num_events %s,".printf ((count_asc) ? "ASC" : "DESC"); } if (count_asc != null || !where.has_non_timestamp_condition()) { return """ SELECT id FROM event NATURAL JOIN ( SELECT %s, %s(timestamp) AS timestamp %s FROM event_view WHERE %s GROUP BY %s) GROUP BY %s ORDER BY %s """.printf ( field, aggregation_type, aggregation_sql, where_sql, field, field, order_sql); } return """ SELECT id, %s(timestamp) AS timestamp FROM event_view WHERE %s AND %s IS NOT NULL GROUP BY %s ORDER BY """.printf ( aggregation_type, where_sql, field, field); } // Used by find_event_ids public WhereClause get_where_clause_from_event_templates ( GenericArray templates) throws EngineError { WhereClause where = new WhereClause (WhereClause.Type.OR); for (int i = 0; i < templates.length; ++i) { Event event_template = templates[i]; where.extend ( get_where_clause_from_event_template (event_template)); } return where; } // Used by get_where_clause_from_event_templates private WhereClause get_where_clause_from_event_template (Event template) throws EngineError { WhereClause where = new WhereClause (WhereClause.Type.AND); // Event ID if (template.id != 0) where.add ("id=?", template.id.to_string()); // Interpretation if (!is_empty_string (template.interpretation)) { assert_no_wildcard ("interpretation", template.interpretation); WhereClause subwhere = get_where_clause_for_symbol ( "interpretation", template.interpretation, interpretations_table); if (!subwhere.is_empty ()) where.extend (subwhere); } // Manifestation if (!is_empty_string (template.manifestation)) { assert_no_wildcard ("manifestation", template.interpretation); WhereClause subwhere = get_where_clause_for_symbol ( "manifestation", template.manifestation, manifestations_table); if (!subwhere.is_empty ()) where.extend (subwhere); } // Actor if (!is_empty_string (template.actor)) { string val = template.actor; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); if (like) where.add_wildcard_condition ("actor", val, negated); else where.add_match_condition ("actor", actors_table.id_try_string (val), negated); } // Origin if (!is_empty_string (template.origin)) { string val = template.origin; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); assert_no_noexpand (val, "origin"); if (like) where.add_wildcard_condition ("origin", val, negated); else where.add_text_condition_subquery ("origin", val, negated); } // Subject templates within the same event template are AND'd // See LP bug #592599. for (int i = 0; i < template.num_subjects(); ++i) { Subject subject_template = template.subjects[i]; // Subject interpretation if (!is_empty_string (subject_template.interpretation)) { assert_no_wildcard ("subject interpretation", template.interpretation); WhereClause subwhere = get_where_clause_for_symbol ( "subj_interpretation", subject_template.interpretation, interpretations_table); if (!subwhere.is_empty ()) where.extend (subwhere); } // Subject manifestation if (!is_empty_string (subject_template.manifestation)) { assert_no_wildcard ("subject manifestation", subject_template.manifestation); WhereClause subwhere = get_where_clause_for_symbol ( "subj_manifestation", subject_template.manifestation, manifestations_table); if (!subwhere.is_empty ()) where.extend (subwhere); } // Mime-Type if (!is_empty_string (subject_template.mimetype)) { string val = subject_template.mimetype; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); assert_no_noexpand (val, "mime-type"); if (like) where.add_wildcard_condition ( "subj_mimetype", val, negated); else where.add_match_condition ("subj_mimetype", mimetypes_table.id_try_string (val), negated); } // URI if (!is_empty_string (subject_template.uri)) { string val = subject_template.uri; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); assert_no_noexpand (val, "uri"); if (like) where.add_wildcard_condition ("subj_id", val, negated); else where.add_text_condition_subquery ("subj_id", val, negated); } // Origin if (!is_empty_string (subject_template.origin)) { string val = subject_template.origin; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); assert_no_noexpand (val, "subject origin"); if (like) where.add_wildcard_condition ( "subj_origin", val, negated); else where.add_text_condition_subquery ( "subj_origin", val, negated); } // Text if (!is_empty_string (subject_template.text)) { // Negation, noexpand and prefix search aren't supported // for subject texts, but "!", "+" and "*" are valid as // plain text characters. where.add_text_condition_subquery ("subj_text_id", subject_template.text, false); } // Current URI if (!is_empty_string (subject_template.current_uri)) { string val = subject_template.current_uri; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); assert_no_noexpand (val, "current_uri"); if (like) where.add_wildcard_condition ( "subj_id_current", val, negated); else where.add_text_condition_subquery ( "subj_id_current", val, negated); } // Current Origin if (!is_empty_string (subject_template.current_origin)) { string val = subject_template.current_origin; bool like = parse_wildcard (ref val); bool negated = parse_negation (ref val); assert_no_noexpand (val, "current_origin"); if (like) where.add_wildcard_condition ( "subj_origin_current", val, negated); else where.add_text_condition_subquery ( "subj_origin_current", val, negated); } // Subject storage if (!is_empty_string (subject_template.storage)) { // FIXME: do we want to support negation? string val = subject_template.storage; assert_no_negation ("subject storage", val); assert_no_wildcard ("subject storage", val); assert_no_noexpand (val, "subject storage"); where.add_text_condition_subquery ("subj_storage_id", val); } } return where; } // Used by get_where_clause_from_event_templates /** * If the value starts with the negation operator, throw an * error. */ protected void assert_no_negation (string field, string? val) throws EngineError { if (val == null || !val.has_prefix ("!")) return; string error_message = "Field '%s' doesn't support negation".printf (field); warning (error_message); throw new EngineError.INVALID_ARGUMENT (error_message); } // Used by get_where_clause_from_event_templates /** * If the value starts with the negation operator, throw an * error. */ protected void assert_no_noexpand (string field, string? val) throws EngineError { if (val == null || !val.has_prefix ("+")) return; string error_message = "Field '%s' doesn't support the no-expand operator".printf (field); warning (error_message); throw new EngineError.INVALID_ARGUMENT (error_message); } // Used by get_where_clause_from_event_templates /** * If the value ends with the wildcard character, throw an error. */ protected void assert_no_wildcard (string field, string? val) throws EngineError { if (val == null || !val.has_suffix ("*")) return; string error_message = "Field '%s' doesn't support prefix search".printf (field); warning (error_message); throw new EngineError.INVALID_ARGUMENT (error_message); } protected WhereClause get_where_clause_for_symbol (string table_name, string symbol, TableLookup lookup_table) throws EngineError { string _symbol = symbol; bool negated = parse_negation (ref _symbol); bool noexpand = parse_noexpand (ref _symbol); List symbols; if (noexpand) symbols = new List (); else symbols = Symbol.get_all_children (_symbol); symbols.prepend (_symbol); WhereClause subwhere = new WhereClause( WhereClause.Type.OR, negated); if (symbols.length () == 1) { subwhere.add_match_condition (table_name, lookup_table.id_try_string (_symbol)); } else { string sql = "(%s)"; string sub_sql = ""; bool first = true; foreach (unowned string uri in symbols) { if (!first) sub_sql = sub_sql + " OR "; sub_sql = sub_sql + "%s = %i ".printf (table_name, lookup_table.id_try_string (uri)); first = false; } sql = sql.printf (sub_sql); subwhere.add(sql); } return subwhere; } private void delete_from_cache (string table, int64 rowid) { TableLookup table_lookup; if (table == "interpretation") table_lookup = interpretations_table; else if (table == "manifestation") table_lookup = manifestations_table; else if (table == "mimetype") table_lookup = mimetypes_table; else if (table == "actor") table_lookup = actors_table; else return; table_lookup.remove((int) rowid); } } } // vim:expandtab:ts=4:sw=4