Index: django/db/models/sql/query.py =================================================================== --- django/db/models/sql/query.py (revision 7397) +++ 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) @@ -354,7 +354,23 @@ 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) + else: + qn_col = col.split() + qn_col[0] = '.'.join((qn(c) for c in qn_col[0].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 +435,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 '') + # TODO: add another feature? + 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 7397) +++ django/db/models/sql/subqueries.py (working copy) @@ -24,7 +24,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) Index: django/db/models/fields/__init__.py =================================================================== --- django/db/models/fields/__init__.py (revision 7397) +++ 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 @@ -66,7 +66,7 @@ # # getattr(obj, opts.pk.attname) -class Field(object): +class _Field(object): # Provide backwards compatibility for the maxlength attribute and # argument for this class and all subclasses. __metaclass__ = LegacyMaxlength @@ -425,6 +425,12 @@ "Returns the value of this field in the given model instance." return getattr(obj, self.attname) +# Use the backend's Field class if it defines one. Otherwise, use _Field. +if connection.features.uses_custom_field: + Field = connection.ops.field_class(_Field) +else: + Field = _Field + class AutoField(Field): empty_strings_allowed = False def __init__(self, *args, **kwargs): @@ -726,6 +732,14 @@ defaults.update(kwargs) return super(DecimalField, self).formfield(**defaults) +class DefaultCharField(CharField): + def __init__(self, *args, **kwargs): + DEFAULT_MAX_LENGTH = 100 + if hasattr(settings, 'DEFAULT_MAX_LENGTH'): + DEFAULT_MAX_LENGTH = settings.DEFAULT_MAX_LENGT + kwargs['max_length'] = kwargs.get('max_length', DEFAULT_MAX_LENGTH) + CharField.__init__(self, *args, **kwargs) + class EmailField(CharField): def __init__(self, *args, **kwargs): kwargs['max_length'] = kwargs.get('max_length', 75) @@ -957,6 +971,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 +1116,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 7397) +++ 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 7397) +++ django/db/backends/__init__.py (working copy) @@ -42,6 +42,7 @@ 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 @@ -49,6 +50,7 @@ supports_constraints = True supports_tablespaces = False uses_case_insensitive_names = False + uses_custom_field = False uses_custom_queryset = False empty_fetchmany_value = [] @@ -215,6 +217,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. 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,568 @@ +""" +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 +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 + uses_custom_field = 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 + 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 + name = '"%s"' % util.truncate_name(name.strip('"'), self._max_name_length) + return name + + def old_quote_id_plus_number(self, name): + try: + return '"%s" + %s' % tuple(s.strip() for s in name.strip('"').split('+')) + except: + return self.quote_name(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): + # limits are handled in custom FirebirdQuerySet + raise NotImplementedError('Limits are handled in a different way in Firebird') + + 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 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, style, 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 = "%s %s;" % ( style.SQL_KEYWORD('CREATE GENERATOR'), + generator_name) + trigger_sql = "\n".join([ + "%s %s %s %s" % ( \ + style.SQL_KEYWORD('CREATE TRIGGER'), trigger_name, style.SQL_KEYWORD('FOR'), + style.SQL_TABLE(table_name)), + "%s 0 %s" % (style.SQL_KEYWORD('ACTIVE BEFORE INSERT POSITION'), style.SQL_KEYWORD('AS')), + style.SQL_KEYWORD('BEGIN'), + " %s ((%s.%s %s) %s (%s.%s = 0)) %s" % ( \ + style.SQL_KEYWORD('IF'), + style.SQL_KEYWORD('NEW'), style.SQL_FIELD(column_name), style.SQL_KEYWORD('IS NULL'), + style.SQL_KEYWORD('OR'), style.SQL_KEYWORD('NEW'), style.SQL_FIELD(column_name), + style.SQL_KEYWORD('THEN') + ), + " %s" % style.SQL_KEYWORD('BEGIN'), + " %s.%s = %s(%s, 1);" % ( \ + style.SQL_KEYWORD('NEW'), style.SQL_FIELD(column_name), + style.SQL_KEYWORD('GEN_ID'), generator_name + ), + " %s" % style.SQL_KEYWORD('END'), + "%s;" % style.SQL_KEYWORD('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 [] + + ############################################################################ + # Custom classes + def field_class(this, DefaultField): + from django.db import connection + from django.db.models.fields import prep_for_like_query + class FirebirdField(DefaultField): + def __init__(self, *args, **keys): + self.encoding = keys.pop('encoding', None) + self.on_update = keys.pop('on_delete', None) + self.on_delete = keys.pop('on_update', None) + super(FirebirdField, self).__init__(*args, **keys) + + def get_db_prep_lookup(self, lookup_type, value): + "Returns field's value prepared for database lookup." + 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 ['%s-01-01 00:00:00' % value, '%s-12-31 23:59:59.999999' % value] + raise TypeError("Field has invalid lookup: %s" % lookup_type) + return FirebirdField + + def query_set_class(this, DefaultQuerySet): + # Getting the base default `Query` object. + DefaultQuery = DefaultQuerySet().query.__class__ + + class FirebirdQuery(DefaultQuery): + #def resolve_columns(self, row, fields=()): + # from django.db.models.fields import DateField, DateTimeField, \ + # TimeField, BooleanField, NullBooleanField, DecimalField, Field + # values = [] + # for value, field in map(None, row, fields): + # # Convert 1 or 0 to True or False + # if value in (1, 0) and isinstance(field, (BooleanField, NullBooleanField)): + # value = bool(value) + # values.append(value) + # return values + + 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: + if self.low_mark: + limit_offset_before = "FIRST %d " % (self.high_mark - self.low_mark) + else: + limit_offset_before = "FIRST %d " % self.high_mark + if self.low_mark: + limit_offset_before += "SKIP %d " % self.low_mark + + sql, params= super(FirebirdQuery, self).as_sql(with_limits=False) + result = sql.replace('SELECT ', "SELECT %s" % limit_offset_before) + return result, params + + 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 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._statement = None #prepared statement + 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.tc_dt.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_immediate(self, query, params=()): + query = query % tuple(params) + self._connection.execute_immediate(query) + + def prepare(self, query): + """ + Returns prepared statement for use with execute_prepared + http://kinterbasdb.sourceforge.net/dist_docs/usage.html#adv_prepared_statements + """ + query.replace("%s", "?") + return self.cursor.prep(query) + + def execute_prepared(self, statement, params): + return self.cursor.execute(statement, params) + + def execute_straight(self, query, params=()): + """ + Firebird-style execute with '?' instead of '%s' + """ + try: + return self.cursor.execute(query, 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(query) + output.append("Parameters:") + output.append(str(params)) + if err_no in (-803,): + raise IntegrityError("\n".join(output)) + raise DatabaseError("\n".join(output)) + + def execute(self, query, params=()): + cquery = self.convert_query(query, len(params)) + if self._get_query() != cquery: + try: + self._statement = self.cursor.prep(cquery) + except Database.ProgrammingError, e: + output = ["Prepare query error."] + output.extend(str(e).split("'")[1].split('\\n')) + output.append("Query:") + output.append(cquery) + raise DatabaseError("\n".join(output)) + try: + return self.cursor.execute(self._statement, 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 + if self._get_query() != cquery: + self._statement = self.cursor.prep(cquery) + return self.cursor.executemany(self._statement, param_list) + + def convert_query(self, query, num_params): + try: + return query % tuple("?" * num_params) + except TypeError, e: + print query, num_params + raise TypeError, e + + def _get_query(self): + if self._statement: + return self._statement.sql + + 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. _current_cursor = None + self._raw_cursor = None + 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() + self._raw_cursor = cursor + cursor = FirebirdCursorWrapper(cursor, self) + self._current_cursor = cursor + 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_straight(""" + 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 ? + 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_straight(""" + 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 ? + 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,507 @@ +# 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, os, os.path, codecs +try: + set +except NameError: + from sets import Set as set # Python 2.3 fallback + +# Setting TEST_MODE to 1 enables cascading deletes (for table flush) which are dangerous +# 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 = 1 +if 'FB_DJANGO_TEST_MODE' in os.environ: + TEST_MODE = int(os.environ['FB_DJANGO_TEST_MODE']) + +DATA_TYPES = { + 'AutoField': '"AutoField"', + 'BooleanField': '"BooleanField"', + 'CharField': 'varchar(%(max_length)s)', + 'CommaSeparatedIntegerField': 'varchar(%(max_length)s) CHARACTER SET ASCII', + 'DateField': '"DateField"', + 'DateTimeField': '"DateTimeField"', + 'DecimalField': 'numeric(%(max_digits)s, %(decimal_places)s)', + 'DefaultCharField': '"CharField"', + 'FileField': 'varchar(%(max_length)s)', + 'FilePathField': 'varchar(%(max_length)s)', + 'FloatField': '"FloatField"', + 'ImageField': '"varchar(%(max_length)s)"', + 'IntegerField': '"IntegerField"', + 'IPAddressField': 'varchar(15) CHARACTER SET ASCII', + 'NullBooleanField': '"NullBooleanField"', + 'OneToOneField': '"OneToOneField"', + 'PhoneNumberField': '"PhoneNumberField"', + 'PositiveIntegerField': '"PositiveIntegerField"', + 'PositiveSmallIntegerField': '"PositiveSmallIntegerField"', + 'SlugField': 'varchar(%(max_length)s)', + 'SmallIntegerField': '"SmallIntegerField"', + 'LargeTextField': '"LargeTextField"', + 'TextField': '"TextField"', + 'TimeField': '"TimeField"', + 'URLField': 'varchar(%(max_length)s) CHARACTER SET ASCII', + 'USStateField': '"USStateField"' +} + +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): + from django.db import connection + 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] + +DEFAULT_MAX_LENGTH = 100 +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 = [ ('AutoField', 'integer'), + ('BooleanField', 'smallint CHECK (VALUE IN (0,1))'), + ('DateField', 'date'), + ('CharField', 'varchar(%i)' % DEFAULT_MAX_LENGTH), + ('DateTimeField', 'timestamp'), + ('FloatField', 'double precision'), + ('IntegerField', 'integer'), + ('IPAddressField', 'varchar(15) CHARACTER SET ASCII'), + ('NullBooleanField', 'smallint CHECK ((VALUE IN (0,1)) OR (VALUE IS NULL))'), + ('OneToOneField', 'integer'), + ('PhoneNumberField', 'varchar(20) CHARACTER SET ASCII'), + ('PositiveIntegerField', 'integer CHECK ((VALUE >= 0) OR (VALUE IS NULL))'), + ('PositiveSmallIntegerField', 'smallint CHECK ((VALUE >= 0) OR (VALUE IS NULL))'), + ('SmallIntegerField', 'smallint'), + ('TextField', 'varchar(%s)' % connection.FB_MAX_VARCHAR), + ('LargeTextField', 'blob sub_type text'), + ('TimeField', 'time'), + ('USStateField', 'varchar(2) CHARACTER SET ASCII') ] + + 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 + row_size = 0 + columns = [(f.db_type().strip('"'), f.get_internal_type(), f) for f in opts.local_fields] + columns_simple = [col[0] for col in columns] + text_field_type = '"TextField"' + max_alowed_bytes = 32765 + if 'TextField' in columns_simple: + max_length = 100 + num_text_fields = 0 + for column in columns: + num_text_fields += (column[0] == 'TextField') + if column[0].startswith('varchar'): + max_length = int(column[0].split('(')[1].split(')')[0]) + if column[1] in DATA_TYPES: + row_size += get_data_size(column[1], max_length) + if row_size > 65536: + max_alowed_bytes = int( (max_alowed_bytes/num_text_fields) - (row_size-65536) ) + n = max_alowed_bytes / connection.BYTES_PER_DEFAULT_CHAR + if n > 512: + text_field_type = 'varchar(%s)' % n + FB_TEXTFIELD_ALTERED = True + print + print "WARNING: Maximum number of characters in TextFields has changed to %s." % n + print " TextField columns with custom charsets will have %s chars available" % max_alowed_bytes + print " The change affects %s table only." % opts.db_table + print " TextFields in other tables will have %s characters maximum" % connection.FB_MAX_VARCHAR + print " or 32765 characters with custom (non-UTF) encoding." + print " If you need more space in those fields try LargeTextFields instead." + print + else: + # Swich to blobs if size is too small (<1024) + text_field_type = '"LargeTextField"' + + # Create tables + for f in opts.local_fields: + col_type = f.db_type() + if col_type.strip('"') == 'TextField': + col_type = text_field_type + fb_version = "%s.%s" % (connection.ops.firebird_version[0], connection.ops.firebird_version[1]) + page_size = connection.ops.page_size + #look at: http://www.volny.cz/iprenosil/interbase/ip_ib_indexcalculator.htm + 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: + print 'STRIP2ASCII', length, flength * connection.BYTES_PER_DEFAULT_CHAR, connection.ops.index_limit + strip2ascii = True + except TypeError: + print 'STRIP2ASCII calculation failed on', f + + 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_fileds = len(opts.unique_together[0]) + num_unique_nonchar_fileds = num_unique_fileds - num_unique_char_fields + limit = connection.ops.index_limit + limit -= ((num_unique_fileds - 1)*64) + limit -= 8*num_unique_nonchar_fileds + max_length = limit/num_unique_char_fields + ascii_length = int(f.max_length) + old_length = ascii_length*connection.BYTES_PER_DEFAULT_CHAR + + if (old_length >= max_length) and (ascii_length < max_length): + strip2ascii = True + elif old_length > max_length: + strip2ascii = False #We change it here + col_type = "varchar(%i) CHARACTER SET ASCII" % max_length + msg = "WARNING: Character set of the '%s' field\n" + msg += " (table %s)\n" + msg += " has changed to ASCII" + msg += " to fit %s-byte limit in FB %s" + if not page_size: + print msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version) + else: + msg += " with page size %s" + print msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version, page_size) + print " The maximum length of '%s' is now %s instead of %s"\ + % (f.column, max_length, old_length) + if strip2ascii: + col_type = "%s %s %s" % (col_type, "CHARACTER SET", "ASCII") + msg = "WARNING: Character set of the '%s' field\n" + msg += " (table %s)\n" + msg += " has changed to ASCII" + msg += " to fit %s-byte limit in FB %s" + if not page_size: + print msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version) + else: + msg += " with page size %s" + print msg % (f.column, opts.db_table, connection.ops.index_limit, fb_version, page_size) + + 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 col_type.strip('"') == 'TextField': + col_type = 'varchar(%i)' % max_alowed_bytes + col_type = "%s %s %s" % (col_type, "CHARACTER SET", charset) + + 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(style, 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(style, 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 = '%s_refs_%s_%x' % (r_col, col, + abs(hash((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 + +def sql_for_pending_references(model, style, pending_references): + """ + Returns any ALTER TABLE statements to add constraints after the fact. + """ + from django.db import connection + + qn = connection.ops.quote_name + final_output = [] + if TEST_MODE < 2: + opts = model._meta + if model in pending_references: + for rel_class, f in pending_references[model]: + rel_opts = rel_class._meta + r_table = rel_opts.db_table + r_col = f.column + table = opts.db_table + col = opts.get_field(f.rel.field_name).column + r_name = connection.ops.reference_name(r_col, col, r_table, table) + if not f.on_update: + f.on_update = 'CASCADE' + if not f.on_delete: + if TEST_MODE > 0: + f.on_delete = 'CASCADE' + else: + if f.null: + f.on_delete = 'SET NULL' + else: + f.on_delete = 'NO ACTION' + final_output.append(style.SQL_KEYWORD('ALTER TABLE') + ' %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) ON UPDATE %s ON DELETE %s;' % \ + (qn(r_table), r_name, qn(r_col), qn(table), qn(col), + f.on_update, f.on_delete)) + + del pending_references[model] + 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 = '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/conf/project_template/settings.py =================================================================== --- django/conf/project_template/settings.py (revision 7397) +++ 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/core/cache/backends/db.py =================================================================== --- django/core/cache/backends/db.py (revision 7397) +++ 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/sql.py =================================================================== --- django/core/management/sql.py (revision 7397) +++ 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,9 +324,14 @@ """ 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 - + + creation_module = get_creation_module() + # If the database backend wants to create many_to_many sql itself, let it + if hasattr(creation_module, "sql_for_pending_references"): + return creation_module.sql_for_pending_references(model, style, pending_references) + qn = connection.ops.quote_name final_output = [] if connection.features.supports_constraints: @@ -336,7 +345,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 +354,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 +425,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: tests/modeltests/custom_methods/models.py =================================================================== --- tests/modeltests/custom_methods/models.py (revision 7397) +++ 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 7397) +++ 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 7397) +++ 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 7397) +++ 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/initial_sql_regress/sql/simple.sql =================================================================== --- tests/regressiontests/initial_sql_regress/sql/simple.sql (revision 7397) +++ tests/regressiontests/initial_sql_regress/sql/simple.sql (working copy) @@ -1,8 +1,8 @@ -INSERT INTO initial_sql_regress_simple (name) VALUES ('John'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('Paul'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('Ringo'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('George'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('Miles O''Brien'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('Semicolon;Man'); -INSERT INTO initial_sql_regress_simple (name) VALUES ('This line has a Windows line ending'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('John'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('Paul'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('Ringo'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('George'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('Miles O''Brien'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('Semicolon;Man'); +INSERT INTO "initial_sql_regress_simple" ("name") VALUES ('This line has a Windows line ending'); Index: tests/regressiontests/backends/models.py =================================================================== --- tests/regressiontests/backends/models.py (revision 7397) +++ 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') [, , , , , , , , , , ]