Personal wiki notebook (not under development)
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Schema_upgrader.py 5.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. import os.path
  2. from model.Persistent import Persistent
  3. class Schema_upgrader:
  4. def __init__( self, database, glob = None, read_file = None ):
  5. """
  6. Create a new schema upgrader and return it.
  7. @type database: Database
  8. @param database: the database to use for all schema upgrades
  9. @type glob: function or NoneType
  10. @param glob: a custom function to use for globbing files as per glob.glob() (optional)
  11. @type read_file: function or NoneType
  12. @param read_file: a custom function to use for reading schema files (optional)
  13. @rtype: Schema_upgrader
  14. @return: newly constructed Schema_upgrader
  15. """
  16. from glob import glob as real_glob
  17. self.__database = database
  18. self.__glob = glob or real_glob
  19. self.__read_file = read_file or Schema_upgrader.__read_file
  20. @staticmethod
  21. def __read_file( filename ):
  22. """
  23. Read a file and return all of its contents.
  24. @type filename: unicode
  25. @param filename: full path of the file to read
  26. @rtype: unicode
  27. @return: full contents of the file
  28. """
  29. return file( filename ).read()
  30. def upgrade_schema( self, to_version ):
  31. """
  32. Upgrade the database from its current version to a given version, applying all intervening
  33. schema delta files necessary to get there, and apply them in version order. If the given
  34. version is unknown, this method will upgrade to the latest known schema version that is
  35. before the given version.
  36. @type to_version: unicode
  37. @param to_version: the desired version to upgrade to, as a string
  38. """
  39. to_version = self.version_string_to_tuple( to_version )
  40. from_version = self.schema_version( self.__database )
  41. self.__database.commit()
  42. # if the database schema version is already equal to to_version, there's nothing to do
  43. if to_version == from_version:
  44. return
  45. if self.__database.backend == Persistent.SQLITE_BACKEND:
  46. extension = u"sqlite"
  47. else:
  48. extension = u"sql"
  49. filenames = self.__glob( u"model/delta/*.%s" % extension )
  50. versions = []
  51. # make a list of all available schema delta files
  52. for filename in filenames:
  53. base_filename = os.path.basename( filename )
  54. try:
  55. version = self.version_string_to_tuple( base_filename )
  56. except ValueError:
  57. continue
  58. # skip those versions that won't help us upgrade
  59. if version <= from_version:
  60. continue
  61. if version > to_version:
  62. continue
  63. versions.append( ( version, filename ) )
  64. if len( versions ) == 0:
  65. print "no new database schemas available"
  66. return
  67. # sort the schema delta files by version
  68. versions.sort( lambda a, b: cmp( a[ 0 ], b[ 0 ] ) )
  69. # apply the schema delta files in sorted order
  70. for ( version, filename ) in versions:
  71. self.apply_schema_delta( version, filename )
  72. self.__database.commit()
  73. print "successfully upgraded database schema"
  74. @staticmethod
  75. def schema_version( database, default_version = None ):
  76. try:
  77. schema_version = database.select_one( tuple, "select * from schema_version;" );
  78. schema_version = tuple( map( int, schema_version ) )
  79. # if there's no schema version table, then use the default version given. if there's no default
  80. # version, then assume the from_version is 1.5.4, which was the last version not to include a
  81. # schema_version table
  82. except:
  83. database.rollback()
  84. schema_version = default_version or ( 1, 5, 4 )
  85. # "release" is a reserved keyword in newer versions of sqlite, so put it in quotes
  86. database.execute( "create table schema_version ( major numeric, minor numeric, \"release\" numeric );", commit = False );
  87. database.execute( "insert into schema_version values ( %s, %s, %s );" % schema_version, commit = False );
  88. return schema_version
  89. def apply_schema_delta( self, version, filename ):
  90. """
  91. Upgrade the database from its current version to a given version, applying only the named
  92. schema delta file to do so. The changes are commited once complete, and the schema_version
  93. within the database is updated accordingly. This method assumes that the schema_version
  94. table exists and has one row.
  95. @type version: tuple
  96. @param version: ( major, minor, release ) with each version part as an integer
  97. @type filename: unicode
  98. @param filename: full path to the schema delta file to apply
  99. """
  100. print "upgrading database schema to version %s.%s.%s" % version
  101. # note: SQLite will auto-commit before certain statements (such as "create table"), which sort
  102. # of defeats the point of transactions. doing an explicit "begin transaction" first gives an
  103. # error later
  104. # http://oss.itsystementwicklung.de/download/pysqlite/doc/sqlite3.html#sqlite3-controlling-transactions
  105. self.__database.execute_script( self.__read_file( filename ), commit = False )
  106. self.__database.execute( "update schema_version set major = %s, minor = %s, \"release\" = %s;" % version, commit = False );
  107. self.__database.commit()
  108. @staticmethod
  109. def version_string_to_tuple( version ):
  110. """
  111. Given a version string with an optional file extension tacked on, convert the version to a
  112. tuple of integers and return it.
  113. @type version: unicode
  114. @param version: a version string of the form "major.minor.release"
  115. @rtype: tuple
  116. @return: ( major, minor, release ) with each version part as an integer
  117. @raises ValueError: invalid version or version parts cannot be converted to integers
  118. """
  119. VERSION_PARTS_COUNT = 3
  120. parts = version.split( "." )
  121. length = len( parts )
  122. if length == VERSION_PARTS_COUNT + 1:
  123. ( major, minor, release, extension ) = parts
  124. elif length == VERSION_PARTS_COUNT:
  125. ( major, minor, release ) = parts
  126. else:
  127. raise ValueError()
  128. try:
  129. major = int( major )
  130. minor = int( minor )
  131. release = int( release )
  132. except ( TypeError, ValueError ):
  133. raise ValueError()
  134. return ( major, minor, release )