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