Contact: fumanchu@aminus.org

Log in as guest/dejavu to create tickets

I think I've seen this ORM somewhere before...

Changeset 127

Show
Ignore:
Timestamp:
12/21/05 02:48:04
Author:
fumanchu
Message:

Initial implementation of #16 (schema changes). See modeling.html ("Managing Schema Changes") for summary. Oh, and Unit has a new remove_property method.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/__init__.py

    r126 r127  
    4242from dejavu.containers import * 
    4343from dejavu.errors import * 
     44from dejavu.schemas import * 
    4445from dejavu.units import * 
    4546from dejavu import logic 
  • trunk/arenas.py

    r126 r127  
    149149        self.storage(cls).create_storage(cls) 
    150150     
     151    def has_storage(self, cls): 
     152        return self.storage(cls).has_storage(cls) 
     153     
     154    def drop_storage(self, cls): 
     155        self.storage(cls).drop_storage(cls) 
     156     
     157    def add_property(self, cls, name): 
     158        self.storage(cls).add_property(cls, name) 
     159     
     160    def drop_property(self, cls, name): 
     161        self.storage(cls).drop_property(cls, name) 
     162     
     163    def rename_property(self, cls, oldname, newname): 
     164        self.storage(cls).rename_property(cls, oldname, newname) 
     165     
    151166    def migrate_class(self, cls, new_store): 
    152167        """migrate_class(cls, new_store). Copy all units of cls to new_store.""" 
     
    249264        classes = [c for c in self.arena._registered_classes.iterkeys() 
    250265                   if issubclass(c, cls)] 
     266        if not classes: 
     267            # Even the requested class is not registered. 
     268            raise errors.UnrecallableError("The '%s' class is not registered." 
     269                                           % cls.__name__) 
    251270         
    252271        for cls in classes: 
  • trunk/doc/index.html

    r126 r127  
    5656        <li>Loading Stores</li> 
    5757        <li>Registering Unit Classes</li> 
     58        <li>Managing Schema Changes</li> 
    5859        </ul> 
    5960    </li> 
  • trunk/doc/modeling.html

    r126 r127  
    688688the chain of associations between two Unit classes.</p> 
    689689 
     690<h4>Managing Schema Changes</h4> 
     691<p>The <tt>Schema</tt> class helps you manage changes to your 
     692Dejavu model throughout its lifetime. For example, let's say that we deploy 
     693our Archaeology application at various libraries around the world. After 
     694a year, one of the developers wishes to implement a new reporting feature; 
     695however, it would be easiest to build if the Unit Property names could 
     696be exposed to the users. Unfortunately, our "ArchID" property on the 
     697Biography class isn't very informative. It would be better if we could 
     698rename that to "ArchaeologistID":</p> 
     699 
     700<pre>class ArchBioSchema(dejavu.Schema): 
     701     
     702    guid = 'da39a3ee5e6b4b0d3255bfef95601890afd80709' 
     703    latest = 2 
     704     
     705    def upgrade_to_2(self): 
     706        self.arena.rename_property(Biography, "ArchID", "ArchaeologistID") 
     707 
     708abs = ArchBioSchema(arena) 
     709abs.upgrade() 
     710</pre> 
     711 
     712<p>Assuming we've already made the change to our model, the above example 
     713renames the property in the persistence layer (the database). There are 
     714also <tt class='def'>add_property(name)</tt> and 
     715<tt class='def'>drop_property(name)</tt> methods. There may be more methods 
     716in future versions of Dejavu; this feature is pretty new.</p> 
     717 
     718<p>The example also declares this change to be "version 2" of our schema. 
     719If you examine the base Schema class, you will see that it already has an 
     720<tt>upgrade_to_0</tt> method and an <tt>upgrade_to_1</tt> method. The 
     721"zeroth" upgrade makes no schema changes; it merely marks all deployed 
     722databases with "version 0". The upgrade to version 1 will automatically 
     723create all of your existing tables for you. Once you've made your first 
     724change to the initial schema, you can write your own <tt>upgrade_to_2</tt> 
     725method as we did above.</p> 
     726 
     727<p>If you call <tt class='def'>upgrade(version)</tt> with a version argument, 
     728then your deployment will be upgraded to that version. If no argument is 
     729given, the installation will be upgraded to <tt>Schema.latest</tt>. You 
     730can even skip steps (i.e. remove broken steps) if it comes to that.</p> 
     731 
     732<h5>The DeployedVersion Unit</h5> 
     733 
     734<p>The <tt>Schema</tt> class uses a magic table in the database to keep 
     735track of each deployment's schema version. The Unit class is called 
     736"DeployedVersion", and it has ID and Version attributes.</p> 
     737 
     738<p>The ID attribute will be set to whatever your <tt>Schema.guid</tt> 
     739is. It's a simple way to isolate multiple installed Dejavu applications. 
     740A given application should use the same guid throughout its lifetime. 
     741I used <tt>sha.new().hexdigest()</tt> to generate the example. Feel free 
     742to use sha.new, a guid generator, a descriptive name, or whatever you 
     743like.</p> 
     744 
     745<p>If you introduce schemas after you've deployed, you should probably 
     746skip upgrade #1 by manually setting <tt>DeployedVersion.Version</tt> 
     747to 1 (or whatever is accurate) at each of your installations.</p> 
     748 
     749 
    690750<hr /> 
    691751 
  • trunk/storage/__init__.py

    r126 r127  
    4040        raise NotImplementedError 
    4141     
    42     def create_storage(self, unitClass): 
    43         pass 
    44      
    4542    def reserve(self, unit): 
    4643        """Reserve storage space for the Unit.""" 
     
    5552     
    5653    def distinct(self, cls, attrs, expr=None): 
    57         """distinct(cls, attrs, expr=None) -> Distinct values for given attributes.""" 
     54        """Distinct values for given attributes.""" 
    5855        raise NotImplementedError 
    5956     
    6057    def multirecall(self, classes, expr): 
    61         """multirecall(classes, expr) -> Full inner join units from each class.""" 
    62         raise NotImplementedError 
     58        """Full inner join units from each class.""" 
     59        raise NotImplementedError 
     60     
     61    #                               Schemas                               # 
     62     
     63    def create_database(self): 
     64        pass 
     65     
     66    def drop_database(self): 
     67        pass 
     68     
     69    def create_storage(self, cls): 
     70        pass 
     71     
     72    def has_storage(self, cls): 
     73        pass 
     74     
     75    def drop_storage(self, cls): 
     76        pass 
     77     
     78    def add_property(self, cls, name): 
     79        pass 
     80     
     81    def drop_property(self, cls, name): 
     82        pass 
     83     
     84    def rename_property(self, cls, oldname, newname): 
     85        pass 
    6386 
    6487 
     
    90113            self.nextstore.destroy(unit) 
    91114     
    92     def create_storage(self, unitClass): 
    93         if self.nextstore: 
    94             self.nextstore.create_storage(unitClass) 
    95      
    96115    def reserve(self, unit): 
    97116        """Reserve storage space for the Unit.""" 
     
    113132        if self.nextstore: 
    114133            return self.nextstore.multirecall(classes, expr) 
     134     
     135    #                               Schemas                               # 
     136     
     137    def create_database(self): 
     138        if self.nextstore: 
     139            self.nextstore.create_database() 
     140     
     141    def drop_database(self): 
     142        if self.nextstore: 
     143            self.nextstore.drop_database() 
     144     
     145    def create_storage(self, cls): 
     146        if self.nextstore: 
     147            self.nextstore.create_storage(cls) 
     148     
     149    def has_storage(self, cls): 
     150        if self.nextstore: 
     151            return self.nextstore.has_storage(cls) 
     152        return False 
     153     
     154    def drop_storage(self, cls): 
     155        if self.nextstore: 
     156            self.nextstore.drop_storage(cls) 
     157     
     158    def add_property(self, cls, name): 
     159        if self.nextstore: 
     160            self.nextstore.add_property(cls, name) 
     161     
     162    def drop_property(self, cls, name): 
     163        if self.nextstore: 
     164            self.nextstore.drop_property(cls, name) 
     165     
     166    def rename_property(self, cls, oldname, newname): 
     167        if self.nextstore: 
     168            self.nextstore.rename_property(cls, oldname, newname) 
    115169 
    116170 
     
    335389            # .cancel does nothing if the thread is already finished. 
    336390            self.timer.cancel() 
     391     
     392    def add_property(self, cls, name): 
     393        self.sweep(cls) 
     394        if self.nextstore: 
     395            self.nextstore.add_property(cls, name) 
     396     
     397    def drop_property(self, cls, name): 
     398        self.sweep(cls) 
     399        if self.nextstore: 
     400            self.nextstore.drop_property(cls, name) 
     401     
     402    def rename_property(self, cls, oldname, newname): 
     403        self.sweep(cls) 
     404        if self.nextstore: 
     405            self.nextstore.rename_property(cls, oldname, newname) 
    337406 
    338407 
  • trunk/storage/db.py

    r126 r127  
    867867            getattr(conn, self.close_connection_method)() 
    868868     
    869     def create_database(self): 
    870         self.execute("CREATE DATABASE %s;" % self.sql_name(self.dbname)) 
    871      
    872     def drop_database(self): 
    873         self.execute("DROP DATABASE %s;" % self.sql_name(self.dbname)) 
    874      
    875     def create_storage(self, unitClass): 
    876         clsname = unitClass.__name__ 
    877         tablename = self.table_name(clsname) 
    878         typename = self.typeAdapter.coerce 
    879          
    880         fields = [] 
    881         for key in unitClass.properties(): 
    882             fields.append(u'%s %s' % (self.column_name(clsname, key), 
    883                                       typename(unitClass, key))) 
    884         self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields))) 
    885          
    886         for index in unitClass.indices(): 
    887             i = self.table_name("i" + clsname + index) 
    888             self.execute(u'CREATE INDEX %s ON %s (%s);' % 
    889                          (i, tablename, self.column_name(clsname, index))) 
    890      
    891     def select(self, unitClass, expr, fields=None, distinct=False): 
    892         clsname = unitClass.__name__ 
     869    def select(self, cls, expr, fields=None, distinct=False): 
     870        clsname = cls.__name__ 
    893871        tablename = self.table_name(clsname) 
    894872        if fields: 
     
    10581036                                     (self.column_name(clsname, key), val)) 
    10591037             
    1060             sql = ('UPDATE %s SET %s WHERE %s;' % 
    1061                    (self.table_name(clsname), u", ".join(parms), 
    1062                     self.id_clause(unit))) 
    1063             self.execute(sql) 
     1038            if parms: 
     1039                sql = ('UPDATE %s SET %s WHERE %s;' % 
     1040                       (self.table_name(clsname), u", ".join(parms), 
     1041                        self.id_clause(unit))) 
     1042                self.execute(sql) 
    10641043            unit.cleanse() 
    10651044     
     
    12961275            if acceptable: 
    12971276                yield unitset 
     1277     
     1278    #                               Schemas                               # 
     1279     
     1280    def create_database(self): 
     1281        self.execute("CREATE DATABASE %s;" % self.sql_name(self.dbname)) 
     1282     
     1283    def drop_database(self): 
     1284        self.execute("DROP DATABASE %s;" % self.sql_name(self.dbname)) 
     1285     
     1286    def create_storage(self, cls): 
     1287        """Create storage for the given class.""" 
     1288        clsname = cls.__name__ 
     1289        tablename = self.table_name(clsname) 
     1290        typename = self.typeAdapter.coerce 
     1291         
     1292        fields = [] 
     1293        for key in cls.properties(): 
     1294            fields.append(u'%s %s' % (self.column_name(clsname, key), 
     1295                                      typename(cls, key))) 
     1296        self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields))) 
     1297         
     1298        for index in cls.indices(): 
     1299            i = self.table_name("i" + clsname + index) 
     1300            self.execute(u'CREATE INDEX %s ON %s (%s);' % 
     1301                         (i, tablename, self.column_name(clsname, index))) 
     1302     
     1303    def has_storage(self, cls): 
     1304        try: 
     1305            # Must use fetch here instead of execute, because e.g. MySQL 
     1306            # must call store_result if the query has a result set 
     1307            # (or it will crash on a subsequent execute). 
     1308            self.fetch("SELECT * FROM %s WHERE 1=0;" % self.table_name(cls.__name__)) 
     1309        except: 
     1310            return False 
     1311        return True 
     1312     
     1313    def drop_storage(self, cls): 
     1314        self.execute(u'DROP TABLE %s;' % self.table_name(cls.__name__)) 
     1315     
     1316    def add_property(self, cls, name): 
     1317        clsname = cls.__name__ 
     1318        self.execute("ALTER TABLE %s ADD COLUMN %s %s;" % 
     1319                     (self.table_name(clsname), 
     1320                      self.column_name(clsname, name), 
     1321                      self.typeAdapter.coerce(cls, name), 
     1322                      )) 
     1323     
     1324    def drop_property(self, cls, name): 
     1325        clsname = cls.__name__ 
     1326        self.execute("ALTER TABLE %s DROP COLUMN %s;" % 
     1327                     (self.table_name(clsname), 
     1328                      self.column_name(clsname, name))) 
     1329     
     1330    def rename_property(self, cls, oldname, newname): 
     1331        clsname = cls.__name__ 
     1332        oldname = self.column_name(clsname, oldname) 
     1333        newname = self.column_name(clsname, newname) 
     1334        if oldname != newname: 
     1335            self.execute("ALTER TABLE %s RENAME COLUMN %s TO %s;" % 
     1336                         (self.table_name(clsname), oldname, newname)) 
    12981337 
    12991338 
  • trunk/storage/storeado.py

    r126 r127  
    4444adLockOptimistic = 3 
    4545adLockBatchOptimistic = 4 
     46 
     47adSchemaColumns = 3 
     48adSchemaTables = 20 
    4649 
    4750adUseClient = 3 
     
    357360            x.args += (query, ) 
    358361            conn = None 
    359             raise x 
    360      
    361     def fetch(self, query, conn=None): 
     362            raise 
     363     
     364    def fetch(self, query, conn=None, schema=False): 
    362365        """fetch(query, conn=None) -> rowdata, columns.""" 
    363366        if conn is None: 
    364367            conn = self.connection() 
    365         self.arena.log(query, dejavu.LOGSQL) 
    366368         
    367         res = win32com.client.Dispatch(r'ADODB.Recordset') 
    368         # Uncomment the following to get .Recordcount 
    369         # res.CursorLocation = adUseClient 
    370369        try: 
    371             if self.threaded: 
    372                 # 'conn' will be a ConnectionWrapper object, which .Open 
    373                 # won't accept. Pass the unwrapped connection instead. 
    374                 res.Open(query, conn.conn, adOpenForwardOnly, adLockReadOnly) 
     370            if schema: 
     371                res = conn.OpenSchema(query) 
    375372            else: 
    376                 res.Open(query, conn, adOpenForwardOnly, adLockReadOnly) 
     373                self.arena.log(query, dejavu.LOGSQL) 
     374                res = win32com.client.Dispatch(r'ADODB.Recordset') 
     375                if self.threaded: 
     376                    # 'conn' will be a ConnectionWrapper object, which .Open 
     377                    # won't accept. Pass the unwrapped connection instead. 
     378                    res.Open(query, conn.conn, adOpenForwardOnly, adLockReadOnly) 
     379                else: 
     380                    res.Open(query, conn, adOpenForwardOnly, adLockReadOnly) 
    377381        except pywintypes.com_error, x: 
    378382            try: 
     
    382386            x.args += (query, ) 
    383387            conn = None 
    384             raise x 
     388            raise 
    385389         
    386390        columns = [(x.Name, x.Type) for x in res.Fields] 
     
    397401         
    398402        return data, columns 
    399          
     403     
    400404    def version(self): 
    401405        adoconn = win32com.client.Dispatch(r'ADODB.Connection') 
    402406        return "ADO Version: %s" % adoconn.Version 
     407     
     408    def get_tables(self, conn=None): 
     409        return self.fetch(adSchemaTables, conn=conn, schema=True) 
     410     
     411    def has_storage(self, cls): 
     412        names = [x[2] for x in self.get_tables()[0]] 
     413        return self.table_name(cls.__name__, quoted=False) in names 
     414     
     415    def get_columns(self, conn=None): 
     416        return self.fetch(adSchemaColumns, conn=conn, schema=True) 
     417     
     418    def rename_property(self, cls, oldname, newname): 
     419        clsname = cls.__name__ 
     420        tblname = self.table_name(clsname, quoted=False) 
     421        oldname = self.column_name(clsname, oldname, quoted=False) 
     422        newname = self.column_name(clsname, newname, quoted=False) 
     423          
     424        conn = self.connection() 
     425        try: 
     426            cat = win32com.client.Dispatch(r'ADOX.Catalog') 
     427            cat.ActiveConnection = conn 
     428            cat.Tables(tblname).Columns(oldname).Name = newname 
     429        except pywintypes.com_error, x: 
     430            conn = None 
     431            cat = None 
     432            raise 
    403433 
    404434 
     
    492522        adoconn.Execute("DROP DATABASE %s;" % self.sql_name(self.dbname)) 
    493523        adoconn.Close() 
     524     
     525    def add_property(self, cls, name): 
     526        clsname = cls.__name__ 
     527        # SQL Server doesn't use the "COLUMN" keyword with "ADD" 
     528        self.execute("ALTER TABLE %s ADD %s %s;" % 
     529                     (self.table_name(clsname), 
     530                      self.column_name(clsname, name), 
     531                      self.typeAdapter.coerce(cls, name), 
     532                      )) 
     533     
     534    def rename_property(self, cls, oldname, newname): 
     535        clsname = cls.__name__ 
     536        oldname = self.column_name(clsname, oldname, quoted=False) 
     537        newname = self.column_name(clsname, newname, quoted=False) 
     538        if oldname != newname: 
     539            self.execute("EXEC sp_rename '%s.%s', '%s', 'COLUMN'" % 
     540                         (self.table_name(clsname), oldname, newname)) 
    494541 
    495542 
  • trunk/storage/storemysql.py

    r126 r127  
    9898            # They also won't truncate trailing spaces like VARCHAR does. 
    9999            if bytes <= 255: 
    100                 return u"VARBINARY(%s) CHARACTER SET utf8" % bytes 
     100                return u"VARBINARY(%s)" % bytes 
    101101            elif bytes < 2 ** 16: 
    102102                return "BLOB" 
     
    173173        return _mysql.connect(**tmplconn) 
    174174     
    175     def create_database(self): 
    176         # _mysql has create_db and drop_db commands, but they're deprecated. 
    177         sql = 'CREATE DATABASE %s;' % self.sql_name(self.dbname) 
    178         conn = self._template_conn() 
    179         self.execute(sql, conn) 
    180         conn.close() 
    181      
    182     def drop_database(self): 
    183         sql = 'DROP DATABASE %s;' % self.sql_name(self.dbname) 
    184         conn = self._template_conn() 
    185         self.execute(sql, conn) 
    186         conn.close() 
    187      
    188     def version(self): 
    189         conn = self._template_conn() 
    190         rowdata, cols = self.fetch("SELECT version();", conn) 
    191         conn.close() 
    192         return "MySQL Version: %s" % rowdata[0][0] 
    193      
    194     def create_storage(self, unitClass): 
    195         clsname = unitClass.__name__ 
    196         tablename = self.table_name(clsname) 
    197          
    198         coerce = self.typeAdapter.coerce 
    199         fields = [] 
    200         for key in unitClass.properties(): 
    201             fields.append(u'%s %s' % (self.column_name(clsname, key), 
    202                                       coerce(unitClass, key))) 
    203         self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields))) 
    204          
    205         for index in unitClass.indices(): 
    206             i = self.table_name("i" + clsname + index) 
    207              
    208             dbtype = coerce(unitClass, index) 
    209             if dbtype.endswith('BLOB') or dbtype == 'TEXT': 
    210                 # MySQL won't allow indexes on a BLOB field 
    211                 # without a specific length. 
    212                 self.execute(u'CREATE INDEX %s ON %s (%s(%s));' % 
    213                              (i, tablename, 
    214                               self.column_name(clsname, index), 255)) 
    215             else: 
    216                 self.execute(u'CREATE INDEX %s ON %s (%s);' % 
    217                              (i, tablename, 
    218                               self.column_name(clsname, index))) 
    219      
    220175    def fetch(self, query, conn=None): 
    221176        """fetch(query, conn=None) -> rowdata, columns. 
     
    238193                     (self.table_name(unit.__class__.__name__), 
    239194                      self.id_clause(unit))) 
    240  
     195     
     196    def version(self): 
     197        conn = self._template_conn() 
     198        rowdata, cols = self.fetch("SELECT version();", conn) 
     199        conn.close() 
     200        return "MySQL Version: %s" % rowdata[0][0] 
     201     
     202    #                               Schemas                               # 
     203     
     204    def create_database(self): 
     205        # _mysql has create_db and drop_db commands, but they're deprecated. 
     206        sql = 'CREATE DATABASE %s;' % self.sql_name(self.dbname) 
     207        conn = self._template_conn() 
     208        self.execute(sql, conn) 
     209        conn.close() 
     210     
     211    def drop_database(self): 
     212        sql = 'DROP DATABASE %s;' % self.sql_name(self.dbname) 
     213        conn = self._template_conn() 
     214        self.execute(sql, conn) 
     215        conn.close() 
     216     
     217    def create_storage(self, cls): 
     218        clsname = cls.__name__ 
     219        tablename = self.table_name(clsname) 
     220         
     221        coerce = self.typeAdapter.coerce 
     222        fields = [] 
     223        for key in cls.properties(): 
     224            fields.append(u'%s %s' % (self.column_name(clsname, key), 
     225                                      coerce(cls, key))) 
     226        self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields))) 
     227         
     228        for index in cls.indices(): 
     229            i = self.table_name("i" + clsname + index) 
     230             
     231            dbtype = coerce(cls, index) 
     232            if dbtype.endswith('BLOB') or dbtype == 'TEXT': 
     233                # MySQL won't allow indexes on a BLOB field 
     234                # without a specific length. 
     235                self.execute(u'CREATE INDEX %s ON %s (%s(%s));' % 
     236                             (i, tablename, 
     237                              self.column_name(clsname, index), 255)) 
     238            else: 
     239                self.execute(u'CREATE INDEX %s ON %s (%s);' % 
     240                             (i, tablename, 
     241                              self.column_name(clsname, index))) 
     242     
     243    def rename_property(self, cls, oldname, newname): 
     244        clsname = cls.__name__ 
     245        oldcolname = self.column_name(clsname, oldname) 
     246        newcolname = self.column_name(clsname, newname) 
     247        if oldcolname != newcolname: 
     248            self.execute("ALTER TABLE %s CHANGE %s %s %s;" % 
     249                         (self.table_name(clsname), oldcolname, newcolname, 
     250                          self.typeAdapter.coerce(cls, newname))) 
     251 
  • trunk/storage/storepypgsql.py

    r126 r127  
    8484        return libpq.PQconnectdb(tmplconn) 
    8585     
    86     def create_database(self): 
    87         c = self._template_conn() 
    88         self.execute('CREATE DATABASE %s' % self.sql_name(self.dbname), c) 
    89         c.finish() 
    90      
    91     def drop_database(self): 
    92         c = self._template_conn() 
    93         self.execute("DROP DATABASE %s;" % self.sql_name(self.dbname), c) 
    94         c.finish() 
    95      
    9686    def version(self): 
    9787        c = self._template_conn() 
     
    114104         
    115105        return data, columns 
     106     
     107    def create_database(self): 
     108        c = self._template_conn() 
     109        self.execute('CREATE DATABASE %s' % self.sql_name(self.dbname), c) 
     110        c.finish() 
     111     
     112    def drop_database(self): 
     113        c = self._template_conn() 
     114        self.execute("DROP DATABASE %s;" % self.sql_name(self.dbname), c) 
     115        c.finish() 
     116     
     117    def has_storage(self, cls): 
     118        # For some odd reason, libpq errors if you try to filter by tablename. 
     119        sql = u"SELECT tablename FROM pg_tables" 
     120        data, cols = self.fetch(sql) 
     121        return [self.table_name(cls.__name__, quoted=False)] in data 
    116122 
  • trunk/storage/storeshelve.py

    r126 r127  
    111111            lock.release() 
    112112     
    113     def create_database(self): 
    114         if not os.path.exists(self.shelvepath): 
    115             os.makedirs(self.shelvepath) 
    116      
    117     def drop_database(self): 
    118         while self.shelves: 
    119             clsname, shelf = self.shelves.popitem() 
    120             shelf.close() 
    121             tbl = os.path.join(self.shelvepath, clsname) 
    122             os.remove(tbl) 
    123      
    124113    def version(self): 
    125114        import sys 
    126115        return "Shelve version: %s" % sys.version 
    127      
    128     def create_storage(self, unitClass): 
    129         pass 
    130116     
    131117    def view(self, cls, fields, expr=None): 
     
    252238                yield unitrow 
    253239 
     240    #                               Schemas                               # 
     241     
     242    def create_database(self): 
     243        if not os.path.exists(self.shelvepath): 
     244            os.makedirs(self.shelvepath) 
     245     
     246    def drop_database(self): 
     247        while self.shelves: 
     248            clsname, shelf = self.shelves.popitem() 
     249            shelf.close() 
     250            tbl = os.path.join(self.shelvepath, clsname) 
     251            os.remove(tbl) 
     252     
     253    def create_storage(self, cls): 
     254        pass 
     255     
     256    def has_storage(self, cls): 
     257        return cls.__name__ in self.shelves 
     258     
     259    def drop_storage(self, cls): 
     260        clsname = cls.__name__ 
     261        shelf = self.shelves.pop(clsname) 
     262        shelf.close() 
     263        tbl = os.path.join(self.shelvepath, clsname) 
     264        os.remove(tbl) 
     265     
     266    def add_property(self, cls, name): 
     267        data, lock = self.shelf(cls) 
     268        try: 
     269            for id, props in data.items(): 
     270                props[name] = None 
     271                data[id] = props 
     272        finally: 
     273            lock.release() 
     274     
     275    def drop_property(self, cls, name): 
     276        data, lock = self.shelf(cls) 
     277        try: 
     278            for id, props in data.items(): 
     279                del props[name] 
     280                data[id] = props 
     281        finally: 
     282            lock.release() 
     283     
     284    def rename_property(self, cls, oldname, newname): 
     285        data, lock = self.shelf(cls) 
     286        try: 
     287            for id, props in data.items(): 
     288                props[newname] = props[oldname] 
     289                del props[oldname] 
     290                data[id] = props 
     291        finally: 
     292            lock.release() 
  • trunk/storage/storesqlite.py

    r126 r127  
    1616    import warnings 
    1717    warnings.warn(_escape_warning, dejavu.StorageWarning) 
     18 
     19_add_column_support = (_version >= storage.Version([3, 2, 0])) 
     20_rename_table_support = (_version >= storage.Version([3, 1, 0])) 
    1821 
    1922 
     
    163166        # SQLite should create the DB if missing. 
    164167        return _sqlite.connect(self.database, self.mode) 
    165      
    166     create_database = _get_conn 
    167      
    168     def drop_database(self): 
    169         self.shutdown() 
    170         # This should accept relative or absolute paths 
    171         os.remove(self.database) 
    172168     
    173169    def version(self): 
     
    192188                     (self.table_name(unit.__class__.__name__), 
    193189                      self.id_clause(unit))) 
    194      
    195     def create_storage(self, unitClass): 
    196         clsname = unitClass.__name__ 
    197         tablename = self.table_name(clsname) 
    198          
    199         # SQLite is typeless. 
    200         fields = [self.column_name(clsname, key) 
    201                   for key in unitClass.properties()] 
    202          
    203         self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields))) 
    204         for index in unitClass.indices(): 
    205             i = self.table_name("i" + clsname + index) 
    206             self.execute(u'CREATE INDEX %s ON %s (%s);' % 
    207                          (i, tablename, self.column_name(clsname, index))) 
    208190     
    209191    def join(self, unitjoin): 
     
    287269                     (u', '.join(colnames), joins, w)) 
    288270        return statement, imp, columns 
    289  
     271     
     272    #                               Schemas                               # 
     273     
     274    create_database = _get_conn 
     275     
     276    def drop_database(self): 
     277        self.shutdown() 
     278        # This should accept relative or absolute paths 
     279        os.remove(self.database) 
     280     
     281    def create_storage(self, cls): 
     282        clsname = cls.__name__ 
     283        tablename = self.table_name(clsname) 
     284         
     285        # SQLite is typeless. 
     286        fields = [self.column_name(clsname, key) for key in cls.properties()] 
     287         
     288        self.execute(u'CREATE TABLE %s (%s);' % (tablename, ", ".join(fields))) 
     289        for index in cls.indices(): 
     290            i = self.table_name("i" + clsname + index) 
     291            self.execute(u'CREATE INDEX %s ON %s (%s);' % 
     292                         (i, tablename, self.column_name(clsname, index))) 
     293     
     294    def add_property(self, cls, name): 
     295        clsname = cls.__name__ 
     296         
     297        if _add_column_support: 
     298            self.execute("ALTER TABLE %s ADD COLUMN %s;" % 
     299                         (self.table_name(clsname), 
     300                          self.column_name(clsname, name))) 
     301        else: 
     302            tablename = self.table_name(clsname, quoted=False) 
     303             
     304            # Create a temporary table with the new schema (no indices). 
     305            # The schema should already be changed in the model layer. 
     306            props = list(cls.properties()) 
     307            fields = ", ".join([self.column_name(clsname, key) for key in props]) 
     308            self.execute("CREATE TABLE [temp_%s] (%s);" % (tablename, fields)) 
     309            oldfields = [] 
     310            for key in props: 
     311                if key == name: 
     312                    oldfields.append(self.toAdapter.coerce(None)) 
     313                else: 
     314                    oldfields.append(self.column_name(clsname, key)) 
     315            self.execute("INSERT INTO [temp_%s] SELECT %s FROM [%s];" % 
     316                         (tablename, ", ".join(oldfields), tablename)) 
     317             
     318            # Drop and re-create the old table. 
     319            self.execute("DROP TABLE [%s];" % tablename) 
     320            self.create_storage(cls) 
     321            self.execute("INSERT INTO [%s] SELECT * FROM [temp_%s];" % 
     322                         (tablename, tablename)) 
     323            self.execute("DROP TABLE [temp_%s];" % tablename) 
     324     
     325    def drop_property(self, cls, name): 
     326        clsname = cls.__name__ 
     327        tablename = self.table_name(clsname, quoted=False) 
     328         
     329        # Create a temporary table with the new schema (no indices). 
     330        # The schema should already be changed in the model layer. 
     331        fields = ", ".join([self.column_name(clsname, key) 
     332                            for key in cls.properties()]) 
     333        self.execute("CREATE TABLE [temp_%s] (%s);" % (tablename, fields)) 
     334        self.execute("INSERT INTO [temp_%s] SELECT %s FROM [%s];" % 
     335                     (tablename, fields, tablename)) 
     336         
     337        # Drop and re-create the old table. 
     338        self.execute("DROP TABLE [%s];" % tablename) 
     339        self.create_storage(cls) 
     340        self.execute("INSERT INTO [%s] SELECT * FROM [temp_%s];" % 
     341                     (tablename, tablename)) 
     342        self.execute("DROP TABLE [temp_%s];" % tablename) 
     343     
     344    def rename_property(self, cls, oldname, newname): 
     345        clsname = cls.__name__ 
     346        tablename = self.table_name(clsname, quoted=False) 
     347         
     348        # Create a temporary table with the new schema (no indices). 
     349        # The schema should already be changed in the model layer. 
     350        props = list(cls.properties()) 
     351        fields = ", ".join([self.column_name(clsname, key) for key in props]) 
     352        self.execute("CREATE TABLE [temp_%s] (%s);" % (tablename, fields)) 
     353        oldfields = [] 
     354        for key in props: 
     355            if key == newname: 
     356                oldfields.append("%s AS %s" % (self.column_name(clsname, oldname), 
     357                                               self.column_name(clsname, newname))) 
     358            else: 
     359                oldfields.append(self.column_name(clsname, key)) 
     360        self.execute("INSERT INTO [temp_%s] SELECT %s FROM [%s];" % 
     361                     (tablename, ", ".join(oldfields), tablename)) 
     362         
     363        # Drop and re-create the old table. 
     364        self.execute("DROP TABLE [%s];" % tablename) 
     365        self.create_storage(cls) 
     366        self.execute("INSERT INTO [%s] SELECT * FROM [temp_%s];" % 
     367                     (tablename, tablename)) 
     368        self.execute("DROP TABLE [temp_%s];" % tablename) 
  • trunk/test/test_dejavu.py

    r126 r127  
    99 
    1010arena.add_store("default", "dejavu.storage.CachingProxy") 
     11arena.register(Animal) 
     12arena.register(Lecture) 
     13arena.register(Vet) 
     14arena.register(Visit) 
     15arena.register(Zoo) 
    1116 
    1217 
  • trunk/test/test_storeburned.py

    r126 r127  
    11import os 
    2 import zoo_fixture 
    32 
    4 zoo_fixture.init() 
    53shelvedb = "dejavu.storage.storeshelve.StorageManagerShelve" 
    64proxiedopts = {u'Path': os.path.join(os.getcwd(), os.path.dirname(__file__))} 
    7 zoo_fixture.arena.add_store('shelvedb', shelvedb, proxiedopts) 
    8  
    95opts = {'Next Store': 'shelvedb', 
    106        'Lifetime': '10 minutes', 
     
    1410 
    1511def run(): 
     12    import zoo_fixture 
     13    # Isolate schema changes from one test to the next. 
     14    reload(zoo_fixture) 
     15    zoo_fixture.init() 
     16    zoo_fixture.arena.add_store('shelvedb', shelvedb, proxiedopts) 
    1617    zoo_fixture.run(SM_class, opts) 
    1718 
  • trunk/test/test_storecaching.py

    r126 r127  
    11import os 
    2 import zoo_fixture 
    32 
    4 zoo_fixture.init() 
    53shelvedb = "dejavu.storage.storeshelve.StorageManagerShelve" 
    64proxiedopts = {u'Path': os.path.join(os.getcwd(), os.path.dirname(__file__))} 
    7 zoo_fixture.arena.add_store('shelvedb', shelvedb, proxiedopts) 
    8  
    95opts = {'Next Store': 'shelvedb', 
    106        'Lifetime': '10 minutes', 
     
    1410 
    1511def run(): 
     12    import zoo_fixture 
     13    # Isolate schema changes from one test to the next. 
     14    reload(zoo_fixture) 
     15     
     16    zoo_fixture.init() 
     17    zoo_fixture.arena.add_store('shelvedb', shelvedb, proxiedopts) 
    1618    zoo_fixture.run(SM_class, opts) 
    1719 
  • trunk/test/test_storemsaccess.py

    r126 r127  
    1717    def run(): 
    1818        import zoo_fixture 
     19        # Isolate schema changes from one test to the next. 
     20        reload(zoo_fixture) 
    1921        zoo_fixture.init() 
    2022        zoo_fixture.run(SM_class, opts) 
  • trunk/test/test_storemysql.py

    r126 r127  
    1919    def run(): 
    2020        import zoo_fixture 
     21        # Isolate schema changes from one test to the next. 
     22        reload(zoo_fixture) 
    2123        zoo_fixture.init() 
    2224        zoo_fixture.run(SM_class, opts) 
  • trunk/test/test_storeproxy.py

    r126 r127  
    11import os 
    2 import zoo_fixture 
    32 
    4 zoo_fixture.init() 
    53shelvedb = "dejavu.storage.storeshelve.StorageManagerShelve" 
    64proxiedopts = {u'Path': os.path.join(os.getcwd(), os.path.dirname(__file__))} 
    7 zoo_fixture.arena.add_store('shelvedb', shelvedb, proxiedopts) 
    8  
    95opts = {'Next Store': 'shelvedb', 
    106        'Lifetime': '10 minutes', 
     
    1410 
    1511def run(): 
     12    import zoo_fixture 
     13    # Isolate schema changes from one test to the next. 
     14    reload(zoo_fixture) 
     15    zoo_fixture.init() 
     16    zoo_fixture.arena.add_store('shelvedb', shelvedb, proxiedopts) 
    1617    zoo_fixture.run(SM_class, opts) 
    1718 
  • trunk/test/test_storepypgsql.py

    r126 r127  
    2323    def run(): 
    2424        import zoo_fixture 
     25        # Isolate schema changes from one test to the next. 
     26        reload(zoo_fixture) 
    2527        zoo_fixture.init() 
    2628        zoo_fixture.run(SM_class, opts) 
  • trunk/test/test_storeshelve.py

    r126 r127  
    1313def run(): 
    1414    import zoo_fixture 
     15    # Isolate schema changes from one test to the next. 
     16    reload(zoo_fixture) 
    1517    zoo_fixture.init() 
    1618    zoo_fixture.run(SM_class, opts) 
  • trunk/test/test_storesqlite.py

    r126 r127  
    1313    def run(): 
    1414        import zoo_fixture 
     15        # Isolate schema changes from one test to the next. 
     16        reload(zoo_fixture) 
    1517        zoo_fixture.init() 
    1618        zoo_fixture.run(SM_class, opts) 
  • trunk/test/test_storesqlserver.py

    r126 r127  
    1616    def run(): 
    1717        import zoo_fixture 
     18        # Isolate schema changes from one test to the next. 
     19        reload(zoo_fixture) 
    1820        zoo_fixture.init() 
    1921        zoo_fixture.run(SM_class, opts) 
  • trunk/test/zoo_fixture.py

    r126 r127  
    700700##        lion.LastEscape = datetime.datetime(2005, 12, 25, 8, 14) 
    701701##        box.commit_since("rollback point name") 
     702     
     703    def testzzzz_Schema_Upgrade(self): 
     704        # Must run last. 
     705        zs = ZooSchema(arena) 
     706         
     707        # In this first upgrade, we simulate the case where the code was 
     708        # upgraded, and the database schema upgrade performed afterward. 
     709        # The Schema.latest property is set, and upgrade() is called with 
     710        # no argument (which should upgrade us to "latest"). 
     711        Animal.set_property("ExhibitID") 
     712        zs.latest = 2 
     713        zs.upgrade() 
     714         
     715        # In this example, we simulate the developer who wants to put 
     716        # model changes inline with database changes (see upgrade_to_3). 
     717        # We do not set latest, but instead supply an arg to upgrade(). 
     718        zs.upgrade(3) 
     719         
     720        # Test that Animals have a new "Family" property, and an ExhibitID. 
     721        box = arena.new_sandbox() 
     722        emp = box.unit(Animal, Family='Emperor Penguin') 
     723        self.assertEqual(emp.ExhibitID, 'The Penguin Encounter') 
    702724 
    703725 
     
    720742    arena.logflags = dejavu.LOGSQL 
    721743 
     744 
     745class ZooSchema(dejavu.Schema): 
     746     
     747    latest = 1 
     748     
     749    def upgrade_to_2(self): 
     750        self.arena.add_property(Animal, "ExhibitID") 
     751        box = self.arena.new_sandbox() 
     752        for exhibit in box.recall(Exhibit): 
     753            for animalID in exhibit.Animals: 
     754                a = box.unit(Animal, ID=animalID) 
     755                if a: 
     756                    # Exhibits are identified by ZooID and Name 
     757                    a.ZooID = exhibit.ZooID 
     758                    a.ExhibitID = exhibit.Name 
     759        box.flush_all() 
     760     
     761    def upgrade_to_3(self): 
     762        Animal.remove_property("Species") 
     763        Animal.set_property("Family") 
     764         
     765        # Note that we drop this column in a separate step from step 2. 
     766        # If we had mixed model properties and SM properties in step 2, 
     767        # we could have done this all in one step. But this is a better 
     768        # demonstration of the possibilities. ;) 
     769        Exhibit.remove_property("Animals") 
     770        self.arena.drop_property(Exhibit, "Animals") 
     771         
     772        self.arena.rename_property(Animal, "Species", "Family") 
     773 
     774 
    722775def setup(SM_class, opts): 
    723776    """setup(SM_class, opts). Set up storage for Zoo classes.""" 
     
    731784    engines.register_classes(arena) 
    732785     
    733     for cls in (Animal, Zoo, Exhibit, Lecture, Vet, Visit, 
    734                 engines.UnitEngine, 
    735                 engines.UnitCollection, engines.UnitEngineRule): 
    736         arena.create_storage(cls) 
     786    ZooSchema(arena).upgrade() 
    737787 
    738788 
  • trunk/units.py

    r126 r127  
    519519        except TypeError, x: 
    520520            x.args += (self.__class__.__name__, self._properties.keys()) 
    521             raise x 
     521            raise 
    522522     
    523523    def dirty(self): 
     
    539539            cls.set_property(key, typ, False, descriptor) 
    540540    set_properties = classmethod(set_properties) 
     541     
     542    def remove_property(cls, key): 
     543        delattr(cls, key) 
     544        del cls._properties[key] 
     545    remove_property = classmethod(remove_property) 
    541546     
    542547    def indices(cls): 
     
    549554            except AttributeError, x: 
    550555                x.args += (cls, key) 
    551                 raise x 
     556                raise 
    552557        return tuple(product) 
    553558    indices = classmethod(indices)