From 5f503d04273cd5f63f49a89d0dd2a201f58d91f6 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 14 Sep 2012 15:51:58 -0500 Subject: [PATCH] db.t.f.epoch(), thanks Niphlod --- VERSION | 2 +- gluon/dal.py | 119 +++++++++++++++++++++++----------------- gluon/tests/test_dal.py | 15 ++--- 3 files changed, 78 insertions(+), 58 deletions(-) diff --git a/VERSION b/VERSION index f3a131c4..18537964 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.0.9 (2012-09-14 09:42:11) stable +Version 2.0.9 (2012-09-14 15:51:53) stable diff --git a/gluon/dal.py b/gluon/dal.py index 73a4ce72..cb8bfb50 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -40,7 +40,7 @@ Example of usage: >>> # from dal import DAL, Field ### create DAL connection (and create DB if it doesn't exist) ->>> db = DAL(('sqlite://storage.sqlite','mysql://a:b@localhost/x'), +>>> db = DAL(('sqlite://storage.sqlite','mysql://a:b@localhost/x'), ... folder=None) ### define a table 'person' (create/alter as necessary) @@ -330,7 +330,7 @@ if not 'google' in DRIVERS: DRIVERS.append('Sybase(Sybase)') except ImportError: logger.debug('no Sybase driver') - + try: import kinterbasdb DRIVERS.append('Interbase(kinterbasdb)') @@ -342,7 +342,7 @@ if not 'google' in DRIVERS: import fdb DRIVERS.append('Firbird(fdb)') except ImportError: - logger.debug('no Firebird driver fdb') + logger.debug('no Firebird driver fdb') ##### try: import firebirdsql @@ -660,7 +660,7 @@ class BaseAdapter(ConnectionPool): def find_driver(self,adapter_args,uri=None): if hasattr(self,'driver') and self.driver!=None: - return + return drivers_available = [driver for driver in self.drivers if driver in globals()] if uri: @@ -668,7 +668,7 @@ class BaseAdapter(ConnectionPool): request_driver = items[1] if len(items)>1 else None else: request_driver = None - request_driver = request_driver or adapter_args.get('driver') + request_driver = request_driver or adapter_args.get('driver') if request_driver: if request_driver in drivers_available: self.driver_name = request_driver @@ -680,7 +680,7 @@ class BaseAdapter(ConnectionPool): self.driver = globals().get(self.driver_name) else: raise RuntimeError, "no driver available %s", self.drivers - + def __init__(self, db,uri,pool_size=0, folder=None, db_codec='UTF-8', credential_decoder=IDENTITY, driver_args={}, @@ -799,7 +799,7 @@ class BaseAdapter(ConnectionPool): 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, + ftype = ftype % dict(schema=schema, tablename=tablename, fieldname=field_name, srid=srid, dimension=dimension) @@ -890,7 +890,7 @@ class BaseAdapter(ConnectionPool): else: table._dbt = pjoin( dbpath, '%s_%s.table' % (table._db._uri_hash, tablename)) - + if table._dbt: table._loggername = pjoin(dbpath, 'sql.log') logfile = self.file_open(table._loggername, 'a') @@ -1002,7 +1002,7 @@ class BaseAdapter(ConnectionPool): query = ['ALTER TABLE %s DROP %s;' % (tablename, key)] metadata_change = True elif sql_fields[key]['sql'] != sql_fields_old[key]['sql'] \ - and not (key in table.fields and + and not (key in table.fields and isinstance(table[key].type, SQLCustomType)) \ and not sql_fields[key]['type'].startswith('reference')\ and not sql_fields[key]['type'].startswith('double')\ @@ -1073,6 +1073,9 @@ class BaseAdapter(ConnectionPool): def EXTRACT(self, first, what): return "EXTRACT(%s FROM %s)" % (what, self.expand(first)) + def EPOCH(self, first): + return self.EXTRACT(first, 'epoch') + def AGGREGATE(self, first, what): return "%s(%s)" % (what, self.expand(first)) @@ -1273,7 +1276,7 @@ class BaseAdapter(ConnectionPool): first = expression.first second = expression.second op = expression.op - if not second is None: + if not second is None: return op(first, second) elif not first is None: return op(first) @@ -1580,14 +1583,14 @@ class BaseAdapter(ConnectionPool): """ sql = self._select(query, fields, attributes) cache = attributes.get('cache', None) - if cache and attributes.get('cacheable',False): + if cache and attributes.get('cacheable',False): del attributes['cache'] (cache_model, time_expire) = cache key = self.uri + '/' + sql if len(key)>200: key = hashlib.md5(key).hexdigest() args = (sql,fields,attributes) return cache_model( - key, + key, lambda self=self,args=args:self._select_aux(*args), time_expire) else: @@ -1906,7 +1909,7 @@ class BaseAdapter(ConnectionPool): for (i,row) in enumerate(rows): new_row = Row() for (j,colname) in enumerate(colnames): - value = row[j] + value = row[j] tmp = tmps[j] if tmp: (tablename,fieldname,table,field,ft) = tmp @@ -1920,14 +1923,14 @@ class BaseAdapter(ConnectionPool): if field.filter_out: value = field.filter_out(value) colset[fieldname] = value - + # for backward compatibility if ft=='id' and fieldname!='id' and \ not 'id' in table.fields: colset['id'] = value if ft == 'id' and not cacheable: - # temporary hack to deal with + # temporary hack to deal with # GoogleDatastoreAdapter # references if isinstance(self, GoogleDatastoreAdapter): @@ -2027,8 +2030,11 @@ class SQLiteAdapter(BaseAdapter): 'second': (17, 19), } try: - (i, j) = table[lookup] - return int(s[i:j]) + if lookup != 'epoch': + (i, j) = table[lookup] + return int(s[i:j]) + else: + return time.mktime(datetime.datetime.strptime(s, '%Y-%m-%d %H:%M:%S').timetuple()) except: return None @@ -2280,6 +2286,9 @@ class MySQLAdapter(BaseAdapter): return 'SUBSTRING(%s,%s,%s)' % (self.expand(field), parameters[0], parameters[1]) + def EPOCH(self, first): + return "UNIX_TIMESTAMP(%s)" % self.expand(first) + def _drop(self,table,mode): # breaks db integrity but without this mysql does not drop table return ['SET FOREIGN_KEY_CHECKS=0;','DROP TABLE %s;' % table, @@ -2984,7 +2993,7 @@ class MSSQLAdapter(BaseAdapter): # (in the form of arg1=value1&arg2=value2&...) # Default values (drivers like FreeTDS insist on uppercase parameter keys) argsdict = { 'DRIVER':'{SQL Server}' } - urlargs = m.group('urlargs') or '' + urlargs = m.group('urlargs') or '' for argmatch in self.REGEX_ARGPATTERN.finditer(urlargs): argsdict[str(argmatch.group('argkey')).upper()] = argmatch.group('argvalue') urlargs = ';'.join(['%s=%s' % (ak, av) for (ak, av) in argsdict.iteritems()]) @@ -3009,6 +3018,9 @@ class MSSQLAdapter(BaseAdapter): return rows[minimum:] return rows[minimum:maximum] + def EPOCH(self, first): + return "DATEDIFF(second, '1970-01-01 00:00:00', %s)" % self.expand(first) + # GIS functions # No STAsGeoJSON in MSSQL @@ -3156,7 +3168,7 @@ class SybaseAdapter(MSSQLAdapter): logger.error('NdGpatch error') raise e else: - m = self.REGEX_URI.match(uri) + m = self.REGEX_URI.match(uri) if not m: raise SyntaxError, \ "Invalid URI string in DAL: %s" % self.uri @@ -3228,6 +3240,9 @@ class FireBirdAdapter(BaseAdapter): def RANDOM(self): return 'RAND()' + def EPOCH(self, first): + return "DATEDIFF(second, '1970-01-01 00:00:00', %s)" % self.expand(first) + def NOT_NULL(self,default,field_type): return 'DEFAULT %s NOT NULL' % self.represent(default,field_type) @@ -3252,7 +3267,7 @@ class FireBirdAdapter(BaseAdapter): def __init__(self,db,uri,pool_size=0,folder=None,db_codec ='UTF-8', credential_decoder=IDENTITY, driver_args={}, - adapter_args={}): + adapter_args={}): self.db = db self.dbengine = "firebird" self.uri = uri @@ -3283,7 +3298,7 @@ class FireBirdAdapter(BaseAdapter): user = credential_decoder(user), password = credential_decoder(password), charset = charset) - + def connect(driver_args=driver_args): return self.driver.connect(**driver_args) self.pool_connection(connect) @@ -3343,7 +3358,7 @@ class FireBirdEmbeddedAdapter(FireBirdAdapter): user=credential_decoder(user), password=credential_decoder(password), charset=charset) - + def connect(driver_args=driver_args): return self.driver.connect(**driver_args) self.pool_connection(connect) @@ -4416,7 +4431,7 @@ class GoogleDatastoreAdapter(NoSQLAdapter): else: projection.append(f.name) - # projection's can't include 'id'. + # projection's can't include 'id'. # it will be added to the result later query_projection = [ p for p in projection if \ @@ -4912,7 +4927,7 @@ class MongoDBAdapter(NoSQLAdapter): print "in expand and this is a query" # any query using 'id':= # set name as _id (as per pymongo/mongodb primary key) - # convert second arg to an objectid field + # convert second arg to an objectid field # (if its not already) # if second arg is 0 convert to objectid if isinstance(expression.first,Field) and \ @@ -4922,9 +4937,9 @@ class MongoDBAdapter(NoSQLAdapter): not isinstance(expression.second,ObjectId): if isinstance(expression.second,int): try: - # Because the reference field is by default - # an integer and therefore this must be an - # integer to be able to work with other + # Because the reference field is by default + # an integer and therefore this must be an + # integer to be able to work with other # databases expression.second = ObjectId(("%X" % expression.second)) except: @@ -5212,27 +5227,27 @@ class MongoDBAdapter(NoSQLAdapter): return result def ADD(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '%s + %s' % (self.expand(first), self.expand(second, first.type)) def SUB(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s - %s)' % (self.expand(first), self.expand(second, first.type)) def MUL(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s * %s)' % (self.expand(first), self.expand(second, first.type)) def DIV(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s / %s)' % (self.expand(first), self.expand(second, first.type)) def MOD(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s %% %s)' % (self.expand(first), self.expand(second, first.type)) def AS(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '%s AS %s' % (self.expand(first), second) #We could implement an option that simulates a full featured SQL database. But I think the option should be set explicit or implemented as another library. @@ -5344,31 +5359,31 @@ class MongoDBAdapter(NoSQLAdapter): #TODO javascript has math def ADD(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '%s + %s' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def SUB(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s - %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def MUL(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s * %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def DIV(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s / %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript has math def MOD(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '(%s %% %s)' % (self.expand(first), self.expand(second, first.type)) #TODO javascript can do this def AS(self, first, second): - raise NotImplementedError, "This must yet be replaced with javescript in order to accomplish this. Sorry" + raise NotImplementedError, "This must yet be replaced with javascript in order to accomplish this. Sorry" return '%s AS %s' % (self.expand(first), second) #We could implement an option that simulates a full featured SQL database. But I think the option should be set explicit or implemented as another library. @@ -6455,7 +6470,7 @@ class Row(object): def values(self): return self.__dict__.values() - + def __iter__(self): return self.__dict__.__iter__() @@ -6648,7 +6663,7 @@ class DAL(object): db.define_table('tablename', Field('fieldname1'), Field('fieldname2')) """ - + @staticmethod def set_folder(folder): """ @@ -7211,7 +7226,7 @@ def index(): Added 2012-08-24 "fields" and "colnames" optional arguments. If either is provided, the results cursor returned by the DB driver will be converted to a DAL Rows object using the db._adapter.parse() method. - + The "fields" argument is a list of DAL Field objects that match the fields returned from the DB. The Field objects should be part of one or more Table objects defined on the DAL object. The "fields" list can @@ -7224,14 +7239,14 @@ def index(): can be specified as a list of field names in tablename.fieldname format. Again, these should represent tables and fields defined on the DAL object. - + It is also possible to specify both "fields" and the associated "colnames". In that case, "fields" can also include DAL Expression objects in addition to Field objects. For Field objects in "fields", the associated "colnames" must still be in tablename.fieldname format. For Expression objects in "fields", the associated "colnames" can be any arbitrary labels. - + Note, the DAL Table objects referred to by "fields" or "colnames" can be dummy tables and do not have to represent any real tables in the database. Also, note that the "fields" and "colnames" must be in the @@ -7390,7 +7405,7 @@ class Table(object): db.users.insert(name='me') # print db.users._insert(...) to see SQL db.users.drop() """ - + def __init__( self, db, @@ -8075,6 +8090,10 @@ class Expression(object): db = self.db return Expression(db, db._adapter.EXTRACT, self, 'second', 'integer') + def epoch(self): + db = self.db + return Expression(db, db._adapter.EPOCH, self, None, 'integer') + def __getslice__(self, start, stop): db = self.db if start < 0: @@ -8215,7 +8234,7 @@ class Expression(object): def st_asgeojson(self, precision=15, options=0, version=1): return Expression(self.db, self.db._adapter.ST_ASGEOJSON, self, - dict(precision=precision, options=options, + dict(precision=precision, options=options, version=version), 'dict') def st_astext(self): @@ -8464,7 +8483,7 @@ class Field(Expression): elif not filename: filename = file.name filename = os.path.basename(filename.replace('/', os.sep)\ - .replace('\\', os.sep)) + .replace('\\', os.sep)) m = REGEX_STORE_PATTERN.search(filename) extension = m and m.group('e') or 'txt' uuid_key = web2py_uuid().replace('-', '')[-16:] @@ -8752,7 +8771,7 @@ class Set(object): key, (lambda self=self,distinct=distinct: \ db._adapter.count(self.query,distinct)), - time_expire) + time_expire) return db._adapter.count(self.query,distinct) def select(self, *fields, **attributes): @@ -8870,7 +8889,7 @@ class RecordUpdater(object): self.colset, self.table, self.id = colset, table, id def __call__(self, **fields): - colset, table, id = self.colset, self.table, self.id + colset, table, id = self.colset, self.table, self.id newfields = fields or dict(colset) for fieldname in newfields.keys(): if not fieldname in table.fields or table[fieldname].type=='id': @@ -9192,7 +9211,7 @@ class Rows(object): def xml(self,strict=False,row_name='row',rows_name='rows'): """ serializes the table using sqlhtml.SQLTABLE (if present) - """ + """ if strict: ncols = len(self.colnames) def f(row,field,indent=' '): diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 688408a0..fee8842f 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -311,12 +311,13 @@ class TestDatetime(unittest.TestCase): 9, 30)), 3) self.assertEqual(len(db(db.t.a == datetime.datetime(1971, 12, 21, 11, 30)).select()), 1) - self.assertEqual(len(db(db.t.a.year() == 1971).select()), 2) - self.assertEqual(len(db(db.t.a.month() == 12).select()), 2) - self.assertEqual(len(db(db.t.a.day() == 21).select()), 3) - self.assertEqual(len(db(db.t.a.hour() == 11).select()), 1) - self.assertEqual(len(db(db.t.a.minutes() == 30).select()), 3) - self.assertEqual(len(db(db.t.a.seconds() == 0).select()), 3) + self.assertEqual(db(db.t.a.year() == 1971).count(), 2) + self.assertEqual(db(db.t.a.month() == 12).count(), 2) + self.assertEqual(db(db.t.a.day() == 21).count(), 3) + self.assertEqual(db(db.t.a.hour() == 11).count(), 1) + self.assertEqual(db(db.t.a.minutes() == 30).count(), 3) + self.assertEqual(db(db.t.a.seconds() == 0).count(), 3) + self.assertEqual(db(db.t.a.epoch()<365*24*3600).count(),1) db.t.drop() @@ -329,7 +330,7 @@ class TestExpressions(unittest.TestCase): self.assertEqual(db.t.insert(a=2), 2) self.assertEqual(db.t.insert(a=3), 3) self.assertEqual(db(db.t.a == 3).update(a=db.t.a + 1), 1) - self.assertEqual(len(db(db.t.a == 4).select()), 1) + self.assertEqual(db(db.t.a == 4).count(), 1) db.t.drop()