The mixed type of handles has been a constant source of bugs. Converting them all to str in the database API should make maintenance easier. The key to BSDDB tables must still be bytes in the database layer.
981 lines
37 KiB
Python
981 lines
37 KiB
Python
#
|
|
# Gramps - a GTK+/GNOME based genealogy program
|
|
#
|
|
# Copyright (C) 2015-2016 Douglas S. Blank <doug.blank@gmail.com>
|
|
# Copyright (C) 2016-2017 Nick Hall
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 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 General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
#
|
|
|
|
#-------------------------------------------------------------------------
|
|
#
|
|
# Standard python modules
|
|
#
|
|
#-------------------------------------------------------------------------
|
|
import os
|
|
import shutil
|
|
import time
|
|
import sys
|
|
import pickle
|
|
from operator import itemgetter
|
|
import logging
|
|
|
|
#------------------------------------------------------------------------
|
|
#
|
|
# Gramps Modules
|
|
#
|
|
#------------------------------------------------------------------------
|
|
from gramps.gen.db.dbconst import (DBLOGNAME, DBBACKEND, KEY_TO_NAME_MAP,
|
|
TXNADD, TXNUPD, TXNDEL,
|
|
PERSON_KEY, FAMILY_KEY, SOURCE_KEY,
|
|
EVENT_KEY, MEDIA_KEY, PLACE_KEY, NOTE_KEY,
|
|
TAG_KEY, CITATION_KEY, REPOSITORY_KEY,
|
|
REFERENCE_KEY)
|
|
from gramps.gen.db.generic import DbGeneric
|
|
from gramps.gen.lib import (Tag, Media, Person, Family, Source,
|
|
Citation, Event, Place, Repository, Note)
|
|
from gramps.gen.lib.genderstats import GenderStats
|
|
from gramps.gen.const import GRAMPS_LOCALE as glocale
|
|
|
|
LOG = logging.getLogger(".dbapi")
|
|
_LOG = logging.getLogger(DBLOGNAME)
|
|
|
|
class DBAPI(DbGeneric):
|
|
"""
|
|
Database backends class for DB-API 2.0 databases
|
|
"""
|
|
def get_schema_version(self, directory=None):
|
|
"""
|
|
Get the version of the schema that the database was created
|
|
under. Assumes 18, if not found.
|
|
"""
|
|
if directory is None:
|
|
directory = self._directory
|
|
version = 18
|
|
if directory:
|
|
versionpath = os.path.join(directory, "schemaversion.txt")
|
|
if os.path.exists(versionpath):
|
|
with open(versionpath, "r") as version_file:
|
|
version = version_file.read()
|
|
version = int(version)
|
|
else:
|
|
LOG.info("Missing '%s'. Assuming version 18.", versionpath)
|
|
return version
|
|
|
|
def write_version(self, directory):
|
|
"""Write files for a newly created DB."""
|
|
_LOG.debug("Write schema version file to %s", str(self.VERSION[0]))
|
|
versionpath = os.path.join(directory, "schemaversion.txt")
|
|
with open(versionpath, "w") as version_file:
|
|
version_file.write(str(self.VERSION[0]))
|
|
|
|
versionpath = os.path.join(directory, str(DBBACKEND))
|
|
_LOG.debug("Write database backend file to 'dbapi'")
|
|
with open(versionpath, "w") as version_file:
|
|
version_file.write("dbapi")
|
|
# Write settings.py and settings.ini:
|
|
settings_py = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"settings.py")
|
|
settings_ini = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
|
"settings.ini")
|
|
LOG.debug("Copy settings.py from: " + settings_py)
|
|
LOG.debug("Copy settings.ini from: " + settings_py)
|
|
shutil.copy2(settings_py, directory)
|
|
shutil.copy2(settings_ini, directory)
|
|
|
|
def _initialize(self, directory):
|
|
# Run code from directory
|
|
from gramps.gen.utils.configmanager import ConfigManager
|
|
config_file = os.path.join(directory, 'settings.ini')
|
|
config_mgr = ConfigManager(config_file)
|
|
config_mgr.register('database.dbtype', 'sqlite')
|
|
config_mgr.register('database.dbname', 'gramps')
|
|
config_mgr.register('database.host', 'localhost')
|
|
config_mgr.register('database.user', 'user')
|
|
config_mgr.register('database.password', 'password')
|
|
config_mgr.register('database.port', 'port')
|
|
config_mgr.load() # load from settings.ini
|
|
settings = {
|
|
"__file__":
|
|
os.path.join(directory, "settings.py"),
|
|
"config": config_mgr
|
|
}
|
|
settings_file = os.path.join(directory, "settings.py")
|
|
with open(settings_file) as fp:
|
|
code = compile(fp.read(), settings_file, 'exec')
|
|
exec(code, globals(), settings)
|
|
self.dbapi = settings["dbapi"]
|
|
|
|
# We use the existence of the person table as a proxy for the database
|
|
# being new
|
|
if not self.dbapi.table_exists("person"):
|
|
self._create_schema()
|
|
|
|
def _create_schema(self):
|
|
"""
|
|
Create and update schema.
|
|
"""
|
|
self.dbapi.begin()
|
|
|
|
# make sure schema is up to date:
|
|
self.dbapi.execute('CREATE TABLE person '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'given_name TEXT, '
|
|
'surname TEXT, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE family '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'father_handle VARCHAR(50), '
|
|
'mother_handle VARCHAR(50), '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE source '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE citation '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE event '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE media '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE place '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'enclosed_by VARCHAR(50), '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE repository '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE note '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'gramps_id TEXT, '
|
|
'blob_data BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE tag '
|
|
'('
|
|
'handle VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'blob_data BLOB'
|
|
')')
|
|
# Secondary:
|
|
self.dbapi.execute('CREATE TABLE reference '
|
|
'('
|
|
'obj_handle VARCHAR(50), '
|
|
'obj_class TEXT, '
|
|
'ref_handle VARCHAR(50), '
|
|
'ref_class TEXT'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE name_group '
|
|
'('
|
|
'name VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'grouping TEXT'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE metadata '
|
|
'('
|
|
'setting VARCHAR(50) PRIMARY KEY NOT NULL, '
|
|
'value BLOB'
|
|
')')
|
|
self.dbapi.execute('CREATE TABLE gender_stats '
|
|
'('
|
|
'given_name TEXT, '
|
|
'female INTEGER, '
|
|
'male INTEGER, '
|
|
'unknown INTEGER'
|
|
')')
|
|
|
|
self._create_secondary_columns()
|
|
|
|
## Indices:
|
|
self.dbapi.execute('CREATE INDEX person_gramps_id '
|
|
'ON person(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX person_surname '
|
|
'ON person(surname)')
|
|
self.dbapi.execute('CREATE INDEX person_given_name '
|
|
'ON person(given_name)')
|
|
self.dbapi.execute('CREATE INDEX source_title '
|
|
'ON source(title)')
|
|
self.dbapi.execute('CREATE INDEX source_gramps_id '
|
|
'ON source(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX citation_page '
|
|
'ON citation(page)')
|
|
self.dbapi.execute('CREATE INDEX citation_gramps_id '
|
|
'ON citation(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX media_desc '
|
|
'ON media(desc)')
|
|
self.dbapi.execute('CREATE INDEX media_gramps_id '
|
|
'ON media(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX place_title '
|
|
'ON place(title)')
|
|
self.dbapi.execute('CREATE INDEX place_enclosed_by '
|
|
'ON place(enclosed_by)')
|
|
self.dbapi.execute('CREATE INDEX place_gramps_id '
|
|
'ON place(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX tag_name '
|
|
'ON tag(name)')
|
|
self.dbapi.execute('CREATE INDEX reference_ref_handle '
|
|
'ON reference(ref_handle)')
|
|
self.dbapi.execute('CREATE INDEX family_gramps_id '
|
|
'ON family(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX event_gramps_id '
|
|
'ON event(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX repository_gramps_id '
|
|
'ON repository(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX note_gramps_id '
|
|
'ON note(gramps_id)')
|
|
self.dbapi.execute('CREATE INDEX reference_obj_handle '
|
|
'ON reference(obj_handle)')
|
|
|
|
self.dbapi.commit()
|
|
|
|
def _close(self):
|
|
self.dbapi.close()
|
|
|
|
def _txn_begin(self):
|
|
"""
|
|
Lowlevel interface to the backend transaction.
|
|
Executes a db BEGIN;
|
|
"""
|
|
_LOG.debug(" DBAPI %s transaction begin", hex(id(self)))
|
|
self.dbapi.begin()
|
|
|
|
def _txn_commit(self):
|
|
"""
|
|
Lowlevel interface to the backend transaction.
|
|
Executes a db END;
|
|
"""
|
|
_LOG.debug(" DBAPI %s transaction commit", hex(id(self)))
|
|
self.dbapi.commit()
|
|
|
|
def _txn_abort(self):
|
|
"""
|
|
Lowlevel interface to the backend transaction.
|
|
Executes a db ROLLBACK;
|
|
"""
|
|
self.dbapi.rollback()
|
|
|
|
def transaction_begin(self, transaction):
|
|
"""
|
|
Transactions are handled automatically by the db layer.
|
|
"""
|
|
_LOG.debug(" %sDBAPI %s transaction begin for '%s'",
|
|
"Batch " if transaction.batch else "",
|
|
hex(id(self)), transaction.get_description())
|
|
self.transaction = transaction
|
|
self.dbapi.begin()
|
|
return transaction
|
|
|
|
def transaction_commit(self, txn):
|
|
"""
|
|
Executed at the end of a transaction.
|
|
"""
|
|
_LOG.debug(" %sDBAPI %s transaction commit for '%s'",
|
|
"Batch " if txn.batch else "",
|
|
hex(id(self)), txn.get_description())
|
|
|
|
action = {TXNADD: "-add",
|
|
TXNUPD: "-update",
|
|
TXNDEL: "-delete",
|
|
None: "-delete"}
|
|
if txn.batch:
|
|
# FIXME: need a User GUI update callback here:
|
|
self.reindex_reference_map(lambda percent: percent)
|
|
self.dbapi.commit()
|
|
if not txn.batch:
|
|
# Now, emit signals:
|
|
for (obj_type_val, txn_type_val) in list(txn):
|
|
if obj_type_val == REFERENCE_KEY:
|
|
continue
|
|
if txn_type_val == TXNDEL:
|
|
handles = [handle for (handle, data) in
|
|
txn[(obj_type_val, txn_type_val)]]
|
|
else:
|
|
handles = [handle for (handle, data) in
|
|
txn[(obj_type_val, txn_type_val)]
|
|
if (handle, None)
|
|
not in txn[(obj_type_val, TXNDEL)]]
|
|
if handles:
|
|
signal = KEY_TO_NAME_MAP[
|
|
obj_type_val] + action[txn_type_val]
|
|
self.emit(signal, (handles, ))
|
|
self.transaction = None
|
|
msg = txn.get_description()
|
|
self.undodb.commit(txn, msg)
|
|
self._after_commit(txn)
|
|
txn.clear()
|
|
self.has_changed = True
|
|
|
|
def transaction_abort(self, txn):
|
|
"""
|
|
Executed after a batch operation abort.
|
|
"""
|
|
self.dbapi.rollback()
|
|
self.transaction = None
|
|
txn.clear()
|
|
txn.first = None
|
|
txn.last = None
|
|
self._after_commit(txn)
|
|
|
|
def _get_metadata(self, key, default=[]):
|
|
"""
|
|
Get an item from the database.
|
|
|
|
Default is an empty list, which is a mutable and
|
|
thus a bad default (pylint will complain).
|
|
|
|
However, it is just used as a value, and not altered, so
|
|
its use here is ok.
|
|
"""
|
|
self.dbapi.execute(
|
|
"SELECT value FROM metadata WHERE setting = ?", [key])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
return pickle.loads(row[0])
|
|
elif default == []:
|
|
return []
|
|
else:
|
|
return default
|
|
|
|
def _set_metadata(self, key, value):
|
|
"""
|
|
key: string
|
|
value: item, will be serialized here
|
|
"""
|
|
self._txn_begin()
|
|
self.dbapi.execute("SELECT 1 FROM metadata WHERE setting = ?", [key])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
self.dbapi.execute(
|
|
"UPDATE metadata SET value = ? WHERE setting = ?",
|
|
[pickle.dumps(value), key])
|
|
else:
|
|
self.dbapi.execute(
|
|
"INSERT INTO metadata (setting, value) VALUES (?, ?)",
|
|
[key, pickle.dumps(value)])
|
|
self._txn_commit()
|
|
|
|
def get_name_group_keys(self):
|
|
"""
|
|
Return the defined names that have been assigned to a default grouping.
|
|
"""
|
|
self.dbapi.execute("SELECT name FROM name_group ORDER BY name")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_name_group_mapping(self, key):
|
|
"""
|
|
Return the default grouping name for a surname.
|
|
"""
|
|
self.dbapi.execute(
|
|
"SELECT grouping FROM name_group WHERE name = ?", [key])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
return row[0]
|
|
else:
|
|
return key
|
|
|
|
def get_person_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Person in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by surnames.
|
|
"""
|
|
if sort_handles:
|
|
self.dbapi.execute("SELECT handle FROM person "
|
|
"ORDER BY surname COLLATE glocale")
|
|
else:
|
|
self.dbapi.execute("SELECT handle FROM person")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_family_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Family in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by surnames.
|
|
"""
|
|
if sort_handles:
|
|
sql = ("SELECT family.handle " +
|
|
"FROM family " +
|
|
"LEFT JOIN person AS father " +
|
|
"ON family.father_handle = father.handle " +
|
|
"LEFT JOIN person AS mother " +
|
|
"ON family.mother_handle = mother.handle " +
|
|
"ORDER BY (CASE WHEN father.handle IS NULL " +
|
|
"THEN mother.surname " +
|
|
"ELSE father.surname " +
|
|
"END), " +
|
|
"(CASE WHEN family.handle IS NULL " +
|
|
"THEN mother.given_name " +
|
|
"ELSE father.given_name " +
|
|
"END) " +
|
|
"COLLATE glocale")
|
|
self.dbapi.execute(sql)
|
|
else:
|
|
self.dbapi.execute("SELECT handle FROM family")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_event_handles(self):
|
|
"""
|
|
Return a list of database handles, one handle for each Event in the
|
|
database.
|
|
"""
|
|
self.dbapi.execute("SELECT handle FROM event")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_citation_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Citation in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by Citation title.
|
|
"""
|
|
if sort_handles:
|
|
self.dbapi.execute("SELECT handle FROM citation "
|
|
"ORDER BY page COLLATE glocale")
|
|
else:
|
|
self.dbapi.execute("SELECT handle FROM citation")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_source_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Source in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by Source title.
|
|
"""
|
|
if sort_handles:
|
|
self.dbapi.execute("SELECT handle FROM source "
|
|
"ORDER BY title COLLATE glocale")
|
|
else:
|
|
self.dbapi.execute("SELECT handle from source")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_place_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Place in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by Place title.
|
|
"""
|
|
if sort_handles:
|
|
self.dbapi.execute("SELECT handle FROM place "
|
|
"ORDER BY title COLLATE glocale")
|
|
else:
|
|
self.dbapi.execute("SELECT handle FROM place")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_repository_handles(self):
|
|
"""
|
|
Return a list of database handles, one handle for each Repository in
|
|
the database.
|
|
"""
|
|
self.dbapi.execute("SELECT handle FROM repository")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_media_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Media in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by title.
|
|
"""
|
|
if sort_handles:
|
|
self.dbapi.execute("SELECT handle FROM media "
|
|
"ORDER BY desc COLLATE glocale")
|
|
else:
|
|
self.dbapi.execute("SELECT handle FROM media")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_note_handles(self):
|
|
"""
|
|
Return a list of database handles, one handle for each Note in the
|
|
database.
|
|
"""
|
|
self.dbapi.execute("SELECT handle FROM note")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_tag_handles(self, sort_handles=False):
|
|
"""
|
|
Return a list of database handles, one handle for each Tag in
|
|
the database.
|
|
|
|
If sort_handles is True, the list is sorted by Tag name.
|
|
"""
|
|
if sort_handles:
|
|
self.dbapi.execute("SELECT handle FROM tag "
|
|
"ORDER BY name COLLATE glocale")
|
|
else:
|
|
self.dbapi.execute("SELECT handle FROM tag")
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_tag_from_name(self, name):
|
|
"""
|
|
Find a Tag in the database from the passed Tag name.
|
|
|
|
If no such Tag exists, None is returned.
|
|
"""
|
|
self.dbapi.execute("SELECT blob_data FROM tag WHERE name = ?", [name])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
return Tag.create(pickle.loads(row[0]))
|
|
return None
|
|
|
|
def get_number_of(self, obj_key):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT count(1) FROM %s" % table
|
|
self.dbapi.execute(sql)
|
|
row = self.dbapi.fetchone()
|
|
return row[0]
|
|
|
|
def has_name_group_key(self, key):
|
|
"""
|
|
Return if a key exists in the name_group table.
|
|
"""
|
|
self.dbapi.execute("SELECT grouping FROM name_group WHERE name = ?",
|
|
[key])
|
|
row = self.dbapi.fetchone()
|
|
return True if row else False
|
|
|
|
def set_name_group_mapping(self, name, grouping):
|
|
"""
|
|
Set the default grouping name for a surname.
|
|
"""
|
|
self.dbapi.execute("SELECT 1 FROM name_group WHERE name = ?",
|
|
[name])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
self.dbapi.execute("DELETE FROM name_group WHERE name = ?",
|
|
[name])
|
|
self.dbapi.execute(
|
|
"INSERT INTO name_group (name, grouping) VALUES(?, ?)",
|
|
[name, grouping])
|
|
|
|
def _commit_base(self, obj, obj_key, trans, change_time):
|
|
"""
|
|
Commit the specified object to the database, storing the changes as
|
|
part of the transaction.
|
|
"""
|
|
old_data = None
|
|
obj.change = int(change_time or time.time())
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
|
|
if self.has_handle(obj_key, obj.handle):
|
|
old_data = self.get_raw_data(obj_key, obj.handle)
|
|
# update the object:
|
|
sql = "UPDATE %s SET blob_data = ? WHERE handle = ?" % table
|
|
self.dbapi.execute(sql,
|
|
[pickle.dumps(obj.serialize()),
|
|
obj.handle])
|
|
else:
|
|
# Insert the object:
|
|
sql = ("INSERT INTO %s (handle, blob_data) VALUES (?, ?)") % table
|
|
self.dbapi.execute(sql,
|
|
[obj.handle,
|
|
pickle.dumps(obj.serialize())])
|
|
self._update_secondary_values(obj)
|
|
if not trans.batch:
|
|
self._update_backlinks(obj, trans)
|
|
if old_data:
|
|
trans.add(obj_key, TXNUPD, obj.handle,
|
|
old_data,
|
|
obj.serialize())
|
|
else:
|
|
trans.add(obj_key, TXNADD, obj.handle,
|
|
None,
|
|
obj.serialize())
|
|
|
|
return old_data
|
|
|
|
def _update_backlinks(self, obj, transaction):
|
|
|
|
# Find existing references
|
|
sql = ("SELECT ref_class, ref_handle " +
|
|
"FROM reference WHERE obj_handle = ?")
|
|
self.dbapi.execute(sql, [obj.handle])
|
|
existing_references = set(self.dbapi.fetchall())
|
|
|
|
# Once we have the list of rows that already have a reference
|
|
# we need to compare it with the list of objects that are
|
|
# still references from the primary object.
|
|
current_references = set(obj.get_referenced_handles_recursively())
|
|
no_longer_required_references = existing_references.difference(
|
|
current_references)
|
|
new_references = current_references.difference(existing_references)
|
|
|
|
# Delete the existing references
|
|
self.dbapi.execute("DELETE FROM reference WHERE obj_handle = ?",
|
|
[obj.handle])
|
|
|
|
# Now, add the current ones
|
|
for (ref_class_name, ref_handle) in current_references:
|
|
sql = ("INSERT INTO reference " +
|
|
"(obj_handle, obj_class, ref_handle, ref_class)" +
|
|
"VALUES(?, ?, ?, ?)")
|
|
self.dbapi.execute(sql, [obj.handle, obj.__class__.__name__,
|
|
ref_handle, ref_class_name])
|
|
|
|
if not transaction.batch:
|
|
# Add new references to the transaction
|
|
for (ref_class_name, ref_handle) in new_references:
|
|
key = (obj.handle, ref_handle)
|
|
data = (obj.handle, obj.__class__.__name__,
|
|
ref_handle, ref_class_name)
|
|
transaction.add(REFERENCE_KEY, TXNADD, key, None, data)
|
|
|
|
# Add old references to the transaction
|
|
for (ref_class_name, ref_handle) in no_longer_required_references:
|
|
key = (obj.handle, ref_handle)
|
|
old_data = (obj.handle, obj.__class__.__name__,
|
|
ref_handle, ref_class_name)
|
|
transaction.add(REFERENCE_KEY, TXNDEL, key, old_data, None)
|
|
|
|
def _do_remove(self, handle, transaction, obj_key):
|
|
if self.readonly or not handle:
|
|
return
|
|
if self.has_handle(obj_key, handle):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "DELETE FROM %s WHERE handle = ?" % table
|
|
self.dbapi.execute(sql, [handle])
|
|
if not transaction.batch:
|
|
data = self.get_raw_data(obj_key, handle)
|
|
transaction.add(obj_key, TXNDEL, handle, data, None)
|
|
|
|
def find_backlink_handles(self, handle, include_classes=None):
|
|
"""
|
|
Find all objects that hold a reference to the object handle.
|
|
|
|
Returns an interator over a list of (class_name, handle) tuples.
|
|
|
|
:param handle: handle of the object to search for.
|
|
:type handle: database handle
|
|
:param include_classes: list of class names to include in the results.
|
|
Default: None means include all classes.
|
|
:type include_classes: list of class names
|
|
|
|
Note that this is a generator function, it returns a iterator for
|
|
use in loops. If you want a list of the results use::
|
|
|
|
result_list = list(find_backlink_handles(handle))
|
|
"""
|
|
self.dbapi.execute("SELECT obj_class, obj_handle "
|
|
"FROM reference "
|
|
"WHERE ref_handle = ?",
|
|
[handle])
|
|
rows = self.dbapi.fetchall()
|
|
for row in rows:
|
|
if (include_classes is None) or (row[0] in include_classes):
|
|
yield (row[0], row[1])
|
|
|
|
def find_initial_person(self):
|
|
"""
|
|
Returns first person in the database
|
|
"""
|
|
handle = self.get_default_handle()
|
|
person = None
|
|
if handle:
|
|
person = self.get_person_from_handle(handle)
|
|
if person:
|
|
return person
|
|
self.dbapi.execute("SELECT handle FROM person")
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
return self.get_person_from_handle(row[0])
|
|
|
|
def _iter_handles(self, obj_key):
|
|
"""
|
|
Return an iterator over handles in the database
|
|
"""
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT handle FROM %s" % table
|
|
self.dbapi.execute(sql)
|
|
rows = self.dbapi.fetchall()
|
|
for row in rows:
|
|
yield row[0]
|
|
|
|
def _iter_raw_data(self, obj_key):
|
|
"""
|
|
Return an iterator over raw data in the database.
|
|
"""
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT handle, blob_data FROM %s" % table
|
|
with self.dbapi.cursor() as cursor:
|
|
cursor.execute(sql)
|
|
rows = cursor.fetchmany()
|
|
while rows:
|
|
for row in rows:
|
|
yield (row[0], pickle.loads(row[1]))
|
|
rows = cursor.fetchmany()
|
|
|
|
def _iter_raw_place_tree_data(self):
|
|
"""
|
|
Return an iterator over raw data in the place hierarchy.
|
|
"""
|
|
to_do = ['']
|
|
sql = 'SELECT handle, blob_data FROM place WHERE enclosed_by = ?'
|
|
while to_do:
|
|
handle = to_do.pop()
|
|
self.dbapi.execute(sql, [handle])
|
|
rows = self.dbapi.fetchall()
|
|
for row in rows:
|
|
to_do.append(row[0])
|
|
yield (row[0], pickle.loads(row[1]))
|
|
|
|
def reindex_reference_map(self, callback):
|
|
"""
|
|
Reindex all primary records in the database.
|
|
"""
|
|
callback(4)
|
|
self.dbapi.execute("DELETE FROM reference")
|
|
primary_table = (
|
|
(self.get_person_cursor, Person),
|
|
(self.get_family_cursor, Family),
|
|
(self.get_event_cursor, Event),
|
|
(self.get_place_cursor, Place),
|
|
(self.get_source_cursor, Source),
|
|
(self.get_citation_cursor, Citation),
|
|
(self.get_media_cursor, Media),
|
|
(self.get_repository_cursor, Repository),
|
|
(self.get_note_cursor, Note),
|
|
(self.get_tag_cursor, Tag),
|
|
)
|
|
# Now we use the functions and classes defined above
|
|
# to loop through each of the primary object tables.
|
|
for cursor_func, class_func in primary_table:
|
|
logging.info("Rebuilding %s reference map", class_func.__name__)
|
|
with cursor_func() as cursor:
|
|
for found_handle, val in cursor:
|
|
obj = class_func.create(val)
|
|
references = set(obj.get_referenced_handles_recursively())
|
|
# handle addition of new references
|
|
for (ref_class_name, ref_handle) in references:
|
|
self.dbapi.execute(
|
|
"INSERT INTO reference "
|
|
"(obj_handle, obj_class, ref_handle, ref_class) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
[obj.handle,
|
|
obj.__class__.__name__,
|
|
ref_handle,
|
|
ref_class_name])
|
|
callback(5)
|
|
|
|
def rebuild_secondary(self, update):
|
|
"""
|
|
Rebuild secondary indices
|
|
"""
|
|
# First, expand blob to individual fields:
|
|
self._update_secondary_values()
|
|
# Next, rebuild stats:
|
|
gstats = self.get_gender_stats()
|
|
self.genderStats = GenderStats(gstats)
|
|
|
|
def has_handle(self, obj_key, handle):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT 1 FROM %s WHERE handle = ?" % table
|
|
self.dbapi.execute(sql, [handle])
|
|
return self.dbapi.fetchone() is not None
|
|
|
|
def has_gramps_id(self, obj_key, gramps_id):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT 1 FROM %s WHERE gramps_id = ?" % table
|
|
self.dbapi.execute(sql, [gramps_id])
|
|
return self.dbapi.fetchone() != None
|
|
|
|
def get_gramps_ids(self, obj_key):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT gramps_id FROM %s" % table
|
|
self.dbapi.execute(sql)
|
|
rows = self.dbapi.fetchall()
|
|
return [row[0] for row in rows]
|
|
|
|
def get_raw_data(self, obj_key, handle):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT blob_data FROM %s WHERE handle = ?" % table
|
|
self.dbapi.execute(sql, [handle])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
return pickle.loads(row[0])
|
|
|
|
def _get_raw_from_id_data(self, obj_key, gramps_id):
|
|
table = KEY_TO_NAME_MAP[obj_key]
|
|
sql = "SELECT blob_data FROM %s WHERE gramps_id = ?" % table
|
|
self.dbapi.execute(sql, [gramps_id])
|
|
row = self.dbapi.fetchone()
|
|
if row:
|
|
return pickle.loads(row[0])
|
|
|
|
def get_gender_stats(self):
|
|
"""
|
|
Returns a dictionary of
|
|
{given_name: (male_count, female_count, unknown_count)}
|
|
"""
|
|
self.dbapi.execute("SELECT given_name, female, male, unknown "
|
|
"FROM gender_stats")
|
|
gstats = {}
|
|
for row in self.dbapi.fetchall():
|
|
gstats[row[0]] = (row[1], row[2], row[3])
|
|
return gstats
|
|
|
|
def save_gender_stats(self, gstats):
|
|
self._txn_begin()
|
|
self.dbapi.execute("DELETE FROM gender_stats")
|
|
for key in gstats.stats:
|
|
female, male, unknown = gstats.stats[key]
|
|
self.dbapi.execute("INSERT INTO gender_stats "
|
|
"(given_name, female, male, unknown) "
|
|
"VALUES (?, ?, ?, ?)",
|
|
[key, female, male, unknown])
|
|
self._txn_commit()
|
|
|
|
def get_surname_list(self):
|
|
"""
|
|
Return the list of locale-sorted surnames contained in the database.
|
|
"""
|
|
self.dbapi.execute("SELECT DISTINCT surname "
|
|
"FROM person "
|
|
"ORDER BY surname")
|
|
surname_list = []
|
|
for row in self.dbapi.fetchall():
|
|
surname_list.append(row[0])
|
|
return surname_list
|
|
|
|
def _sql_type(self, schema_type, max_length):
|
|
"""
|
|
Given a schema type, return the SQL type for
|
|
a new column.
|
|
"""
|
|
if schema_type == 'string':
|
|
if max_length:
|
|
return "VARCHAR(%s)" % max_length
|
|
else:
|
|
return "TEXT"
|
|
elif schema_type in ['boolean', 'integer']:
|
|
return "INTEGER"
|
|
elif schema_type == 'number':
|
|
return "REAL"
|
|
else:
|
|
return "BLOB"
|
|
|
|
def _create_secondary_columns(self):
|
|
"""
|
|
Create secondary columns.
|
|
"""
|
|
LOG.info("Creating secondary columns...")
|
|
for cls in (Person, Family, Event, Place, Repository, Source,
|
|
Citation, Media, Note, Tag):
|
|
table_name = cls.__name__.lower()
|
|
for field, schema_type, max_length in cls.get_secondary_fields():
|
|
sql_type = self._sql_type(schema_type, max_length)
|
|
try:
|
|
# test to see if it exists:
|
|
self.dbapi.execute("SELECT %s FROM %s LIMIT 1"
|
|
% (field, table_name))
|
|
LOG.info(" Table %s, field %s is up to date",
|
|
table_name, field)
|
|
except:
|
|
# if not, let's add it
|
|
LOG.info(" Table %s, field %s was added",
|
|
table_name, field)
|
|
self.dbapi.execute("ALTER TABLE %s ADD COLUMN %s %s"
|
|
% (table_name, field, sql_type))
|
|
|
|
def _update_secondary_values(self, obj):
|
|
"""
|
|
Given a primary object update its secondary field values
|
|
in the database.
|
|
Does not commit.
|
|
"""
|
|
table = obj.__class__.__name__
|
|
fields = [field[0] for field in obj.get_secondary_fields()]
|
|
sets = []
|
|
values = []
|
|
for field in fields:
|
|
sets.append("%s = ?" % field)
|
|
values.append(getattr(obj, field))
|
|
|
|
# Derived fields
|
|
if table == 'Person':
|
|
given_name, surname = self._get_person_data(obj)
|
|
sets.append("given_name = ?")
|
|
values.append(given_name)
|
|
sets.append("surname = ?")
|
|
values.append(surname)
|
|
if table == 'Place':
|
|
handle = self._get_place_data(obj)
|
|
sets.append("enclosed_by = ?")
|
|
values.append(handle)
|
|
|
|
if len(values) > 0:
|
|
table_name = table.lower()
|
|
self.dbapi.execute("UPDATE %s SET %s where handle = ?"
|
|
% (table_name, ", ".join(sets)),
|
|
self._sql_cast_list(values)
|
|
+ [obj.handle])
|
|
|
|
def _sql_cast_list(self, values):
|
|
"""
|
|
Given a list of field names and values, return the values
|
|
in the appropriate type.
|
|
"""
|
|
return [v if not isinstance(v, bool) else int(v) for v in values]
|
|
|
|
def get_summary(self):
|
|
"""
|
|
Returns dictionary of summary item.
|
|
Should include, if possible:
|
|
|
|
_("Number of people")
|
|
_("Version")
|
|
_("Schema version")
|
|
"""
|
|
summary = super().get_summary()
|
|
summary.update(self.dbapi.__class__.get_summary())
|
|
return summary
|