changeset 2217:98d3bf8ffb19

store Intervals as two columns (and other fixes
author Richard Jones <richard@users.sourceforge.net>
date Sun, 18 Apr 2004 05:31:03 +0000
parents 759ed26e24dd
children 5673b24ceb0d
files CHANGES.txt TODO.txt roundup/backends/back_mysql.py roundup/backends/back_sqlite.py roundup/backends/rdbms_common.py test/db_test_base.py test/test_dates.py
diffstat 7 files changed, 176 insertions(+), 90 deletions(-) [+]
line wrap: on
line diff
--- a/CHANGES.txt	Sat Apr 17 01:50:00 2004 +0000
+++ b/CHANGES.txt	Sun Apr 18 05:31:03 2004 +0000
@@ -31,6 +31,7 @@
 - sqlite backend had stopped using the global lock
 - better check for anonymous viewing of user items (sf bug 933510)
 - stop Interval from displaying an empty string (sf bug 934022)
+- fixed storage of some datatypes in some RDBMS backends
 
 
 2004-03-27 0.7.0b2
--- a/TODO.txt	Sat Apr 17 01:50:00 2004 +0000
+++ b/TODO.txt	Sun Apr 18 05:31:03 2004 +0000
@@ -1,8 +1,5 @@
 This file contains items that need doing before the next release:
 
-. make Intervals store timestamps, not strings
-
-
 Optionally:
 - have rdbms backends look up the journal for actor if it's not set
 - migrate to numeric ID values (fixes bug 817217)
--- a/roundup/backends/back_mysql.py	Sat Apr 17 01:50:00 2004 +0000
+++ b/roundup/backends/back_mysql.py	Sun Apr 18 05:31:03 2004 +0000
@@ -126,7 +126,7 @@
         # no fractional seconds for MySQL
         hyperdb.Date   : lambda x: x.formal(sep=' '),
         hyperdb.Link   : int,
-        hyperdb.Interval  : lambda x: x.serialise(),
+        hyperdb.Interval  : str,
         hyperdb.Password  : str,
         hyperdb.Boolean   : int,
         hyperdb.Number    : lambda x: x,
@@ -190,7 +190,7 @@
         sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
         self.cursor.execute(sql, ('__textids', 1))
 
-    def add_actor_column(self):
+    def add_new_columns_v2(self):
         '''While we're adding the actor column, we need to update the
         tables to have the correct datatypes.'''
         for klass in self.classes.values():
@@ -274,8 +274,15 @@
                     # convert to new MySQL data type
                     prop = properties[name]
                     if v is not None:
-                        v = self.hyperdb_to_sql_value[prop.__class__](v)
-                    l.append(v)
+                        e = self.hyperdb_to_sql_value[prop.__class__](v)
+                    l.append(e)
+
+                    # Intervals store the seconds value too
+                    if isinstance(prop, Interval):
+                        if v is not None:
+                            l.append(v.as_seconds())
+                        else:
+                            l.append(e)
 
             self.drop_class_table_indexes(cn, old_spec[0])
 
@@ -595,25 +602,24 @@
                         # If range creation fails - ignore that search parameter
                         pass                        
             elif isinstance(propclass, Interval):
+                # filter using the __<prop>_int__ column
                 if isinstance(v, type([])):
                     s = ','.join([a for x in v])
-                    where.append('_%s in (%s)'%(k, s))
-                    args = args + [date.Interval(x).serialise() for x in v]
+                    where.append('__%s_int__ in (%s)'%(k, s))
+                    args = args + [date.Interval(x).as_seconds() for x in v]
                 else:
                     try:
                         # Try to filter on range of intervals
                         date_rng = Range(v, date.Interval)
-                        if (date_rng.from_value):
-                            where.append('_%s >= %s'%(k, a))
-                            args.append(date_rng.from_value.serialise())
-                        if (date_rng.to_value):
-                            where.append('_%s <= %s'%(k, a))
-                            args.append(date_rng.to_value.serialise())
+                        if date_rng.from_value:
+                            where.append('__%s_int__ >= %s'%(k, a))
+                            args.append(date_rng.from_value.as_seconds())
+                        if date_rng.to_value:
+                            where.append('__%s_int__ <= %s'%(k, a))
+                            args.append(date_rng.to_value.as_seconds())
                     except ValueError:
                         # If range creation fails - ignore that search parameter
                         pass                        
-                    #where.append('_%s=%s'%(k, a))
-                    #args.append(date.Interval(v).serialise())
             else:
                 if isinstance(v, type([])):
                     s = ','.join([a for x in v])
@@ -634,34 +640,27 @@
             args = args + v
 
         # "grouping" is just the first-order sorting in the SQL fetch
-        # can modify it...)
         orderby = []
         ordercols = []
-        if group[0] is not None and group[1] is not None:
-            if group[0] != '-':
-                orderby.append('_'+group[1])
-                ordercols.append('_'+group[1])
-            else:
-                orderby.append('_'+group[1]+' desc')
-                ordercols.append('_'+group[1])
-
-        # now add in the sorting
-        group = ''
-        if sort[0] is not None and sort[1] is not None:
-            direction, colname = sort
-            if direction != '-':
-                if colname == 'id':
-                    orderby.append(colname)
+        mlsort = []
+        for sortby in group, sort:
+            sdir, prop = sortby
+            if sdir and prop:
+                if isinstance(props[prop], Multilink):
+                    mlsort.append(sortby)
+                    continue
+                elif isinstance(props[prop], Interval):
+                    # use the int column for sorting
+                    o = '__'+prop+'_int__'
+                    ordercols.append(o)
+                elif prop == 'id':
+                    o = 'id'
                 else:
-                    orderby.append('_'+colname)
-                    ordercols.append('_'+colname)
-            else:
-                if colname == 'id':
-                    orderby.append(colname+' desc')
-                    ordercols.append(colname)
-                else:
-                    orderby.append('_'+colname+' desc')
-                    ordercols.append('_'+colname)
+                    o = '_'+prop
+                    ordercols.append(o)
+                if sdir == '-':
+                    o += ' desc'
+                orderby.append(o)
 
         # construct the SQL
         frum = ','.join(frum)
@@ -669,14 +668,14 @@
             where = ' where ' + (' and '.join(where))
         else:
             where = ''
-        cols = ['id']
+        cols = ['distinct(id)']
         if orderby:
             cols = cols + ordercols
             order = ' order by %s'%(','.join(orderby))
         else:
             order = ''
         cols = ','.join(cols)
-        sql = 'select %s from %s %s%s%s'%(cols, frum, where, group, order)
+        sql = 'select %s from %s %s%s'%(cols, frum, where, order)
         args = tuple(args)
         if __debug__:
             print >>hyperdb.DEBUG, 'filter', (self, sql, args)
@@ -685,7 +684,28 @@
 
         # return the IDs (the first column)
         # XXX numeric ids
-        return [str(row[0]) for row in l]
+        l = [str(row[0]) for row in l]
+
+        if not mlsort:
+            return l
+
+        # ergh. someone wants to sort by a multilink.
+        r = []
+        for id in l:
+            m = []
+            for ml in mlsort:
+                m.append(self.get(id, ml[1]))
+            r.append((id, m))
+        i = 0
+        for sortby in mlsort:
+            def sortfun(a, b, dir=sortby[i]):
+                if dir == '-':
+                    return cmp(b[1][i], a[1][i])
+                else:
+                    return cmp(a[1][i], b[1][i])
+            r.sort(sortfun)
+            i += 1
+        return [i[0] for i in r]
 
 class Class(MysqlClass, rdbms_common.Class):
     pass
--- a/roundup/backends/back_sqlite.py	Sat Apr 17 01:50:00 2004 +0000
+++ b/roundup/backends/back_sqlite.py	Sun Apr 18 05:31:03 2004 +0000
@@ -1,4 +1,4 @@
-# $Id: back_sqlite.py,v 1.24 2004-04-07 01:12:26 richard Exp $
+# $Id: back_sqlite.py,v 1.25 2004-04-18 05:31:02 richard Exp $
 '''Implements a backend for SQLite.
 
 See https://pysqlite.sourceforge.net/ for pysqlite info
@@ -32,7 +32,7 @@
         hyperdb.String : str,
         hyperdb.Date   : lambda x: x.serialise(),
         hyperdb.Link   : int,
-        hyperdb.Interval  : lambda x: x.serialise(),
+        hyperdb.Interval  : str,
         hyperdb.Password  : str,
         hyperdb.Boolean   : int,
         hyperdb.Number    : lambda x: x,
@@ -40,8 +40,7 @@
     sql_to_hyperdb_value = {
         hyperdb.String : str,
         hyperdb.Date   : lambda x: date.Date(str(x)),
-#        hyperdb.Link   : int,      # XXX numeric ids
-        hyperdb.Link   : str,
+        hyperdb.Link   : str, # XXX numeric ids
         hyperdb.Interval  : date.Interval,
         hyperdb.Password  : lambda x: password.Password(encrypted=x),
         hyperdb.Boolean   : int,
@@ -95,17 +94,17 @@
         sql = 'insert into ids (name, num) values (%s,%s)'%(self.arg, self.arg)
         self.cursor.execute(sql, ('__textids', 1))
 
-    def add_actor_column(self):
+    def add_new_columns_v2(self):
         # update existing tables to have the new actor column
         tables = self.database_schema['tables']
         for classname, spec in self.classes.items():
             if tables.has_key(classname):
                 dbspec = tables[classname]
-                self.update_class(spec, dbspec, force=1, adding_actor=1)
+                self.update_class(spec, dbspec, force=1, adding_v2=1)
                 # we've updated - don't try again
                 tables[classname] = spec.schema()
 
-    def update_class(self, spec, old_spec, force=0, adding_actor=0):
+    def update_class(self, spec, old_spec, force=0, adding_v2=0):
         ''' Determine the differences between the current spec and the
             database version of the spec, and update where necessary.
 
@@ -141,7 +140,7 @@
         old_has = old_has.has_key
 
         # now figure how we populate the new table
-        if adding_actor:
+        if adding_v2:
             fetch = ['_activity', '_creation', '_creator']
         else:
             fetch = ['_actor', '_activity', '_creation', '_creator']
@@ -201,14 +200,41 @@
         self.create_class_table(spec)
 
         if olddata:
+            inscols = []
+            for propname,x in new_spec[1]:
+                prop = properties[propname]
+                if isinstance(prop, hyperdb.Multilink):
+                    continue
+                elif isinstance(prop, hyperdb.Interval):
+                    inscols.append('_'+propname)
+                    inscols.append('__'+propname+'_int__')
+                elif old_has(propname):
+                    # we copy this col over from the old table
+                    inscols.append('_'+propname)
+
             # do the insert of the old data - the new columns will have
             # NULL values
-            args = ','.join([self.arg for x in fetch])
-            sql = 'insert into _%s (%s) values (%s)'%(cn, fetchcols, args)
+            args = ','.join([self.arg for x in inscols])
+            cols = ','.join(inscols)
+            sql = 'insert into _%s (%s) values (%s)'%(cn, cols, args)
             if __debug__:
                 print >>hyperdb.DEBUG, 'update_class', (self, sql, olddata[0])
             for entry in olddata:
-                self.cursor.execute(sql, tuple(entry))
+                d = []
+                for name in inscols:
+                    # generate the new value for the Interval int column
+                    if name.endswith('_int__'):
+                        name = name[2:-6]
+                        if entry.has_key(name):
+                            v = hyperdb.Interval(entry[name]).as_seconds()
+                        else:
+                            v = None
+                    elif entry.has_key(name):
+                        v = entry[name]
+                    else:
+                        v = None
+                    d.append(v)
+                self.cursor.execute(sql, tuple(d))
 
         return 1
 
--- a/roundup/backends/rdbms_common.py	Sat Apr 17 01:50:00 2004 +0000
+++ b/roundup/backends/rdbms_common.py	Sun Apr 18 05:31:03 2004 +0000
@@ -1,4 +1,4 @@
-# $Id: rdbms_common.py,v 1.90 2004-04-08 00:40:20 richard Exp $
+# $Id: rdbms_common.py,v 1.91 2004-04-18 05:31:02 richard Exp $
 ''' Relational database (SQL) backend common code.
 
 Basics:
@@ -205,7 +205,7 @@
 
             # version 1 didn't have the actor column (note that in
             # MySQL this will also transition the tables to typed columns)
-            self.add_actor_column()
+            self.add_new_columns_v2()
 
             # version 1 doesn't have the OTK, session and indexing in the
             # database
@@ -224,7 +224,6 @@
                 klass.index(nodeid)
         self.indexer.save_index()
 
-
     hyperdb_to_sql_datatypes = {
         hyperdb.String : 'VARCHAR(255)',
         hyperdb.Date   : 'TIMESTAMP',
@@ -261,6 +260,10 @@
             datatype = self.hyperdb_to_sql_datatypes[prop.__class__]
             cols.append(('_'+col, datatype))
 
+            # Intervals stored as two columns
+            if isinstance(prop, Interval):
+                cols.append(('__'+col+'_int__', 'BIGINT'))
+
         cols.sort()
         return cols, mls
 
@@ -613,7 +616,7 @@
         hyperdb.String : str,
         hyperdb.Date   : lambda x: x.formal(sep=' ', sec='%.3f'),
         hyperdb.Link   : int,
-        hyperdb.Interval  : lambda x: x.serialise(),
+        hyperdb.Interval  : str,
         hyperdb.Password  : str,
         hyperdb.Boolean   : lambda x: x and 'TRUE' or 'FALSE',
         hyperdb.Number    : lambda x: x,
@@ -657,6 +660,17 @@
         # figure the values to insert
         vals = []
         for col,dt in cols:
+            # this is somewhat dodgy....
+            if col.endswith('_int__'):
+                # XXX eugh, this test suxxors
+                value = values[col[2:-6]]
+                # this is an Interval special "int" column
+                if value is not None:
+                    vals.append(value.as_seconds())
+                else:
+                    vals.append(value)
+                continue
+
             prop = props[col[1:]]
             value = values[col[1:]]
             if value:
@@ -710,6 +724,11 @@
             prop = props[col]
             if isinstance(prop, Multilink):
                 mls.append(col)
+            elif isinstance(prop, Interval):
+                # Intervals store the seconds value too
+                cols.append(col)
+                # extra leading '_' added by code below
+                cols.append('_' +col + '_int__')
             else:
                 cols.append(col)
         cols.sort()
@@ -717,11 +736,25 @@
         # figure the values to insert
         vals = []
         for col in cols:
-            prop = props[col]
-            value = values[col]
-            if value is not None:
-                value = self.hyperdb_to_sql_value[prop.__class__](value)
-            vals.append(value)
+            if col.endswith('_int__'):
+                # XXX eugh, this test suxxors
+                # Intervals store the seconds value too
+                col = col[1:-6]
+                prop = props[col]
+                value = values[col]
+                if value is None:
+                    vals.append(None)
+                else:
+                    vals.append(value.as_seconds())
+            else:
+                prop = props[col]
+                value = values[col]
+                if value is None:
+                    e = None
+                else:
+                    e = self.hyperdb_to_sql_value[prop.__class__](value)
+                vals.append(e)
+
         vals.append(int(nodeid))
         vals = tuple(vals)
 
@@ -816,6 +849,10 @@
         props = cl.getprops(protected=1)
         for col in range(len(cols)):
             name = cols[col][0][1:]
+            if name.endswith('_int__'):
+                # XXX eugh, this test suxxors
+                # ignore the special Interval-as-seconds column
+                continue
             value = values[col]
             if value is not None:
                 value = self.sql_to_hyperdb_value[props[name].__class__](value)
@@ -1840,9 +1877,9 @@
 
         # generate the where clause
         s = ' and '.join(['lower(_%s)=%s'%(col, self.db.arg) for col in where])
-        sql = 'select id from _%s where %s and __retired__=%s'%(self.classname,
-            s, self.db.arg)
-        args.append(0)
+        sql = 'select id from _%s where %s and __retired__<>%s'%(
+            self.classname, s, self.db.arg)
+        args.append(1)
         self.db.sql(sql, tuple(args))
         # XXX numeric ids
         l = [str(x[0]) for x in self.db.sql_fetchall()]
@@ -1989,25 +2026,24 @@
                         # If range creation fails - ignore that search parameter
                         pass                        
             elif isinstance(propclass, Interval):
+                # filter using the __<prop>_int__ column
                 if isinstance(v, type([])):
                     s = ','.join([a for x in v])
-                    where.append('_%s in (%s)'%(k, s))
-                    args = args + [date.Interval(x).serialise() for x in v]
+                    where.append('__%s_int__ in (%s)'%(k, s))
+                    args = args + [date.Interval(x).as_seconds() for x in v]
                 else:
                     try:
                         # Try to filter on range of intervals
                         date_rng = Range(v, date.Interval)
                         if date_rng.from_value:
-                            where.append('_%s >= %s'%(k, a))
-                            args.append(date_rng.from_value.serialise())
+                            where.append('__%s_int__ >= %s'%(k, a))
+                            args.append(date_rng.from_value.as_seconds())
                         if date_rng.to_value:
-                            where.append('_%s <= %s'%(k, a))
-                            args.append(date_rng.to_value.serialise())
+                            where.append('__%s_int__ <= %s'%(k, a))
+                            args.append(date_rng.to_value.as_seconds())
                     except ValueError:
                         # If range creation fails - ignore that search parameter
                         pass                        
-                    #where.append('_%s=%s'%(k, a))
-                    #args.append(date.Interval(v).serialise())
             else:
                 if isinstance(v, type([])):
                     s = ','.join([a for x in v])
@@ -2037,6 +2073,10 @@
                 if isinstance(props[prop], Multilink):
                     mlsort.append(sortby)
                     continue
+                elif isinstance(props[prop], Interval):
+                    # use the int column for sorting
+                    o = '__'+prop+'_int__'
+                    ordercols.append(o)
                 elif prop == 'id':
                     o = 'id'
                 else:
@@ -2072,7 +2112,7 @@
 
         # return the IDs (the first column)
         # XXX numeric ids
-        l =  [str(row[0]) for row in l]
+        l = [str(row[0]) for row in l]
 
         if not mlsort:
             return l
@@ -2247,12 +2287,14 @@
             d[propname] = value
 
         # get a new id if necessary
-        if newid is None or not self.hasnode(newid):
+        if newid is None:
             newid = self.db.newid(self.classname)
-            self.db.addnode(self.classname, newid, d)
+
+        # insert new node or update existing?
+        if not self.hasnode(newid):
+            self.db.addnode(self.classname, newid, d) # insert
         else:
-            # update
-            self.db.setnode(self.classname, newid, d)
+            self.db.setnode(self.classname, newid, d) # update 
 
         # retire?
         if retire:
--- a/test/db_test_base.py	Sat Apr 17 01:50:00 2004 +0000
+++ b/test/db_test_base.py	Sun Apr 18 05:31:03 2004 +0000
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: db_test_base.py,v 1.24 2004-04-08 00:43:23 richard Exp $ 
+# $Id: db_test_base.py,v 1.25 2004-04-18 05:31:02 richard Exp $ 
 
 import unittest, os, shutil, errno, imp, sys, time, pprint
 
@@ -948,7 +948,7 @@
             # ensure retired items are retired :)
             l = items.keys(); l.sort()
             m = klass.list(); m.sort()
-            ae(l, m)
+            ae(l, m, '%s id list wrong %r vs. %r'%(cn, l, m))
             for id, props in items.items():
                 for name, value in props.items():
                     l = klass.get(id, name)
--- a/test/test_dates.py	Sat Apr 17 01:50:00 2004 +0000
+++ b/test/test_dates.py	Sun Apr 18 05:31:03 2004 +0000
@@ -15,7 +15,7 @@
 # BASIS, AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
 # SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
 # 
-# $Id: test_dates.py,v 1.31 2003-12-04 23:06:53 richard Exp $
+# $Id: test_dates.py,v 1.32 2004-04-18 05:31:03 richard Exp $
 from __future__ import nested_scopes
 
 import unittest, time
@@ -265,14 +265,14 @@
         ae(str(Interval('1y') + Interval('1y')), '+ 2y')
         ae(str(Interval('1y') + Interval('1m')), '+ 1y 1m')
         ae(str(Interval('1y') + Interval('2:40')), '+ 1y 2:40')
-        ae(str(Interval('1y') + Interval('- 1y')), '')
-        ae(str(Interval('- 1y') + Interval('1y')), '')
+        ae(str(Interval('1y') + Interval('- 1y')), '00:00')
+        ae(str(Interval('- 1y') + Interval('1y')), '00:00')
         ae(str(Interval('- 1y') + Interval('- 1y')), '- 2y')
         ae(str(Interval('1y') + Interval('- 1m')), '+ 11m')
         ae(str(Interval('1:00') + Interval('1:00')), '+ 2:00')
         ae(str(Interval('0:50') + Interval('0:50')), '+ 1:40')
-        ae(str(Interval('1:50') + Interval('- 1:50')), '')
-        ae(str(Interval('- 1:50') + Interval('1:50')), '')
+        ae(str(Interval('1:50') + Interval('- 1:50')), '00:00')
+        ae(str(Interval('- 1:50') + Interval('1:50')), '00:00')
         ae(str(Interval('- 1:50') + Interval('- 1:50')), '- 3:40')
         ae(str(Interval('1:59:59') + Interval('00:00:01')), '+ 2:00')
         ae(str(Interval('2:00') + Interval('- 00:00:01')), '+ 1:59:59')
@@ -282,11 +282,11 @@
         ae(str(Interval('1y') - Interval('- 1y')), '+ 2y')
         ae(str(Interval('1y') - Interval('- 1m')), '+ 1y 1m')
         ae(str(Interval('1y') - Interval('- 2:40')), '+ 1y 2:40')
-        ae(str(Interval('1y') - Interval('1y')), '')
+        ae(str(Interval('1y') - Interval('1y')), '00:00')
         ae(str(Interval('1y') - Interval('1m')), '+ 11m')
         ae(str(Interval('1:00') - Interval('- 1:00')), '+ 2:00')
         ae(str(Interval('0:50') - Interval('- 0:50')), '+ 1:40')
-        ae(str(Interval('1:50') - Interval('1:50')), '')
+        ae(str(Interval('1:50') - Interval('1:50')), '00:00')
         ae(str(Interval('1:59:59') - Interval('- 00:00:01')), '+ 2:00')
         ae(str(Interval('2:00') - Interval('00:00:01')), '+ 1:59:59')
 

Roundup Issue Tracker: http://roundup-tracker.org/