[IMP] [WIP] order_by: now using aliases in _generate_order_by, _generate_o2m_order_by, _inherits_join_add, ... added some tests. Next comits will clean a bit the code, because currently it is a bit messy.

bzr revid: tde@openerp.com-20121207115424-x8gkjcqpi8dz96g2
This commit is contained in:
Thibault Delavallée 2012-12-07 12:54:24 +01:00
parent f810fb0526
commit e36b44e82f
6 changed files with 253 additions and 68 deletions

View File

@ -432,7 +432,6 @@ class test_expression(common.TransactionCase):
partner_state_id_col._auto_join = False
partner_parent_id_col._auto_join = False
state_country_id_col._auto_join = False
BaseModel._where_calc = self._base_model_where_calc
if __name__ == '__main__':
unittest2.main()

View File

@ -2,16 +2,17 @@ import unittest2
import openerp.tests.common as common
class test_expression(common.TransactionCase):
def test_search_order(self):
class test_search(common.TransactionCase):
def test_00_search_order(self):
registry, cr, uid = self.registry, self.cr, self.uid
# Create 6 partners with a given name, and a given creation order to
# ensure the order of their ID. Some are set as unactive to verify they
# are by default excluded from the searches and to provide a second
# `order` argument.
# Create 6 partners with a given name, and a given creation order to
# ensure the order of their ID. Some are set as unactive to verify they
# are by default excluded from the searches and to provide a second
# `order` argument.
partners = registry('res.partner')
c = partners.create(cr, uid, {'name': 'test_search_order_C'})
@ -23,9 +24,9 @@ class test_expression(common.TransactionCase):
# The tests.
# The basic searches should exclude records that have active = False.
# The order of the returned ids should be given by the `order`
# parameter of search().
# The basic searches should exclude records that have active = False.
# The order of the returned ids should be given by the `order`
# parameter of search().
name_asc = partners.search(cr, uid, [('name', 'like', 'test_search_order%')], order="name asc")
self.assertEqual([a, ab, b, c], name_asc, "Search with 'NAME ASC' order failed.")
@ -36,9 +37,9 @@ class test_expression(common.TransactionCase):
id_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%')], order="id desc")
self.assertEqual([ab, b, a, c], id_desc, "Search with 'ID DESC' order failed.")
# The inactive records shouldn't be excluded as soon as a condition on
# that field is present in the domain. The `order` parameter of
# search() should support any legal coma-separated values.
# The inactive records shouldn't be excluded as soon as a condition on
# that field is present in the domain. The `order` parameter of
# search() should support any legal coma-separated values.
active_asc_id_asc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="active asc, id asc")
self.assertEqual([d, e, c, a, b, ab], active_asc_id_asc, "Search with 'ACTIVE ASC, ID ASC' order failed.")
@ -57,4 +58,28 @@ class test_expression(common.TransactionCase):
id_desc_active_desc = partners.search(cr, uid, [('name', 'like', 'test_search_order%'), '|', ('active', '=', True), ('active', '=', False)], order="id desc, active desc")
self.assertEqual([e, ab, b, a, d, c], id_desc_active_desc, "Search with 'ID DESC, ACTIVE DESC' order failed.")
def test_10_m2order(self):
registry, cr, uid = self.registry, self.cr, self.uid
users_obj = registry('res.users')
# Find Employee group
group_employee_ref = self.registry('ir.model.data').get_object_reference(cr, uid, 'base', 'group_user')
group_employee_id = group_employee_ref and group_employee_ref[1] or False
search_user = users_obj.create(cr, uid, {'name': '__search', 'login': '__search', 'groups_id': [(6, 0, [group_employee_id])]})
a = users_obj.create(cr, uid, {'name': '__test_A', 'login': '__z_test_A'})
b = users_obj.create(cr, uid, {'name': '__test_B', 'login': '__a_test_B'})
# Do: search on res.users, order on a field on res.partner, then res.users
print '\n\n'
user_ids = users_obj.search(cr, search_user, [], order='name asc, login desc')
self.assertTrue(set([search_user, a, b]).issubset(set(user_ids)), 'cacaprout')
# DO: order on a many2one
print '\n\n'
user_ids = users_obj.search(cr, search_user, [], order='state_id asc, country_id desc, name asc, login desc')
print user_ids[0:25]
# self.assertTrue(set([search_user, a, b]).issubset(set(user_ids)), 'cacaprout')
if __name__ == '__main__':
unittest2.main()

