This works only on newly created fields over MySQL. Moving fields around on 
your model wont reorder them. 

Version 2.15.3-stable+timestamp.2017.08.07.12.51.45
migrator.py line 364
                    field_position = sql_fields[key]['sortable']
                    after = "FIRST"
                    if field_position > 1:
                        for f in sql_fields:
                            if sql_fields[f]['sortable'] == field_position - 
1:
                                after = "AFTER `%s`" % f
                                break
                    query = ['ALTER TABLE %s ADD %s %s %s;' % (
                        table._rname, sql_fields[key]['rname'],
                        sql_fields_aux[key]['sql'].replace(', ', new_add), 
after)]

Theres no way to implement column reorder because the db is not being asked 
for the table schema, this information is read from the migration files. 
This function only checks whther the fields on your model are in the 
migration files, in no particular order. It would be nice if there were a 
way.

-- 
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
--- 
You received this message because you are subscribed to the Google Groups 
"web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/d/optout.
import copy
import datetime
import locale
import os
import pickle
import sys
from ._compat import PY2, string_types, pjoin, iteritems, to_bytes, exists
from ._load import portalocker
from .helpers.classes import SQLCustomType, DatabaseStoredFile


class Migrator(object):
	def __init__(self, adapter):
		self.adapter = adapter

	@property
	def db(self):
		return self.adapter.db

	@property
	def dialect(self):
		return self.adapter.dialect

	@property
	def dbengine(self):
		return self.adapter.dbengine

	def create_table(self, table, migrate=True, fake_migrate=False,
					 polymodel=None):
		db = table._db
		fields = []
		# PostGIS geo fields are added after the table has been created
		postcreation_fields = []
		sql_fields = {}
		sql_fields_aux = {}
		TFK = {}
		tablename = table._tablename
		types = self.adapter.types
		for sortable, field in enumerate(table, start=1):
			field_name = field.name
			field_type = field.type
			if isinstance(field_type, SQLCustomType):
				ftype = field_type.native or field_type.type
			elif field_type.startswith(('reference', 'big-reference')):
				if field_type.startswith('reference'):
					referenced = field_type[10:].strip()
					type_name = 'reference'
				else:
					referenced = field_type[14:].strip()
					type_name = 'big-reference'

				if referenced == '.':
					referenced = tablename
				constraint_name = self.dialect.constraint_name(
					table._raw_rname, field._raw_rname)
				# if not '.' in referenced \
				#         and referenced != tablename \
				#         and hasattr(table,'_primarykey'):
				#     ftype = types['integer']
				#else:
				try:
					rtable = db[referenced]
					rfield = rtable._id
					rfieldname = rfield.name
					rtablename = referenced
				except (KeyError, ValueError, AttributeError) as e:
					self.db.logger.debug('Error: %s' % e)
					try:
						rtablename, rfieldname = referenced.split('.')
						rtable = db[rtablename]
						rfield = rtable[rfieldname]
					except Exception as e:
						self.db.logger.debug('Error: %s' % e)
						raise KeyError(
							'Cannot resolve reference %s in %s definition' % (
								referenced, table._tablename))

				# must be PK reference or unique
				if getattr(rtable, '_primarykey', None) and rfieldname in \
				   rtable._primarykey or rfield.unique:
					ftype = types[rfield.type[:9]] % \
						dict(length=rfield.length)
					# multicolumn primary key reference?
					if not rfield.unique and len(rtable._primarykey) > 1:
						# then it has to be a table level FK
						if rtablename not in TFK:
							TFK[rtablename] = {}
						TFK[rtablename][rfieldname] = field_name
					else:
						fk = rtable._rname + ' (' + rfield._rname + ')'
						ftype = ftype + \
							types['reference FK'] % dict(
								# should be quoted
								constraint_name=constraint_name,
								foreign_key=fk,
								table_name=table._rname,
								field_name=field._rname,
								on_delete_action=field.ondelete)
				else:
					# make a guess here for circular references
					if referenced in db:
						id_fieldname = db[referenced]._id._rname
					elif referenced == tablename:
						id_fieldname = table._id._rname
					else:  # make a guess
						id_fieldname = self.dialect.quote('id')
					#gotcha: the referenced table must be defined before
					#the referencing one to be able to create the table
					#Also if it's not recommended, we can still support
					#references to tablenames without rname to make
					#migrations and model relationship work also if tables
					#are not defined in order
					if referenced == tablename:
						real_referenced = db[referenced]._rname
					else:
						real_referenced = (
							referenced in db and db[referenced]._rname or
							referenced)
					rfield = db[referenced]._id
					ftype_info = dict(
						index_name=self.dialect.quote(field._raw_rname+'__idx'),
						field_name=field._rname,
						constraint_name=self.dialect.quote(constraint_name),
						foreign_key='%s (%s)' % (
							real_referenced, rfield._rname),
						on_delete_action=field.ondelete)
					ftype_info['null'] = ' NOT NULL' if field.notnull else \
						self.dialect.allow_null
					ftype_info['unique'] = ' UNIQUE' if field.unique else ''
					ftype = types[type_name] % ftype_info
			elif field_type.startswith('list:reference'):
				ftype = types[field_type[:14]]
			elif field_type.startswith('decimal'):
				precision, scale = map(int, field_type[8:-1].split(','))
				ftype = types[field_type[:7]] % \
					dict(precision=precision, scale=scale)
			elif field_type.startswith('geo'):
				if not hasattr(self.adapter, 'srid'):
					raise RuntimeError('Adapter does not support geometry')
				srid = self.adapter.srid
				geotype, parms = field_type[:-1].split('(')
				if geotype not in types:
					raise SyntaxError(
						'Field: unknown field type: %s for %s' % (
							field_type, field_name))
				ftype = types[geotype]
				if self.dbengine == 'postgres' and geotype == 'geometry':
					if self.db._ignore_field_case is True:
						field_name = field_name.lower()
					# parameters: schema, srid, dimension
					dimension = 2  # GIS.dimension ???
					parms = parms.split(',')
					if len(parms) == 3:
						schema, srid, dimension = parms
					elif len(parms) == 2:
						schema, srid = parms
					else:
						schema = parms[0]
					ftype = "SELECT AddGeometryColumn ('%%(schema)s', '%%(tablename)s', '%%(fieldname)s', %%(srid)s, '%s', %%(dimension)s);" % types[geotype]
					ftype = ftype % dict(schema=schema,
										 tablename=table._raw_rname,
										 fieldname=field._raw_rname, srid=srid,
										 dimension=dimension)
					postcreation_fields.append(ftype)
			elif field_type not in types:
				raise SyntaxError('Field: unknown field type: %s for %s' % (
					field_type, field_name))
			else:
				ftype = types[field_type] % {'length': field.length}

			if not field_type.startswith(('id', 'reference', 'big-reference')):
				if field.notnull:
					ftype += ' NOT NULL'
				else:
					ftype += self.dialect.allow_null
				if field.unique:
					ftype += ' UNIQUE'
				if field.custom_qualifier:
					ftype += ' %s' % field.custom_qualifier

			# add to list of fields
			sql_fields[field_name] = dict(
				length=field.length,
				unique=field.unique,
				notnull=field.notnull,
				sortable=sortable,
				type=str(field_type),
				sql=ftype,
				rname=field._rname,
				raw_rname=field._raw_rname)

			if field.notnull and field.default is not None:
				# Caveat: sql_fields and sql_fields_aux
				# differ for default values.
				# sql_fields is used to trigger migrations and sql_fields_aux
				# is used for create tables.
				# The reason is that we do not want to trigger
				# a migration simply because a default value changes.
				not_null = self.dialect.not_null(field.default, field_type)
				ftype = ftype.replace('NOT NULL', not_null)
			sql_fields_aux[field_name] = dict(sql=ftype)
			# Postgres - PostGIS:
			# geometry fields are added after the table has been created, not now
			if not (self.dbengine == 'postgres' and
			   field_type.startswith('geom')):
				fields.append('%s %s' % (field._rname, ftype))
		other = ';'

		# backend-specific extensions to fields
		if self.dbengine == 'mysql':
			if not hasattr(table, "_primarykey"):
				fields.append('PRIMARY KEY (%s)' % (table._id._rname))
			engine = self.adapter.adapter_args.get('engine', 'InnoDB')
			other = ' ENGINE=%s CHARACTER SET utf8;' % engine

		fields = ',\n    '.join(fields)
		for rtablename in TFK:
			rtable = db[rtablename]
			rfields = TFK[rtablename]
			pkeys = [rtable[pk]._rname for pk in rtable._primarykey]
			fk_fields = [table[rfields[k]] for k in rtable._primarykey]
			fkeys = [f._rname for f in fk_fields]
			constraint_name = self.dialect.constraint_name(
				table._raw_rname, '_'.join(f._raw_rname for f in fk_fields))
			on_delete = list(set(f.ondelete for f in fk_fields))
			if len(on_delete) > 1:
				raise SyntaxError('Table %s has incompatible ON DELETE actions in multi-field foreign key.' % table._dalname)
			fields = fields + ',\n    ' + \
				types['reference TFK'] % dict(
					constraint_name=constraint_name,
					table_name=table._rname,
					field_name=', '.join(fkeys),
					foreign_table=rtable._rname,
					foreign_key=', '.join(pkeys),
					on_delete_action=on_delete[0])

		if getattr(table, '_primarykey', None):
			query = "CREATE TABLE %s(\n    %s,\n    %s) %s" % \
				(table._rname, fields,
				 self.dialect.primary_key(', '.join([
					table[pk]._rname
					for pk in table._primarykey])), other)
		else:
			query = "CREATE TABLE %s(\n    %s\n)%s" % \
				(table._rname, fields, other)

		uri = self.adapter.uri
		if uri.startswith('sqlite:///') \
				or uri.startswith('spatialite:///'):
			if PY2:
				path_encoding = sys.getfilesystemencoding() \
					or locale.getdefaultlocale()[1] or 'utf8'
				dbpath = uri[9:uri.rfind('/')].decode(
					'utf8').encode(path_encoding)
			else:
				dbpath = uri[9:uri.rfind('/')]
		else:
			dbpath = self.adapter.folder

		if not migrate:
			return query
		elif uri.startswith('sqlite:memory') or \
				uri.startswith('spatialite:memory'):
			table._dbt = None
		elif isinstance(migrate, string_types):
			table._dbt = pjoin(dbpath, migrate)
		else:
			table._dbt = pjoin(
				dbpath, '%s_%s.table' % (db._uri_hash, tablename))

		if not table._dbt or not self.file_exists(table._dbt):
			if table._dbt:
				self.log('timestamp: %s\n%s\n'
						 % (datetime.datetime.today().isoformat(),
							query), table)
			if not fake_migrate:
				self.adapter.create_sequence_and_triggers(query, table)
				db.commit()
				# Postgres geom fields are added now,
				# after the table has been created
				for query in postcreation_fields:
					self.adapter.execute(query)
					db.commit()
			if table._dbt:
				tfile = self.file_open(table._dbt, 'wb')
				pickle.dump(sql_fields, tfile)
				self.file_close(tfile)
				if fake_migrate:
					self.log('faked!\n', table)
				else:
					self.log('success!\n', table)
		else:
			tfile = self.file_open(table._dbt, 'rb')
			try:
				sql_fields_old = pickle.load(tfile)
			except EOFError:
				self.file_close(tfile)
				raise RuntimeError('File %s appears corrupted' % table._dbt)
			self.file_close(tfile)
			# add missing rnames
			for key, item in sql_fields_old.items():
				tmp = sql_fields.get(key)
				if tmp:
					item.setdefault('rname', tmp['rname'])
					item.setdefault('raw_rname', tmp['raw_rname'])
				else:
					item.setdefault('rname', self.dialect.quote(key))
					item.setdefault('raw_rname', key)
			if sql_fields != sql_fields_old:
				self.migrate_table(
					table,
					sql_fields, sql_fields_old,
					sql_fields_aux, None,
					fake_migrate=fake_migrate
					)
		return query

	def _fix(self, item):
		k, v = item
		if not isinstance(v, dict):
			v = dict(type='unknown', sql=v)
		if self.db._ignore_field_case is not True:
			return k, v
		return k.lower(), v

	def migrate_table(self, table, sql_fields, sql_fields_old, sql_fields_aux,
					  logfile, fake_migrate=False):
		# logfile is deprecated (moved to adapter.log method)
		db = table._db
		db._migrated.append(table._tablename)
		tablename = table._tablename
		if self.dbengine in ('firebird',):
			drop_expr = 'ALTER TABLE %s DROP %s;'
		else:
			drop_expr = 'ALTER TABLE %s DROP COLUMN %s;'
		field_types = dict((x.lower(), table[x].type)
			for x in sql_fields.keys() if x in table)
		# make sure all field names are lower case to avoid
		# migrations because of case change
		sql_fields = dict(map(self._fix, iteritems(sql_fields)))
		sql_fields_old = dict(map(self._fix, iteritems(sql_fields_old)))
		sql_fields_aux = dict(map(self._fix, iteritems(sql_fields_aux)))
		if db._debug:
			db.logger.debug('migrating %s to %s' % (
				sql_fields_old, sql_fields))

		keys = list(sql_fields.keys())
		for key in sql_fields_old:
			if key not in keys:
				keys.append(key)
		new_add = self.dialect.concat_add(table._rname)

		metadata_change = False
		sql_fields_current = copy.copy(sql_fields_old)
		for key in keys:
			query = None
			if key not in sql_fields_old:
				sql_fields_current[key] = sql_fields[key]
				if self.dbengine in ('postgres',) and \
				   sql_fields[key]['type'].startswith('geometry'):
					# 'sql' == ftype in sql
					query = [sql_fields[key]['sql']]
				else:
					field_position = sql_fields[key]['sortable']
					after = "FIRST"
					if field_position > 1:
						for f in sql_fields:
							if sql_fields[f]['sortable'] == field_position - 1:
								after = "AFTER `%s`" % f
								break
					query = ['ALTER TABLE %s ADD %s %s %s;' % (
						table._rname, sql_fields[key]['rname'],
						sql_fields_aux[key]['sql'].replace(', ', new_add), after)]
				metadata_change = True
			elif self.dbengine in ('sqlite', 'spatialite'):
				if key in sql_fields:
					sql_fields_current[key] = sql_fields[key]
					# Field rname has changes, add new column
					if (sql_fields[key]['raw_rname'].lower() !=
					   sql_fields_old[key]['raw_rname'].lower()):
						tt = sql_fields_aux[key]['sql'].replace(', ', new_add)
					query = ['ALTER TABLE %s ADD %s %s;' % (
								table._rname, sql_fields[key]['rname'], tt),
							'UPDATE %s SET %s=%s;' % (
								table._rname, sql_fields[key]['rname'],
								sql_fields_old[key]['rname'])]
				metadata_change = True
			elif key not in sql_fields:
				del sql_fields_current[key]
				ftype = sql_fields_old[key]['type']
				if self.dbengine == 'postgres' and \
				   ftype.startswith('geometry'):
					geotype, parms = ftype[:-1].split('(')
					schema = parms.split(',')[0]
					query = ["SELECT DropGeometryColumn ('%(schema)s', \
							 '%(table)s', '%(field)s');" % dict(
								schema=schema, table=table._raw_rname,
								field=sql_fields_old[key]['raw_rname'])]
				else:
					query = [drop_expr % (
						table._rname, sql_fields_old[key]['rname'])]
				metadata_change = True
			# The field has a new rname, temp field is not needed
			elif (sql_fields[key]['raw_rname'].lower() !=
			   sql_fields_old[key]['raw_rname'].lower()):
				sql_fields_current[key] = sql_fields[key]
				tt = sql_fields_aux[key]['sql'].replace(', ', new_add)
				query = [
					'ALTER TABLE %s ADD %s %s;' % (
						table._rname, sql_fields[key]['rname'], tt),
					'UPDATE %s SET %s=%s;' % (
						table._rname, sql_fields[key]['rname'],
						sql_fields_old[key]['rname']),
					drop_expr % (table._rname, sql_fields_old[key]['rname']),
				]
				metadata_change = True
			elif (
			   sql_fields[key]['sql'] != sql_fields_old[key]['sql'] and not
			   isinstance(field_types.get(key), SQLCustomType) and not
			   sql_fields[key]['type'].startswith('reference') and not
			   sql_fields[key]['type'].startswith('double') and not
			   sql_fields[key]['type'].startswith('id')):
				sql_fields_current[key] = sql_fields[key]
				tt = sql_fields_aux[key]['sql'].replace(', ', new_add)
				key_tmp = self.dialect.quote(key + '__tmp')
				query = [
					'ALTER TABLE %s ADD %s %s;' % (table._rname, key_tmp, tt),
					'UPDATE %s SET %s=%s;' % (
						table._rname, key_tmp, sql_fields_old[key]['rname']),
					drop_expr % (table._rname, sql_fields_old[key]['rname']),
					'ALTER TABLE %s ADD %s %s;' % (
						table._rname, sql_fields[key]['rname'], tt),
					'UPDATE %s SET %s=%s;' % (
						table._rname, sql_fields[key]['rname'], key_tmp),
					drop_expr % (table._rname, key_tmp)
				]
				metadata_change = True
			elif sql_fields[key] != sql_fields_old[key]:
				sql_fields_current[key] = sql_fields[key]
				metadata_change = True

			if query:
				self.log(
					'timestamp: %s\n' % datetime.datetime.today().isoformat(),
					table)
				for sub_query in query:
					self.log(sub_query + '\n', table)
					if fake_migrate:
						if db._adapter.commit_on_alter_table:
							self.save_dbt(table, sql_fields_current)
						self.log('faked!\n', table)
					else:
						self.adapter.execute(sub_query)
						# Caveat: mysql, oracle and firebird
						# do not allow multiple alter table
						# in one transaction so we must commit
						# partial transactions and
						# update table._dbt after alter table.
						if db._adapter.commit_on_alter_table:
							db.commit()
							self.save_dbt(table, sql_fields_current)
							self.log('success!\n', table)

			elif metadata_change:
				self.save_dbt(table, sql_fields_current)

		if metadata_change and not (
		   query and db._adapter.commit_on_alter_table):
			db.commit()
			self.save_dbt(table, sql_fields_current)
			self.log('success!\n', table)

	def save_dbt(self, table, sql_fields_current):
		tfile = self.file_open(table._dbt, 'wb')
		pickle.dump(sql_fields_current, tfile)
		self.file_close(tfile)

	def log(self, message, table=None):
		isabs = None
		logfilename = self.adapter.adapter_args.get('logfile', 'sql.log')
		writelog = bool(logfilename)
		if writelog:
			isabs = os.path.isabs(logfilename)
		if table and table._dbt and writelog and self.adapter.folder:
			if isabs:
				table._loggername = logfilename
			else:
				table._loggername = pjoin(self.adapter.folder, logfilename)
			logfile = self.file_open(table._loggername, 'ab')
			logfile.write(to_bytes(message))
			self.file_close(logfile)

	@staticmethod
	def file_open(filename, mode='rb', lock=True):
		#to be used ONLY for files that on GAE may not be on filesystem
		if lock:
			fileobj = portalocker.LockedFile(filename, mode)
		else:
			fileobj = open(filename, mode)
		return fileobj

	@staticmethod
	def file_close(fileobj):
		#to be used ONLY for files that on GAE may not be on filesystem
		if fileobj:
			fileobj.close()

	@staticmethod
	def file_delete(filename):
		os.unlink(filename)

	@staticmethod
	def file_exists(filename):
		#to be used ONLY for files that on GAE may not be on filesystem
		return exists(filename)


class InDBMigrator(Migrator):
	def file_exists(self, filename):
		return DatabaseStoredFile.exists(self.db, filename)

	def file_open(self, filename, mode='rb', lock=True):
		return DatabaseStoredFile(self.db, filename, mode)

	@staticmethod
	def file_close(fileobj):
		fileobj.close_connection()

	def file_delete(self, filename):
		query = "DELETE FROM web2py_filesystem WHERE path='%s'" % filename
		self.db.executesql(query)
		self.db.commit()

Reply via email to