# -*- coding: utf-8 -*-
# Owlready2
# Copyright (C) 2017-2019 Jean-Baptiste LAMY
# LIMICS (Laboratoire d'informatique médicale et d'ingénierie des connaissances en santé), UMR_S 1142
# University Paris 13, Sorbonne paris-Cité, Bobigny, France

# 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 3 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 Lesser 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 <http://www.gnu.org/licenses/>.

import sys, os, os.path, sqlite3, time, re
from collections import defaultdict
from itertools import chain

import owlready2
from owlready2.base import *
from owlready2.driver import BaseMainGraph, BaseSubGraph
from owlready2.driver import _guess_format, _save
from owlready2.util import FTS, _LazyListMixin
from owlready2.base import _universal_abbrev_2_iri

if   (sqlite3.sqlite_version == "3.40.0") or (sqlite3.sqlite_version == "3.41.2"):
  print("\nWarning: SQLite3 version 3.40.0 and 3.41.2 have huge performance regressions; please install version 3.41.1 or 3.42!\n", file = sys.stderr)

def all_combinations(l):
  """returns all the combinations of the sublist in the given list (i.e. l[0] x l[1] x ... x l[n])."""
  if len(l) == 0: return ()
  if len(l) == 1: return [(a,) for a in l[0]]
  r = []
  for a in l[0]: r.extend((a,) + b for b in all_combinations(l[1:]))
  return r


class _Connexion(object):
  def __init__(self, pool, uri):
    self.pool    = pool
    self.db      = sqlite3.connect(uri, check_same_thread = False, uri = True)
    self.execute = self.db.execute
    #self.execute("PRAGMA journal_mode=WAL")
    #self.execute("PRAGMA read_uncommitted=1")
    
  def __enter__(self): return self
  def __exit__(self, exc_type = None, exc_val = None, exc_tb = None): self.pool.queue.put(self)
  
class _ConnexionPool(object):
  def __init__(self, uri, nb_connexion = 3):
    #import gevent.queue; self.queue = gevent.queue.Queue()
    import queue; self.queue = queue.Queue()
    #import multiprocessing; self.queue = multiprocessing.Queue()
    for i in range(nb_connexion): self.queue.put(_Connexion(self, uri))
    self.get = self.queue.get
    