View File

@ -319,16 +319,18 @@ def _quote(to_quote):
return to_quote
def generate_table_alias(src_model, joined_tables=[]):
def generate_table_alias(src_table_alias, joined_tables=[]):
""" Generate a standard table alias name. An alias is generated as following:
- the base is the source model table name
- the base is the source table name (that can already be an alias)
- then, each joined table is added in the alias using a 'link field name'
that is used to render unique aliases for a given path
- returns a tuple composed of the alias, and the full table alias to be
added in a from condition
Examples:
- src_model=res.users, join_tables=[]:
alias = '"res_users"'
- src_model=res.users, join_tables=[(res.partner, 'parent_id')]
alias = '"res_partner" as "res_users__parent_id"'
- src_table_alias='res_users', join_tables=[]:
alias = ('res_users','"res_users"')
- src_model='res_users', join_tables=[(res.partner, 'parent_id')]
alias = ('res_users__parent_id', '"res_partner" as "res_users__parent_id"')
:param model src_model: model source of the alias
:param list join_tables: list of tuples
@ -336,12 +338,12 @@ def generate_table_alias(src_model, joined_tables=[]):
:return tuple: (table alias, alias statement for from clause with quotes added)
"""
alias = src_model._table
alias = src_table_alias
if not joined_tables:
return ('%s' % alias, '%s' % _quote(alias))
for link in joined_tables:
alias += '__' + link[1]
return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]._table), _quote(alias)))
return ('%s' % alias, '%s as %s' % (_quote(joined_tables[-1][0]), _quote(alias)))
def normalize_leaf(element):
@ -499,8 +501,8 @@ class ExtendedLeaf(object):
# --------------------------------------------------
def generate_alias(self):
links = [(context[1], context[4]) for context in self.context_stack]
alias, alias_statement = generate_table_alias(self._tables[0], links)
links = [(context[1]._table, context[4]) for context in self.context_stack]
alias, alias_statement = generate_table_alias(self._tables[0]._table, links)
return alias
def add_join_context(self, table, lhs_col, table_col, link):

View File

