server/schemahooks.py
branchtls-sprint
changeset 1263 01152fffd593
parent 1138 22f634977c95
parent 1251 af40e615dc89
child 1398 5fe84a5f7035
equal deleted inserted replaced
1246:76b3cd5d4f31 1263:01152fffd593
     4 - perform physical update on the source when necessary
     4 - perform physical update on the source when necessary
     5 
     5 
     6 checking for schema consistency is done in hooks.py
     6 checking for schema consistency is done in hooks.py
     7 
     7 
     8 :organization: Logilab
     8 :organization: Logilab
     9 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
     9 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
    10 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
    10 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
    11 """
    11 """
    12 __docformat__ = "restructuredtext en"
    12 __docformat__ = "restructuredtext en"
    13 
    13 
    14 from yams.schema import BASE_TYPES
    14 from yams.schema import BASE_TYPES
    15 from yams.buildobjs import EntityType, RelationType, RelationDefinition
    15 from yams.buildobjs import EntityType, RelationType, RelationDefinition
    16 from yams.schema2sql import eschema2sql, rschema2sql, _type_from_constraints
    16 from yams.schema2sql import eschema2sql, rschema2sql, _type_from_constraints
    17 
    17 
    18 from cubicweb import ValidationError, RepositoryError
    18 from cubicweb import ValidationError, RepositoryError
    19 from cubicweb.server import schemaserial as ss
    19 from cubicweb.server import schemaserial as ss
       
    20 from cubicweb.server.sqlutils import SQL_PREFIX
    20 from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation
    21 from cubicweb.server.pool import Operation, SingleLastOperation, PreCommitOperation
    21 from cubicweb.server.hookhelper import (entity_attr, entity_name,
    22 from cubicweb.server.hookhelper import (entity_attr, entity_name,
    22                                      check_internal_entity)
    23                                      check_internal_entity)
    23     
    24     
    24 # core entity and relation types which can't be removed
    25 # core entity and relation types which can't be removed
    42         constraints.append(cstr)
    43         constraints.append(cstr)
    43     return constraints
    44     return constraints
    44 
    45 
    45 def add_inline_relation_column(session, etype, rtype):
    46 def add_inline_relation_column(session, etype, rtype):
    46     """add necessary column and index for an inlined relation"""
    47     """add necessary column and index for an inlined relation"""
       
    48     table = SQL_PREFIX + etype
       
    49     column = SQL_PREFIX + rtype
    47     try:
    50     try:
    48         session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
    51         session.system_sql(str('ALTER TABLE %s ADD COLUMN %s integer'
    49                                % (etype, rtype)))
    52                                % (table, column)))
    50         session.info('added column %s to table %s', rtype, etype)
    53         session.info('added column %s to table %s', column, table)
    51     except:
    54     except:
    52         # silent exception here, if this error has not been raised because the 
    55         # silent exception here, if this error has not been raised because the 
    53         # column already exists, index creation will fail anyway
    56         # column already exists, index creation will fail anyway
    54         session.exception('error while adding column %s to table %s', etype, rtype)
    57         session.exception('error while adding column %s to table %s',
       
    58                           table, column)
    55     # create index before alter table which may expectingly fail during test
    59     # create index before alter table which may expectingly fail during test
    56     # (sqlite) while index creation should never fail (test for index existence
    60     # (sqlite) while index creation should never fail (test for index existence
    57     # is done by the dbhelper)
    61     # is done by the dbhelper)
    58     session.pool.source('system').create_index(session, etype, rtype)
    62     session.pool.source('system').create_index(session, table, column)
    59     session.info('added index on %s(%s)', etype, rtype)
    63     session.info('added index on %s(%s)', table, column)
    60     session.add_query_data('createdattrs', '%s.%s' % (etype, rtype))
    64     session.add_query_data('createdattrs', '%s.%s' % (etype, rtype))
    61 
    65 
    62 
    66 
    63 class SchemaOperation(Operation):
    67 class SchemaOperation(Operation):
    64     """base class for schema operations"""
    68     """base class for schema operations"""
   148     """
   152     """
   149     # final entities can't be deleted, don't care about that
   153     # final entities can't be deleted, don't care about that
   150     name = check_internal_entity(session, eid, CORE_ETYPES)
   154     name = check_internal_entity(session, eid, CORE_ETYPES)
   151     # delete every entities of this type
   155     # delete every entities of this type
   152     session.unsafe_execute('DELETE %s X' % name)
   156     session.unsafe_execute('DELETE %s X' % name)
   153     DropTableOp(session, table=name)
   157     DropTableOp(session, table=SQL_PREFIX + name)
   154     DeleteEETypeOp(session, name)
   158     DeleteEETypeOp(session, name)
   155 
   159 
   156 def after_del_eetype(session, eid):
   160 def after_del_eetype(session, eid):
   157     # workflow cleanup
   161     # workflow cleanup
   158     session.execute('DELETE State X WHERE NOT X state_of Y')
   162     session.execute('DELETE State X WHERE NOT X state_of Y')
   223     if (rschema.is_final() or rschema.inlined):
   227     if (rschema.is_final() or rschema.inlined):
   224         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
   228         rset = execute('Any COUNT(X) WHERE X is %s, X relation_type R, '
   225                        'R eid %%(x)s, X from_entity E, E name %%(name)s'
   229                        'R eid %%(x)s, X from_entity E, E name %%(name)s'
   226                        % rdeftype, {'x': rteid, 'name': str(subjschema)})
   230                        % rdeftype, {'x': rteid, 'name': str(subjschema)})
   227         if rset[0][0] == 0 and not subjschema.eid in pendings:
   231         if rset[0][0] == 0 and not subjschema.eid in pendings:
   228             DropColumnOp(session, table=subjschema.type, column=rschema.type)
   232             DropColumnOp(session, table=SQL_PREFIX + subjschema.type,
       
   233                          column=SQL_PREFIX + rschema.type)
   229     elif lastrel:
   234     elif lastrel:
   230         DropTableOp(session, table='%s_relation' % rschema.type)
   235         DropTableOp(session, table='%s_relation' % rschema.type)
   231     # if this is the last instance, drop associated relation type
   236     # if this is the last instance, drop associated relation type
   232     if lastrel and not rteid in pendings:
   237     if lastrel and not rteid in pendings:
   233         execute('DELETE ERType X WHERE X eid %(x)s', {'x': rteid}, 'x')
   238         execute('DELETE ERType X WHERE X eid %(x)s', {'x': rteid}, 'x')
   271     # but remove it before doing anything more dangerous...
   276     # but remove it before doing anything more dangerous...
   272     schema = session.repo.schema
   277     schema = session.repo.schema
   273     eschema = schema.add_entity_type(etype)
   278     eschema = schema.add_entity_type(etype)
   274     eschema.set_default_groups()
   279     eschema.set_default_groups()
   275     # generate table sql and rql to add metadata
   280     # generate table sql and rql to add metadata
   276     tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema)
   281     tablesql = eschema2sql(session.pool.source('system').dbhelper, eschema,
       
   282                            prefix=SQL_PREFIX)
   277     relrqls = []
   283     relrqls = []
   278     for rtype in ('is', 'is_instance_of', 'creation_date', 'modification_date',
   284     for rtype in ('is', 'is_instance_of', 'creation_date', 'modification_date',
   279                   'created_by', 'owned_by'):
   285                   'created_by', 'owned_by'):
   280         rschema = schema[rtype]
   286         rschema = schema[rtype]
   281         sampletype = rschema.subjects()[0]
   287         sampletype = rschema.subjects()[0]
   396             attrtype = attrtype.replace(' UNIQUE', '')
   402             attrtype = attrtype.replace(' UNIQUE', '')
   397         else:
   403         else:
   398             extra_unique_index = False
   404             extra_unique_index = False
   399         # added some str() wrapping query since some backend (eg psycopg) don't
   405         # added some str() wrapping query since some backend (eg psycopg) don't
   400         # allow unicode queries
   406         # allow unicode queries
       
   407         table = SQL_PREFIX + subj
       
   408         column = SQL_PREFIX + rtype
   401         try:
   409         try:
   402             session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
   410             session.system_sql(str('ALTER TABLE %s ADD COLUMN %s %s'
   403                                    % (subj, rtype, attrtype)))
   411                                    % (table, column, attrtype)))
   404             self.info('added column %s to table %s', rtype, subj)
   412             self.info('added column %s to table %s', table, column)
   405         except Exception, ex:
   413         except Exception, ex:
   406             # the column probably already exists. this occurs when
   414             # the column probably already exists. this occurs when
   407             # the entity's type has just been added or if the column
   415             # the entity's type has just been added or if the column
   408             # has not been previously dropped
   416             # has not been previously dropped
   409             self.error('error while altering table %s: %s', subj, ex)
   417             self.error('error while altering table %s: %s', table, ex)
   410         if extra_unique_index or entity.indexed:
   418         if extra_unique_index or entity.indexed:
   411             try:
   419             try:
   412                 sysource.create_index(session, subj, rtype,
   420                 sysource.create_index(session, table, column,
   413                                       unique=extra_unique_index)
   421                                       unique=extra_unique_index)
   414             except Exception, ex:
   422             except Exception, ex:
   415                 self.error('error while creating index for %s.%s: %s',
   423                 self.error('error while creating index for %s.%s: %s',
   416                            subj, rtype, ex)
   424                            table, column, ex)
   417         # postgres doesn't implement, so do it in two times
   425         # postgres doesn't implement, so do it in two times
   418         # ALTER TABLE %s ADD COLUMN %s %s SET DEFAULT %s
   426         # ALTER TABLE %s ADD COLUMN %s %s SET DEFAULT %s
   419         if default is not None:
   427         if default is not None:
   420             if isinstance(default, unicode):
   428             if isinstance(default, unicode):
   421                 default = default.encode(sysource.encoding)
   429                 default = default.encode(sysource.encoding)
   422             try:
   430             try:
   423                 session.system_sql('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT '
   431                 session.system_sql('ALTER TABLE %s ALTER COLUMN %s SET DEFAULT '
   424                                    '%%(default)s' % (subj, rtype),
   432                                    '%%(default)s' % (table, column),
   425                                    {'default': default})
   433                                    {'default': default})
   426             except Exception, ex:
   434             except Exception, ex:
   427                 # not supported by sqlite for instance
   435                 # not supported by sqlite for instance
   428                 self.error('error while altering table %s: %s', subj, ex)
   436                 self.error('error while altering table %s: %s', table, ex)
   429             session.system_sql('UPDATE %s SET %s=%%(default)s' % (subj, rtype),
   437             session.system_sql('UPDATE %s SET %s=%%(default)s' % (table, column),
   430                                {'default': default})
   438                                {'default': default})
   431         AddErdefOp(session, rdef)
   439         AddErdefOp(session, rdef)
   432 
   440 
   433 def after_add_efrdef(session, entity):
   441 def after_add_efrdef(session, entity):
   434     AddEFRDefPreCommitOp(session, entity=entity)
   442     AddEFRDefPreCommitOp(session, entity=entity)
   539     oldname = newname = None # make pylint happy
   547     oldname = newname = None # make pylint happy
   540 
   548 
   541     def precommit_event(self):
   549     def precommit_event(self):
   542         # we need sql to operate physical changes on the system database
   550         # we need sql to operate physical changes on the system database
   543         sqlexec = self.session.system_sql
   551         sqlexec = self.session.system_sql
   544         sqlexec('ALTER TABLE %s RENAME TO %s' % (self.oldname, self.newname))
   552         sqlexec('ALTER TABLE %s%s RENAME TO %s%s' % (SQL_PREFIX, self.oldname,
       
   553                                                      SQL_PREFIX, self.newname))
   545         self.info('renamed table %s to %s', self.oldname, self.newname)
   554         self.info('renamed table %s to %s', self.oldname, self.newname)
   546         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
   555         sqlexec('UPDATE entities SET type=%s WHERE type=%s',
   547                 (self.newname, self.oldname))
   556                 (self.newname, self.oldname))
   548         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
   557         sqlexec('UPDATE deleted_entities SET type=%s WHERE type=%s',
   549                 (self.newname, self.oldname))
   558                 (self.newname, self.oldname))
   557     rschema = values = None # make pylint happy
   566     rschema = values = None # make pylint happy
   558 
   567 
   559     def precommit_event(self):
   568     def precommit_event(self):
   560         if 'indexed' in self.values:
   569         if 'indexed' in self.values:
   561             sysource = self.session.pool.source('system')
   570             sysource = self.session.pool.source('system')
   562             table, column = self.kobj[0], self.rschema.type
   571             etype, rtype = self.kobj[0], self.rschema.type
       
   572             table = SQL_PREFIX + etype
       
   573             column = SQL_PREFIX + rtype
   563             if self.values['indexed']:
   574             if self.values['indexed']:
   564                 sysource.create_index(self.session, table, column)
   575                 sysource.create_index(self.session, table, column)
   565             else:
   576             else:
   566                 sysource.drop_index(self.session, table, column)
   577                 sysource.drop_index(self.session, table, column)
   567                 
   578                 
   568     def commit_event(self):
   579     def commit_event(self):
   569         # structure should be clean, not need to remove entity's relations
   580         # structure should be clean, not need to remove entity's relations
   570         # at this point
   581         # at this point
   571         self.rschema._rproperties[self.kobj].update(self.values)
   582         self.rschema._rproperties[self.kobj].update(self.values)
       
   583 
   572     
   584     
   573 def after_update_erdef(session, entity):
   585 def after_update_erdef(session, entity):
   574     desttype = entity.to_entity[0].name
   586     desttype = entity.to_entity[0].name
   575     rschema = session.repo.schema[entity.relation_type[0].name]
   587     rschema = session.repo.schema[entity.relation_type[0].name]
   576     newvalues = {}
   588     newvalues = {}
   601         if not entity.inlined_changed(inlined): # check in-lining is necessary/possible
   613         if not entity.inlined_changed(inlined): # check in-lining is necessary/possible
   602             return # nothing to do
   614             return # nothing to do
   603         # inlined changed, make necessary physical changes!
   615         # inlined changed, make necessary physical changes!
   604         sqlexec = self.session.system_sql
   616         sqlexec = self.session.system_sql
   605         rtype = rschema.type
   617         rtype = rschema.type
       
   618         eidcolumn = SQL_PREFIX + 'eid'
   606         if not inlined:
   619         if not inlined:
   607             # need to create the relation if it has not been already done by another
   620             # need to create the relation if it has not been already done by another
   608             # event of the same transaction
   621             # event of the same transaction
   609             if not rschema.type in session.query_data('createdtables', ()):
   622             if not rschema.type in session.query_data('createdtables', ()):
   610                 tablesql = rschema2sql(rschema)
   623                 tablesql = rschema2sql(rschema)
   612                 for sql in tablesql.split(';'):
   625                 for sql in tablesql.split(';'):
   613                     if sql.strip():
   626                     if sql.strip():
   614                         sqlexec(sql)
   627                         sqlexec(sql)
   615                 session.add_query_data('createdtables', rschema.type)
   628                 session.add_query_data('createdtables', rschema.type)
   616             # copy existant data
   629             # copy existant data
       
   630             column = SQL_PREFIX + rtype
   617             for etype in rschema.subjects():
   631             for etype in rschema.subjects():
   618                 sqlexec('INSERT INTO %s_relation SELECT eid, %s FROM %s WHERE NOT %s IS NULL'
   632                 table = SQL_PREFIX + str(etype)
   619                         % (rtype, rtype, etype, rtype))
   633                 sqlexec('INSERT INTO %s_relation SELECT %s, %s FROM %s WHERE NOT %s IS NULL'
       
   634                         % (rtype, eidcolumn, column, table, column))
   620             # drop existant columns
   635             # drop existant columns
   621             for etype in rschema.subjects():
   636             for etype in rschema.subjects():
   622                 DropColumnOp(session, table=str(etype), column=rtype)
   637                 DropColumnOp(session, table=SQL_PREFIX + str(etype),
       
   638                              column=SQL_PREFIX + rtype)
   623         else:
   639         else:
   624             for etype in rschema.subjects():
   640             for etype in rschema.subjects():
   625                 try:
   641                 try:
   626                     add_inline_relation_column(session, str(etype), rtype)                    
   642                     add_inline_relation_column(session, str(etype), rtype)                    
   627                 except Exception, ex:
   643                 except Exception, ex:
   633                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
   649                 # XXX don't use, it's not supported by sqlite (at least at when i tried it)
   634                 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
   650                 #sqlexec('UPDATE %(etype)s SET %(rtype)s=eid_to '
   635                 #        'FROM %(rtype)s_relation '
   651                 #        'FROM %(rtype)s_relation '
   636                 #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
   652                 #        'WHERE %(etype)s.eid=%(rtype)s_relation.eid_from'
   637                 #        % locals())
   653                 #        % locals())
   638                 cursor = sqlexec('SELECT eid_from, eid_to FROM %(etype)s, '
   654                 table = SQL_PREFIX + str(etype)
   639                                  '%(rtype)s_relation WHERE %(etype)s.eid='
   655                 cursor = sqlexec('SELECT eid_from, eid_to FROM %(table)s, '
       
   656                                  '%(rtype)s_relation WHERE %(table)s.%(eidcolumn)s='
   640                                  '%(rtype)s_relation.eid_from' % locals())
   657                                  '%(rtype)s_relation.eid_from' % locals())
   641                 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
   658                 args = [{'val': eid_to, 'x': eid} for eid, eid_to in cursor.fetchall()]
   642                 if args:
   659                 if args:
   643                     cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE eid=%%(x)s'
   660                     column = SQL_PREFIX + rtype
   644                                        % (etype, rtype), args)
   661                     cursor.executemany('UPDATE %s SET %s=%%(val)s WHERE %s=%%(x)s'
       
   662                                        % (table, column, eidcolumn), args)
   645                 # drop existant table
   663                 # drop existant table
   646                 DropTableOp(session, table='%s_relation' % rtype)
   664                 DropTableOp(session, table='%s_relation' % rtype)
   647 
   665 
   648     def commit_event(self):
   666     def commit_event(self):
   649         # structure should be clean, not need to remove entity's relations
   667         # structure should be clean, not need to remove entity's relations
   687         self.prepare_constraints(rtype, subjtype, objtype)
   705         self.prepare_constraints(rtype, subjtype, objtype)
   688         cstrtype = self.entity.type
   706         cstrtype = self.entity.type
   689         self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
   707         self.cstr = rtype.constraint_by_type(subjtype, objtype, cstrtype)
   690         self._cstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
   708         self._cstr = CONSTRAINTS[cstrtype].deserialize(self.entity.value)
   691         self._cstr.eid = self.entity.eid
   709         self._cstr.eid = self.entity.eid
       
   710         table = SQL_PREFIX + str(subjtype)
       
   711         column = SQL_PREFIX + str(rtype)
   692         # alter the physical schema on size constraint changes
   712         # alter the physical schema on size constraint changes
   693         if self._cstr.type() == 'SizeConstraint' and (
   713         if self._cstr.type() == 'SizeConstraint' and (
   694             self.cstr is None or self.cstr.max != self._cstr.max):
   714             self.cstr is None or self.cstr.max != self._cstr.max):
   695             try:
   715             try:
   696                 session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE VARCHAR(%s)'
   716                 session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE VARCHAR(%s)'
   697                                    % (subjtype, rtype, self._cstr.max))
   717                                    % (table, column, self._cstr.max))
   698                 self.info('altered column %s of table %s: now VARCHAR(%s)',
   718                 self.info('altered column %s of table %s: now VARCHAR(%s)',
   699                           rtype, subjtype, self._cstr.max)
   719                           column, table, self._cstr.max)
   700             except Exception, ex:
   720             except Exception, ex:
   701                 # not supported by sqlite for instance
   721                 # not supported by sqlite for instance
   702                 self.error('error while altering table %s: %s', subjtype, ex)
   722                 self.error('error while altering table %s: %s', table, ex)
   703         elif cstrtype == 'UniqueConstraint':
   723         elif cstrtype == 'UniqueConstraint':
   704             session.pool.source('system').create_index(
   724             session.pool.source('system').create_index(
   705                 self.session, str(subjtype), str(rtype), unique=True)
   725                 self.session, table, column, unique=True)
   706         
   726         
   707     def commit_event(self):
   727     def commit_event(self):
   708         if self.cancelled:
   728         if self.cancelled:
   709             return
   729             return
   710         # in-place removing
   730         # in-place removing
   711         if not self.cstr is None:
   731         if not self.cstr is None:
   712             self.constraints.remove(self.cstr)
   732             self.constraints.remove(self.cstr)
   713         self.constraints.append(self._cstr)
   733         self.constraints.append(self._cstr)
   714 
   734 
       
   735 
   715 def after_add_econstraint(session, entity):
   736 def after_add_econstraint(session, entity):
   716     ConstraintOp(session, entity=entity)
   737     ConstraintOp(session, entity=entity)
   717 
   738 
   718 def after_update_econstraint(session, entity):
   739 def after_update_econstraint(session, entity):
   719     ConstraintOp(session, entity=entity)
   740     ConstraintOp(session, entity=entity)
       
   741 
   720 
   742 
   721 class DelConstraintOp(ConstraintOp):
   743 class DelConstraintOp(ConstraintOp):
   722     """actually remove a constraint of a relation definition"""
   744     """actually remove a constraint of a relation definition"""
   723     rtype = subjtype = objtype = None # make pylint happy
   745     rtype = subjtype = objtype = None # make pylint happy
   724     
   746     
   725     def precommit_event(self):
   747     def precommit_event(self):
   726         self.prepare_constraints(self.rtype, self.subjtype, self.objtype)
   748         self.prepare_constraints(self.rtype, self.subjtype, self.objtype)
   727         cstrtype = self.cstr.type()
   749         cstrtype = self.cstr.type()
       
   750         table = SQL_PREFIX + str(self.subjtype)
       
   751         column = SQL_PREFIX + str(self.rtype)
   728         # alter the physical schema on size/unique constraint changes
   752         # alter the physical schema on size/unique constraint changes
   729         if cstrtype == 'SizeConstraint':
   753         if cstrtype == 'SizeConstraint':
   730             try:
   754             try:
   731                 self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
   755                 self.session.system_sql('ALTER TABLE %s ALTER COLUMN %s TYPE TEXT'
   732                                         % (self.subjtype, self.rtype))
   756                                         % (table, column))
   733                 self.info('altered column %s of table %s: now TEXT', 
   757                 self.info('altered column %s of table %s: now TEXT', 
   734                           self.rtype,  self.subjtype)
   758                           column, table)
   735             except Exception, ex:
   759             except Exception, ex:
   736                 # not supported by sqlite for instance
   760                 # not supported by sqlite for instance
   737                 self.error('error while altering table %s: %s', 
   761                 self.error('error while altering table %s: %s', table, ex)
   738                            self.subjtype, ex)
       
   739         elif cstrtype == 'UniqueConstraint':
   762         elif cstrtype == 'UniqueConstraint':
   740             self.session.pool.source('system').drop_index(
   763             self.session.pool.source('system').drop_index(
   741                 self.session, str(self.subjtype), str(self.rtype), unique=True)
   764                 self.session, table, column, unique=True)
   742                 
   765                 
   743     def commit_event(self):
   766     def commit_event(self):
   744         self.constraints.remove(self.cstr)
   767         self.constraints.remove(self.cstr)
   745 
   768 
   746 
   769