Index: django/conf/project_template/settings.py =================================================================== --- django/conf/project_template/settings.py (revision 7398) +++ django/conf/project_template/settings.py (working copy) @@ -9,7 +9,7 @@ MANAGERS = ADMINS -DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. +DATABASE_ENGINE = '' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3', 'oracle' or 'firebird'. DATABASE_NAME = '' # Or path to database file if using sqlite3. DATABASE_USER = '' # Not used with sqlite3. DATABASE_PASSWORD = '' # Not used with sqlite3. Index: django/db/models/sql/query.py =================================================================== --- django/db/models/sql/query.py (revision 7398) +++ django/db/models/sql/query.py (working copy) @@ -113,7 +113,7 @@ if name in self.quote_cache: return self.quote_cache[name] if ((name in self.alias_map and name not in self.table_map) or - name in self.extra_select): + name in self.extra_select) and not self.connection.features.always_quote: self.quote_cache[name] = name return name r = self.connection.ops.quote_name(name) @@ -187,6 +187,7 @@ distinct=False) obj.select = [] obj.extra_select = {} + obj.add_count_column() data = obj.execute_sql(SINGLE) if not data: @@ -242,7 +243,17 @@ result.append('GROUP BY %s' % ', '.join(grouping)) if ordering: - result.append('ORDER BY %s' % ', '.join(ordering)) + if self.connection.features.order_by_ordinal: + ordinals = {} + for i, c in enumerate(out_cols): + try: + c = c.split(' AS ')[1] + except IndexError: + pass + ordinals[c] = i + 1 + result.append('ORDER BY %s' % ', '.join([ '%s %s' % (ordinals.get(o.split()[0], o.split()[0]), o.split()[1]) for o in ordering])) + else: + result.append('ORDER BY %s' % ', '.join(ordering)) # FIXME: Pull this out to make life easier for Oracle et al. if with_limits: @@ -354,7 +365,35 @@ the model. """ qn = self.quote_name_unless_alias - result = ['(%s) AS %s' % (col, alias) for alias, col in self.extra_select.items()] + if self.connection.features.always_quote: + result = [] + for alias, col in self.extra_select.items(): + if isinstance(col, basestring): + if col.find('+') != -1: + qn_col = [c.strip() for c in col.split('+')] + qn_col[0] = '.'.join((qn(c) for c in qn_col[0].split('.'))) + col = ' + '.join(qn_col) + elif col.find('>') != -1: + qn_col = [c.strip() for c in col.split('>')] + qn_col[0] = '.'.join((qn(c) for c in qn_col[0].split('.'))) + #col = ' > '.join(qn_col) + qn_table = qn(self.model._meta.db_table) + col = 'SELECT COUNT(*) FROM %s WHERE %s > %s' % (qn_table, qn_col[0], qn_col[1]) + elif col.find('<') != -1: + qn_col = [c.strip() for c in col.split('<')] + qn_col[0] = '.'.join((qn(c) for c in qn_col[0].split('.'))) + #col = ' < '.join(qn_col) + col = 'SELECT COUNT(*) FROM %s WHERE %s < %s' % (qn_table, qn_col[0], qn_col[1]) + else: + qn_col = col.split() + for i, qc in enumerate(qn_col): + if qc.lower() not in ('select', 'count(*)', 'from', 'where', "=", ">", "<"): + qn_col[i] = '.'.join((qn(c) for c in qn_col[i].split('.'))) + col = ' '.join(qn_col) + + result.append('(%s) AS %s' % (col, qn(alias))) + else: + result = ['(%s) AS %s' % (col, alias) for alias, col in self.extra_select.items()] aliases = self.extra_select.keys() if self.select: for col in self.select: @@ -419,14 +458,26 @@ if not self.alias_refcount[alias]: continue name, alias, join_type, lhs, lhs_col, col, nullable = self.alias_map[alias] - alias_str = (alias != name and ' AS %s' % alias or '') + + if self.connection.features.always_quote: + aliased = (alias != name and '%s %s' % (qn(name), qn(alias)) or qn(name)) + else: + alias_str = (alias != name and ' AS %s' % alias or '') if join_type and not first: - result.append('%s %s%s ON (%s.%s = %s.%s)' + if self.connection.features.always_quote: + result.append('%s %s ON (%s.%s = %s.%s)' + % (join_type, aliased, qn(lhs), + qn2(lhs_col), qn(alias), qn2(col))) + else: + result.append('%s %s%s ON (%s.%s = %s.%s)' % (join_type, qn(name), alias_str, qn(lhs), qn2(lhs_col), qn(alias), qn2(col))) else: connector = not first and ', ' or '' - result.append('%s%s%s' % (connector, qn(name), alias_str)) + if self.connection.features.always_quote: + result.append('%s%s' % (connector, aliased)) + else: + result.append('%s%s%s' % (connector, qn(name), alias_str)) first = False for t in self.extra_tables: alias, created = self.table_alias(t) Index: django/db/models/sql/subqueries.py =================================================================== --- django/db/models/sql/subqueries.py (revision 7398) +++ django/db/models/sql/subqueries.py (working copy) @@ -6,12 +6,19 @@ from django.core.exceptions import FieldError from django.db.models.sql.constants import * from django.db.models.sql.datastructures import RawValue, Date -from django.db.models.sql.query import Query +from django.db.models.sql.query import Query as _Query from django.db.models.sql.where import AND __all__ = ['DeleteQuery', 'UpdateQuery', 'InsertQuery', 'DateQuery', 'CountQuery'] +from django.db import connection +if connection.features.uses_custom_queryset: + #connection.cursor() + Query = connection.ops.query_class(_Query) +else: + Query = _Query + class DeleteQuery(Query): """ Delete queries are done through this class, since they are more constrained @@ -24,7 +31,8 @@ """ assert len(self.tables) == 1, \ "Can only delete from one table at a time." - result = ['DELETE FROM %s' % self.tables[0]] + qn = self.quote_name_unless_alias + result = ['DELETE FROM %s' % qn(self.tables[0])] where, params = self.where.as_sql() result.append('WHERE %s' % where) return ' '.join(result), tuple(params) @@ -349,6 +357,7 @@ multiple distinct columns and turn it into SQL that can be used on a variety of backends (it requires a select in the FROM clause). """ + import constants def get_from_clause(self): result, params = self._query.as_sql() return ['(%s) AS A1' % result], params @@ -356,3 +365,10 @@ def get_ordering(self): return () + def execute_sql(self, result_type=constants.SINGLE): + if Query is _Query: + return super(Query, self).execute_sql(result_type) + sql, params = self._query.as_sql() + cursor = self.connection.cursor() + cursor.execute(sql, params) + return len(cursor.fetchall()), Index: django/db/models/fields/__init__.py =================================================================== --- django/db/models/fields/__init__.py (revision 7398) +++ django/db/models/fields/__init__.py (working copy) @@ -6,7 +6,7 @@ except ImportError: from django.utils import _decimal as decimal # for Python 2.3 -from django.db import get_creation_module +from django.db import connection, get_creation_module from django.db.models import signals from django.dispatch import dispatcher from django.conf import settings @@ -87,7 +87,8 @@ editable=True, serialize=True, prepopulate_from=None, unique_for_date=None, unique_for_month=None, unique_for_year=None, validator_list=None, choices=None, radio_admin=None, help_text='', - db_column=None, db_tablespace=None, auto_created=False): + db_column=None, db_tablespace=None, auto_created=False, + encoding=None): self.name = name self.verbose_name = verbose_name self.primary_key = primary_key @@ -109,6 +110,7 @@ self.help_text = help_text self.db_column = db_column self.db_tablespace = db_tablespace or settings.DEFAULT_INDEX_TABLESPACE + self.encoding = encoding # Set db_index to True if the field has a relationship and doesn't explicitly set db_index. self.db_index = db_index @@ -223,6 +225,8 @@ def get_db_prep_lookup(self, lookup_type, value): "Returns field's value prepared for database lookup." + if connection.features.uses_custom_lookups: + return connection.ops.get_db_prep_lookup(lookup_type, value) if lookup_type in ('exact', 'regex', 'iregex', 'gt', 'gte', 'lt', 'lte', 'month', 'day', 'search'): return [value] elif lookup_type in ('range', 'in'): @@ -957,6 +961,15 @@ defaults.update(kwargs) return super(IPAddressField, self).formfield(**defaults) +class LargeTextField(Field): + def get_manipulator_field_objs(self): + return [oldforms.LargeTextField] + + def formfield(self, **kwargs): + defaults = {'widget': forms.Textarea} + defaults.update(kwargs) + return super(LargeTextField, self).formfield(**defaults) + class NullBooleanField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): @@ -1093,8 +1106,8 @@ # doesn't support microseconds. if settings.DATABASE_ENGINE == 'mysql' and hasattr(value, 'microsecond'): value = value.replace(microsecond=0) - if settings.DATABASE_ENGINE == 'oracle': - # cx_Oracle expects a datetime.datetime to persist into TIMESTAMP field. + if settings.DATABASE_ENGINE in ('oracle', 'firebird'): + # cx_Oracle and kinterbasdb expect a datetime.datetime to persist into TIMESTAMP field. if isinstance(value, datetime.time): value = datetime.datetime(1900, 1, 1, value.hour, value.minute, value.second, value.microsecond) Index: django/db/models/fields/related.py =================================================================== --- django/db/models/fields/related.py (revision 7398) +++ django/db/models/fields/related.py (working copy) @@ -376,18 +376,19 @@ new_ids.add(obj) # Add the newly created or already existing objects to the join table. # First find out which items are already added, to avoid adding them twice + qn = connection.ops.quote_name cursor = connection.cursor() cursor.execute("SELECT %s FROM %s WHERE %s = %%s AND %s IN (%s)" % \ - (target_col_name, self.join_table, source_col_name, - target_col_name, ",".join(['%s'] * len(new_ids))), + (qn(target_col_name), qn(self.join_table), qn(source_col_name), + qn(target_col_name), ",".join(['%s'] * len(new_ids))), [self._pk_val] + list(new_ids)) existing_ids = set([row[0] for row in cursor.fetchall()]) # Add the ones that aren't there already for obj_id in (new_ids - existing_ids): - cursor.execute("INSERT INTO %s (%s, %s) VALUES (%%s, %%s)" % \ - (self.join_table, source_col_name, target_col_name), - [self._pk_val, obj_id]) + cursor.execute('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' % \ + (qn(self.join_table), qn(source_col_name), qn(target_col_name)), + (self._pk_val, obj_id)) transaction.commit_unless_managed() def _remove_items(self, source_col_name, target_col_name, *objs): Index: django/db/backends/__init__.py =================================================================== --- django/db/backends/__init__.py (revision 7398) +++ django/db/backends/__init__.py (working copy) @@ -42,13 +42,16 @@ class BaseDatabaseFeatures(object): allows_group_by_ordinal = True allows_unique_and_pk = True + always_quote = False autoindexes_primary_keys = True inline_fk_references = True needs_datetime_string_cast = True needs_upper_for_iops = False + order_by_ordinal = False supports_constraints = True supports_tablespaces = False uses_case_insensitive_names = False + uses_custom_lookups = False uses_custom_queryset = False empty_fetchmany_value = [] @@ -215,6 +218,9 @@ """ raise NotImplementedError() + def reference_name(self, r_col, col, r_table, table): + return '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table)))) + def random_function_sql(self): """ Returns a SQL expression that returns a random value. Property changes on: django/db/backends/firebird ___________________________________________________________________ Name: svn:ignore + *.pyc Index: django/db/backends/firebird/base.py =================================================================== --- django/db/backends/firebird/base.py (revision 0) +++ django/db/backends/firebird/base.py (revision 0) @@ -0,0 +1,502 @@ +""" +Firebird database backend for Django. + +Requires KInterbasDB 3.2: http://kinterbasdb.sourceforge.net/ +The egenix mx (mx.DateTime) is NOT required + +Database charset should be UNICODE_FSS or UTF8 (FireBird 2.0+) +To use UTF8 encoding add FIREBIRD_CHARSET = 'UTF8' to your settings.py +UNICODE_FSS works with all versions and uses less memory +""" + +from django.db.backends import BaseDatabaseWrapper, BaseDatabaseFeatures, BaseDatabaseOperations, util +import sys +import datetime +try: + import decimal +except ImportError: + from django.utils import _decimal as decimal # for Python 2.3 + +try: + import kinterbasdb as Database + Database.init(type_conv=200) +except ImportError, e: + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured, "Error loading KInterbasDB module: %s" % e + +DatabaseError = Database.DatabaseError +IntegrityError = Database.IntegrityError + +from django.db.backends.firebird.creation import TEST_MODE + +class DatabaseFeatures(BaseDatabaseFeatures): + allows_unique_and_pk = False + always_quote = True + inline_fk_references = False + needs_datetime_string_cast = False + needs_upper_for_iops = True + order_by_ordinal = True + uses_custom_lookups = True + uses_custom_queryset = True + supports_constraints = TEST_MODE < 2 + +################################################################################ +# Database operations (db.connection.ops) +class DatabaseOperations(BaseDatabaseOperations): + """ + This class encapsulates all backend-specific differences, such as the way + a backend performs ordering or calculates the ID of a recently-inserted + row. + """ + # Utility ops: names, version, page size etc.: + _max_name_length = 31 + def __init__(self): + self._firebird_version = None + self._page_size = None + + def get_generator_name(self, name): + return '%s$G' % util.truncate_name(name.strip('"'), self._max_name_length-2).upper() + + def get_trigger_name(self, name): + return '%s$T' % util.truncate_name(name.strip('"'), self._max_name_length-2).upper() + + def _get_firebird_version(self): + if self._firebird_version is None: + from django.db import connection + if not connection.connection: + connection.cursor() + self._firebird_version = [int(val) for val in connection.server_version.split()[-1].split('.')] + return self._firebird_version + firebird_version = property(_get_firebird_version) + + def reference_name(self, r_col, col, r_table, table): + base_name = util.truncate_name('%s$%s' % (r_col, col), self._max_name_length-5) + return util.truncate_name(('%s$%x' % (base_name, abs(hash((r_table, table))))), self._max_name_length).upper() + + def _get_page_size(self): + if self._page_size is None: + from django.db import connection + self._page_size = connection.database_info(Database.isc_info_page_size, 'i') + return self._page_size + page_size = property(_get_page_size) + + def _get_index_limit(self): + if self.firebird_version[0] < 2: + self._index_limit = 252 + else: + page_size = self._get_page_size() + self._index_limit = page_size/4 + return self._index_limit + index_limit = property(_get_index_limit) + + def max_name_length(self): + return self._max_name_length + + def quote_name(self, name): + if not isinstance(name, basestring): + return name + elif name == '%s': + return '%s{{ FBLABEL }}' + name = '"%s"' % util.truncate_name(name.strip('"'), self._max_name_length) + return name + + def field_cast_sql(self, db_type): + return '%s' + + ############################################################################ + # Basic SQL ops: + def last_insert_id(self, cursor, table_name, pk_name=None): + generator_name = self.get_generator_name(table_name) + cursor.execute('SELECT GEN_ID(%s, 0) from RDB$DATABASE' % generator_name) + return cursor.fetchone()[0] + + def date_extract_sql(self, lookup_type, column_name): + # lookup_type is 'year', 'month', 'day' + return "EXTRACT(%s FROM %s)" % (lookup_type, column_name) + + def date_trunc_sql(self, lookup_type, column_name): + if lookup_type == 'year': + sql = "EXTRACT(year FROM %s)||'-01-01 00:00:00'" % column_name + elif lookup_type == 'month': + sql = "EXTRACT(year FROM %s)||'-'||EXTRACT(month FROM %s)||'-01 00:00:00'" % (column_name, column_name) + elif lookup_type == 'day': + sql = "EXTRACT(year FROM %s)||'-'||EXTRACT(month FROM %s)||'-'||EXTRACT(day FROM %s)||' 00:00:00'" % (column_name, column_name, column_name) + return "CAST(%s AS TIMESTAMP)" % sql + + #def datetime_cast_sql(self): + # return None + + def drop_sequence_sql(self, table): + return "DROP GENERATOR %s;" % self.get_generator_name(table) + + def drop_foreignkey_sql(self): + return "DROP CONSTRAINT" + + def limit_offset_sql(self, limit, offset=None): + return '' + + def random_function_sql(self): + return "rand()" + + def pk_default_value(self): + """ + Returns the value to use during an INSERT statement to specify that + the field should use its default value. + """ + return 'NULL' + + def lookup_cast(self, lookup_type): + if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): + return "UPPER(%s)" + return "%s" + + def start_transaction_sql(self): + return "" + + def fulltext_search_sql(self, field_name): + # We use varchar for TextFields so this is possible + # Look at http://www.volny.cz/iprenosil/interbase/ip_ib_strings.htm + return '%%s CONTAINING %s' % self.quote_name(field_name) + + ############################################################################ + # Advanced SQL ops: + def autoinc_sql(self, table_name, column_name): + """ + To simulate auto-incrementing primary keys in Firebird, we have to + create a generator and a trigger. + + Create the generators and triggers names based only on table name + since django only support one auto field per model + """ + generator_name = self.get_generator_name(table_name) + trigger_name = self.get_trigger_name(table_name) + column_name = self.quote_name(column_name) + table_name = self.quote_name(table_name) + + generator_sql = "CREATE GENERATOR %s;" % generator_name + trigger_sql = "\n".join([ + "CREATE TRIGGER %s FOR %s" % (trigger_name, table_name), + "ACTIVE BEFORE INSERT POSITION 0 AS", + "BEGIN", + " IF ((NEW.%s IS NULL) OR (NEW.%s = 0)) THEN" % (column_name, column_name), + " BEGIN", + " NEW.%s = GEN_ID(%s, 1);" % (column_name, generator_name), + " END", + "END;"]) + return (generator_sql, trigger_sql) + + def sequence_reset_sql(self, style, model_list): + from django.db import models + output = [] + sql = ['%s %s %s' % (style.SQL_KEYWORD('CREATE OR ALTER PROCEDURE'), + style.SQL_TABLE('"GENERATOR_RESET"'), + style.SQL_KEYWORD('AS'))] + sql.append('%s %s' % (style.SQL_KEYWORD('DECLARE VARIABLE'), style.SQL_COLTYPE('start_val integer;'))) + sql.append('%s %s' % (style.SQL_KEYWORD('DECLARE VARIABLE'), style.SQL_COLTYPE('gen_val integer;'))) + sql.append('\t%s' % style.SQL_KEYWORD('BEGIN')) + sql.append('\t\t%s %s %s %s %s %s;' % (style.SQL_KEYWORD('SELECT MAX'), style.SQL_FIELD('(%(col)s)'), + style.SQL_KEYWORD('FROM'), style.SQL_TABLE('%(table)s'), + style.SQL_KEYWORD('INTO'), style.SQL_COLTYPE(':start_val'))) + sql.append('\t\t%s (%s %s) %s' % (style.SQL_KEYWORD('IF'), style.SQL_COLTYPE('start_val'), + style.SQL_KEYWORD('IS NULL'), style.SQL_KEYWORD('THEN'))) + sql.append('\t\t\t%s = %s(%s, 1 - %s(%s, 0));' %\ + (style.SQL_COLTYPE('gen_val'), style.SQL_KEYWORD('GEN_ID'), style.SQL_TABLE('%(gen)s'), + style.SQL_KEYWORD('GEN_ID'), style.SQL_TABLE('%(gen)s'))) + sql.append('\t\t%s' % style.SQL_KEYWORD('ELSE')) + sql.append('\t\t\t%s = %s(%s, %s - %s(%s, 0));' %\ + (style.SQL_COLTYPE('gen_val'), style.SQL_KEYWORD('GEN_ID'), + style.SQL_TABLE('%(gen)s'), style.SQL_COLTYPE('start_val'), style.SQL_KEYWORD('GEN_ID'), + style.SQL_TABLE('%(gen)s'))) + sql.append('\t\t%s;' % style.SQL_KEYWORD('EXIT')) + sql.append('%s;' % style.SQL_KEYWORD('END')) + sql ="\n".join(sql) + for model in model_list: + for f in model._meta.fields: + if isinstance(f, models.AutoField): + generator_name = self.get_generator_name(model._meta.db_table) + column_name = self.quote_name(f.db_column or f.name) + table_name = self.quote_name(model._meta.db_table) + output.append(sql % {'col' : column_name, 'table' : table_name, 'gen' : generator_name}) + output.append('%s %s;' % (style.SQL_KEYWORD('EXECUTE PROCEDURE'), + style.SQL_TABLE('"GENERATOR_RESET"'))) + break # Only one AutoField is allowed per model, so don't bother continuing. + for f in model._meta.many_to_many: + generator_name = self.get_generator_name(f.m2m_db_table()) + table_name = self.quote_name(f.m2m_db_table()) + column_name = '"id"' + output.append(sql % {'col' : column_name, 'table' : table_name, 'gen' : generator_name}) + output.append('%s %s;' % (style.SQL_KEYWORD('EXECUTE PROCEDURE'), + style.SQL_TABLE('"GENERATOR_RESET"'))) + return output + + def sql_flush(self, style, tables, sequences): + if tables: + sql = ['%s %s %s;' % \ + (style.SQL_KEYWORD('DELETE'), + style.SQL_KEYWORD('FROM'), + style.SQL_TABLE(self.quote_name(table)) + ) for table in tables] + for generator_info in sequences: + table_name = generator_info['table'] + query = "%s %s %s 0;" % (style.SQL_KEYWORD('SET GENERATOR'), + self.get_generator_name(table_name), style.SQL_KEYWORD('TO')) + sql.append(query) + return sql + else: + return [] + + def get_db_prep_lookup(self, lookup_type, value): + "Returns field's value prepared for database lookup." + from django.db.models.fields import prep_for_like_query + if lookup_type in ('exact', 'iexact', 'regex', 'iregex', 'gt', 'gte', 'lt', + 'lte', 'month', 'day', 'search', 'icontains', + 'startswith', 'istartswith'): + return [value] + elif lookup_type in ('range', 'in'): + return value + elif lookup_type in ('contains',): + return ["%%%s%%" % prep_for_like_query(value)] + elif lookup_type in ('endswith', 'iendswith'): + return ["%%%s" % prep_for_like_query(value)] + elif lookup_type == 'isnull': + return [] + elif lookup_type == 'year': + try: + value = int(value) + except ValueError: + raise ValueError("The __year lookup type requires an integer argument") + return [datetime.datetime(value, 1, 1, 0, 0, 0), datetime.datetime(value, 12, 31, 23, 59, 59, 999999)] + raise TypeError("Field has invalid lookup: %s" % lookup_type) + + def query_class(this, DefaultQuery): + class FirebirdQuery(DefaultQuery): + def as_sql(self, with_limits=True): + do_offset = with_limits and (self.high_mark or self.low_mark) + if not do_offset: + return super(FirebirdQuery, self).as_sql(with_limits=False) + + self.pre_sql_setup() + limit_offset_before = [] + if self.high_mark: + limit_offset_before.append("FIRST %d" % (self.high_mark - self.low_mark)) + if self.low_mark: + limit_offset_before.append("SKIP %d" % self.low_mark) + + sql, params= super(FirebirdQuery, self).as_sql(with_limits=False) + result = sql.replace('SELECT', "SELECT %s" % ' '.join(limit_offset_before)) + return result, params + return FirebirdQuery + + def query_set_class(this, DefaultQuerySet): + # Getting the base default `Query` object. + DefaultQuery = DefaultQuerySet().query.__class__ + + FirebirdQuery = this.query_class(DefaultQuery) + + from django.db import connection + class FirebirdQuerySet(DefaultQuerySet): + "The FirebirdQuerySet is overriden to use FirebirdQuery." + def __init__(self, model=None, query=None): + super(FirebirdQuerySet, self).__init__(model=model, query=query) + self.query = query or FirebirdQuery(self.model, connection) + return FirebirdQuerySet + +################################################################################ +# Cursor wrapper +class FirebirdCursorWrapper(object): + """ + Django uses "format" ('%s') style placeholders, but Firebird uses "qmark" ('?') style. + This fixes it -- but note that if you want to use a literal "%s" in a query, + you'll need to use "%%s". + + We also do all automatic type conversions here. + """ + import kinterbasdb.typeconv_datetime_stdlib as tc_dt + import kinterbasdb.typeconv_fixed_decimal as tc_fd + import kinterbasdb.typeconv_text_unicode as tc_tu + import django.utils.encoding as dj_ue + + def ascii_conv_in(self, text): + if text is not None: + return self.dj_ue.smart_str(text, 'ascii') + + def ascii_conv_out(self, text): + if text is not None: + return self.dj_ue.smart_unicode(text) + + def blob_conv_in(self, text): + return self.tc_tu.unicode_conv_in((self.dj_ue.smart_unicode(text), self.FB_CHARSET_CODE)) + + def blob_conv_out(self, text): + return self.tc_tu.unicode_conv_out((text, self.FB_CHARSET_CODE)) + + def fixed_conv_in(self, (val, scale)): + if val is not None: + if isinstance(val, basestring): + val = decimal.Decimal(val) + return self.tc_fd.fixed_conv_in_precise((val, scale)) + + def timestamp_conv_in(self, timestamp): + if isinstance(timestamp, basestring): + #Replaces 6 digits microseconds to 4 digits allowed in Firebird + timestamp = timestamp[:24] + return self.tc_dt.timestamp_conv_in(timestamp) + + def time_conv_in(self, value): + import datetime + if isinstance(value, datetime.datetime): + value = datetime.time(value.hour, value.minute, value.second, value.microsecond) + return self.tc_dt.time_conv_in(value) + + def date_conv_in(self, value): + if isinstance(value, basestring): + #Replaces 6 digits microseconds to 4 digits allowed in Firebird + value = value[:24] + return self.tc_dt.date_conv_in(value) + + def unicode_conv_in(self, text): + if text[0] is not None: + return self.tc_tu.unicode_conv_in((self.dj_ue.smart_unicode(text[0]), self.FB_CHARSET_CODE)) + + def __init__(self, cursor, connection): + self.cursor = cursor + self._connection = connection + self.FB_CHARSET_CODE = 3 #UNICODE_FSS + if connection.charset == 'UTF8': + self.FB_CHARSET_CODE = 4 # UTF-8 with Firebird 2.0+ + self.cursor.set_type_trans_in({ + 'DATE': self.date_conv_in, + 'TIME': self.time_conv_in, + 'TIMESTAMP': self.timestamp_conv_in, + 'FIXED': self.fixed_conv_in, + 'TEXT': self.ascii_conv_in, + 'TEXT_UNICODE': self.unicode_conv_in, + 'BLOB': self.blob_conv_in + }) + self.cursor.set_type_trans_out({ + 'DATE': self.tc_dt.date_conv_out, + 'TIME': self.tc_dt.time_conv_out, + 'TIMESTAMP': self.tc_dt.timestamp_conv_out, + 'FIXED': self.tc_fd.fixed_conv_out_precise, + 'TEXT': self.ascii_conv_out, + 'TEXT_UNICODE': self.tc_tu.unicode_conv_out, + 'BLOB': self.blob_conv_out + }) + + def execute(self, query, params=()): + if query.find('{{ FBLABEL }}') != -1: + cquery = query.replace('{{ FBLABEL }}', '') % tuple(["'%s'" % p for p in params]) + params = () + else: + cquery = self.convert_query(query, len(params)) + + try: + return self.cursor.execute(cquery, params) + except Database.ProgrammingError, e: + err_no = int(str(e).split()[0].strip(',()')) + output = ["Execute query error. FB error No. %i" % err_no] + output.extend(str(e).split("'")[1].split('\\n')) + output.append("Query:") + output.append(cquery) + output.append("Parameters:") + output.append(str(params)) + if err_no in (-803,): + raise IntegrityError("\n".join(output)) + raise DatabaseError("\n".join(output)) + + def executemany(self, query, param_list): + try: + cquery = self.convert_query(query, len(param_list[0])) + except IndexError: + return None + return self.cursor.executemany(cquery, param_list) + + def convert_query(self, query, num_params): + return query % tuple("?" * num_params) + + def fetchone(self): + return self.cursor.fetchone() + + def fetchmany(self, size=None): + if size is None: + return self.cursor.fetchmany() + return self.cursor.fetchmany(size) + + def fetchall(self): + return self.cursor.fetchall() + + def __getattr__(self, attr): + if attr in self.__dict__: + return self.__dict__[attr] + else: + return getattr(self.cursor, attr) + +################################################################################ +# DatabaseWrapper(db.connection) +class DatabaseWrapper(BaseDatabaseWrapper): + features = DatabaseFeatures() + ops = DatabaseOperations() + operators = { + 'exact': '= %s', + 'iexact': '= UPPER(%s)', + 'contains': "LIKE %s ESCAPE'\\'", + 'icontains': 'CONTAINING %s', #case is ignored + 'gt': '> %s', + 'gte': '>= %s', + 'lt': '< %s', + 'lte': '<= %s', + 'startswith': 'STARTING WITH %s', #looks to be faster then LIKE + 'endswith': "LIKE %s ESCAPE'\\'", + 'istartswith': 'STARTING WITH UPPER(%s)', + 'iendswith': "LIKE UPPER(%s) ESCAPE'\\'" + } + + def __init__(self, **kwargs): + from django.conf import settings + super(DatabaseWrapper, self).__init__(**kwargs) + self.charset = 'UNICODE_FSS' + self.FB_MAX_VARCHAR = 10921 #32765 MAX /3 + self.BYTES_PER_DEFAULT_CHAR = 3 + if hasattr(settings, 'FIREBIRD_CHARSET'): + if settings.FIREBIRD_CHARSET == 'UTF8': + self.charset = 'UTF8' + self.FB_MAX_VARCHAR = 8191 #32765 MAX /4 + self.BYTES_PER_DEFAULT_CHAR = 4 + + def _connect(self, settings): + if settings.DATABASE_NAME == '': + from django.core.exceptions import ImproperlyConfigured + raise ImproperlyConfigured, "You need to specify DATABASE_NAME in your Django settings file." + kwargs = {'charset' : self.charset } + if settings.DATABASE_HOST: + kwargs['dsn'] = "%s:%s" % (settings.DATABASE_HOST, settings.DATABASE_NAME) + else: + kwargs['dsn'] = "localhost:%s" % settings.DATABASE_NAME + if settings.DATABASE_USER: + kwargs['user'] = settings.DATABASE_USER + if settings.DATABASE_PASSWORD: + kwargs['password'] = settings.DATABASE_PASSWORD + self.connection = Database.connect(**kwargs) + assert self.connection.charset == self.charset + + def cursor(self): + from django.conf import settings + cursor = self._cursor(settings) + if settings.DEBUG: + self._debug_cursor = self.make_debug_cursor(cursor) + return self._debug_cursor + return cursor + + def _cursor(self, settings): + if self.connection is None: + self._connect(settings) + cursor = self.connection.cursor() + cursor = FirebirdCursorWrapper(cursor, self) + return cursor + + def __getattr__(self, attr): + if attr in self.__dict__: + return self.__dict__[attr] + else: + return getattr(self.connection, attr) + Index: django/db/backends/firebird/client.py =================================================================== --- django/db/backends/firebird/client.py (revision 0) +++ django/db/backends/firebird/client.py (revision 0) @@ -0,0 +1,11 @@ +from django.conf import settings +import os + +def runshell(): + args = [settings.DATABASE_NAME] + args += ["-u %s" % settings.DATABASE_USER] + if settings.DATABASE_PASSWORD: + args += ["-p %s" % settings.DATABASE_PASSWORD] + if 'FIREBIRD' not in os.environ: + path = '/opt/firebird/bin/' + os.system(path + 'isql ' + ' '.join(args)) Index: django/db/backends/firebird/__init__.py =================================================================== --- django/db/backends/firebird/__init__.py (revision 0) +++ django/db/backends/firebird/__init__.py (revision 0) @@ -0,0 +1 @@ + Index: django/db/backends/firebird/introspection.py =================================================================== --- django/db/backends/firebird/introspection.py (revision 0) +++ django/db/backends/firebird/introspection.py (revision 0) @@ -0,0 +1,97 @@ +from django.db import transaction +from django.db.backends.firebird.base import DatabaseOperations + +qn = quote_name = DatabaseOperations().quote_name + +def get_table_list(cursor): + "Returns a list of table names in the current database." + cursor.execute(""" + SELECT rdb$relation_name FROM rdb$relations + WHERE rdb$system_flag = 0 AND rdb$view_blr IS NULL ORDER BY rdb$relation_name""") + return [str(row[0].strip()) for row in cursor.fetchall()] + +def get_table_description(cursor, table_name): + "Returns a description of the table, with the DB-API cursor.description interface." + #cursor.execute("SELECT FIRST 1 * FROM %s" % quote_name(table_name)) + #return cursor.description + # (name, type_code, display_size, internal_size, precision, scale, null_ok) + cursor.execute(""" + SELECT DISTINCT R.RDB$FIELD_NAME AS FNAME, + F.RDB$FIELD_TYPE AS FTYPE, + F.RDB$CHARACTER_LENGTH AS FCHARLENGTH, + F.RDB$FIELD_LENGTH AS FLENGTH, + F.RDB$FIELD_PRECISION AS FPRECISION, + F.RDB$FIELD_SCALE AS FSCALE, + R.RDB$NULL_FLAG AS NULL_FLAG, + R.RDB$FIELD_POSITION + FROM RDB$RELATION_FIELDS R + JOIN RDB$FIELDS F ON R.RDB$FIELD_SOURCE = F.RDB$FIELD_NAME + WHERE F.RDB$SYSTEM_FLAG=0 and R.RDB$RELATION_NAME STARTING WITH %s + ORDER BY R.RDB$FIELD_POSITION + """, (qn(table_name).strip('"'),)) + return [(row[0].rstrip(), row[1], row[2] or 0, row[3], row[4], row[5], row[6] and True or False) for row in cursor.fetchall()] + + +def get_relations(cursor, table_name): + """ + Returns a dictionary of {field_index: (field_index_other_table, other_table)} + representing all relationships to the given table. Indexes are 0-based. + """ + cursor.execute(""" + SELECT seg.rdb$field_name, seg_ref.rdb$field_name, idx_ref.rdb$relation_name + FROM rdb$indices idx + INNER JOIN rdb$index_segments seg + ON seg.rdb$index_name = idx.rdb$index_name + INNER JOIN rdb$indices idx_ref + ON idx_ref.rdb$index_name = idx.rdb$foreign_key + INNER JOIN rdb$index_segments seg_ref + ON seg_ref.rdb$index_name = idx_ref.rdb$index_name + WHERE idx.rdb$relation_name STARTING WITH %s + AND idx.rdb$foreign_key IS NOT NULL""", [qn(table_name).strip('"')]) + + relations = {} + for row in cursor.fetchall(): + relations[row[0].rstrip()] = (row[1].strip(), row[2].strip()) + return relations + + +def get_indexes(cursor, table_name): + """ + Returns a dictionary of fieldname -> infodict for the given table, + where each infodict is in the format: + {'primary_key': boolean representing whether it's the primary key, + 'unique': boolean representing whether it's a unique index} + """ + + # This query retrieves each field name and index type on the given table. + cursor.execute(""" + SELECT seg.RDB$FIELD_NAME, const.RDB$CONSTRAINT_TYPE + FROM RDB$RELATION_CONSTRAINTS const + LEFT JOIN RDB$INDEX_SEGMENTS seg + ON seg.RDB$INDEX_NAME = const.RDB$INDEX_NAME + WHERE const.RDB$RELATION_NAME STARTING WITH ? + ORDER BY seg.RDB$FIELD_POSITION)""", + [qn(table_name).strip('"')]) + indexes = {} + for row in cursor.fetchall(): + indexes[row[0]] = {'primary_key': row[1].startswith('PRIMARY'), + 'unique': row[1].startswith('UNIQUE')} + return indexes + +# Maps type codes to Django Field types. +DATA_TYPES_REVERSE = { + 261: 'LargeTextField', + 14: 'CharField', + 40: 'CharField', + 11: 'FloatField', + 27: 'FloatField', + 10: 'FloatField', + 16: 'IntegerField', + 8: 'IntegerField', + 9: 'IntegerField', + 7: 'SmallIntegerField', + 12: 'DateField', + 13: 'TimeField', + 35: 'DateTimeField', + 37: 'CharField' +} Index: django/db/backends/firebird/creation.py =================================================================== --- django/db/backends/firebird/creation.py (revision 0) +++ django/db/backends/firebird/creation.py (revision 0) @@ -0,0 +1,494 @@ +# This dictionary maps Field objects to their associated Firebird column +# types, as strings. Column-type strings can contain format strings; they'll +# be interpolated against the values of Field.__dict__ before being output. +# If a column type is set to None, it won't be included in the output. + +from kinterbasdb import connect, create_database +from django.core.management import call_command +from django.conf import settings + +import sys +import os +import codecs +import warnings + +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + + +# Setting TEST_MODE to 2 disables strict FK constraints (for forward/post references) +# Setting TEST_MODE to 0 is the most secure option (it even fails some official Django tests because of it) +TEST_MODE = 0 +if 'FB_DJANGO_TEST_MODE' in os.environ: + TEST_MODE = int(os.environ['FB_DJANGO_TEST_MODE']) + +DATA_TYPES = { + 'AutoField': 'integer', + 'BooleanField': '"BooleanField"', + 'CharField': 'varchar(%(max_length)s)', + 'CommaSeparatedIntegerField': 'varchar(%(max_length)s) CHARACTER SET ASCII', + 'DateField': 'date', + 'DateTimeField': 'timestamp', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 'double precision', + 'ImageField': '"varchar(%(max_length)s)"', + 'IntegerField': 'integer', + 'IPAddressField': 'varchar(15) CHARACTER SET ASCII', + 'NullBooleanField': '"NullBooleanField"', + 'OneToOneField': 'integer', + 'PhoneNumberField': 'varchar(20) CHARACTER SET ASCII', + 'PositiveIntegerField': '"PositiveIntegerField"', + 'PositiveSmallIntegerField': '"PositiveSmallIntegerField"', + 'SlugField': 'varchar(%(max_length)s)', + 'SmallIntegerField': 'smallint', + 'LargeTextField': 'blob sub_type text', + 'TextField': '"TextField"', + 'TimeField': 'time', + 'URLField': 'varchar(%(max_length)s) CHARACTER SET ASCII', + 'USStateField': 'varchar(2) CHARACTER SET ASCII' +} + +PYTHON_TO_FB_ENCODING_MAP = { + 'ascii': 'ASCII', + 'utf_8': hasattr(settings, 'FIREBIRD_CHARSET') \ + and settings.FIREBIRD_CHARSET in ('UNICODE_FSS', 'UTF8') \ + and settings.FIREBIRD_CHARSET or 'UNICODE_FSS', + 'shift_jis': 'SJIS_0208', + 'euc_jp': 'EUCJ_0208', + 'cp737': 'DOS737', + 'cp437': 'DOS437', + 'cp850': 'DOS850', + 'cp865': 'DOS865', + 'cp860': 'DOS860', + 'cp863': 'DOS863', + 'cp775': 'DOS775', + 'cp862': 'DOS862', + 'cp864': 'DOS864', + 'iso8859_1': 'ISO8859_1', + 'iso8859_2': 'ISO8859_2', + 'iso8859_3': 'ISO8859_3', + 'iso8859_4': 'ISO8859_4', + 'iso8859_5': 'ISO8859_5', + 'iso8859_6': 'ISO8859_6', + 'iso8859_7': 'ISO8859_7', + 'iso8859_8': 'ISO8859_8', + 'iso8859_9': 'ISO8859_9', + 'iso8859_13': 'ISO8859_13', + 'euc_kr': 'KSC_5601', + 'cp852': 'DOS852', + 'cp857': 'DOS857', + 'cp861': 'DOS861', + 'cp866': 'DOS866', + 'cp869': 'DOS869', + 'cp1250': 'WIN1250', + 'cp1251': 'WIN1251', + 'cp1252': 'WIN1252', + 'cp1253': 'WIN1253', + 'cp1254': 'WIN1254', + 'big5': 'BIG_5', + 'gb2312': 'GB_2312', + 'cp1255': 'WIN1255', + 'cp1256': 'WIN1256', + 'cp1257': 'WIN1257', + 'koi8_r': 'KOI8-R', + 'koi8_u': 'KOI8-U', + 'cp1258': 'WIN1258' + } + +def get_data_size(data_type, max_length=100, char_bytes=None): + from django.db import connection + if char_bytes is None: + char_bytes = connection.BYTES_PER_DEFAULT_CHAR + size_map = { + 'AutoField': 8, + 'BooleanField': 4, + 'CharField': char_bytes*max_length, + 'CommaSeparatedIntegerField': max_length, + 'DateField': 16, + 'DateTimeField': 16, + 'DecimalField': 16, + 'FileField': char_bytes*max_length, + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': 16, + 'ImageField': char_bytes*max_length, + 'IntegerField': 8, + 'IPAddressField': 15, + 'NullBooleanField': 4, + 'OneToOneField': 8, + 'PhoneNumberField': 20, + 'PositiveIntegerField': 8, + 'PositiveSmallIntegerField': 4, + 'SlugField': char_bytes*max_length, + 'SmallIntegerField': 4, + 'TextBlob': 8, + 'TextField': 32767, + 'TimeField': 16, + 'URLField': max_length, + 'USStateField': char_bytes*2 + } + return size_map[data_type] + +def validate_rowsize(opts): + from django.db import connection + errs = set() + row_size = 0 + columns = [(f.db_type().strip('"'), f.get_internal_type(), f.max_length, f.encoding) for f in opts.local_fields] + columns_simple = [col[0] for col in columns] + text_field_type = '"TextField"' + max_allowed_bytes = 32765 + if 'TextField' in columns_simple: + max_length = 100 + num_text_fields = 0 + text_columns = [] + for column in columns: + if column[0] == 'TextField': + num_text_fields += 1 + text_columns.append(column) + if column[0].startswith('varchar') or (column[0] == 'TextField' and column[2]): + max_length = column[2] + if column[1] in DATA_TYPES: + if column[3]: + charbytes = 1 + else: + charbytes = connection.BYTES_PER_DEFAULT_CHAR + coltype = column[1] + if coltype == 'TextField' and column[2]: + # Calculate the right size for TextFields with custom max_length + coltype = 'CharField' + row_size += get_data_size(coltype, max_length, charbytes) + if row_size > 65536: + max_allowed_bytes = int( (max_allowed_bytes/num_text_fields) - (row_size-65536) ) + n = max_allowed_bytes / connection.BYTES_PER_DEFAULT_CHAR + if n > 512: + text_field_type = 'varchar(%s)' % n + errs.add("Row size limit in %s: Maximum number of characters in TextFields has changed to %s." + % (opts.db_table, n)) + else: + errs.add("Row size limit in %s: Field type changed to BLOB." + % opts.db_table) + # Swich to blobs if size is too small (<512) + text_field_type = 'blob sub_type text' + return errs, text_field_type, max_allowed_bytes + +def validate_index_limit(f, col_type, opts): + # More info: http://www.volny.cz/iprenosil/interbase/ip_ib_indexcalculator.htm + + from django.db import connection + + fb_version = "%s.%s" % (connection.ops.firebird_version[0], connection.ops.firebird_version[1]) + page_size = connection.ops.page_size + + errs = set() + + if connection.ops.index_limit < 1000: + strip2ascii = False + custom_charset = False + if col_type.startswith('varchar'): + if (f.unique or f.primary_key or f.db_index): + length = f.max_length + if not length: + try: + length = f.rel.to._meta.pk.max_length + except AttributeError: + pass + if f.encoding: + if not f.encoding.upper().startswith('UTF'): + custom_charset = True + if not custom_charset: + try: + flength = length * connection.BYTES_PER_DEFAULT_CHAR + if flength >= connection.ops.index_limit: + strip2ascii = True + except TypeError: + pass + + if len(opts.unique_together) > 0: + if f.column in opts.unique_together[0]: + num_unique_char_fields = len([ fld for fld in opts.unique_together[0] if opts.get_field(fld).db_type().startswith('varchar') ]) + num_unique_fields = len(opts.unique_together[0]) + num_unique_nonchar_fields = num_unique_fields - num_unique_char_fields + limit = connection.ops.index_limit + limit -= (num_unique_fields-1) * 52 + limit -= 8 * num_unique_nonchar_fields + max_length = limit/num_unique_char_fields + ascii_length = int(f.max_length) + if not f.encoding: + old_length = ascii_length*connection.BYTES_PER_DEFAULT_CHAR + else: + old_length = ascii_length + + if (old_length > max_length) and (ascii_length < max_length) and not f.encoding: + strip2ascii = True + elif old_length > max_length: + strip2ascii = False #We change it here + f.max_length = max_length + if not f.encoding: + f.encoding = 'ascii' + msg = "Index limit: Character set of the '%s' field (table %s) " + msg += "has changed to ASCII encoding to fit %s-byte limit in FB %s" + if not f.encoding: + if not page_size: + errs.add(msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version)) + else: + msg += " with page size %s" + errs.add(msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version, page_size)) + if max_length != ascii_length: + errs.add("Index limit: the maximum length of '%s' is %s instead of %s" + % (f.column, max_length, ascii_length)) + if strip2ascii: + f.encoding = 'ascii' + msg = "Index limit: Character set of the '%s' field (table %s) " + msg += "has changed to ASCII to fit %s-byte limit in FB %s" + if not page_size: + errs.add(msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version)) + else: + msg += " with page size %s" + errs.add(msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version, page_size)) + + return errs, col_type + +def sql_model_create(model, style, known_models=set()): + """ + Returns the SQL required to create a single model, as a tuple of: + (list_of_sql, pending_references_dict) + """ + from django.db import connection, models + + opts = model._meta + final_output = [] + table_output = [] + pending_references = {} + qn = connection.ops.quote_name + + # Create domains + domains = [ ('BooleanField', 'smallint CHECK (VALUE IN (0,1))'), + ('NullBooleanField', 'smallint CHECK ((VALUE IN (0,1)) OR (VALUE IS NULL))'), + ('PositiveIntegerField', 'integer CHECK ((VALUE >= 0) OR (VALUE IS NULL))'), + ('PositiveSmallIntegerField', 'smallint CHECK ((VALUE >= 0) OR (VALUE IS NULL))'), + ('TextField', 'varchar(%s)' % connection.FB_MAX_VARCHAR) ] + + connected = True + try: + cursor = connection.cursor() + except: + connected = False + if connected: + cursor.execute("SELECT RDB$FIELD_NAME FROM RDB$FIELDS") + existing_domains = set([row[0].strip() for row in cursor.fetchall() if not row[0].startswith('RDB$')]) + domains = map(lambda domain: '%s "%s" AS %s;' % ('CREATE DOMAIN', domain[0], domain[1]), + filter(lambda x: x[0] not in existing_domains, domains)) + final_output.extend(domains) + + # Check that row size is less than 64k and adjust TextFields if needed + errs, text_field_type, max_allowed_bytes = validate_rowsize(opts) + + # Create tables + for f in opts.local_fields: + col_type = f.db_type() + if col_type.strip('"') == 'TextField': + col_type = text_field_type + + errs, col_type = validate_index_limit(f, col_type, opts) + + if (col_type.startswith('varchar') or col_type.strip('"') == 'TextField') and f.encoding: + charset = PYTHON_TO_FB_ENCODING_MAP[codecs.lookup(f.encoding).name] + if f.max_length and f.max_length < max_allowed_bytes: + max_allowed_bytes = f.max_length + col_type = 'varchar(%i)' % max_allowed_bytes + col_type = "%s %s %s" % (col_type, "CHARACTER SET", charset) + + if not f.encoding: + max_allowed_length = connection.BYTES_PER_DEFAULT_CHAR * max_allowed_bytes + if col_type.strip('"') == 'TextField' and f.max_length and f.max_length < max_allowed_length: + col_type = 'varchar(%i)' % f.max_length + + if col_type is None: + # Skip ManyToManyFields, because they're not represented as + # database columns in this table. + continue + else: + # Make the definition (e.g. 'foo VARCHAR(30)') for this field. + field_output = [style.SQL_FIELD(qn(f.column)), + style.SQL_COLTYPE(col_type)] + field_output.append(style.SQL_KEYWORD('%s' % (not f.null and 'NOT NULL' or ''))) + if f.unique and not f.primary_key: + field_output.append(style.SQL_KEYWORD('UNIQUE')) + if f.primary_key: + field_output.append(style.SQL_KEYWORD('PRIMARY KEY')) + if f.rel: + # We haven't yet created the table to which this field + # is related, so save it for later. + pr = pending_references.setdefault(f.rel.to, []).append((model, f)) + table_output.append(' '.join(field_output)) + if opts.order_with_respect_to: + table_output.append(style.SQL_FIELD(qn('_order')) + ' ' + \ + style.SQL_COLTYPE(models.IntegerField().db_type())) + for field_constraints in opts.unique_together: + table_output.append(style.SQL_KEYWORD('UNIQUE') + ' (%s)' % \ + ", ".join([qn(style.SQL_FIELD(opts.get_field(f).column)) for f in field_constraints])) + + full_statement = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + style.SQL_TABLE(qn(opts.db_table)) + ' ('] + for i, line in enumerate(table_output): # Combine and add commas. + full_statement.append(' %s%s' % (line, i < len(table_output)-1 and ',' or '')) + full_statement.append(');') + final_output.append('\n'.join(full_statement)) + + if opts.has_auto_field: + # Add any extra SQL needed to support auto-incrementing primary keys. + auto_column = opts.auto_field.db_column or opts.auto_field.name + autoinc_sql = connection.ops.autoinc_sql(opts.db_table, auto_column) + if autoinc_sql: + for stmt in autoinc_sql: + final_output.append(stmt) + + # Declare exteral functions + if connected: + cursor.execute("SELECT RDB$FUNCTION_NAME FROM RDB$FUNCTIONS") + existing_functions = set([row[0].strip().upper() for row in cursor.fetchall()]) + if 'RAND' not in existing_functions: + final_output.append('%s %s\n\t%s %s\n\t%s %s\n\t%s;' % (style.SQL_KEYWORD('DECLARE EXTERNAL FUNCTION'), + style.SQL_TABLE('RAND'), style.SQL_KEYWORD('RETURNS'), style.SQL_COLTYPE('DOUBLE PRECISION'), + style.SQL_KEYWORD('BY VALUE ENTRY_POINT'), style.SQL_FIELD("'IB_UDF_rand'"), + style.SQL_TABLE("MODULE_NAME 'ib_udf'"))) + if 'SUBSTR' not in existing_functions: + final_output.append("""DECLARE EXTERNAL FUNCTION SUBSTR CSTRING(255), SMALLINT, SMALLINT + RETURNS CSTRING(255) FREE_IT + ENTRY_POINT 'IB_UDF_substr' MODULE_NAME 'ib_udf';""") + + # Create stored procedures + if hasattr(model, 'procedures'): + for proc in model.procedures: + final_output.append(proc.create_procedure_sql()) + + # Create triggers + if hasattr(model, 'triggers'): + for proc in model.triggers: + final_output.append(proc.create_trigger_sql()) + + return final_output, pending_references + +def many_to_many_sql_for_model(model, style): + from django.db import connection, models + from django.contrib.contenttypes import generic + from django.db.backends.util import truncate_name + + opts = model._meta + final_output = [] + qn = connection.ops.quote_name + for f in opts.local_many_to_many: + if not isinstance(f.rel, generic.GenericRel): + table_output = [style.SQL_KEYWORD('CREATE TABLE') + ' ' + \ + style.SQL_TABLE(qn(f.m2m_db_table())) + ' ('] + table_output.append(' %s %s %s,' % + (style.SQL_FIELD(qn('id')), + style.SQL_COLTYPE(models.AutoField(primary_key=True).db_type()), + style.SQL_KEYWORD('NOT NULL PRIMARY KEY'))) + + table_output.append(' %s %s %s,' % + (style.SQL_FIELD(qn(f.m2m_column_name())), + style.SQL_COLTYPE(models.ForeignKey(model).db_type()), + style.SQL_KEYWORD('NOT NULL'))) + table_output.append(' %s %s %s,' % + (style.SQL_FIELD(qn(f.m2m_reverse_name())), + style.SQL_COLTYPE(models.ForeignKey(f.rel.to).db_type()), + style.SQL_KEYWORD('NOT NULL'))) + deferred = [ + (f.m2m_db_table(), f.m2m_column_name(), opts.db_table, + opts.pk.column), + ( f.m2m_db_table(), f.m2m_reverse_name(), + f.rel.to._meta.db_table, f.rel.to._meta.pk.column) + ] + + table_output.append(' %s (%s, %s)' % + (style.SQL_KEYWORD('UNIQUE'), + style.SQL_FIELD(qn(f.m2m_column_name())), + style.SQL_FIELD(qn(f.m2m_reverse_name())))) + table_output.append(');') + final_output.append('\n'.join(table_output)) + + autoinc_sql = connection.ops.autoinc_sql(f.m2m_db_table(), 'id') + if autoinc_sql: + for stmt in autoinc_sql: + final_output.append(stmt) + + if TEST_MODE < 2: + for r_table, r_col, table, col in deferred: + r_name = connection.ops.reference_name(r_col, col, r_table, table) + final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % + (qn(r_table), + truncate_name(r_name, connection.ops.max_name_length()), + qn(r_col), qn(table), qn(col), + 'ON DELETE CASCADE ON UPDATE CASCADE')) + + return final_output + + +TEST_DATABASE_PREFIX = 'test_' +def create_test_db(settings, connection, verbosity, autoclobber): + # KInterbasDB supports dynamic database creation and deletion + # via the module-level function create_database and the method Connection.drop_database. + + if settings.TEST_DATABASE_NAME: + TEST_DATABASE_NAME = settings.TEST_DATABASE_NAME + else: + dbnametuple = os.path.split(settings.DATABASE_NAME) + TEST_DATABASE_NAME = os.path.join(dbnametuple[0], TEST_DATABASE_PREFIX + dbnametuple[1]) + + dsn = "localhost:%s" % TEST_DATABASE_NAME + if settings.DATABASE_HOST: + dsn = "%s:%s" % (settings.DATABASE_HOST, TEST_DATABASE_NAME) + + if os.path.isfile(TEST_DATABASE_NAME): + sys.stderr.write("Database %s already exists\n" % TEST_DATABASE_NAME) + if not autoclobber: + confirm = raw_input("Type 'yes' if you would like to try deleting the test database '%s', or 'no' to cancel: " % TEST_DATABASE_NAME) + if autoclobber or confirm == 'yes': + if verbosity >= 1: + print "Destroying old test database..." + old_connection = connect(dsn=dsn, user=settings.DATABASE_USER, password=settings.DATABASE_PASSWORD) + old_connection.drop_database() + else: + print "Tests cancelled." + sys.exit(1) + + if verbosity >= 1: + print "Creating test database..." + try: + charset = hasattr(settings, 'FIREBIRD_CHARSET') \ + and settings.FIREBIRD_CHARSET in ('UNICODE_FSS', 'UTF8') \ + and settings.FIREBIRD_CHARSET or 'UNICODE_FSS' + if hasattr(settings, 'FIREBIRD_CHARSET'): + if settings.FIREBIRD_CHARSET == 'UTF8': + charset='UTF8' + create_database("create database '%s' user '%s' password '%s' default character set %s" % \ + (dsn, settings.DATABASE_USER, settings.DATABASE_PASSWORD, charset)) + except Exception, e: + sys.stderr.write("Got an error creating the test database: %s\n" % e) + sys.exit(2) + + connection.close() + settings.DATABASE_NAME = TEST_DATABASE_NAME + + call_command('syncdb', verbosity=verbosity, interactive=False) + + if settings.CACHE_BACKEND.startswith('db://'): + cache_name = settings.CACHE_BACKEND[len('db://'):] + call_command('createcachetable', cache_name) + + # Get a cursor (even though we don't need one yet). This has + # the side effect of initializing the test database. + cursor = connection.cursor() + + return TEST_DATABASE_NAME + +def destroy_test_db(settings, connection, old_database_name, verbosity): + # KInterbasDB supports dynamic database deletion via the method Connection.drop_database. + if verbosity >= 1: + print "Destroying test database..." + connection.drop_database() + + Index: django/__init__.py =================================================================== --- django/__init__.py (revision 7398) +++ django/__init__.py (working copy) @@ -1,4 +1,4 @@ -VERSION = (0, 97, 'queryset-refactor') +VERSION = (0, 97, 'queryset-refactor-firebird') def get_version(): "Returns the version as a human-format string." Index: django/core/cache/backends/db.py =================================================================== --- django/core/cache/backends/db.py (revision 7398) +++ django/core/cache/backends/db.py (working copy) @@ -47,19 +47,20 @@ if timeout is None: timeout = self.default_timeout cursor = connection.cursor() - cursor.execute("SELECT COUNT(*) FROM %s" % self._table) + qn = connection.ops.quote_name + cursor.execute("SELECT COUNT(*) FROM %s" % qn(self._table)) num = cursor.fetchone()[0] now = datetime.now().replace(microsecond=0) exp = datetime.fromtimestamp(time.time() + timeout).replace(microsecond=0) if num > self._max_entries: self._cull(cursor, now) encoded = base64.encodestring(pickle.dumps(value, 2)).strip() - cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s" % self._table, [key]) + cursor.execute("SELECT cache_key FROM %s WHERE cache_key = %%s" % qn(self._table), [key]) try: if mode == 'set' and cursor.fetchone(): - cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % self._table, [encoded, str(exp), key]) + cursor.execute("UPDATE %s SET value = %%s, expires = %%s WHERE cache_key = %%s" % qn(self._table), [encoded, str(exp), key]) else: - cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % self._table, [key, encoded, str(exp)]) + cursor.execute("INSERT INTO %s (cache_key, value, expires) VALUES (%%s, %%s, %%s)" % qn(self._table), [key, encoded, str(exp)]) except DatabaseError: # To be threadsafe, updates/inserts are allowed to fail silently pass Index: django/core/management/validation.py =================================================================== --- django/core/management/validation.py (revision 7398) +++ django/core/management/validation.py (working copy) @@ -1,4 +1,5 @@ import sys +import warnings from django.core.management.color import color_style from django.utils.itercompat import is_iterable @@ -19,7 +20,7 @@ Returns number of errors. """ from django.conf import settings - from django.db import models, connection + from django.db import models, connection, get_creation_module from django.db.models.loading import get_app_errors from django.db.models.fields.related import RelatedObject @@ -31,6 +32,14 @@ for cls in models.get_models(app): opts = cls._meta + # Do row-specific validation. + if settings.DATABASE_ENGINE == 'firebird': + creation = get_creation_module() + errs, _, _ = creation.validate_rowsize(opts) + for err in errs: + warnings.warn(err) + #print "It is recommended to explicitly set max_length or change the field to 'LargeTextField'" + # Do field-specific validation. for f in opts.local_fields: if f.name == 'id' and not f.primary_key and opts.pk.name == 'id': @@ -69,6 +78,12 @@ if db_version < (5, 0, 3) and isinstance(f, (models.CharField, models.CommaSeparatedIntegerField, models.SlugField)) and f.max_length > 255: e.add(opts, '"%s": %s cannot have a "max_length" greater than 255 when you are using a version of MySQL prior to 5.0.3 (you are using %s).' % (f.name, f.__class__.__name__, '.'.join([str(n) for n in db_version[:3]]))) + if settings.DATABASE_ENGINE == 'firebird': + errs, _ = creation.validate_index_limit(f, f.db_type(), opts) + for err in errs: + warnings.warn(err) + #print "It is recommended to explicitly adjust max_length and/or change the character encoding." + # Check to see if the related field will clash with any existing # fields, m2m fields, m2m related objects or related objects if f.rel: Index: django/core/management/sql.py =================================================================== --- django/core/management/sql.py (revision 7398) +++ django/core/management/sql.py (working copy) @@ -1,4 +1,5 @@ from django.core.management.base import CommandError +from django.conf import settings import os import re @@ -66,7 +67,7 @@ def sql_create(app, style): "Returns a list of the CREATE TABLE SQL statements for the given app." - from django.db import models + from django.db import models, get_creation_module from django.conf import settings if settings.DATABASE_ENGINE == 'dummy': @@ -99,7 +100,7 @@ # Create the many-to-many join tables. for model in app_models: final_output.extend(many_to_many_sql_for_model(model, style)) - + # Handle references to tables that are from other apps # but don't exist physically. not_installed_models = set(pending_references.keys()) @@ -166,7 +167,7 @@ col = f.column r_table = model._meta.db_table r_col = model._meta.get_field(f.rel.field_name).column - r_name = '%s_refs_%s_%x' % (col, r_col, abs(hash((table, r_table)))) + r_name = connection.ops.reference_name(r_col, col, r_table, table) output.append('%s %s %s %s;' % \ (style.SQL_KEYWORD('ALTER TABLE'), style.SQL_TABLE(qn(table)), @@ -250,8 +251,11 @@ Returns the SQL required to create a single model, as a tuple of: (list_of_sql, pending_references_dict) """ - from django.db import connection, models - + from django.db import connection, models, get_creation_module + creation_module = get_creation_module() + # If the database backend wants to create model itself, let it + if hasattr(creation_module, "sql_model_create"): + return creation_module.sql_model_create(model, style, known_models) opts = model._meta final_output = [] table_output = [] @@ -320,7 +324,7 @@ """ Returns any ALTER TABLE statements to add constraints after the fact. """ - from django.db import connection + from django.db import connection, get_creation_module from django.db.backends.util import truncate_name qn = connection.ops.quote_name @@ -336,7 +340,7 @@ col = opts.get_field(f.rel.field_name).column # For MySQL, r_name must be unique in the first 64 characters. # So we are careful with character usage here. - r_name = '%s_refs_%s_%x' % (r_col, col, abs(hash((r_table, table)))) + r_name = connection.ops.reference_name(r_col, col, r_table, table) final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % \ (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()), qn(r_col), qn(table), qn(col), @@ -345,10 +349,15 @@ return final_output def many_to_many_sql_for_model(model, style): - from django.db import connection, models + from django.db import connection, models, get_creation_module from django.contrib.contenttypes import generic from django.db.backends.util import truncate_name - + + creation_module = get_creation_module() + # If the database backend wants to create many_to_many sql itself, let it + if hasattr(creation_module, "many_to_many_sql_for_model"): + return creation_module.many_to_many_sql_for_model(model, style) + opts = model._meta final_output = [] qn = connection.ops.quote_name @@ -411,8 +420,7 @@ final_output.append('\n'.join(table_output)) for r_table, r_col, table, col in deferred: - r_name = '%s_refs_%s_%x' % (r_col, col, - abs(hash((r_table, table)))) + r_name = connection.ops.reference_name(r_col, col, r_table, table) final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s)%s;' % (qn(r_table), truncate_name(r_name, connection.ops.max_name_length()), Index: django/contrib/redirects/models.py =================================================================== --- django/contrib/redirects/models.py (revision 7398) +++ django/contrib/redirects/models.py (working copy) @@ -4,9 +4,9 @@ class Redirect(models.Model): site = models.ForeignKey(Site, radio_admin=models.VERTICAL) - old_path = models.CharField(_('redirect from'), max_length=200, db_index=True, + old_path = models.CharField(_('redirect from'), max_length=192, db_index=True, encoding='ascii', help_text=_("This should be an absolute path, excluding the domain name. Example: '/events/search/'.")) - new_path = models.CharField(_('redirect to'), max_length=200, blank=True, + new_path = models.CharField(_('redirect to'), max_length=192, blank=True, encoding='ascii', help_text=_("This can be either an absolute path (as above) or a full URL starting with 'http://'.")) class Meta: Index: django/contrib/admin/models.py =================================================================== --- django/contrib/admin/models.py (revision 7398) +++ django/contrib/admin/models.py (working copy) @@ -18,10 +18,10 @@ action_time = models.DateTimeField(_('action time'), auto_now=True) user = models.ForeignKey(User) content_type = models.ForeignKey(ContentType, blank=True, null=True) - object_id = models.TextField(_('object id'), blank=True, null=True) + object_id = models.TextField(_('object id'), blank=True, null=True, max_length=1024) object_repr = models.CharField(_('object repr'), max_length=200) action_flag = models.PositiveSmallIntegerField(_('action flag')) - change_message = models.TextField(_('change message'), blank=True) + change_message = models.TextField(_('change message'), blank=True, max_length=5252) objects = LogEntryManager() class Meta: verbose_name = _('log entry') Index: django/contrib/admin/views/main.py =================================================================== --- django/contrib/admin/views/main.py (revision 7398) +++ django/contrib/admin/views/main.py (working copy) @@ -750,7 +750,7 @@ for bit in self.query.split(): or_queries = [models.Q(**{construct_search(field_name): bit}) for field_name in self.lookup_opts.admin.search_fields] other_qs = QuerySet(self.model) - if qs._select_related: + if qs.query.select_related: other_qs = other_qs.select_related() other_qs = other_qs.filter(reduce(operator.or_, or_queries)) qs = qs & other_qs Index: django/contrib/contenttypes/models.py =================================================================== --- django/contrib/contenttypes/models.py (revision 7398) +++ django/contrib/contenttypes/models.py (working copy) @@ -63,8 +63,8 @@ class ContentType(models.Model): name = models.CharField(max_length=100) - app_label = models.CharField(max_length=100) - model = models.CharField(_('python model class name'), max_length=100) + app_label = models.CharField(max_length=100, encoding='ascii') + model = models.CharField(_('python model class name'), max_length=100, encoding='ascii') objects = ContentTypeManager() class Meta: Index: django/contrib/auth/models.py =================================================================== --- django/contrib/auth/models.py (revision 7398) +++ django/contrib/auth/models.py (working copy) @@ -72,7 +72,7 @@ """ name = models.CharField(_('name'), max_length=50) content_type = models.ForeignKey(ContentType) - codename = models.CharField(_('codename'), max_length=100) + codename = models.CharField(_('codename'), max_length=100, encoding='ascii') class Meta: verbose_name = _('permission') Index: django/contrib/flatpages/models.py =================================================================== --- django/contrib/flatpages/models.py (revision 7398) +++ django/contrib/flatpages/models.py (working copy) @@ -5,7 +5,8 @@ class FlatPage(models.Model): - url = models.CharField(_('URL'), max_length=100, validator_list=[validators.isAlphaNumericURL], db_index=True, + url = models.CharField(_('URL'), max_length=100, validator_list=[validators.isAlphaNumericURL], + db_index=True, encoding='ascii', help_text=_("Example: '/about/contact/'. Make sure to have leading and trailing slashes.")) title = models.CharField(_('title'), max_length=200) content = models.TextField(_('content')) Index: tests/modeltests/custom_methods/models.py =================================================================== --- tests/modeltests/custom_methods/models.py (revision 7398) +++ tests/modeltests/custom_methods/models.py (working copy) @@ -27,15 +27,16 @@ """ from django.db import connection cursor = connection.cursor() + # Some backends really really need quotes! cursor.execute(""" - SELECT id, headline, pub_date - FROM custom_methods_article - WHERE pub_date = %s - AND id != %s""", [str(self.pub_date), self.id]) + SELECT "id", "headline", "pub_date" + FROM "custom_methods_article" + WHERE "pub_date" = %s + AND "id" != %s""", [str(self.pub_date), self.id]) # The asterisk in "(*row)" tells Python to expand the list into # positional arguments to Article(). return [self.__class__(*row) for row in cursor.fetchall()] - + __test__ = {'API_TESTS':""" # Create a couple of Articles. >>> from datetime import date Index: tests/modeltests/lookup/models.py =================================================================== --- tests/modeltests/lookup/models.py (revision 7398) +++ tests/modeltests/lookup/models.py (working copy) @@ -296,7 +296,34 @@ >>> a4.save() >>> a5 = Article(pub_date=now, headline='hey-Foo') >>> a5.save() +"""} +# Firebird support 'magic values' +if settings.DATABASE_ENGINE in ('firebird',): + __test__['API_TESTS'] += r""" +# and yet more: +>>> a10 = Article(pub_date='today', headline='foobar') +>>> a10.save() +>>> a11 = Article(pub_date='tomorrow', headline='foobaz') +>>> a11.save() +>>> a12 = Article(pub_date='yesterday', headline='ooF') +>>> a12.save() +>>> a13 = Article(pub_date='now', headline='foobarbaz') +>>> a13.save() +>>> a14 = Article(pub_date='today', headline='zoocarfaz') +>>> a14.save() +>>> a15 = Article(pub_date='today', headline='barfoobaz') +>>> a15.save() +>>> a16 = Article(pub_date='today', headline='bazbaRFOO') +>>> a16.save() +>>> Article.objects.filter(pub_date__exact='yesterday') +[] +""" + + +# Firebird doesn't support regular expression lookups +if settings.DATABASE_ENGINE not in ('firebird',): + __test__['API_TESTS'] += r""" # zero-or-more >>> Article.objects.filter(headline__regex=r'fo*') [, , , ] @@ -370,10 +397,10 @@ [, , , , ] >>> Article.objects.filter(headline__iregex=r'b.*ar') [, , , , ] -"""} +""" -if settings.DATABASE_ENGINE not in ('mysql', 'mysql_old'): +if settings.DATABASE_ENGINE not in ('mysql', 'mysql_old', 'firebird'): __test__['API_TESTS'] += r""" # grouping and backreferences >>> Article.objects.filter(headline__regex=r'b(.).*b\1') Index: tests/modeltests/many_to_one/models.py =================================================================== --- tests/modeltests/many_to_one/models.py (revision 7398) +++ tests/modeltests/many_to_one/models.py (working copy) @@ -5,6 +5,7 @@ """ from django.db import models +from django.conf import settings class Reporter(models.Model): first_name = models.CharField(max_length=30) @@ -149,8 +150,23 @@ >>> sql = queryset.query.as_sql()[0] >>> sql.count('INNER JOIN') 1 +""" +} +if settings.DATABASE_ENGINE == 'firebird': + __test__['API_TESTS'] += """ # The automatically joined table has a predictable name. +>>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["\\"many_to_one_reporter\\".\\"last_name\\"='Smith'"]) +[, ] + +# And should work fine with the unicode that comes out of +# newforms.Form.cleaned_data +>>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["\\"many_to_one_reporter\\".\\"last_name\\"='%s'" % u'Smith']) +[, ] +""" +else: + __test__['API_TESTS'] += """\ +# The automatically joined table has a predictable name. >>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_reporter.last_name='Smith'"]) [, ] @@ -158,7 +174,10 @@ # newforms.Form.cleaned_data >>> Article.objects.filter(reporter__first_name__exact='John').extra(where=["many_to_one_reporter.last_name='%s'" % u'Smith']) [, ] +""" + +__test__['API_TESTS'] += """ # Find all Articles for the Reporter whose ID is 1. # Use direct ID check, pk check, and object comparison >>> Article.objects.filter(reporter__id__exact=1) @@ -273,4 +292,4 @@ >>> Article.objects.all() [] -"""} +""" Index: tests/regressiontests/serializers_regress/tests.py =================================================================== --- tests/regressiontests/serializers_regress/tests.py (revision 7398) +++ tests/regressiontests/serializers_regress/tests.py (working copy) @@ -22,6 +22,10 @@ import decimal except ImportError: from django.utils import _decimal as decimal +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback # A set of functions that can be used to recreate # test data objects of various kinds. @@ -83,8 +87,9 @@ testcase.assertEqual(data, instance.data_id) def m2m_compare(testcase, pk, klass, data): + # Use sets to ignore order of data instance = klass.objects.get(id=pk) - testcase.assertEqual(data, [obj.id for obj in instance.data.all()]) + testcase.assertEqual(set(data), set([obj.id for obj in instance.data.all()])) def o2o_compare(testcase, pk, klass, data): instance = klass.objects.get(data=data) Index: tests/regressiontests/backends/models.py =================================================================== --- tests/regressiontests/backends/models.py (revision 7398) +++ tests/regressiontests/backends/models.py (working copy) @@ -29,7 +29,7 @@ >>> opts = Square._meta >>> f1, f2 = opts.get_field('root'), opts.get_field('square') >>> query = ('INSERT INTO %s (%s, %s) VALUES (%%s, %%s)' -... % (t_convert(opts.db_table), qn(f1.column), qn(f2.column))) +... % ((qn(t_convert(opts.db_table)), qn(f1.column), qn(f2.column)))) >>> cursor.executemany(query, [(i, i**2) for i in range(-5, 6)]) and None or None >>> Square.objects.order_by('root') [, , , , , , , , , , ] @@ -48,7 +48,7 @@ >>> opts2 = Person._meta >>> f3, f4 = opts2.get_field('first_name'), opts2.get_field('last_name') >>> query2 = ('SELECT %s, %s FROM %s ORDER BY %s' -... % (qn(f3.column), qn(f4.column), t_convert(opts2.db_table), +... % (qn(f3.column), qn(f4.column), qn(t_convert(opts2.db_table)), ... qn(f3.column))) >>> cursor.execute(query2) and None or None >>> cursor.fetchone()