[SQL-CVS] r770 - trunk/SQLObject/sqlobject/manager
SQLObject is a Python ORM.
Brought to you by:
ianbicking,
phd
|
From: <sub...@co...> - 2005-05-05 23:00:05
|
Author: ianb
Date: 2005-05-05 22:59:56 +0000 (Thu, 05 May 2005)
New Revision: 770
Modified:
trunk/SQLObject/sqlobject/manager/command.py
Log:
Added record command, to record the database status at a certain point in time
Modified: trunk/SQLObject/sqlobject/manager/command.py
===================================================================
--- trunk/SQLObject/sqlobject/manager/command.py 2005-05-05 22:58:15 UTC (rev 769)
+++ trunk/SQLObject/sqlobject/manager/command.py 2005-05-05 22:59:56 UTC (rev 770)
@@ -1,6 +1,7 @@
#!/usr/bin/env python
import optparse
import fnmatch
+import re
import os
import sys
try:
@@ -8,11 +9,61 @@
from paste import CONFIG
except ImportError:
pyconfig = None
+ CONFIG = {}
+import time
import sqlobject
+from sqlobject import col
from sqlobject.util import moduleloader
from sqlobject.declarative import DeclarativeMeta
+class SQLObjectVersionTable(sqlobject.SQLObject):
+ """
+ This table is used to store information about the database and
+ its version (used with record and update commands).
+ """
+ class sqlmeta:
+ table = 'sqlobject_db_version'
+ version = col.StringCol()
+ updated = col.DateTimeCol(default=col.DateTimeCol.now)
+
+def db_differences(soClass, conn):
+ """
+ Returns the differences between a class and the table in a
+ connection. Returns [] if no differences are found. This
+ function does the best it can; it can miss many differences.
+ """
+ # @@: Repeats a lot from CommandStatus.command, but it's hard
+ # to actually factor out the display logic. Or I'm too lazy
+ # to do so.
+ diffs = []
+ if not conn.tableExists(soClass.sqlmeta.table):
+ diffs.append('Does not exist in database')
+ else:
+ try:
+ columns = conn.columnsFromSchema(soClass.sqlmeta.table,
+ soClass)
+ except AttributeError:
+ # Database does not support reading columns
+ pass
+ else:
+ existing = {}
+ for col in columns:
+ col = col.withClass(soClass)
+ existing[col.dbName] = col
+ missing = {}
+ for col in soClass.sqlmeta._columns:
+ if existing.has_key(col.dbName):
+ del existing[col.dbName]
+ else:
+ missing[col.dbName] = col
+ for col in existing.values():
+ diffs.append('Database has extra column: %s'
+ % col.dbName)
+ for col in missing.values():
+ diffs.append('Database missing column: %s' % col.dbName)
+ return diffs
+
class CommandRunner(object):
def __init__(self):
@@ -254,6 +305,15 @@
return response[0].lower() == 'y'
print 'Y or N please'
+ def shorten_filename(self, fn):
+ """
+ Shortens a filename to make it relative to the current
+ directory (if it can). For display purposes.
+ """
+ if fn.startswith(os.getcwd() + '/'):
+ fn = fn[len(os.getcwd())+1:]
+ return fn
+
class CommandSQL(Command):
name = 'sql'
@@ -499,7 +559,225 @@
sys.stdout.write("%r\t" % col)
sys.stdout.write("\n")
print
-
+
+class CommandRecord(Command):
+
+ name = 'record'
+ summary = 'Record state of table definitions'
+ parser = standard_parser()
+ parser.add_option('--output-dir',
+ help="Base directory for recorded definitions",
+ dest="output_dir",
+ metavar="DIR",
+ default=None)
+ parser.add_option('--no-db-record',
+ help="Don't record version to database",
+ dest="db_record",
+ action="store_false",
+ default=True)
+ parser.add_option('--force-create',
+ help="Create a new version even if appears to be "
+ "identical to the last version",
+ action="store_true",
+ dest="force_create")
+ parser.add_option('--name',
+ help="The name to append to the version. The "
+ "version should sort after previous versions (so "
+ "any versions from the same day should come "
+ "alphabetically before this version).",
+ dest="version_name",
+ metavar="NAME")
+ parser.add_option('--force-db-version',
+ help="Update the database version, and include no "
+ "database information. This is for databases that "
+ "were developed without any interaction with "
+ "this tool, to create a 'beginning' revision.",
+ metavar="VERSION_NAME",
+ dest="force_db_version")
+
+ version_regex = re.compile(r'^\d\d\d\d-\d\d-\d\d')
+
+ def command(self):
+ if self.options.force_db_version:
+ self.command_force_db_version()
+ return
+
+ v = self.options.verbose
+ sim = self.options.simulate
+ classes = self.classes()
+ if not classes:
+ print "No classes found!"
+ return
+
+ output_dir = self.find_output_dir()
+ version = os.path.basename(output_dir)
+ print "Creating version %s" % version
+ conns = []
+ files = {}
+ for cls in self.classes():
+ dbName = cls._connection.dbName
+ if cls._connection not in conns:
+ conns.append(cls._connection)
+ fn = os.path.join(cls.__name__
+ + '_' + dbName + '.sql')
+ if sim:
+ continue
+ files[fn] = ''.join([
+ '-- Exported definition from %s\n'
+ % time.strftime('%Y-%m-%dT%H:%M:%S'),
+ '-- Class %s.%s\n'
+ % (cls.__module__, cls.__name__),
+ '-- Database: %s\n'
+ % dbName,
+ cls.createTableSQL().strip(),
+ '\n'])
+ last_version_dir = self.find_last_version()
+ if last_version_dir and not self.options.force_create:
+ if v > 1:
+ print "Checking %s to see if it is current" % last_version_dir
+ files_copy = files.copy()
+ for fn in os.listdir(last_version_dir):
+ if not fn.endswith('.sql'):
+ continue
+ if not files_copy.has_key(fn):
+ if v > 1:
+ print "Missing file %s" % fn
+ break
+ f = open(os.path.join(last_version_dir, fn), 'r')
+ content = f.read()
+ f.close()
+ if (self.strip_comments(files_copy[fn])
+ != self.strip_comments(content)):
+ if v > 1:
+ print "Content does not match: %s" % fn
+ break
+ del files_copy[fn]
+ else:
+ # No differences so far
+ if not files_copy:
+ # Used up all files
+ print ("Current status matches version %s"
+ % os.path.basename(last_version_dir))
+ return
+ if v > 1:
+ print "Extra files: %s" % ', '.join(files_copy.keys())
+ if v:
+ print ("Current state does not match %s"
+ % os.path.basename(last_version_dir))
+ if v > 1 and not last_version_dir:
+ print "No last version to check"
+ if not sim:
+ os.mkdir(output_dir)
+ if v:
+ print 'Making directory %s' % self.shorten_filename(output_dir)
+ files = files.items()
+ files.sort()
+ for fn, content in files:
+ if v:
+ print ' Writing %s' % self.shorten_filename(fn)
+ if not sim:
+ f = open(os.path.join(output_dir, fn), 'w')
+ f.write(content)
+ f.close()
+ all_diffs = []
+ for cls in self.classes():
+ for conn in conns:
+ diffs = db_differences(cls, conn)
+ for diff in diffs:
+ if len(conns) > 1:
+ diff = ' (%s).%s: %s' % (
+ conn.uri(), cls.sqlmeta.table, diff)
+ else:
+ diff = ' %s: %s' % (cls.sqlmeta.table, diff)
+ all_diffs.append(diff)
+ if all_diffs:
+ print 'Database does not match schema:'
+ print '\n'.join(all_diffs)
+ if self.options.db_record:
+ print '(Not updating database version)'
+ elif self.options.db_record:
+ for conn in conns:
+ self.update_db(version, conn)
+
+ def update_db(self, version, conn):
+ v = self.options.verbose
+ if not conn.tableExists(SQLObjectVersionTable.sqlmeta.table):
+ if v:
+ print ('Creating table %s'
+ % SQLObjectVersionTable.sqlmeta.table)
+ sql = SQLObjectVersionTable.createTableSQL(connection=conn)
+ if v > 1:
+ print sql
+ if not self.options.simulate:
+ SQLObjectVersionTable.createTable(connection=conn)
+ if not self.options.simulate:
+ SQLObjectVersionTable.clearTable(connection=conn)
+ SQLObjectVersionTable(
+ version=version,
+ connection=conn)
+
+ def strip_comments(self, sql):
+ lines = [l for l in sql.splitlines()
+ if not l.strip().startswith('--')]
+ return '\n'.join(lines)
+
+ def base_dir(self):
+ base = self.options.output_dir
+ if base is None:
+ base = CONFIG.get('sqlobject_history_dir', '.')
+ return base
+
+ def find_output_dir(self):
+ today = time.strftime('%Y-%m-%d', time.localtime())
+ if self.options.version_name:
+ dir = os.path.join(self.base_dir(), today + '-' +
+ self.options.version_name)
+ if os.path.exists(dir):
+ print ("Error, directory already exists: %s"
+ % dir)
+ sys.exit(1)
+ return dir
+ extra = ''
+ while 1:
+ dir = os.path.join(self.base_dir(), today + extra)
+ if not os.path.exists(dir):
+ return dir
+ if not extra:
+ extra = 'a'
+ else:
+ extra = chr(ord(extra)+1)
+
+ def find_last_version(self):
+ names = []
+ for fn in os.listdir(self.base_dir()):
+ if not self.version_regex.search(fn):
+ continue
+ names.append(fn)
+ if not names:
+ return None
+ names.sort()
+ return os.path.join(self.base_dir(), names[-1])
+
+ def command_force_db_version(self):
+ v = self.options.verbose
+ sim = self.options.simulate
+ version = self.options.force_db_version
+ if not self.version_regex.search(version):
+ print "Versions must be in the format YYYY-MM-DD..."
+ print "You version %s does not fit this" % version
+ return
+ version_dir = os.path.join(self.base_dir(), version)
+ if not os.path.exists(version_dir):
+ if v:
+ print 'Creating %s' % self.shorten_filename(version_dir)
+ if not sim:
+ os.mkdir(version_dir)
+ elif v:
+ print ('Directory %s exists'
+ % self.shorten_filename(version_dir))
+ if self.options.db_record:
+ self.update_db(version, self.connection())
+
def update_sys_path(paths, verbose):
if isinstance(paths, (str, unicode)):
paths = [paths]
|