class Graph(BaseMainGraph):
  _SUPPORT_CLONING = True
  def __init__(self, filename, clone = None, exclusive = True, sqlite_tmp_dir = "", world = None, profiling = False, read_only = False, enable_thread_parallelism = False, lock = None, extra_lock = None, connection = None, journal_mode = None):
    exists        = os.path.exists(filename) and os.path.getsize(filename) # BEFORE creating db!
    initialize_db = (clone is None) and ((filename == ":memory:") or (not exists))
    
    if clone and (filename != ":memory:"):
      if exists: raise ValueError("Cannot save existent quadstore in '%s': File already exists! Use a new filename for saving quadstore or, for opening an already existent quadstore, do not create any triple before calling set_backend() (including creating an empty ontology or loading a module that does so)." % filename)
      
    if sqlite_tmp_dir: os.environ["SQLITE_TMPDIR"] = sqlite_tmp_dir
    
    self.filename = filename
    options = []
    if filename == ":memory:":
      filename = str(id(self))
      options.append("mode=memory")
      
    self.read_only = read_only
    if read_only: options.append("mode=ro")
    #if enable_thread_parallelism: options.append("cache=shared") # Actually reduces performance, because of synchronization for accessing the shared cache!
    
    uri = "file:%s" % filename
    if options: uri = "%s?%s" % (uri, "&".join(options))

    self.db = connection or sqlite3.connect(uri, isolation_level = "EXCLUSIVE" if exclusive else "DEFERRED", check_same_thread = False, uri = True)
    
    #print("XXX", filename, connection, read_only)
    if exclusive: self.db.execute("""PRAGMA locking_mode = EXCLUSIVE""")
    if exclusive and read_only: self.db.execute("""PRAGMA read_uncommitted = True""") # Exclusive + no write => no need for read lock
    #if read_only: self.db.execute("""PRAGMA query_only = 1""") # No, because we may need to create TEMP tables (SPARQL module does that)
    
    self.db.execute("""PRAGMA cache_size = -200000""")
    self.db.execute("""PRAGMA mmap_size = 30000000000""")
    self.db.execute("""PRAGMA page_size = 32768""")
    if journal_mode: self.db.execute("""PRAGMA journal_mode = %s""" % journal_mode)
    
    if sqlite_tmp_dir:
      try: self.db.execute("""PRAGMA temp_store_directory = '%s'""" % sqlite_tmp_dir)
      except: pass # Deprecated PRAGMA
    else:
      self.db.execute("""PRAGMA temp_store = memory""")
      
    if profiling:
      import time
      from collections import Counter
      self.requests_counts = Counter()
      self.requests_times  = Counter()
      
      def execute(s, args = ()):
        if "SELECT" in s:
          self.requests_counts[s] += 1
          t0 = time.time()
          r = list(self.db.execute(s, args))
          t = time.time() - t0
          self.requests_times[s] += t
        return self.db.execute(s, args)
      self.execute = execute
      
      def reset_profiling():
        self.requests_counts = Counter()
        self.requests_times  = Counter()
      self.reset_profiling = reset_profiling
      
      def show_profiling():
        print(file = sys.stderr)
        print("Request counts:", file = sys.stderr)
        for s, nb in self.requests_counts.most_common():
          print(" ", nb, "\t", s.replace("\n", " "), file = sys.stderr)
        print(file = sys.stderr)
        print("Request total times:", file = sys.stderr)
        for s, nb in self.requests_times.most_common():
          print(" ", nb, "\t", s.replace("\n", " "), file = sys.stderr)
        print(file = sys.stderr)
        print("Request mean times:", file = sys.stderr)
        rmt = Counter()
        for s, nb in self.requests_counts.most_common():
          rmt[s] = self.requests_times[s] / nb
        for s, nb in rmt.most_common():
          print(" ", nb, "\t", s.replace("\n", " "), file = sys.stderr)
      self.show_profiling = show_profiling
      
    else:
      self.execute  = self.db.execute
      
    self.has_thread_parallelism = enable_thread_parallelism
    if enable_thread_parallelism:
      if exclusive: raise ValueError("Cannot enable thread parallelism with exclusive mode! Please add 'exclusive=False'.")
      self.has_thread_parallelism = True
      self.connexion_pool  = _ConnexionPool(uri)
      
    self.c_2_onto          = {}
    self.onto_2_subgraph   = {}
    self.world             = world
    self.c                 = None
    self.nb_added_triples  = 0
    
    if   lock:
      self.lock = lock
      self.acquire_write_lock = self._acquire_write_lock_with_lock
      self.release_write_lock = self._release_write_lock_with_lock
    elif extra_lock:
      self.lock = extra_lock
      self.acquire_write_lock = self._acquire_write_lock_with_extra_lock
      self.release_write_lock = self._release_write_lock_with_extra_lock
    elif read_only:
      import multiprocessing
      self.lock = multiprocessing.RLock()
      self.acquire_write_lock = self._acquire_write_lock_with_lock
      self.release_write_lock = self._release_write_lock_with_lock
      
    self.lock_level = 0
    
    if initialize_db:
      self.prop_fts = set()
      
      self.execute("""CREATE TABLE store (version INTEGER, current_blank INTEGER, current_resource INTEGER)""")
      self.execute("""INSERT INTO store VALUES (12, 0, 300)""")
      self.execute("""CREATE TABLE objs (c INTEGER, s INTEGER, p INTEGER, o INTEGER)""")
      self.execute("""CREATE TABLE datas (c INTEGER, s INTEGER, p INTEGER, o BLOB, d INTEGER)""")
      self.execute("""CREATE VIEW quads AS SELECT c,s,p,o,NULL AS d FROM objs UNION ALL SELECT c,s,p,o,d FROM datas""")
      self.execute("""CREATE VIEW quads2 AS SELECT c,s,p,o,'o' AS d FROM objs UNION ALL SELECT c,s,p,o,d FROM datas""") # AVoid using NULL because, in SQL, NULL != NULL.
      
      self.execute("""CREATE TABLE ontologies (c INTEGER PRIMARY KEY, iri TEXT, last_update DOUBLE)""")
      self.execute("""CREATE TABLE ontology_alias (iri TEXT, alias TEXT)""")
      self.execute("""CREATE TABLE prop_fts (storid INTEGER)""")
      try:
        self.execute("""CREATE TABLE resources (storid INTEGER PRIMARY KEY, iri TEXT) WITHOUT ROWID""")
      except sqlite3.OperationalError: # Old SQLite3 does not support WITHOUT ROWID -- here it is just an optimization
        self.execute("""CREATE TABLE resources (storid INTEGER PRIMARY KEY, iri TEXT)""")
      self.db.executemany("INSERT INTO resources VALUES (?,?)", _universal_abbrev_2_iri.items())
      self.execute("""CREATE UNIQUE INDEX index_resources_iri ON resources(iri)""")
      
      self.execute("""CREATE INDEX index_objs_sp ON objs(s,p)""")
      self.execute("""CREATE UNIQUE INDEX index_objs_op ON objs(o,p,c,s)""") # c is for onto.classes(), etc
      self.execute("""CREATE INDEX index_objs_c ON objs(c)""")
      
      self.execute("""CREATE INDEX index_datas_sp ON datas(s,p)""")
      self.execute("""CREATE UNIQUE INDEX index_datas_op ON datas(o,p,c,d,s)""")
      self.execute("""CREATE INDEX index_datas_c ON datas(c)""")
      self.indexed = True
      
      self.execute("""CREATE TABLE last_numbered_iri(prefix TEXT, i INTEGER)""")
      self.execute("""CREATE INDEX index_last_numbered_iri ON last_numbered_iri(prefix)""")
      
      self.analyze()
      self.db.commit()
      
    else:
      self.indexed = True
      if clone:
        s = "\n".join(clone.db.iterdump())
        self.db.cursor().executescript(s)
        
      version = self.execute("SELECT version FROM store").fetchone()[0]
      if version < 12:
        from owlready2.triplelite_update import update_graph
        update_graph(self, version)      
        
      self.prop_fts = { storid for (storid,) in self.execute("""SELECT storid FROM prop_fts;""") }
      
      self.analyze()
      
    # Unindexed table for deprioritizing SPARQL subqueries
    self.execute("""DROP TABLE IF EXISTS one""")
    self.execute("""CREATE TEMP TABLE one (i INTEGER)""")
    self.execute("""INSERT INTO one VALUES (1)""")
    
    self.current_changes = self.db.total_changes
    self.select_abbreviate_method()

  #def execute_long_with_gevent(self, sql, args = ()):
  #  with self.connexion_pool.get() as db:
  #    return self._get_gevent_hub().threadpool.apply(db.execute, (sql, args))
  
  def __del__(self):
    self.db.close()
    
  def get_wal_mode(self):
    mode = list(self.execute("PRAGMA journal_mode"))[0][0]
    if mode == "delete": return False
    return mode
  
  def set_wal_mode(self, mode):
    if mode == True: mode = "wal"
    self.execute("PRAGMA journal_mode=%s" % mode)
    
  def analyze(self):
    self.nb_added_triples = 0
    
    if self.read_only: return
    if sqlite3.sqlite_version_info[1] < 33: return # ANALYZE sqlite_schema not supported

    #self.db.execute("""PRAGMA cache_size = -100""") # The two following queries are * faster * with a small cache!
    #import time
    #t0 = time.perf_counter()
    nb_datas = self.execute("""SELECT COUNT() FROM datas INDEXED BY index_datas_c""").fetchone()[0]
    #nb_datas = self.execute("""SELECT MAX(rowid) FROM datas""").fetchone()[0] or 10
    #if nb_datas: print(nb_datas, time.perf_counter() - t0)
    #t0 = time.perf_counter()
    nb_objs  = self.execute("""SELECT COUNT() FROM objs INDEXED BY index_objs_c""" ).fetchone()[0]
    #nb_objs  = self.execute("""SELECT MAX(rowid) FROM objs""" ).fetchone()[0] or 10
    #if nb_objs: print(nb_objs, time.perf_counter() - t0)
    #self.db.execute("""PRAGMA cache_size = -200000""")
    nb_iris  = self.execute("""SELECT MAX(storid) FROM resources""" ).fetchone()[0] or 300
    
    #nb_datas, nb_objs, nb_iris = 1889602, 1469278, 457914
    #nb_datas, nb_objs, nb_iris = 506257, 520138, 172489
    #nb_datas, nb_objs, nb_iris = 1889602, 1069278, 172489

    #print("ANALYZE", nb_datas, nb_objs, nb_iris)
    #nb_p  = self.execute("""SELECT COUNT() FROM objs WHERE p=6 AND o IN (13,14,15)""" ).fetchone()[0] or 1
    #print("NB p", nb_p)
    
    try:
      self.execute("""DELETE FROM sqlite_stat1""")
    except:
      self.execute("""PRAGMA analysis_limit = 20""")
      self.execute("""ANALYZE""")
      self.execute("""DELETE FROM sqlite_stat1""")
      
    self.execute("""INSERT INTO sqlite_stat1 VALUES
('objs', 'index_objs_op', '%s 4 3 3 1'),
('objs', 'index_objs_sp', '%s 3 2'),
('datas', 'index_datas_op', '%s 4 3 3 3 1'),
('datas', 'index_datas_sp', '%s 3 2'),
('resources', 'index_resources_iri', '%s 1'),
('resources', 'resources', '%s 1')
""" % (nb_objs, nb_objs, nb_datas, nb_datas, nb_iris, nb_iris))

    #self.execute("""PRAGMA analysis_limit = 100""")
    #self.execute("""ANALYZE""")
    self.execute("""ANALYZE sqlite_schema""")
    
  def set_indexed(self, indexed): pass
  
  def close(self):
    self.db.close()
    
  def acquire_write_lock(self):
    if not self.db.in_transaction: self.execute("BEGIN IMMEDIATE")
    self.lock_level += 1
  def release_write_lock(self):
    self.lock_level -= 1
  def _acquire_write_lock_with_lock(self):
    self.lock.acquire()
    self.lock_level += 1
  def _release_write_lock_with_lock(self):
    self.lock.release()
    self.lock_level -= 1
  def _acquire_write_lock_with_extra_lock(self):
    self.lock.acquire()
    if not self.db.in_transaction: self.execute("BEGIN IMMEDIATE")
    self.lock_level += 1
  def _release_write_lock_with_extra_lock(self):
    self.lock.release()
    self.lock_level -= 1
  def has_write_lock(self): return self.lock_level
  
  def select_abbreviate_method(self):
    if self.world:
      self.world._abbreviate   = self._abbreviate
      self.world._unabbreviate = self._unabbreviate
    for subgraph in self.onto_2_subgraph.values():
      subgraph.onto._abbreviate   = subgraph._abbreviate   = self._abbreviate
      subgraph.onto._unabbreviate = subgraph._unabbreviate = self._unabbreviate
      
  def fix_base_iri(self, base_iri, c = None):
    if base_iri.endswith("#") or base_iri.endswith("/"): return base_iri
    use_slash = self.execute("SELECT iri FROM resources WHERE iri=? LIMIT 1", ("%s/" % base_iri,)).fetchone()
    if use_slash: return "%s/" % base_iri
    use_hash = self.execute("SELECT resources.iri FROM resources WHERE SUBSTR(resources.iri, 1, ?)=? LIMIT 1", (len(base_iri) + 1, "%s#" % base_iri,)).fetchone()
    if use_hash: return "%s#" % base_iri
    use_slash = self.execute("SELECT resources.iri FROM resources WHERE SUBSTR(resources.iri, 1, ?)=? LIMIT 1", (len(base_iri) + 1, "%s/" % base_iri,)).fetchone()
    if use_slash: return "%s/" % base_iri
    return "%s#" % base_iri
    
  def sub_graph(self, onto):
    new_in_quadstore = False
    c = self.execute("SELECT c FROM ontologies WHERE iri=?", (onto._base_iri,)).fetchone()
    if c is None:
      c = self.execute("SELECT ontologies.c FROM ontologies, ontology_alias WHERE ontology_alias.alias=? AND ontologies.iri=ontology_alias.iri", (onto._base_iri,)).fetchone()
      if c is None:
        new_in_quadstore = True
        self.execute("INSERT INTO ontologies VALUES (NULL, ?, 0)", (onto._base_iri,))
        c = self.execute("SELECT c FROM ontologies WHERE iri=?", (onto._base_iri,)).fetchone()
    c = c[0]
    self.c_2_onto[c] = onto
    
    return SubGraph(self, onto, c, self.db), new_in_quadstore
  
  def ontologies_iris(self):
    for (iri,) in self.execute("SELECT iri FROM ontologies").fetchall(): yield iri
    
  def _abbreviate(self, iri, create_if_missing = True):
    r = self.execute("SELECT storid FROM resources WHERE iri=? LIMIT 1", (iri,)).fetchone()
    if r: return r[0]
    if create_if_missing:
      #storid = max(self.execute("SELECT MAX(storid)+1 FROM resources").fetchone()[0], 301) # First 300 values are reserved
      storid = self.execute("UPDATE store SET current_resource=current_resource+1").execute("SELECT current_resource FROM store").fetchone()[0]
      
      self.execute("INSERT INTO resources VALUES (?,?)", (storid, iri))
      return storid
    
  def _unabbreviate(self, storid):
    return self.execute("SELECT iri FROM resources WHERE storid=? LIMIT 1", (storid,)).fetchone()[0]
  
  def get_storid_dict(self):
    return dict(self.execute("SELECT storid, iri FROM resources").fetchall())
  
  def _new_numbered_iri_2(self, prefix):
    cur = self.execute("SELECT iri FROM resources WHERE iri GLOB ? ORDER BY LENGTH(iri) DESC, iri DESC", ("%s*" % prefix,))
    while True:
      iri = cur.fetchone()
      if not iri:
        self.execute("""INSERT INTO last_numbered_iri VALUES (?,?)""", (prefix, 1))
        return "%s1" % prefix
      num = iri[0][len(prefix):]
      if num.isdigit():
        i = int(num) + 1
        self.execute("""INSERT INTO last_numbered_iri VALUES (?,?)""", (prefix, i))
        return "%s%s" % (prefix, i)
      
  def _new_numbered_iri(self, prefix):
    i = self.execute("""SELECT i FROM last_numbered_iri WHERE prefix=?""", (prefix,)).fetchone()
    if i is None: return self._new_numbered_iri_2(prefix)
    
    i = i[0] + 1
    iri = "%s%s" % (prefix, i)
    if self.execute("""SELECT storid FROM resources WHERE iri=?""", (iri,)).fetchone(): # Already exists, due to a name clash, e.g. "c1" + "1" vs "c" + "11"
      return self._new_numbered_iri_2(prefix)
    
    self.execute("""UPDATE last_numbered_iri SET i=? WHERE prefix=?""", (i, prefix))
    return iri
    
  
  def _refactor(self, storid, new_iri):
    self.execute("UPDATE resources SET iri=? WHERE storid=?", (new_iri, storid,))
    
  def _refactor_onto(self, storid, old_base_iri, new_base_iri):
    self._refactor(storid, new_base_iri)

    if old_base_iri.endswith("#"):
      self.execute("UPDATE resources SET iri=?||SUBSTR(iri,?) WHERE SUBSTR(iri,1,?)=?", (new_base_iri, len(old_base_iri) + 1, len(old_base_iri), old_base_iri))
    else:
      self.execute("UPDATE resources SET iri=?||SUBSTR(iri,?) WHERE SUBSTR(iri,1,?)=? AND (NOT INSTR(SUBSTR(iri,?), '/')) AND (NOT INSTR(SUBSTR(iri,?), '#'))", (new_base_iri, len(old_base_iri) + 1, len(old_base_iri), old_base_iri, len(old_base_iri) + 1, len(old_base_iri) + 1))
      
    
  def has_changes(self): return self.current_changes != self.db.total_changes

  def commit(self):
    if (self.current_changes != self.db.total_changes) or self.db.in_transaction:
      self.current_changes = self.db.total_changes
      self.db.commit()
      
  def context_2_user_context(self, c):
    user_c = self.c_2_onto.get(c)
    if user_c is None:
      iri = self.execute("SELECT iri FROM ontologies WHERE c=? LIMIT 1", (c,)).fetchone()[0]
      return self.world.get_ontology(iri)
    return user_c
  
  def new_blank_node(self):
    #blank = self.execute("SELECT current_blank+1 FROM store").fetchone()[0]
    #self.execute("UPDATE store SET current_blank=?", (blank,))
    blank = self.execute("UPDATE store SET current_blank=current_blank+1").execute("SELECT current_blank FROM store").fetchone()[0]
    return -blank
    
  def _get_obj_triples_spo_spo(self, s, p, o):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs")
        else:         cur = self.execute("SELECT s,p,o FROM objs WHERE o=?", (o,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE p=?", (p,))
        else:         cur = self.execute("SELECT s,p,o FROM objs WHERE p=? AND o=?", (p, o,))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE s=?", (s,))
        else:         cur = self.execute("SELECT s,p,o FROM objs WHERE s=? AND o=?", (s, o,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE s=? AND p=?", (s, p,))
        else:         cur = self.execute("SELECT s,p,o FROM objs INDEXED BY index_objs_sp WHERE s=? AND p=? AND o=?", (s, p, o,))
    return cur.fetchall()
  
  def _get_data_triples_spod_spod(self, s, p, o, d):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas")
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE o=?", (o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE o=? AND d=?", (o,d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE p=?", (p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE p=? AND o=?", (p, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE p=? AND o=? AND d=?", (p, o, d,))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE s=?", (s,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE s=? AND o=?", (s, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE s=? AND o=? AND d=?", (s, o, d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE s=? AND p=?", (s, p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE s=? AND p=? AND o=?", (s, p, o))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE s=? AND p=? AND o=? AND d=?", (s, p, o, d,))
    return cur.fetchall()
    
  def _get_triples_spod_spod(self, s, p, o, d):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads")
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE o=?", (o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE o=? AND d=?", (o,d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE p=?", (p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE p=? AND o=?", (p, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE p=? AND o=? AND d=?", (p, o, d,))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE s=?", (s,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE s=? AND o=?", (s, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE s=? AND o=? AND d=?", (s, o, d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE s=? AND p=?", (s, p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE s=? AND p=? AND o=?", (s, p, o))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE s=? AND p=? AND o=? AND d=?", (s, p, o, d,))
    return cur.fetchall()
    
  def _get_obj_triples_cspo_cspo(self, c, s, p, o):
    if c is None:
      if s is None:
        if p is None:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs")
          else:         cur = self.execute("SELECT c,s,p,o FROM objs WHERE o=?", (o,))
        else:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs WHERE p=?", (p,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs WHERE p=? AND o=?", (p, o,))
      else:
        if p is None:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs WHERE s=?", (s,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs WHERE INDEXED BY index_objs_sp s=? AND o=?", (s, o,))
        else:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs WHERE s=? AND p=?", (s, p,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs WHERE INDEXED BY index_objs_sp s=? AND p=? AND o=?", (s, p, o,))
    else:
      if s is None:
        if p is None:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs WHERE c=?", (c,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs WHERE c=? AND o=?", (c, o,))
        else:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs WHERE c=? AND p=?", (c, p,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs WHERE c=? AND p=? AND o=?", (c, p, o,))
      else:
        if p is None:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs WHERE c=? AND s=?", (c, s,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND o=?", (c, s, o,))
        else:
          if o is None: cur = self.execute("SELECT c,s,p,o FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND p=?", (c, s, p,))
          else:         cur = self.execute("SELECT c,s,p,o FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND p=? AND o=?", (c, s, p, o,))
    return cur.fetchall()
  
  
  def _get_obj_triples_sp_co(self, s, p):
    return self.execute("SELECT c,o FROM objs WHERE s=? AND p=?", (s, p)).fetchall()
    
  def _get_triples_s_p(self, s):
    for (x,) in self.execute("SELECT DISTINCT p FROM quads WHERE s=?", (s,)).fetchall(): yield x
    
  def _get_obj_triples_o_p(self, o):
    for (x,) in self.execute("SELECT DISTINCT p FROM quads WHERE o=?", (o,)).fetchall(): yield x
    
  def _get_obj_triples_s_po(self, s):
    return self.execute("SELECT p,o FROM objs WHERE s=?", (s,)).fetchall()
  
  def _get_obj_triples_sp_o(self, s, p):
    for (x,) in self.execute("SELECT o FROM objs WHERE s=? AND p=?", (s, p)).fetchall(): yield x
    
  def _get_data_triples_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM datas WHERE s=? AND p=?", (s, p)).fetchall()

  def _get_triples_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM quads WHERE s=? AND p=?", (s, p)).fetchall()
    
  def _get_data_triples_s_pod(self, s):
    return self.execute("SELECT p,o,d FROM datas WHERE s=?", (s,)).fetchall()
  
  def _get_triples_s_pod(self, s):
    return self.execute("SELECT p,o,d FROM quads WHERE s=?", (s,)).fetchall()
    
  def _get_obj_triples_po_s(self, p, o):
    for (x,) in self.execute("SELECT s FROM objs WHERE p=? AND o=?", (p, o)).fetchall(): yield x
    
  def _get_obj_triples_spi_o(self, s, p, i):
    for (x,) in self.execute("SELECT o FROM objs WHERE s=? AND p=? UNION SELECT s FROM objs WHERE p=? AND o=?", (s, p, i, s)).fetchall(): yield x
    
  def _get_obj_triples_pio_s(self, p, i, o):
    for (x,) in self.execute("SELECT s FROM objs WHERE p=? AND o=? UNION SELECT o FROM objs WHERE s=? AND p=?", (p, o, o, i)).fetchall(): yield x
    
  def _get_obj_triple_sp_o(self, s, p):
    r = self.execute("SELECT o FROM objs WHERE s=? AND p=? LIMIT 1", (s, p)).fetchone()
    if r: return r[0]
    return None
  
  def _get_triple_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM quads WHERE s=? AND p=? LIMIT 1", (s, p)).fetchone()
    
  def _get_data_triple_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM datas WHERE s=? AND p=? LIMIT 1", (s, p)).fetchone()
  
  def _get_obj_triple_po_s(self, p, o):
    r = self.execute("SELECT s FROM objs WHERE p=? AND o=? LIMIT 1", (p, o)).fetchone()
    if r: return r[0]
    return None
  
  def _has_obj_triple_spo(self, s = None, p = None, o = None):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s FROM objs LIMIT 1")
        else:         cur = self.execute("SELECT s FROM objs WHERE o=? LIMIT 1", (o,))
      else:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE p=? LIMIT 1", (p,))
        else:         cur = self.execute("SELECT s FROM objs WHERE p=? AND o=? LIMIT 1", (p, o))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE s=? LIMIT 1", (s,))
        else:         cur = self.execute("SELECT s FROM objs INDEXED BY index_objs_sp WHERE s=? AND o=? LIMIT 1", (s, o))
      else:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE s=? AND p=? LIMIT 1", (s, p))
        else:         cur = self.execute("SELECT s FROM objs INDEXED BY index_objs_sp WHERE s=? AND p=? AND o=? LIMIT 1", (s, p, o))
    return not cur.fetchone() is None
  
  def _has_data_triple_spod(self, s = None, p = None, o = None, d = None):
    if s is None:
      if p is None:
        if o is None:   cur = self.execute("SELECT s FROM datas LIMIT 1")
        elif d is None: cur = self.execute("SELECT s FROM datas WHERE o=? LIMIT 1", (o,))
        else:           cur = self.execute("SELECT s FROM datas WHERE o=? AND d=? LIMIT 1", (o,d,))
      else:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE p=? LIMIT 1", (p,))
        elif d is None: cur = self.execute("SELECT s FROM datas WHERE p=? AND o=? LIMIT 1", (p, o))
        else:           cur = self.execute("SELECT s FROM datas WHERE p=? AND o=? AND d=? LIMIT 1", (p, o, d))
    else:
      if p is None:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE s=? LIMIT 1", (s,))
        elif d is None: cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE s=? AND o=? LIMIT 1", (s, o))
        else:           cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE s=? AND o=? AND d=? LIMIT 1", (s, o, d))
      else:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE s=? AND p=? LIMIT 1", (s, p))
        elif d is None: cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE s=? AND p=? AND o=? LIMIT 1", (s, p, o))
        else:           cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE s=? AND p=? AND o=? AND d=? LIMIT 1", (s, p, o, d))
    return not cur.fetchone() is None
  
  def _del_obj_triple_raw_spo(self, s, p, o):
    if s is None:
      if p is None:
        if o is None: self.execute("DELETE FROM objs")
        else:         self.execute("DELETE FROM objs WHERE o=?", (o,))
      else:
        if o is None: self.execute("DELETE FROM objs WHERE p=?", (p,))
        else:         self.execute("DELETE FROM objs WHERE p=? AND o=?", (p, o,))
    else:
      if p is None:
        if o is None: self.execute("DELETE FROM objs WHERE s=?", (s,))
        else:         self.execute("DELETE FROM objs INDEXED BY index_objs_sp WHERE s=? AND o=?", (s, o,))
      else:
        if o is None: self.execute("DELETE FROM objs WHERE s=? AND p=?", (s, p,))
        else:         self.execute("DELETE FROM objs INDEXED BY index_objs_sp WHERE s=? AND p=? AND o=?", (s, p, o,))
        
  def _del_data_triple_raw_spod(self, s, p, o, d):
    if s is None:
      if p is None:
        if   o is None: self.execute("DELETE FROM datas")
        elif d is None: self.execute("DELETE FROM datas WHERE o=?", (o,))
        else:           self.execute("DELETE FROM datas WHERE o=? AND d=?", (o, d,))
      else:
        if   o is None: self.execute("DELETE FROM datas WHERE p=?", (p,))
        elif d is None: self.execute("DELETE FROM datas WHERE p=? AND o=?", (p, o,))
        else:           self.execute("DELETE FROM datas WHERE p=? AND o=? AND d=?", (p, o, d,))
    else:
      if p is None:
        if   o is None: self.execute("DELETE FROM datas WHERE s=?", (s,))
        elif d is None: self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE s=? AND o=?", (s, o,))
        else:           self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE s=? AND o=? AND d=?", (s, o, d,))
      else:
        if   o is None: self.execute("DELETE FROM datas WHERE s=? AND p=?", (s, p,))
        elif d is None: self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE s=? AND p=? AND o=?", (s, p, o,))
        else:           self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE s=? AND p=? AND o=? AND d=?", (s, p, o, d,))
        
  def _punned_entities(self):
    from owlready2.base import rdf_type, owl_class, owl_named_individual
    cur = self.execute("SELECT q1.s FROM objs q1, objs q2 WHERE q1.s=q2.s AND q1.p=? AND q2.p=? AND q1.o=? AND q2.o=?", (rdf_type, rdf_type, owl_class, owl_named_individual))
    return [storid for (storid,) in cur.fetchall()]
  
  
  def __bool__(self): return True # Reimplemented to avoid calling __len__ in this case
  def __len__(self):
    #return self.execute("SELECT COUNT() FROM quads").fetchone()[0] # slow
    return self.execute("SELECT COUNT() FROM objs").fetchone()[0] + self.execute("SELECT COUNT() FROM datas").fetchone()[0]

  


  def _get_obj_triples_transitive_sp(self, s, p):
    for (x,) in self.execute("""
WITH RECURSIVE transit(x)
AS (  SELECT o FROM objs WHERE s=? AND p=?
UNION SELECT objs.o FROM objs, transit WHERE objs.s=transit.x AND objs.p=?)
SELECT x FROM transit""", (s, p, p)).fetchall(): yield x


    
  def _get_obj_triples_transitive_po(self, p, o):
    for (x,) in self.execute("""
WITH RECURSIVE transit(x)
AS (  SELECT s FROM objs WHERE p=? AND o=?
UNION SELECT objs.s FROM objs, transit WHERE objs.p=? AND objs.o=transit.x)
SELECT x FROM transit""", (p, o, p)).fetchall(): yield x
    
# Slower than Python implementation
#  def _get_obj_triples_transitive_sym2(self, s, p):
#    r = { s }
#    for (s, o) in self.execute("""
#WITH RECURSIVE transit(s,o)
#AS (  SELECT s,o from objs WHERE (s=? OR o=?) AND (p=?)
#    UNION SELECT objs.s,quads.o FROM objs, transit WHERE (quads.s=transit.s OR objs.o=transit.o OR objs.s=transit.o OR objs.o=transit.s) AND objs.p=?)
#SELECT s, o FROM transit""", (s, s, p, p)):
#      r.add(s)
#      r.add(o)
#    yield from r
    

  def _destroy_collect_storids(self, destroyed_storids, modified_relations, storid):
    for (blank_using,) in list(self.execute("""SELECT s FROM quads WHERE o=? AND p IN (
    %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) AND s < 0""" % (
      SOME,
      ONLY,
      VALUE,
      owl_onclass,
      owl_onproperty,
      owl_complementof,
      owl_inverse_property,
      owl_ondatarange,
      owl_annotatedsource,
      owl_annotatedproperty,
      owl_annotatedtarget,
    ), (storid,))):
      if not blank_using in destroyed_storids:
        destroyed_storids.add(blank_using)
        self._destroy_collect_storids(destroyed_storids, modified_relations, blank_using)
        
    for (c, blank_using) in list(self.execute("""SELECT c, s FROM objs WHERE o=? AND p=%s AND s < 0""" % (rdf_first,), (storid,))):
      list_user, prop_user, root, previouss, nexts, length = self._rdf_list_analyze(blank_using)
      destroyed_storids.update(previouss)
      destroyed_storids.add   (blank_using)
      destroyed_storids.update(nexts)
      if prop_user == owl_propertychain:
        modified_relations[list_user].add(prop_user)
        
      else:
        if (not list_user in destroyed_storids): # and (prop_user != owl_propertychain):
          destroyed_storids.add(list_user)
          self._destroy_collect_storids(destroyed_storids, modified_relations, list_user)
        
    for (c, blank_used) in list(self.execute("""
SELECT c, o FROM objs q1 WHERE s=? AND o < 0 AND (SELECT COUNT() FROM objs q2 WHERE q2.o=q1.o) = 1;
""", (storid,))):
      if not blank_used in destroyed_storids:
        destroyed_storids.add(blank_used)
        self._destroy_collect_storids(destroyed_storids, modified_relations, blank_used)
      
  def _rdf_list_analyze(self, blank):
    previouss = []
    nexts     = []
    length    = 1
    b         = self._get_obj_triple_sp_o(blank, rdf_rest)
    while b != rdf_nil:
      nexts.append(b)
      length += 1
      b       = self._get_obj_triple_sp_o(b, rdf_rest)
      
    b         = self._get_obj_triple_po_s(rdf_rest, blank)
    if b:
      while b:
        previouss.append(b)
        length += 1
        root    = b
        b       = self._get_obj_triple_po_s(rdf_rest, b)
    else:
      root = blank
      
    list_user, prop_user = self.execute("SELECT s, p FROM objs WHERE o=? LIMIT 1", (root,)).fetchone() or (None, None)
    return list_user, prop_user, root, previouss, nexts, length
  
  def restore_iri(self, storid, iri):
    self.execute("INSERT INTO resources VALUES (?,?)", (storid, iri))
      
  def destroy_entity(self, storid, destroyer, relation_updater, undoer_objs = None, undoer_datas = None):
    destroyed_storids   = { storid }
    modified_relations  = defaultdict(set)
    self._destroy_collect_storids(destroyed_storids, modified_relations, storid)

    args = ",".join(["?" for i in destroyed_storids])
    for s,p in self.execute("SELECT DISTINCT s,p FROM objs WHERE o IN (%s)" % args, tuple(destroyed_storids)):
      if not s in destroyed_storids:
        modified_relations[s].add(p)
        
    for p,o in self.execute("SELECT DISTINCT p,o FROM objs WHERE s IN (%s)" % args, tuple(destroyed_storids)):
      if (o > 300) and (p > 300) and (not o in destroyed_storids):
        modified_relations[o].add(p)
        
    # Two separate loops because high level destruction must be ended before removing from the quadstore (high level may need the quadstore)
    for storid in destroyed_storids:
      destroyer(storid)
      
    for storid in destroyed_storids:
      if undoer_objs is not None:
        undoer_objs .extend(self.execute("SELECT c,s,p,o FROM objs WHERE s=? OR o=?", (storid, storid)))
        undoer_datas.extend(self.execute("SELECT c,s,p,o,d FROM datas WHERE s=?", (storid,)))
      self.execute("DELETE FROM objs  WHERE s=? OR o=?", (storid, storid))
      self.execute("DELETE FROM datas WHERE s=?", (storid,))

    for s, ps in modified_relations.items():
      relation_updater(destroyed_storids, s, ps)
      
    self.execute("DELETE FROM resources WHERE storid=?", (storid,)) # At the end, so as the resource is still available for logging during destroying
    
    return destroyed_storids
  
  def _iter_ontology_iri(self, c = None):
    if c:
      return self.execute("SELECT iri FROM ontologies WHERE c=?", (c,)).fetchone()[0]
    else:
      return self.execute("SELECT c, iri FROM ontologies").fetchall()
    
  def _iter_triples(self, quads = False, sort_by_s = False, c = None):
    cursor = self.db.cursor() # Use a new cursor => can iterate without loading all data in a big list, while still being able to query the default cursor
    sql = ""
    if c:         sql += " WHERE c=%s" % c
    if sort_by_s: sql += " ORDER BY s"

    if quads:
      cursor.execute("SELECT c,s,p,o,d FROM quads %s" % sql)
    else:
      cursor.execute("SELECT s,p,o,d FROM quads %s" % sql)
      
    return cursor
      
  def get_fts_prop_storid(self): return self.prop_fts

  def enable_full_text_search(self, prop_storid):
    self.prop_fts.add(prop_storid)
    
    self.execute("""INSERT INTO prop_fts VALUES (?)""", (prop_storid,));
    
    self.execute("""CREATE VIRTUAL TABLE fts_%s USING fts5(s UNINDEXED, o, d UNINDEXED, content=datas, content_rowid=rowid)""" % prop_storid)
    self.execute("""INSERT INTO fts_%s(rowid, s, o, d) SELECT rowid, s, o, d FROM datas WHERE p=%s""" % (prop_storid, prop_storid))
    
    self.db.cursor().executescript("""
CREATE TRIGGER fts_%s_after_insert AFTER INSERT ON datas WHEN new.p=%s BEGIN
  INSERT INTO fts_%s(rowid, s, o, d) VALUES (new.rowid, new.s, new.o, new.d);
END;
CREATE TRIGGER fts_%s_after_delete AFTER DELETE ON datas WHEN old.p=%s BEGIN
  INSERT INTO fts_%s(fts_%s, rowid, s, o, d) VALUES('delete', old.rowid, old.s, old.o, old.d);
END;
CREATE TRIGGER fts_%s_after_update AFTER UPDATE ON datas WHEN new.p=%s BEGIN
  INSERT INTO fts_%s(fts_%s, rowid, s, o, d) VALUES('delete', old.rowid, old.s, old.o, old.d);
  INSERT INTO fts_%s(rowid, s, o, d) VALUES (new.rowid, new.s, new.o, new.d);
END;""" % (prop_storid, prop_storid, prop_storid,   prop_storid, prop_storid, prop_storid, prop_storid,   prop_storid, prop_storid, prop_storid, prop_storid, prop_storid))
    
    
  def disable_full_text_search(self, prop_storid):
    if not isinstance(prop_storid, int): prop_storid = prop_storid.storid
    self.prop_fts.discard(prop_storid)
    
    self.execute("""DELETE FROM prop_fts WHERE storid = ?""", (prop_storid,))
    self.execute("""DROP TABLE fts_%s""" % prop_storid)
    self.execute("""DROP TRIGGER fts_%s_after_insert""" % prop_storid)
    self.execute("""DROP TRIGGER fts_%s_after_delete""" % prop_storid)
    self.execute("""DROP TRIGGER fts_%s_after_update""" % prop_storid)
    



    
class SubGraph(BaseSubGraph):
  def __init__(self, parent, onto, c, db):
    BaseSubGraph.__init__(self, parent, onto)
    self.c      = c
    self.db     = db
    self.execute          = db.execute
    self._abbreviate       = parent._abbreviate
    self._unabbreviate     = parent._unabbreviate
    self._new_numbered_iri = parent._new_numbered_iri
    
    self.parent.onto_2_subgraph[onto] = self
    self.read_only = parent.read_only
    
  def import_triples_from_queue(self, queue, filename = None, delete_existing_triples = True):
    cur = self.db.cursor()
    new_abbrevs = []

    if not self.db.in_transaction: cur.execute("BEGIN")
    if delete_existing_triples:
      cur.execute("DELETE FROM objs WHERE c=?",  (self.c,))
      cur.execute("DELETE FROM datas WHERE c=?", (self.c,))
      
    # Re-implement _abbreviate() for speed and blank node support
    abbrevs = { "" : 60 }
    #current_resource = max(self.execute("SELECT MAX(storid) FROM resources").fetchone()[0], 300) # First 300 values are reserved
    current_resource = self.execute("SELECT current_resource FROM store").fetchone()[0]
    def _abbreviate(iri):
        nonlocal current_resource
        storid = abbrevs.get(iri)
        if storid is None:
          if iri.startswith("_"): # A blank node
            storid = abbrevs[iri] = self.parent.new_blank_node()
          else:
            r = cur.execute("SELECT storid FROM resources WHERE iri=? LIMIT 1", (iri,)).fetchone()
            if r:
              storid = abbrevs[iri] = r[0]
            else:
              current_resource += 1
              storid = abbrevs[iri] = current_resource
              new_abbrevs.append((storid, iri))
        return storid
      
    if filename: date = os.path.getmtime(filename)
    else:        date = time.time()
    
    def insert_objs(triples):
      objs = [(_abbreviate(s), _abbreviate(p), _abbreviate(o)) for s, p, o in triples]
      cur.executemany("INSERT OR IGNORE INTO objs VALUES (%s,?,?,?)" % self.c, objs)
      if new_abbrevs:
        cur.executemany("INSERT INTO resources VALUES (?,?)", new_abbrevs)
        new_abbrevs.clear()
        
    def insert_datas(triples):
      datas = [(_abbreviate(s), _abbreviate(p), o, _abbreviate(d) if (d and (not d.startswith("@"))) else d or 60) for s, p, o, d in triples]
      cur.executemany("INSERT OR IGNORE INTO datas VALUES (%s,?,?,?,?)" % self.c, datas)
      if new_abbrevs:
        cur.executemany("INSERT INTO resources VALUES (?,?)", new_abbrevs)
        new_abbrevs.clear()
        
    def finish():
      onto_base_iri = cur.execute("SELECT resources.iri FROM objs, resources WHERE objs.c=? AND objs.o=? AND resources.storid=objs.s LIMIT 1", (self.c, owl_ontology)).fetchone()
      if onto_base_iri: onto_base_iri = onto_base_iri[0]
      else:             onto_base_iri = ""
      
      if onto_base_iri.endswith("/"):
        cur.execute("UPDATE ontologies SET last_update=?,iri=? WHERE c=?", (date, onto_base_iri, self.c,))
      elif onto_base_iri:
        onto_base_iri = self.parent.fix_base_iri(onto_base_iri, self.c)
        cur.execute("UPDATE ontologies SET last_update=?,iri=? WHERE c=?", (date, onto_base_iri, self.c,))
      else:
        cur.execute("UPDATE ontologies SET last_update=? WHERE c=?", (date, self.c,))
        
      cur.execute("UPDATE store SET current_resource=?", (current_resource,))
      
      self.parent.select_abbreviate_method()
      self.parent.analyze()
      return onto_base_iri
    
    if queue:
      while True:
        command, triples = queue.get()
        
        if   command == "objs":  insert_objs (triples)
        elif command == "datas": insert_datas(triples)
        
        elif command == "finish":
          return finish()
          break
        
        elif command == "error":
          import owlready2
          raise owlready2.OwlReadyOntologyParsingError(*triples)
        
    return insert_objs, insert_datas, finish

  def create_parse_func(self, filename = None, delete_existing_triples = True, datatype_attr = "http://www.w3.org/1999/02/22-rdf-syntax-ns#datatype"):
    objs         = []
    datas        = []
    new_abbrevs  = []
    
    cur = self.db.cursor()
    
    if not self.db.in_transaction: cur.execute("BEGIN")
    if delete_existing_triples:
      cur.execute("DELETE FROM objs WHERE c=?", (self.c,))
      cur.execute("DELETE FROM datas WHERE c=?", (self.c,))
      
    # Re-implement _abbreviate() for speed
    abbrevs = {}
    #current_resource = max(self.execute("SELECT MAX(storid) FROM resources").fetchone()[0], 300) # First 300 values are reserved
    current_resource = self.execute("SELECT current_resource FROM store").fetchone()[0]
    def _abbreviate(iri):
        nonlocal current_resource
        storid = abbrevs.get(iri)
        if not storid is None: return storid
        r = cur.execute("SELECT storid FROM resources WHERE iri=? LIMIT 1", (iri,)).fetchone()
        if r:
          abbrevs[iri] = r[0]
          return r[0]
        current_resource += 1
        storid = current_resource
        new_abbrevs.append((storid, iri))
        abbrevs[iri] = storid
        return storid
      
    def insert_objs():
      nonlocal objs, new_abbrevs
      if owlready2.namespace._LOG_LEVEL: print("* OwlReady2 * Importing %s object triples from ontology %s ..." % (len(objs), self.onto._base_iri), file = sys.stderr)
      cur.executemany("INSERT INTO resources VALUES (?,?)", new_abbrevs)
      cur.executemany("INSERT OR IGNORE INTO objs VALUES (%s,?,?,?)" % self.c, objs)
      objs        .clear()
      new_abbrevs .clear()
      
    def insert_datas():
      nonlocal datas, new_abbrevs
      if owlready2.namespace._LOG_LEVEL: print("* OwlReady2 * Importing %s data triples from ontology %s ..." % (len(datas), self.onto._base_iri), file = sys.stderr)
      cur.executemany("INSERT OR IGNORE INTO datas VALUES (%s,?,?,?,?)" % self.c, datas)
      datas.clear()
      
    def on_prepare_obj(s, p, o):
      if isinstance(s, str): s = _abbreviate(s)
      if isinstance(o, str): o = _abbreviate(o)
      objs.append((s, _abbreviate(p), o))
      if len(objs) > 1000000: insert_objs()
      
    def on_prepare_data(s, p, o, d):
      if isinstance(s, str): s = _abbreviate(s)
      if   d and (not d.startswith("@")): d = _abbreviate(d)
      elif d == 0: d = 60 # Replace missing datatype by string
      datas.append((s, _abbreviate(p), o, d or 0))
      if len(datas) > 1000000: insert_datas()
      
      
    def on_finish():
      if filename: date = os.path.getmtime(filename)
      else:        date = time.time()

      insert_objs()
      insert_datas()
      
      onto_base_iri = cur.execute("SELECT resources.iri FROM objs, resources WHERE objs.c=? AND objs.o=? AND resources.storid=objs.s LIMIT 1", (self.c, owl_ontology)).fetchone()
      if onto_base_iri: onto_base_iri = onto_base_iri[0]
      else:             onto_base_iri = ""
      
      if onto_base_iri.endswith("/"):
        cur.execute("UPDATE ontologies SET last_update=?,iri=? WHERE c=?", (date, onto_base_iri, self.c,))
      elif onto_base_iri:
        onto_base_iri = self.parent.fix_base_iri(onto_base_iri, self.c)
        cur.execute("UPDATE ontologies SET last_update=?,iri=? WHERE c=?", (date, onto_base_iri, self.c,))
      else:
        cur.execute("UPDATE ontologies SET last_update=? WHERE c=?", (date, self.c,))
        
      cur.execute("UPDATE store SET current_resource=?", (current_resource,))
      
      self.parent.select_abbreviate_method()
      self.parent.analyze()
      
      return onto_base_iri
    
    
    return objs, datas, on_prepare_obj, on_prepare_data, insert_objs, insert_datas, self.parent.new_blank_node, _abbreviate, on_finish

  def context_2_user_context(self, c): return self.parent.context_2_user_context(c)
 
  def add_ontology_alias(self, iri, alias):
    self.execute("INSERT into ontology_alias VALUES (?,?)", (iri, alias))
    
  def get_last_update_time(self):
    return self.execute("SELECT last_update FROM ontologies WHERE c=?", (self.c,)).fetchone()[0]
  
  def set_last_update_time(self, t):
    if not self.read_only: self.execute("UPDATE ontologies SET last_update=? WHERE c=?", (t, self.c))
    
  def destroy(self):
    self.execute("DELETE FROM objs WHERE c=?",       (self.c,))
    self.execute("DELETE FROM datas WHERE c=?",      (self.c,))
    self.execute("DELETE FROM ontologies WHERE c=?", (self.c,))
    
  def _set_obj_triple_raw_spo(self, s, p, o):
    if (s is None) or (p is None) or (o is None): raise ValueError
    #self.execute("DELETE FROM objs WHERE c=? AND s=? AND p=?", (self.c, s, p,))
    self.execute("DELETE FROM objs WHERE s=? AND p=?", (s, p,))
    self.execute("INSERT INTO objs VALUES (?, ?, ?, ?)", (self.c, s, p, o))
    self.parent.nb_added_triples += 1
    if self.parent.nb_added_triples > 1000: self.parent.analyze()
    
  def _add_obj_triple_raw_spo(self, s, p, o):
    if (s is None) or (p is None) or (o is None): raise ValueError
    self.execute("INSERT OR IGNORE INTO objs VALUES (?, ?, ?, ?)", (self.c, s, p, o))
    self.parent.nb_added_triples += 1
    if self.parent.nb_added_triples > 1000: self.parent.analyze()
    
  def _del_obj_triple_raw_spo(self, s = None, p = None, o = None):
    if s is None:
      if p is None:
        if o is None: self.execute("DELETE FROM objs WHERE c=?", (self.c,))
        else:         self.execute("DELETE FROM objs WHERE c=? AND o=?", (self.c, o,))
      else:
        if o is None: self.execute("DELETE FROM objs WHERE c=? AND p=?", (self.c, p,))
        else:         self.execute("DELETE FROM objs WHERE c=? AND p=? AND o=?", (self.c, p, o,))
    else:
      if p is None:
        if o is None: self.execute("DELETE FROM objs WHERE c=? AND s=?", (self.c, s,))
        else:         self.execute("DELETE FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND o=?", (self.c, s, o))
      else:
        if o is None: self.execute("DELETE FROM objs WHERE c=? AND s=? AND p=?", (self.c, s, p,))
        else:         self.execute("DELETE FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND p=? AND o=?", (self.c, s, p, o,))
        
  def _set_data_triple_raw_spod(self, s, p, o, d):
    if (s is None) or (p is None) or (o is None) or (d is None): raise ValueError
    #self.execute("DELETE FROM datas WHERE c=? AND s=? AND p=?", (self.c, s, p,))
    self.execute("DELETE FROM datas WHERE s=? AND p=?", (s, p,))
    self.execute("INSERT INTO datas VALUES (?, ?, ?, ?, ?)", (self.c, s, p, o, d))
    self.parent.nb_added_triples += 1
    if self.parent.nb_added_triples > 1000: self.parent.analyze()

  def _add_data_triple_raw_spod(self, s, p, o, d):
    if (s is None) or (p is None) or (o is None) or (d is None): raise ValueError
    self.execute("INSERT OR IGNORE INTO datas VALUES (?, ?, ?, ?, ?)", (self.c, s, p, o, d))
    self.parent.nb_added_triples += 1
    if self.parent.nb_added_triples > 1000: self.parent.analyze()
    
  def _del_data_triple_raw_spod(self, s, p, o, d):
    if s is None:
      if p is None:
        if o is None:   self.execute("DELETE FROM datas WHERE c=?", (self.c,))
        elif d is None: self.execute("DELETE FROM datas WHERE c=? AND o=?", (self.c, o,))
        else:           self.execute("DELETE FROM datas WHERE c=? AND o=? AND d=?", (self.c, o, d,))
      else:
        if o is None:   self.execute("DELETE FROM datas WHERE c=? AND p=?", (self.c, p,))
        elif d is None: self.execute("DELETE FROM datas WHERE c=? AND p=? AND o=?", (self.c, p, o,))
        else:           self.execute("DELETE FROM datas WHERE c=? AND p=? AND o=? AND d=?", (self.c, p, o, d,))
    else:
      if p is None:
        if o is None:   self.execute("DELETE FROM datas WHERE c=? AND s=?", (self.c, s,))
        elif d is None: self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND o=?", (self.c, s, o,))
        else:           self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND o=? AND d=?", (self.c, s, o, d,))
      else:
        if o is None:   self.execute("DELETE FROM datas WHERE c=? AND s=? AND p=?", (self.c, s, p,))
        elif d is None: self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND p=? AND o=?", (self.c, s, p, o,))
        else:           self.execute("DELETE FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND p=? AND o=? AND d=?", (self.c, s, p, o, d,))
        
  def _has_obj_triple_spo(self, s = None, p = None, o = None):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE c=? LIMIT 1", (self.c,))
        else:         cur = self.execute("SELECT s FROM objs WHERE c=? AND o=? LIMIT 1", (self.c, o))
      else:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE c=? AND p=? LIMIT 1", (self.c, p,))
        else:         cur = self.execute("SELECT s FROM objs WHERE c=? AND p=? AND o=? LIMIT 1", (self.c, p, o))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE c=? AND s=? LIMIT 1", (self.c, s,))
        else:         cur = self.execute("SELECT s FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND o=? LIMIT 1", (self.c, s, o))
      else:
        if o is None: cur = self.execute("SELECT s FROM objs WHERE c=? AND s=? AND p=? LIMIT 1", (self.c, s, p,))
        else:         cur = self.execute("SELECT s FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND p=? AND o=? LIMIT 1", (self.c, s, p, o))
    return not cur.fetchone() is None
       
  def _has_data_triple_spod(self, s = None, p = None, o = None, d = None):
    if s is None:
      if p is None:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE c=? LIMIT 1", (self.c,))
        elif d is None: cur = self.execute("SELECT s FROM datas WHERE c=? AND o=? LIMIT 1", (self.c, o))
        else:           cur = self.execute("SELECT s FROM datas WHERE c=? AND o=? AND d=? LIMIT 1", (self.c, o, d))
      else:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE c=? AND p=? LIMIT 1", (self.c, p,))
        elif d is None: cur = self.execute("SELECT s FROM datas WHERE c=? AND p=? AND o=? LIMIT 1", (self.c, p, o))
        else:           cur = self.execute("SELECT s FROM datas WHERE c=? AND p=? AND o=? AND d=? LIMIT 1", (self.c, p, o, d))
    else:
      if p is None:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE c=? AND s=? LIMIT 1", (self.c, s,))
        elif d is None: cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND o=? LIMIT 1", (self.c, s, o))
        else:           cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND o=? AND d=? LIMIT 1", (self.c, s, o, d))
      else:
        if o is None:   cur = self.execute("SELECT s FROM datas WHERE c=? AND s=? AND p=? LIMIT 1", (self.c, s, p,))
        elif d is None: cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND p=? AND o=? LIMIT 1", (self.c, s, p, o))
        else:           cur = self.execute("SELECT s FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND p=? AND o=? AND d=? LIMIT 1", (self.c, s, p, o, d))
    return not cur.fetchone() is None
    
        
  def _get_obj_triples_spo_spo(self, s = None, p = None, o = None):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE c=?", (self.c,))
        else:         cur = self.execute("SELECT s,p,o FROM objs WHERE c=? AND o=?", (self.c, o,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE c=? AND p=?", (self.c, p,))
        else:         cur = self.execute("SELECT s,p,o FROM objs WHERE c=? AND p=? AND o=?", (self.c, p, o,))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE c=? AND s=?", (self.c, s,))
        else:         cur = self.execute("SELECT s,p,o FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND o=?", (self.c, s, o,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o FROM objs WHERE c=? AND s=? AND p=?", (self.c, s, p,))
        else:         cur = self.execute("SELECT s,p,o FROM objs INDEXED BY index_objs_sp WHERE c=? AND s=? AND p=? AND o=?", (self.c, s, p, o,))
    return cur.fetchall()

  def _get_data_triples_spod_spod(self, s, p, o, d = ""):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=?", (self.c,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND o=?", (self.c, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND o=? AND d=?", (self.c, o,d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND p=?", (self.c, p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND p=? AND o=?", (self.c, p, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND p=? AND o=? AND d=?", (self.c, p, o, d,))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND s=?", (self.c, s,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND o=?", (self.c, s, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas WINDEXED BY index_datas_sp HERE c=? AND s=? AND o=? AND d=?", (self.c, s, o, d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM datas WHERE c=? AND s=? AND p=?", (self.c, s, p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND p=? AND o=?", (self.c, s, p, o))
          else:
            cur = self.execute("SELECT s,p,o,d FROM datas INDEXED BY index_datas_sp WHERE c=? AND s=? AND p=? AND o=? AND d=?", (self.c, s, p, o, d,))
    return cur.fetchall()

  def _get_triples_spod_spod(self, s, p, o, d = ""):
    if s is None:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=?", (self.c,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND o=?", (self.c, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND o=? AND d=?", (self.c, o,d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND p=?", (self.c, p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND p=? AND o=?", (self.c, p, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND p=? AND o=? AND d=?", (self.c, p, o, d,))
    else:
      if p is None:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND s=?", (self.c, s,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND s=? AND o=?", (self.c, s, o,))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND s=? AND o=? AND d=?", (self.c, s, o, d,))
      else:
        if o is None: cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND s=? AND p=?", (self.c, s, p,))
        else:
          if d is None:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND s=? AND p=? AND o=?", (self.c, s, p, o))
          else:
            cur = self.execute("SELECT s,p,o,d FROM quads WHERE c=? AND s=? AND p=? AND o=? AND d=?", (self.c, s, p, o, d,))
    return cur.fetchall()

  
  def _get_obj_triples_s_po(self, s):
    return self.execute("SELECT p,o FROM objs WHERE c=? AND s=?", (self.c, s,)).fetchall()
  
  def _get_obj_triples_sp_o(self, s, p):
    for (x,) in self.execute("SELECT o FROM objs WHERE c=? AND s=? AND p=?", (self.c, s, p,)).fetchall(): yield x
    
  def _get_obj_triples_sp_co(self, s, p):
    return self.execute("SELECT c,o FROM objs WHERE c=? AND s=? AND p=?", (self.c, s, p,)).fetchall()
    
  def _get_triples_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM quads WHERE c=? AND s=? AND p=?", (self.c, s, p)).fetchall()
    
  def _get_data_triples_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM datas WHERE c=? AND s=? AND p=?", (self.c, s, p)).fetchall()

  def _get_data_triples_s_pod(self, s):
    return self.execute("SELECT p,o,d FROM datas WHERE c=? AND s=?", (self.c, s)).fetchall()
    
  def _get_triples_s_pod(self, s):
    return self.execute("SELECT p,o,d FROM quads WHERE c=? AND s=?", (self.c, s)).fetchall()
   
  def _get_obj_triples_po_s(self, p, o):
    for (x,) in self.execute("SELECT s FROM objs WHERE c=? AND p=? AND o=?", (self.c, p, o)).fetchall(): yield x
    
  def _get_obj_triples_spi_o(self, s, p, i):
    for (x,) in self.execute("SELECT o FROM objs WHERE c=? AND s=? AND p=? UNION SELECT s FROM objs WHERE c=? AND p=? AND o=?", (self.c, s, p, self.c, i, s)).fetchall(): yield x
    
  def _get_obj_triples_pio_s(self, p, i, o):
    for (x,) in self.execute("SELECT s FROM objs WHERE c=? AND p=? AND o=? UNION SELECT o FROM objs WHERE c=? AND s=? AND p=?", (self.c, p, o, self.c, o, i)).fetchall(): yield x
    
  def _get_obj_triple_sp_o(self, s, p):
    r = self.execute("SELECT o FROM objs WHERE c=? AND s=? AND p=? LIMIT 1", (self.c, s, p,)).fetchone()
    if r: return r[0]
    return None
  
  def _get_triple_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM quads WHERE c=? AND s=? AND p=? LIMIT 1", (self.c, s, p)).fetchone()
    
  def _get_data_triple_sp_od(self, s, p):
    return self.execute("SELECT o,d FROM datas WHERE c=? AND s=? AND p=? LIMIT 1", (self.c, s, p,)).fetchone()
  
  def _get_obj_triple_po_s(self, p, o):
    r = self.execute("SELECT s FROM objs WHERE c=? AND p=? AND o=? LIMIT 1", (self.c, p, o,)).fetchone()
    if r: return r[0]
    return None
  
  def _get_triples_s_p(self, s):
    for (x,) in self.execute("SELECT DISTINCT p FROM quads WHERE c=? AND s=?", (self.c, s,)).fetchall(): yield x
    
  def _get_obj_triples_o_p(self, o):
    for (x,) in self.execute("SELECT DISTINCT p FROM quads WHERE c=? AND o=?", (self.c, o,)).fetchall(): yield x
    
  def _get_obj_triples_cspo_cspo(self, c, s, p, o):
    return [(self.c, s, p, o) for (s, p, o) in self._get_obj_triples_spo_spo(s, p, o)]
  
  def search(self, prop_vals, c = None, debug = False): return self.parent.search(prop_vals, self.c, debug)
  
  def __len__(self):
    #return self.execute("SELECT COUNT() FROM quads WHERE c=?", (self.c,)).fetchone()[0] # slow
    return self.execute("SELECT COUNT() FROM objs WHERE c=?", (self.c,)).fetchone()[0] + self.execute("SELECT COUNT() FROM datas WHERE c=?", (self.c,)).fetchone()[0]
  
  
  def _iter_ontology_iri(self, c = None):
    if c:
      return self.execute("SELECT iri FROM ontologies WHERE c=?", (c,)).fetchone()[0]
    else:
      return self.execute("SELECT c, iri FROM ontologies").fetchall()
    
  def _iter_triples(self, quads = False, sort_by_s = False):
    return self.parent._iter_triples(quads, sort_by_s, self.c)
  
  def _refactor(self, storid, new_iri): return self.parent._refactor(storid, new_iri)
    
  def _refactor_onto(self, storid, old_base_iri, new_base_iri): return self.parent._refactor_onto(storid, old_base_iri, new_base_iri)
    
  def _get_obj_triples_transitive_sp(self, s, p):
    for (x,) in self.execute("""
WITH RECURSIVE transit(x)
AS (  SELECT o FROM objs WHERE c=? AND s=? AND p=?
UNION SELECT objs.o FROM objs, transit WHERE objs.c=? AND objs.s=transit.x AND objs.p=?)
SELECT x FROM transit""", (self.c, s, p, self.c, p)).fetchall(): yield x
  
  def _get_obj_triples_transitive_po(self, p, o):
    for (x,) in self.execute("""
WITH RECURSIVE transit(x)
AS (  SELECT s FROM objs WHERE c=? AND p=? AND o=?
UNION SELECT objs.s FROM objs, transit WHERE objs.c=? AND objs.p=? AND objs.o=transit.x)
SELECT x FROM transit""", (self.c, p, o, self.c, p)).fetchall(): yield x

#  def _get_obj_triples_transitive_sym(self, s, p):
#    r = { s }
#    for (s, o) in self.execute("""
#WITH RECURSIVE transit(s,o)
#AS (  SELECT s,o from objs WHERE (s=? OR o=?) AND p=? AND c=?
#    UNION SELECT objs.s,quads.o FROM objs, transit WHERE (quads.s=transit.s OR objs.o=transit.o OR objs.s=transit.o OR objs.o=transit.s) AND objs.p=? AND objs.c=?)
#SELECT s, o FROM transit""", (s, s, p, self.c, p, self.c)):
#      r.add(s)
#      r.add(o)
#    yield from r


class _SearchMixin(list):
  __slots__ = []
  
  def sql_request(self):
    transits, sql, params = self.sql_components()
    if transits: sql = "WITH RECURSIVE %s %s" % (", ".join(transits), sql)
    return sql, params
    
  def _do_search(self):
    if self.has_bm25():
      sql, params = self.sql_request()
      o_2_bm25 = {}
      for (o, bm25) in self.world.graph.execute(sql, params).fetchall():
        if o in o_2_bm25:
          o_2_bm25[o] = min(bm25, o_2_bm25[o])
        else:
          o_2_bm25[o] = bm25
      os_bm25s = sorted(o_2_bm25.items(), key = lambda x: x[1])
      return ((self.world._get_by_storid(o), bm25) for (o, bm25) in os_bm25s)
    else:
      sql, params = self.sql_request()
      return (self.world._get_by_storid(o) for (o,) in self.world.graph.execute(sql, params).fetchall())
  _get_content = _do_search  

  def _do_search_rdf(self):
    sql, params = self.sql_request()
    return self.world.graph.execute(sql, params).fetchall()
  
  def first(self):
    sql, params = self.sql_request()
    o = self.world.graph.execute(sql, params).fetchone()
    if o: return self.world._get_by_storid(o[0])
    
  def has_bm25(self): return False
  
  def __len__(self):
    sql, params = self.sql_request()
    sql =  "SELECT COUNT() FROM (%s)" % sql
    return self.world.graph.execute(sql, params).fetchone()[0]
        
      
class _PopulatedSearchList(FirstList):
  __slots__ = ["world", "prop_vals", "_c", "id", "transits", "tables", "conditions", "params", "alternatives", "excepts", "except_conditions", "except_params", "nested_searchs", "target", "bm25"]
  def has_bm25(self): return self.bm25

_NEXT_SEARCH_ID = 0
class _SearchList(FirstList, _SearchMixin, _LazyListMixin):
  __slots__ = ["world", "prop_vals", "_c", "id", "transits", "tables", "conditions", "params", "alternatives", "excepts", "except_conditions", "except_params", "nested_searchs", "target", "bm25"]
  _PopulatedClass = _PopulatedSearchList
  
  def has_bm25(self): return self.bm25
  
  def __init__(self, world, prop_vals, c = None, case_sensitive = True, bm25 = False):
    global _NEXT_SEARCH_ID
    
    super().__init__()
    self.world     = world
    self.prop_vals = prop_vals
    self._c         = c
    
    _NEXT_SEARCH_ID += 1
    self.id = _NEXT_SEARCH_ID

    self.tables            = []
    self.transits          = []
    self.conditions        = []
    self.params            = []
    self.alternatives      = []
    self.excepts           = []
    self.except_conditions = []
    self.except_params     = []
    self.nested_searchs    = []

    self.bm25 = bm25

    
    n = 0
    for k, v, d in prop_vals:
      if v is None:
        self.excepts.append(k)
        continue
      
      n += 1
      i = "%s_%s" % (self.id, n)
      if n == 1:
        self.target = i
        if   d == "quads": self.tables.append("quads q%s" % i)
        elif d is None:    self.tables.append("objs q%s" % i)
        else:              self.tables.append("datas q%s" % i)
      else:
        if   d == "quads": self.tables.append("quads q%s" % i)
        else:
          
          if d is None:    self.tables.append("objs q%s INDEXED BY index_objs_sp" % i)
          else:            self.tables.append("datas q%s INDEXED BY index_datas_sp" % i)
          
      if not c is None:
        self.conditions  .append("q%s.c = ?" % i)
        self.params      .append(c)
        
      if   k == " iri":
        if n > 1: self.conditions.append("q%s.s = q%s.s" % (i, self.target))
        self.tables    .append("resources")
        self.conditions.append("resources.storid = q%s.s" % i)
        if case_sensitive:
          if "*" in v: self.conditions.append("resources.iri GLOB ?")
          else:        self.conditions.append("resources.iri = ?")
          self.params.append(v)
        else:
          self.conditions.append("resources.iri LIKE ?")
          self.params.append(v.replace("*", "%"))
          
      elif k == " is_a":
        if n > 1: self.conditions.append("q%s.s = q%s.s" % (i, self.target))
        if   isinstance(v, (_UnionSearchList, _PopulatedUnionSearchList)):
          for search in v.searches: self.transits.extend(search.transits)
          self.alternatives.append(v.explode(lambda target: [
            ["q%s.s = q%s.s" % (i, self.target), "(q%s.p = %s OR q%s.p = %s)" % (i, rdf_type, i, rdfs_subclassof), "q%s.o = q%s.s" % (i, target)] ]))
        elif isinstance(v, (_SearchMixin, _PopulatedSearchList)):
          self.conditions.append("(q%s.p = %s OR q%s.p = %s) AND q%s.o = q%s.s" % (i, rdf_type, i, rdfs_subclassof, i, v.target))
          self.nested_searchs.append(v)
          self.nested_searchs.extend(v.nested_searchs)
        else:
          if isinstance(v, Or): v = "), (".join(str(c.storid) for c in v.Classes)
          transit_name = "transit_%s" % i
          self.transits.append("""%s(x)
AS (      VALUES (%s)
UNION ALL SELECT objs.s FROM objs, %s WHERE objs.o=%s.x AND objs.p IN (%s, %s))
""" % (transit_name, v,
       transit_name, transit_name, rdfs_subclassof, rdf_type))
          self.tables.append(transit_name)
          self.conditions.append("q%s.s = %s.x" % (i, transit_name))
          
      elif k == " type":
        if n > 1: self.conditions.append("q%s.s = q%s.s" % (i, self.target))
        if   isinstance(v, (_UnionSearchList, _PopulatedUnionSearchList)):
          for search in v.searches: self.transits.extend(search.transits)
          self.alternatives.append(v.explode(lambda target: [
            ["q%s.s = q%s.s" % (i, self.target), "q%s.p = %s" % (i, rdf_type), "q%s.o = q%s.s" % (i, target)] ]))
        elif isinstance(v, (_SearchMixin, _PopulatedSearchList)):
          self.conditions.append("q%s.p = %s AND q%s.o = q%s.s" % (i, rdf_type, i, v.target))
          self.nested_searchs.append(v)
          self.nested_searchs.extend(v.nested_searchs)
        else:
          if isinstance(v, Or): v = "), (".join(str(c.storid) for c in v.Classes)
          transit_name = "transit_%s" % i
          self.transits.append("""%s(x)
AS (      VALUES (%s)
UNION ALL SELECT objs.s FROM objs, %s WHERE objs.o=%s.x AND objs.p=%s)
""" % (transit_name, v, transit_name, transit_name, rdfs_subclassof))
          self.tables.append(transit_name)
          self.conditions.append("q%s.p = %s AND q%s.o = %s.x" % (i, rdf_type, i, transit_name))
          
      elif k == " subclass_of":
        if n > 1: self.conditions.append("q%s.s = q%s.s" % (i, self.target))
        if   isinstance(v, (_UnionSearchList, _PopulatedUnionSearchList)):
          for search in v.searches: self.transits.extend(search.transits)
          self.alternatives.append(v.explode(lambda target: [
            ["q%s.s = q%s.s" % (i, self.target), "q%s.p = %s" % (i, rdfs_subclassof), "q%s.o = q%s.s" % (i, target)] ]))
        elif isinstance(v, (_SearchMixin, _PopulatedSearchList)):
          self.conditions.append("q%s.p = %s AND q%s.o = q%s.s" % (i, rdfs_subclassof, i, v.target))
          self.nested_searchs.append(v)
          self.nested_searchs.extend(v.nested_searchs)
        else:
          if isinstance(v, Or): v = "), (".join(str(c.storid) for c in v.Classes)
          transit_name = "transit_%s" % i
          self.transits.append("""%s(x)
AS (      VALUES (%s)
UNION ALL SELECT objs.s FROM objs, %s WHERE objs.o=%s.x AND objs.p=%s)
""" % (transit_name, v, transit_name, transit_name, rdfs_subclassof))
          self.tables.append(transit_name)
          self.conditions.append("q%s.s = %s.x" % (i, transit_name))
          
      elif k == " subproperty_of":
        if n > 1: self.conditions.append("q%s.s = q%s.s" % (i, self.target))
        if   isinstance(v, (_UnionSearchList, _PopulatedUnionSearchList)):
          for search in v.searches: self.transits.extend(search.transits)
          self.alternatives.append(v.explode(lambda target: [
            ["q%s.s = q%s.s" % (i, self.target), "q%s.p = %s" % (i, rdfs_subpropertyof), "q%s.o = q%s.s" % (i, target)] ]))
        elif isinstance(v, (_SearchMixin, _PopulatedSearchList)):
          self.conditions.append("q%s.p = %s AND q%s.o = q%s.s" % (i, rdfs_subpropertyof, i, v.target))
          self.nested_searchs.append(v)
          self.nested_searchs.extend(v.nested_searchs)
        else:
          if isinstance(v, Or): v = "), (".join(str(c.storid) for c in v.Classes)
          transit_name = "transit_%s" % i
          self.transits.append("""%s(x)
AS (      VALUES (%s)
UNION ALL SELECT objs.s FROM objs, %s WHERE objs.o=%s.x AND objs.p=%s)
""" % (transit_name, v, transit_name, transit_name, rdfs_subpropertyof))
          self.tables.append(transit_name)
          self.conditions.append("q%s.s = %s.x" % (i, transit_name))
          
      elif isinstance(k, tuple): # Prop with inverse
        if n == 1: # Does not work if it is the FIRST => add a dumb first.
          n += 1
          i = "%s_%s" % (self.id, n)
          self.tables.append("objs q%s" % i)
          if not c is None:
            self.conditions  .append("q%s.c = ?" % i)
            self.params      .append(c)
            
        if   isinstance(v, (_UnionSearchList, _PopulatedUnionSearchList)):
          for search in v.searches: self.transits.extend(search.transits)
          self.alternatives.append(v.explode(lambda target: [
            ["q%s.s = q%s.s" % (i, self.target), "q%s.p = %s" % (i, k[0]), "q%s.o = q%s.s" % (i, target)],
            ["q%s.o = q%s.s" % (i, self.target), "q%s.p = %s" % (i, k[0]), "q%s.s = q%s.s" % (i, target)] ]))
        else:
          if   isinstance(v, (_SearchMixin, _PopulatedSearchList)): # First, to avoid comparing v with "*", which would require to populate it!
            cond1 = "q%s.s = q%s.s AND q%s.p = ? AND q%s.o = q%s.s" % (i, self.target, i, i, v.target)
            cond2 = "q%s.o = q%s.s AND q%s.p = ? AND q%s.s = q%s.s" % (i, self.target, i, i, v.target)
            params1 = [k[0]]
            params2 = [k[1]]
            self.nested_searchs.append(v)
            self.nested_searchs.extend(v.nested_searchs)
          elif v == "*":
            cond1 = "q%s.s = q%s.s AND q%s.p = ?" % (i, self.target, i)
            cond2 = "q%s.o = q%s.s AND q%s.p = ?" % (i, self.target, i)
            params1 = [k[0]]
            params2 = [k[1]]
          else:
            cond1 = "q%s.s = q%s.s AND q%s.p = ? AND q%s.o = ?" % (i, self.target, i, i)
            cond2 = "q%s.o = q%s.s AND q%s.p = ? AND q%s.s = ?" % (i, self.target, i, i)
            params1 = [k[0], v]
            params2 = [k[1], v]
            
          current_table = self.tables.pop()
          if current_table.endswith(" INDEXED BY index_objs_sp"): current_table2 = "%sop" % current_table[:-2]
          else:                                                   current_table2 = current_table
          self.alternatives.append((([current_table], cond1, params1), ([current_table2], cond2, params2)))
          
      else: # Prop without inverse
        if n > 1: self.conditions.append("q%s.s = q%s.s" % (i, self.target))
        if isinstance(v, FTS):
          self.tables    .append("fts_%s" % k)
          self.conditions.append("q%s.s = fts_%s.s" % (self.target, k))
          self.conditions.append("fts_%s.o MATCH ?" % k)
          self.params    .append(v)
          if v.lang != "":
            self.conditions.append("fts_%s.d = ?" % (k,))
            self.params    .append("@%s" % v.lang)
          if self.bm25: self.bm25 = "fts_%s" % k
          
        else:
          self.conditions.append("q%s.p = ?" % i)
          self.params    .append(k)
          if   isinstance(v, (_UnionSearchList, _PopulatedUnionSearchList)):
            alternatives = []
            for search in v.searches:
              if search.excepts: raise NotImplementedError("Nested search with union and exception are not supported.")
              self.transits.extend(search.transits)
              if search.alternatives:
                for combination in all_combinations(search.alternatives):
                  combination_tabless, combination_conditions, combination_paramss = zip(*combination)
                  alternatives.append((search.tables + [t for ts in combination_tabless for t in ts],
                                       " AND ".join(search.conditions + combination_conditions + ["q%s.o = q%s.s" % (i, search.target)]),
                                       search.params + [p for ps in combination_paramss for p in ps]))
              else:
                alternatives.append((search.tables,
                                     " AND ".join(search.conditions + ["q%s.o = q%s.s" % (i, search.target)]),
                                     search.params))
            self.alternatives.append(tuple(alternatives))
            
          elif isinstance(v, (_SearchMixin, _PopulatedSearchList)):
            self.conditions.append("q%s.o = q%s.s" % (i, v.target))
            self.nested_searchs.append(v)
            self.nested_searchs.extend(v.nested_searchs)
            
          elif isinstance(v, NumS):
            for operator, value in v.operators_and_values:
              self.conditions.append("q%s.o %s ?" % (i, operator))
              self.params    .append(value)
              
          elif isinstance(v, str):
            if   v == "*": pass
            elif case_sensitive:
              if "*" in v:
                self.conditions.append("q%s.o GLOB ?" % i)
                self.params    .append(v)
              else:
                self.conditions.append("q%s.o = ?" % i)
                self.params    .append(v)
            else:
              self.conditions.append("q%s.o LIKE ?" % i)
              self.params    .append(v.replace("*", "%"))
              
          else:
            self.conditions.append("q%s.o = ?" % i)
            self.params    .append(v)
            if d and (d != "*"):
              self.conditions.append("q%s.d = ?" % i)
              self.params    .append(d)
              
    if self.excepts:
      for except_p in self.excepts:  # Use only quads because it may contain several properties mixing objs and datas
        if isinstance(except_p, tuple): # Prop with inverse
          self.except_conditions.append("quads.s = candidates.s AND quads.p = ?")
          self.except_conditions.append("quads.o = candidates.s AND quads.p = ?")
          self.except_params    .append(except_p[0])
          self.except_params    .append(except_p[1])
        else:
          self.except_conditions.append("quads.s = candidates.s AND quads.p = ?")
          self.except_params    .append(except_p)
          
  def sql_components(self, last_request = True):
    transits   = self.transits   + [x for search in self.nested_searchs for x in search.transits]
    tables     = self.tables     + [x for search in self.nested_searchs for x in search.tables]
    conditions = self.conditions + [x for search in self.nested_searchs for x in search.conditions]
    params     = self.params     + [x for search in self.nested_searchs for x in search.params]
    
    if self.nested_searchs:
      for search in self.nested_searchs:
        if search.excepts: raise ValueError("Nested searches with exclusions are not supported!")
        
    if not self.alternatives:
      if self.bm25:
        sql = "SELECT DISTINCT q%s.s, bm25(%s) FROM %s WHERE %s" % (self.target, self.bm25, ", "   .join(tables), " AND ".join(conditions))
      else:
        sql = "SELECT DISTINCT q%s.s FROM %s WHERE %s" % (self.target, ", "   .join(tables), " AND ".join(conditions))
        
    else:
      conditions0 = conditions
      params0     = params
      params      = []
      sqls        = []
      for combination in all_combinations(self.alternatives):
        combination_tabless, combination_conditions, combination_paramss = zip(*combination)
        sql = "SELECT DISTINCT q%s.s FROM %s WHERE %s" % (self.target, ", ".join(tables + [t for ts in combination_tabless for t in ts]), " AND ".join(conditions0 + list(combination_conditions)))
        sqls.append(sql)
        params.extend(params0)
        for combination_params in combination_paramss: params.extend(combination_params)
      sql = "SELECT DISTINCT * FROM (\n%s\n)" % "\nUNION ALL\n".join(sqls)
      
    if self.excepts:
      if sql.startswith("SELECT DISTINCT"): sql = "SELECT %s" % sql[16:]
      transits.append("""candidates(s) AS (%s)""" % sql)
      sql = """SELECT DISTINCT s FROM candidates
EXCEPT %s""" % ("\nUNION ALL ".join("""SELECT candidates.s FROM candidates, quads WHERE %s""" % except_condition for except_condition in self.except_conditions))
      params = params + self.except_params
      
    return transits, sql, params
  
  def __or__(self, other):
    if isinstance(other, _UnionSearchList):
      return _UnionSearchList(self.world, [self, *other.searches])
    return _UnionSearchList(self.world, [self, other])
  
  def __and__(self, other):
    if isinstance(other, _IntersectionSearchList):
      return _IntersectionSearchList(self.world, [self, *other.searches])
    return _IntersectionSearchList(self.world, [self, other])
  
  def dump(self):
    sql, params = self.sql_request()
    print("search debug:")
    prop_vals = ["(nested search)" if isinstance(i[1], (_SearchMixin, _PopulatedSearchList)) else i for i in self.prop_vals]
    print("  prop_vals = ", prop_vals)
    try:
      sql_with_params = sql.replace("?", "%s") % tuple(params)
      print("  req       =\n%s" % sql_with_params)
    except:
      print("  req       =\n%s" % sql)
      print("  params    = ", params)
    



class _PopulatedUnionSearchList(FirstList):
  __slots__ = ["world", "searches"]
  

class _UnionSearchList(FirstList, _SearchMixin, _LazyListMixin):
  __slots__ = ["world", "searches"]
  _PopulatedClass = _PopulatedUnionSearchList
  
  def has_bm25(self): return any(search.has_bm25() for search in self.searches)
  
  nested_searchs = []
  def __init__(self, world, searches):
    self.world    = world
    self.searches = searches
    
  def sql_components(self, last_request = True):
    transits_sqls_params = [s.sql_components(False) for s in self.searches]
    if last_request:
      sql = "SELECT DISTINCT * FROM (\n%s\n)" % "\nUNION ALL\n".join(sql2 for (transits2, sql2, params2) in transits_sqls_params)
    else:
      sql = "SELECT DISTINCT * AS x FROM (\n%s\n)" % "\nUNION ALL\n".join(sql2 for (transits2, sql2, params2) in transits_sqls_params)
      
    params   = []
    transits = []
    for (transits2, sql2, params2) in transits_sqls_params:
      params  .extend(params2)
      transits.extend(transits2)
      
    return transits, sql, params
    
  def __or__(self, other):
    if isinstance(other, _UnionSearchList):
      return _UnionSearchList(self.world, self.searches + other.searches)
    return _UnionSearchList(self.world, self.searches + [other])
  
  def dump(self):
    sql, params = self.sql_request()
    print("search debug:")
    try:
      sql_with_params = sql.replace("?", "%s") % tuple(params)
      print("  req       =\n%s" % sql_with_params)
    except:
      print("  req       =\n%s" % sql)
      print("  params    = ", params)
      
  def explode(self, gen):
    alternatives = []
    for search in self.searches:
      if search.excepts: raise NotImplementedError("Nested search with both union and exception are not supported.")
      if search.alternatives:
        for combination in all_combinations(search.alternatives):
          combination_tabless, combination_conditions, combination_paramss = zip(*combination)
          for alternative_conditions in gen(search.target):
            alternatives.append((search.tables + [t for ts in combination_tabless for t in ts],
                                 " AND ".join(search.conditions + combination_conditions + alternative_conditions),
                                 search.params + [p for ps in combination_paramss for p in ps]))
            
      else:
        for alternative_conditions in gen(search.target):
          alternatives.append((search.tables, " AND ".join(search.conditions + alternative_conditions), search.params))
        
    return tuple(alternatives)
    


class _PopulatedIntersectionSearchList(FirstList):
  __slots__ = ["world", "searches"]
  

class _IntersectionSearchList(FirstList, _SearchMixin, _LazyListMixin):
  __slots__ = ["world", "searches"]
  _PopulatedClass = _PopulatedIntersectionSearchList
  
  def has_bm25(self): return any(search.has_bm25() for search in self.searches)
  
  nested_searchs = []
  def __init__(self, world, searches):
    self.world    = world
    self.searches = searches
    
  def sql_components(self, last_request = True):
    transits_sqls_params = [s.sql_components(False) for s in self.searches]
    if last_request:
      sql = "SELECT DISTINCT * FROM (\n%s\n)" % "\nINTERSECT\n".join(sql2 for (transits2, sql2, params2) in transits_sqls_params)
    else:
      sql = "SELECT DISTINCT * AS x FROM (\n%s\n)" % "\nINTERSECT\n".join(sql2 for (transits2, sql2, params2) in transits_sqls_params)
      
    params   = []
    transits = []
    for (transits2, sql2, params2) in transits_sqls_params:
      params  .extend(params2)
      transits.extend(transits2)
      
    return transits, sql, params
    
  def __and__(self, other):
    if isinstance(other, _IntersectionSearchList):
      return _IntersectionSearchList(self.world, self.searches + other.searches)
    return _IntersectionSearchList(self.world, self.searches + [other])
  
  def dump(self):
    sql, params = self.sql_request()
    print("search debug:")
    try:
      sql_with_params = sql.replace("?", "%s") % tuple(params)
      print("  req       =\n%s" % sql_with_params)
    except:
      print("  req       =\n%s" % sql)
      print("  params    = ", params)
      
  def explode(self, gen): raise NotImplementedError("Nested search with intersection are not supported.")
  
  def _do_search(self):
    return (self.world._get_by_storid(o) for (o,) in self._do_search_rdf())
  _get_content = _do_search
  
  def _do_search_rdf(self):
    r = set()
    first = True
    for search in self.searches:
      sql, params = search.sql_request()
      r1 = self.world.graph.execute(sql, params).fetchall()
      if first:
        r.update(r1)
        first = False
      else:
        r.intersection_update(r1)
    return list(r)
  
  def __len__(self):
    return len(self._do_search_rdf())