@ -58,7 +58,6 @@ import types
import psycopg2
from lxml import etree
import warnings
import fields
import openerp
@ -2720,20 +2719,23 @@ class BaseModel(object):
return result
def _inherits_join_add(self, current_table, parent_model_name, query):
def _inherits_join_add(self, current_model, parent_model_name, query):
"""
Add missing table SELECT and JOIN clause to ``query`` for reaching the parent table (no duplicates)
:param current_table: current model object
:param current_model: current model object
:param parent_model_name: name of the parent model for which the clauses should be added
:param query: query object on which the JOIN should be added
"""
inherits_field = current_table._inherits[parent_model_name]
inherits_field = current_model._inherits[parent_model_name]
parent_model = self.pool.get(parent_model_name)
parent_table_name = parent_model._table
quoted_parent_table_name = '"%s"' % parent_table_name
if quoted_parent_table_name not in query.tables:
query.tables.append(quoted_parent_table_name)
query.where_clause.append('(%s.%s = %s.id)' % (current_table._table, inherits_field, parent_table_name))
parent_alias = query.add_implicit_join((current_model, parent_model, inherits_field, 'id', inherits_field))
return parent_alias
# table_alias = expression.generate_table_alias(current_model, [(parent_model, inherits_field)])
# print '\t... _inherits_join_add trying to add %s in %s' % (table_alias, query.tables)
# # query.add_table(table_alias)
# if table_alias not in query.tables:
# query.tables.append(table_alias)
# query.where_clause.append('("%s".%s = %s.id)' % (current_model._table, inherits_field, table_alias))
def _inherits_join_calc(self, field, query):
"""
@ -2744,13 +2746,15 @@ class BaseModel(object):
:param query: query object on which the JOIN should be added
:return: qualified name of field, to be used in SELECT clause
"""
print '\t--> _inherits_join_calc'
current_table = self
parent_alias = current_table._table
while field in current_table._inherit_fields and not field in current_table._columns:
parent_model_name = current_table._inherit_fields[field][0]
parent_table = self.pool.get(parent_model_name)
self._inherits_join_add(current_table, parent_model_name, query)
parent_alias = self._inherits_join_add(current_table, parent_model_name, query)
current_table = parent_table
return '"%s".%s' % (current_table._table, field)
return '%s."%s"' % (parent_alias, field)
def _parent_store_compute(self, cr):
if not self._parent_store:
@ -4659,28 +4663,49 @@ class BaseModel(object):
:param query: the current query object
"""
def apply_rule(added_clause, added_params, added_tables, parent_model=None, child_object=None):
""" :param string parent_model: string of the parent model
:param model child_object: model object, base of the rule application
"""
if added_clause:
print '--> _apply_ir_rules.apply_rule,', added_clause, added_params, added_tables, parent_model, child_object
if parent_model and child_object:
print '\t... calling inherits-Join_add with parent_model %s, child_object %s' % (parent_model, child_object)
# as inherited rules are being applied, we need to add the missing JOIN
# to reach the parent table (if it was not JOINed yet in the query)
child_object._inherits_join_add(child_object, parent_model, query)
parent_alias = child_object._inherits_join_add(child_object, parent_model, query)
# inherited rules are applied on the external table -> need to get the alias and replace
parent_table = self.pool.get(parent_model)._table
print parent_table, parent_alias
added_clause = [clause.replace('"%s"' % parent_table, '"%s"' % parent_alias) for clause in added_clause]
# not sure of myself here (in the ORM, this statment is quite cool)
new_tables = []
for table in added_tables:
if table == '"%s"' % (parent_table):
new_table = '"%s" as "%s"' % (parent_table, parent_alias)
else:
new_table = table.replace('"%s"' % parent_table, '"%s"' % parent_alias)
new_tables.append(new_table)
added_tables = new_tables
print '\t... adding tables %s, clause %s (params %s)' % (added_tables, added_clause, added_params)
query.where_clause += added_clause
query.where_clause_params += added_params
for table in added_tables:
quoted_table_name = '%s' % (table)
if quoted_table_name not in query.tables:
query.tables.append(quoted_table_name)
print '\t... adding table %s in %s' % (table, query.tables)
if table not in query.tables:
query.tables.append(table)
return True
return False
# apply main rules on the object
rule_obj = self.pool.get('ir.rule')
apply_rule(*rule_obj.domain_get(cr, uid, self._name, mode, context=context))
rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, self._name, mode, context=context)
apply_rule(rule_where_clause, rule_where_clause_params, rule_tables)
# apply ir.rules from the parents (through _inherits)
for inherited_model in self._inherits:
kwargs = dict(parent_model=inherited_model, child_object=self) #workaround for python2.5
apply_rule(*rule_obj.domain_get(cr, uid, inherited_model, mode, context=context), **kwargs)
rule_where_clause, rule_where_clause_params, rule_tables = rule_obj.domain_get(cr, uid, inherited_model, mode, context=context)
apply_rule(rule_where_clause, rule_where_clause_params, rule_tables,
parent_model=inherited_model, child_object=self)
def _generate_m2o_order_by(self, order_field, query):
"""
@ -4690,6 +4715,7 @@ class BaseModel(object):
:return: the qualified field name to use in an ORDER BY clause to sort by ``order_field``
"""
print '_generate_m2o_order_by'
if order_field not in self._columns and order_field in self._inherit_fields:
# also add missing joins for reaching the table containing the m2o field
qualified_field = self._inherits_join_calc(order_field, query)
@ -4715,17 +4741,16 @@ class BaseModel(object):
# extract the field names, to be able to qualify them and add desc/asc
m2o_order_list = []
for order_part in m2o_order.split(","):
m2o_order_list.append(order_part.strip().split(" ",1)[0].strip())
m2o_order_list.append(order_part.strip().split(" ", 1)[0].strip())
m2o_order = m2o_order_list
# Join the dest m2o table if it's not joined yet. We use [LEFT] OUTER join here
# as we don't want to exclude results that have NULL values for the m2o
src_table, src_field = qualified_field.replace('"','').split('.', 1)
query.join((src_table, dest_model._table, src_field, 'id'), outer=True)
qualify = lambda field: '"%s"."%s"' % (dest_model._table, field)
src_table, src_field = qualified_field.replace('"', '').split('.', 1)
dst_alias = query.add_join((src_table, dest_model._table, src_field, 'id'), outer=True)
qualify = lambda field: '"%s"."%s"' % (dst_alias, field)
return map(qualify, m2o_order) if isinstance(m2o_order, list) else qualify(m2o_order)
def _generate_order_by(self, order_spec, query):
"""
Attempt to consruct an appropriate ORDER BY clause based on order_spec, which must be
@ -4742,9 +4767,9 @@ class BaseModel(object):
subelems.append(' ')
order_list.append('"%s"."%s" %s' % (table, subelems[0], subelems[1]))
return order_list
order_by_clause = ','.join(_split_order(self._order, self._table))
if order_spec:
print '-->_generate_order_by beginning'
order_by_elements = []
self._check_qorder(order_spec)
for order_part in order_spec.split(','):
@ -4761,7 +4786,7 @@ class BaseModel(object):
elif order_column._type == 'many2one':
inner_clause = self._generate_m2o_order_by(order_field, query)
else:
continue # ignore non-readable or "non-joinable" fields
continue # ignore non-readable or "non-joinable" fields
elif order_field in self._inherit_fields:
parent_obj = self.pool.get(self._inherit_fields[order_field][3])
order_column = parent_obj._columns[order_field]
@ -4770,7 +4795,7 @@ class BaseModel(object):
elif order_column._type == 'many2one':
inner_clause = self._generate_m2o_order_by(order_field, query)
else:
continue # ignore non-readable or "non-joinable" fields
continue # ignore non-readable or "non-joinable" fields
if inner_clause:
if isinstance(inner_clause, list):
for clause in inner_clause:
@ -4779,6 +4804,7 @@ class BaseModel(object):
order_by_elements.append("%s %s" % (inner_clause, order_direction))
if order_by_elements:
order_by_clause = ",".join(order_by_elements)
print '-->_generate_order_by ending'
return order_by_clause and (' ORDER BY %s ' % order_by_clause) or ''

