diff --git a/VERSION b/VERSION index c4dfb583..d75c30d9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.7.4-stable+timestamp.2013.10.20.16.41.51 +Version 2.7.4-stable+timestamp.2013.10.20.15.42.03 diff --git a/gluon/dal.py b/gluon/dal.py index bd2988ba..8820f83c 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -195,7 +195,8 @@ CALLABLETYPES = (types.LambdaType, types.FunctionType, TABLE_ARGS = set( ('migrate','primarykey','fake_migrate','format','redefine', 'singular','plural','trigger_name','sequence_name','fields', - 'common_filter','polymodel','table_class','on_define','actual_name')) + 'common_filter','polymodel','table_class','on_define','actual_name', + 'rname')) SELECT_ARGS = set( ('orderby', 'groupby', 'limitby','required', 'cache', 'left', @@ -864,11 +865,12 @@ class BaseAdapter(ConnectionPool): id_fieldname = table._id.name else: #make a guess id_fieldname = 'id' + real_referenced = db[referenced]._rname or db[referenced] ftype = types[field_type[:9]] % dict( index_name = field_name+'__idx', field_name = field_name, constraint_name = constraint_name, - foreign_key = '%s (%s)' % (referenced, + foreign_key = '%s (%s)' % (real_referenced, id_fieldname), on_delete_action=field.ondelete) elif field_type.startswith('list:reference'): @@ -965,13 +967,16 @@ class BaseAdapter(ConnectionPool): foreign_key = ', '.join(pkeys), on_delete_action = field.ondelete) + #if there's a _rname, let's use that instead + table_rname = table._rname or tablename + if getattr(table,'_primarykey',None): query = "CREATE TABLE %s(\n %s,\n %s) %s" % \ - (tablename, fields, + (table_rname, fields, self.PRIMARY_KEY(', '.join(table._primarykey)),other) else: query = "CREATE TABLE %s(\n %s\n)%s" % \ - (tablename, fields, other) + (table_rname, fields, other) if self.uri.startswith('sqlite:///') \ or self.uri.startswith('spatialite:///'): @@ -1023,10 +1028,12 @@ class BaseAdapter(ConnectionPool): raise RuntimeError('File %s appears corrupted' % table._dbt) self.file_close(tfile) if sql_fields != sql_fields_old: - self.migrate_table(table, - sql_fields, sql_fields_old, - sql_fields_aux, None, - fake_migrate=fake_migrate) + self.migrate_table( + table, + sql_fields, sql_fields_old, + sql_fields_aux, None, + fake_migrate=fake_migrate + ) return query def migrate_table( @@ -1211,7 +1218,8 @@ class BaseAdapter(ConnectionPool): return 'PRIMARY KEY(%s)' % key def _drop(self, table, mode): - return ['DROP TABLE %s;' % table] + table_rname = table._rname or table + return ['DROP TABLE %s;' % table_rname] def drop(self, table, mode=''): db = table._db @@ -1229,15 +1237,17 @@ class BaseAdapter(ConnectionPool): self.log('success!\n', table) def _insert(self, table, fields): + table_rname = table._rname or table if fields: keys = ','.join(f.name for f, v in fields) values = ','.join(self.expand(v, f.type) for f, v in fields) - return 'INSERT INTO %s(%s) VALUES (%s);' % (table, keys, values) + return 'INSERT INTO %s(%s) VALUES (%s);' % (table_rname, keys, values) else: return self._insert_empty(table) def _insert_empty(self, table): - return 'INSERT INTO %s DEFAULT VALUES;' % table + table_rname = table._rname or table + return 'INSERT INTO %s DEFAULT VALUES;' % table_rname def insert(self, table, fields): query = self._insert(table,fields) @@ -1393,9 +1403,10 @@ class BaseAdapter(ConnectionPool): return '%s AS %s' % (self.expand(first), second) def ON(self, first, second): + table_rname = first._ot and first or first._rname or first._tablename if use_common_filters(second): second = self.common_filter(second,[first._tablename]) - return '%s ON %s' % (self.expand(first), self.expand(second)) + return '%s ON %s' % (self.expand(table_rname), self.expand(second)) def INVERT(self, first): return '%s DESC' % self.expand(first) @@ -1406,9 +1417,14 @@ class BaseAdapter(ConnectionPool): def CAST(self, first, second): return 'CAST(%s AS %s)' % (first, second) - def expand(self, expression, field_type=None): + def expand(self, expression, field_type=None, colnames=False): if isinstance(expression, Field): - out = '%s.%s' % (expression.table._tablename, expression.name) + et = expression.table + if not colnames: + table_rname = et._ot and et._tablename or et._rname or et._tablename + else: + table_rname = et._tablename + out = '%s.%s' % (table_rname, expression.name) if field_type == 'string' and not expression.type in ( 'string','text','json','password'): out = self.CAST(out, self.types['text']) @@ -1440,7 +1456,9 @@ class BaseAdapter(ConnectionPool): return str(expression) def table_alias(self,name): - return str(name if isinstance(name,Table) else self.db[name]) + if not isinstance(name, Table): + name = self.db[name]._rname or self.db[name] + return str(name) def alias(self, table, alias): """ @@ -1448,7 +1466,7 @@ class BaseAdapter(ConnectionPool): with alias name. """ other = copy.copy(table) - other['_ot'] = other._ot or other._tablename + other['_ot'] = other._ot or other._rname or other._tablename other['ALL'] = SQLALL(other) other['_tablename'] = alias for fieldname in other.fields: @@ -1460,7 +1478,7 @@ class BaseAdapter(ConnectionPool): return other def _truncate(self, table, mode=''): - tablename = table._tablename + tablename = table._rname or table._tablename return ['TRUNCATE TABLE %s %s;' % (tablename, mode or '')] def truncate(self, table, mode= ' '): @@ -1485,7 +1503,7 @@ class BaseAdapter(ConnectionPool): sql_v = ','.join(['%s=%s' % (field.name, self.expand(value, field.type)) \ for (field, value) in fields]) - tablename = "%s" % self.db[tablename] + tablename = "%s" % (self.db[tablename]._rname or tablename) return 'UPDATE %s SET %s%s;' % (tablename, sql_v, sql_w) def update(self, tablename, query, fields): @@ -1510,6 +1528,7 @@ class BaseAdapter(ConnectionPool): sql_w = ' WHERE ' + self.expand(query) else: sql_w = '' + tablename = '%s' % (self.db[tablename]._rname or tablename) return 'DELETE FROM %s%s;' % (tablename, sql_w) def delete(self, tablename, query): @@ -1583,7 +1602,9 @@ class BaseAdapter(ConnectionPool): if len(tablenames) < 1: raise SyntaxError('Set: no tables selected') - self._colnames = map(self.expand, fields) + def colexpand(field): + return self.expand(field, colnames=True) + self._colnames = map(colexpand, fields) def geoexpand(field): if isinstance(field.type,str) and field.type.startswith('geometry'): field = field.st_astext() @@ -1692,7 +1713,13 @@ class BaseAdapter(ConnectionPool): else: sql_o += ' ORDER BY %s' % self.expand(orderby) if (limitby and not groupby and tablenames and orderby_on_limitby and not orderby): - sql_o += ' ORDER BY %s' % ', '.join(['%s.%s'%(t,x) for t in tablenames for x in (hasattr(self.db[t],'_primarykey') and self.db[t]._primarykey or [self.db[t]._id.name])]) + sql_o += ' ORDER BY %s' % ', '.join( + ['%s.%s'%(t,x) for t in tablenames for x in ( + hasattr(self.db[t],'_primarykey') and self.db[t]._primarykey + or [self.db[t]._id.name] + ) + ] + ) # oracle does not support limitby sql = self.select_limitby(sql_s, sql_f, sql_t, sql_w, sql_o, limitby) if for_update and self.can_select_for_update is True: @@ -2508,7 +2535,8 @@ class MySQLAdapter(BaseAdapter): 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, + table_rname = table._rname or table + return ['SET FOREIGN_KEY_CHECKS=0;','DROP TABLE %s;' % table_rname, 'SET FOREIGN_KEY_CHECKS=1;'] def _insert_empty(self, table): @@ -2616,7 +2644,7 @@ class PostgreSQLAdapter(BaseAdapter): } - QUOTE_TEMPLATE = '%s' + QUOTE_TEMPLATE = '"%s"' def varquote(self,name): return varquote_aux(name,'"%s"') @@ -2630,7 +2658,7 @@ class PostgreSQLAdapter(BaseAdapter): return "'%s'" % str(obj).replace("'","''") def sequence_name(self,table): - return '%s_id_Seq' % table + return '%s_id_seq' % table def RANDOM(self): return 'RANDOM()' @@ -2720,7 +2748,7 @@ class PostgreSQLAdapter(BaseAdapter): self.try_json() def lastrowid(self,table): - self.execute("select currval('%s')" % table._sequence_name) + self.execute("""select currval('"%s"')""" % table._sequence_name) return int(self.cursor.fetchone()[0]) def try_json(self): @@ -3369,7 +3397,7 @@ class MSSQL3Adapter(MSSQLAdapter): class MSSQL4Adapter(MSSQLAdapter): """ support for true pagination in MSSQL >= 2012""" - + def select_limitby(self, sql_s, sql_f, sql_t, sql_w, sql_o, limitby): if not sql_o: #if there is no orderby, we can't use the brand new statements @@ -3382,7 +3410,7 @@ class MSSQL4Adapter(MSSQLAdapter): (sql_s, sql_f, sql_t, sql_w, sql_o) def rowslice(self,rows,minimum=0,maximum=None): - return rows + return rows class MSSQL2Adapter(MSSQLAdapter): drivers = ('pyodbc',) @@ -6593,7 +6621,7 @@ class IMAPAdapter(NoSQLAdapter): else: message[item] = ";".join([i for i in value]) - if (not message.is_multipart() and + if (not message.is_multipart() and (not message.get_content_type().startswith( "multipart"))): if isinstance(content, basestring): @@ -8357,8 +8385,14 @@ class Table(object): self._actual = False # set to True by define_table() self._tablename = tablename self._ot = args.get('actual_name') - self._sequence_name = args.get('sequence_name') or \ - db and db._adapter.sequence_name(tablename) + self._rname = args.get('rname') + if not self._rname: + self._sequence_name = args.get('sequence_name') or \ + db and db._adapter.sequence_name(tablename) + else: + tb = self._rname[1:-1] + self._sequence_name = args.get('sequence_name') or \ + db and db._adapter.sequence_name(tb) self._trigger_name = args.get('trigger_name') or \ db and db._adapter.trigger_name(tablename) self._common_filter = args.get('common_filter') @@ -8503,10 +8537,10 @@ class Table(object): clones.append(field.clone( unique=False, type=field.type if nfk else 'bigint')) archive_db.define_table( - archive_name, + archive_name, Field(current_record,field_type,label=current_record_label), *clones,**dict(format=self._format)) - + self._before_update.append( lambda qset,fs,db=archive_db,an=archive_name,cn=current_record: archive_record(qset,fs,db[an],cn)) @@ -8690,7 +8724,7 @@ class Table(object): def __str__(self): if self._ot is not None: - ot = self._db._adapter.QUOTE_TEMPLATE % self._ot + ot = self._ot if 'Oracle' in str(type(self._db._adapter)): return '%s %s' % (ot, self._tablename) return '%s AS %s' % (ot, self._tablename) diff --git a/gluon/tests/test_dal.py b/gluon/tests/test_dal.py index 0646727d..bea336e1 100644 --- a/gluon/tests/test_dal.py +++ b/gluon/tests/test_dal.py @@ -836,6 +836,158 @@ class TestValidateAndInsert(unittest.TestCase): db.val_and_insert.drop() +class TestZRName(unittest.TestCase): + """ + tests for highly experimental rname attribute + """ + def testSelect(self): + db = DAL(DEFAULT_URI, check_reserved=['all']) + rname = db._adapter.QUOTE_TEMPLATE % 'a very complicated tablename' + db.define_table( + 'easy_name', + Field('a_field'), + rname=rname + ) + rtn = db.easy_name.insert(a_field='a') + self.assertEqual(rtn.id, 1) + rtn = db(db.easy_name.a_field == 'a').select() + self.assertEqual(len(rtn), 1) + self.assertEqual(rtn[0].id, 1) + self.assertEqual(rtn[0].a_field, 'a') + db.easy_name.insert(a_field='b') + rtn = db(db.easy_name.id > 0).delete() + self.assertEqual(rtn, 2) + rtn = db(db.easy_name.id > 0).count() + self.assertEqual(rtn, 0) + db.easy_name.insert(a_field='a') + db.easy_name.insert(a_field='b') + rtn = db(db.easy_name.id > 0).count() + self.assertEqual(rtn, 2) + rtn = db(db.easy_name.a_field == 'a').update(a_field='c') + rtn = db(db.easy_name.a_field == 'c').count() + self.assertEqual(rtn, 1) + rtn = db(db.easy_name.a_field != 'c').count() + self.assertEqual(rtn, 1) + avg = db.easy_name.id.avg() + rtn = db(db.easy_name.id > 0).select(avg) + self.assertEqual(rtn[0][avg], 3) + rname = db._adapter.QUOTE_TEMPLATE % 'this is the person table' + db.define_table( + 'person', + Field('name', default="Michael"), + Field('uuid'), + rname=rname + ) + rname = db._adapter.QUOTE_TEMPLATE % 'this is the pet table' + db.define_table( + 'pet', + Field('friend','reference person'), + Field('name'), + rname=rname + ) + michael = db.person.insert() #default insert + john = db.person.insert(name='John') + luke = db.person.insert(name='Luke') + + #michael owns Phippo + phippo = db.pet.insert(friend=michael, name="Phippo") + #john owns Dunstin and Gertie + dunstin = db.pet.insert(friend=john, name="Dunstin") + gertie = db.pet.insert(friend=john, name="Gertie") + + rtn = db(db.person.id == db.pet.friend).select(orderby=db.person.id|db.pet.id) + self.assertEqual(len(rtn), 3) + self.assertEqual(rtn[0].person.id, michael) + self.assertEqual(rtn[0].person.name, 'Michael') + self.assertEqual(rtn[0].pet.id, phippo) + self.assertEqual(rtn[0].pet.name, 'Phippo') + self.assertEqual(rtn[1].person.id, john) + self.assertEqual(rtn[1].person.name, 'John') + self.assertEqual(rtn[1].pet.name, 'Dunstin') + self.assertEqual(rtn[2].pet.name, 'Gertie') + #fetch owners, eventually with pet + #main point is retrieving Luke with no pets + rtn = db(db.person.id > 0).select( + orderby=db.person.id|db.pet.id, + left=db.pet.on(db.person.id == db.pet.friend) + ) + self.assertEqual(rtn[0].person.id, michael) + self.assertEqual(rtn[0].person.name, 'Michael') + self.assertEqual(rtn[0].pet.id, phippo) + self.assertEqual(rtn[0].pet.name, 'Phippo') + self.assertEqual(rtn[3].person.name, 'Luke') + self.assertEqual(rtn[3].person.id, luke) + self.assertEqual(rtn[3].pet.name, None) + #lets test a subquery + subq = db(db.pet.name == "Gertie")._select(db.pet.friend) + rtn = db(db.person.id.belongs(subq)).select() + self.assertEqual(rtn[0].id, 2) + self.assertEqual(rtn[0]('person.name'), 'John') + #as dict + rtn = db(db.person.id > 0).select().as_dict() + self.assertEqual(rtn[1]['name'], 'Michael') + #as list + rtn = db(db.person.id > 0).select().as_list() + self.assertEqual(rtn[0]['name'], 'Michael') + #isempty + rtn = db(db.person.id > 0).isempty() + self.assertEqual(rtn, False) + #join argument + rtn = db(db.person).select(orderby=db.person.id|db.pet.id, + join=db.pet.on(db.person.id==db.pet.friend)) + self.assertEqual(len(rtn), 3) + self.assertEqual(rtn[0].person.id, michael) + self.assertEqual(rtn[0].person.name, 'Michael') + self.assertEqual(rtn[0].pet.id, phippo) + self.assertEqual(rtn[0].pet.name, 'Phippo') + self.assertEqual(rtn[1].person.id, john) + self.assertEqual(rtn[1].person.name, 'John') + self.assertEqual(rtn[1].pet.name, 'Dunstin') + self.assertEqual(rtn[2].pet.name, 'Gertie') + + #aliases + rname = db._adapter.QUOTE_TEMPLATE % 'the cubs' + db.define_table('pet_farm', + Field('name'), + Field('father','reference pet_farm'), + Field('mother','reference pet_farm'), + rname=rname + ) + + minali = db.pet_farm.insert(name='Minali') + osbert = db.pet_farm.insert(name='Osbert') + #they had a cub + selina = db.pet_farm.insert(name='Selina', father=osbert, mother=minali) + + father = db.pet_farm.with_alias('father') + mother = db.pet_farm.with_alias('mother') + + #fetch pets with relatives + rtn = db().select( + db.pet_farm.name, father.name, mother.name, + left=[ + father.on(father.id == db.pet_farm.father), + mother.on(mother.id == db.pet_farm.mother) + ], + orderby=db.pet_farm.id + ) + + self.assertEqual(len(rtn), 3) + self.assertEqual(rtn[0].pet_farm.name, 'Minali') + self.assertEqual(rtn[0].father.name, None) + self.assertEqual(rtn[0].mother.name, None) + self.assertEqual(rtn[1].pet_farm.name, 'Osbert') + self.assertEqual(rtn[2].pet_farm.name, 'Selina') + self.assertEqual(rtn[2].father.name, 'Osbert') + self.assertEqual(rtn[2].mother.name, 'Minali') + + #clean up + db.pet_farm.drop() + db.pet.drop() + db.person.drop() + db.easy_name.drop() + + if __name__ == '__main__': unittest.main()