%PDF- %PDF-
| Direktori : /lib/calibre/calibre/library/ |
| Current File : //lib/calibre/calibre/library/custom_columns.py |
#!/usr/bin/env python3
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import json, re, numbers
from functools import partial
from calibre import prints, force_unicode
from calibre.constants import preferred_encoding
from calibre.library.field_metadata import FieldMetadata
from calibre.utils.date import parse_date
from calibre.utils.config import tweaks
from polyglot.builtins import string_or_bytes
class CustomColumns:
CUSTOM_DATA_TYPES = frozenset(['rating', 'text', 'comments', 'datetime',
'int', 'float', 'bool', 'series', 'composite', 'enumeration'])
def custom_table_names(self, num):
return 'custom_column_%d'%num, 'books_custom_column_%d_link'%num
@property
def custom_tables(self):
return {x[0] for x in self.conn.get(
'SELECT name FROM sqlite_master WHERE type="table" AND '
'(name GLOB "custom_column_*" OR name GLOB "books_custom_column_*")')}
def __init__(self):
# Verify that CUSTOM_DATA_TYPES is a (possibly improper) subset of
# VALID_DATA_TYPES
if len(self.CUSTOM_DATA_TYPES - FieldMetadata.VALID_DATA_TYPES) > 0:
raise ValueError('Unknown custom column type in set')
# Delete marked custom columns
for record in self.conn.get(
'SELECT id FROM custom_columns WHERE mark_for_delete=1'):
num = record[0]
table, lt = self.custom_table_names(num)
self.conn.executescript('''\
DROP INDEX IF EXISTS {table}_idx;
DROP INDEX IF EXISTS {lt}_aidx;
DROP INDEX IF EXISTS {lt}_bidx;
DROP TRIGGER IF EXISTS fkc_update_{lt}_a;
DROP TRIGGER IF EXISTS fkc_update_{lt}_b;
DROP TRIGGER IF EXISTS fkc_insert_{lt};
DROP TRIGGER IF EXISTS fkc_delete_{lt};
DROP TRIGGER IF EXISTS fkc_insert_{table};
DROP TRIGGER IF EXISTS fkc_delete_{table};
DROP VIEW IF EXISTS tag_browser_{table};
DROP VIEW IF EXISTS tag_browser_filtered_{table};
DROP TABLE IF EXISTS {table};
DROP TABLE IF EXISTS {lt};
'''.format(table=table, lt=lt)
)
self.conn.execute('DELETE FROM custom_columns WHERE mark_for_delete=1')
self.conn.commit()
# Load metadata for custom columns
self.custom_column_label_map, self.custom_column_num_map = {}, {}
triggers = []
remove = []
custom_tables = self.custom_tables
for record in self.conn.get(
'SELECT label,name,datatype,editable,display,normalized,id,is_multiple FROM custom_columns'):
data = {
'label':record[0],
'name':record[1],
'datatype':record[2],
'editable':bool(record[3]),
'display':json.loads(record[4]),
'normalized':bool(record[5]),
'num':record[6],
'is_multiple':bool(record[7]),
}
if data['display'] is None:
data['display'] = {}
# set up the is_multiple separator dict
if data['is_multiple']:
if data['display'].get('is_names', False):
seps = {'cache_to_list': '|', 'ui_to_list': '&', 'list_to_ui': ' & '}
elif data['datatype'] == 'composite':
seps = {'cache_to_list': ',', 'ui_to_list': ',', 'list_to_ui': ', '}
else:
seps = {'cache_to_list': '|', 'ui_to_list': ',', 'list_to_ui': ', '}
else:
seps = {}
data['multiple_seps'] = seps
table, lt = self.custom_table_names(data['num'])
if table not in custom_tables or (data['normalized'] and lt not in
custom_tables):
remove.append(data)
continue
self.custom_column_label_map[data['label']] = data['num']
self.custom_column_num_map[data['num']] = \
self.custom_column_label_map[data['label']] = data
# Create Foreign Key triggers
if data['normalized']:
trigger = 'DELETE FROM %s WHERE book=OLD.id;'%lt
else:
trigger = 'DELETE FROM %s WHERE book=OLD.id;'%table
triggers.append(trigger)
if remove:
for data in remove:
prints('WARNING: Custom column %r not found, removing.' %
data['label'])
self.conn.execute('DELETE FROM custom_columns WHERE id=?',
(data['num'],))
self.conn.commit()
if triggers:
self.conn.execute('''\
CREATE TEMP TRIGGER custom_books_delete_trg
AFTER DELETE ON books
BEGIN
%s
END;
'''%(' \n'.join(triggers)))
self.conn.commit()
# Setup data adapters
def adapt_text(x, d):
if d['is_multiple']:
if x is None:
return []
if isinstance(x, (str, bytes)):
x = x.split(d['multiple_seps']['ui_to_list'])
x = [y.strip() for y in x if y.strip()]
x = [y.decode(preferred_encoding, 'replace') if not isinstance(y,
str) else y for y in x]
return [' '.join(y.split()) for y in x]
else:
return x if x is None or isinstance(x, str) else \
x.decode(preferred_encoding, 'replace')
def adapt_datetime(x, d):
if isinstance(x, (str, bytes)):
x = parse_date(x, assume_utc=False, as_utc=False)
return x
def adapt_bool(x, d):
if isinstance(x, (str, bytes)):
if isinstance(x, bytes):
x = force_unicode(x)
x = x.lower()
if x == 'true':
x = True
elif x == 'false':
x = False
elif x == 'none':
x = None
else:
x = bool(int(x))
return x
def adapt_enum(x, d):
v = adapt_text(x, d)
if not v:
v = None
return v
def adapt_number(x, d):
if x is None:
return None
if isinstance(x, (str, bytes)):
if isinstance(x, bytes):
x = force_unicode(x)
if x.lower() == 'none':
return None
if d['datatype'] == 'int':
return int(x)
return float(x)
self.custom_data_adapters = {
'float': adapt_number,
'int': adapt_number,
'rating':lambda x,d : x if x is None else min(10., max(0., float(x))),
'bool': adapt_bool,
'comments': lambda x,d: adapt_text(x, {'is_multiple':False}),
'datetime' : adapt_datetime,
'text':adapt_text,
'series':adapt_text,
'enumeration': adapt_enum
}
# Create Tag Browser categories for custom columns
for k in sorted(self.custom_column_label_map.keys()):
v = self.custom_column_label_map[k]
if v['normalized']:
is_category = True
else:
is_category = False
is_m = v['multiple_seps']
tn = 'custom_column_{}'.format(v['num'])
self.field_metadata.add_custom_field(label=v['label'],
table=tn, column='value', datatype=v['datatype'],
colnum=v['num'], name=v['name'], display=v['display'],
is_multiple=is_m, is_category=is_category,
is_editable=v['editable'], is_csp=False)
def get_custom(self, idx, label=None, num=None, index_is_id=False):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
row = self.data._data[idx] if index_is_id else self.data[idx]
ans = row[self.FIELD_MAP[data['num']]]
if data['is_multiple'] and data['datatype'] == 'text':
ans = ans.split(data['multiple_seps']['cache_to_list']) if ans else []
if data['display'].get('sort_alpha', False):
ans.sort(key=lambda x:x.lower())
if data['datatype'] == 'datetime' and isinstance(ans, string_or_bytes):
from calibre.db.tables import c_parse, UNDEFINED_DATE
ans = c_parse(ans)
if ans is UNDEFINED_DATE:
ans = None
return ans
def get_custom_extra(self, idx, label=None, num=None, index_is_id=False):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
# add future datatypes with an extra column here
if data['datatype'] not in ['series']:
return None
ign,lt = self.custom_table_names(data['num'])
idx = idx if index_is_id else self.id(idx)
return self.conn.get('''SELECT extra FROM %s
WHERE book=?'''%lt, (idx,), all=False)
def get_custom_and_extra(self, idx, label=None, num=None, index_is_id=False):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
idx = idx if index_is_id else self.id(idx)
row = self.data._data[idx]
ans = row[self.FIELD_MAP[data['num']]]
if data['is_multiple'] and data['datatype'] == 'text':
ans = ans.split(data['multiple_seps']['cache_to_list']) if ans else []
if data['display'].get('sort_alpha', False):
ans.sort(key=lambda x: x.lower())
if data['datatype'] == 'datetime' and isinstance(ans, string_or_bytes):
from calibre.db.tables import c_parse, UNDEFINED_DATE
ans = c_parse(ans)
if ans is UNDEFINED_DATE:
ans = None
if data['datatype'] != 'series':
return (ans, None)
ign,lt = self.custom_table_names(data['num'])
extra = self.conn.get('''SELECT extra FROM %s
WHERE book=?'''%lt, (idx,), all=False)
return (ans, extra)
# convenience methods for tag editing
def get_custom_items_with_ids(self, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table,lt = self.custom_table_names(data['num'])
if not data['normalized']:
return []
ans = self.conn.get('SELECT id, value FROM %s'%table)
return ans
def rename_custom_item(self, old_id, new_name, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table,lt = self.custom_table_names(data['num'])
# check if item exists
new_id = self.conn.get(
'SELECT id FROM %s WHERE value=?'%table, (new_name,), all=False)
if new_id is None or old_id == new_id:
self.conn.execute('UPDATE %s SET value=? WHERE id=?'%table, (new_name, old_id))
new_id = old_id
else:
# New id exists. If the column is_multiple, then process like
# tags, otherwise process like publishers (see database2)
if data['is_multiple']:
books = self.conn.get('''SELECT book from %s
WHERE value=?'''%lt, (old_id,))
for (book_id,) in books:
self.conn.execute('''DELETE FROM %s
WHERE book=? and value=?'''%lt, (book_id, new_id))
self.conn.execute('''UPDATE %s SET value=?
WHERE value=?'''%lt, (new_id, old_id,))
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (old_id,))
self.dirty_books_referencing('#'+data['label'], new_id, commit=False)
self.conn.commit()
def delete_custom_item_using_id(self, id, label=None, num=None):
if id:
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table,lt = self.custom_table_names(data['num'])
self.dirty_books_referencing('#'+data['label'], id, commit=False)
self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id,))
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id,))
self.conn.commit()
def is_item_used_in_multiple(self, item, label=None, num=None):
existing_tags = self.all_custom(label=label, num=num)
return item.lower() in {t.lower() for t in existing_tags}
def delete_item_from_multiple(self, item, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if data['datatype'] != 'text' or not data['is_multiple']:
raise ValueError('Column %r is not text/multiple'%data['label'])
existing_tags = list(self.all_custom(label=label, num=num))
lt = [t.lower() for t in existing_tags]
try:
idx = lt.index(item.lower())
except ValueError:
idx = -1
books_affected = []
if idx > -1:
table, lt = self.custom_table_names(data['num'])
id_ = self.conn.get('SELECT id FROM %s WHERE value = ?'%table,
(existing_tags[idx],), all=False)
if id_:
books = self.conn.get('SELECT book FROM %s WHERE value = ?'%lt, (id_,))
if books:
books_affected = [b[0] for b in books]
self.conn.execute('DELETE FROM %s WHERE value=?'%lt, (id_,))
self.conn.execute('DELETE FROM %s WHERE id=?'%table, (id_,))
self.conn.commit()
return books_affected
# end convenience methods
def get_next_cc_series_num_for(self, series, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if data['datatype'] != 'series':
return None
table, lt = self.custom_table_names(data['num'])
# get the id of the row containing the series string
series_id = self.conn.get('SELECT id from %s WHERE value=?'%table,
(series,), all=False)
if series_id is None:
if isinstance(tweaks['series_index_auto_increment'], numbers.Number):
return float(tweaks['series_index_auto_increment'])
return 1.0
series_indices = self.conn.get('''
SELECT {lt}.extra FROM {lt}
WHERE {lt}.book IN (SELECT book FROM {lt} where value=?)
ORDER BY {lt}.extra
'''.format(lt=lt), (series_id,))
return self._get_next_series_num_for_list(series_indices)
def all_custom(self, label=None, num=None):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
table, lt = self.custom_table_names(data['num'])
if data['normalized']:
ans = self.conn.get('SELECT value FROM %s'%table)
else:
ans = self.conn.get('SELECT DISTINCT value FROM %s'%table)
ans = {x[0] for x in ans}
return ans
def delete_custom_column(self, label=None, num=None):
data = None
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if data is None:
raise ValueError('No such column')
self.conn.execute(
'UPDATE custom_columns SET mark_for_delete=1 WHERE id=?',
(data['num'],))
self.conn.commit()
def set_custom_column_metadata(self, num, name=None, label=None,
is_editable=None, display=None, notify=True, update_last_modified=False):
changed = False
if name is not None:
self.conn.execute('UPDATE custom_columns SET name=? WHERE id=?',
(name, num))
changed = True
if label is not None:
self.conn.execute('UPDATE custom_columns SET label=? WHERE id=?',
(label, num))
changed = True
if is_editable is not None:
self.conn.execute('UPDATE custom_columns SET editable=? WHERE id=?',
(bool(is_editable), num))
self.custom_column_num_map[num]['is_editable'] = bool(is_editable)
changed = True
if display is not None:
self.conn.execute('UPDATE custom_columns SET display=? WHERE id=?',
(json.dumps(display), num))
changed = True
if changed:
self.conn.commit()
if notify:
self.notify('metadata', [])
return changed
def set_custom_bulk_multiple(self, ids, add=[], remove=[],
label=None, num=None, notify=False):
'''
Fast algorithm for updating custom column is_multiple datatypes.
Do not use with other custom column datatypes.
'''
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if not data['editable']:
raise ValueError('Column %r is not editable'%data['label'])
if data['datatype'] != 'text' or not data['is_multiple']:
raise ValueError('Column %r is not text/multiple'%data['label'])
add = self.cleanup_tags(add)
remove = self.cleanup_tags(remove)
remove = set(remove) - set(add)
if not ids or (not add and not remove):
return
# get custom table names
cust_table, link_table = self.custom_table_names(data['num'])
# Add tags that do not already exist into the custom cust_table
all_tags = self.all_custom(num=data['num'])
lt = [t.lower() for t in all_tags]
new_tags = [t for t in add if t.lower() not in lt]
if new_tags:
self.conn.executemany('INSERT INTO %s(value) VALUES (?)'%cust_table,
[(x,) for x in new_tags])
# Create the temporary temp_tables to store the ids for books and tags
# to be operated on
temp_tables = ('temp_bulk_tag_edit_books', 'temp_bulk_tag_edit_add',
'temp_bulk_tag_edit_remove')
drops = '\n'.join(['DROP TABLE IF EXISTS %s;'%t for t in temp_tables])
creates = '\n'.join(['CREATE TEMP TABLE %s(id INTEGER PRIMARY KEY);'%t
for t in temp_tables])
self.conn.executescript(drops + creates)
# Populate the books temp cust_table
self.conn.executemany(
'INSERT INTO temp_bulk_tag_edit_books VALUES (?)', [(x,) for x in ids])
# Populate the add/remove tags temp temp_tables
for table, tags in enumerate([add, remove]):
if not tags:
continue
table = temp_tables[table+1]
insert = ('INSERT INTO {tt}(id) SELECT {ct}.id FROM {ct} WHERE value=?'
' COLLATE PYNOCASE LIMIT 1').format(tt=table, ct=cust_table)
self.conn.executemany(insert, [(x,) for x in tags])
# now do the real work -- removing and adding the tags
if remove:
self.conn.execute(
'''DELETE FROM %s WHERE
book IN (SELECT id FROM %s) AND
value IN (SELECT id FROM %s)'''
% (link_table, temp_tables[0], temp_tables[2]))
if add:
self.conn.execute(
'''
INSERT OR REPLACE INTO {0}(book, value) SELECT {1}.id, {2}.id FROM {1}, {2}
'''.format(link_table, temp_tables[0], temp_tables[1])
)
# get rid of the temp tables
self.conn.executescript(drops)
self.dirtied(ids, commit=False)
self.conn.commit()
# set the in-memory copies of the tags
for x in ids:
tags = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
(x,), all=False)
self.data.set(x, self.FIELD_MAP[data['num']], tags, row_is_id=True)
if notify:
self.notify('metadata', ids)
def set_custom_bulk(self, ids, val, label=None, num=None,
append=False, notify=True, extras=None):
'''
Change the value of a column for a set of books. The ids parameter is a
list of book ids to change. The extra field must be None or a list the
same length as ids.
'''
if extras is not None and len(extras) != len(ids):
raise ValueError('Length of ids and extras is not the same')
ev = None
for idx,id in enumerate(ids):
if extras is not None:
ev = extras[idx]
self._set_custom(id, val, label=label, num=num, append=append,
notify=notify, extra=ev)
self.dirtied(ids, commit=False)
self.conn.commit()
def set_custom(self, id, val, label=None, num=None, append=False,
notify=True, extra=None, commit=True, allow_case_change=False):
rv = self._set_custom(id, val, label=label, num=num, append=append,
notify=notify, extra=extra,
allow_case_change=allow_case_change)
self.dirtied({id}|rv, commit=False)
if commit:
self.conn.commit()
return rv
def _set_custom(self, id_, val, label=None, num=None, append=False,
notify=True, extra=None, allow_case_change=False):
if label is not None:
data = self.custom_column_label_map[label]
if num is not None:
data = self.custom_column_num_map[num]
if data['datatype'] == 'composite':
return set()
if not data['editable']:
raise ValueError('Column %r is not editable'%data['label'])
table, lt = self.custom_table_names(data['num'])
getter = partial(self.get_custom, id_, num=data['num'],
index_is_id=True)
val = self.custom_data_adapters[data['datatype']](val, data)
if data['datatype'] == 'series' and extra is None:
(val, extra) = self._get_series_values(val)
if extra is None:
extra = 1.0
books_to_refresh = set()
if data['normalized']:
if data['datatype'] == 'enumeration' and (
val and val not in data['display']['enum_values']):
return books_to_refresh
if not append or not data['is_multiple']:
self.conn.execute('DELETE FROM %s WHERE book=?'%lt, (id_,))
self.conn.execute(
'''DELETE FROM {} WHERE (SELECT COUNT(id) FROM {} WHERE
value={}.id) < 1'''.format(table, lt, table))
self.data._data[id_][self.FIELD_MAP[data['num']]] = None
set_val = val if data['is_multiple'] else [val]
existing = getter()
if not existing:
existing = set()
else:
existing = set(existing)
# preserve the order in set_val
for x in [v for v in set_val if v not in existing]:
# normalized types are text and ratings, so we can do this check
# to see if we need to re-add the value
if not x:
continue
case_change = False
existing = list(self.all_custom(num=data['num']))
lx = [t.lower() if hasattr(t, 'lower') else t for t in existing]
try:
idx = lx.index(x.lower() if hasattr(x, 'lower') else x)
except ValueError:
idx = -1
if idx > -1:
ex = existing[idx]
xid = self.conn.get(
'SELECT id FROM %s WHERE value=?'%table, (ex,), all=False)
if allow_case_change and ex != x:
case_change = True
self.conn.execute(
'UPDATE %s SET value=? WHERE id=?'%table, (x, xid))
else:
xid = self.conn.execute(
'INSERT INTO %s(value) VALUES(?)'%table, (x,)).lastrowid
if not self.conn.get(
'SELECT book FROM %s WHERE book=? AND value=?'%lt, (id_, xid), all=False):
if data['datatype'] == 'series':
self.conn.execute(
'''INSERT INTO %s(book, value, extra)
VALUES (?,?,?)'''%lt, (id_, xid, extra))
self.data.set(id_, self.FIELD_MAP[data['num']]+1,
extra, row_is_id=True)
else:
self.conn.execute(
'''INSERT INTO %s(book, value)
VALUES (?,?)'''%lt, (id_, xid))
if case_change:
bks = self.conn.get('SELECT book FROM %s WHERE value=?'%lt,
(xid,))
books_to_refresh |= {bk[0] for bk in bks}
nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'],
(id_,), all=False)
self.data.set(id_, self.FIELD_MAP[data['num']], nval,
row_is_id=True)
else:
self.conn.execute('DELETE FROM %s WHERE book=?'%table, (id_,))
if val is not None:
self.conn.execute(
'INSERT INTO %s(book,value) VALUES (?,?)'%table, (id_, val))
nval = self.conn.get(
'SELECT custom_%s FROM meta2 WHERE id=?'%data['num'], (id_,), all=False)
self.data.set(id_, self.FIELD_MAP[data['num']], nval,
row_is_id=True)
if notify:
self.notify('metadata', [id_])
return books_to_refresh
def clean_custom(self):
st = ('DELETE FROM {table} WHERE (SELECT COUNT(id) FROM {lt} WHERE'
' {lt}.value={table}.id) < 1;')
statements = []
for data in self.custom_column_num_map.values():
if data['normalized']:
table, lt = self.custom_table_names(data['num'])
statements.append(st.format(lt=lt, table=table))
if statements:
self.conn.executescript(' \n'.join(statements))
self.conn.commit()
def custom_columns_in_meta(self):
lines = {}
for data in self.custom_column_label_map.values():
table, lt = self.custom_table_names(data['num'])
if data['normalized']:
query = '%s.value'
if data['is_multiple']:
# query = 'group_concat(%s.value, "{0}")'.format(
# data['multiple_seps']['cache_to_list'])
# if not display.get('sort_alpha', False):
if data['multiple_seps']['cache_to_list'] == '|':
query = 'sortconcat_bar(link.id, %s.value)'
elif data['multiple_seps']['cache_to_list'] == '&':
query = 'sortconcat_amper(link.id, %s.value)'
else:
prints('WARNING: unknown value in multiple_seps',
data['multiple_seps']['cache_to_list'])
query = 'sortconcat_bar(link.id, %s.value)'
line = '''(SELECT {query} FROM {lt} AS link INNER JOIN
{table} ON(link.value={table}.id) WHERE link.book=books.id)
custom_{num}
'''.format(query=query%table, lt=lt, table=table, num=data['num'])
if data['datatype'] == 'series':
line += ''',(SELECT extra FROM {lt} WHERE {lt}.book=books.id)
custom_index_{num}'''.format(lt=lt, num=data['num'])
else:
line = '''
(SELECT value FROM {table} WHERE book=books.id) custom_{num}
'''.format(table=table, num=data['num'])
lines[data['num']] = line
return lines
def create_custom_column(self, label, name, datatype, is_multiple,
editable=True, display={}):
if not label:
raise ValueError(_('No label was provided'))
if re.match('^\\w*$', label) is None or not label[0].isalpha() or label.lower() != label:
raise ValueError(_('The label must contain only lower case letters, digits and underscores, and start with a letter'))
if datatype not in self.CUSTOM_DATA_TYPES:
raise ValueError('%r is not a supported data type'%datatype)
normalized = datatype not in ('datetime', 'comments', 'int', 'bool',
'float', 'composite')
is_multiple = is_multiple and datatype in ('text', 'composite')
num = self.conn.execute(
('INSERT INTO '
'custom_columns(label,name,datatype,is_multiple,editable,display,normalized)'
'VALUES (?,?,?,?,?,?,?)'),
(label, name, datatype, is_multiple, editable,
json.dumps(display), normalized)).lastrowid
if datatype in ('rating', 'int'):
dt = 'INT'
elif datatype in ('text', 'comments', 'series', 'composite', 'enumeration'):
dt = 'TEXT'
elif datatype in ('float',):
dt = 'REAL'
elif datatype == 'datetime':
dt = 'timestamp'
elif datatype == 'bool':
dt = 'BOOL'
collate = 'COLLATE NOCASE' if dt == 'TEXT' else ''
table, lt = self.custom_table_names(num)
if normalized:
if datatype == 'series':
s_index = 'extra REAL,'
else:
s_index = ''
lines = [
'''\
CREATE TABLE %s(
id INTEGER PRIMARY KEY AUTOINCREMENT,
value %s NOT NULL %s,
UNIQUE(value));
'''%(table, dt, collate),
'CREATE INDEX %s_idx ON %s (value %s);'%(table, table, collate),
'''\
CREATE TABLE %s(
id INTEGER PRIMARY KEY AUTOINCREMENT,
book INTEGER NOT NULL,
value INTEGER NOT NULL,
%s
UNIQUE(book, value)
);'''%(lt, s_index),
'CREATE INDEX %s_aidx ON %s (value);'%(lt,lt),
'CREATE INDEX %s_bidx ON %s (book);'%(lt,lt),
'''\
CREATE TRIGGER fkc_update_{lt}_a
BEFORE UPDATE OF book ON {lt}
BEGIN
SELECT CASE
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
END;
END;
CREATE TRIGGER fkc_update_{lt}_b
BEFORE UPDATE OF author ON {lt}
BEGIN
SELECT CASE
WHEN (SELECT id from {table} WHERE id=NEW.value) IS NULL
THEN RAISE(ABORT, 'Foreign key violation: value not in {table}')
END;
END;
CREATE TRIGGER fkc_insert_{lt}
BEFORE INSERT ON {lt}
BEGIN
SELECT CASE
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
WHEN (SELECT id from {table} WHERE id=NEW.value) IS NULL
THEN RAISE(ABORT, 'Foreign key violation: value not in {table}')
END;
END;
CREATE TRIGGER fkc_delete_{lt}
AFTER DELETE ON {table}
BEGIN
DELETE FROM {lt} WHERE value=OLD.id;
END;
CREATE VIEW tag_browser_{table} AS SELECT
id,
value,
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link as bl,
ratings as r
WHERE {lt}.value={table}.id and bl.book={lt}.book and
r.id = bl.rating and r.rating <> 0) avg_rating,
value AS sort
FROM {table};
CREATE VIEW tag_browser_filtered_{table} AS SELECT
id,
value,
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
books_list_filter(book)) count,
(SELECT AVG(r.rating)
FROM {lt},
books_ratings_link as bl,
ratings as r
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
r.id = bl.rating AND r.rating <> 0 AND
books_list_filter(bl.book)) avg_rating,
value AS sort
FROM {table};
'''.format(lt=lt, table=table),
]
else:
lines = [
'''\
CREATE TABLE %s(
id INTEGER PRIMARY KEY AUTOINCREMENT,
book INTEGER,
value %s NOT NULL %s,
UNIQUE(book));
'''%(table, dt, collate),
'CREATE INDEX %s_idx ON %s (book);'%(table, table),
'''\
CREATE TRIGGER fkc_insert_{table}
BEFORE INSERT ON {table}
BEGIN
SELECT CASE
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
END;
END;
CREATE TRIGGER fkc_update_{table}
BEFORE UPDATE OF book ON {table}
BEGIN
SELECT CASE
WHEN (SELECT id from books WHERE id=NEW.book) IS NULL
THEN RAISE(ABORT, 'Foreign key violation: book not in books')
END;
END;
'''.format(table=table),
]
script = ' \n'.join(lines)
self.conn.executescript(script)
self.conn.commit()
return num