server/sources/rql2sql.py
branchstable
changeset 7193 7eaef037ea9d
parent 7108 bcdf22734059
child 7194 79686c864bbf
equal deleted inserted replaced
7192:9e92c8558fea 7193:7eaef037ea9d
   317 
   317 
   318 
   318 
   319 # IGenerator implementation for RQL->SQL #######################################
   319 # IGenerator implementation for RQL->SQL #######################################
   320 
   320 
   321 class StateInfo(object):
   321 class StateInfo(object):
       
   322     """this class stores data accumulated during the RQL syntax tree visit
       
   323     for later SQL generation.
       
   324 
       
   325     Attributes related to OUTER JOIN handling
       
   326     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
   327     * `outer_chains`, list of list of strings. Each list represent a tables
       
   328       that have to be outer joined together.
       
   329 
       
   330     * `outer_tables`, dictionnary used as index of tables used in outer join ::
       
   331 
       
   332         'table alias': (outertype, [conditions], [chain])
       
   333 
       
   334       where:
       
   335 
       
   336       * `outertype` is one of None, 'LEFT', 'RIGHT', 'FULL'
       
   337       * `conditions` is a list of join conditions (string)
       
   338       * `chain` is a list of table alias (the *outer chain*) in which the key
       
   339         alias appears
       
   340 
       
   341     * `outer_pending` is a dictionary containing some conditions that will have
       
   342       to be added to the outer join when the table will be turned into an
       
   343       outerjoin ::
       
   344 
       
   345        'table alias': [conditions]
       
   346     """
   322     def __init__(self, select, existssols, unstablevars):
   347     def __init__(self, select, existssols, unstablevars):
   323         self.existssols = existssols
   348         self.existssols = existssols
   324         self.unstablevars = unstablevars
   349         self.unstablevars = unstablevars
   325         self.subtables = {}
   350         self.subtables = {}
   326         self.needs_source_cb = None
   351         self.needs_source_cb = None
   336         self.done = set()
   361         self.done = set()
   337         self.tables = self.subtables.copy()
   362         self.tables = self.subtables.copy()
   338         self.actual_tables = [[]]
   363         self.actual_tables = [[]]
   339         for _, tsql in self.tables.itervalues():
   364         for _, tsql in self.tables.itervalues():
   340             self.actual_tables[-1].append(tsql)
   365             self.actual_tables[-1].append(tsql)
       
   366         self.outer_chains = []
   341         self.outer_tables = {}
   367         self.outer_tables = {}
       
   368         self.outer_pending = {}
   342         self.duplicate_switches = []
   369         self.duplicate_switches = []
   343         self.aliases = {}
   370         self.aliases = {}
   344         self.restrictions = []
   371         self.restrictions = []
   345         self._restr_stack = []
   372         self._restr_stack = []
   346         self.ignore_varmap = False
   373         self.ignore_varmap = False
   401     def pop_scope(self):
   428     def pop_scope(self):
   402         del self.scopes[self.scope_nodes[-1]]
   429         del self.scopes[self.scope_nodes[-1]]
   403         self.scope_nodes.pop()
   430         self.scope_nodes.pop()
   404         restrictions = self.restrictions
   431         restrictions = self.restrictions
   405         self.restrictions = self._restr_stack.pop()
   432         self.restrictions = self._restr_stack.pop()
   406         return restrictions, self.actual_tables.pop()
   433         scope = len(self.actual_tables) - 1
       
   434         # check if we have some outer chain for this scope
       
   435         matching_chains = []
       
   436         for chain in self.outer_chains:
       
   437             for tablealias in chain:
       
   438                 if self.tables[tablealias][0] < scope:
       
   439                     # chain belongs to outer scope
       
   440                     break
       
   441             else:
       
   442                 # chain match current scope
       
   443                 matching_chains.append(chain)
       
   444         # call to `tables_sql` will pop actual_tables
       
   445         tables = self.tables_sql(matching_chains)
       
   446         # cleanup outer join related structure for tables in matching chains
       
   447         for chain in matching_chains:
       
   448             self.outer_chains.remove(chain)
       
   449             for alias in chain:
       
   450                 del self.outer_tables[alias]
       
   451         return restrictions, tables
       
   452 
       
   453     # tables handling #########################################################
       
   454 
       
   455     def add_table(self, table, key=None, scope=-1):
       
   456         if key is None:
       
   457             key = table
       
   458         if key in self.tables:
       
   459             return
       
   460         if scope < 0:
       
   461             scope = len(self.actual_tables) + scope
       
   462         self.tables[key] = (scope, table)
       
   463         self.actual_tables[scope].append(table)
       
   464 
       
   465     def alias_and_add_table(self, tablename, scope=-1):
       
   466         alias = '%s%s' % (tablename, self.count)
       
   467         self.count += 1
       
   468         self.add_table('%s AS %s' % (tablename, alias), alias, scope)
       
   469         return alias
       
   470 
       
   471     def relation_table(self, relation):
       
   472         """return the table alias used by the given relation"""
       
   473         if relation in self.done:
       
   474             return relation._q_sqltable
       
   475         rid = 'rel_%s%s' % (relation.r_type, self.count)
       
   476         # relation's table is belonging to the root scope if it is the principal
       
   477         # table of one of it's variable and if that variable belong's to parent
       
   478         # scope
       
   479         for varref in relation.iget_nodes(VariableRef):
       
   480             var = varref.variable
       
   481             if isinstance(var, ColumnAlias):
       
   482                 scope = 0
       
   483                 break
       
   484             # XXX may have a principal without being invariant for this generation,
       
   485             #     not sure this is a pb or not
       
   486             if var.stinfo.get('principal') is relation and var.scope is var.stmt:
       
   487                 scope = 0
       
   488                 break
       
   489         else:
       
   490             scope = -1
       
   491         self.count += 1
       
   492         self.add_table('%s_relation AS %s' % (relation.r_type, rid), rid, scope=scope)
       
   493         relation._q_sqltable = rid
       
   494         self.done.add(relation)
       
   495         return rid
       
   496 
       
   497     def fti_table(self, relation, fti_table):
       
   498         """return the table alias used by the given has_text relation,
       
   499         `fti_table` being the table name for the plain text index
       
   500         """
       
   501         if relation in self.done:
       
   502             try:
       
   503                 return relation._q_sqltable
       
   504             except AttributeError:
       
   505                 pass
       
   506         self.done.add(relation)
       
   507         scope = self.scopes[relation.scope]
       
   508         alias = self.alias_and_add_table(fti_table, scope=scope)
       
   509         relation._q_sqltable = alias
       
   510         return alias
       
   511 
       
   512     # outer join handling ######################################################
       
   513 
       
   514     def mark_as_used_in_outer_join(self, tablealias, addpending=True):
       
   515         """Mark table of given alias as used in outer join. This must be called
       
   516         after `outer_tables[tablealias]` has been initialized.
       
   517         """
       
   518         # remove a table from actual_table because it's used in an outer join
       
   519         # chain
       
   520         scope, tabledef = self.tables[tablealias]
       
   521         self.actual_tables[scope].remove(tabledef)
       
   522         # check if there are some pending outer join condition for this table
       
   523         if addpending:
       
   524             try:
       
   525                 pending_conditions = self.outer_pending.pop(tablealias)
       
   526             except KeyError:
       
   527                 pass
       
   528             else:
       
   529                 self.outer_tables[tablealias][1].extend(pending_conditions)
       
   530         else:
       
   531             assert not tablealias in self.outer_pending
       
   532 
       
   533     def add_outer_join_condition(self, tablealias, condition):
       
   534         try:
       
   535             outer, conditions, chain = self.outer_tables[tablealias]
       
   536             conditions.append(condition)
       
   537         except KeyError:
       
   538             self.outer_pending.setdefault(tablealias, []).append(condition)
       
   539 
       
   540     def replace_tables_by_outer_join(self, leftalias, rightalias,
       
   541                                      outertype, condition):
       
   542         """tell we need <leftalias> <outertype> JOIN <rightalias> ON <condition>
       
   543         """
       
   544         assert leftalias != rightalias, leftalias
       
   545         outer_tables = self.outer_tables
       
   546         louter, lconditions, lchain = outer_tables.get(leftalias,
       
   547                                                       (None, None, None))
       
   548         router, rconditions, rchain = outer_tables.get(rightalias,
       
   549                                                       (None, None, None))
       
   550         if lchain is None and rchain is None:
       
   551             # create a new outer chaine
       
   552             chain = [leftalias, rightalias]
       
   553             outer_tables[leftalias] = (None, [], chain)
       
   554             outer_tables[rightalias] = (outertype, [condition], chain)
       
   555             self.outer_chains.append(chain)
       
   556             self.mark_as_used_in_outer_join(leftalias, addpending=False)
       
   557             self.mark_as_used_in_outer_join(rightalias)
       
   558         elif lchain is None:
       
   559             # [A > B > C] + [D > A] -> [D > A > B > C]
       
   560             if rightalias == rchain[0]:
       
   561                 outer_tables[leftalias] = (None, [], rchain)
       
   562                 conditions = outer_tables[rightalias][1] + [condition]
       
   563                 outer_tables[rightalias] = (outertype, conditions, rchain)
       
   564                 rchain.insert(0, leftalias)
       
   565             else:
       
   566                 # [A > B > C] + [D > B] -> [A > B > C < D]
       
   567                 if outertype == 'LEFT':
       
   568                     outertype = 'RIGHT'
       
   569                 outer_tables[leftalias] = (outertype, [condition], rchain)
       
   570                 rchain.append(leftalias)
       
   571             self.mark_as_used_in_outer_join(leftalias)
       
   572         elif rchain is None:
       
   573             # [A > B > C] + [B > D] -> [A > B > C > D]
       
   574             outer_tables[rightalias] = (outertype, [condition], lchain)
       
   575             lchain.append(rightalias)
       
   576             self.mark_as_used_in_outer_join(rightalias)
       
   577         elif lchain is rchain:
       
   578             # already in the same chain, simply check compatibility and append
       
   579             # the condition if it's ok
       
   580             lidx = lchain.index(leftalias)
       
   581             ridx = lchain.index(rightalias)
       
   582             if (outertype == 'FULL' and router != 'FULL') \
       
   583                    or (lidx < ridx and router != 'LEFT') \
       
   584                    or (ridx < lidx and louter != 'RIGHT'):
       
   585                 raise BadRQLQuery()
       
   586             # merge conditions
       
   587             if lidx < ridx:
       
   588                 rconditions.append(condition)
       
   589             else:
       
   590                 lconditions.append(condition)
       
   591         else:
       
   592             if louter is not None:
       
   593                 raise BadRQLQuery()
       
   594             # merge chains
       
   595             self.outer_chains.remove(lchain)
       
   596             self.mark_as_used_in_outer_join(leftalias)
       
   597             rchain += lchain
       
   598             for alias, (aouter, aconditions, achain) in outer_tables.iteritems():
       
   599                 if achain is lchain:
       
   600                     outer_tables[alias] = (aouter, aconditions, rchain)
       
   601 
       
   602     # sql generation helpers ###################################################
       
   603 
       
   604     def tables_sql(self, outer_chains=None):
       
   605         """generate SQL for FROM clause"""
       
   606         # sort for test predictability
       
   607         tables = sorted(self.actual_tables.pop())
       
   608         # process outer joins
       
   609         if outer_chains is None:
       
   610             assert not self.actual_tables, self.actual_tables
       
   611             assert not self.outer_pending
       
   612             outer_chains = self.outer_chains
       
   613         for chain in sorted(outer_chains):
       
   614             tablealias = chain[0]
       
   615             outertype, conditions, _ = self.outer_tables[tablealias]
       
   616             assert _ is chain, (chain, _)
       
   617             assert outertype is None, (chain, self.outer_chains)
       
   618             assert not conditions, (chain, self.outer_chains)
       
   619             assert len(chain) > 1
       
   620             tabledef = self.tables[tablealias][1]
       
   621             outerjoin = [tabledef]
       
   622             for tablealias in chain[1:]:
       
   623                 outertype, conditions, _ = self.outer_tables[tablealias]
       
   624                 assert _ is chain, (chain, self.outer_chains)
       
   625                 assert outertype in ('LEFT', 'RIGHT', 'FULL'), (
       
   626                     tablealias, outertype, conditions)
       
   627                 assert isinstance(conditions, (list)), (
       
   628                     tablealias, outertype, conditions)
       
   629                 tabledef = self.tables[tablealias][1]
       
   630                 outerjoin.append('%s OUTER JOIN %s ON (%s)' % (
       
   631                     outertype, tabledef, ' AND '.join(conditions)))
       
   632             tables.append(' '.join(outerjoin))
       
   633         return ', '.join(tables)
       
   634 
   407 
   635 
   408 def extract_fake_having_terms(having):
   636 def extract_fake_having_terms(having):
   409     """RQL's HAVING may be used to contains stuff that should go in the WHERE
   637     """RQL's HAVING may be used to contains stuff that should go in the WHERE
   410     clause of the SQL query, due to RQL grammar limitation. Split them...
   638     clause of the SQL query, due to RQL grammar limitation. Split them...
   411 
   639 
   445             else:
   673             else:
   446                 node = oor or compnode
   674                 node = oor or compnode
   447                 fakehaving.append(node)
   675                 fakehaving.append(node)
   448                 node.parent.remove(node)
   676                 node.parent.remove(node)
   449     return fakehaving
   677     return fakehaving
       
   678 
   450 
   679 
   451 class SQLGenerator(object):
   680 class SQLGenerator(object):
   452     """
   681     """
   453     generation of SQL from the fully expanded RQL syntax tree
   682     generation of SQL from the fully expanded RQL syntax tree
   454     SQL is designed to be used with a CubicWeb SQL schema
   683     SQL is designed to be used with a CubicWeb SQL schema
   596         self._in_wrapping_query = False
   825         self._in_wrapping_query = False
   597         self._state = state
   826         self._state = state
   598         try:
   827         try:
   599             sql = self._solutions_sql(select, morerestr, sols, distinct,
   828             sql = self._solutions_sql(select, morerestr, sols, distinct,
   600                                       needalias or needwrap)
   829                                       needalias or needwrap)
   601             # generate groups / having before wrapping query selection to
   830             # generate groups / having before wrapping query selection to get
   602             # get correct column aliases
   831             # correct column aliases
   603             self._in_wrapping_query = needwrap
   832             self._in_wrapping_query = needwrap
   604             if groups:
   833             if groups:
   605                 # no constant should be inserted in GROUP BY else the backend will
   834                 # no constant should be inserted in GROUP BY else the backend
   606                 # interpret it as a positional index in the selection
   835                 # will interpret it as a positional index in the selection
   607                 groups = ','.join(vref.accept(self) for vref in groups
   836                 groups = ','.join(vref.accept(self) for vref in groups
   608                                   if not isinstance(vref, Constant))
   837                                   if not isinstance(vref, Constant))
   609             if having:
   838             if having:
   610                 # filter out constants as for GROUP BY
   839                 # filter out constants as for GROUP BY
   611                 having = ' AND '.join(term.accept(self) for term in having
   840                 having = ' AND '.join(term.accept(self) for term in having
   612                                       if not isinstance(term, Constant))
   841                                       if not isinstance(term, Constant))
   613             if needwrap:
   842             if needwrap:
   614                 sql = '%s FROM (%s) AS T1' % (self._selection_sql(outerselection, distinct,
   843                 sql = '%s FROM (%s) AS T1' % (
   615                                                                   needalias),
   844                     self._selection_sql(outerselection, distinct,needalias),
   616                                               sql)
   845                     sql)
   617             if groups:
   846             if groups:
   618                 sql += '\nGROUP BY %s' % groups
   847                 sql += '\nGROUP BY %s' % groups
   619             if having:
   848             if having:
   620                 sql += '\nHAVING %s' % having
   849                 sql += '\nHAVING %s' % having
   621             # sort
   850             # sort
   624                 for sortterm in sorts:
   853                 for sortterm in sorts:
   625                     _term = self._sortterm_sql(sortterm, fselectidx)
   854                     _term = self._sortterm_sql(sortterm, fselectidx)
   626                     if _term is not None:
   855                     if _term is not None:
   627                         sqlsortterms.append(_term)
   856                         sqlsortterms.append(_term)
   628                 if sqlsortterms:
   857                 if sqlsortterms:
   629                     sql = self.dbhelper.sql_add_order_by(sql, sqlsortterms,
   858                     sql = self.dbhelper.sql_add_order_by(
   630                                                          origselection,
   859                         sql, sqlsortterms, origselection, fneedwrap,
   631                                                          fneedwrap,
   860                         select.limit or select.offset)
   632                                                          select.limit or select.offset)
       
   633                     ## sql += '\nORDER BY %s' % ','.join(sqlsortterms)
       
   634                     ## if sorts and fneedwrap:
       
   635                     ##     selection = ['T1.C%s' % i for i in xrange(len(origselection))]
       
   636                     ##     sql = 'SELECT %s FROM (%s) AS T1' % (','.join(selection), sql)
       
   637             else:
   861             else:
   638                 sqlsortterms = None
   862                 sqlsortterms = None
   639             state.finalize_source_cbs()
   863             state.finalize_source_cbs()
   640         finally:
   864         finally:
   641             select.selection = origselection
   865             select.selection = origselection
   681             if self._state.restrictions:
   905             if self._state.restrictions:
   682                 sql.append('WHERE %s' % ' AND '.join(self._state.restrictions))
   906                 sql.append('WHERE %s' % ' AND '.join(self._state.restrictions))
   683             self._state.merge_source_cbs(self._state._needs_source_cb)
   907             self._state.merge_source_cbs(self._state._needs_source_cb)
   684             # add required tables
   908             # add required tables
   685             assert len(self._state.actual_tables) == 1, self._state.actual_tables
   909             assert len(self._state.actual_tables) == 1, self._state.actual_tables
   686             tables = self._state.actual_tables[-1]
   910             tables = self._state.tables_sql()
   687             if tables:
   911             if tables:
   688                 # sort for test predictability
   912                 sql.insert(1, 'FROM %s' % tables)
   689                 sql.insert(1, 'FROM %s' % ', '.join(sorted(tables)))
       
   690             elif self._state.restrictions and self.dbhelper.needs_from_clause:
   913             elif self._state.restrictions and self.dbhelper.needs_from_clause:
   691                 sql.insert(1, 'FROM (SELECT 1) AS _T')
   914                 sql.insert(1, 'FROM (SELECT 1) AS _T')
   692             sqls.append('\n'.join(sql))
   915             sqls.append('\n'.join(sql))
   693         if distinct:
   916         if distinct:
   694             return '\nUNION\n'.join(sqls)
   917             return '\nUNION\n'.join(sqls)
   771         if restriction:
   994         if restriction:
   772             restrictions.append(restriction)
   995             restrictions.append(restriction)
   773         restriction = ' AND '.join(restrictions)
   996         restriction = ' AND '.join(restrictions)
   774         if not restriction:
   997         if not restriction:
   775             if tables:
   998             if tables:
   776                 return 'SELECT 1 FROM %s' % ', '.join(tables)
   999                 return 'SELECT 1 FROM %s' % tables
   777             return ''
  1000             return ''
   778         if not tables:
  1001         if not tables:
   779             # XXX could leave surrounding EXISTS() in this case no?
  1002             # XXX could leave surrounding EXISTS() in this case no?
   780             sql = 'SELECT 1 WHERE %s' % restriction
  1003             sql = 'SELECT 1 WHERE %s' % restriction
   781         else:
  1004         else:
   782             sql = 'SELECT 1 FROM %s WHERE %s' % (', '.join(tables), restriction)
  1005             sql = 'SELECT 1 FROM %s WHERE %s' % (tables, restriction)
   783         return sql
  1006         return sql
   784 
  1007 
   785 
  1008 
   786     def visit_relation(self, relation):
  1009     def visit_relation(self, relation):
   787         """generate SQL for a relation"""
  1010         """generate SQL for a relation"""
   818             return '%s%s' % (lhssql, rhs.accept(self))
  1041             return '%s%s' % (lhssql, rhs.accept(self))
   819         elif '%s.%s' % (lhs, relation.r_type) in self._varmap:
  1042         elif '%s.%s' % (lhs, relation.r_type) in self._varmap:
   820             # relation has already been processed by a previous step
  1043             # relation has already been processed by a previous step
   821             return ''
  1044             return ''
   822         elif relation.optional:
  1045         elif relation.optional:
   823             # check it has not already been treaten (to get necessary
       
   824             # information to add an outer join condition)
       
   825             if relation in self._state.done:
       
   826                 return ''
       
   827             # OPTIONAL relation, generate a left|right outer join
  1046             # OPTIONAL relation, generate a left|right outer join
   828             sql = self._visit_outer_join_relation(relation, rschema)
  1047             if rtype == 'identity' or rschema.inlined:
       
  1048                 sql = self._visit_outer_join_inlined_relation(relation, rschema)
       
  1049             else:
       
  1050                 sql = self._visit_outer_join_relation(relation, rschema)
   829         elif rschema.inlined:
  1051         elif rschema.inlined:
   830             sql = self._visit_inlined_relation(relation)
  1052             sql = self._visit_inlined_relation(relation)
   831         else:
  1053         else:
   832             # regular (non final) relation
  1054             # regular (non final) relation
   833             sql = self._visit_relation(relation, rschema)
  1055             sql = self._visit_relation(relation, rschema)
   886         if relation.r_type == 'identity':
  1108         if relation.r_type == 'identity':
   887             # special case "X identity Y"
  1109             # special case "X identity Y"
   888             lhs, rhs = relation.get_parts()
  1110             lhs, rhs = relation.get_parts()
   889             return '%s%s' % (lhs.accept(self), rhs.accept(self))
  1111             return '%s%s' % (lhs.accept(self), rhs.accept(self))
   890         lhsvar, lhsconst, rhsvar, rhsconst = relation_info(relation)
  1112         lhsvar, lhsconst, rhsvar, rhsconst = relation_info(relation)
   891         rid = self._relation_table(relation)
  1113         rid = self._state.relation_table(relation)
   892         sqls = []
  1114         sqls = []
   893         sqls += self._process_relation_term(relation, rid, lhsvar, lhsconst, 'eid_from')
  1115         sqls += self._process_relation_term(relation, rid, lhsvar, lhsconst, 'eid_from')
   894         sqls += self._process_relation_term(relation, rid, rhsvar, rhsconst, 'eid_to')
  1116         sqls += self._process_relation_term(relation, rid, rhsvar, rhsconst, 'eid_to')
   895         sql = ' AND '.join(sqls)
  1117         sql = ' AND '.join(sqls)
   896         if rschema.symmetric:
  1118         if rschema.symmetric:
   923            -> Y LEFT OUTER JOIN relation ON (relation.eid_to=Y.eid)
  1145            -> Y LEFT OUTER JOIN relation ON (relation.eid_to=Y.eid)
   924               LEFT OUTER JOIN X ON (relation.eid_from=X.eid)
  1146               LEFT OUTER JOIN X ON (relation.eid_from=X.eid)
   925            elif it's a full outer join:
  1147            elif it's a full outer join:
   926            -> X FULL OUTER JOIN Y ON (X.relation=Y.eid)
  1148            -> X FULL OUTER JOIN Y ON (X.relation=Y.eid)
   927         """
  1149         """
   928         lhsvar, lhsconst, rhsvar, rhsconst = relation_info(relation)
  1150         leftvar, leftconst, rightvar, rightconst = relation_info(relation)
   929         if relation.optional == 'right':
  1151         assert not (leftconst and rightconst), "doesn't make sense"
       
  1152         if relation.optional == 'left':
       
  1153             leftvar, rightvar = rightvar, leftvar
       
  1154             leftconst, rightconst = rightconst, leftconst
       
  1155             joinattr, restrattr = 'eid_to', 'eid_from'
       
  1156         else:
   930             joinattr, restrattr = 'eid_from', 'eid_to'
  1157             joinattr, restrattr = 'eid_from', 'eid_to'
   931         else:
  1158         # search table for this variable, to use as left table of the outer join
   932             lhsvar, rhsvar = rhsvar, lhsvar
  1159         leftalias = None
   933             lhsconst, rhsconst = rhsconst, lhsconst
  1160         if leftvar:
   934             joinattr, restrattr = 'eid_to', 'eid_from'
  1161             # take care, may return None for invariant variable
   935         if relation.optional == 'both':
  1162             leftalias = self._var_table(leftvar)
   936             outertype = 'FULL'
  1163         if leftalias is None:
   937         else:
  1164             if leftvar.stinfo['principal'] is not relation:
   938             outertype = 'LEFT'
  1165                 # use variable's principal relation
   939         if rschema.inlined or relation.r_type == 'identity':
  1166                 leftalias = leftvar.stinfo['principal']._q_sqltable
   940             self._state.done.add(relation)
  1167             else:
   941             t1 = self._var_table(lhsvar)
  1168                 # search for relation on which we should join
   942             if relation.r_type == 'identity':
  1169                 for orelation in leftvar.stinfo['relations']:
   943                 attr = 'eid'
  1170                     if (orelation is not relation and
   944             else:
  1171                         not self.schema.rschema(orelation.r_type).final):
   945                 attr = relation.r_type
  1172                         break
   946             # reset lhs/rhs, we need the initial order now
  1173                 else:
   947             lhs, rhs = relation.get_variable_parts()
  1174                     for orelation in rightvar.stinfo['relations']:
   948             if '%s.%s' % (lhs.name, attr) in self._varmap:
  1175                         if (orelation is not relation and
   949                 lhssql = self._varmap['%s.%s' % (lhs.name, attr)]
  1176                             not self.schema.rschema(orelation.r_type).final
   950             else:
  1177                             and orelation.optional):
   951                 lhssql = '%s.%s%s' % (self._var_table(lhs.variable), SQL_PREFIX, attr)
  1178                             break
   952             if not rhsvar is None:
  1179                     else:
   953                 t2 = self._var_table(rhsvar)
  1180                         # unexpected
   954                 if t2 is None:
  1181                         assert False, leftvar
   955                     if rhsconst is not None:
  1182                 leftalias = self._state.relation_table(orelation)
   956                         # inlined relation with invariant as rhs
  1183         # right table of the outer join
   957                         condition = '%s=%s' % (lhssql, rhsconst.accept(self))
  1184         rightalias = self._state.relation_table(relation)
   958                         if relation.r_type != 'identity':
  1185         # compute join condition
   959                             condition = '(%s OR %s IS NULL)' % (condition, lhssql)
  1186         if not leftconst or (leftvar and not leftvar._q_invariant):
   960                         if not lhsvar.stinfo.get('optrelations'):
  1187             leftsql = leftvar.accept(self)
   961                             return condition
  1188         else:
   962                         self.add_outer_join_condition(lhsvar, t1, condition)
  1189             leftsql = leftconst.accept(self)
   963                     return
  1190         condition = '%s.%s=%s' % (rightalias, joinattr, leftsql)
   964             else:
  1191         if rightconst:
   965                 condition = '%s=%s' % (lhssql, rhsconst.accept(self))
  1192             condition += ' AND %s.%s=%s' % (rightalias, restrattr, rightconst.accept(self))
   966                 self.add_outer_join_condition(lhsvar, t1, condition)
  1193         # record outer join
   967             join = '%s OUTER JOIN %s ON (%s=%s)' % (
  1194         outertype = 'FULL' if relation.optional == 'both' else 'LEFT'
   968                 outertype, self._state.tables[t2][1], lhssql, rhs.accept(self))
  1195         self._state.replace_tables_by_outer_join(leftalias, rightalias,
   969             self.replace_tables_by_outer_join(join, t1, t2)
  1196                                                  outertype, condition)
   970             return ''
  1197         # need another join?
   971         lhssql = lhsconst and lhsconst.accept(self) or lhsvar.accept(self)
  1198         if rightconst is None:
   972         rhssql = rhsconst and rhsconst.accept(self) or rhsvar.accept(self)
  1199             # we need another outer join for the other side of the relation (e.g.
   973         rid = self._relation_table(relation)
  1200             # for "X relation Y?" in RQL, we treated earlier the (cw_X.eid /
   974         if not lhsvar:
  1201             # relation.eid_from) join, now we've to do (relation.eid_to /
   975             join = ''
  1202             # cw_Y.eid)
   976             toreplace = []
  1203             leftalias = rightalias
   977             maintable = rid
  1204             rightsql = rightvar.accept(self) # accept before using var_table
   978         else:
  1205             rightalias = self._var_table(rightvar)
   979             join = '%s OUTER JOIN %s ON (%s.%s=%s' % (
  1206             if rightalias is None:
   980                 outertype, self._state.tables[rid][1], rid, joinattr, lhssql)
  1207                 if rightvar.stinfo['principal'] is not relation:
   981             toreplace = [rid]
  1208                     self._state.replace_tables_by_outer_join(
   982             maintable = self._var_table(lhsvar)
  1209                         leftalias, rightvar.stinfo['principal']._q_sqltable,
   983             if rhsconst:
  1210                         outertype, '%s.%s=%s' % (leftalias, restrattr, rightvar.accept(self)))
   984                 join += ' AND %s.%s=%s)' % (rid, restrattr, rhssql)
  1211             else:
   985             else:
  1212                 self._state.replace_tables_by_outer_join(
   986                 join += ')'
  1213                     leftalias, rightalias, outertype,
   987         if not rhsconst:
  1214                     '%s.%s=%s' % (leftalias, restrattr, rightvar.accept(self)))
   988             rhstable = rhsvar._q_sqltable
  1215         # this relation will hence be expressed in FROM clause, return nothing
   989             if rhstable:
  1216         # here
   990                 assert rhstable is not None, rhsvar
  1217         return ''
   991                 join += ' %s OUTER JOIN %s ON (%s.%s=%s)' % (
  1218 
   992                     outertype, self._state.tables[rhstable][1], rid, restrattr,
  1219 
   993                     rhssql)
  1220     def _visit_outer_join_inlined_relation(self, relation, rschema):
   994                 toreplace.append(rhstable)
  1221         leftvar, leftconst, rightvar, rightconst = relation_info(relation)
   995         self.replace_tables_by_outer_join(join, maintable, *toreplace)
  1222         assert not (leftconst and rightconst), "doesn't make sense"
       
  1223         if relation.optional != 'right':
       
  1224             leftvar, rightvar = rightvar, leftvar
       
  1225             leftconst, rightconst = rightconst, leftconst
       
  1226         outertype = 'FULL' if relation.optional == 'both' else 'LEFT'
       
  1227         leftalias = self._var_table(leftvar)
       
  1228         attr = 'eid' if relation.r_type == 'identity' else relation.r_type
       
  1229         lhs, rhs = relation.get_variable_parts()
       
  1230         try:
       
  1231             lhssql = self._varmap['%s.%s' % (lhs.name, attr)]
       
  1232         except KeyError:
       
  1233             lhssql = '%s.%s%s' % (self._var_table(lhs.variable), SQL_PREFIX, attr)
       
  1234         if rightvar is not None:
       
  1235             rightalias = self._var_table(rightvar)
       
  1236             if rightalias is None:
       
  1237                 if rightconst is not None:
       
  1238                     # inlined relation with invariant as rhs
       
  1239                     condition = '%s=%s' % (lhssql, rightconst.accept(self))
       
  1240                     if relation.r_type != 'identity':
       
  1241                         condition = '(%s OR %s IS NULL)' % (condition, lhssql)
       
  1242                     if not leftvar.stinfo.get('optrelations'):
       
  1243                         return condition
       
  1244                     self._state.add_outer_join_condition(leftalias, condition)
       
  1245                 return
       
  1246         self._state.replace_tables_by_outer_join(
       
  1247             leftalias, rightalias, outertype, '%s=%s' % (lhssql, rhs.accept(self)))
   996         return ''
  1248         return ''
   997 
  1249 
   998     def _visit_var_attr_relation(self, relation, rhs_vars):
  1250     def _visit_var_attr_relation(self, relation, rhs_vars):
   999         """visit an attribute relation with variable(s) in the RHS
  1251         """visit an attribute relation with variable(s) in the RHS
  1000 
  1252 
  1052             else:
  1304             else:
  1053                 sql = '%s%s' % (lhssql, rhssql)
  1305                 sql = '%s%s' % (lhssql, rhssql)
  1054         except AttributeError:
  1306         except AttributeError:
  1055             sql = '%s%s' % (lhssql, rhssql)
  1307             sql = '%s%s' % (lhssql, rhssql)
  1056         if lhs.variable.stinfo.get('optrelations'):
  1308         if lhs.variable.stinfo.get('optrelations'):
  1057             self.add_outer_join_condition(lhs.variable, table, sql)
  1309             self._state.add_outer_join_condition(table, sql)
  1058         else:
  1310         else:
  1059             return sql
  1311             return sql
  1060 
  1312 
  1061     def _visit_has_text_relation(self, rel):
  1313     def _visit_has_text_relation(self, rel):
  1062         """generate SQL for a has_text relation"""
  1314         """generate SQL for a has_text relation"""
  1063         lhs, rhs = rel.get_parts()
  1315         lhs, rhs = rel.get_parts()
  1064         const = rhs.children[0]
  1316         const = rhs.children[0]
  1065         alias = self._fti_table(rel)
  1317         alias = self._state.fti_table(rel, self.dbhelper.fti_table)
  1066         jointo = lhs.accept(self)
  1318         jointo = lhs.accept(self)
  1067         restriction = ''
  1319         restriction = ''
  1068         lhsvar = lhs.variable
  1320         lhsvar = lhs.variable
  1069         me_is_principal = lhsvar.stinfo.get('principal') is rel
  1321         me_is_principal = lhsvar.stinfo.get('principal') is rel
  1070         if me_is_principal:
  1322         if me_is_principal:
  1074             elif not lhsvar.name in self._varmap:
  1326             elif not lhsvar.name in self._varmap:
  1075                 # join on entities instead of etype's table to get result for
  1327                 # join on entities instead of etype's table to get result for
  1076                 # external entities on multisources configurations
  1328                 # external entities on multisources configurations
  1077                 ealias = lhsvar._q_sqltable = '_' + lhsvar.name
  1329                 ealias = lhsvar._q_sqltable = '_' + lhsvar.name
  1078                 jointo = lhsvar._q_sql = '%s.eid' % ealias
  1330                 jointo = lhsvar._q_sql = '%s.eid' % ealias
  1079                 self.add_table('entities AS %s' % ealias, ealias)
  1331                 self._state.add_table('entities AS %s' % ealias, ealias)
  1080                 if not lhsvar._q_invariant or len(lhsvar.stinfo['possibletypes']) == 1:
  1332                 if not lhsvar._q_invariant or len(lhsvar.stinfo['possibletypes']) == 1:
  1081                     restriction = " AND %s.type='%s'" % (ealias, self._state.solution[lhs.name])
  1333                     restriction = " AND %s.type='%s'" % (ealias, self._state.solution[lhs.name])
  1082                 else:
  1334                 else:
  1083                     etypes = ','.join("'%s'" % etype for etype in lhsvar.stinfo['possibletypes'])
  1335                     etypes = ','.join("'%s'" % etype for etype in lhsvar.stinfo['possibletypes'])
  1084                     restriction = " AND %s.type IN (%s)" % (ealias, etypes)
  1336                     restriction = " AND %s.type IN (%s)" % (ealias, etypes)
  1137                 rel = iter(func.children[0].variable.stinfo['ftirels']).next()
  1389                 rel = iter(func.children[0].variable.stinfo['ftirels']).next()
  1138             except KeyError:
  1390             except KeyError:
  1139                 raise BadRQLQuery("can't use FTIRANK on variable not used in an"
  1391                 raise BadRQLQuery("can't use FTIRANK on variable not used in an"
  1140                                   " 'has_text' relation (eg full-text search)")
  1392                                   " 'has_text' relation (eg full-text search)")
  1141             const = rel.get_parts()[1].children[0]
  1393             const = rel.get_parts()[1].children[0]
  1142             return self.dbhelper.fti_rank_order(self._fti_table(rel),
  1394             return self.dbhelper.fti_rank_order(
  1143                                                 const.eval(self._args))
  1395                 self._state.fti_table(rel, self.dbhelper.fti_table),
       
  1396                 const.eval(self._args))
  1144         args = [c.accept(self) for c in func.children]
  1397         args = [c.accept(self) for c in func.children]
  1145         if func in self._state.source_cb_funcs:
  1398         if func in self._state.source_cb_funcs:
  1146             # function executed as a callback on the source
  1399             # function executed as a callback on the source
  1147             assert len(args) == 1
  1400             assert len(args) == 1
  1148             return args[0]
  1401             return args[0]
  1186         if colalias.name in self._varmap:
  1439         if colalias.name in self._varmap:
  1187             sql = self._varmap[colalias.name]
  1440             sql = self._varmap[colalias.name]
  1188             table = sql.split('.', 1)[0]
  1441             table = sql.split('.', 1)[0]
  1189             colalias._q_sqltable = table
  1442             colalias._q_sqltable = table
  1190             colalias._q_sql = sql
  1443             colalias._q_sql = sql
  1191             self.add_table(table)
  1444             self._state.add_table(table)
  1192             return sql
  1445             return sql
  1193         return colalias._q_sql
  1446         return colalias._q_sql
  1194 
  1447 
  1195     def visit_variable(self, variable):
  1448     def visit_variable(self, variable):
  1196         """get the table name and sql string for a variable"""
  1449         """get the table name and sql string for a variable"""
  1210         elif variable._q_invariant:
  1463         elif variable._q_invariant:
  1211             # since variable is invariant, we know we won't found final relation
  1464             # since variable is invariant, we know we won't found final relation
  1212             principal = variable.stinfo['principal']
  1465             principal = variable.stinfo['principal']
  1213             if principal is None:
  1466             if principal is None:
  1214                 vtablename = '_' + variable.name
  1467                 vtablename = '_' + variable.name
  1215                 self.add_table('entities AS %s' % vtablename, vtablename)
  1468                 self._state.add_table('entities AS %s' % vtablename, vtablename)
  1216                 sql = '%s.eid' % vtablename
  1469                 sql = '%s.eid' % vtablename
  1217                 if variable.stinfo['typerel'] is not None:
  1470                 if variable.stinfo['typerel'] is not None:
  1218                     # add additional restriction on entities.type column
  1471                     # add additional restriction on entities.type column
  1219                     pts = variable.stinfo['possibletypes']
  1472                     pts = variable.stinfo['possibletypes']
  1220                     if len(pts) == 1:
  1473                     if len(pts) == 1:
  1223                     else:
  1476                     else:
  1224                         etypes = ','.join("'%s'" % et for et in pts)
  1477                         etypes = ','.join("'%s'" % et for et in pts)
  1225                         restr = '%s.type IN (%s)' % (vtablename, etypes)
  1478                         restr = '%s.type IN (%s)' % (vtablename, etypes)
  1226                     self._state.add_restriction(restr)
  1479                     self._state.add_restriction(restr)
  1227             elif principal.r_type == 'has_text':
  1480             elif principal.r_type == 'has_text':
  1228                 sql = '%s.%s' % (self._fti_table(principal),
  1481                 sql = '%s.%s' % (self._state.fti_table(principal,
       
  1482                                                        self.dbhelper.fti_table),
  1229                                  self.dbhelper.fti_uid_attr)
  1483                                  self.dbhelper.fti_uid_attr)
  1230             elif principal in variable.stinfo['rhsrelations']:
  1484             elif principal in variable.stinfo['rhsrelations']:
  1231                 if self.schema.rschema(principal.r_type).inlined:
  1485                 if self.schema.rschema(principal.r_type).inlined:
  1232                     sql = self._linked_var_sql(variable)
  1486                     sql = self._linked_var_sql(variable)
  1233                 else:
  1487                 else:
  1234                     sql = '%s.eid_to' % self._relation_table(principal)
  1488                     sql = '%s.eid_to' % self._state.relation_table(principal)
  1235             else:
  1489             else:
  1236                 sql = '%s.eid_from' % self._relation_table(principal)
  1490                 sql = '%s.eid_from' % self._state.relation_table(principal)
  1237         else:
  1491         else:
  1238             # standard variable: get table name according to etype and use .eid
  1492             # standard variable: get table name according to etype and use .eid
  1239             # attribute
  1493             # attribute
  1240             sql, vtablename = self._var_info(variable)
  1494             sql, vtablename = self._var_info(variable)
  1241         variable._q_sqltable = vtablename
  1495         variable._q_sqltable = vtablename
  1281         KeyError when the key is not found in the varmap
  1535         KeyError when the key is not found in the varmap
  1282         """
  1536         """
  1283         sql = self._varmap[key]
  1537         sql = self._varmap[key]
  1284         tablealias = sql.split('.', 1)[0]
  1538         tablealias = sql.split('.', 1)[0]
  1285         scope = self._temp_table_scope(term.stmt, tablealias)
  1539         scope = self._temp_table_scope(term.stmt, tablealias)
  1286         self.add_table(tablealias, scope=scope)
  1540         self._state.add_table(tablealias, scope=scope)
  1287         return sql, tablealias
  1541         return sql, tablealias
  1288 
  1542 
  1289     def _var_info(self, var):
  1543     def _var_info(self, var):
  1290         try:
  1544         try:
  1291             return self._mapped_term(var, var.name)
  1545             return self._mapped_term(var, var.name)
  1295             # XXX this check should be moved in rql.stcheck
  1549             # XXX this check should be moved in rql.stcheck
  1296             if self.schema.eschema(etype).final:
  1550             if self.schema.eschema(etype).final:
  1297                 raise BadRQLQuery(var.stmt.root)
  1551                 raise BadRQLQuery(var.stmt.root)
  1298             tablealias = '_' + var.name
  1552             tablealias = '_' + var.name
  1299             sql = '%s.%seid' % (tablealias, SQL_PREFIX)
  1553             sql = '%s.%seid' % (tablealias, SQL_PREFIX)
  1300             self.add_table('%s%s AS %s' % (SQL_PREFIX, etype, tablealias),
  1554             self._state.add_table('%s%s AS %s' % (SQL_PREFIX, etype, tablealias),
  1301                            tablealias, scope=scope)
  1555                            tablealias, scope=scope)
  1302         return sql, tablealias
  1556         return sql, tablealias
  1303 
  1557 
  1304     def _inlined_var_sql(self, var, rtype):
  1558     def _inlined_var_sql(self, var, rtype):
  1305         try:
  1559         try:
  1306             sql = self._varmap['%s.%s' % (var.name, rtype)]
  1560             sql = self._varmap['%s.%s' % (var.name, rtype)]
  1307             scope = self._state.scopes[var.scope]
  1561             scope = self._state.scopes[var.scope]
  1308             self.add_table(sql.split('.', 1)[0], scope=scope)
  1562             self._state.add_table(sql.split('.', 1)[0], scope=scope)
  1309         except KeyError:
  1563         except KeyError:
  1310             sql = '%s.%s%s' % (self._var_table(var), SQL_PREFIX, rtype)
  1564             sql = '%s.%s%s' % (self._var_table(var), SQL_PREFIX, rtype)
  1311             #self._state.done.add(var.name)
  1565             #self._state.done.add(var.name)
  1312         return sql
  1566         return sql
  1313 
  1567 
  1343             sql = '%s.%s%s' % (linkedvar._q_sqltable, SQL_PREFIX, rel.r_type)
  1597             sql = '%s.%s%s' % (linkedvar._q_sqltable, SQL_PREFIX, rel.r_type)
  1344         return sql
  1598         return sql
  1345 
  1599 
  1346     # tables handling #########################################################
  1600     # tables handling #########################################################
  1347 
  1601 
  1348     def alias_and_add_table(self, tablename, scope=-1):
       
  1349         alias = '%s%s' % (tablename, self._state.count)
       
  1350         self._state.count += 1
       
  1351         self.add_table('%s AS %s' % (tablename, alias), alias, scope)
       
  1352         return alias
       
  1353 
       
  1354     def add_table(self, table, key=None, scope=-1):
       
  1355         if key is None:
       
  1356             key = table
       
  1357         if key in self._state.tables:
       
  1358             return
       
  1359         if scope < 0:
       
  1360             scope = len(self._state.actual_tables) + scope
       
  1361         self._state.tables[key] = (scope, table)
       
  1362         self._state.actual_tables[scope].append(table)
       
  1363 
       
  1364     def replace_tables_by_outer_join(self, substitute, lefttable, *tables):
       
  1365         for table in tables:
       
  1366             try:
       
  1367                 scope, alias = self._state.tables[table]
       
  1368                 self._state.actual_tables[scope].remove(alias)
       
  1369             except ValueError: # huum, not sure about what should be done here
       
  1370                 msg = "%s already used in an outer join, don't know what to do!"
       
  1371                 raise Exception(msg % table)
       
  1372         try:
       
  1373             tablealias = self._state.outer_tables[lefttable]
       
  1374             actualtables = self._state.actual_tables[-1]
       
  1375         except KeyError:
       
  1376             tablescope, tablealias = self._state.tables[lefttable]
       
  1377             actualtables = self._state.actual_tables[tablescope]
       
  1378         outerjoin = '%s %s' % (tablealias, substitute)
       
  1379         self._update_outer_tables(lefttable, actualtables, tablealias, outerjoin)
       
  1380         for table in tables:
       
  1381             self._state.outer_tables[table] = outerjoin
       
  1382 
       
  1383     def add_outer_join_condition(self, var, table, condition):
       
  1384         try:
       
  1385             tablealias = self._state.outer_tables[table]
       
  1386             actualtables = self._state.actual_tables[-1]
       
  1387         except KeyError:
       
  1388             for rel in var.stinfo.get('optrelations'):
       
  1389                 self.visit_relation(rel)
       
  1390             assert self._state.outer_tables
       
  1391             self.add_outer_join_condition(var, table, condition)
       
  1392             return
       
  1393         before, after = tablealias.split(' AS %s ' % table, 1)
       
  1394         beforep, afterp = after.split(')', 1)
       
  1395         outerjoin = '%s AS %s %s AND %s) %s' % (before, table, beforep,
       
  1396                                                 condition, afterp)
       
  1397         self._update_outer_tables(table, actualtables, tablealias, outerjoin)
       
  1398 
       
  1399     def _update_outer_tables(self, table, actualtables, oldalias, newalias):
       
  1400         actualtables.remove(oldalias)
       
  1401         actualtables.append(newalias)
       
  1402         self._state.outer_tables[table] = newalias
       
  1403         # some tables which have already been used as outer table and replaced
       
  1404         # by <oldalias> may not be reused here, though their associated value
       
  1405         # in the outer_tables dict has to be updated as well
       
  1406         for table, outerexpr in self._state.outer_tables.iteritems():
       
  1407             if outerexpr == oldalias:
       
  1408                 self._state.outer_tables[table] = newalias
       
  1409 
       
  1410     def _var_table(self, var):
  1602     def _var_table(self, var):
  1411         var.accept(self)#.visit_variable(var)
  1603         var.accept(self)#.visit_variable(var)
  1412         return var._q_sqltable
  1604         return var._q_sqltable
  1413 
       
  1414     def _relation_table(self, relation):
       
  1415         """return the table alias used by the given relation"""
       
  1416         if relation in self._state.done:
       
  1417             return relation._q_sqltable
       
  1418         assert not self.schema.rschema(relation.r_type).final, relation.r_type
       
  1419         rid = 'rel_%s%s' % (relation.r_type, self._state.count)
       
  1420         # relation's table is belonging to the root scope if it is the principal
       
  1421         # table of one of it's variable and if that variable belong's to parent
       
  1422         # scope
       
  1423         for varref in relation.iget_nodes(VariableRef):
       
  1424             var = varref.variable
       
  1425             if isinstance(var, ColumnAlias):
       
  1426                 scope = 0
       
  1427                 break
       
  1428             # XXX may have a principal without being invariant for this generation,
       
  1429             #     not sure this is a pb or not
       
  1430             if var.stinfo.get('principal') is relation and var.scope is var.stmt:
       
  1431                 scope = 0
       
  1432                 break
       
  1433         else:
       
  1434             scope = -1
       
  1435         self._state.count += 1
       
  1436         self.add_table('%s_relation AS %s' % (relation.r_type, rid), rid, scope=scope)
       
  1437         relation._q_sqltable = rid
       
  1438         self._state.done.add(relation)
       
  1439         return rid
       
  1440 
       
  1441     def _fti_table(self, relation):
       
  1442         if relation in self._state.done:
       
  1443             try:
       
  1444                 return relation._q_sqltable
       
  1445             except AttributeError:
       
  1446                 pass
       
  1447         self._state.done.add(relation)
       
  1448         scope = self._state.scopes[relation.scope]
       
  1449         alias = self.alias_and_add_table(self.dbhelper.fti_table, scope=scope)
       
  1450         relation._q_sqltable = alias
       
  1451         return alias