[schema] improve normalization of RQLExpressions
Parse and print back the expression instead of manipulating the string.
Among other benefits, it means we don't mangle embedded string constants
that contain commas or multiple spaces.
Closes #6694426
# copyright 2004-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of cubicweb.## yams is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## yams is distributed in the hope that it will be useful, but WITHOUT ANY# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR# A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with yams. If not, see <http://www.gnu.org/licenses/>."""write a schema as sql"""__docformat__="restructuredtext en"fromhashlibimportmd5fromsiximportstring_typesfromsix.movesimportrangefromyams.constraintsimport(SizeConstraint,UniqueConstraint,Attribute,NOW,TODAY)# default are usually not handled at the sql level. If you want them, set# SET_DEFAULT to TrueSET_DEFAULT=Falsedefrschema_has_table(rschema,skip_relations):"""Return True if the given schema should have a table in the database"""returnnot(rschema.finalorrschema.inlinedorrschema.ruleorrschema.typeinskip_relations)defschema2sql(dbhelper,schema,skip_entities=(),skip_relations=(),prefix=''):"""write to the output stream a SQL schema to store the objects corresponding to the given schema """output=[]w=output.appendforetypeinsorted(schema.entities()):eschema=schema.eschema(etype)ifeschema.finaloreschema.typeinskip_entities:continuew(eschema2sql(dbhelper,eschema,skip_relations,prefix=prefix))forrtypeinsorted(schema.relations()):rschema=schema.rschema(rtype)ifrschema_has_table(rschema,skip_relations):w(rschema2sql(rschema))return'\n'.join(output)defdropschema2sql(dbhelper,schema,skip_entities=(),skip_relations=(),prefix=''):"""write to the output stream a SQL schema to store the objects corresponding to the given schema """output=[]w=output.appendforetypeinsorted(schema.entities()):eschema=schema.eschema(etype)ifeschema.finaloreschema.typeinskip_entities:continuestmts=dropeschema2sql(dbhelper,eschema,skip_relations,prefix=prefix)forstmtinstmts:w(stmt)forrtypeinsorted(schema.relations()):rschema=schema.rschema(rtype)ifrschema_has_table(rschema,skip_relations):w(droprschema2sql(rschema))return'\n'.join(output)defeschema_attrs(eschema,skip_relations):attrs=[attrdefforattrdefineschema.attribute_definitions()ifnotattrdef[0].typeinskip_relations]attrs+=[(rschema,None)forrschemaineschema.subject_relations()ifnotrschema.finalandrschema.inlined]returnattrsdefunique_index_name(eschema,columns):returnu'unique_%s'%md5((eschema.type+','+','.join(sorted(columns))).encode('ascii')).hexdigest()defiter_unique_index_names(eschema):forcolumnsineschema._unique_togetheror():yieldcolumns,unique_index_name(eschema,columns)defdropeschema2sql(dbhelper,eschema,skip_relations=(),prefix=''):"""return sql to drop an entity type's table"""# not necessary to drop indexes, that's implictly done when# dropping the table, but we need to drop SQLServer views used to# create multicol unique indicesstatements=[]tablename=prefix+eschema.typeifeschema._unique_togetherisnotNone:forcolumns,index_nameiniter_unique_index_names(eschema):cols=['%s%s'%(prefix,col)forcolincolumns]sqls=dbhelper.sqls_drop_multicol_unique_index(tablename,cols,index_name)statements+=sqlsstatements+=['DROP TABLE %s;'%(tablename)]returnstatementsdefeschema2sql(dbhelper,eschema,skip_relations=(),prefix=''):"""write an entity schema as SQL statements to stdout"""output=[]w=output.appendtable=prefix+eschema.typew('CREATE TABLE %s('%(table))attrs=eschema_attrs(eschema,skip_relations)# XXX handle objectinline physical modeforiinrange(len(attrs)):rschema,attrschema=attrs[i]ifattrschemaisnotNone:sqltype=aschema2sql(dbhelper,eschema,rschema,attrschema,indent=' ')else:# inline relationsqltype='integer REFERENCES entities (eid)'ifi==len(attrs)-1:w(' %s%s%s'%(prefix,rschema.type,sqltype))else:w(' %s%s%s,'%(prefix,rschema.type,sqltype))forrschema,aschemainattrs:ifaschemaisNone:# inline relationcontinueattr=rschema.typerdef=rschema.rdef(eschema.type,aschema.type)forconstraintinrdef.constraints:cstrname,check=check_constraint(eschema,aschema,attr,constraint,dbhelper,prefix=prefix)ifcstrnameisnotNone:w(', CONSTRAINT %s CHECK(%s)'%(cstrname,check))w(');')# create indexesforiinrange(len(attrs)):rschema,attrschema=attrs[i]ifattrschemaisNoneoreschema.rdef(rschema).indexed:w(dbhelper.sql_create_index(table,prefix+rschema.type))forcolumns,index_nameiniter_unique_index_names(eschema):cols=['%s%s'%(prefix,col)forcolincolumns]sqls=dbhelper.sqls_create_multicol_unique_index(table,cols,index_name)forsqlinsqls:w(sql)w('')return'\n'.join(output)defas_sql(value,dbhelper,prefix):ifisinstance(value,Attribute):returnprefix+value.attrelifisinstance(value,TODAY):returndbhelper.sql_current_date()elifisinstance(value,NOW):returndbhelper.sql_current_timestamp()else:# XXX more quoting for literals?returnvaluedefcheck_constraint(eschema,aschema,attr,constraint,dbhelper,prefix=''):# XXX should find a better namecstrname='cstr'+md5(eschema.type+attr+constraint.type()+(constraint.serialize()or'')).hexdigest()ifconstraint.type()=='BoundaryConstraint':value=as_sql(constraint.boundary,dbhelper,prefix)returncstrname,'%s%s%s%s'%(prefix,attr,constraint.operator,value)elifconstraint.type()=='IntervalBoundConstraint':condition=[]ifconstraint.minvalueisnotNone:value=as_sql(constraint.minvalue,dbhelper,prefix)condition.append('%s%s >= %s'%(prefix,attr,value))ifconstraint.maxvalueisnotNone:value=as_sql(constraint.maxvalue,dbhelper,prefix)condition.append('%s%s <= %s'%(prefix,attr,value))returncstrname,' AND '.join(condition)elifconstraint.type()=='StaticVocabularyConstraint':sample=next(iter(constraint.vocabulary()))ifnotisinstance(sample,string_types):values=', '.join(str(word)forwordinconstraint.vocabulary())else:# XXX better quoting?values=', '.join("'%s'"%word.replace("'","''")forwordinconstraint.vocabulary())returncstrname,'%s%s IN (%s)'%(prefix,attr,values)returnNone,Nonedefaschema2sql(dbhelper,eschema,rschema,aschema,creating=True,indent=''):"""write an attribute schema as SQL statements to stdout"""attr=rschema.typerdef=rschema.rdef(eschema.type,aschema.type)sqltype=type_from_constraints(dbhelper,aschema.type,rdef.constraints,creating)ifSET_DEFAULT:default=eschema.default(attr)ifdefaultisnotNone:ifaschema.type=='Boolean':sqltype+=' DEFAULT %s'%dbhelper.boolean_value(default)elifaschema.type=='String':sqltype+=' DEFAULT %r'%str(default)elifaschema.typein('Int','BigInt','Float'):sqltype+=' DEFAULT %s'%default# XXX ignore default for other type# this is expected for NOW / TODAYifcreating:ifrdef.uid:sqltype+=' PRIMARY KEY REFERENCES entities (eid)'elifrdef.cardinality[0]=='1':# don't set NOT NULL if backend isn't able to change it laterifdbhelper.alter_column_support:sqltype+=' NOT NULL'# else we're getting sql type to alter a column, we don't want key / indexes# / null modifiersreturnsqltypedeftype_from_constraints(dbhelper,etype,constraints,creating=True):"""return a sql type string corresponding to the constraints"""constraints=list(constraints)unique,sqltype=False,Nonesize_constrained_string=dbhelper.TYPE_MAPPING.get('SizeConstrainedString','varchar(%s)')ifetype=='String':forconstraintinconstraints:ifisinstance(constraint,SizeConstraint):ifconstraint.maxisnotNone:sqltype=size_constrained_string%constraint.maxelifisinstance(constraint,UniqueConstraint):unique=TrueifsqltypeisNone:sqltype=dbhelper.TYPE_MAPPING[etype]ifcreatingandunique:sqltype+=' UNIQUE'returnsqltype_SQL_SCHEMA="""CREATE TABLE %(table)s ( eid_from INTEGER NOT NULL REFERENCES entities (eid), eid_to INTEGER NOT NULL REFERENCES entities (eid), CONSTRAINT %(table)s_p_key PRIMARY KEY(eid_from, eid_to));CREATE INDEX %(table)s_from_idx ON %(table)s(eid_from);CREATE INDEX %(table)s_to_idx ON %(table)s(eid_to);"""defrschema2sql(rschema):assertnotrschema.rulereturn_SQL_SCHEMA%{'table':'%s_relation'%rschema.type}defdroprschema2sql(rschema):"""return sql to drop a relation type's table"""# not necessary to drop indexes, that's implictly done when dropping# the tablereturn'DROP TABLE %s_relation;'%rschema.typedefgrant_schema(schema,user,set_owner=True,skip_entities=(),prefix=''):"""write to the output stream a SQL schema to store the objects corresponding to the given schema """output=[]w=output.appendforetypeinsorted(schema.entities()):eschema=schema.eschema(etype)ifeschema.finaloretypeinskip_entities:continuew(grant_eschema(eschema,user,set_owner,prefix=prefix))forrtypeinsorted(schema.relations()):rschema=schema.rschema(rtype)ifrschema_has_table(rschema,skip_relations=()):# XXX skip_relations should be specifiedw(grant_rschema(rschema,user,set_owner))return'\n'.join(output)defgrant_eschema(eschema,user,set_owner=True,prefix=''):output=[]w=output.appendetype=eschema.typeifset_owner:w('ALTER TABLE %s%s OWNER TO %s;'%(prefix,etype,user))w('GRANT ALL ON %s%s TO %s;'%(prefix,etype,user))return'\n'.join(output)defgrant_rschema(rschema,user,set_owner=True):output=[]ifset_owner:output.append('ALTER TABLE %s_relation OWNER TO %s;'%(rschema.type,user))output.append('GRANT ALL ON %s_relation TO %s;'%(rschema.type,user))return'\n'.join(output)