Change is_multiple to a dict containing the split/join characters to use in various situations.

This commit is contained in:
Charles Haley 2011-06-04 09:24:10 +01:00
parent 3fc513ec2a
commit 6de35843fb
14 changed files with 151 additions and 86 deletions

View File

@ -204,7 +204,8 @@ class CollectionsBookList(BookList):
elif fm['datatype'] == 'text' and fm['is_multiple']:
val = orig_val
elif fm['datatype'] == 'composite' and fm['is_multiple']:
val = [v.strip() for v in val.split(fm['is_multiple'])]
val = [v.strip() for v in
val.split(fm['is_multiple']['ui_to_list'])]
else:
val = [val]

View File

@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
lxml based OPF parser.
'''
import re, sys, unittest, functools, os, uuid, glob, cStringIO, json
import re, sys, unittest, functools, os, uuid, glob, cStringIO, json, copy
from urllib import unquote
from urlparse import urlparse
@ -457,6 +457,14 @@ def serialize_user_metadata(metadata_elem, all_user_metadata, tail='\n'+(' '*8))
for name, fm in all_user_metadata.items():
try:
if fm.get('is_multiple'):
# migrate is_multiple back to a character
fm = copy.copy(fm)
dt = fm.get('datatype', None)
if dt == 'composite':
fm['is_multiple'] = ','
else:
fm['is_multiple'] = '|'
fm = object_to_unicode(fm)
fm = json.dumps(fm, default=to_json, ensure_ascii=False)
except:
@ -585,6 +593,17 @@ class OPF(object): # {{{
fm = elem.get('content')
try:
fm = json.loads(fm, object_hook=from_json)
im = fm.get('is_multiple', None)
if im and not isinstance(im, dict):
# Must migrate the is_multiple from char to dict
dt = fm.get('datatype', None)
if dt == 'composite':
im = {'cache_to_list': ',', 'ui_to_list': ',', 'list_to_ui': ', '}
elif fm.get('display', {}).get('is_names', False):
im = {'cache_to_list': '|', 'ui_to_list': '&', 'list_to_ui': ', '}
else:
im = {'cache_to_list': '|', 'ui_to_list': ',', 'list_to_ui': ', '}
fm['is_multiple'] = im
temp.set_user_metadata(name, fm)
except:
prints('Failed to read user metadata:', name)

View File

@ -226,16 +226,14 @@ class Comments(Base):
class Text(Base):
def setup_ui(self, parent):
if self.col_metadata['display'].get('is_names', False):
self.sep = u' & '
else:
self.sep = u', '
self.sep = self.col_metadata['multiple_seps']
values = self.all_values = list(self.db.all_custom(num=self.col_id))
values.sort(key=sort_key)
if self.col_metadata['is_multiple']:
w = MultiCompleteLineEdit(parent)
w.set_separator(self.sep.strip())
if self.sep == u' & ':
w.set_separator(self.sep['ui_to_list'])
if self.sep['ui_to_list'] == '&':
w.set_space_before_sep(True)
w.set_add_separator(tweaks['authors_completer_append_separator'])
w.update_items_cache(values)
@ -269,12 +267,12 @@ class Text(Base):
if self.col_metadata['is_multiple']:
if not val:
val = []
self.widgets[1].setText(self.sep.join(val))
self.widgets[1].setText(self.sep['list_to_ui'].join(val))
def getter(self):
if self.col_metadata['is_multiple']:
val = unicode(self.widgets[1].text()).strip()
ans = [x.strip() for x in val.split(self.sep.strip()) if x.strip()]
ans = [x.strip() for x in val.split(self.sep['ui_to_list']) if x.strip()]
if not ans:
ans = None
return ans
@ -899,9 +897,10 @@ class BulkText(BulkBase):
if not self.a_c_checkbox.isChecked():
return
if self.col_metadata['is_multiple']:
ism = self.col_metadata['multiple_seps']
if self.col_metadata['display'].get('is_names', False):
val = self.gui_val
add = [v.strip() for v in val.split('&') if v.strip()]
add = [v.strip() for v in val.split(ism['ui_to_list']) if v.strip()]
self.db.set_custom_bulk(book_ids, add, num=self.col_id)
else:
remove_all, adding, rtext = self.gui_val
@ -911,10 +910,10 @@ class BulkText(BulkBase):
else:
txt = rtext
if txt:
remove = set([v.strip() for v in txt.split(',')])
remove = set([v.strip() for v in txt.split(ism['ui_to_list'])])
txt = adding
if txt:
add = set([v.strip() for v in txt.split(',')])
add = set([v.strip() for v in txt.split(ism['ui_to_list'])])
else:
add = set()
self.db.set_custom_bulk_multiple(book_ids, add=add,

View File

@ -520,7 +520,7 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
elif not fm['is_multiple']:
val = [val]
elif fm['datatype'] == 'composite':
val = [v.strip() for v in val.split(fm['is_multiple'])]
val = [v.strip() for v in val.split(fm['is_multiple']['ui_to_list'])]
elif field == 'authors':
val = [v.replace('|', ',') for v in val]
else:
@ -655,14 +655,14 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog):
if self.destination_field_fm['is_multiple']:
if self.comma_separated.isChecked():
if dest == 'authors' or \
(self.destination_field_fm['is_custom'] and
self.destination_field_fm['datatype'] == 'text' and
self.destination_field_fm['display'].get('is_names', False)):
splitter = ' & '
else:
splitter = ','
splitter = self.destination_field_fm['is_multiple']['ui_to_list']
# if dest == 'authors' or \
# (self.destination_field_fm['is_custom'] and
# self.destination_field_fm['datatype'] == 'text' and
# self.destination_field_fm['display'].get('is_names', False)):
# splitter = ' & '
# else:
# splitter = ','
res = []
for v in val:
for x in v.split(splitter):

View File

@ -608,10 +608,11 @@ class BooksModel(QAbstractTableModel): # {{{
def text_type(r, mult=None, idx=-1):
text = self.db.data[r][idx]
if text and mult is not None:
if mult:
return QVariant(u' & '.join(text.split('|')))
return QVariant(u', '.join(sorted(text.split('|'),key=sort_key)))
if text and mult:
jv = mult['list_to_ui']
sv = mult['cache_to_list']
return QVariant(jv.join(
sorted([t.strip() for t in text.split(sv)], key=sort_key)))
return QVariant(text)
def decorated_text_type(r, idx=-1):
@ -665,8 +666,6 @@ class BooksModel(QAbstractTableModel): # {{{
datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments', 'composite', 'enumeration'):
mult=self.custom_columns[col]['is_multiple']
if mult is not None:
mult = self.custom_columns[col]['display'].get('is_names', False)
self.dc[col] = functools.partial(text_type, idx=idx, mult=mult)
if datatype in ['text', 'composite', 'enumeration'] and not mult:
if self.custom_columns[col]['display'].get('use_decorations', False):
@ -722,9 +721,9 @@ class BooksModel(QAbstractTableModel): # {{{
if id_ in self.color_cache:
if key in self.color_cache[id_]:
return self.color_cache[id_][key]
if mi is None:
mi = self.db.get_metadata(id_, index_is_id=True)
try:
if mi is None:
mi = self.db.get_metadata(id_, index_is_id=True)
color = composite_formatter.safe_format(fmt, mi, '', mi)
if color in self.colors:
color = QColor(color)

View File

@ -205,7 +205,7 @@ class ConditionEditor(QWidget): # {{{
tt = _('Enter a regular expression')
elif m.get('is_multiple', False):
tt += '\n' + _('You can match multiple values by separating'
' them with %s')%m['is_multiple']
' them with %s')%m['is_multiple']['ui_to_list']
self.value_box.setToolTip(tt)
if action in ('is set', 'is not set', 'is true', 'is false',
'is undefined'):

View File

@ -13,6 +13,9 @@ from calibre.gui2 import error_dialog
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
# Note: in this class, we are treating is_multiple as the boolean that
# custom_columns expects to find in its structure. It does not use the dict
column_types = {
0:{'datatype':'text',
'text':_('Text, column shown in the tag browser'),

View File

@ -509,7 +509,7 @@ class ResultCache(SearchQueryParser): # {{{
valq_mkind, valq = self._matchkind(query)
loc = self.field_metadata[location]['rec_index']
split_char = self.field_metadata[location]['is_multiple']
split_char = self.field_metadata[location]['is_multiple']['cache_to_list']
for id_ in candidates:
item = self._data[id_]
if item is None:
@ -665,7 +665,8 @@ class ResultCache(SearchQueryParser): # {{{
if fm['is_multiple'] and \
len(query) > 1 and query.startswith('#') and \
query[1:1] in '=<>!':
vf = lambda item, loc=fm['rec_index'], ms=fm['is_multiple']:\
vf = lambda item, loc=fm['rec_index'], \
ms=fm['is_multiple']['cache_to_list']:\
len(item[loc].split(ms)) if item[loc] is not None else 0
return self.get_numeric_matches(location, query[1:],
candidates, val_func=vf)
@ -703,7 +704,8 @@ class ResultCache(SearchQueryParser): # {{{
['composite', 'text', 'comments', 'series', 'enumeration']:
exclude_fields.append(db_col[x])
col_datatype[db_col[x]] = self.field_metadata[x]['datatype']
is_multiple_cols[db_col[x]] = self.field_metadata[x]['is_multiple']
is_multiple_cols[db_col[x]] = \
self.field_metadata[x]['is_multiple'].get('cache_to_list', None)
try:
rating_query = int(query) * 2
@ -1045,13 +1047,14 @@ class SortKeyGenerator(object):
elif dt in ('text', 'comments', 'composite', 'enumeration'):
if val:
sep = fm['is_multiple']
if sep:
if fm['display'].get('is_names', False):
val = sep.join(
[author_to_author_sort(v) for v in val.split(sep)])
if fm['is_multiple']:
jv = fm['is_multiple']['list_to_ui']
sv = fm['is_multiple']['cache_to_list']
if '&' in jv:
val = jv.join(
[author_to_author_sort(v) for v in val.split(sv)])
else:
val = sep.join(sorted(val.split(sep),
val = jv.join(sorted(val.split(sv),
key=self.string_sort_key))
val = self.string_sort_key(val)

View File

@ -88,7 +88,7 @@ class Rule(object): # {{{
if dt in ('comments', 'series', 'text', 'enumeration', 'composite'):
ism = m.get('is_multiple', False)
if ism:
return self.multiple_condition(col, action, val, ',' if ism == '|' else ism)
return self.multiple_condition(col, action, val, ism['ui_to_list'])
return self.text_condition(col, action, val)
def identifiers_condition(self, col, action, val):

View File

@ -78,6 +78,18 @@ class CustomColumns(object):
}
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):
@ -119,7 +131,7 @@ class CustomColumns(object):
if x is None:
return []
if isinstance(x, (str, unicode, bytes)):
x = x.split('&' if d['display'].get('is_names', False) else',')
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,
unicode) else y for y in x]
@ -181,10 +193,7 @@ class CustomColumns(object):
is_category = True
else:
is_category = False
if v['is_multiple']:
is_m = ',' if v['datatype'] == 'composite' else '|'
else:
is_m = None
is_m = v['multiple_seps']
tn = 'custom_column_{0}'.format(v['num'])
self.field_metadata.add_custom_field(label=v['label'],
table=tn, column='value', datatype=v['datatype'],
@ -200,7 +209,7 @@ class CustomColumns(object):
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('|') if ans else []
ans = ans.split(data['multiple_seps']['cache_to_list']) if ans else []
if data['display'].get('sort_alpha', False):
ans.sort(cmp=lambda x,y:cmp(x.lower(), y.lower()))
return ans
@ -571,9 +580,17 @@ class CustomColumns(object):
if data['normalized']:
query = '%s.value'
if data['is_multiple']:
query = 'group_concat(%s.value, "|")'
if not display.get('sort_alpha', False):
query = 'sort_concat(link.id, %s.value)'
# 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}

View File

@ -1250,7 +1250,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
dex = field['rec_index']
for book in self.data.iterall():
if field['is_multiple']:
vals = [v.strip() for v in book[dex].split(field['is_multiple'])
vals = [v.strip() for v in
book[dex].split(field['is_multiple']['cache_to_list'])
if v.strip()]
if id_ in vals:
ans.add(book[0])
@ -1378,7 +1379,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tcategories[category] = {}
# create a list of category/field_index for the books scan to use.
# This saves iterating through field_metadata for each book
md.append((category, cat['rec_index'], cat['is_multiple'], False))
md.append((category, cat['rec_index'],
cat['is_multiple'].get('cache_to_list', None), False))
for category in tb_cats.iterkeys():
cat = tb_cats[category]
@ -1386,7 +1388,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
cat['display'].get('make_category', False):
tids[category] = {}
tcategories[category] = {}
md.append((category, cat['rec_index'], cat['is_multiple'],
md.append((category, cat['rec_index'],
cat['is_multiple'].get('cache_to_list', None),
cat['datatype'] == 'composite'))
#print 'end phase "collection":', time.clock() - last, 'seconds'
#last = time.clock()

View File

@ -50,9 +50,16 @@ class FieldMetadata(dict):
datatype: the type of information in the field. Valid values are listed in
VALID_DATA_TYPES below.
is_multiple: valid for the text datatype. If None, the field is to be
treated as a single term. If not None, it contains a string, and the field
is assumed to contain a list of terms separated by that string
is_multiple: valid for the text datatype. If {}, the field is to be
treated as a single term. If not None, it contains a dict of the form
{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '}
where the cache_to_list contains the character used to split the value in
the meta2 table, ui_to_list contains the character used to create a list
from a value shown in the ui (each resulting value must be strip()ed and
empty values removed), and list_to_ui contains the string used in join()
to create a displayable string from the list.
kind == field: is a db field.
kind == category: standard tag category that isn't a field. see news.
@ -97,7 +104,9 @@ class FieldMetadata(dict):
'link_column':'author',
'category_sort':'sort',
'datatype':'text',
'is_multiple':',',
'is_multiple':{'cache_to_list': ',',
'ui_to_list': '&',
'list_to_ui': ' & '},
'kind':'field',
'name':_('Authors'),
'search_terms':['authors', 'author'],
@ -109,7 +118,7 @@ class FieldMetadata(dict):
'link_column':'series',
'category_sort':'(title_sort(name))',
'datatype':'series',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Series'),
'search_terms':['series'],
@ -119,7 +128,9 @@ class FieldMetadata(dict):
('formats', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':',',
'is_multiple':{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '},
'kind':'field',
'name':_('Formats'),
'search_terms':['formats', 'format'],
@ -131,7 +142,7 @@ class FieldMetadata(dict):
'link_column':'publisher',
'category_sort':'name',
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Publishers'),
'search_terms':['publisher'],
@ -143,7 +154,7 @@ class FieldMetadata(dict):
'link_column':'rating',
'category_sort':'rating',
'datatype':'rating',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Ratings'),
'search_terms':['rating'],
@ -154,7 +165,7 @@ class FieldMetadata(dict):
'column':'name',
'category_sort':'name',
'datatype':None,
'is_multiple':None,
'is_multiple':{},
'kind':'category',
'name':_('News'),
'search_terms':[],
@ -166,7 +177,9 @@ class FieldMetadata(dict):
'link_column': 'tag',
'category_sort':'name',
'datatype':'text',
'is_multiple':',',
'is_multiple':{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '},
'kind':'field',
'name':_('Tags'),
'search_terms':['tags', 'tag'],
@ -176,7 +189,9 @@ class FieldMetadata(dict):
('identifiers', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':',',
'is_multiple':{'cache_to_list': ',',
'ui_to_list': ',',
'list_to_ui': ', '},
'kind':'field',
'name':_('Identifiers'),
'search_terms':['identifiers', 'identifier', 'isbn'],
@ -186,7 +201,7 @@ class FieldMetadata(dict):
('author_sort',{'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Author Sort'),
'search_terms':['author_sort'],
@ -196,7 +211,9 @@ class FieldMetadata(dict):
('au_map', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':',',
'is_multiple':{'cache_to_list': ',',
'ui_to_list': None,
'list_to_ui': None},
'kind':'field',
'name':None,
'search_terms':[],
@ -206,7 +223,7 @@ class FieldMetadata(dict):
('comments', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Comments'),
'search_terms':['comments', 'comment'],
@ -216,7 +233,7 @@ class FieldMetadata(dict):
('cover', {'table':None,
'column':None,
'datatype':'int',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':['cover'],
@ -226,7 +243,7 @@ class FieldMetadata(dict):
('id', {'table':None,
'column':None,
'datatype':'int',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':[],
@ -236,7 +253,7 @@ class FieldMetadata(dict):
('last_modified', {'table':None,
'column':None,
'datatype':'datetime',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Modified'),
'search_terms':['last_modified'],
@ -246,7 +263,7 @@ class FieldMetadata(dict):
('ondevice', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('On Device'),
'search_terms':['ondevice'],
@ -256,7 +273,7 @@ class FieldMetadata(dict):
('path', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Path'),
'search_terms':[],
@ -266,7 +283,7 @@ class FieldMetadata(dict):
('pubdate', {'table':None,
'column':None,
'datatype':'datetime',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Published'),
'search_terms':['pubdate'],
@ -276,7 +293,7 @@ class FieldMetadata(dict):
('marked', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name': None,
'search_terms':['marked'],
@ -286,7 +303,7 @@ class FieldMetadata(dict):
('series_index',{'table':None,
'column':None,
'datatype':'float',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':['series_index'],
@ -296,7 +313,7 @@ class FieldMetadata(dict):
('sort', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Title Sort'),
'search_terms':['title_sort'],
@ -306,7 +323,7 @@ class FieldMetadata(dict):
('size', {'table':None,
'column':None,
'datatype':'float',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Size'),
'search_terms':['size'],
@ -316,7 +333,7 @@ class FieldMetadata(dict):
('timestamp', {'table':None,
'column':None,
'datatype':'datetime',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Date'),
'search_terms':['date'],
@ -326,7 +343,7 @@ class FieldMetadata(dict):
('title', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':_('Title'),
'search_terms':['title'],
@ -336,7 +353,7 @@ class FieldMetadata(dict):
('uuid', {'table':None,
'column':None,
'datatype':'text',
'is_multiple':None,
'is_multiple':{},
'kind':'field',
'name':None,
'search_terms':[],
@ -508,7 +525,7 @@ class FieldMetadata(dict):
if datatype == 'series':
key += '_index'
self._tb_cats[key] = {'table':None, 'column':None,
'datatype':'float', 'is_multiple':None,
'datatype':'float', 'is_multiple':{},
'kind':'field', 'name':'',
'search_terms':[key], 'label':label+'_index',
'colnum':None, 'display':{},
@ -560,7 +577,7 @@ class FieldMetadata(dict):
if icu_lower(label) != label:
st.append(icu_lower(label))
self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':None,
'datatype':None, 'is_multiple':{},
'kind':'user', 'name':name,
'search_terms':st, 'is_custom':False,
'is_category':True, 'is_csp': False}
@ -570,7 +587,7 @@ class FieldMetadata(dict):
if label in self._tb_cats:
raise ValueError('Duplicate user field [%s]'%(label))
self._tb_cats[label] = {'table':None, 'column':None,
'datatype':None, 'is_multiple':None,
'datatype':None, 'is_multiple':{},
'kind':'search', 'name':name,
'search_terms':[], 'is_custom':False,
'is_category':True, 'is_csp': False}

View File

@ -171,7 +171,7 @@ class Restore(Thread):
for x in fields:
if x in cfm:
if x == 'is_multiple':
args.append(cfm[x] is not None)
args.append(bool(cfm[x]))
else:
args.append(cfm[x])
if len(args) == len(fields):

View File

@ -121,9 +121,12 @@ class SortedConcatenate(object):
return None
return self.sep.join(map(self.ans.get, sorted(self.ans.keys())))
class SafeSortedConcatenate(SortedConcatenate):
class SortedConcatenateBar(SortedConcatenate):
sep = '|'
class SortedConcatenateAmper(SortedConcatenate):
sep = '&'
class IdentifiersConcat(object):
'''String concatenation aggregator for the identifiers map'''
def __init__(self):
@ -220,7 +223,8 @@ class DBThread(Thread):
self.conn.execute('pragma cache_size=5000')
encoding = self.conn.execute('pragma encoding').fetchone()[0]
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
self.conn.create_aggregate('sortconcat_bar', 2, SortedConcatenateBar)
self.conn.create_aggregate('sortconcat_amper', 2, SortedConcatenateAmper)
self.conn.create_aggregate('identifiers_concat', 2, IdentifiersConcat)
load_c_extensions(self.conn)
self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row)