View File

@ -21,12 +21,21 @@
#.apidoc title: Query object
def _quote(to_quote):
if '"' not in to_quote:
return '"%s"' % to_quote
return to_quote
def _get_alias_from_statement(string):
if len(string.split(' as ')) > 1:
alias = string.split(' as ')[1].replace('"', '')
else:
alias = string.replace('"', '')
return alias
class Query(object):
"""
Dumb implementation of a Query object, using 3 string lists so far
@ -44,6 +53,9 @@ class Query(object):
# holds the list of tables joined using default JOIN.
# the table names are stored double-quoted (backwards compatibility)
self.tables = tables or []
# holds a mapping of table aliases:
# self._table_alias_mapping = {'alias_1': 'table_name'}
self._table_alias_mapping = {}
# holds the list of WHERE clause elements, to be joined with
# 'AND' when generating the final query
@ -67,7 +79,110 @@ class Query(object):
# LEFT JOIN "table_c" ON ("table_a"."table_a_col2" = "table_c"."table_c_col")
self.joins = joins or {}
def join(self, connection, outer=False):
def _add_table_alias(self, table_alias):
pass
def _get_table_aliases(self):
aliases = []
for table in self.tables:
if len(table.split(' as ')) > 1:
aliases.append(table.split(' as ')[1].replace('"', ''))
else:
aliases.append(table.replace('"', ''))
# print '--', aliases
return aliases
def _get_alias_mapping(self):
mapping = {}
aliases = self._get_table_aliases()
for alias in aliases:
for table in self.tables:
if '"%s"' % (alias) in table:
mapping.setdefault(alias, table)
return mapping
def add_new_join(self, connection, implicit=True, outer=False):
""" Join a destination table to the current table.
:param implicit: False if the join is an explicit join. This allows
to fall back on the previous implementation of ``join`` before
OpenERP 7.0. It therefore adds the JOIN specified in ``connection``
If True, the join is done implicitely, by adding the table alias
in the from clause and the join condition in the where clause
of the query.
:param connection: a tuple ``(lhs, table, lhs_col, col, link)``.
The join corresponds to the SQL equivalent of::
(lhs.lhs_col = table.col)
Note that all connection elements are strings. Please refer to expression.py for more details about joins.
:param outer: True if a LEFT OUTER JOIN should be used, if possible
(no promotion to OUTER JOIN is supported in case the JOIN
was already present in the query, as for the moment
implicit INNER JOINs are only connected from NON-NULL
columns so it would not be correct (e.g. for
``_inherits`` or when a domain criterion explicitly
adds filtering)
"""
from openerp.osv.expression import generate_table_alias
(lhs, table, lhs_col, col, link) = connection
alias, alias_statement = generate_table_alias(lhs._table, [(table._table, link)])
if implicit:
print '\t\t... Query: trying to add %s in %s (received %s)' % (alias_statement, self.tables, connection)
if alias_statement not in self.tables:
self.tables.append(alias_statement)
condition = '("%s"."%s" = "%s"."%s")' % (lhs._table, lhs_col, alias, col)
print '\t\t... added %s' % (condition)
self.where_clause.append(condition)
return alias
else:
(lhs, table, lhs_col, col) = connection
lhs = _quote(lhs)
table = _quote(table)
print connection
aliases = []
for table in self.tables:
if len(table.split(' as ')) > 1:
aliases.append(table.split(' as ')[1])
else:
aliases.append(table)
print '--', aliases
aliases = [table.split(' as ') for table in self.tables]
assert lhs in self.aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables))
if table in self.tables:
# already joined, must ignore (promotion to outer and multiple joins not supported yet)
pass
else:
# add JOIN
self.tables.append(table)
self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN'))
return self
def add_implicit_join(self, connection):
""" Adds an implicit join. This means that left-hand table is added to the
Query.tables (adding a table in the from clause), and that a join
condition is added in Query.where_clause.
Implicit joins use expression.generate_table_alias to generate the
alias the the joined table.
:param connection: a tuple``(lhs, table, lhs_col, col, link)`` Please
refer to expression.py for more details about joins.
"""
from openerp.osv.expression import generate_table_alias
(lhs, table, lhs_col, col, link) = connection
alias, alias_statement = generate_table_alias(lhs._table, [(table._table, link)])
print '\t\t... Query: trying to add %s in %s (received %s)' % (alias_statement, self.tables, connection)
if alias_statement not in self.tables:
self.tables.append(alias_statement)
condition = '("%s"."%s" = "%s"."%s")' % (lhs._table, lhs_col, alias, col)
print '\t\t... added %s' % (condition)
self.where_clause.append(condition)
return alias
def add_join(self, connection, outer=False):
"""Adds the JOIN specified in ``connection``.
:param connection: a tuple ``(lhs, table, lhs_col, col)``.
@ -75,6 +190,8 @@ class Query(object):
(lhs.lhs_col = table.col)
Note that all connection elements are strings.
:param outer: True if a LEFT OUTER JOIN should be used, if possible
(no promotion to OUTER JOIN is supported in case the JOIN
was already present in the query, as for the moment
@ -83,36 +200,52 @@ class Query(object):
``_inherits`` or when a domain criterion explicitly
adds filtering)
"""
from openerp.osv.expression import generate_table_alias
(lhs, table, lhs_col, col) = connection
lhs = _quote(lhs)
table = _quote(table)
assert lhs in self.tables, "Left-hand-side table must already be part of the query!"
if table in self.tables:
# lhs = _quote(lhs)
# table = _quote(table)
print '\t\t... Query.add_join(): adding connection %s' % str(connection)
aliases = self._get_table_aliases()
assert lhs in aliases, "Left-hand-side table %s must already be part of the query tables %s!" % (lhs, str(self.tables))
rhs, rhs_statement = generate_table_alias(lhs, [(connection[1], connection[2])])
print rhs, rhs_statement
if rhs_statement in self.tables:
# already joined, must ignore (promotion to outer and multiple joins not supported yet)
pass
else:
# add JOIN
self.tables.append(table)
self.joins.setdefault(lhs, []).append((table, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN'))
return self
self.tables.append(rhs_statement)
self.joins.setdefault(lhs, []).append((rhs, lhs_col, col, outer and 'LEFT JOIN' or 'JOIN'))
return rhs
def get_sql(self):
"""Returns (query_from, query_where, query_params)"""
query_from = ''
tables_to_process = list(self.tables)
alias_mapping = self._get_alias_mapping()
# print 'tables_to_process %s' % (tables_to_process)
# print 'self.joins %s' % (self.joins)
# print 'alias_mapping %s' % (alias_mapping)
def add_joins_for_table(table, query_from):
for (dest_table, lhs_col, col, join) in self.joins.get(table, []):
tables_to_process.remove(dest_table)
query_from += ' %s %s ON (%s."%s" = %s."%s")' % \
(join, dest_table, table, lhs_col, dest_table, col)
# print dest_table
tables_to_process.remove(alias_mapping[dest_table])
query_from += ' %s %s ON ("%s"."%s" = "%s"."%s")' % \
(join, alias_mapping[dest_table], table, lhs_col, dest_table, col)
query_from = add_joins_for_table(dest_table, query_from)
return query_from
for table in tables_to_process:
query_from += table
if table in self.joins:
query_from = add_joins_for_table(table, query_from)
if _get_alias_from_statement(table) in self.joins:
query_from = add_joins_for_table(_get_alias_from_statement(table), query_from)
query_from += ','
query_from = query_from[:-1] # drop last comma
return (query_from, " AND ".join(self.where_clause), self.where_clause_params)

View File

@ -28,8 +28,8 @@ class QueryTestCase(unittest.TestCase):
query = Query()
query.tables.extend(['"product_product"','"product_template"'])
query.where_clause.append("product_product.template_id = product_template.id")
query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
query.join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join
query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
query.add_join(("product_product", "res_user", "user_id", "id"), outer=True) # outer join
self.assertEquals(query.get_sql()[0].strip(),
""""product_product" LEFT JOIN "res_user" ON ("product_product"."user_id" = "res_user"."id"),"product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") """.strip())
self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
@ -38,8 +38,8 @@ class QueryTestCase(unittest.TestCase):
query = Query()
query.tables.extend(['"product_product"','"product_template"'])
query.where_clause.append("product_product.template_id = product_template.id")
query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
query.add_join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
self.assertEquals(query.get_sql()[0].strip(),
""""product_product","product_template" JOIN "product_category" ON ("product_template"."categ_id" = "product_category"."id") LEFT JOIN "res_user" ON ("product_category"."user_id" = "res_user"."id")""".strip())
self.assertEquals(query.get_sql()[1].strip(), """product_product.template_id = product_template.id""".strip())
@ -48,8 +48,8 @@ class QueryTestCase(unittest.TestCase):
query = Query()
query.tables.extend(['"product_product"','"product_template"'])
query.where_clause.append("product_product.template_id = product_template.id")
query.join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
query.join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
query.add_join(("product_template", "product_category", "categ_id", "id"), outer=False) # add normal join
query.add_join(("product_category", "res_user", "user_id", "id"), outer=True) # CHAINED outer join
query.tables.append('"account.account"')
query.where_clause.append("product_category.expense_account_id = account_account.id") # additional implicit join
self.assertEquals(query.get_sql()[0].strip(),