From 28308078f397d1de36fd0da417ac7da2544ba12d Mon Sep 17 00:00:00 2001 From: Tim Graham Date: Mon, 12 Jan 2015 15:20:40 -0500 Subject: [PATCH] Fixed #22603 -- Reorganized classes in django.db.backends. --- .../contrib/gis/db/backends/base/__init__.py | 0 .../gis/db/backends/{ => base}/adapter.py | 0 .../contrib/gis/db/backends/base/features.py | 82 + .../db/backends/{base.py => base/models.py} | 202 --- .../gis/db/backends/base/operations.py | 114 ++ django/contrib/gis/db/backends/mysql/base.py | 24 +- .../contrib/gis/db/backends/mysql/features.py | 12 + .../gis/db/backends/mysql/operations.py | 7 +- .../contrib/gis/db/backends/oracle/adapter.py | 2 +- django/contrib/gis/db/backends/oracle/base.py | 19 +- .../gis/db/backends/oracle/features.py | 7 + .../contrib/gis/db/backends/oracle/models.py | 2 +- .../gis/db/backends/oracle/operations.py | 5 +- .../contrib/gis/db/backends/postgis/base.py | 17 +- .../gis/db/backends/postgis/features.py | 9 + .../contrib/gis/db/backends/postgis/models.py | 2 +- .../gis/db/backends/postgis/operations.py | 4 +- .../gis/db/backends/spatialite/adapter.py | 2 +- .../gis/db/backends/spatialite/base.py | 33 +- .../gis/db/backends/spatialite/features.py | 16 + .../gis/db/backends/spatialite/models.py | 2 +- .../gis/db/backends/spatialite/operations.py | 4 +- django/db/backends/__init__.py | 1540 ----------------- django/db/backends/base/__init__.py | 0 django/db/backends/base/base.py | 507 ++++++ django/db/backends/base/client.py | 15 + django/db/backends/{ => base}/creation.py | 10 +- django/db/backends/base/features.py | 250 +++ django/db/backends/base/introspection.py | 178 ++ django/db/backends/base/operations.py | 555 ++++++ django/db/backends/{ => base}/schema.py | 2 +- django/db/backends/base/validation.py | 38 + django/db/backends/dummy/base.py | 11 +- django/db/backends/mysql/base.py | 297 +--- django/db/backends/mysql/client.py | 2 +- django/db/backends/mysql/creation.py | 2 +- django/db/backends/mysql/features.py | 70 + django/db/backends/mysql/introspection.py | 8 +- django/db/backends/mysql/operations.py | 200 +++ django/db/backends/mysql/schema.py | 2 +- django/db/backends/mysql/validation.py | 2 +- django/db/backends/oracle/base.py | 561 +----- django/db/backends/oracle/client.py | 2 +- django/db/backends/oracle/creation.py | 2 +- django/db/backends/oracle/features.py | 57 + django/db/backends/oracle/introspection.py | 4 +- django/db/backends/oracle/operations.py | 447 +++++ django/db/backends/oracle/schema.py | 2 +- django/db/backends/oracle/utils.py | 42 + .../db/backends/postgresql_psycopg2/base.py | 54 +- .../db/backends/postgresql_psycopg2/client.py | 2 +- .../backends/postgresql_psycopg2/creation.py | 2 +- .../backends/postgresql_psycopg2/features.py | 28 + .../postgresql_psycopg2/introspection.py | 4 +- .../postgresql_psycopg2/operations.py | 2 +- .../db/backends/postgresql_psycopg2/schema.py | 6 +- .../db/backends/postgresql_psycopg2/utils.py | 7 + django/db/backends/sqlite3/base.py | 288 +-- django/db/backends/sqlite3/client.py | 2 +- django/db/backends/sqlite3/creation.py | 2 +- django/db/backends/sqlite3/features.py | 79 + django/db/backends/sqlite3/introspection.py | 4 +- django/db/backends/sqlite3/operations.py | 198 +++ django/db/backends/sqlite3/schema.py | 9 +- django/db/backends/sqlite3/utils.py | 11 + docs/ref/migration-operations.txt | 2 +- docs/ref/schema-editor.txt | 2 +- docs/releases/1.8.txt | 13 + tests/backends/tests.py | 2 +- 69 files changed, 3098 insertions(+), 2990 deletions(-) create mode 100644 django/contrib/gis/db/backends/base/__init__.py rename django/contrib/gis/db/backends/{ => base}/adapter.py (100%) create mode 100644 django/contrib/gis/db/backends/base/features.py rename django/contrib/gis/db/backends/{base.py => base/models.py} (50%) create mode 100644 django/contrib/gis/db/backends/base/operations.py create mode 100644 django/contrib/gis/db/backends/mysql/features.py create mode 100644 django/contrib/gis/db/backends/oracle/features.py create mode 100644 django/contrib/gis/db/backends/postgis/features.py create mode 100644 django/contrib/gis/db/backends/spatialite/features.py create mode 100644 django/db/backends/base/__init__.py create mode 100644 django/db/backends/base/base.py create mode 100644 django/db/backends/base/client.py rename django/db/backends/{ => base}/creation.py (99%) create mode 100644 django/db/backends/base/features.py create mode 100644 django/db/backends/base/introspection.py create mode 100644 django/db/backends/base/operations.py rename django/db/backends/{ => base}/schema.py (100%) create mode 100644 django/db/backends/base/validation.py create mode 100644 django/db/backends/mysql/features.py create mode 100644 django/db/backends/mysql/operations.py create mode 100644 django/db/backends/oracle/features.py create mode 100644 django/db/backends/oracle/operations.py create mode 100644 django/db/backends/oracle/utils.py create mode 100644 django/db/backends/postgresql_psycopg2/features.py create mode 100644 django/db/backends/postgresql_psycopg2/utils.py create mode 100644 django/db/backends/sqlite3/features.py create mode 100644 django/db/backends/sqlite3/operations.py create mode 100644 django/db/backends/sqlite3/utils.py diff --git a/django/contrib/gis/db/backends/base/__init__.py b/django/contrib/gis/db/backends/base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/contrib/gis/db/backends/adapter.py b/django/contrib/gis/db/backends/base/adapter.py similarity index 100% rename from django/contrib/gis/db/backends/adapter.py rename to django/contrib/gis/db/backends/base/adapter.py diff --git a/django/contrib/gis/db/backends/base/features.py b/django/contrib/gis/db/backends/base/features.py new file mode 100644 index 0000000000..faf471b6aa --- /dev/null +++ b/django/contrib/gis/db/backends/base/features.py @@ -0,0 +1,82 @@ +from functools import partial + + +class BaseSpatialFeatures(object): + gis_enabled = True + + # Does the database contain a SpatialRefSys model to store SRID information? + has_spatialrefsys_table = True + + # Does the backend support the django.contrib.gis.utils.add_srs_entry() utility? + supports_add_srs_entry = True + # Does the backend introspect GeometryField to its subtypes? + supports_geometry_field_introspection = True + + # Reference implementation of 3D functions is: + # http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions + supports_3d_functions = False + # Does the database support SRID transform operations? + supports_transform = True + # Do geometric relationship operations operate on real shapes (or only on bounding boxes)? + supports_real_shape_operations = True + # Can geometry fields be null? + supports_null_geometries = True + # Can the `distance` GeoQuerySet method be applied on geodetic coordinate systems? + supports_distance_geodetic = True + # Is the database able to count vertices on polygons (with `num_points`)? + supports_num_points_poly = True + + # The following properties indicate if the database backend support + # certain lookups (dwithin, left and right, relate, ...) + supports_distances_lookups = True + supports_left_right_lookups = False + + @property + def supports_bbcontains_lookup(self): + return 'bbcontains' in self.connection.ops.gis_operators + + @property + def supports_contained_lookup(self): + return 'contained' in self.connection.ops.gis_operators + + @property + def supports_dwithin_lookup(self): + return 'dwithin' in self.connection.ops.gis_operators + + @property + def supports_relate_lookup(self): + return 'relate' in self.connection.ops.gis_operators + + # For each of those methods, the class will have a property named + # `has__method` (defined in __init__) which accesses connection.ops + # to determine GIS method availability. + geoqueryset_methods = ( + 'area', 'centroid', 'difference', 'distance', 'distance_spheroid', + 'envelope', 'force_rhr', 'geohash', 'gml', 'intersection', 'kml', + 'length', 'num_geom', 'perimeter', 'point_on_surface', 'reverse', + 'scale', 'snap_to_grid', 'svg', 'sym_difference', 'transform', + 'translate', 'union', 'unionagg', + ) + + # Specifies whether the Collect and Extent aggregates are supported by the database + @property + def supports_collect_aggr(self): + return 'Collect' in self.connection.ops.valid_aggregates + + @property + def supports_extent_aggr(self): + return 'Extent' in self.connection.ops.valid_aggregates + + @property + def supports_make_line_aggr(self): + return 'MakeLine' in self.connection.ops.valid_aggregates + + def __init__(self, *args): + super(BaseSpatialFeatures, self).__init__(*args) + for method in self.geoqueryset_methods: + # Add dynamically properties for each GQS method, e.g. has_force_rhr_method, etc. + setattr(self.__class__, 'has_%s_method' % method, + property(partial(BaseSpatialFeatures.has_ops_method, method=method))) + + def has_ops_method(self, method): + return getattr(self.connection.ops, method, False) diff --git a/django/contrib/gis/db/backends/base.py b/django/contrib/gis/db/backends/base/models.py similarity index 50% rename from django/contrib/gis/db/backends/base.py rename to django/contrib/gis/db/backends/base/models.py index 8d8c2e7b7c..a84b23dfe8 100644 --- a/django/contrib/gis/db/backends/base.py +++ b/django/contrib/gis/db/backends/base/models.py @@ -1,8 +1,3 @@ -""" -Base/mixin classes for the spatial backend database operations and the -`SpatialRefSys` model. -""" -from functools import partial import re from django.contrib.gis import gdal @@ -10,203 +5,6 @@ from django.utils import six from django.utils.encoding import python_2_unicode_compatible -class BaseSpatialFeatures(object): - gis_enabled = True - - # Does the database contain a SpatialRefSys model to store SRID information? - has_spatialrefsys_table = True - - # Does the backend support the django.contrib.gis.utils.add_srs_entry() utility? - supports_add_srs_entry = True - # Does the backend introspect GeometryField to its subtypes? - supports_geometry_field_introspection = True - - # Reference implementation of 3D functions is: - # http://postgis.net/docs/PostGIS_Special_Functions_Index.html#PostGIS_3D_Functions - supports_3d_functions = False - # Does the database support SRID transform operations? - supports_transform = True - # Do geometric relationship operations operate on real shapes (or only on bounding boxes)? - supports_real_shape_operations = True - # Can geometry fields be null? - supports_null_geometries = True - # Can the `distance` GeoQuerySet method be applied on geodetic coordinate systems? - supports_distance_geodetic = True - # Is the database able to count vertices on polygons (with `num_points`)? - supports_num_points_poly = True - - # The following properties indicate if the database backend support - # certain lookups (dwithin, left and right, relate, ...) - supports_distances_lookups = True - supports_left_right_lookups = False - - @property - def supports_bbcontains_lookup(self): - return 'bbcontains' in self.connection.ops.gis_operators - - @property - def supports_contained_lookup(self): - return 'contained' in self.connection.ops.gis_operators - - @property - def supports_dwithin_lookup(self): - return 'dwithin' in self.connection.ops.gis_operators - - @property - def supports_relate_lookup(self): - return 'relate' in self.connection.ops.gis_operators - - # For each of those methods, the class will have a property named - # `has__method` (defined in __init__) which accesses connection.ops - # to determine GIS method availability. - geoqueryset_methods = ( - 'area', 'centroid', 'difference', 'distance', 'distance_spheroid', - 'envelope', 'force_rhr', 'geohash', 'gml', 'intersection', 'kml', - 'length', 'num_geom', 'perimeter', 'point_on_surface', 'reverse', - 'scale', 'snap_to_grid', 'svg', 'sym_difference', 'transform', - 'translate', 'union', 'unionagg', - ) - - # Specifies whether the Collect and Extent aggregates are supported by the database - @property - def supports_collect_aggr(self): - return 'Collect' in self.connection.ops.valid_aggregates - - @property - def supports_extent_aggr(self): - return 'Extent' in self.connection.ops.valid_aggregates - - @property - def supports_make_line_aggr(self): - return 'MakeLine' in self.connection.ops.valid_aggregates - - def __init__(self, *args): - super(BaseSpatialFeatures, self).__init__(*args) - for method in self.geoqueryset_methods: - # Add dynamically properties for each GQS method, e.g. has_force_rhr_method, etc. - setattr(self.__class__, 'has_%s_method' % method, - property(partial(BaseSpatialFeatures.has_ops_method, method=method))) - - def has_ops_method(self, method): - return getattr(self.connection.ops, method, False) - - -class BaseSpatialOperations(object): - """ - This module holds the base `BaseSpatialBackend` object, which is - instantiated by each spatial database backend with the features - it has. - """ - truncate_params = {} - - # Quick booleans for the type of this spatial backend, and - # an attribute for the spatial database version tuple (if applicable) - postgis = False - spatialite = False - mysql = False - oracle = False - spatial_version = None - - # How the geometry column should be selected. - select = None - - # Does the spatial database have a geometry or geography type? - geography = False - geometry = False - - area = False - centroid = False - difference = False - distance = False - distance_sphere = False - distance_spheroid = False - envelope = False - force_rhr = False - mem_size = False - bounding_circle = False - num_geom = False - num_points = False - perimeter = False - perimeter3d = False - point_on_surface = False - polygonize = False - reverse = False - scale = False - snap_to_grid = False - sym_difference = False - transform = False - translate = False - union = False - - # Aggregates - collect = False - extent = False - extent3d = False - make_line = False - unionagg = False - - # Serialization - geohash = False - geojson = False - gml = False - kml = False - svg = False - - # Constructors - from_text = False - from_wkb = False - - # Default conversion functions for aggregates; will be overridden if implemented - # for the spatial backend. - def convert_extent(self, box, srid): - raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') - - def convert_extent3d(self, box, srid): - raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.') - - def convert_geom(self, geom_val, geom_field): - raise NotImplementedError('Aggregate method not implemented for this spatial backend.') - - # For quoting column values, rather than columns. - def geo_quote_name(self, name): - return "'%s'" % name - - # GeometryField operations - def geo_db_type(self, f): - """ - Returns the database column type for the geometry field on - the spatial backend. - """ - raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_type() method') - - def get_distance(self, f, value, lookup_type): - """ - Returns the distance parameters for the given geometry field, - lookup value, and lookup type. - """ - raise NotImplementedError('Distance operations not available on this spatial backend.') - - def get_geom_placeholder(self, f, value, compiler): - """ - Returns the placeholder for the given geometry field with the given - value. Depending on the spatial backend, the placeholder may contain a - stored procedure call to the transformation function of the spatial - backend. - """ - raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_placeholder() method') - - # Spatial SQL Construction - def spatial_aggregate_sql(self, agg): - raise NotImplementedError('Aggregate support not implemented for this spatial backend.') - - # Routines for getting the OGC-compliant models. - def geometry_columns(self): - raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method') - - def spatial_ref_sys(self): - raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method') - - @python_2_unicode_compatible class SpatialRefSysMixin(object): """ diff --git a/django/contrib/gis/db/backends/base/operations.py b/django/contrib/gis/db/backends/base/operations.py new file mode 100644 index 0000000000..56f1f6c61e --- /dev/null +++ b/django/contrib/gis/db/backends/base/operations.py @@ -0,0 +1,114 @@ +class BaseSpatialOperations(object): + """ + This module holds the base `BaseSpatialBackend` object, which is + instantiated by each spatial database backend with the features + it has. + """ + truncate_params = {} + + # Quick booleans for the type of this spatial backend, and + # an attribute for the spatial database version tuple (if applicable) + postgis = False + spatialite = False + mysql = False + oracle = False + spatial_version = None + + # How the geometry column should be selected. + select = None + + # Does the spatial database have a geometry or geography type? + geography = False + geometry = False + + area = False + centroid = False + difference = False + distance = False + distance_sphere = False + distance_spheroid = False + envelope = False + force_rhr = False + mem_size = False + bounding_circle = False + num_geom = False + num_points = False + perimeter = False + perimeter3d = False + point_on_surface = False + polygonize = False + reverse = False + scale = False + snap_to_grid = False + sym_difference = False + transform = False + translate = False + union = False + + # Aggregates + collect = False + extent = False + extent3d = False + make_line = False + unionagg = False + + # Serialization + geohash = False + geojson = False + gml = False + kml = False + svg = False + + # Constructors + from_text = False + from_wkb = False + + # Default conversion functions for aggregates; will be overridden if implemented + # for the spatial backend. + def convert_extent(self, box, srid): + raise NotImplementedError('Aggregate extent not implemented for this spatial backend.') + + def convert_extent3d(self, box, srid): + raise NotImplementedError('Aggregate 3D extent not implemented for this spatial backend.') + + def convert_geom(self, geom_val, geom_field): + raise NotImplementedError('Aggregate method not implemented for this spatial backend.') + + # For quoting column values, rather than columns. + def geo_quote_name(self, name): + return "'%s'" % name + + # GeometryField operations + def geo_db_type(self, f): + """ + Returns the database column type for the geometry field on + the spatial backend. + """ + raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_type() method') + + def get_distance(self, f, value, lookup_type): + """ + Returns the distance parameters for the given geometry field, + lookup value, and lookup type. + """ + raise NotImplementedError('Distance operations not available on this spatial backend.') + + def get_geom_placeholder(self, f, value, compiler): + """ + Returns the placeholder for the given geometry field with the given + value. Depending on the spatial backend, the placeholder may contain a + stored procedure call to the transformation function of the spatial + backend. + """ + raise NotImplementedError('subclasses of BaseSpatialOperations must provide a geo_db_placeholder() method') + + # Spatial SQL Construction + def spatial_aggregate_sql(self, agg): + raise NotImplementedError('Aggregate support not implemented for this spatial backend.') + + # Routines for getting the OGC-compliant models. + def geometry_columns(self): + raise NotImplementedError('subclasses of BaseSpatialOperations must a provide geometry_columns() method') + + def spatial_ref_sys(self): + raise NotImplementedError('subclasses of BaseSpatialOperations must a provide spatial_ref_sys() method') diff --git a/django/contrib/gis/db/backends/mysql/base.py b/django/contrib/gis/db/backends/mysql/base.py index 802f7206f4..62702d1ffe 100644 --- a/django/contrib/gis/db/backends/mysql/base.py +++ b/django/contrib/gis/db/backends/mysql/base.py @@ -1,22 +1,10 @@ -from django.db.backends.mysql.base import ( - DatabaseWrapper as MySQLDatabaseWrapper, - DatabaseFeatures as MySQLDatabaseFeatures, -) -from django.contrib.gis.db.backends.base import BaseSpatialFeatures -from django.contrib.gis.db.backends.mysql.creation import MySQLCreation -from django.contrib.gis.db.backends.mysql.introspection import MySQLIntrospection -from django.contrib.gis.db.backends.mysql.operations import MySQLOperations -from django.contrib.gis.db.backends.mysql.schema import MySQLGISSchemaEditor +from django.db.backends.mysql.base import DatabaseWrapper as MySQLDatabaseWrapper - -class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): - has_spatialrefsys_table = False - supports_add_srs_entry = False - supports_distances_lookups = False - supports_transform = False - supports_real_shape_operations = False - supports_null_geometries = False - supports_num_points_poly = False +from .creation import MySQLCreation +from .features import DatabaseFeatures +from .introspection import MySQLIntrospection +from .operations import MySQLOperations +from .schema import MySQLGISSchemaEditor class DatabaseWrapper(MySQLDatabaseWrapper): diff --git a/django/contrib/gis/db/backends/mysql/features.py b/django/contrib/gis/db/backends/mysql/features.py new file mode 100644 index 0000000000..02c611b292 --- /dev/null +++ b/django/contrib/gis/db/backends/mysql/features.py @@ -0,0 +1,12 @@ +from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures +from django.db.backends.mysql.features import DatabaseFeatures as MySQLDatabaseFeatures + + +class DatabaseFeatures(BaseSpatialFeatures, MySQLDatabaseFeatures): + has_spatialrefsys_table = False + supports_add_srs_entry = False + supports_distances_lookups = False + supports_transform = False + supports_real_shape_operations = False + supports_null_geometries = False + supports_num_points_poly = False diff --git a/django/contrib/gis/db/backends/mysql/operations.py b/django/contrib/gis/db/backends/mysql/operations.py index a64f0cc60d..18dcca458d 100644 --- a/django/contrib/gis/db/backends/mysql/operations.py +++ b/django/contrib/gis/db/backends/mysql/operations.py @@ -1,8 +1,7 @@ -from django.db.backends.mysql.base import DatabaseOperations - -from django.contrib.gis.db.backends.adapter import WKTAdapter -from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.base.adapter import WKTAdapter +from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations from django.contrib.gis.db.backends.utils import SpatialOperator +from django.db.backends.mysql.operations import DatabaseOperations class MySQLOperations(DatabaseOperations, BaseSpatialOperations): diff --git a/django/contrib/gis/db/backends/oracle/adapter.py b/django/contrib/gis/db/backends/oracle/adapter.py index ca627bd37d..e26603e531 100644 --- a/django/contrib/gis/db/backends/oracle/adapter.py +++ b/django/contrib/gis/db/backends/oracle/adapter.py @@ -1,5 +1,5 @@ from cx_Oracle import CLOB -from django.contrib.gis.db.backends.adapter import WKTAdapter +from django.contrib.gis.db.backends.base.adapter import WKTAdapter class OracleSpatialAdapter(WKTAdapter): diff --git a/django/contrib/gis/db/backends/oracle/base.py b/django/contrib/gis/db/backends/oracle/base.py index 56db33a9e4..9d7ab7debd 100644 --- a/django/contrib/gis/db/backends/oracle/base.py +++ b/django/contrib/gis/db/backends/oracle/base.py @@ -1,17 +1,10 @@ -from django.db.backends.oracle.base import ( - DatabaseWrapper as OracleDatabaseWrapper, - DatabaseFeatures as OracleDatabaseFeatures, -) -from django.contrib.gis.db.backends.base import BaseSpatialFeatures -from django.contrib.gis.db.backends.oracle.creation import OracleCreation -from django.contrib.gis.db.backends.oracle.introspection import OracleIntrospection -from django.contrib.gis.db.backends.oracle.operations import OracleOperations -from django.contrib.gis.db.backends.oracle.schema import OracleGISSchemaEditor +from django.db.backends.oracle.base import DatabaseWrapper as OracleDatabaseWrapper - -class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures): - supports_add_srs_entry = False - supports_geometry_field_introspection = False +from .creation import OracleCreation +from .features import DatabaseFeatures +from .introspection import OracleIntrospection +from .operations import OracleOperations +from .schema import OracleGISSchemaEditor class DatabaseWrapper(OracleDatabaseWrapper): diff --git a/django/contrib/gis/db/backends/oracle/features.py b/django/contrib/gis/db/backends/oracle/features.py new file mode 100644 index 0000000000..ef56ec627d --- /dev/null +++ b/django/contrib/gis/db/backends/oracle/features.py @@ -0,0 +1,7 @@ +from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures +from django.db.backends.oracle.features import DatabaseFeatures as OracleDatabaseFeatures + + +class DatabaseFeatures(BaseSpatialFeatures, OracleDatabaseFeatures): + supports_add_srs_entry = False + supports_geometry_field_introspection = False diff --git a/django/contrib/gis/db/backends/oracle/models.py b/django/contrib/gis/db/backends/oracle/models.py index 3b7bbf665b..5d7a6323a9 100644 --- a/django/contrib/gis/db/backends/oracle/models.py +++ b/django/contrib/gis/db/backends/oracle/models.py @@ -8,7 +8,7 @@ model and the `SDO_COORD_REF_SYS` is used for the SpatialRefSys model. """ from django.contrib.gis.db import models -from django.contrib.gis.db.backends.base import SpatialRefSysMixin +from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin from django.utils.encoding import python_2_unicode_compatible diff --git a/django/contrib/gis/db/backends/oracle/operations.py b/django/contrib/gis/db/backends/oracle/operations.py index 4c37a501e8..5f371555a6 100644 --- a/django/contrib/gis/db/backends/oracle/operations.py +++ b/django/contrib/gis/db/backends/oracle/operations.py @@ -9,12 +9,13 @@ """ import re -from django.db.backends.oracle.base import DatabaseOperations, Database -from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations from django.contrib.gis.db.backends.oracle.adapter import OracleSpatialAdapter from django.contrib.gis.db.backends.utils import SpatialOperator from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance +from django.db.backends.oracle.base import Database +from django.db.backends.oracle.operations import DatabaseOperations from django.utils import six diff --git a/django/contrib/gis/db/backends/postgis/base.py b/django/contrib/gis/db/backends/postgis/base.py index 66e4cd2879..083f76d0a8 100644 --- a/django/contrib/gis/db/backends/postgis/base.py +++ b/django/contrib/gis/db/backends/postgis/base.py @@ -1,20 +1,15 @@ from django.conf import settings -from django.db.backends import NO_DB_ALIAS +from django.db.backends.base.base import NO_DB_ALIAS from django.db.backends.postgresql_psycopg2.base import ( DatabaseWrapper as Psycopg2DatabaseWrapper, - DatabaseFeatures as Psycopg2DatabaseFeatures ) -from django.contrib.gis.db.backends.base import BaseSpatialFeatures -from django.contrib.gis.db.backends.postgis.creation import PostGISCreation -from django.contrib.gis.db.backends.postgis.introspection import PostGISIntrospection -from django.contrib.gis.db.backends.postgis.operations import PostGISOperations -from django.contrib.gis.db.backends.postgis.schema import PostGISSchemaEditor from django.utils.functional import cached_property - -class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures): - supports_3d_functions = True - supports_left_right_lookups = True +from .creation import PostGISCreation +from .features import DatabaseFeatures +from .introspection import PostGISIntrospection +from .operations import PostGISOperations +from .schema import PostGISSchemaEditor class DatabaseWrapper(Psycopg2DatabaseWrapper): diff --git a/django/contrib/gis/db/backends/postgis/features.py b/django/contrib/gis/db/backends/postgis/features.py new file mode 100644 index 0000000000..dfa225c6e7 --- /dev/null +++ b/django/contrib/gis/db/backends/postgis/features.py @@ -0,0 +1,9 @@ +from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures +from django.db.backends.postgresql_psycopg2.features import ( + DatabaseFeatures as Psycopg2DatabaseFeatures, +) + + +class DatabaseFeatures(BaseSpatialFeatures, Psycopg2DatabaseFeatures): + supports_3d_functions = True + supports_left_right_lookups = True diff --git a/django/contrib/gis/db/backends/postgis/models.py b/django/contrib/gis/db/backends/postgis/models.py index 1baa6feff7..a438ffcc20 100644 --- a/django/contrib/gis/db/backends/postgis/models.py +++ b/django/contrib/gis/db/backends/postgis/models.py @@ -2,7 +2,7 @@ The GeometryColumns and SpatialRefSys models for the PostGIS backend. """ from django.db import models -from django.contrib.gis.db.backends.base import SpatialRefSysMixin +from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin from django.utils.encoding import python_2_unicode_compatible diff --git a/django/contrib/gis/db/backends/postgis/operations.py b/django/contrib/gis/db/backends/postgis/operations.py index db5cecc984..26a31b3d44 100644 --- a/django/contrib/gis/db/backends/postgis/operations.py +++ b/django/contrib/gis/db/backends/postgis/operations.py @@ -1,13 +1,13 @@ import re from django.conf import settings -from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter from django.contrib.gis.db.backends.utils import SpatialOperator from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance from django.core.exceptions import ImproperlyConfigured -from django.db.backends.postgresql_psycopg2.base import DatabaseOperations +from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations from django.db.utils import ProgrammingError from django.utils.functional import cached_property diff --git a/django/contrib/gis/db/backends/spatialite/adapter.py b/django/contrib/gis/db/backends/spatialite/adapter.py index 8a5c2d88bf..70ad900046 100644 --- a/django/contrib/gis/db/backends/spatialite/adapter.py +++ b/django/contrib/gis/db/backends/spatialite/adapter.py @@ -1,5 +1,5 @@ from django.db.backends.sqlite3.base import Database -from django.contrib.gis.db.backends.adapter import WKTAdapter +from django.contrib.gis.db.backends.base.adapter import WKTAdapter class SpatiaLiteAdapter(WKTAdapter): diff --git a/django/contrib/gis/db/backends/spatialite/base.py b/django/contrib/gis/db/backends/spatialite/base.py index 63efb3ac61..4f02dfb6b0 100644 --- a/django/contrib/gis/db/backends/spatialite/base.py +++ b/django/contrib/gis/db/backends/spatialite/base.py @@ -1,32 +1,19 @@ import sys from ctypes.util import find_library + from django.conf import settings - from django.core.exceptions import ImproperlyConfigured -from django.db.backends.sqlite3.base import (Database, - DatabaseWrapper as SQLiteDatabaseWrapper, - DatabaseFeatures as SQLiteDatabaseFeatures, SQLiteCursorWrapper) -from django.contrib.gis.db.backends.base import BaseSpatialFeatures -from django.contrib.gis.db.backends.spatialite.client import SpatiaLiteClient -from django.contrib.gis.db.backends.spatialite.creation import SpatiaLiteCreation -from django.contrib.gis.db.backends.spatialite.introspection import SpatiaLiteIntrospection -from django.contrib.gis.db.backends.spatialite.operations import SpatiaLiteOperations -from django.contrib.gis.db.backends.spatialite.schema import SpatialiteSchemaEditor +from django.db.backends.sqlite3.base import ( + Database, DatabaseWrapper as SQLiteDatabaseWrapper, SQLiteCursorWrapper, +) from django.utils import six -from django.utils.functional import cached_property - -class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures): - supports_distance_geodetic = False - # SpatiaLite can only count vertices in LineStrings - supports_num_points_poly = False - - @cached_property - def supports_initspatialmetadata_in_one_transaction(self): - # SpatiaLite 4.1+ support initializing all metadata in one transaction - # which can result in a significant performance improvement when - # creating the database. - return self.connection.ops.spatial_version >= (4, 1, 0) +from .client import SpatiaLiteClient +from .creation import SpatiaLiteCreation +from .features import DatabaseFeatures +from .introspection import SpatiaLiteIntrospection +from .operations import SpatiaLiteOperations +from .schema import SpatialiteSchemaEditor class DatabaseWrapper(SQLiteDatabaseWrapper): diff --git a/django/contrib/gis/db/backends/spatialite/features.py b/django/contrib/gis/db/backends/spatialite/features.py new file mode 100644 index 0000000000..4ab382e00b --- /dev/null +++ b/django/contrib/gis/db/backends/spatialite/features.py @@ -0,0 +1,16 @@ +from django.contrib.gis.db.backends.base.features import BaseSpatialFeatures +from django.db.backends.sqlite3.features import DatabaseFeatures as SQLiteDatabaseFeatures +from django.utils.functional import cached_property + + +class DatabaseFeatures(BaseSpatialFeatures, SQLiteDatabaseFeatures): + supports_distance_geodetic = False + # SpatiaLite can only count vertices in LineStrings + supports_num_points_poly = False + + @cached_property + def supports_initspatialmetadata_in_one_transaction(self): + # SpatiaLite 4.1+ support initializing all metadata in one transaction + # which can result in a significant performance improvement when + # creating the database. + return self.connection.ops.spatial_version >= (4, 1, 0) diff --git a/django/contrib/gis/db/backends/spatialite/models.py b/django/contrib/gis/db/backends/spatialite/models.py index 4b86391456..a1f0f2d44b 100644 --- a/django/contrib/gis/db/backends/spatialite/models.py +++ b/django/contrib/gis/db/backends/spatialite/models.py @@ -3,7 +3,7 @@ """ from django.db import connection, models from django.db.backends.signals import connection_created -from django.contrib.gis.db.backends.base import SpatialRefSysMixin +from django.contrib.gis.db.backends.base.models import SpatialRefSysMixin from django.contrib.gis.db.backends.spatialite.base import DatabaseWrapper from django.utils.encoding import python_2_unicode_compatible diff --git a/django/contrib/gis/db/backends/spatialite/operations.py b/django/contrib/gis/db/backends/spatialite/operations.py index b0f36f1eac..8940653930 100644 --- a/django/contrib/gis/db/backends/spatialite/operations.py +++ b/django/contrib/gis/db/backends/spatialite/operations.py @@ -1,13 +1,13 @@ import re import sys -from django.contrib.gis.db.backends.base import BaseSpatialOperations +from django.contrib.gis.db.backends.base.operations import BaseSpatialOperations from django.contrib.gis.db.backends.utils import SpatialOperator from django.contrib.gis.db.backends.spatialite.adapter import SpatiaLiteAdapter from django.contrib.gis.geometry.backend import Geometry from django.contrib.gis.measure import Distance from django.core.exceptions import ImproperlyConfigured -from django.db.backends.sqlite3.base import DatabaseOperations +from django.db.backends.sqlite3.operations import DatabaseOperations from django.db.utils import DatabaseError from django.utils import six from django.utils.functional import cached_property diff --git a/django/db/backends/__init__.py b/django/db/backends/__init__.py index fdb4d35d04..e69de29bb2 100644 --- a/django/db/backends/__init__.py +++ b/django/db/backends/__init__.py @@ -1,1540 +0,0 @@ -from collections import deque -import datetime -import decimal -import time -import warnings - -try: - from django.utils.six.moves import _thread as thread -except ImportError: - from django.utils.six.moves import _dummy_thread as thread -from collections import namedtuple -from contextlib import contextmanager -from importlib import import_module - -from django.conf import settings -from django.core import checks -from django.db import DEFAULT_DB_ALIAS -from django.db.backends.signals import connection_created -from django.db.backends import utils -from django.db.transaction import TransactionManagementError -from django.db.utils import DatabaseError, DatabaseErrorWrapper, ProgrammingError -from django.utils.dateparse import parse_duration -from django.utils.deprecation import RemovedInDjango19Warning -from django.utils.functional import cached_property -from django.utils import six -from django.utils import timezone - -# Structure returned by DatabaseIntrospection.get_table_list() -TableInfo = namedtuple('TableInfo', ['name', 'type']) - -# Structure returned by the DB-API cursor.description interface (PEP 249) -FieldInfo = namedtuple('FieldInfo', - 'name type_code display_size internal_size precision scale null_ok') - -NO_DB_ALIAS = '__no_db__' - - -class BaseDatabaseWrapper(object): - """ - Represents a database connection. - """ - # Mapping of Field objects to their column types. - data_types = {} - # Mapping of Field objects to their SQL suffix such as AUTOINCREMENT. - data_types_suffix = {} - # Mapping of Field objects to their SQL for CHECK constraints. - data_type_check_constraints = {} - ops = None - vendor = 'unknown' - SchemaEditorClass = None - - queries_limit = 9000 - - def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS, - allow_thread_sharing=False): - # Connection related attributes. - # The underlying database connection. - self.connection = None - # `settings_dict` should be a dictionary containing keys such as - # NAME, USER, etc. It's called `settings_dict` instead of `settings` - # to disambiguate it from Django settings modules. - self.settings_dict = settings_dict - self.alias = alias - # Query logging in debug mode or when explicitly enabled. - self.queries_log = deque(maxlen=self.queries_limit) - self.force_debug_cursor = False - - # Transaction related attributes. - # Tracks if the connection is in autocommit mode. Per PEP 249, by - # default, it isn't. - self.autocommit = False - # Tracks if the connection is in a transaction managed by 'atomic'. - self.in_atomic_block = False - # Increment to generate unique savepoint ids. - self.savepoint_state = 0 - # List of savepoints created by 'atomic'. - self.savepoint_ids = [] - # Tracks if the outermost 'atomic' block should commit on exit, - # ie. if autocommit was active on entry. - self.commit_on_exit = True - # Tracks if the transaction should be rolled back to the next - # available savepoint because of an exception in an inner block. - self.needs_rollback = False - - # Connection termination related attributes. - self.close_at = None - self.closed_in_transaction = False - self.errors_occurred = False - - # Thread-safety related attributes. - self.allow_thread_sharing = allow_thread_sharing - self._thread_ident = thread.get_ident() - - @property - def queries_logged(self): - return self.force_debug_cursor or settings.DEBUG - - @property - def queries(self): - if len(self.queries_log) == self.queries_log.maxlen: - warnings.warn( - "Limit for query logging exceeded, only the last {} queries " - "will be returned.".format(self.queries_log.maxlen)) - return list(self.queries_log) - - ##### Backend-specific methods for creating connections and cursors ##### - - def get_connection_params(self): - """Returns a dict of parameters suitable for get_new_connection.""" - raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a get_connection_params() method') - - def get_new_connection(self, conn_params): - """Opens a connection to the database.""" - raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a get_new_connection() method') - - def init_connection_state(self): - """Initializes the database connection settings.""" - raise NotImplementedError('subclasses of BaseDatabaseWrapper may require an init_connection_state() method') - - def create_cursor(self): - """Creates a cursor. Assumes that a connection is established.""" - raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a create_cursor() method') - - ##### Backend-specific methods for creating connections ##### - - def connect(self): - """Connects to the database. Assumes that the connection is closed.""" - # In case the previous connection was closed while in an atomic block - self.in_atomic_block = False - self.savepoint_ids = [] - self.needs_rollback = False - # Reset parameters defining when to close the connection - max_age = self.settings_dict['CONN_MAX_AGE'] - self.close_at = None if max_age is None else time.time() + max_age - self.closed_in_transaction = False - self.errors_occurred = False - # Establish the connection - conn_params = self.get_connection_params() - self.connection = self.get_new_connection(conn_params) - self.set_autocommit(self.settings_dict['AUTOCOMMIT']) - self.init_connection_state() - connection_created.send(sender=self.__class__, connection=self) - - def ensure_connection(self): - """ - Guarantees that a connection to the database is established. - """ - if self.connection is None: - with self.wrap_database_errors: - self.connect() - - ##### Backend-specific wrappers for PEP-249 connection methods ##### - - def _cursor(self): - self.ensure_connection() - with self.wrap_database_errors: - return self.create_cursor() - - def _commit(self): - if self.connection is not None: - with self.wrap_database_errors: - return self.connection.commit() - - def _rollback(self): - if self.connection is not None: - with self.wrap_database_errors: - return self.connection.rollback() - - def _close(self): - if self.connection is not None: - with self.wrap_database_errors: - return self.connection.close() - - ##### Generic wrappers for PEP-249 connection methods ##### - - def cursor(self): - """ - Creates a cursor, opening a connection if necessary. - """ - self.validate_thread_sharing() - if self.queries_logged: - cursor = self.make_debug_cursor(self._cursor()) - else: - cursor = self.make_cursor(self._cursor()) - return cursor - - def commit(self): - """ - Commits a transaction and resets the dirty flag. - """ - self.validate_thread_sharing() - self.validate_no_atomic_block() - self._commit() - # A successful commit means that the database connection works. - self.errors_occurred = False - - def rollback(self): - """ - Rolls back a transaction and resets the dirty flag. - """ - self.validate_thread_sharing() - self.validate_no_atomic_block() - self._rollback() - # A successful rollback means that the database connection works. - self.errors_occurred = False - - def close(self): - """ - Closes the connection to the database. - """ - self.validate_thread_sharing() - # Don't call validate_no_atomic_block() to avoid making it difficult - # to get rid of a connection in an invalid state. The next connect() - # will reset the transaction state anyway. - if self.closed_in_transaction or self.connection is None: - return - try: - self._close() - finally: - if self.in_atomic_block: - self.closed_in_transaction = True - self.needs_rollback = True - else: - self.connection = None - - ##### Backend-specific savepoint management methods ##### - - def _savepoint(self, sid): - with self.cursor() as cursor: - cursor.execute(self.ops.savepoint_create_sql(sid)) - - def _savepoint_rollback(self, sid): - with self.cursor() as cursor: - cursor.execute(self.ops.savepoint_rollback_sql(sid)) - - def _savepoint_commit(self, sid): - with self.cursor() as cursor: - cursor.execute(self.ops.savepoint_commit_sql(sid)) - - def _savepoint_allowed(self): - # Savepoints cannot be created outside a transaction - return self.features.uses_savepoints and not self.get_autocommit() - - ##### Generic savepoint management methods ##### - - def savepoint(self): - """ - Creates a savepoint inside the current transaction. Returns an - identifier for the savepoint that will be used for the subsequent - rollback or commit. Does nothing if savepoints are not supported. - """ - if not self._savepoint_allowed(): - return - - thread_ident = thread.get_ident() - tid = str(thread_ident).replace('-', '') - - self.savepoint_state += 1 - sid = "s%s_x%d" % (tid, self.savepoint_state) - - self.validate_thread_sharing() - self._savepoint(sid) - - return sid - - def savepoint_rollback(self, sid): - """ - Rolls back to a savepoint. Does nothing if savepoints are not supported. - """ - if not self._savepoint_allowed(): - return - - self.validate_thread_sharing() - self._savepoint_rollback(sid) - - def savepoint_commit(self, sid): - """ - Releases a savepoint. Does nothing if savepoints are not supported. - """ - if not self._savepoint_allowed(): - return - - self.validate_thread_sharing() - self._savepoint_commit(sid) - - def clean_savepoints(self): - """ - Resets the counter used to generate unique savepoint ids in this thread. - """ - self.savepoint_state = 0 - - ##### Backend-specific transaction management methods ##### - - def _set_autocommit(self, autocommit): - """ - Backend-specific implementation to enable or disable autocommit. - """ - raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a _set_autocommit() method') - - ##### Generic transaction management methods ##### - - def get_autocommit(self): - """ - Check the autocommit state. - """ - self.ensure_connection() - return self.autocommit - - def set_autocommit(self, autocommit): - """ - Enable or disable autocommit. - """ - self.validate_no_atomic_block() - self.ensure_connection() - self._set_autocommit(autocommit) - self.autocommit = autocommit - - def get_rollback(self): - """ - Get the "needs rollback" flag -- for *advanced use* only. - """ - if not self.in_atomic_block: - raise TransactionManagementError( - "The rollback flag doesn't work outside of an 'atomic' block.") - return self.needs_rollback - - def set_rollback(self, rollback): - """ - Set or unset the "needs rollback" flag -- for *advanced use* only. - """ - if not self.in_atomic_block: - raise TransactionManagementError( - "The rollback flag doesn't work outside of an 'atomic' block.") - self.needs_rollback = rollback - - def validate_no_atomic_block(self): - """ - Raise an error if an atomic block is active. - """ - if self.in_atomic_block: - raise TransactionManagementError( - "This is forbidden when an 'atomic' block is active.") - - def validate_no_broken_transaction(self): - if self.needs_rollback: - raise TransactionManagementError( - "An error occurred in the current transaction. You can't " - "execute queries until the end of the 'atomic' block.") - - ##### Foreign key constraints checks handling ##### - - @contextmanager - def constraint_checks_disabled(self): - """ - Context manager that disables foreign key constraint checking. - """ - disabled = self.disable_constraint_checking() - try: - yield - finally: - if disabled: - self.enable_constraint_checking() - - def disable_constraint_checking(self): - """ - Backends can implement as needed to temporarily disable foreign key - constraint checking. Should return True if the constraints were - disabled and will need to be reenabled. - """ - return False - - def enable_constraint_checking(self): - """ - Backends can implement as needed to re-enable foreign key constraint - checking. - """ - pass - - def check_constraints(self, table_names=None): - """ - Backends can override this method if they can apply constraint - checking (e.g. via "SET CONSTRAINTS ALL IMMEDIATE"). Should raise an - IntegrityError if any invalid foreign key references are encountered. - """ - pass - - ##### Connection termination handling ##### - - def is_usable(self): - """ - Tests if the database connection is usable. - - This function may assume that self.connection is not None. - - Actual implementations should take care not to raise exceptions - as that may prevent Django from recycling unusable connections. - """ - raise NotImplementedError( - "subclasses of BaseDatabaseWrapper may require an is_usable() method") - - def close_if_unusable_or_obsolete(self): - """ - Closes the current connection if unrecoverable errors have occurred, - or if it outlived its maximum age. - """ - if self.connection is not None: - # If the application didn't restore the original autocommit setting, - # don't take chances, drop the connection. - if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']: - self.close() - return - - # If an exception other than DataError or IntegrityError occurred - # since the last commit / rollback, check if the connection works. - if self.errors_occurred: - if self.is_usable(): - self.errors_occurred = False - else: - self.close() - return - - if self.close_at is not None and time.time() >= self.close_at: - self.close() - return - - ##### Thread safety handling ##### - - def validate_thread_sharing(self): - """ - Validates that the connection isn't accessed by another thread than the - one which originally created it, unless the connection was explicitly - authorized to be shared between threads (via the `allow_thread_sharing` - property). Raises an exception if the validation fails. - """ - if not (self.allow_thread_sharing - or self._thread_ident == thread.get_ident()): - raise DatabaseError("DatabaseWrapper objects created in a " - "thread can only be used in that same thread. The object " - "with alias '%s' was created in thread id %s and this is " - "thread id %s." - % (self.alias, self._thread_ident, thread.get_ident())) - - ##### Miscellaneous ##### - - def prepare_database(self): - """ - Hook to do any database check or preparation, generally called before - migrating a project or an app. - """ - pass - - @cached_property - def wrap_database_errors(self): - """ - Context manager and decorator that re-throws backend-specific database - exceptions using Django's common wrappers. - """ - return DatabaseErrorWrapper(self) - - def make_debug_cursor(self, cursor): - """ - Creates a cursor that logs all queries in self.queries_log. - """ - return utils.CursorDebugWrapper(cursor, self) - - def make_cursor(self, cursor): - """ - Creates a cursor without debug logging. - """ - return utils.CursorWrapper(cursor, self) - - @contextmanager - def temporary_connection(self): - """ - Context manager that ensures that a connection is established, and - if it opened one, closes it to avoid leaving a dangling connection. - This is useful for operations outside of the request-response cycle. - - Provides a cursor: with self.temporary_connection() as cursor: ... - """ - must_close = self.connection is None - cursor = self.cursor() - try: - yield cursor - finally: - cursor.close() - if must_close: - self.close() - - @cached_property - def _nodb_connection(self): - """ - Alternative connection to be used when there is no need to access - the main database, specifically for test db creation/deletion. - This also prevents the production database from being exposed to - potential child threads while (or after) the test database is destroyed. - Refs #10868, #17786, #16969. - """ - settings_dict = self.settings_dict.copy() - settings_dict['NAME'] = None - #backend = load_backend(settings_dict['ENGINE']) - nodb_connection = self.__class__( - settings_dict, - alias=NO_DB_ALIAS, - allow_thread_sharing=False) - return nodb_connection - - def _start_transaction_under_autocommit(self): - """ - Only required when autocommits_when_autocommit_is_off = True. - """ - raise NotImplementedError( - 'subclasses of BaseDatabaseWrapper may require a ' - '_start_transaction_under_autocommit() method' - ) - - def schema_editor(self, *args, **kwargs): - """ - Returns a new instance of this backend's SchemaEditor. - """ - if self.SchemaEditorClass is None: - raise NotImplementedError( - 'The SchemaEditorClass attribute of this database wrapper is still None') - return self.SchemaEditorClass(self, *args, **kwargs) - - -class BaseDatabaseFeatures(object): - gis_enabled = False - allows_group_by_pk = False - # True if django.db.backends.utils.typecast_timestamp is used on values - # returned from dates() calls. - needs_datetime_string_cast = True - empty_fetchmany_value = [] - update_can_self_select = True - - # Does the backend distinguish between '' and None? - interprets_empty_strings_as_nulls = False - - # Does the backend allow inserting duplicate NULL rows in a nullable - # unique field? All core backends implement this correctly, but other - # databases such as SQL Server do not. - supports_nullable_unique_constraints = True - - # Does the backend allow inserting duplicate rows when a unique_together - # constraint exists and some fields are nullable but not all of them? - supports_partially_nullable_unique_constraints = True - - can_use_chunked_reads = True - can_return_id_from_insert = False - has_bulk_insert = False - uses_savepoints = False - can_release_savepoints = False - can_combine_inserts_with_and_without_auto_increment_pk = False - - # If True, don't use integer foreign keys referring to, e.g., positive - # integer primary keys. - related_fields_match_type = False - allow_sliced_subqueries = True - has_select_for_update = False - has_select_for_update_nowait = False - - supports_select_related = True - - # Does the default test database allow multiple connections? - # Usually an indication that the test database is in-memory - test_db_allows_multiple_connections = True - - # Can an object be saved without an explicit primary key? - supports_unspecified_pk = False - - # Can a fixture contain forward references? i.e., are - # FK constraints checked at the end of transaction, or - # at the end of each save operation? - supports_forward_references = True - - # Does the backend truncate names properly when they are too long? - truncates_names = False - - # Is there a REAL datatype in addition to floats/doubles? - has_real_datatype = False - supports_subqueries_in_group_by = True - supports_bitwise_or = True - - # Is there a true datatype for timedeltas? - has_native_duration_field = False - - # Does the database driver support timedeltas as arguments? - # This is only relevant when there is a native duration field. - # Specifically, there is a bug with cx_Oracle: - # https://bitbucket.org/anthony_tuininga/cx_oracle/issue/7/ - driver_supports_timedelta_args = False - - # Do time/datetime fields have microsecond precision? - supports_microsecond_precision = True - - # Does the __regex lookup support backreferencing and grouping? - supports_regex_backreferencing = True - - # Can date/datetime lookups be performed using a string? - supports_date_lookup_using_string = True - - # Can datetimes with timezones be used? - supports_timezones = True - - # Does the database have a copy of the zoneinfo database? - has_zoneinfo_database = True - - # When performing a GROUP BY, is an ORDER BY NULL required - # to remove any ordering? - requires_explicit_null_ordering_when_grouping = False - - # Does the backend order NULL values as largest or smallest? - nulls_order_largest = False - - # Is there a 1000 item limit on query parameters? - supports_1000_query_parameters = True - - # Can an object have an autoincrement primary key of 0? MySQL says No. - allows_auto_pk_0 = True - - # Do we need to NULL a ForeignKey out, or can the constraint check be - # deferred - can_defer_constraint_checks = False - - # date_interval_sql can properly handle mixed Date/DateTime fields and timedeltas - supports_mixed_date_datetime_comparisons = True - - # Does the backend support tablespaces? Default to False because it isn't - # in the SQL standard. - supports_tablespaces = False - - # Does the backend reset sequences between tests? - supports_sequence_reset = True - - # Can the backend determine reliably the length of a CharField? - can_introspect_max_length = True - - # Can the backend determine reliably if a field is nullable? - # Note that this is separate from interprets_empty_strings_as_nulls, - # although the latter feature, when true, interferes with correct - # setting (and introspection) of CharFields' nullability. - # This is True for all core backends. - can_introspect_null = True - - # Confirm support for introspected foreign keys - # Every database can do this reliably, except MySQL, - # which can't do it for MyISAM tables - can_introspect_foreign_keys = True - - # Can the backend introspect an AutoField, instead of an IntegerField? - can_introspect_autofield = False - - # Can the backend introspect a BigIntegerField, instead of an IntegerField? - can_introspect_big_integer_field = True - - # Can the backend introspect an BinaryField, instead of an TextField? - can_introspect_binary_field = True - - # Can the backend introspect an DecimalField, instead of an FloatField? - can_introspect_decimal_field = True - - # Can the backend introspect an IPAddressField, instead of an CharField? - can_introspect_ip_address_field = False - - # Can the backend introspect a PositiveIntegerField, instead of an IntegerField? - can_introspect_positive_integer_field = False - - # Can the backend introspect a SmallIntegerField, instead of an IntegerField? - can_introspect_small_integer_field = False - - # Can the backend introspect a TimeField, instead of a DateTimeField? - can_introspect_time_field = True - - # Support for the DISTINCT ON clause - can_distinct_on_fields = False - - # Does the backend decide to commit before SAVEPOINT statements - # when autocommit is disabled? http://bugs.python.org/issue8145#msg109965 - autocommits_when_autocommit_is_off = False - - # Does the backend prevent running SQL queries in broken transactions? - atomic_transactions = True - - # Can we roll back DDL in a transaction? - can_rollback_ddl = False - - # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? - supports_combined_alters = False - - # Does it support foreign keys? - supports_foreign_keys = True - - # Does it support CHECK constraints? - supports_column_check_constraints = True - - # Does the backend support 'pyformat' style ("... %(name)s ...", {'name': value}) - # parameter passing? Note this can be provided by the backend even if not - # supported by the Python driver - supports_paramstyle_pyformat = True - - # Does the backend require literal defaults, rather than parameterized ones? - requires_literal_defaults = False - - # Does the backend require a connection reset after each material schema change? - connection_persists_old_columns = False - - # What kind of error does the backend throw when accessing closed cursor? - closed_cursor_error_class = ProgrammingError - - # Does 'a' LIKE 'A' match? - has_case_insensitive_like = True - - # Does the backend require the sqlparse library for splitting multi-line - # statements before executing them? - requires_sqlparse_for_splitting = True - - # Suffix for backends that don't support "SELECT xxx;" queries. - bare_select_suffix = '' - - # If NULL is implied on columns without needing to be explicitly specified - implied_column_null = False - - uppercases_column_names = False - - # Does the backend support "select for update" queries with limit (and offset)? - supports_select_for_update_with_limit = True - - def __init__(self, connection): - self.connection = connection - - @cached_property - def supports_transactions(self): - """Confirm support for transactions.""" - with self.connection.cursor() as cursor: - cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)') - self.connection.set_autocommit(False) - cursor.execute('INSERT INTO ROLLBACK_TEST (X) VALUES (8)') - self.connection.rollback() - self.connection.set_autocommit(True) - cursor.execute('SELECT COUNT(X) FROM ROLLBACK_TEST') - count, = cursor.fetchone() - cursor.execute('DROP TABLE ROLLBACK_TEST') - return count == 0 - - @cached_property - def supports_stddev(self): - """Confirm support for STDDEV and related stats functions.""" - class StdDevPop(object): - sql_function = 'STDDEV_POP' - - try: - self.connection.ops.check_aggregate_support(StdDevPop()) - return True - except NotImplementedError: - return False - - def introspected_boolean_field_type(self, field=None, created_separately=False): - """ - What is the type returned when the backend introspects a BooleanField? - The optional arguments may be used to give further details of the field to be - introspected; in particular, they are provided by Django's test suite: - field -- the field definition - created_separately -- True if the field was added via a SchemaEditor's AddField, - False if the field was created with the model - - Note that return value from this function is compared by tests against actual - introspection results; it should provide expectations, not run an introspection - itself. - """ - if self.can_introspect_null and field and field.null: - return 'NullBooleanField' - return 'BooleanField' - - -class BaseDatabaseOperations(object): - """ - This class encapsulates all backend-specific differences, such as the way - a backend performs ordering or calculates the ID of a recently-inserted - row. - """ - compiler_module = "django.db.models.sql.compiler" - - # Integer field safe ranges by `internal_type` as documented - # in docs/ref/models/fields.txt. - integer_field_ranges = { - 'SmallIntegerField': (-32768, 32767), - 'IntegerField': (-2147483648, 2147483647), - 'BigIntegerField': (-9223372036854775808, 9223372036854775807), - 'PositiveSmallIntegerField': (0, 32767), - 'PositiveIntegerField': (0, 2147483647), - } - - def __init__(self, connection): - self.connection = connection - self._cache = None - - def autoinc_sql(self, table, column): - """ - Returns any SQL needed to support auto-incrementing primary keys, or - None if no SQL is necessary. - - This SQL is executed when a table is created. - """ - return None - - def bulk_batch_size(self, fields, objs): - """ - Returns the maximum allowed batch size for the backend. The fields - are the fields going to be inserted in the batch, the objs contains - all the objects to be inserted. - """ - return len(objs) - - def cache_key_culling_sql(self): - """ - Returns an SQL query that retrieves the first cache key greater than the - n smallest. - - This is used by the 'db' cache backend to determine where to start - culling. - """ - return "SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" - - def unification_cast_sql(self, output_field): - """ - Given a field instance, returns the SQL necessary to cast the result of - a union to that type. Note that the resulting string should contain a - '%s' placeholder for the expression being cast. - """ - return '%s' - - def date_extract_sql(self, lookup_type, field_name): - """ - Given a lookup_type of 'year', 'month' or 'day', returns the SQL that - extracts a value from the given date field field_name. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_extract_sql() method') - - def date_interval_sql(self, sql, connector, timedelta): - """ - Implements the date interval functionality for expressions - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_interval_sql() method') - - def date_trunc_sql(self, lookup_type, field_name): - """ - Given a lookup_type of 'year', 'month' or 'day', returns the SQL that - truncates the given date field field_name to a date object with only - the given specificity. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetrunc_sql() method') - - def datetime_cast_sql(self): - """ - Returns the SQL necessary to cast a datetime value so that it will be - retrieved as a Python datetime object instead of a string. - - This SQL should include a '%s' in place of the field's name. - """ - return "%s" - - def datetime_extract_sql(self, lookup_type, field_name, tzname): - """ - Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or - 'second', returns the SQL that extracts a value from the given - datetime field field_name, and a tuple of parameters. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_extract_sql() method') - - def datetime_trunc_sql(self, lookup_type, field_name, tzname): - """ - Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or - 'second', returns the SQL that truncates the given datetime field - field_name to a datetime object with only the given specificity, and - a tuple of parameters. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_trunk_sql() method') - - def deferrable_sql(self): - """ - Returns the SQL necessary to make a constraint "initially deferred" - during a CREATE TABLE statement. - """ - return '' - - def distinct_sql(self, fields): - """ - Returns an SQL DISTINCT clause which removes duplicate rows from the - result set. If any fields are given, only the given fields are being - checked for duplicates. - """ - if fields: - raise NotImplementedError('DISTINCT ON fields is not supported by this database backend') - else: - return 'DISTINCT' - - def drop_foreignkey_sql(self): - """ - Returns the SQL command that drops a foreign key. - """ - return "DROP CONSTRAINT" - - def drop_sequence_sql(self, table): - """ - Returns any SQL necessary to drop the sequence for the given table. - Returns None if no SQL is necessary. - """ - return None - - def fetch_returned_insert_id(self, cursor): - """ - Given a cursor object that has just performed an INSERT...RETURNING - statement into a table that has an auto-incrementing ID, returns the - newly created ID. - """ - return cursor.fetchone()[0] - - def field_cast_sql(self, db_type, internal_type): - """ - Given a column type (e.g. 'BLOB', 'VARCHAR'), and an internal type - (e.g. 'GenericIPAddressField'), returns the SQL necessary to cast it - before using it in a WHERE statement. Note that the resulting string - should contain a '%s' placeholder for the column being searched against. - """ - return '%s' - - def force_no_ordering(self): - """ - Returns a list used in the "ORDER BY" clause to force no ordering at - all. Returning an empty list means that nothing will be included in the - ordering. - """ - return [] - - def for_update_sql(self, nowait=False): - """ - Returns the FOR UPDATE SQL clause to lock rows for an update operation. - """ - if nowait: - return 'FOR UPDATE NOWAIT' - else: - return 'FOR UPDATE' - - def fulltext_search_sql(self, field_name): - """ - Returns the SQL WHERE clause to use in order to perform a full-text - search of the given field_name. Note that the resulting string should - contain a '%s' placeholder for the value being searched against. - """ - raise NotImplementedError('Full-text search is not implemented for this database backend') - - def last_executed_query(self, cursor, sql, params): - """ - Returns a string of the query last executed by the given cursor, with - placeholders replaced with actual values. - - `sql` is the raw query containing placeholders, and `params` is the - sequence of parameters. These are used by default, but this method - exists for database backends to provide a better implementation - according to their own quoting schemes. - """ - from django.utils.encoding import force_text - - # Convert params to contain Unicode values. - to_unicode = lambda s: force_text(s, strings_only=True, errors='replace') - if isinstance(params, (list, tuple)): - u_params = tuple(to_unicode(val) for val in params) - elif params is None: - u_params = () - else: - u_params = {to_unicode(k): to_unicode(v) for k, v in params.items()} - - return six.text_type("QUERY = %r - PARAMS = %r") % (sql, u_params) - - def last_insert_id(self, cursor, table_name, pk_name): - """ - Given a cursor object that has just performed an INSERT statement into - a table that has an auto-incrementing ID, returns the newly created ID. - - This method also receives the table name and the name of the primary-key - column. - """ - return cursor.lastrowid - - def lookup_cast(self, lookup_type): - """ - Returns the string to use in a query when performing lookups - ("contains", "like", etc). The resulting string should contain a '%s' - placeholder for the column being searched against. - """ - return "%s" - - def max_in_list_size(self): - """ - Returns the maximum number of items that can be passed in a single 'IN' - list condition, or None if the backend does not impose a limit. - """ - return None - - def max_name_length(self): - """ - Returns the maximum length of table and column names, or None if there - is no limit. - """ - return None - - def no_limit_value(self): - """ - Returns the value to use for the LIMIT when we are wanting "LIMIT - infinity". Returns None if the limit clause can be omitted in this case. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a no_limit_value() method') - - 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 'DEFAULT' - - def prepare_sql_script(self, sql, _allow_fallback=False): - """ - Takes a SQL script that may contain multiple lines and returns a list - of statements to feed to successive cursor.execute() calls. - - Since few databases are able to process raw SQL scripts in a single - cursor.execute() call and PEP 249 doesn't talk about this use case, - the default implementation is conservative. - """ - # Remove _allow_fallback and keep only 'return ...' in Django 1.9. - try: - # This import must stay inside the method because it's optional. - import sqlparse - except ImportError: - if _allow_fallback: - # Without sqlparse, fall back to the legacy (and buggy) logic. - warnings.warn( - "Providing initial SQL data on a %s database will require " - "sqlparse in Django 1.9." % self.connection.vendor, - RemovedInDjango19Warning) - from django.core.management.sql import _split_statements - return _split_statements(sql) - else: - raise - else: - return [sqlparse.format(statement, strip_comments=True) - for statement in sqlparse.split(sql) if statement] - - def process_clob(self, value): - """ - Returns the value of a CLOB column, for backends that return a locator - object that requires additional processing. - """ - return value - - def return_insert_id(self): - """ - For backends that support returning the last insert ID as part - of an insert query, this method returns the SQL and params to - append to the INSERT query. The returned fragment should - contain a format string to hold the appropriate column. - """ - pass - - def compiler(self, compiler_name): - """ - Returns the SQLCompiler class corresponding to the given name, - in the namespace corresponding to the `compiler_module` attribute - on this backend. - """ - if self._cache is None: - self._cache = import_module(self.compiler_module) - return getattr(self._cache, compiler_name) - - def quote_name(self, name): - """ - Returns a quoted version of the given table, index or column name. Does - not quote the given name if it's already been quoted. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a quote_name() method') - - def random_function_sql(self): - """ - Returns an SQL expression that returns a random value. - """ - return 'RANDOM()' - - def regex_lookup(self, lookup_type): - """ - Returns the string to use in a query when performing regular expression - lookups (using "regex" or "iregex"). The resulting string should - contain a '%s' placeholder for the column being searched against. - - If the feature is not supported (or part of it is not supported), a - NotImplementedError exception can be raised. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations may require a regex_lookup() method') - - def savepoint_create_sql(self, sid): - """ - Returns the SQL for starting a new savepoint. Only required if the - "uses_savepoints" feature is True. The "sid" parameter is a string - for the savepoint id. - """ - return "SAVEPOINT %s" % self.quote_name(sid) - - def savepoint_commit_sql(self, sid): - """ - Returns the SQL for committing the given savepoint. - """ - return "RELEASE SAVEPOINT %s" % self.quote_name(sid) - - def savepoint_rollback_sql(self, sid): - """ - Returns the SQL for rolling back the given savepoint. - """ - return "ROLLBACK TO SAVEPOINT %s" % self.quote_name(sid) - - def set_time_zone_sql(self): - """ - Returns the SQL that will set the connection's time zone. - - Returns '' if the backend doesn't support time zones. - """ - return '' - - def sql_flush(self, style, tables, sequences, allow_cascade=False): - """ - Returns a list of SQL statements required to remove all data from - the given database tables (without actually removing the tables - themselves). - - The returned value also includes SQL statements required to reset DB - sequences passed in :param sequences:. - - The `style` argument is a Style object as returned by either - color_style() or no_style() in django.core.management.color. - - The `allow_cascade` argument determines whether truncation may cascade - to tables with foreign keys pointing the tables being truncated. - PostgreSQL requires a cascade even if these tables are empty. - """ - raise NotImplementedError('subclasses of BaseDatabaseOperations must provide a sql_flush() method') - - def sequence_reset_by_name_sql(self, style, sequences): - """ - Returns a list of the SQL statements required to reset sequences - passed in :param sequences:. - - The `style` argument is a Style object as returned by either - color_style() or no_style() in django.core.management.color. - """ - return [] - - def sequence_reset_sql(self, style, model_list): - """ - Returns a list of the SQL statements required to reset sequences for - the given models. - - The `style` argument is a Style object as returned by either - color_style() or no_style() in django.core.management.color. - """ - return [] # No sequence reset required by default. - - def start_transaction_sql(self): - """ - Returns the SQL statement required to start a transaction. - """ - return "BEGIN;" - - def end_transaction_sql(self, success=True): - """ - Returns the SQL statement required to end a transaction. - """ - if not success: - return "ROLLBACK;" - return "COMMIT;" - - def tablespace_sql(self, tablespace, inline=False): - """ - Returns the SQL that will be used in a query to define the tablespace. - - Returns '' if the backend doesn't support tablespaces. - - If inline is True, the SQL is appended to a row; otherwise it's appended - to the entire CREATE TABLE or CREATE INDEX statement. - """ - return '' - - def prep_for_like_query(self, x): - """Prepares a value for use in a LIKE query.""" - from django.utils.encoding import force_text - return force_text(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_") - - # Same as prep_for_like_query(), but called for "iexact" matches, which - # need not necessarily be implemented using "LIKE" in the backend. - prep_for_iexact_query = prep_for_like_query - - def validate_autopk_value(self, value): - """ - Certain backends do not accept some values for "serial" fields - (for example zero in MySQL). This method will raise a ValueError - if the value is invalid, otherwise returns validated value. - """ - return value - - def value_to_db_date(self, value): - """ - Transform a date value to an object compatible with what is expected - by the backend driver for date columns. - """ - if value is None: - return None - return six.text_type(value) - - def value_to_db_datetime(self, value): - """ - Transform a datetime value to an object compatible with what is expected - by the backend driver for datetime columns. - """ - if value is None: - return None - return six.text_type(value) - - def value_to_db_time(self, value): - """ - Transform a time value to an object compatible with what is expected - by the backend driver for time columns. - """ - if value is None: - return None - if timezone.is_aware(value): - raise ValueError("Django does not support timezone-aware times.") - return six.text_type(value) - - def value_to_db_decimal(self, value, max_digits, decimal_places): - """ - Transform a decimal.Decimal value to an object compatible with what is - expected by the backend driver for decimal (numeric) columns. - """ - return utils.format_number(value, max_digits, decimal_places) - - def year_lookup_bounds_for_date_field(self, value): - """ - Returns a two-elements list with the lower and upper bound to be used - with a BETWEEN operator to query a DateField value using a year - lookup. - - `value` is an int, containing the looked-up year. - """ - first = datetime.date(value, 1, 1) - second = datetime.date(value, 12, 31) - return [first, second] - - def year_lookup_bounds_for_datetime_field(self, value): - """ - Returns a two-elements list with the lower and upper bound to be used - with a BETWEEN operator to query a DateTimeField value using a year - lookup. - - `value` is an int, containing the looked-up year. - """ - first = datetime.datetime(value, 1, 1) - second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999) - if settings.USE_TZ: - tz = timezone.get_current_timezone() - first = timezone.make_aware(first, tz) - second = timezone.make_aware(second, tz) - return [first, second] - - def get_db_converters(self, expression): - """Get a list of functions needed to convert field data. - - Some field types on some backends do not provide data in the correct - format, this is the hook for coverter functions. - """ - return [] - - def convert_durationfield_value(self, value, expression, context): - if value is not None: - value = str(decimal.Decimal(value) / decimal.Decimal(1000000)) - value = parse_duration(value) - return value - - def check_aggregate_support(self, aggregate_func): - """Check that the backend supports the provided aggregate - - This is used on specific backends to rule out known aggregates - that are known to have faulty implementations. If the named - aggregate function has a known problem, the backend should - raise NotImplementedError. - """ - pass - - def combine_expression(self, connector, sub_expressions): - """Combine a list of subexpressions into a single expression, using - the provided connecting operator. This is required because operators - can vary between backends (e.g., Oracle with %% and &) and between - subexpression types (e.g., date expressions) - """ - conn = ' %s ' % connector - return conn.join(sub_expressions) - - def combine_duration_expression(self, connector, sub_expressions): - return self.combine_expression(connector, sub_expressions) - - def modify_insert_params(self, placeholders, params): - """Allow modification of insert parameters. Needed for Oracle Spatial - backend due to #10888. - """ - return params - - def integer_field_range(self, internal_type): - """ - Given an integer field internal type (e.g. 'PositiveIntegerField'), - returns a tuple of the (min_value, max_value) form representing the - range of the column type bound to the field. - """ - return self.integer_field_ranges[internal_type] - - -class BaseDatabaseIntrospection(object): - """ - This class encapsulates all backend-specific introspection utilities - """ - data_types_reverse = {} - - def __init__(self, connection): - self.connection = connection - - def get_field_type(self, data_type, description): - """Hook for a database backend to use the cursor description to - match a Django field type to a database column. - - For Oracle, the column data_type on its own is insufficient to - distinguish between a FloatField and IntegerField, for example.""" - return self.data_types_reverse[data_type] - - def table_name_converter(self, name): - """Apply a conversion to the name for the purposes of comparison. - - The default table name converter is for case sensitive comparison. - """ - return name - - def column_name_converter(self, name): - """ - Apply a conversion to the column name for the purposes of comparison. - - Uses table_name_converter() by default. - """ - return self.table_name_converter(name) - - def table_names(self, cursor=None, include_views=False): - """ - Returns a list of names of all tables that exist in the database. - The returned table list is sorted by Python's default sorting. We - do NOT use database's ORDER BY here to avoid subtle differences - in sorting order between databases. - """ - def get_names(cursor): - return sorted(ti.name for ti in self.get_table_list(cursor) - if include_views or ti.type == 't') - if cursor is None: - with self.connection.cursor() as cursor: - return get_names(cursor) - return get_names(cursor) - - def get_table_list(self, cursor): - """ - Returns an unsorted list of TableInfo named tuples of all tables and - views that exist in the database. - """ - raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_table_list() method') - - def django_table_names(self, only_existing=False, include_views=True): - """ - Returns a list of all table names that have associated Django models and - are in INSTALLED_APPS. - - If only_existing is True, the resulting list will only include the tables - that actually exist in the database. - """ - from django.apps import apps - from django.db import router - tables = set() - for app_config in apps.get_app_configs(): - for model in router.get_migratable_models(app_config, self.connection.alias): - if not model._meta.managed: - continue - tables.add(model._meta.db_table) - tables.update(f.m2m_db_table() for f in model._meta.local_many_to_many) - tables = list(tables) - if only_existing: - existing_tables = self.table_names(include_views=include_views) - tables = [ - t - for t in tables - if self.table_name_converter(t) in existing_tables - ] - return tables - - def installed_models(self, tables): - "Returns a set of all models represented by the provided list of table names." - from django.apps import apps - from django.db import router - all_models = [] - for app_config in apps.get_app_configs(): - all_models.extend(router.get_migratable_models(app_config, self.connection.alias)) - tables = list(map(self.table_name_converter, tables)) - return { - m for m in all_models - if self.table_name_converter(m._meta.db_table) in tables - } - - def sequence_list(self): - "Returns a list of information about all DB sequences for all models in all apps." - from django.apps import apps - from django.db import models, router - - sequence_list = [] - - for app_config in apps.get_app_configs(): - for model in router.get_migratable_models(app_config, self.connection.alias): - if not model._meta.managed: - continue - if model._meta.swapped: - continue - for f in model._meta.local_fields: - if isinstance(f, models.AutoField): - sequence_list.append({'table': model._meta.db_table, 'column': f.column}) - break # Only one AutoField is allowed per model, so don't bother continuing. - - for f in model._meta.local_many_to_many: - # If this is an m2m using an intermediate table, - # we don't need to reset the sequence. - if f.rel.through is None: - sequence_list.append({'table': f.m2m_db_table(), 'column': None}) - - return sequence_list - - def get_key_columns(self, cursor, table_name): - """ - Backends can override this to return a list of (column_name, referenced_table_name, - referenced_column_name) for all key columns in given table. - """ - raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_key_columns() method') - - def get_primary_key_column(self, cursor, table_name): - """ - Returns the name of the primary key column for the given table. - """ - for column in six.iteritems(self.get_indexes(cursor, table_name)): - if column[1]['primary_key']: - return column[0] - return None - - def get_indexes(self, cursor, table_name): - """ - Returns a dictionary of indexed 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} - - Only single-column indexes are introspected. - """ - raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_indexes() method') - - def get_constraints(self, cursor, table_name): - """ - Retrieves any constraints or keys (unique, pk, fk, check, index) - across one or more columns. - - Returns a dict mapping constraint names to their attributes, - where attributes is a dict with keys: - * columns: List of columns this covers - * primary_key: True if primary key, False otherwise - * unique: True if this is a unique constraint, False otherwise - * foreign_key: (table, column) of target, or None - * check: True if check constraint, False otherwise - * index: True if index, False otherwise. - - Some backends may return special constraint names that don't exist - if they don't name constraints of a certain type (e.g. SQLite) - """ - raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_constraints() method') - - -class BaseDatabaseClient(object): - """ - This class encapsulates all backend-specific methods for opening a - client shell. - """ - # This should be a string representing the name of the executable - # (e.g., "psql"). Subclasses must override this. - executable_name = None - - def __init__(self, connection): - # connection is an instance of BaseDatabaseWrapper. - self.connection = connection - - def runshell(self): - raise NotImplementedError('subclasses of BaseDatabaseClient must provide a runshell() method') - - -class BaseDatabaseValidation(object): - """ - This class encapsulates all backend-specific model validation. - """ - def __init__(self, connection): - self.connection = connection - - def validate_field(self, errors, opts, f): - """ - By default, there is no backend-specific validation. - - This method has been deprecated by the new checks framework. New - backends should implement check_field instead. - """ - # This is deliberately commented out. It exists as a marker to - # remind us to remove this method, and the check_field() shim, - # when the time comes. - # warnings.warn('"validate_field" has been deprecated", RemovedInDjango19Warning) - pass - - def check_field(self, field, **kwargs): - class ErrorList(list): - """A dummy list class that emulates API used by the older - validate_field() method. When validate_field() is fully - deprecated, this dummy can be removed too. - """ - def add(self, opts, error_message): - self.append(checks.Error(error_message, hint=None, obj=field)) - - errors = ErrorList() - # Some tests create fields in isolation -- the fields are not attached - # to any model, so they have no `model` attribute. - opts = field.model._meta if hasattr(field, 'model') else None - self.validate_field(errors, field, opts) - return list(errors) diff --git a/django/db/backends/base/__init__.py b/django/db/backends/base/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/django/db/backends/base/base.py b/django/db/backends/base/base.py new file mode 100644 index 0000000000..fb9260d8ca --- /dev/null +++ b/django/db/backends/base/base.py @@ -0,0 +1,507 @@ +from collections import deque +from contextlib import contextmanager +import time +import warnings + +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS +from django.db.backends.signals import connection_created +from django.db.backends import utils +from django.db.transaction import TransactionManagementError +from django.db.utils import DatabaseError, DatabaseErrorWrapper +from django.utils.functional import cached_property +try: + from django.utils.six.moves import _thread as thread +except ImportError: + from django.utils.six.moves import _dummy_thread as thread + + +NO_DB_ALIAS = '__no_db__' + + +class BaseDatabaseWrapper(object): + """ + Represents a database connection. + """ + # Mapping of Field objects to their column types. + data_types = {} + # Mapping of Field objects to their SQL suffix such as AUTOINCREMENT. + data_types_suffix = {} + # Mapping of Field objects to their SQL for CHECK constraints. + data_type_check_constraints = {} + ops = None + vendor = 'unknown' + SchemaEditorClass = None + + queries_limit = 9000 + + def __init__(self, settings_dict, alias=DEFAULT_DB_ALIAS, + allow_thread_sharing=False): + # Connection related attributes. + # The underlying database connection. + self.connection = None + # `settings_dict` should be a dictionary containing keys such as + # NAME, USER, etc. It's called `settings_dict` instead of `settings` + # to disambiguate it from Django settings modules. + self.settings_dict = settings_dict + self.alias = alias + # Query logging in debug mode or when explicitly enabled. + self.queries_log = deque(maxlen=self.queries_limit) + self.force_debug_cursor = False + + # Transaction related attributes. + # Tracks if the connection is in autocommit mode. Per PEP 249, by + # default, it isn't. + self.autocommit = False + # Tracks if the connection is in a transaction managed by 'atomic'. + self.in_atomic_block = False + # Increment to generate unique savepoint ids. + self.savepoint_state = 0 + # List of savepoints created by 'atomic'. + self.savepoint_ids = [] + # Tracks if the outermost 'atomic' block should commit on exit, + # ie. if autocommit was active on entry. + self.commit_on_exit = True + # Tracks if the transaction should be rolled back to the next + # available savepoint because of an exception in an inner block. + self.needs_rollback = False + + # Connection termination related attributes. + self.close_at = None + self.closed_in_transaction = False + self.errors_occurred = False + + # Thread-safety related attributes. + self.allow_thread_sharing = allow_thread_sharing + self._thread_ident = thread.get_ident() + + @property + def queries_logged(self): + return self.force_debug_cursor or settings.DEBUG + + @property + def queries(self): + if len(self.queries_log) == self.queries_log.maxlen: + warnings.warn( + "Limit for query logging exceeded, only the last {} queries " + "will be returned.".format(self.queries_log.maxlen)) + return list(self.queries_log) + + ##### Backend-specific methods for creating connections and cursors ##### + + def get_connection_params(self): + """Returns a dict of parameters suitable for get_new_connection.""" + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a get_connection_params() method') + + def get_new_connection(self, conn_params): + """Opens a connection to the database.""" + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a get_new_connection() method') + + def init_connection_state(self): + """Initializes the database connection settings.""" + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require an init_connection_state() method') + + def create_cursor(self): + """Creates a cursor. Assumes that a connection is established.""" + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a create_cursor() method') + + ##### Backend-specific methods for creating connections ##### + + def connect(self): + """Connects to the database. Assumes that the connection is closed.""" + # In case the previous connection was closed while in an atomic block + self.in_atomic_block = False + self.savepoint_ids = [] + self.needs_rollback = False + # Reset parameters defining when to close the connection + max_age = self.settings_dict['CONN_MAX_AGE'] + self.close_at = None if max_age is None else time.time() + max_age + self.closed_in_transaction = False + self.errors_occurred = False + # Establish the connection + conn_params = self.get_connection_params() + self.connection = self.get_new_connection(conn_params) + self.set_autocommit(self.settings_dict['AUTOCOMMIT']) + self.init_connection_state() + connection_created.send(sender=self.__class__, connection=self) + + def ensure_connection(self): + """ + Guarantees that a connection to the database is established. + """ + if self.connection is None: + with self.wrap_database_errors: + self.connect() + + ##### Backend-specific wrappers for PEP-249 connection methods ##### + + def _cursor(self): + self.ensure_connection() + with self.wrap_database_errors: + return self.create_cursor() + + def _commit(self): + if self.connection is not None: + with self.wrap_database_errors: + return self.connection.commit() + + def _rollback(self): + if self.connection is not None: + with self.wrap_database_errors: + return self.connection.rollback() + + def _close(self): + if self.connection is not None: + with self.wrap_database_errors: + return self.connection.close() + + ##### Generic wrappers for PEP-249 connection methods ##### + + def cursor(self): + """ + Creates a cursor, opening a connection if necessary. + """ + self.validate_thread_sharing() + if self.queries_logged: + cursor = self.make_debug_cursor(self._cursor()) + else: + cursor = self.make_cursor(self._cursor()) + return cursor + + def commit(self): + """ + Commits a transaction and resets the dirty flag. + """ + self.validate_thread_sharing() + self.validate_no_atomic_block() + self._commit() + # A successful commit means that the database connection works. + self.errors_occurred = False + + def rollback(self): + """ + Rolls back a transaction and resets the dirty flag. + """ + self.validate_thread_sharing() + self.validate_no_atomic_block() + self._rollback() + # A successful rollback means that the database connection works. + self.errors_occurred = False + + def close(self): + """ + Closes the connection to the database. + """ + self.validate_thread_sharing() + # Don't call validate_no_atomic_block() to avoid making it difficult + # to get rid of a connection in an invalid state. The next connect() + # will reset the transaction state anyway. + if self.closed_in_transaction or self.connection is None: + return + try: + self._close() + finally: + if self.in_atomic_block: + self.closed_in_transaction = True + self.needs_rollback = True + else: + self.connection = None + + ##### Backend-specific savepoint management methods ##### + + def _savepoint(self, sid): + with self.cursor() as cursor: + cursor.execute(self.ops.savepoint_create_sql(sid)) + + def _savepoint_rollback(self, sid): + with self.cursor() as cursor: + cursor.execute(self.ops.savepoint_rollback_sql(sid)) + + def _savepoint_commit(self, sid): + with self.cursor() as cursor: + cursor.execute(self.ops.savepoint_commit_sql(sid)) + + def _savepoint_allowed(self): + # Savepoints cannot be created outside a transaction + return self.features.uses_savepoints and not self.get_autocommit() + + ##### Generic savepoint management methods ##### + + def savepoint(self): + """ + Creates a savepoint inside the current transaction. Returns an + identifier for the savepoint that will be used for the subsequent + rollback or commit. Does nothing if savepoints are not supported. + """ + if not self._savepoint_allowed(): + return + + thread_ident = thread.get_ident() + tid = str(thread_ident).replace('-', '') + + self.savepoint_state += 1 + sid = "s%s_x%d" % (tid, self.savepoint_state) + + self.validate_thread_sharing() + self._savepoint(sid) + + return sid + + def savepoint_rollback(self, sid): + """ + Rolls back to a savepoint. Does nothing if savepoints are not supported. + """ + if not self._savepoint_allowed(): + return + + self.validate_thread_sharing() + self._savepoint_rollback(sid) + + def savepoint_commit(self, sid): + """ + Releases a savepoint. Does nothing if savepoints are not supported. + """ + if not self._savepoint_allowed(): + return + + self.validate_thread_sharing() + self._savepoint_commit(sid) + + def clean_savepoints(self): + """ + Resets the counter used to generate unique savepoint ids in this thread. + """ + self.savepoint_state = 0 + + ##### Backend-specific transaction management methods ##### + + def _set_autocommit(self, autocommit): + """ + Backend-specific implementation to enable or disable autocommit. + """ + raise NotImplementedError('subclasses of BaseDatabaseWrapper may require a _set_autocommit() method') + + ##### Generic transaction management methods ##### + + def get_autocommit(self): + """ + Check the autocommit state. + """ + self.ensure_connection() + return self.autocommit + + def set_autocommit(self, autocommit): + """ + Enable or disable autocommit. + """ + self.validate_no_atomic_block() + self.ensure_connection() + self._set_autocommit(autocommit) + self.autocommit = autocommit + + def get_rollback(self): + """ + Get the "needs rollback" flag -- for *advanced use* only. + """ + if not self.in_atomic_block: + raise TransactionManagementError( + "The rollback flag doesn't work outside of an 'atomic' block.") + return self.needs_rollback + + def set_rollback(self, rollback): + """ + Set or unset the "needs rollback" flag -- for *advanced use* only. + """ + if not self.in_atomic_block: + raise TransactionManagementError( + "The rollback flag doesn't work outside of an 'atomic' block.") + self.needs_rollback = rollback + + def validate_no_atomic_block(self): + """ + Raise an error if an atomic block is active. + """ + if self.in_atomic_block: + raise TransactionManagementError( + "This is forbidden when an 'atomic' block is active.") + + def validate_no_broken_transaction(self): + if self.needs_rollback: + raise TransactionManagementError( + "An error occurred in the current transaction. You can't " + "execute queries until the end of the 'atomic' block.") + + ##### Foreign key constraints checks handling ##### + + @contextmanager + def constraint_checks_disabled(self): + """ + Context manager that disables foreign key constraint checking. + """ + disabled = self.disable_constraint_checking() + try: + yield + finally: + if disabled: + self.enable_constraint_checking() + + def disable_constraint_checking(self): + """ + Backends can implement as needed to temporarily disable foreign key + constraint checking. Should return True if the constraints were + disabled and will need to be reenabled. + """ + return False + + def enable_constraint_checking(self): + """ + Backends can implement as needed to re-enable foreign key constraint + checking. + """ + pass + + def check_constraints(self, table_names=None): + """ + Backends can override this method if they can apply constraint + checking (e.g. via "SET CONSTRAINTS ALL IMMEDIATE"). Should raise an + IntegrityError if any invalid foreign key references are encountered. + """ + pass + + ##### Connection termination handling ##### + + def is_usable(self): + """ + Tests if the database connection is usable. + + This function may assume that self.connection is not None. + + Actual implementations should take care not to raise exceptions + as that may prevent Django from recycling unusable connections. + """ + raise NotImplementedError( + "subclasses of BaseDatabaseWrapper may require an is_usable() method") + + def close_if_unusable_or_obsolete(self): + """ + Closes the current connection if unrecoverable errors have occurred, + or if it outlived its maximum age. + """ + if self.connection is not None: + # If the application didn't restore the original autocommit setting, + # don't take chances, drop the connection. + if self.get_autocommit() != self.settings_dict['AUTOCOMMIT']: + self.close() + return + + # If an exception other than DataError or IntegrityError occurred + # since the last commit / rollback, check if the connection works. + if self.errors_occurred: + if self.is_usable(): + self.errors_occurred = False + else: + self.close() + return + + if self.close_at is not None and time.time() >= self.close_at: + self.close() + return + + ##### Thread safety handling ##### + + def validate_thread_sharing(self): + """ + Validates that the connection isn't accessed by another thread than the + one which originally created it, unless the connection was explicitly + authorized to be shared between threads (via the `allow_thread_sharing` + property). Raises an exception if the validation fails. + """ + if not (self.allow_thread_sharing + or self._thread_ident == thread.get_ident()): + raise DatabaseError("DatabaseWrapper objects created in a " + "thread can only be used in that same thread. The object " + "with alias '%s' was created in thread id %s and this is " + "thread id %s." + % (self.alias, self._thread_ident, thread.get_ident())) + + ##### Miscellaneous ##### + + def prepare_database(self): + """ + Hook to do any database check or preparation, generally called before + migrating a project or an app. + """ + pass + + @cached_property + def wrap_database_errors(self): + """ + Context manager and decorator that re-throws backend-specific database + exceptions using Django's common wrappers. + """ + return DatabaseErrorWrapper(self) + + def make_debug_cursor(self, cursor): + """ + Creates a cursor that logs all queries in self.queries_log. + """ + return utils.CursorDebugWrapper(cursor, self) + + def make_cursor(self, cursor): + """ + Creates a cursor without debug logging. + """ + return utils.CursorWrapper(cursor, self) + + @contextmanager + def temporary_connection(self): + """ + Context manager that ensures that a connection is established, and + if it opened one, closes it to avoid leaving a dangling connection. + This is useful for operations outside of the request-response cycle. + + Provides a cursor: with self.temporary_connection() as cursor: ... + """ + must_close = self.connection is None + cursor = self.cursor() + try: + yield cursor + finally: + cursor.close() + if must_close: + self.close() + + @cached_property + def _nodb_connection(self): + """ + Alternative connection to be used when there is no need to access + the main database, specifically for test db creation/deletion. + This also prevents the production database from being exposed to + potential child threads while (or after) the test database is destroyed. + Refs #10868, #17786, #16969. + """ + settings_dict = self.settings_dict.copy() + settings_dict['NAME'] = None + nodb_connection = self.__class__( + settings_dict, + alias=NO_DB_ALIAS, + allow_thread_sharing=False) + return nodb_connection + + def _start_transaction_under_autocommit(self): + """ + Only required when autocommits_when_autocommit_is_off = True. + """ + raise NotImplementedError( + 'subclasses of BaseDatabaseWrapper may require a ' + '_start_transaction_under_autocommit() method' + ) + + def schema_editor(self, *args, **kwargs): + """ + Returns a new instance of this backend's SchemaEditor. + """ + if self.SchemaEditorClass is None: + raise NotImplementedError( + 'The SchemaEditorClass attribute of this database wrapper is still None') + return self.SchemaEditorClass(self, *args, **kwargs) diff --git a/django/db/backends/base/client.py b/django/db/backends/base/client.py new file mode 100644 index 0000000000..aced8e7ebd --- /dev/null +++ b/django/db/backends/base/client.py @@ -0,0 +1,15 @@ +class BaseDatabaseClient(object): + """ + This class encapsulates all backend-specific methods for opening a + client shell. + """ + # This should be a string representing the name of the executable + # (e.g., "psql"). Subclasses must override this. + executable_name = None + + def __init__(self, connection): + # connection is an instance of BaseDatabaseWrapper. + self.connection = connection + + def runshell(self): + raise NotImplementedError('subclasses of BaseDatabaseClient must provide a runshell() method') diff --git a/django/db/backends/creation.py b/django/db/backends/base/creation.py similarity index 99% rename from django/db/backends/creation.py rename to django/db/backends/base/creation.py index 42a58c03e8..5044fcfb6c 100644 --- a/django/db/backends/creation.py +++ b/django/db/backends/base/creation.py @@ -3,16 +3,16 @@ import sys import time import warnings +from django.apps import apps from django.conf import settings +from django.core import serializers +from django.db import router +from django.db.backends.utils import truncate_name from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import force_bytes -from django.utils.six.moves import input from django.utils.six import StringIO -from django.db import router -from django.apps import apps -from django.core import serializers +from django.utils.six.moves import input -from .utils import truncate_name # The prefix to put on the default database name when creating # the test database. diff --git a/django/db/backends/base/features.py b/django/db/backends/base/features.py new file mode 100644 index 0000000000..0b326bb079 --- /dev/null +++ b/django/db/backends/base/features.py @@ -0,0 +1,250 @@ +from django.db.utils import ProgrammingError +from django.utils.functional import cached_property + + +class BaseDatabaseFeatures(object): + gis_enabled = False + allows_group_by_pk = False + # True if django.db.backends.utils.typecast_timestamp is used on values + # returned from dates() calls. + needs_datetime_string_cast = True + empty_fetchmany_value = [] + update_can_self_select = True + + # Does the backend distinguish between '' and None? + interprets_empty_strings_as_nulls = False + + # Does the backend allow inserting duplicate NULL rows in a nullable + # unique field? All core backends implement this correctly, but other + # databases such as SQL Server do not. + supports_nullable_unique_constraints = True + + # Does the backend allow inserting duplicate rows when a unique_together + # constraint exists and some fields are nullable but not all of them? + supports_partially_nullable_unique_constraints = True + + can_use_chunked_reads = True + can_return_id_from_insert = False + has_bulk_insert = False + uses_savepoints = False + can_release_savepoints = False + can_combine_inserts_with_and_without_auto_increment_pk = False + + # If True, don't use integer foreign keys referring to, e.g., positive + # integer primary keys. + related_fields_match_type = False + allow_sliced_subqueries = True + has_select_for_update = False + has_select_for_update_nowait = False + + supports_select_related = True + + # Does the default test database allow multiple connections? + # Usually an indication that the test database is in-memory + test_db_allows_multiple_connections = True + + # Can an object be saved without an explicit primary key? + supports_unspecified_pk = False + + # Can a fixture contain forward references? i.e., are + # FK constraints checked at the end of transaction, or + # at the end of each save operation? + supports_forward_references = True + + # Does the backend truncate names properly when they are too long? + truncates_names = False + + # Is there a REAL datatype in addition to floats/doubles? + has_real_datatype = False + supports_subqueries_in_group_by = True + supports_bitwise_or = True + + # Is there a true datatype for timedeltas? + has_native_duration_field = False + + # Does the database driver support timedeltas as arguments? + # This is only relevant when there is a native duration field. + # Specifically, there is a bug with cx_Oracle: + # https://bitbucket.org/anthony_tuininga/cx_oracle/issue/7/ + driver_supports_timedelta_args = False + + # Do time/datetime fields have microsecond precision? + supports_microsecond_precision = True + + # Does the __regex lookup support backreferencing and grouping? + supports_regex_backreferencing = True + + # Can date/datetime lookups be performed using a string? + supports_date_lookup_using_string = True + + # Can datetimes with timezones be used? + supports_timezones = True + + # Does the database have a copy of the zoneinfo database? + has_zoneinfo_database = True + + # When performing a GROUP BY, is an ORDER BY NULL required + # to remove any ordering? + requires_explicit_null_ordering_when_grouping = False + + # Does the backend order NULL values as largest or smallest? + nulls_order_largest = False + + # Is there a 1000 item limit on query parameters? + supports_1000_query_parameters = True + + # Can an object have an autoincrement primary key of 0? MySQL says No. + allows_auto_pk_0 = True + + # Do we need to NULL a ForeignKey out, or can the constraint check be + # deferred + can_defer_constraint_checks = False + + # date_interval_sql can properly handle mixed Date/DateTime fields and timedeltas + supports_mixed_date_datetime_comparisons = True + + # Does the backend support tablespaces? Default to False because it isn't + # in the SQL standard. + supports_tablespaces = False + + # Does the backend reset sequences between tests? + supports_sequence_reset = True + + # Can the backend determine reliably the length of a CharField? + can_introspect_max_length = True + + # Can the backend determine reliably if a field is nullable? + # Note that this is separate from interprets_empty_strings_as_nulls, + # although the latter feature, when true, interferes with correct + # setting (and introspection) of CharFields' nullability. + # This is True for all core backends. + can_introspect_null = True + + # Confirm support for introspected foreign keys + # Every database can do this reliably, except MySQL, + # which can't do it for MyISAM tables + can_introspect_foreign_keys = True + + # Can the backend introspect an AutoField, instead of an IntegerField? + can_introspect_autofield = False + + # Can the backend introspect a BigIntegerField, instead of an IntegerField? + can_introspect_big_integer_field = True + + # Can the backend introspect an BinaryField, instead of an TextField? + can_introspect_binary_field = True + + # Can the backend introspect an DecimalField, instead of an FloatField? + can_introspect_decimal_field = True + + # Can the backend introspect an IPAddressField, instead of an CharField? + can_introspect_ip_address_field = False + + # Can the backend introspect a PositiveIntegerField, instead of an IntegerField? + can_introspect_positive_integer_field = False + + # Can the backend introspect a SmallIntegerField, instead of an IntegerField? + can_introspect_small_integer_field = False + + # Can the backend introspect a TimeField, instead of a DateTimeField? + can_introspect_time_field = True + + # Support for the DISTINCT ON clause + can_distinct_on_fields = False + + # Does the backend decide to commit before SAVEPOINT statements + # when autocommit is disabled? http://bugs.python.org/issue8145#msg109965 + autocommits_when_autocommit_is_off = False + + # Does the backend prevent running SQL queries in broken transactions? + atomic_transactions = True + + # Can we roll back DDL in a transaction? + can_rollback_ddl = False + + # Can we issue more than one ALTER COLUMN clause in an ALTER TABLE? + supports_combined_alters = False + + # Does it support foreign keys? + supports_foreign_keys = True + + # Does it support CHECK constraints? + supports_column_check_constraints = True + + # Does the backend support 'pyformat' style ("... %(name)s ...", {'name': value}) + # parameter passing? Note this can be provided by the backend even if not + # supported by the Python driver + supports_paramstyle_pyformat = True + + # Does the backend require literal defaults, rather than parameterized ones? + requires_literal_defaults = False + + # Does the backend require a connection reset after each material schema change? + connection_persists_old_columns = False + + # What kind of error does the backend throw when accessing closed cursor? + closed_cursor_error_class = ProgrammingError + + # Does 'a' LIKE 'A' match? + has_case_insensitive_like = True + + # Does the backend require the sqlparse library for splitting multi-line + # statements before executing them? + requires_sqlparse_for_splitting = True + + # Suffix for backends that don't support "SELECT xxx;" queries. + bare_select_suffix = '' + + # If NULL is implied on columns without needing to be explicitly specified + implied_column_null = False + + uppercases_column_names = False + + # Does the backend support "select for update" queries with limit (and offset)? + supports_select_for_update_with_limit = True + + def __init__(self, connection): + self.connection = connection + + @cached_property + def supports_transactions(self): + """Confirm support for transactions.""" + with self.connection.cursor() as cursor: + cursor.execute('CREATE TABLE ROLLBACK_TEST (X INT)') + self.connection.set_autocommit(False) + cursor.execute('INSERT INTO ROLLBACK_TEST (X) VALUES (8)') + self.connection.rollback() + self.connection.set_autocommit(True) + cursor.execute('SELECT COUNT(X) FROM ROLLBACK_TEST') + count, = cursor.fetchone() + cursor.execute('DROP TABLE ROLLBACK_TEST') + return count == 0 + + @cached_property + def supports_stddev(self): + """Confirm support for STDDEV and related stats functions.""" + class StdDevPop(object): + sql_function = 'STDDEV_POP' + + try: + self.connection.ops.check_aggregate_support(StdDevPop()) + return True + except NotImplementedError: + return False + + def introspected_boolean_field_type(self, field=None, created_separately=False): + """ + What is the type returned when the backend introspects a BooleanField? + The optional arguments may be used to give further details of the field to be + introspected; in particular, they are provided by Django's test suite: + field -- the field definition + created_separately -- True if the field was added via a SchemaEditor's AddField, + False if the field was created with the model + + Note that return value from this function is compared by tests against actual + introspection results; it should provide expectations, not run an introspection + itself. + """ + if self.can_introspect_null and field and field.null: + return 'NullBooleanField' + return 'BooleanField' diff --git a/django/db/backends/base/introspection.py b/django/db/backends/base/introspection.py new file mode 100644 index 0000000000..d20bfa1df0 --- /dev/null +++ b/django/db/backends/base/introspection.py @@ -0,0 +1,178 @@ +from collections import namedtuple + +from django.utils import six + + +# Structure returned by DatabaseIntrospection.get_table_list() +TableInfo = namedtuple('TableInfo', ['name', 'type']) + +# Structure returned by the DB-API cursor.description interface (PEP 249) +FieldInfo = namedtuple('FieldInfo', + 'name type_code display_size internal_size precision scale null_ok') + + +class BaseDatabaseIntrospection(object): + """ + This class encapsulates all backend-specific introspection utilities + """ + data_types_reverse = {} + + def __init__(self, connection): + self.connection = connection + + def get_field_type(self, data_type, description): + """Hook for a database backend to use the cursor description to + match a Django field type to a database column. + + For Oracle, the column data_type on its own is insufficient to + distinguish between a FloatField and IntegerField, for example.""" + return self.data_types_reverse[data_type] + + def table_name_converter(self, name): + """Apply a conversion to the name for the purposes of comparison. + + The default table name converter is for case sensitive comparison. + """ + return name + + def column_name_converter(self, name): + """ + Apply a conversion to the column name for the purposes of comparison. + + Uses table_name_converter() by default. + """ + return self.table_name_converter(name) + + def table_names(self, cursor=None, include_views=False): + """ + Returns a list of names of all tables that exist in the database. + The returned table list is sorted by Python's default sorting. We + do NOT use database's ORDER BY here to avoid subtle differences + in sorting order between databases. + """ + def get_names(cursor): + return sorted(ti.name for ti in self.get_table_list(cursor) + if include_views or ti.type == 't') + if cursor is None: + with self.connection.cursor() as cursor: + return get_names(cursor) + return get_names(cursor) + + def get_table_list(self, cursor): + """ + Returns an unsorted list of TableInfo named tuples of all tables and + views that exist in the database. + """ + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_table_list() method') + + def django_table_names(self, only_existing=False, include_views=True): + """ + Returns a list of all table names that have associated Django models and + are in INSTALLED_APPS. + + If only_existing is True, the resulting list will only include the tables + that actually exist in the database. + """ + from django.apps import apps + from django.db import router + tables = set() + for app_config in apps.get_app_configs(): + for model in router.get_migratable_models(app_config, self.connection.alias): + if not model._meta.managed: + continue + tables.add(model._meta.db_table) + tables.update(f.m2m_db_table() for f in model._meta.local_many_to_many) + tables = list(tables) + if only_existing: + existing_tables = self.table_names(include_views=include_views) + tables = [ + t + for t in tables + if self.table_name_converter(t) in existing_tables + ] + return tables + + def installed_models(self, tables): + "Returns a set of all models represented by the provided list of table names." + from django.apps import apps + from django.db import router + all_models = [] + for app_config in apps.get_app_configs(): + all_models.extend(router.get_migratable_models(app_config, self.connection.alias)) + tables = list(map(self.table_name_converter, tables)) + return { + m for m in all_models + if self.table_name_converter(m._meta.db_table) in tables + } + + def sequence_list(self): + "Returns a list of information about all DB sequences for all models in all apps." + from django.apps import apps + from django.db import models, router + + sequence_list = [] + + for app_config in apps.get_app_configs(): + for model in router.get_migratable_models(app_config, self.connection.alias): + if not model._meta.managed: + continue + if model._meta.swapped: + continue + for f in model._meta.local_fields: + if isinstance(f, models.AutoField): + sequence_list.append({'table': model._meta.db_table, 'column': f.column}) + break # Only one AutoField is allowed per model, so don't bother continuing. + + for f in model._meta.local_many_to_many: + # If this is an m2m using an intermediate table, + # we don't need to reset the sequence. + if f.rel.through is None: + sequence_list.append({'table': f.m2m_db_table(), 'column': None}) + + return sequence_list + + def get_key_columns(self, cursor, table_name): + """ + Backends can override this to return a list of (column_name, referenced_table_name, + referenced_column_name) for all key columns in given table. + """ + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_key_columns() method') + + def get_primary_key_column(self, cursor, table_name): + """ + Returns the name of the primary key column for the given table. + """ + for column in six.iteritems(self.get_indexes(cursor, table_name)): + if column[1]['primary_key']: + return column[0] + return None + + def get_indexes(self, cursor, table_name): + """ + Returns a dictionary of indexed 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} + + Only single-column indexes are introspected. + """ + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_indexes() method') + + def get_constraints(self, cursor, table_name): + """ + Retrieves any constraints or keys (unique, pk, fk, check, index) + across one or more columns. + + Returns a dict mapping constraint names to their attributes, + where attributes is a dict with keys: + * columns: List of columns this covers + * primary_key: True if primary key, False otherwise + * unique: True if this is a unique constraint, False otherwise + * foreign_key: (table, column) of target, or None + * check: True if check constraint, False otherwise + * index: True if index, False otherwise. + + Some backends may return special constraint names that don't exist + if they don't name constraints of a certain type (e.g. SQLite) + """ + raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_constraints() method') diff --git a/django/db/backends/base/operations.py b/django/db/backends/base/operations.py new file mode 100644 index 0000000000..c4e78e719e --- /dev/null +++ b/django/db/backends/base/operations.py @@ -0,0 +1,555 @@ +import datetime +import decimal +from importlib import import_module +import warnings + +from django.conf import settings +from django.db.backends import utils +from django.utils import six, timezone +from django.utils.dateparse import parse_duration +from django.utils.deprecation import RemovedInDjango19Warning +from django.utils.encoding import force_text + + +class BaseDatabaseOperations(object): + """ + This class encapsulates all backend-specific differences, such as the way + a backend performs ordering or calculates the ID of a recently-inserted + row. + """ + compiler_module = "django.db.models.sql.compiler" + + # Integer field safe ranges by `internal_type` as documented + # in docs/ref/models/fields.txt. + integer_field_ranges = { + 'SmallIntegerField': (-32768, 32767), + 'IntegerField': (-2147483648, 2147483647), + 'BigIntegerField': (-9223372036854775808, 9223372036854775807), + 'PositiveSmallIntegerField': (0, 32767), + 'PositiveIntegerField': (0, 2147483647), + } + + def __init__(self, connection): + self.connection = connection + self._cache = None + + def autoinc_sql(self, table, column): + """ + Returns any SQL needed to support auto-incrementing primary keys, or + None if no SQL is necessary. + + This SQL is executed when a table is created. + """ + return None + + def bulk_batch_size(self, fields, objs): + """ + Returns the maximum allowed batch size for the backend. The fields + are the fields going to be inserted in the batch, the objs contains + all the objects to be inserted. + """ + return len(objs) + + def cache_key_culling_sql(self): + """ + Returns an SQL query that retrieves the first cache key greater than the + n smallest. + + This is used by the 'db' cache backend to determine where to start + culling. + """ + return "SELECT cache_key FROM %s ORDER BY cache_key LIMIT 1 OFFSET %%s" + + def unification_cast_sql(self, output_field): + """ + Given a field instance, returns the SQL necessary to cast the result of + a union to that type. Note that the resulting string should contain a + '%s' placeholder for the expression being cast. + """ + return '%s' + + def date_extract_sql(self, lookup_type, field_name): + """ + Given a lookup_type of 'year', 'month' or 'day', returns the SQL that + extracts a value from the given date field field_name. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_extract_sql() method') + + def date_interval_sql(self, sql, connector, timedelta): + """ + Implements the date interval functionality for expressions + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a date_interval_sql() method') + + def date_trunc_sql(self, lookup_type, field_name): + """ + Given a lookup_type of 'year', 'month' or 'day', returns the SQL that + truncates the given date field field_name to a date object with only + the given specificity. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetrunc_sql() method') + + def datetime_cast_sql(self): + """ + Returns the SQL necessary to cast a datetime value so that it will be + retrieved as a Python datetime object instead of a string. + + This SQL should include a '%s' in place of the field's name. + """ + return "%s" + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + """ + Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or + 'second', returns the SQL that extracts a value from the given + datetime field field_name, and a tuple of parameters. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_extract_sql() method') + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + """ + Given a lookup_type of 'year', 'month', 'day', 'hour', 'minute' or + 'second', returns the SQL that truncates the given datetime field + field_name to a datetime object with only the given specificity, and + a tuple of parameters. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a datetime_trunk_sql() method') + + def deferrable_sql(self): + """ + Returns the SQL necessary to make a constraint "initially deferred" + during a CREATE TABLE statement. + """ + return '' + + def distinct_sql(self, fields): + """ + Returns an SQL DISTINCT clause which removes duplicate rows from the + result set. If any fields are given, only the given fields are being + checked for duplicates. + """ + if fields: + raise NotImplementedError('DISTINCT ON fields is not supported by this database backend') + else: + return 'DISTINCT' + + def drop_foreignkey_sql(self): + """ + Returns the SQL command that drops a foreign key. + """ + return "DROP CONSTRAINT" + + def drop_sequence_sql(self, table): + """ + Returns any SQL necessary to drop the sequence for the given table. + Returns None if no SQL is necessary. + """ + return None + + def fetch_returned_insert_id(self, cursor): + """ + Given a cursor object that has just performed an INSERT...RETURNING + statement into a table that has an auto-incrementing ID, returns the + newly created ID. + """ + return cursor.fetchone()[0] + + def field_cast_sql(self, db_type, internal_type): + """ + Given a column type (e.g. 'BLOB', 'VARCHAR'), and an internal type + (e.g. 'GenericIPAddressField'), returns the SQL necessary to cast it + before using it in a WHERE statement. Note that the resulting string + should contain a '%s' placeholder for the column being searched against. + """ + return '%s' + + def force_no_ordering(self): + """ + Returns a list used in the "ORDER BY" clause to force no ordering at + all. Returning an empty list means that nothing will be included in the + ordering. + """ + return [] + + def for_update_sql(self, nowait=False): + """ + Returns the FOR UPDATE SQL clause to lock rows for an update operation. + """ + if nowait: + return 'FOR UPDATE NOWAIT' + else: + return 'FOR UPDATE' + + def fulltext_search_sql(self, field_name): + """ + Returns the SQL WHERE clause to use in order to perform a full-text + search of the given field_name. Note that the resulting string should + contain a '%s' placeholder for the value being searched against. + """ + raise NotImplementedError('Full-text search is not implemented for this database backend') + + def last_executed_query(self, cursor, sql, params): + """ + Returns a string of the query last executed by the given cursor, with + placeholders replaced with actual values. + + `sql` is the raw query containing placeholders, and `params` is the + sequence of parameters. These are used by default, but this method + exists for database backends to provide a better implementation + according to their own quoting schemes. + """ + # Convert params to contain Unicode values. + to_unicode = lambda s: force_text(s, strings_only=True, errors='replace') + if isinstance(params, (list, tuple)): + u_params = tuple(to_unicode(val) for val in params) + elif params is None: + u_params = () + else: + u_params = {to_unicode(k): to_unicode(v) for k, v in params.items()} + + return six.text_type("QUERY = %r - PARAMS = %r") % (sql, u_params) + + def last_insert_id(self, cursor, table_name, pk_name): + """ + Given a cursor object that has just performed an INSERT statement into + a table that has an auto-incrementing ID, returns the newly created ID. + + This method also receives the table name and the name of the primary-key + column. + """ + return cursor.lastrowid + + def lookup_cast(self, lookup_type): + """ + Returns the string to use in a query when performing lookups + ("contains", "like", etc). The resulting string should contain a '%s' + placeholder for the column being searched against. + """ + return "%s" + + def max_in_list_size(self): + """ + Returns the maximum number of items that can be passed in a single 'IN' + list condition, or None if the backend does not impose a limit. + """ + return None + + def max_name_length(self): + """ + Returns the maximum length of table and column names, or None if there + is no limit. + """ + return None + + def no_limit_value(self): + """ + Returns the value to use for the LIMIT when we are wanting "LIMIT + infinity". Returns None if the limit clause can be omitted in this case. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a no_limit_value() method') + + 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 'DEFAULT' + + def prepare_sql_script(self, sql, _allow_fallback=False): + """ + Takes a SQL script that may contain multiple lines and returns a list + of statements to feed to successive cursor.execute() calls. + + Since few databases are able to process raw SQL scripts in a single + cursor.execute() call and PEP 249 doesn't talk about this use case, + the default implementation is conservative. + """ + # Remove _allow_fallback and keep only 'return ...' in Django 1.9. + try: + # This import must stay inside the method because it's optional. + import sqlparse + except ImportError: + if _allow_fallback: + # Without sqlparse, fall back to the legacy (and buggy) logic. + warnings.warn( + "Providing initial SQL data on a %s database will require " + "sqlparse in Django 1.9." % self.connection.vendor, + RemovedInDjango19Warning) + from django.core.management.sql import _split_statements + return _split_statements(sql) + else: + raise + else: + return [sqlparse.format(statement, strip_comments=True) + for statement in sqlparse.split(sql) if statement] + + def process_clob(self, value): + """ + Returns the value of a CLOB column, for backends that return a locator + object that requires additional processing. + """ + return value + + def return_insert_id(self): + """ + For backends that support returning the last insert ID as part + of an insert query, this method returns the SQL and params to + append to the INSERT query. The returned fragment should + contain a format string to hold the appropriate column. + """ + pass + + def compiler(self, compiler_name): + """ + Returns the SQLCompiler class corresponding to the given name, + in the namespace corresponding to the `compiler_module` attribute + on this backend. + """ + if self._cache is None: + self._cache = import_module(self.compiler_module) + return getattr(self._cache, compiler_name) + + def quote_name(self, name): + """ + Returns a quoted version of the given table, index or column name. Does + not quote the given name if it's already been quoted. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a quote_name() method') + + def random_function_sql(self): + """ + Returns an SQL expression that returns a random value. + """ + return 'RANDOM()' + + def regex_lookup(self, lookup_type): + """ + Returns the string to use in a query when performing regular expression + lookups (using "regex" or "iregex"). The resulting string should + contain a '%s' placeholder for the column being searched against. + + If the feature is not supported (or part of it is not supported), a + NotImplementedError exception can be raised. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations may require a regex_lookup() method') + + def savepoint_create_sql(self, sid): + """ + Returns the SQL for starting a new savepoint. Only required if the + "uses_savepoints" feature is True. The "sid" parameter is a string + for the savepoint id. + """ + return "SAVEPOINT %s" % self.quote_name(sid) + + def savepoint_commit_sql(self, sid): + """ + Returns the SQL for committing the given savepoint. + """ + return "RELEASE SAVEPOINT %s" % self.quote_name(sid) + + def savepoint_rollback_sql(self, sid): + """ + Returns the SQL for rolling back the given savepoint. + """ + return "ROLLBACK TO SAVEPOINT %s" % self.quote_name(sid) + + def set_time_zone_sql(self): + """ + Returns the SQL that will set the connection's time zone. + + Returns '' if the backend doesn't support time zones. + """ + return '' + + def sql_flush(self, style, tables, sequences, allow_cascade=False): + """ + Returns a list of SQL statements required to remove all data from + the given database tables (without actually removing the tables + themselves). + + The returned value also includes SQL statements required to reset DB + sequences passed in :param sequences:. + + The `style` argument is a Style object as returned by either + color_style() or no_style() in django.core.management.color. + + The `allow_cascade` argument determines whether truncation may cascade + to tables with foreign keys pointing the tables being truncated. + PostgreSQL requires a cascade even if these tables are empty. + """ + raise NotImplementedError('subclasses of BaseDatabaseOperations must provide a sql_flush() method') + + def sequence_reset_by_name_sql(self, style, sequences): + """ + Returns a list of the SQL statements required to reset sequences + passed in :param sequences:. + + The `style` argument is a Style object as returned by either + color_style() or no_style() in django.core.management.color. + """ + return [] + + def sequence_reset_sql(self, style, model_list): + """ + Returns a list of the SQL statements required to reset sequences for + the given models. + + The `style` argument is a Style object as returned by either + color_style() or no_style() in django.core.management.color. + """ + return [] # No sequence reset required by default. + + def start_transaction_sql(self): + """ + Returns the SQL statement required to start a transaction. + """ + return "BEGIN;" + + def end_transaction_sql(self, success=True): + """ + Returns the SQL statement required to end a transaction. + """ + if not success: + return "ROLLBACK;" + return "COMMIT;" + + def tablespace_sql(self, tablespace, inline=False): + """ + Returns the SQL that will be used in a query to define the tablespace. + + Returns '' if the backend doesn't support tablespaces. + + If inline is True, the SQL is appended to a row; otherwise it's appended + to the entire CREATE TABLE or CREATE INDEX statement. + """ + return '' + + def prep_for_like_query(self, x): + """Prepares a value for use in a LIKE query.""" + return force_text(x).replace("\\", "\\\\").replace("%", "\%").replace("_", "\_") + + # Same as prep_for_like_query(), but called for "iexact" matches, which + # need not necessarily be implemented using "LIKE" in the backend. + prep_for_iexact_query = prep_for_like_query + + def validate_autopk_value(self, value): + """ + Certain backends do not accept some values for "serial" fields + (for example zero in MySQL). This method will raise a ValueError + if the value is invalid, otherwise returns validated value. + """ + return value + + def value_to_db_date(self, value): + """ + Transform a date value to an object compatible with what is expected + by the backend driver for date columns. + """ + if value is None: + return None + return six.text_type(value) + + def value_to_db_datetime(self, value): + """ + Transform a datetime value to an object compatible with what is expected + by the backend driver for datetime columns. + """ + if value is None: + return None + return six.text_type(value) + + def value_to_db_time(self, value): + """ + Transform a time value to an object compatible with what is expected + by the backend driver for time columns. + """ + if value is None: + return None + if timezone.is_aware(value): + raise ValueError("Django does not support timezone-aware times.") + return six.text_type(value) + + def value_to_db_decimal(self, value, max_digits, decimal_places): + """ + Transform a decimal.Decimal value to an object compatible with what is + expected by the backend driver for decimal (numeric) columns. + """ + return utils.format_number(value, max_digits, decimal_places) + + def year_lookup_bounds_for_date_field(self, value): + """ + Returns a two-elements list with the lower and upper bound to be used + with a BETWEEN operator to query a DateField value using a year + lookup. + + `value` is an int, containing the looked-up year. + """ + first = datetime.date(value, 1, 1) + second = datetime.date(value, 12, 31) + return [first, second] + + def year_lookup_bounds_for_datetime_field(self, value): + """ + Returns a two-elements list with the lower and upper bound to be used + with a BETWEEN operator to query a DateTimeField value using a year + lookup. + + `value` is an int, containing the looked-up year. + """ + first = datetime.datetime(value, 1, 1) + second = datetime.datetime(value, 12, 31, 23, 59, 59, 999999) + if settings.USE_TZ: + tz = timezone.get_current_timezone() + first = timezone.make_aware(first, tz) + second = timezone.make_aware(second, tz) + return [first, second] + + def get_db_converters(self, expression): + """Get a list of functions needed to convert field data. + + Some field types on some backends do not provide data in the correct + format, this is the hook for coverter functions. + """ + return [] + + def convert_durationfield_value(self, value, expression, context): + if value is not None: + value = str(decimal.Decimal(value) / decimal.Decimal(1000000)) + value = parse_duration(value) + return value + + def check_aggregate_support(self, aggregate_func): + """Check that the backend supports the provided aggregate + + This is used on specific backends to rule out known aggregates + that are known to have faulty implementations. If the named + aggregate function has a known problem, the backend should + raise NotImplementedError. + """ + pass + + def combine_expression(self, connector, sub_expressions): + """Combine a list of subexpressions into a single expression, using + the provided connecting operator. This is required because operators + can vary between backends (e.g., Oracle with %% and &) and between + subexpression types (e.g., date expressions) + """ + conn = ' %s ' % connector + return conn.join(sub_expressions) + + def combine_duration_expression(self, connector, sub_expressions): + return self.combine_expression(connector, sub_expressions) + + def modify_insert_params(self, placeholders, params): + """Allow modification of insert parameters. Needed for Oracle Spatial + backend due to #10888. + """ + return params + + def integer_field_range(self, internal_type): + """ + Given an integer field internal type (e.g. 'PositiveIntegerField'), + returns a tuple of the (min_value, max_value) form representing the + range of the column type bound to the field. + """ + return self.integer_field_ranges[internal_type] diff --git a/django/db/backends/schema.py b/django/db/backends/base/schema.py similarity index 100% rename from django/db/backends/schema.py rename to django/db/backends/base/schema.py index dfda05bc19..4a63088d30 100644 --- a/django/db/backends/schema.py +++ b/django/db/backends/base/schema.py @@ -3,9 +3,9 @@ import hashlib from django.db.backends.utils import truncate_name from django.db.models.fields.related import ManyToManyField from django.db.transaction import atomic +from django.utils import six from django.utils.encoding import force_bytes from django.utils.log import getLogger -from django.utils import six logger = getLogger('django.db.backends.schema') diff --git a/django/db/backends/base/validation.py b/django/db/backends/base/validation.py new file mode 100644 index 0000000000..1d558c84ac --- /dev/null +++ b/django/db/backends/base/validation.py @@ -0,0 +1,38 @@ +from django.core import checks + + +class BaseDatabaseValidation(object): + """ + This class encapsulates all backend-specific model validation. + """ + def __init__(self, connection): + self.connection = connection + + def validate_field(self, errors, opts, f): + """ + By default, there is no backend-specific validation. + + This method has been deprecated by the new checks framework. New + backends should implement check_field instead. + """ + # This is deliberately commented out. It exists as a marker to + # remind us to remove this method, and the check_field() shim, + # when the time comes. + # warnings.warn('"validate_field" has been deprecated", RemovedInDjango19Warning) + pass + + def check_field(self, field, **kwargs): + class ErrorList(list): + """A dummy list class that emulates API used by the older + validate_field() method. When validate_field() is fully + deprecated, this dummy can be removed too. + """ + def add(self, opts, error_message): + self.append(checks.Error(error_message, hint=None, obj=field)) + + errors = ErrorList() + # Some tests create fields in isolation -- the fields are not attached + # to any model, so they have no `model` attribute. + opts = field.model._meta if hasattr(field, 'model') else None + self.validate_field(errors, field, opts) + return list(errors) diff --git a/django/db/backends/dummy/base.py b/django/db/backends/dummy/base.py index 61a788e433..e39d7e621e 100644 --- a/django/db/backends/dummy/base.py +++ b/django/db/backends/dummy/base.py @@ -8,10 +8,13 @@ ImproperlyConfigured. """ from django.core.exceptions import ImproperlyConfigured -from django.db.backends import (BaseDatabaseOperations, BaseDatabaseClient, - BaseDatabaseIntrospection, BaseDatabaseWrapper, BaseDatabaseFeatures, - BaseDatabaseValidation) -from django.db.backends.creation import BaseDatabaseCreation +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.client import BaseDatabaseClient +from django.db.backends.base.creation import BaseDatabaseCreation +from django.db.backends.base.features import BaseDatabaseFeatures +from django.db.backends.base.operations import BaseDatabaseOperations +from django.db.backends.base.introspection import BaseDatabaseIntrospection +from django.db.backends.base.validation import BaseDatabaseValidation def complain(*args, **kwargs): diff --git a/django/db/backends/mysql/base.py b/django/db/backends/mysql/base.py index ab72aefe09..f3413f43f3 100644 --- a/django/db/backends/mysql/base.py +++ b/django/db/backends/mysql/base.py @@ -9,15 +9,35 @@ from __future__ import unicode_literals import datetime import re import sys -import uuid import warnings +from django.conf import settings +from django.db import utils +from django.db.backends import utils as backend_utils +from django.db.backends.base.base import BaseDatabaseWrapper +from django.utils.encoding import force_str +from django.db.backends.mysql.schema import DatabaseSchemaEditor +from django.utils import six, timezone +from django.utils.functional import cached_property +from django.utils.safestring import SafeBytes, SafeText + try: import MySQLdb as Database except ImportError as e: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading MySQLdb module: %s" % e) +from MySQLdb.converters import conversions, Thing2Literal +from MySQLdb.constants import FIELD_TYPE, CLIENT + +# Some of these import MySQLdb, so import them after checking if it's installed. +from .client import DatabaseClient +from .creation import DatabaseCreation +from .features import DatabaseFeatures +from .introspection import DatabaseIntrospection +from .operations import DatabaseOperations +from .validation import DatabaseValidation + # We want version (1, 2, 1, 'final', 2) or later. We can't just use # lexicographic ordering in this check because then (1, 2, 1, 'gamma') # inadvertently passes the version test. @@ -27,28 +47,6 @@ if (version < (1, 2, 1) or (version[:3] == (1, 2, 1) and from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("MySQLdb-1.2.1p2 or newer is required; you have %s" % Database.__version__) -from MySQLdb.converters import conversions, Thing2Literal -from MySQLdb.constants import FIELD_TYPE, CLIENT - -try: - import pytz -except ImportError: - pytz = None - -from django.conf import settings -from django.db import utils -from django.db.backends import (utils as backend_utils, BaseDatabaseFeatures, - BaseDatabaseOperations, BaseDatabaseWrapper) -from django.db.backends.mysql.client import DatabaseClient -from django.db.backends.mysql.creation import DatabaseCreation -from django.db.backends.mysql.introspection import DatabaseIntrospection -from django.db.backends.mysql.validation import DatabaseValidation -from django.utils.encoding import force_str, force_text -from django.db.backends.mysql.schema import DatabaseSchemaEditor -from django.utils.functional import cached_property -from django.utils.safestring import SafeBytes, SafeText -from django.utils import six -from django.utils import timezone DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError @@ -159,259 +157,6 @@ class CursorWrapper(object): self.close() -class DatabaseFeatures(BaseDatabaseFeatures): - empty_fetchmany_value = () - update_can_self_select = False - allows_group_by_pk = True - related_fields_match_type = True - allow_sliced_subqueries = False - has_bulk_insert = True - has_select_for_update = True - has_select_for_update_nowait = False - supports_forward_references = False - supports_regex_backreferencing = False - supports_date_lookup_using_string = False - can_introspect_autofield = True - can_introspect_binary_field = False - can_introspect_small_integer_field = True - supports_timezones = False - requires_explicit_null_ordering_when_grouping = True - allows_auto_pk_0 = False - uses_savepoints = True - can_release_savepoints = True - atomic_transactions = False - supports_column_check_constraints = False - - @cached_property - def _mysql_storage_engine(self): - "Internal method used in Django tests. Don't rely on this from your code" - with self.connection.cursor() as cursor: - cursor.execute("SELECT ENGINE FROM INFORMATION_SCHEMA.ENGINES WHERE SUPPORT = 'DEFAULT'") - result = cursor.fetchone() - return result[0] - - @cached_property - def can_introspect_foreign_keys(self): - "Confirm support for introspected foreign keys" - return self._mysql_storage_engine != 'MyISAM' - - @cached_property - def supports_microsecond_precision(self): - # See https://github.com/farcepest/MySQLdb1/issues/24 for the reason - # about requiring MySQLdb 1.2.5 - return self.connection.mysql_version >= (5, 6, 4) and Database.version_info >= (1, 2, 5) - - @cached_property - def has_zoneinfo_database(self): - # MySQL accepts full time zones names (eg. Africa/Nairobi) but rejects - # abbreviations (eg. EAT). When pytz isn't installed and the current - # time zone is LocalTimezone (the only sensible value in this - # context), the current time zone name will be an abbreviation. As a - # consequence, MySQL cannot perform time zone conversions reliably. - if pytz is None: - return False - - # Test if the time zone definitions are installed. - with self.connection.cursor() as cursor: - cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1") - return cursor.fetchone() is not None - - def introspected_boolean_field_type(self, *args, **kwargs): - return 'IntegerField' - - -class DatabaseOperations(BaseDatabaseOperations): - compiler_module = "django.db.backends.mysql.compiler" - - # MySQL stores positive fields as UNSIGNED ints. - integer_field_ranges = dict(BaseDatabaseOperations.integer_field_ranges, - PositiveSmallIntegerField=(0, 4294967295), - PositiveIntegerField=(0, 18446744073709551615), - ) - - def date_extract_sql(self, lookup_type, field_name): - # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html - if lookup_type == 'week_day': - # DAYOFWEEK() returns an integer, 1-7, Sunday=1. - # Note: WEEKDAY() returns 0-6, Monday=0. - return "DAYOFWEEK(%s)" % field_name - else: - return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) - - def date_trunc_sql(self, lookup_type, field_name): - fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] - format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. - format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') - try: - i = fields.index(lookup_type) + 1 - except ValueError: - sql = field_name - else: - format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) - sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) - return sql - - def datetime_extract_sql(self, lookup_type, field_name, tzname): - if settings.USE_TZ: - field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name - params = [tzname] - else: - params = [] - # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html - if lookup_type == 'week_day': - # DAYOFWEEK() returns an integer, 1-7, Sunday=1. - # Note: WEEKDAY() returns 0-6, Monday=0. - sql = "DAYOFWEEK(%s)" % field_name - else: - sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) - return sql, params - - def datetime_trunc_sql(self, lookup_type, field_name, tzname): - if settings.USE_TZ: - field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name - params = [tzname] - else: - params = [] - fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] - format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. - format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') - try: - i = fields.index(lookup_type) + 1 - except ValueError: - sql = field_name - else: - format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) - sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) - return sql, params - - def date_interval_sql(self, timedelta): - return "INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND" % ( - timedelta.days, timedelta.seconds, timedelta.microseconds), [] - - def format_for_duration_arithmetic(self, sql): - return 'INTERVAL %s MICROSECOND' % sql - - def drop_foreignkey_sql(self): - return "DROP FOREIGN KEY" - - def force_no_ordering(self): - """ - "ORDER BY NULL" prevents MySQL from implicitly ordering by grouped - columns. If no ordering would otherwise be applied, we don't want any - implicit sorting going on. - """ - return [(None, ("NULL", [], False))] - - def fulltext_search_sql(self, field_name): - return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name - - def last_executed_query(self, cursor, sql, params): - # With MySQLdb, cursor objects have an (undocumented) "_last_executed" - # attribute where the exact query sent to the database is saved. - # See MySQLdb/cursors.py in the source distribution. - return force_text(getattr(cursor, '_last_executed', None), errors='replace') - - def no_limit_value(self): - # 2**64 - 1, as recommended by the MySQL documentation - return 18446744073709551615 - - def quote_name(self, name): - if name.startswith("`") and name.endswith("`"): - return name # Quoting once is enough. - return "`%s`" % name - - def random_function_sql(self): - return 'RAND()' - - def sql_flush(self, style, tables, sequences, allow_cascade=False): - # NB: The generated SQL below is specific to MySQL - # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements - # to clear all tables of all data - if tables: - sql = ['SET FOREIGN_KEY_CHECKS = 0;'] - for table in tables: - sql.append('%s %s;' % ( - style.SQL_KEYWORD('TRUNCATE'), - style.SQL_FIELD(self.quote_name(table)), - )) - sql.append('SET FOREIGN_KEY_CHECKS = 1;') - sql.extend(self.sequence_reset_by_name_sql(style, sequences)) - return sql - else: - return [] - - def validate_autopk_value(self, value): - # MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653. - if value == 0: - raise ValueError('The database backend does not accept 0 as a ' - 'value for AutoField.') - return value - - def value_to_db_datetime(self, value): - if value is None: - return None - - # MySQL doesn't support tz-aware datetimes - if timezone.is_aware(value): - if settings.USE_TZ: - value = value.astimezone(timezone.utc).replace(tzinfo=None) - else: - raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.") - - return six.text_type(value) - - def value_to_db_time(self, value): - if value is None: - return None - - # MySQL doesn't support tz-aware times - if timezone.is_aware(value): - raise ValueError("MySQL backend does not support timezone-aware times.") - - return six.text_type(value) - - def max_name_length(self): - return 64 - - def bulk_insert_sql(self, fields, num_values): - items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) - return "VALUES " + ", ".join([items_sql] * num_values) - - def combine_expression(self, connector, sub_expressions): - """ - MySQL requires special cases for ^ operators in query expressions - """ - if connector == '^': - return 'POW(%s)' % ','.join(sub_expressions) - return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) - - def get_db_converters(self, expression): - converters = super(DatabaseOperations, self).get_db_converters(expression) - internal_type = expression.output_field.get_internal_type() - if internal_type in ['BooleanField', 'NullBooleanField']: - converters.append(self.convert_booleanfield_value) - if internal_type == 'UUIDField': - converters.append(self.convert_uuidfield_value) - if internal_type == 'TextField': - converters.append(self.convert_textfield_value) - return converters - - def convert_booleanfield_value(self, value, expression, context): - if value in (0, 1): - value = bool(value) - return value - - def convert_uuidfield_value(self, value, expression, context): - if value is not None: - value = uuid.UUID(value) - return value - - def convert_textfield_value(self, value, expression, context): - if value is not None: - value = force_text(value) - return value - - class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'mysql' # This dictionary maps Field objects to their associated MySQL column diff --git a/django/db/backends/mysql/client.py b/django/db/backends/mysql/client.py index dc1d9846b8..29059b0fa2 100644 --- a/django/db/backends/mysql/client.py +++ b/django/db/backends/mysql/client.py @@ -1,6 +1,6 @@ import subprocess -from django.db.backends import BaseDatabaseClient +from django.db.backends.base.client import BaseDatabaseClient class DatabaseClient(BaseDatabaseClient): diff --git a/django/db/backends/mysql/creation.py b/django/db/backends/mysql/creation.py index 8cce569058..5749f9da49 100644 --- a/django/db/backends/mysql/creation.py +++ b/django/db/backends/mysql/creation.py @@ -1,4 +1,4 @@ -from django.db.backends.creation import BaseDatabaseCreation +from django.db.backends.base.creation import BaseDatabaseCreation class DatabaseCreation(BaseDatabaseCreation): diff --git a/django/db/backends/mysql/features.py b/django/db/backends/mysql/features.py new file mode 100644 index 0000000000..8f2e99a791 --- /dev/null +++ b/django/db/backends/mysql/features.py @@ -0,0 +1,70 @@ +from django.db.backends.base.features import BaseDatabaseFeatures +from django.utils.functional import cached_property + +from .base import Database + +try: + import pytz +except ImportError: + pytz = None + + +class DatabaseFeatures(BaseDatabaseFeatures): + empty_fetchmany_value = () + update_can_self_select = False + allows_group_by_pk = True + related_fields_match_type = True + allow_sliced_subqueries = False + has_bulk_insert = True + has_select_for_update = True + has_select_for_update_nowait = False + supports_forward_references = False + supports_regex_backreferencing = False + supports_date_lookup_using_string = False + can_introspect_autofield = True + can_introspect_binary_field = False + can_introspect_small_integer_field = True + supports_timezones = False + requires_explicit_null_ordering_when_grouping = True + allows_auto_pk_0 = False + uses_savepoints = True + can_release_savepoints = True + atomic_transactions = False + supports_column_check_constraints = False + + @cached_property + def _mysql_storage_engine(self): + "Internal method used in Django tests. Don't rely on this from your code" + with self.connection.cursor() as cursor: + cursor.execute("SELECT ENGINE FROM INFORMATION_SCHEMA.ENGINES WHERE SUPPORT = 'DEFAULT'") + result = cursor.fetchone() + return result[0] + + @cached_property + def can_introspect_foreign_keys(self): + "Confirm support for introspected foreign keys" + return self._mysql_storage_engine != 'MyISAM' + + @cached_property + def supports_microsecond_precision(self): + # See https://github.com/farcepest/MySQLdb1/issues/24 for the reason + # about requiring MySQLdb 1.2.5 + return self.connection.mysql_version >= (5, 6, 4) and Database.version_info >= (1, 2, 5) + + @cached_property + def has_zoneinfo_database(self): + # MySQL accepts full time zones names (eg. Africa/Nairobi) but rejects + # abbreviations (eg. EAT). When pytz isn't installed and the current + # time zone is LocalTimezone (the only sensible value in this + # context), the current time zone name will be an abbreviation. As a + # consequence, MySQL cannot perform time zone conversions reliably. + if pytz is None: + return False + + # Test if the time zone definitions are installed. + with self.connection.cursor() as cursor: + cursor.execute("SELECT 1 FROM mysql.time_zone LIMIT 1") + return cursor.fetchone() is not None + + def introspected_boolean_field_type(self, *args, **kwargs): + return 'IntegerField' diff --git a/django/db/backends/mysql/introspection.py b/django/db/backends/mysql/introspection.py index 3ff28cf673..dc78a00bfc 100644 --- a/django/db/backends/mysql/introspection.py +++ b/django/db/backends/mysql/introspection.py @@ -1,10 +1,14 @@ from collections import namedtuple import re -from .base import FIELD_TYPE + from django.utils.datastructures import OrderedSet -from django.db.backends import BaseDatabaseIntrospection, FieldInfo, TableInfo +from django.db.backends.base.introspection import ( + BaseDatabaseIntrospection, FieldInfo, TableInfo, +) from django.utils.encoding import force_text +from MySQLdb.constants import FIELD_TYPE + FieldInfo = namedtuple('FieldInfo', FieldInfo._fields + ('extra',)) foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") diff --git a/django/db/backends/mysql/operations.py b/django/db/backends/mysql/operations.py new file mode 100644 index 0000000000..7edc13b3ff --- /dev/null +++ b/django/db/backends/mysql/operations.py @@ -0,0 +1,200 @@ +from __future__ import unicode_literals + +import uuid + +from django.conf import settings +from django.db.backends.base.operations import BaseDatabaseOperations +from django.utils import six, timezone +from django.utils.encoding import force_text + + +class DatabaseOperations(BaseDatabaseOperations): + compiler_module = "django.db.backends.mysql.compiler" + + # MySQL stores positive fields as UNSIGNED ints. + integer_field_ranges = dict(BaseDatabaseOperations.integer_field_ranges, + PositiveSmallIntegerField=(0, 4294967295), + PositiveIntegerField=(0, 18446744073709551615), + ) + + def date_extract_sql(self, lookup_type, field_name): + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + if lookup_type == 'week_day': + # DAYOFWEEK() returns an integer, 1-7, Sunday=1. + # Note: WEEKDAY() returns 0-6, Monday=0. + return "DAYOFWEEK(%s)" % field_name + else: + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + + def date_trunc_sql(self, lookup_type, field_name): + fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] + format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. + format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') + try: + i = fields.index(lookup_type) + 1 + except ValueError: + sql = field_name + else: + format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) + sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) + return sql + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name + params = [tzname] + else: + params = [] + # http://dev.mysql.com/doc/mysql/en/date-and-time-functions.html + if lookup_type == 'week_day': + # DAYOFWEEK() returns an integer, 1-7, Sunday=1. + # Note: WEEKDAY() returns 0-6, Monday=0. + sql = "DAYOFWEEK(%s)" % field_name + else: + sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + return sql, params + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = "CONVERT_TZ(%s, 'UTC', %%s)" % field_name + params = [tzname] + else: + params = [] + fields = ['year', 'month', 'day', 'hour', 'minute', 'second'] + format = ('%%Y-', '%%m', '-%%d', ' %%H:', '%%i', ':%%s') # Use double percents to escape. + format_def = ('0000-', '01', '-01', ' 00:', '00', ':00') + try: + i = fields.index(lookup_type) + 1 + except ValueError: + sql = field_name + else: + format_str = ''.join([f for f in format[:i]] + [f for f in format_def[i:]]) + sql = "CAST(DATE_FORMAT(%s, '%s') AS DATETIME)" % (field_name, format_str) + return sql, params + + def date_interval_sql(self, timedelta): + return "INTERVAL '%d 0:0:%d:%d' DAY_MICROSECOND" % ( + timedelta.days, timedelta.seconds, timedelta.microseconds), [] + + def format_for_duration_arithmetic(self, sql): + return 'INTERVAL %s MICROSECOND' % sql + + def drop_foreignkey_sql(self): + return "DROP FOREIGN KEY" + + def force_no_ordering(self): + """ + "ORDER BY NULL" prevents MySQL from implicitly ordering by grouped + columns. If no ordering would otherwise be applied, we don't want any + implicit sorting going on. + """ + return [(None, ("NULL", [], False))] + + def fulltext_search_sql(self, field_name): + return 'MATCH (%s) AGAINST (%%s IN BOOLEAN MODE)' % field_name + + def last_executed_query(self, cursor, sql, params): + # With MySQLdb, cursor objects have an (undocumented) "_last_executed" + # attribute where the exact query sent to the database is saved. + # See MySQLdb/cursors.py in the source distribution. + return force_text(getattr(cursor, '_last_executed', None), errors='replace') + + def no_limit_value(self): + # 2**64 - 1, as recommended by the MySQL documentation + return 18446744073709551615 + + def quote_name(self, name): + if name.startswith("`") and name.endswith("`"): + return name # Quoting once is enough. + return "`%s`" % name + + def random_function_sql(self): + return 'RAND()' + + def sql_flush(self, style, tables, sequences, allow_cascade=False): + # NB: The generated SQL below is specific to MySQL + # 'TRUNCATE x;', 'TRUNCATE y;', 'TRUNCATE z;'... style SQL statements + # to clear all tables of all data + if tables: + sql = ['SET FOREIGN_KEY_CHECKS = 0;'] + for table in tables: + sql.append('%s %s;' % ( + style.SQL_KEYWORD('TRUNCATE'), + style.SQL_FIELD(self.quote_name(table)), + )) + sql.append('SET FOREIGN_KEY_CHECKS = 1;') + sql.extend(self.sequence_reset_by_name_sql(style, sequences)) + return sql + else: + return [] + + def validate_autopk_value(self, value): + # MySQLism: zero in AUTO_INCREMENT field does not work. Refs #17653. + if value == 0: + raise ValueError('The database backend does not accept 0 as a ' + 'value for AutoField.') + return value + + def value_to_db_datetime(self, value): + if value is None: + return None + + # MySQL doesn't support tz-aware datetimes + if timezone.is_aware(value): + if settings.USE_TZ: + value = value.astimezone(timezone.utc).replace(tzinfo=None) + else: + raise ValueError("MySQL backend does not support timezone-aware datetimes when USE_TZ is False.") + + return six.text_type(value) + + def value_to_db_time(self, value): + if value is None: + return None + + # MySQL doesn't support tz-aware times + if timezone.is_aware(value): + raise ValueError("MySQL backend does not support timezone-aware times.") + + return six.text_type(value) + + def max_name_length(self): + return 64 + + def bulk_insert_sql(self, fields, num_values): + items_sql = "(%s)" % ", ".join(["%s"] * len(fields)) + return "VALUES " + ", ".join([items_sql] * num_values) + + def combine_expression(self, connector, sub_expressions): + """ + MySQL requires special cases for ^ operators in query expressions + """ + if connector == '^': + return 'POW(%s)' % ','.join(sub_expressions) + return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) + + def get_db_converters(self, expression): + converters = super(DatabaseOperations, self).get_db_converters(expression) + internal_type = expression.output_field.get_internal_type() + if internal_type in ['BooleanField', 'NullBooleanField']: + converters.append(self.convert_booleanfield_value) + if internal_type == 'UUIDField': + converters.append(self.convert_uuidfield_value) + if internal_type == 'TextField': + converters.append(self.convert_textfield_value) + return converters + + def convert_booleanfield_value(self, value, expression, context): + if value in (0, 1): + value = bool(value) + return value + + def convert_uuidfield_value(self, value, expression, context): + if value is not None: + value = uuid.UUID(value) + return value + + def convert_textfield_value(self, value, expression, context): + if value is not None: + value = force_text(value) + return value diff --git a/django/db/backends/mysql/schema.py b/django/db/backends/mysql/schema.py index 697490e888..4512e99d7e 100644 --- a/django/db/backends/mysql/schema.py +++ b/django/db/backends/mysql/schema.py @@ -1,4 +1,4 @@ -from django.db.backends.schema import BaseDatabaseSchemaEditor +from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.models import NOT_PROVIDED diff --git a/django/db/backends/mysql/validation.py b/django/db/backends/mysql/validation.py index d69cb7e3e0..9dcd14403f 100644 --- a/django/db/backends/mysql/validation.py +++ b/django/db/backends/mysql/validation.py @@ -1,5 +1,5 @@ from django.core import checks -from django.db.backends import BaseDatabaseValidation +from django.db.backends.base.validation import BaseDatabaseValidation class DatabaseValidation(BaseDatabaseValidation): diff --git a/django/db/backends/oracle/base.py b/django/db/backends/oracle/base.py index c684ddd842..134321cecc 100644 --- a/django/db/backends/oracle/base.py +++ b/django/db/backends/oracle/base.py @@ -7,12 +7,20 @@ from __future__ import unicode_literals import datetime import decimal -import re +import os import platform import sys -import uuid import warnings +from django.conf import settings +from django.db import utils +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.validation import BaseDatabaseValidation +from django.utils import six, timezone +from django.utils.duration import duration_string +from django.utils.encoding import force_bytes, force_text +from django.utils.functional import cached_property + def _setup_environment(environ): # Cygwin requires some special voodoo to set the environment variables @@ -29,7 +37,6 @@ def _setup_environment(environ): for name, value in environ: kernel32.SetEnvironmentVariableA(name, value) else: - import os os.environ.update(environ) _setup_environment([ @@ -47,520 +54,18 @@ except ImportError as e: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading cx_Oracle module: %s" % e) -try: - import pytz -except ImportError: - pytz = None - -from django.conf import settings -from django.db import utils -from django.db.backends import (BaseDatabaseFeatures, BaseDatabaseOperations, - BaseDatabaseWrapper, BaseDatabaseValidation, utils as backend_utils) -from django.db.backends.oracle.client import DatabaseClient -from django.db.backends.oracle.creation import DatabaseCreation -from django.db.backends.oracle.introspection import DatabaseIntrospection -from django.db.backends.oracle.schema import DatabaseSchemaEditor -from django.db.utils import InterfaceError -from django.utils import six, timezone -from django.utils.duration import duration_string -from django.utils.encoding import force_bytes, force_text -from django.utils.functional import cached_property - +# Some of these import cx_Oracle, so import them after checking if it's installed. +from .client import DatabaseClient +from .creation import DatabaseCreation +from .features import DatabaseFeatures +from .introspection import DatabaseIntrospection +from .operations import DatabaseOperations +from .schema import DatabaseSchemaEditor +from .utils import convert_unicode, Oracle_datetime DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError -# Check whether cx_Oracle was compiled with the WITH_UNICODE option if cx_Oracle is pre-5.1. This will -# also be True for cx_Oracle 5.1 and in Python 3.0. See #19606 -if int(Database.version.split('.', 1)[0]) >= 5 and \ - (int(Database.version.split('.', 2)[1]) >= 1 or - not hasattr(Database, 'UNICODE')): - convert_unicode = force_text -else: - convert_unicode = force_bytes - - -class Oracle_datetime(datetime.datetime): - """ - A datetime object, with an additional class attribute - to tell cx_Oracle to save the microseconds too. - """ - input_size = Database.TIMESTAMP - - @classmethod - def from_datetime(cls, dt): - return Oracle_datetime(dt.year, dt.month, dt.day, - dt.hour, dt.minute, dt.second, dt.microsecond) - - -class DatabaseFeatures(BaseDatabaseFeatures): - empty_fetchmany_value = () - needs_datetime_string_cast = False - interprets_empty_strings_as_nulls = True - uses_savepoints = True - has_select_for_update = True - has_select_for_update_nowait = True - can_return_id_from_insert = True - allow_sliced_subqueries = False - supports_subqueries_in_group_by = False - supports_transactions = True - supports_timezones = False - has_zoneinfo_database = pytz is not None - supports_bitwise_or = False - has_native_duration_field = True - can_defer_constraint_checks = True - supports_partially_nullable_unique_constraints = False - truncates_names = True - has_bulk_insert = True - supports_tablespaces = True - supports_sequence_reset = False - can_introspect_max_length = False - can_introspect_time_field = False - atomic_transactions = False - supports_combined_alters = False - nulls_order_largest = True - requires_literal_defaults = True - connection_persists_old_columns = True - closed_cursor_error_class = InterfaceError - bare_select_suffix = " FROM DUAL" - uppercases_column_names = True - # select for update with limit can be achieved on Oracle, but not with the current backend. - supports_select_for_update_with_limit = False - - def introspected_boolean_field_type(self, field=None, created_separately=False): - """ - Some versions of Oracle -- we've seen this on 11.2.0.1 and suspect - it goes back -- have a weird bug where, when an integer column is - added to an existing table with a default, its precision is later - reported on introspection as 0, regardless of the real precision. - For Django introspection, this means that such columns are reported - as IntegerField even if they are really BigIntegerField or BooleanField. - - The bug is solved in Oracle 11.2.0.2 and up. - """ - if self.connection.oracle_full_version < '11.2.0.2' and field and field.has_default() and created_separately: - return 'IntegerField' - return super(DatabaseFeatures, self).introspected_boolean_field_type(field, created_separately) - - -class DatabaseOperations(BaseDatabaseOperations): - compiler_module = "django.db.backends.oracle.compiler" - - # Oracle uses NUMBER(11) and NUMBER(19) for integer fields. - integer_field_ranges = { - 'SmallIntegerField': (-99999999999, 99999999999), - 'IntegerField': (-99999999999, 99999999999), - 'BigIntegerField': (-9999999999999999999, 9999999999999999999), - 'PositiveSmallIntegerField': (0, 99999999999), - 'PositiveIntegerField': (0, 99999999999), - } - - def autoinc_sql(self, table, column): - # To simulate auto-incrementing primary keys in Oracle, we have to - # create a sequence and a trigger. - sq_name = self._get_sequence_name(table) - tr_name = self._get_trigger_name(table) - tbl_name = self.quote_name(table) - col_name = self.quote_name(column) - sequence_sql = """ -DECLARE - i INTEGER; -BEGIN - SELECT COUNT(*) INTO i FROM USER_CATALOG - WHERE TABLE_NAME = '%(sq_name)s' AND TABLE_TYPE = 'SEQUENCE'; - IF i = 0 THEN - EXECUTE IMMEDIATE 'CREATE SEQUENCE "%(sq_name)s"'; - END IF; -END; -/""" % locals() - trigger_sql = """ -CREATE OR REPLACE TRIGGER "%(tr_name)s" -BEFORE INSERT ON %(tbl_name)s -FOR EACH ROW -WHEN (new.%(col_name)s IS NULL) - BEGIN - SELECT "%(sq_name)s".nextval - INTO :new.%(col_name)s FROM dual; - END; -/""" % locals() - return sequence_sql, trigger_sql - - def cache_key_culling_sql(self): - return """ - SELECT cache_key - FROM (SELECT cache_key, rank() OVER (ORDER BY cache_key) AS rank FROM %s) - WHERE rank = %%s + 1 - """ - - def date_extract_sql(self, lookup_type, field_name): - if lookup_type == 'week_day': - # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. - return "TO_CHAR(%s, 'D')" % field_name - else: - # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm - return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) - - def date_interval_sql(self, timedelta): - """ - Implements the interval functionality for expressions - format for Oracle: - INTERVAL '3 00:03:20.000000' DAY(1) TO SECOND(6) - """ - minutes, seconds = divmod(timedelta.seconds, 60) - hours, minutes = divmod(minutes, 60) - days = str(timedelta.days) - day_precision = len(days) - fmt = "INTERVAL '%s %02d:%02d:%02d.%06d' DAY(%d) TO SECOND(6)" - return fmt % (days, hours, minutes, seconds, timedelta.microseconds, - day_precision), [] - - def date_trunc_sql(self, lookup_type, field_name): - # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 - if lookup_type in ('year', 'month'): - return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) - else: - return "TRUNC(%s)" % field_name - - # Oracle crashes with "ORA-03113: end-of-file on communication channel" - # if the time zone name is passed in parameter. Use interpolation instead. - # https://groups.google.com/forum/#!msg/django-developers/zwQju7hbG78/9l934yelwfsJ - # This regexp matches all time zone names from the zoneinfo database. - _tzname_re = re.compile(r'^[\w/:+-]+$') - - def _convert_field_to_tz(self, field_name, tzname): - if not self._tzname_re.match(tzname): - raise ValueError("Invalid time zone name: %s" % tzname) - # Convert from UTC to local time, returning TIMESTAMP WITH TIME ZONE. - result = "(FROM_TZ(%s, '0:00') AT TIME ZONE '%s')" % (field_name, tzname) - # Extracting from a TIMESTAMP WITH TIME ZONE ignore the time zone. - # Convert to a DATETIME, which is called DATE by Oracle. There's no - # built-in function to do that; the easiest is to go through a string. - result = "TO_CHAR(%s, 'YYYY-MM-DD HH24:MI:SS')" % result - result = "TO_DATE(%s, 'YYYY-MM-DD HH24:MI:SS')" % result - # Re-convert to a TIMESTAMP because EXTRACT only handles the date part - # on DATE values, even though they actually store the time part. - return "CAST(%s AS TIMESTAMP)" % result - - def datetime_extract_sql(self, lookup_type, field_name, tzname): - if settings.USE_TZ: - field_name = self._convert_field_to_tz(field_name, tzname) - if lookup_type == 'week_day': - # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. - sql = "TO_CHAR(%s, 'D')" % field_name - else: - # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm - sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) - return sql, [] - - def datetime_trunc_sql(self, lookup_type, field_name, tzname): - if settings.USE_TZ: - field_name = self._convert_field_to_tz(field_name, tzname) - # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 - if lookup_type in ('year', 'month'): - sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) - elif lookup_type == 'day': - sql = "TRUNC(%s)" % field_name - elif lookup_type == 'hour': - sql = "TRUNC(%s, 'HH24')" % field_name - elif lookup_type == 'minute': - sql = "TRUNC(%s, 'MI')" % field_name - else: - sql = field_name # Cast to DATE removes sub-second precision. - return sql, [] - - def get_db_converters(self, expression): - converters = super(DatabaseOperations, self).get_db_converters(expression) - internal_type = expression.output_field.get_internal_type() - if internal_type == 'TextField': - converters.append(self.convert_textfield_value) - elif internal_type == 'BinaryField': - converters.append(self.convert_binaryfield_value) - elif internal_type in ['BooleanField', 'NullBooleanField']: - converters.append(self.convert_booleanfield_value) - elif internal_type == 'DateField': - converters.append(self.convert_datefield_value) - elif internal_type == 'TimeField': - converters.append(self.convert_timefield_value) - elif internal_type == 'UUIDField': - converters.append(self.convert_uuidfield_value) - converters.append(self.convert_empty_values) - return converters - - def convert_empty_values(self, value, expression, context): - # Oracle stores empty strings as null. We need to undo this in - # order to adhere to the Django convention of using the empty - # string instead of null, but only if the field accepts the - # empty string. - field = expression.output_field - if value is None and field.empty_strings_allowed: - value = '' - if field.get_internal_type() == 'BinaryField': - value = b'' - return value - - def convert_textfield_value(self, value, expression, context): - if isinstance(value, Database.LOB): - value = force_text(value.read()) - return value - - def convert_binaryfield_value(self, value, expression, context): - if isinstance(value, Database.LOB): - value = force_bytes(value.read()) - return value - - def convert_booleanfield_value(self, value, expression, context): - if value in (1, 0): - value = bool(value) - return value - - # cx_Oracle always returns datetime.datetime objects for - # DATE and TIMESTAMP columns, but Django wants to see a - # python datetime.date, .time, or .datetime. - def convert_datefield_value(self, value, expression, context): - if isinstance(value, Database.Timestamp): - return value.date() - - def convert_timefield_value(self, value, expression, context): - if isinstance(value, Database.Timestamp): - value = value.time() - return value - - def convert_uuidfield_value(self, value, expression, context): - if value is not None: - value = uuid.UUID(value) - return value - - def deferrable_sql(self): - return " DEFERRABLE INITIALLY DEFERRED" - - def drop_sequence_sql(self, table): - return "DROP SEQUENCE %s;" % self.quote_name(self._get_sequence_name(table)) - - def fetch_returned_insert_id(self, cursor): - return int(cursor._insert_id_var.getvalue()) - - def field_cast_sql(self, db_type, internal_type): - if db_type and db_type.endswith('LOB'): - return "DBMS_LOB.SUBSTR(%s)" - else: - return "%s" - - def last_executed_query(self, cursor, sql, params): - # http://cx-oracle.sourceforge.net/html/cursor.html#Cursor.statement - # The DB API definition does not define this attribute. - statement = cursor.statement - if statement and six.PY2 and not isinstance(statement, unicode): - statement = statement.decode('utf-8') - # Unlike Psycopg's `query` and MySQLdb`'s `_last_executed`, CxOracle's - # `statement` doesn't contain the query parameters. refs #20010. - return super(DatabaseOperations, self).last_executed_query(cursor, statement, params) - - def last_insert_id(self, cursor, table_name, pk_name): - sq_name = self._get_sequence_name(table_name) - cursor.execute('SELECT "%s".currval FROM dual' % sq_name) - return cursor.fetchone()[0] - - def lookup_cast(self, lookup_type): - if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): - return "UPPER(%s)" - return "%s" - - def max_in_list_size(self): - return 1000 - - def max_name_length(self): - return 30 - - def prep_for_iexact_query(self, x): - return x - - def process_clob(self, value): - if value is None: - return '' - return force_text(value.read()) - - def quote_name(self, name): - # SQL92 requires delimited (quoted) names to be case-sensitive. When - # not quoted, Oracle has case-insensitive behavior for identifiers, but - # always defaults to uppercase. - # We simplify things by making Oracle identifiers always uppercase. - if not name.startswith('"') and not name.endswith('"'): - name = '"%s"' % backend_utils.truncate_name(name.upper(), - self.max_name_length()) - # Oracle puts the query text into a (query % args) construct, so % signs - # in names need to be escaped. The '%%' will be collapsed back to '%' at - # that stage so we aren't really making the name longer here. - name = name.replace('%', '%%') - return name.upper() - - def random_function_sql(self): - return "DBMS_RANDOM.RANDOM" - - def regex_lookup(self, lookup_type): - if lookup_type == 'regex': - match_option = "'c'" - else: - match_option = "'i'" - return 'REGEXP_LIKE(%%s, %%s, %s)' % match_option - - def return_insert_id(self): - return "RETURNING %s INTO %%s", (InsertIdVar(),) - - def savepoint_create_sql(self, sid): - return convert_unicode("SAVEPOINT " + self.quote_name(sid)) - - def savepoint_rollback_sql(self, sid): - return convert_unicode("ROLLBACK TO SAVEPOINT " + self.quote_name(sid)) - - def sql_flush(self, style, tables, sequences, allow_cascade=False): - # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', - # 'TRUNCATE z;'... style SQL statements - if tables: - # Oracle does support TRUNCATE, but it seems to get us into - # FK referential trouble, whereas DELETE FROM table works. - sql = ['%s %s %s;' % ( - style.SQL_KEYWORD('DELETE'), - style.SQL_KEYWORD('FROM'), - style.SQL_FIELD(self.quote_name(table)) - ) for table in tables] - # Since we've just deleted all the rows, running our sequence - # ALTER code will reset the sequence to 0. - sql.extend(self.sequence_reset_by_name_sql(style, sequences)) - return sql - else: - return [] - - def sequence_reset_by_name_sql(self, style, sequences): - sql = [] - for sequence_info in sequences: - sequence_name = self._get_sequence_name(sequence_info['table']) - table_name = self.quote_name(sequence_info['table']) - column_name = self.quote_name(sequence_info['column'] or 'id') - query = _get_sequence_reset_sql() % { - 'sequence': sequence_name, - 'table': table_name, - 'column': column_name, - } - sql.append(query) - return sql - - def sequence_reset_sql(self, style, model_list): - from django.db import models - output = [] - query = _get_sequence_reset_sql() - for model in model_list: - for f in model._meta.local_fields: - if isinstance(f, models.AutoField): - table_name = self.quote_name(model._meta.db_table) - sequence_name = self._get_sequence_name(model._meta.db_table) - column_name = self.quote_name(f.column) - output.append(query % {'sequence': sequence_name, - 'table': table_name, - 'column': column_name}) - # Only one AutoField is allowed per model, so don't - # continue to loop - break - for f in model._meta.many_to_many: - if not f.rel.through: - table_name = self.quote_name(f.m2m_db_table()) - sequence_name = self._get_sequence_name(f.m2m_db_table()) - column_name = self.quote_name('id') - output.append(query % {'sequence': sequence_name, - 'table': table_name, - 'column': column_name}) - return output - - def start_transaction_sql(self): - return '' - - def tablespace_sql(self, tablespace, inline=False): - if inline: - return "USING INDEX TABLESPACE %s" % self.quote_name(tablespace) - else: - return "TABLESPACE %s" % self.quote_name(tablespace) - - def value_to_db_date(self, value): - """ - Transform a date value to an object compatible with what is expected - by the backend driver for date columns. - The default implementation transforms the date to text, but that is not - necessary for Oracle. - """ - return value - - def value_to_db_datetime(self, value): - """ - Transform a datetime value to an object compatible with what is expected - by the backend driver for datetime columns. - - If naive datetime is passed assumes that is in UTC. Normally Django - models.DateTimeField makes sure that if USE_TZ is True passed datetime - is timezone aware. - """ - - if value is None: - return None - - # cx_Oracle doesn't support tz-aware datetimes - if timezone.is_aware(value): - if settings.USE_TZ: - value = value.astimezone(timezone.utc).replace(tzinfo=None) - else: - raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.") - - return Oracle_datetime.from_datetime(value) - - def value_to_db_time(self, value): - if value is None: - return None - - if isinstance(value, six.string_types): - return datetime.datetime.strptime(value, '%H:%M:%S') - - # Oracle doesn't support tz-aware times - if timezone.is_aware(value): - raise ValueError("Oracle backend does not support timezone-aware times.") - - return Oracle_datetime(1900, 1, 1, value.hour, value.minute, - value.second, value.microsecond) - - def year_lookup_bounds_for_date_field(self, value): - # Create bounds as real date values - first = datetime.date(value, 1, 1) - last = datetime.date(value, 12, 31) - return [first, last] - - def year_lookup_bounds_for_datetime_field(self, value): - # cx_Oracle doesn't support tz-aware datetimes - bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value) - if settings.USE_TZ: - bounds = [b.astimezone(timezone.utc) for b in bounds] - return [Oracle_datetime.from_datetime(b) for b in bounds] - - def combine_expression(self, connector, sub_expressions): - "Oracle requires special cases for %% and & operators in query expressions" - if connector == '%%': - return 'MOD(%s)' % ','.join(sub_expressions) - elif connector == '&': - return 'BITAND(%s)' % ','.join(sub_expressions) - elif connector == '|': - raise NotImplementedError("Bit-wise or is not supported in Oracle.") - elif connector == '^': - return 'POWER(%s)' % ','.join(sub_expressions) - return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) - - def _get_sequence_name(self, table): - name_length = self.max_name_length() - 3 - return '%s_SQ' % backend_utils.truncate_name(table, name_length).upper() - - def _get_trigger_name(self, table): - name_length = self.max_name_length() - 3 - return '%s_TR' % backend_utils.truncate_name(table, name_length).upper() - - def bulk_insert_sql(self, fields, num_values): - items_sql = "SELECT %s FROM DUAL" % ", ".join(["%s"] * len(fields)) - return " UNION ALL ".join([items_sql] * num_values) - class _UninitializedOperatorsDescriptor(object): @@ -897,19 +402,6 @@ class VariableWrapper(object): setattr(self.var, key, value) -class InsertIdVar(object): - """ - A late-binding cursor variable that can be passed to Cursor.execute - as a parameter, in order to receive the id of the row created by an - insert statement. - """ - - def bind_parameter(self, cursor): - param = cursor.cursor.var(Database.NUMBER) - cursor._insert_id_var = param - return param - - class FormatStylePlaceholderCursor(object): """ Django uses "format" (e.g. '%s') style placeholders, but Oracle uses ":var" @@ -1117,20 +609,3 @@ def to_unicode(s): if isinstance(s, six.string_types): return force_text(s) return s - - -def _get_sequence_reset_sql(): - # TODO: colorize this SQL code with style.SQL_KEYWORD(), etc. - return """ -DECLARE - table_value integer; - seq_value integer; -BEGIN - SELECT NVL(MAX(%(column)s), 0) INTO table_value FROM %(table)s; - SELECT NVL(last_number - cache_size, 0) INTO seq_value FROM user_sequences - WHERE sequence_name = '%(sequence)s'; - WHILE table_value > seq_value LOOP - SELECT "%(sequence)s".nextval INTO seq_value FROM dual; - END LOOP; -END; -/""" diff --git a/django/db/backends/oracle/client.py b/django/db/backends/oracle/client.py index 75be718d6f..da4f5af275 100644 --- a/django/db/backends/oracle/client.py +++ b/django/db/backends/oracle/client.py @@ -1,6 +1,6 @@ import subprocess -from django.db.backends import BaseDatabaseClient +from django.db.backends.base.client import BaseDatabaseClient class DatabaseClient(BaseDatabaseClient): diff --git a/django/db/backends/oracle/creation.py b/django/db/backends/oracle/creation.py index 1c1c435fa1..de9de2c5b1 100644 --- a/django/db/backends/oracle/creation.py +++ b/django/db/backends/oracle/creation.py @@ -2,7 +2,7 @@ import sys import time from django.conf import settings -from django.db.backends.creation import BaseDatabaseCreation +from django.db.backends.base.creation import BaseDatabaseCreation from django.db.utils import DatabaseError from django.utils.six.moves import input diff --git a/django/db/backends/oracle/features.py b/django/db/backends/oracle/features.py new file mode 100644 index 0000000000..93c0beaf1e --- /dev/null +++ b/django/db/backends/oracle/features.py @@ -0,0 +1,57 @@ +from django.db.backends.base.features import BaseDatabaseFeatures +from django.db.utils import InterfaceError + +try: + import pytz +except ImportError: + pytz = None + + +class DatabaseFeatures(BaseDatabaseFeatures): + empty_fetchmany_value = () + needs_datetime_string_cast = False + interprets_empty_strings_as_nulls = True + uses_savepoints = True + has_select_for_update = True + has_select_for_update_nowait = True + can_return_id_from_insert = True + allow_sliced_subqueries = False + supports_subqueries_in_group_by = False + supports_transactions = True + supports_timezones = False + has_zoneinfo_database = pytz is not None + supports_bitwise_or = False + has_native_duration_field = True + can_defer_constraint_checks = True + supports_partially_nullable_unique_constraints = False + truncates_names = True + has_bulk_insert = True + supports_tablespaces = True + supports_sequence_reset = False + can_introspect_max_length = False + can_introspect_time_field = False + atomic_transactions = False + supports_combined_alters = False + nulls_order_largest = True + requires_literal_defaults = True + connection_persists_old_columns = True + closed_cursor_error_class = InterfaceError + bare_select_suffix = " FROM DUAL" + uppercases_column_names = True + # select for update with limit can be achieved on Oracle, but not with the current backend. + supports_select_for_update_with_limit = False + + def introspected_boolean_field_type(self, field=None, created_separately=False): + """ + Some versions of Oracle -- we've seen this on 11.2.0.1 and suspect + it goes back -- have a weird bug where, when an integer column is + added to an existing table with a default, its precision is later + reported on introspection as 0, regardless of the real precision. + For Django introspection, this means that such columns are reported + as IntegerField even if they are really BigIntegerField or BooleanField. + + The bug is solved in Oracle 11.2.0.2 and up. + """ + if self.connection.oracle_full_version < '11.2.0.2' and field and field.has_default() and created_separately: + return 'IntegerField' + return super(DatabaseFeatures, self).introspected_boolean_field_type(field, created_separately) diff --git a/django/db/backends/oracle/introspection.py b/django/db/backends/oracle/introspection.py index 9618c47c64..3596368b6b 100644 --- a/django/db/backends/oracle/introspection.py +++ b/django/db/backends/oracle/introspection.py @@ -2,7 +2,9 @@ import re import cx_Oracle -from django.db.backends import BaseDatabaseIntrospection, FieldInfo, TableInfo +from django.db.backends.base.introspection import ( + BaseDatabaseIntrospection, FieldInfo, TableInfo, +) from django.utils.encoding import force_text foreign_key_re = re.compile(r"\sCONSTRAINT `[^`]*` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)` \(`([^`]*)`\)") diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py new file mode 100644 index 0000000000..f00fd3fbea --- /dev/null +++ b/django/db/backends/oracle/operations.py @@ -0,0 +1,447 @@ +from __future__ import unicode_literals + +import datetime +import re +import uuid + +from django.conf import settings +from django.db.backends.base.operations import BaseDatabaseOperations +from django.db.backends.utils import truncate_name +from django.utils import six, timezone +from django.utils.encoding import force_bytes, force_text + +from .base import Database +from .utils import convert_unicode, InsertIdVar, Oracle_datetime + + +class DatabaseOperations(BaseDatabaseOperations): + compiler_module = "django.db.backends.oracle.compiler" + + # Oracle uses NUMBER(11) and NUMBER(19) for integer fields. + integer_field_ranges = { + 'SmallIntegerField': (-99999999999, 99999999999), + 'IntegerField': (-99999999999, 99999999999), + 'BigIntegerField': (-9999999999999999999, 9999999999999999999), + 'PositiveSmallIntegerField': (0, 99999999999), + 'PositiveIntegerField': (0, 99999999999), + } + + # TODO: colorize this SQL code with style.SQL_KEYWORD(), etc. + _sequence_reset_sql = """ +DECLARE + table_value integer; + seq_value integer; +BEGIN + SELECT NVL(MAX(%(column)s), 0) INTO table_value FROM %(table)s; + SELECT NVL(last_number - cache_size, 0) INTO seq_value FROM user_sequences + WHERE sequence_name = '%(sequence)s'; + WHILE table_value > seq_value LOOP + SELECT "%(sequence)s".nextval INTO seq_value FROM dual; + END LOOP; +END; +/""" + + def autoinc_sql(self, table, column): + # To simulate auto-incrementing primary keys in Oracle, we have to + # create a sequence and a trigger. + sq_name = self._get_sequence_name(table) + tr_name = self._get_trigger_name(table) + tbl_name = self.quote_name(table) + col_name = self.quote_name(column) + sequence_sql = """ +DECLARE + i INTEGER; +BEGIN + SELECT COUNT(*) INTO i FROM USER_CATALOG + WHERE TABLE_NAME = '%(sq_name)s' AND TABLE_TYPE = 'SEQUENCE'; + IF i = 0 THEN + EXECUTE IMMEDIATE 'CREATE SEQUENCE "%(sq_name)s"'; + END IF; +END; +/""" % locals() + trigger_sql = """ +CREATE OR REPLACE TRIGGER "%(tr_name)s" +BEFORE INSERT ON %(tbl_name)s +FOR EACH ROW +WHEN (new.%(col_name)s IS NULL) + BEGIN + SELECT "%(sq_name)s".nextval + INTO :new.%(col_name)s FROM dual; + END; +/""" % locals() + return sequence_sql, trigger_sql + + def cache_key_culling_sql(self): + return """ + SELECT cache_key + FROM (SELECT cache_key, rank() OVER (ORDER BY cache_key) AS rank FROM %s) + WHERE rank = %%s + 1 + """ + + def date_extract_sql(self, lookup_type, field_name): + if lookup_type == 'week_day': + # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. + return "TO_CHAR(%s, 'D')" % field_name + else: + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm + return "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + + def date_interval_sql(self, timedelta): + """ + Implements the interval functionality for expressions + format for Oracle: + INTERVAL '3 00:03:20.000000' DAY(1) TO SECOND(6) + """ + minutes, seconds = divmod(timedelta.seconds, 60) + hours, minutes = divmod(minutes, 60) + days = str(timedelta.days) + day_precision = len(days) + fmt = "INTERVAL '%s %02d:%02d:%02d.%06d' DAY(%d) TO SECOND(6)" + return fmt % (days, hours, minutes, seconds, timedelta.microseconds, + day_precision), [] + + def date_trunc_sql(self, lookup_type, field_name): + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 + if lookup_type in ('year', 'month'): + return "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) + else: + return "TRUNC(%s)" % field_name + + # Oracle crashes with "ORA-03113: end-of-file on communication channel" + # if the time zone name is passed in parameter. Use interpolation instead. + # https://groups.google.com/forum/#!msg/django-developers/zwQju7hbG78/9l934yelwfsJ + # This regexp matches all time zone names from the zoneinfo database. + _tzname_re = re.compile(r'^[\w/:+-]+$') + + def _convert_field_to_tz(self, field_name, tzname): + if not self._tzname_re.match(tzname): + raise ValueError("Invalid time zone name: %s" % tzname) + # Convert from UTC to local time, returning TIMESTAMP WITH TIME ZONE. + result = "(FROM_TZ(%s, '0:00') AT TIME ZONE '%s')" % (field_name, tzname) + # Extracting from a TIMESTAMP WITH TIME ZONE ignore the time zone. + # Convert to a DATETIME, which is called DATE by Oracle. There's no + # built-in function to do that; the easiest is to go through a string. + result = "TO_CHAR(%s, 'YYYY-MM-DD HH24:MI:SS')" % result + result = "TO_DATE(%s, 'YYYY-MM-DD HH24:MI:SS')" % result + # Re-convert to a TIMESTAMP because EXTRACT only handles the date part + # on DATE values, even though they actually store the time part. + return "CAST(%s AS TIMESTAMP)" % result + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = self._convert_field_to_tz(field_name, tzname) + if lookup_type == 'week_day': + # TO_CHAR(field, 'D') returns an integer from 1-7, where 1=Sunday. + sql = "TO_CHAR(%s, 'D')" % field_name + else: + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions050.htm + sql = "EXTRACT(%s FROM %s)" % (lookup_type.upper(), field_name) + return sql, [] + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + if settings.USE_TZ: + field_name = self._convert_field_to_tz(field_name, tzname) + # http://docs.oracle.com/cd/B19306_01/server.102/b14200/functions230.htm#i1002084 + if lookup_type in ('year', 'month'): + sql = "TRUNC(%s, '%s')" % (field_name, lookup_type.upper()) + elif lookup_type == 'day': + sql = "TRUNC(%s)" % field_name + elif lookup_type == 'hour': + sql = "TRUNC(%s, 'HH24')" % field_name + elif lookup_type == 'minute': + sql = "TRUNC(%s, 'MI')" % field_name + else: + sql = field_name # Cast to DATE removes sub-second precision. + return sql, [] + + def get_db_converters(self, expression): + converters = super(DatabaseOperations, self).get_db_converters(expression) + internal_type = expression.output_field.get_internal_type() + if internal_type == 'TextField': + converters.append(self.convert_textfield_value) + elif internal_type == 'BinaryField': + converters.append(self.convert_binaryfield_value) + elif internal_type in ['BooleanField', 'NullBooleanField']: + converters.append(self.convert_booleanfield_value) + elif internal_type == 'DateField': + converters.append(self.convert_datefield_value) + elif internal_type == 'TimeField': + converters.append(self.convert_timefield_value) + elif internal_type == 'UUIDField': + converters.append(self.convert_uuidfield_value) + converters.append(self.convert_empty_values) + return converters + + def convert_empty_values(self, value, expression, context): + # Oracle stores empty strings as null. We need to undo this in + # order to adhere to the Django convention of using the empty + # string instead of null, but only if the field accepts the + # empty string. + field = expression.output_field + if value is None and field.empty_strings_allowed: + value = '' + if field.get_internal_type() == 'BinaryField': + value = b'' + return value + + def convert_textfield_value(self, value, expression, context): + if isinstance(value, Database.LOB): + value = force_text(value.read()) + return value + + def convert_binaryfield_value(self, value, expression, context): + if isinstance(value, Database.LOB): + value = force_bytes(value.read()) + return value + + def convert_booleanfield_value(self, value, expression, context): + if value in (1, 0): + value = bool(value) + return value + + # cx_Oracle always returns datetime.datetime objects for + # DATE and TIMESTAMP columns, but Django wants to see a + # python datetime.date, .time, or .datetime. + def convert_datefield_value(self, value, expression, context): + if isinstance(value, Database.Timestamp): + return value.date() + + def convert_timefield_value(self, value, expression, context): + if isinstance(value, Database.Timestamp): + value = value.time() + return value + + def convert_uuidfield_value(self, value, expression, context): + if value is not None: + value = uuid.UUID(value) + return value + + def deferrable_sql(self): + return " DEFERRABLE INITIALLY DEFERRED" + + def drop_sequence_sql(self, table): + return "DROP SEQUENCE %s;" % self.quote_name(self._get_sequence_name(table)) + + def fetch_returned_insert_id(self, cursor): + return int(cursor._insert_id_var.getvalue()) + + def field_cast_sql(self, db_type, internal_type): + if db_type and db_type.endswith('LOB'): + return "DBMS_LOB.SUBSTR(%s)" + else: + return "%s" + + def last_executed_query(self, cursor, sql, params): + # http://cx-oracle.sourceforge.net/html/cursor.html#Cursor.statement + # The DB API definition does not define this attribute. + statement = cursor.statement + if statement and six.PY2 and not isinstance(statement, unicode): + statement = statement.decode('utf-8') + # Unlike Psycopg's `query` and MySQLdb`'s `_last_executed`, CxOracle's + # `statement` doesn't contain the query parameters. refs #20010. + return super(DatabaseOperations, self).last_executed_query(cursor, statement, params) + + def last_insert_id(self, cursor, table_name, pk_name): + sq_name = self._get_sequence_name(table_name) + cursor.execute('SELECT "%s".currval FROM dual' % sq_name) + return cursor.fetchone()[0] + + def lookup_cast(self, lookup_type): + if lookup_type in ('iexact', 'icontains', 'istartswith', 'iendswith'): + return "UPPER(%s)" + return "%s" + + def max_in_list_size(self): + return 1000 + + def max_name_length(self): + return 30 + + def prep_for_iexact_query(self, x): + return x + + def process_clob(self, value): + if value is None: + return '' + return force_text(value.read()) + + def quote_name(self, name): + # SQL92 requires delimited (quoted) names to be case-sensitive. When + # not quoted, Oracle has case-insensitive behavior for identifiers, but + # always defaults to uppercase. + # We simplify things by making Oracle identifiers always uppercase. + if not name.startswith('"') and not name.endswith('"'): + name = '"%s"' % truncate_name(name.upper(), self.max_name_length()) + # Oracle puts the query text into a (query % args) construct, so % signs + # in names need to be escaped. The '%%' will be collapsed back to '%' at + # that stage so we aren't really making the name longer here. + name = name.replace('%', '%%') + return name.upper() + + def random_function_sql(self): + return "DBMS_RANDOM.RANDOM" + + def regex_lookup(self, lookup_type): + if lookup_type == 'regex': + match_option = "'c'" + else: + match_option = "'i'" + return 'REGEXP_LIKE(%%s, %%s, %s)' % match_option + + def return_insert_id(self): + return "RETURNING %s INTO %%s", (InsertIdVar(),) + + def savepoint_create_sql(self, sid): + return convert_unicode("SAVEPOINT " + self.quote_name(sid)) + + def savepoint_rollback_sql(self, sid): + return convert_unicode("ROLLBACK TO SAVEPOINT " + self.quote_name(sid)) + + def sql_flush(self, style, tables, sequences, allow_cascade=False): + # Return a list of 'TRUNCATE x;', 'TRUNCATE y;', + # 'TRUNCATE z;'... style SQL statements + if tables: + # Oracle does support TRUNCATE, but it seems to get us into + # FK referential trouble, whereas DELETE FROM table works. + sql = ['%s %s %s;' % ( + style.SQL_KEYWORD('DELETE'), + style.SQL_KEYWORD('FROM'), + style.SQL_FIELD(self.quote_name(table)) + ) for table in tables] + # Since we've just deleted all the rows, running our sequence + # ALTER code will reset the sequence to 0. + sql.extend(self.sequence_reset_by_name_sql(style, sequences)) + return sql + else: + return [] + + def sequence_reset_by_name_sql(self, style, sequences): + sql = [] + for sequence_info in sequences: + sequence_name = self._get_sequence_name(sequence_info['table']) + table_name = self.quote_name(sequence_info['table']) + column_name = self.quote_name(sequence_info['column'] or 'id') + query = self._sequence_reset_sql % { + 'sequence': sequence_name, + 'table': table_name, + 'column': column_name, + } + sql.append(query) + return sql + + def sequence_reset_sql(self, style, model_list): + from django.db import models + output = [] + query = self._sequence_reset_sql + for model in model_list: + for f in model._meta.local_fields: + if isinstance(f, models.AutoField): + table_name = self.quote_name(model._meta.db_table) + sequence_name = self._get_sequence_name(model._meta.db_table) + column_name = self.quote_name(f.column) + output.append(query % {'sequence': sequence_name, + 'table': table_name, + 'column': column_name}) + # Only one AutoField is allowed per model, so don't + # continue to loop + break + for f in model._meta.many_to_many: + if not f.rel.through: + table_name = self.quote_name(f.m2m_db_table()) + sequence_name = self._get_sequence_name(f.m2m_db_table()) + column_name = self.quote_name('id') + output.append(query % {'sequence': sequence_name, + 'table': table_name, + 'column': column_name}) + return output + + def start_transaction_sql(self): + return '' + + def tablespace_sql(self, tablespace, inline=False): + if inline: + return "USING INDEX TABLESPACE %s" % self.quote_name(tablespace) + else: + return "TABLESPACE %s" % self.quote_name(tablespace) + + def value_to_db_date(self, value): + """ + Transform a date value to an object compatible with what is expected + by the backend driver for date columns. + The default implementation transforms the date to text, but that is not + necessary for Oracle. + """ + return value + + def value_to_db_datetime(self, value): + """ + Transform a datetime value to an object compatible with what is expected + by the backend driver for datetime columns. + + If naive datetime is passed assumes that is in UTC. Normally Django + models.DateTimeField makes sure that if USE_TZ is True passed datetime + is timezone aware. + """ + + if value is None: + return None + + # cx_Oracle doesn't support tz-aware datetimes + if timezone.is_aware(value): + if settings.USE_TZ: + value = value.astimezone(timezone.utc).replace(tzinfo=None) + else: + raise ValueError("Oracle backend does not support timezone-aware datetimes when USE_TZ is False.") + + return Oracle_datetime.from_datetime(value) + + def value_to_db_time(self, value): + if value is None: + return None + + if isinstance(value, six.string_types): + return datetime.datetime.strptime(value, '%H:%M:%S') + + # Oracle doesn't support tz-aware times + if timezone.is_aware(value): + raise ValueError("Oracle backend does not support timezone-aware times.") + + return Oracle_datetime(1900, 1, 1, value.hour, value.minute, + value.second, value.microsecond) + + def year_lookup_bounds_for_date_field(self, value): + # Create bounds as real date values + first = datetime.date(value, 1, 1) + last = datetime.date(value, 12, 31) + return [first, last] + + def year_lookup_bounds_for_datetime_field(self, value): + # cx_Oracle doesn't support tz-aware datetimes + bounds = super(DatabaseOperations, self).year_lookup_bounds_for_datetime_field(value) + if settings.USE_TZ: + bounds = [b.astimezone(timezone.utc) for b in bounds] + return [Oracle_datetime.from_datetime(b) for b in bounds] + + def combine_expression(self, connector, sub_expressions): + "Oracle requires special cases for %% and & operators in query expressions" + if connector == '%%': + return 'MOD(%s)' % ','.join(sub_expressions) + elif connector == '&': + return 'BITAND(%s)' % ','.join(sub_expressions) + elif connector == '|': + raise NotImplementedError("Bit-wise or is not supported in Oracle.") + elif connector == '^': + return 'POWER(%s)' % ','.join(sub_expressions) + return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) + + def _get_sequence_name(self, table): + name_length = self.max_name_length() - 3 + return '%s_SQ' % truncate_name(table, name_length).upper() + + def _get_trigger_name(self, table): + name_length = self.max_name_length() - 3 + return '%s_TR' % truncate_name(table, name_length).upper() + + def bulk_insert_sql(self, fields, num_values): + items_sql = "SELECT %s FROM DUAL" % ", ".join(["%s"] * len(fields)) + return " UNION ALL ".join([items_sql] * num_values) diff --git a/django/db/backends/oracle/schema.py b/django/db/backends/oracle/schema.py index 4865c65bb0..6b1dcf5343 100644 --- a/django/db/backends/oracle/schema.py +++ b/django/db/backends/oracle/schema.py @@ -4,7 +4,7 @@ import binascii from django.utils import six from django.utils.text import force_text -from django.db.backends.schema import BaseDatabaseSchemaEditor +from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.utils import DatabaseError diff --git a/django/db/backends/oracle/utils.py b/django/db/backends/oracle/utils.py new file mode 100644 index 0000000000..c63bf39c1c --- /dev/null +++ b/django/db/backends/oracle/utils.py @@ -0,0 +1,42 @@ +import datetime + +from django.utils.encoding import force_bytes, force_text + +from .base import Database + +# Check whether cx_Oracle was compiled with the WITH_UNICODE option if cx_Oracle is pre-5.1. This will +# also be True for cx_Oracle 5.1 and in Python 3.0. See #19606 +if int(Database.version.split('.', 1)[0]) >= 5 and \ + (int(Database.version.split('.', 2)[1]) >= 1 or + not hasattr(Database, 'UNICODE')): + convert_unicode = force_text +else: + convert_unicode = force_bytes + + +class InsertIdVar(object): + """ + A late-binding cursor variable that can be passed to Cursor.execute + as a parameter, in order to receive the id of the row created by an + insert statement. + """ + + def bind_parameter(self, cursor): + param = cursor.cursor.var(Database.NUMBER) + cursor._insert_id_var = param + return param + + +class Oracle_datetime(datetime.datetime): + """ + A datetime object, with an additional class attribute + to tell cx_Oracle to save the microseconds too. + """ + input_size = Database.TIMESTAMP + + @classmethod + def from_datetime(cls, dt): + return Oracle_datetime( + dt.year, dt.month, dt.day, + dt.hour, dt.minute, dt.second, dt.microsecond, + ) diff --git a/django/db/backends/postgresql_psycopg2/base.py b/django/db/backends/postgresql_psycopg2/base.py index 8b45b5d73e..34b2870773 100644 --- a/django/db/backends/postgresql_psycopg2/base.py +++ b/django/db/backends/postgresql_psycopg2/base.py @@ -5,19 +5,11 @@ Requires psycopg 2: http://initd.org/projects/psycopg2 """ from django.conf import settings -from django.db.backends import (BaseDatabaseFeatures, BaseDatabaseWrapper, - BaseDatabaseValidation) -from django.db.backends.postgresql_psycopg2.operations import DatabaseOperations -from django.db.backends.postgresql_psycopg2.client import DatabaseClient -from django.db.backends.postgresql_psycopg2.creation import DatabaseCreation -from django.db.backends.postgresql_psycopg2.version import get_version -from django.db.backends.postgresql_psycopg2.introspection import DatabaseIntrospection -from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor -from django.db.utils import InterfaceError +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.validation import BaseDatabaseValidation from django.utils.encoding import force_str from django.utils.functional import cached_property from django.utils.safestring import SafeText, SafeBytes -from django.utils.timezone import utc try: import psycopg2 as Database @@ -27,6 +19,16 @@ except ImportError as e: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading psycopg2 module: %s" % e) +# Some of these import psycopg2, so import them after checking if it's installed. +from .client import DatabaseClient +from .creation import DatabaseCreation +from .features import DatabaseFeatures +from .introspection import DatabaseIntrospection +from .operations import DatabaseOperations +from .schema import DatabaseSchemaEditor +from .utils import utc_tzinfo_factory +from .version import get_version + DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError @@ -37,38 +39,6 @@ psycopg2.extensions.register_adapter(SafeText, psycopg2.extensions.QuotedString) psycopg2.extras.register_uuid() -def utc_tzinfo_factory(offset): - if offset != 0: - raise AssertionError("database connection isn't set to UTC") - return utc - - -class DatabaseFeatures(BaseDatabaseFeatures): - needs_datetime_string_cast = False - can_return_id_from_insert = True - has_real_datatype = True - has_native_duration_field = True - driver_supports_timedelta_args = True - can_defer_constraint_checks = True - has_select_for_update = True - has_select_for_update_nowait = True - has_bulk_insert = True - uses_savepoints = True - can_release_savepoints = True - supports_tablespaces = True - supports_transactions = True - can_introspect_autofield = True - can_introspect_ip_address_field = True - can_introspect_small_integer_field = True - can_distinct_on_fields = True - can_rollback_ddl = True - supports_combined_alters = True - nulls_order_largest = True - closed_cursor_error_class = InterfaceError - has_case_insensitive_like = False - requires_sqlparse_for_splitting = False - - class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'postgresql' # This dictionary maps Field objects to their associated PostgreSQL column diff --git a/django/db/backends/postgresql_psycopg2/client.py b/django/db/backends/postgresql_psycopg2/client.py index b4189da0f6..aa60e58943 100644 --- a/django/db/backends/postgresql_psycopg2/client.py +++ b/django/db/backends/postgresql_psycopg2/client.py @@ -1,6 +1,6 @@ import subprocess -from django.db.backends import BaseDatabaseClient +from django.db.backends.base.client import BaseDatabaseClient class DatabaseClient(BaseDatabaseClient): diff --git a/django/db/backends/postgresql_psycopg2/creation.py b/django/db/backends/postgresql_psycopg2/creation.py index 353d0b794f..128c2ce95a 100644 --- a/django/db/backends/postgresql_psycopg2/creation.py +++ b/django/db/backends/postgresql_psycopg2/creation.py @@ -1,4 +1,4 @@ -from django.db.backends.creation import BaseDatabaseCreation +from django.db.backends.base.creation import BaseDatabaseCreation from django.db.backends.utils import truncate_name diff --git a/django/db/backends/postgresql_psycopg2/features.py b/django/db/backends/postgresql_psycopg2/features.py new file mode 100644 index 0000000000..64acd0570a --- /dev/null +++ b/django/db/backends/postgresql_psycopg2/features.py @@ -0,0 +1,28 @@ +from django.db.backends.base.features import BaseDatabaseFeatures +from django.db.utils import InterfaceError + + +class DatabaseFeatures(BaseDatabaseFeatures): + needs_datetime_string_cast = False + can_return_id_from_insert = True + has_real_datatype = True + has_native_duration_field = True + driver_supports_timedelta_args = True + can_defer_constraint_checks = True + has_select_for_update = True + has_select_for_update_nowait = True + has_bulk_insert = True + uses_savepoints = True + can_release_savepoints = True + supports_tablespaces = True + supports_transactions = True + can_introspect_autofield = True + can_introspect_ip_address_field = True + can_introspect_small_integer_field = True + can_distinct_on_fields = True + can_rollback_ddl = True + supports_combined_alters = True + nulls_order_largest = True + closed_cursor_error_class = InterfaceError + has_case_insensitive_like = False + requires_sqlparse_for_splitting = False diff --git a/django/db/backends/postgresql_psycopg2/introspection.py b/django/db/backends/postgresql_psycopg2/introspection.py index d3fe3d2200..7236f5121f 100644 --- a/django/db/backends/postgresql_psycopg2/introspection.py +++ b/django/db/backends/postgresql_psycopg2/introspection.py @@ -1,7 +1,9 @@ from __future__ import unicode_literals from collections import namedtuple -from django.db.backends import BaseDatabaseIntrospection, FieldInfo, TableInfo +from django.db.backends.base.introspection import ( + BaseDatabaseIntrospection, FieldInfo, TableInfo, +) from django.utils.encoding import force_text diff --git a/django/db/backends/postgresql_psycopg2/operations.py b/django/db/backends/postgresql_psycopg2/operations.py index 31cbe5919f..8e90a4020b 100644 --- a/django/db/backends/postgresql_psycopg2/operations.py +++ b/django/db/backends/postgresql_psycopg2/operations.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals from django.conf import settings -from django.db.backends import BaseDatabaseOperations +from django.db.backends.base.operations import BaseDatabaseOperations class DatabaseOperations(BaseDatabaseOperations): diff --git a/django/db/backends/postgresql_psycopg2/schema.py b/django/db/backends/postgresql_psycopg2/schema.py index 7b479c28b0..c75123a349 100644 --- a/django/db/backends/postgresql_psycopg2/schema.py +++ b/django/db/backends/postgresql_psycopg2/schema.py @@ -1,4 +1,6 @@ -from django.db.backends.schema import BaseDatabaseSchemaEditor +from django.db.backends.base.schema import BaseDatabaseSchemaEditor + +import psycopg2 class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): @@ -10,8 +12,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_create_text_index = "CREATE INDEX %(name)s ON %(table)s (%(columns)s text_pattern_ops)%(extra)s" def quote_value(self, value): - # Inner import so backend fails nicely if it's not present - import psycopg2 return psycopg2.extensions.adapt(value) def _model_indexes_sql(self, model): diff --git a/django/db/backends/postgresql_psycopg2/utils.py b/django/db/backends/postgresql_psycopg2/utils.py new file mode 100644 index 0000000000..2c03ab36cd --- /dev/null +++ b/django/db/backends/postgresql_psycopg2/utils.py @@ -0,0 +1,7 @@ +from django.utils.timezone import utc + + +def utc_tzinfo_factory(offset): + if offset != 0: + raise AssertionError("database connection isn't set to UTC") + return utc diff --git a/django/db/backends/sqlite3/base.py b/django/db/backends/sqlite3/base.py index 801351336d..a71bae99b2 100644 --- a/django/db/backends/sqlite3/base.py +++ b/django/db/backends/sqlite3/base.py @@ -9,26 +9,22 @@ from __future__ import unicode_literals import datetime import decimal import re -import sys -import uuid import warnings from django.conf import settings from django.db import utils -from django.db.backends import (utils as backend_utils, BaseDatabaseFeatures, - BaseDatabaseOperations, BaseDatabaseWrapper, BaseDatabaseValidation) -from django.db.backends.sqlite3.client import DatabaseClient -from django.db.backends.sqlite3.creation import DatabaseCreation -from django.db.backends.sqlite3.introspection import DatabaseIntrospection -from django.db.backends.sqlite3.schema import DatabaseSchemaEditor -from django.db.models import fields, aggregates -from django.utils.dateparse import parse_date, parse_datetime, parse_time, parse_duration -from django.utils.duration import duration_string +from django.db.backends import utils as backend_utils +from django.db.backends.base.base import BaseDatabaseWrapper +from django.db.backends.base.validation import BaseDatabaseValidation +from django.utils import six, timezone +from django.utils.dateparse import parse_date, parse_duration, parse_time from django.utils.encoding import force_text -from django.utils.functional import cached_property from django.utils.safestring import SafeBytes -from django.utils import six -from django.utils import timezone + +try: + import pytz +except ImportError: + pytz = None try: try: @@ -39,23 +35,19 @@ except ImportError as exc: from django.core.exceptions import ImproperlyConfigured raise ImproperlyConfigured("Error loading either pysqlite2 or sqlite3 modules (tried in that order): %s" % exc) -try: - import pytz -except ImportError: - pytz = None +# Some of these import sqlite3, so import them after checking if it's installed. +from .client import DatabaseClient +from .creation import DatabaseCreation +from .features import DatabaseFeatures +from .introspection import DatabaseIntrospection +from .operations import DatabaseOperations +from .schema import DatabaseSchemaEditor +from .utils import parse_datetime_with_timezone_support DatabaseError = Database.DatabaseError IntegrityError = Database.IntegrityError -def parse_datetime_with_timezone_support(value): - dt = parse_datetime(value) - # Confirm that dt is naive before overwriting its tzinfo. - if dt is not None and settings.USE_TZ and timezone.is_naive(dt): - dt = dt.replace(tzinfo=timezone.utc) - return dt - - def adapt_datetime_with_timezone_support(value): # Equivalent to DateTimeField.get_db_prep_value. Used only by raw SQL. if settings.USE_TZ: @@ -91,250 +83,6 @@ if six.PY2: Database.register_adapter(SafeBytes, lambda s: s.decode('utf-8')) -class DatabaseFeatures(BaseDatabaseFeatures): - # SQLite cannot handle us only partially reading from a cursor's result set - # and then writing the same rows to the database in another cursor. This - # setting ensures we always read result sets fully into memory all in one - # go. - can_use_chunked_reads = False - test_db_allows_multiple_connections = False - supports_unspecified_pk = True - supports_timezones = False - supports_1000_query_parameters = False - supports_mixed_date_datetime_comparisons = False - has_bulk_insert = True - can_combine_inserts_with_and_without_auto_increment_pk = False - supports_foreign_keys = False - supports_column_check_constraints = False - autocommits_when_autocommit_is_off = True - can_introspect_decimal_field = False - can_introspect_positive_integer_field = True - can_introspect_small_integer_field = True - supports_transactions = True - atomic_transactions = False - can_rollback_ddl = True - supports_paramstyle_pyformat = False - supports_sequence_reset = False - - @cached_property - def uses_savepoints(self): - return Database.sqlite_version_info >= (3, 6, 8) - - @cached_property - def can_release_savepoints(self): - return self.uses_savepoints - - @cached_property - def can_share_in_memory_db(self): - return ( - sys.version_info[:2] >= (3, 4) and - Database.__name__ == 'sqlite3.dbapi2' and - Database.sqlite_version_info >= (3, 7, 13) - ) - - @cached_property - def supports_stddev(self): - """Confirm support for STDDEV and related stats functions - - SQLite supports STDDEV as an extension package; so - connection.ops.check_aggregate_support() can't unilaterally - rule out support for STDDEV. We need to manually check - whether the call works. - """ - with self.connection.cursor() as cursor: - cursor.execute('CREATE TABLE STDDEV_TEST (X INT)') - try: - cursor.execute('SELECT STDDEV(*) FROM STDDEV_TEST') - has_support = True - except utils.DatabaseError: - has_support = False - cursor.execute('DROP TABLE STDDEV_TEST') - return has_support - - @cached_property - def has_zoneinfo_database(self): - return pytz is not None - - -class DatabaseOperations(BaseDatabaseOperations): - def bulk_batch_size(self, fields, objs): - """ - SQLite has a compile-time default (SQLITE_LIMIT_VARIABLE_NUMBER) of - 999 variables per query. - - If there is just single field to insert, then we can hit another - limit, SQLITE_MAX_COMPOUND_SELECT which defaults to 500. - """ - limit = 999 if len(fields) > 1 else 500 - return (limit // len(fields)) if len(fields) > 0 else len(objs) - - def check_aggregate_support(self, aggregate): - bad_fields = (fields.DateField, fields.DateTimeField, fields.TimeField) - bad_aggregates = (aggregates.Sum, aggregates.Avg, - aggregates.Variance, aggregates.StdDev) - if aggregate.refs_field(bad_aggregates, bad_fields): - raise NotImplementedError( - 'You cannot use Sum, Avg, StdDev and Variance aggregations ' - 'on date/time fields in sqlite3 ' - 'since date/time is saved as text.') - - def date_extract_sql(self, lookup_type, field_name): - # sqlite doesn't support extract, so we fake it with the user-defined - # function django_date_extract that's registered in connect(). Note that - # single quotes are used because this is a string (and could otherwise - # cause a collision with a field name). - return "django_date_extract('%s', %s)" % (lookup_type.lower(), field_name) - - def date_interval_sql(self, timedelta): - return "'%s'" % duration_string(timedelta), [] - - def format_for_duration_arithmetic(self, sql): - """Do nothing here, we will handle it in the custom function.""" - return sql - - def date_trunc_sql(self, lookup_type, field_name): - # sqlite doesn't support DATE_TRUNC, so we fake it with a user-defined - # function django_date_trunc that's registered in connect(). Note that - # single quotes are used because this is a string (and could otherwise - # cause a collision with a field name). - return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name) - - def datetime_extract_sql(self, lookup_type, field_name, tzname): - # Same comment as in date_extract_sql. - if settings.USE_TZ: - if pytz is None: - from django.core.exceptions import ImproperlyConfigured - raise ImproperlyConfigured("This query requires pytz, " - "but it isn't installed.") - return "django_datetime_extract('%s', %s, %%s)" % ( - lookup_type.lower(), field_name), [tzname] - - def datetime_trunc_sql(self, lookup_type, field_name, tzname): - # Same comment as in date_trunc_sql. - if settings.USE_TZ: - if pytz is None: - from django.core.exceptions import ImproperlyConfigured - raise ImproperlyConfigured("This query requires pytz, " - "but it isn't installed.") - return "django_datetime_trunc('%s', %s, %%s)" % ( - lookup_type.lower(), field_name), [tzname] - - def drop_foreignkey_sql(self): - return "" - - def pk_default_value(self): - return "NULL" - - def quote_name(self, name): - if name.startswith('"') and name.endswith('"'): - return name # Quoting once is enough. - return '"%s"' % name - - def no_limit_value(self): - return -1 - - def sql_flush(self, style, tables, sequences, allow_cascade=False): - # NB: The generated SQL below is specific to SQLite - # Note: The DELETE FROM... SQL generated below works for SQLite databases - # because constraints don't exist - sql = ['%s %s %s;' % ( - style.SQL_KEYWORD('DELETE'), - style.SQL_KEYWORD('FROM'), - style.SQL_FIELD(self.quote_name(table)) - ) for table in tables] - # Note: No requirement for reset of auto-incremented indices (cf. other - # sql_flush() implementations). Just return SQL at this point - return sql - - def value_to_db_datetime(self, value): - if value is None: - return None - - # SQLite doesn't support tz-aware datetimes - if timezone.is_aware(value): - if settings.USE_TZ: - value = value.astimezone(timezone.utc).replace(tzinfo=None) - else: - raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.") - - return six.text_type(value) - - def value_to_db_time(self, value): - if value is None: - return None - - # SQLite doesn't support tz-aware datetimes - if timezone.is_aware(value): - raise ValueError("SQLite backend does not support timezone-aware times.") - - return six.text_type(value) - - def get_db_converters(self, expression): - converters = super(DatabaseOperations, self).get_db_converters(expression) - internal_type = expression.output_field.get_internal_type() - if internal_type == 'DateTimeField': - converters.append(self.convert_datetimefield_value) - elif internal_type == 'DateField': - converters.append(self.convert_datefield_value) - elif internal_type == 'TimeField': - converters.append(self.convert_timefield_value) - elif internal_type == 'DecimalField': - converters.append(self.convert_decimalfield_value) - elif internal_type == 'UUIDField': - converters.append(self.convert_uuidfield_value) - return converters - - def convert_decimalfield_value(self, value, expression, context): - return backend_utils.typecast_decimal(expression.output_field.format_number(value)) - - def convert_datefield_value(self, value, expression, context): - if value is not None and not isinstance(value, datetime.date): - value = parse_date(value) - return value - - def convert_datetimefield_value(self, value, expression, context): - if value is not None and not isinstance(value, datetime.datetime): - value = parse_datetime_with_timezone_support(value) - return value - - def convert_timefield_value(self, value, expression, context): - if value is not None and not isinstance(value, datetime.time): - value = parse_time(value) - return value - - def convert_uuidfield_value(self, value, expression, context): - if value is not None: - value = uuid.UUID(value) - return value - - def bulk_insert_sql(self, fields, num_values): - res = [] - res.append("SELECT %s" % ", ".join( - "%%s AS %s" % self.quote_name(f.column) for f in fields - )) - res.extend(["UNION ALL SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1)) - return " ".join(res) - - def combine_expression(self, connector, sub_expressions): - # SQLite doesn't have a power function, so we fake it with a - # user-defined function django_power that's registered in connect(). - if connector == '^': - return 'django_power(%s)' % ','.join(sub_expressions) - return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) - - def combine_duration_expression(self, connector, sub_expressions): - if connector not in ['+', '-']: - raise utils.DatabaseError('Invalid connector for timedelta: %s.' % connector) - fn_params = ["'%s'" % connector] + sub_expressions - if len(fn_params) > 3: - raise ValueError('Too many params for timedelta operations.') - return "django_format_dtdelta(%s)" % ', '.join(fn_params) - - def integer_field_range(self, internal_type): - # SQLite doesn't enforce any integer constraints - return (None, None) - - class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'sqlite' # SQLite doesn't actually support most of these types, but it "does the right diff --git a/django/db/backends/sqlite3/client.py b/django/db/backends/sqlite3/client.py index 6553dced65..2e6477a1ca 100644 --- a/django/db/backends/sqlite3/client.py +++ b/django/db/backends/sqlite3/client.py @@ -1,6 +1,6 @@ import subprocess -from django.db.backends import BaseDatabaseClient +from django.db.backends.base.client import BaseDatabaseClient class DatabaseClient(BaseDatabaseClient): diff --git a/django/db/backends/sqlite3/creation.py b/django/db/backends/sqlite3/creation.py index 3f1372630a..3632e656e0 100644 --- a/django/db/backends/sqlite3/creation.py +++ b/django/db/backends/sqlite3/creation.py @@ -2,7 +2,7 @@ import os import sys from django.core.exceptions import ImproperlyConfigured -from django.db.backends.creation import BaseDatabaseCreation +from django.db.backends.base.creation import BaseDatabaseCreation from django.utils.six.moves import input diff --git a/django/db/backends/sqlite3/features.py b/django/db/backends/sqlite3/features.py new file mode 100644 index 0000000000..fa5a002603 --- /dev/null +++ b/django/db/backends/sqlite3/features.py @@ -0,0 +1,79 @@ +from __future__ import unicode_literals + +import sys + +from django.db import utils +from django.db.backends.base.features import BaseDatabaseFeatures +from django.utils.functional import cached_property + +from .base import Database + +try: + import pytz +except ImportError: + pytz = None + + +class DatabaseFeatures(BaseDatabaseFeatures): + # SQLite cannot handle us only partially reading from a cursor's result set + # and then writing the same rows to the database in another cursor. This + # setting ensures we always read result sets fully into memory all in one + # go. + can_use_chunked_reads = False + test_db_allows_multiple_connections = False + supports_unspecified_pk = True + supports_timezones = False + supports_1000_query_parameters = False + supports_mixed_date_datetime_comparisons = False + has_bulk_insert = True + can_combine_inserts_with_and_without_auto_increment_pk = False + supports_foreign_keys = False + supports_column_check_constraints = False + autocommits_when_autocommit_is_off = True + can_introspect_decimal_field = False + can_introspect_positive_integer_field = True + can_introspect_small_integer_field = True + supports_transactions = True + atomic_transactions = False + can_rollback_ddl = True + supports_paramstyle_pyformat = False + supports_sequence_reset = False + + @cached_property + def uses_savepoints(self): + return Database.sqlite_version_info >= (3, 6, 8) + + @cached_property + def can_release_savepoints(self): + return self.uses_savepoints + + @cached_property + def can_share_in_memory_db(self): + return ( + sys.version_info[:2] >= (3, 4) and + Database.__name__ == 'sqlite3.dbapi2' and + Database.sqlite_version_info >= (3, 7, 13) + ) + + @cached_property + def supports_stddev(self): + """Confirm support for STDDEV and related stats functions + + SQLite supports STDDEV as an extension package; so + connection.ops.check_aggregate_support() can't unilaterally + rule out support for STDDEV. We need to manually check + whether the call works. + """ + with self.connection.cursor() as cursor: + cursor.execute('CREATE TABLE STDDEV_TEST (X INT)') + try: + cursor.execute('SELECT STDDEV(*) FROM STDDEV_TEST') + has_support = True + except utils.DatabaseError: + has_support = False + cursor.execute('DROP TABLE STDDEV_TEST') + return has_support + + @cached_property + def has_zoneinfo_database(self): + return pytz is not None diff --git a/django/db/backends/sqlite3/introspection.py b/django/db/backends/sqlite3/introspection.py index 8dfa7e0ec0..13b658bd6a 100644 --- a/django/db/backends/sqlite3/introspection.py +++ b/django/db/backends/sqlite3/introspection.py @@ -1,6 +1,8 @@ import re -from django.db.backends import BaseDatabaseIntrospection, FieldInfo, TableInfo +from django.db.backends.base.introspection import ( + BaseDatabaseIntrospection, FieldInfo, TableInfo, +) field_size_re = re.compile(r'^\s*(?:var)?char\s*\(\s*(\d+)\s*\)\s*$') diff --git a/django/db/backends/sqlite3/operations.py b/django/db/backends/sqlite3/operations.py new file mode 100644 index 0000000000..ad05f88585 --- /dev/null +++ b/django/db/backends/sqlite3/operations.py @@ -0,0 +1,198 @@ +from __future__ import unicode_literals + +import datetime +import uuid + +from django.conf import settings +from django.core.exceptions import ImproperlyConfigured +from django.db import utils +from django.db.backends import utils as backend_utils +from django.db.backends.base.operations import BaseDatabaseOperations +from django.db.models import fields, aggregates +from django.utils.dateparse import parse_date, parse_time +from django.utils.duration import duration_string +from django.utils import six, timezone + +from .utils import parse_datetime_with_timezone_support + +try: + import pytz +except ImportError: + pytz = None + + +class DatabaseOperations(BaseDatabaseOperations): + def bulk_batch_size(self, fields, objs): + """ + SQLite has a compile-time default (SQLITE_LIMIT_VARIABLE_NUMBER) of + 999 variables per query. + + If there is just single field to insert, then we can hit another + limit, SQLITE_MAX_COMPOUND_SELECT which defaults to 500. + """ + limit = 999 if len(fields) > 1 else 500 + return (limit // len(fields)) if len(fields) > 0 else len(objs) + + def check_aggregate_support(self, aggregate): + bad_fields = (fields.DateField, fields.DateTimeField, fields.TimeField) + bad_aggregates = (aggregates.Sum, aggregates.Avg, + aggregates.Variance, aggregates.StdDev) + if aggregate.refs_field(bad_aggregates, bad_fields): + raise NotImplementedError( + 'You cannot use Sum, Avg, StdDev and Variance aggregations ' + 'on date/time fields in sqlite3 ' + 'since date/time is saved as text.') + + def date_extract_sql(self, lookup_type, field_name): + # sqlite doesn't support extract, so we fake it with the user-defined + # function django_date_extract that's registered in connect(). Note that + # single quotes are used because this is a string (and could otherwise + # cause a collision with a field name). + return "django_date_extract('%s', %s)" % (lookup_type.lower(), field_name) + + def date_interval_sql(self, timedelta): + return "'%s'" % duration_string(timedelta), [] + + def format_for_duration_arithmetic(self, sql): + """Do nothing here, we will handle it in the custom function.""" + return sql + + def date_trunc_sql(self, lookup_type, field_name): + # sqlite doesn't support DATE_TRUNC, so we fake it with a user-defined + # function django_date_trunc that's registered in connect(). Note that + # single quotes are used because this is a string (and could otherwise + # cause a collision with a field name). + return "django_date_trunc('%s', %s)" % (lookup_type.lower(), field_name) + + def datetime_extract_sql(self, lookup_type, field_name, tzname): + # Same comment as in date_extract_sql. + if settings.USE_TZ: + if pytz is None: + raise ImproperlyConfigured("This query requires pytz, " + "but it isn't installed.") + return "django_datetime_extract('%s', %s, %%s)" % ( + lookup_type.lower(), field_name), [tzname] + + def datetime_trunc_sql(self, lookup_type, field_name, tzname): + # Same comment as in date_trunc_sql. + if settings.USE_TZ: + if pytz is None: + raise ImproperlyConfigured("This query requires pytz, " + "but it isn't installed.") + return "django_datetime_trunc('%s', %s, %%s)" % ( + lookup_type.lower(), field_name), [tzname] + + def drop_foreignkey_sql(self): + return "" + + def pk_default_value(self): + return "NULL" + + def quote_name(self, name): + if name.startswith('"') and name.endswith('"'): + return name # Quoting once is enough. + return '"%s"' % name + + def no_limit_value(self): + return -1 + + def sql_flush(self, style, tables, sequences, allow_cascade=False): + # NB: The generated SQL below is specific to SQLite + # Note: The DELETE FROM... SQL generated below works for SQLite databases + # because constraints don't exist + sql = ['%s %s %s;' % ( + style.SQL_KEYWORD('DELETE'), + style.SQL_KEYWORD('FROM'), + style.SQL_FIELD(self.quote_name(table)) + ) for table in tables] + # Note: No requirement for reset of auto-incremented indices (cf. other + # sql_flush() implementations). Just return SQL at this point + return sql + + def value_to_db_datetime(self, value): + if value is None: + return None + + # SQLite doesn't support tz-aware datetimes + if timezone.is_aware(value): + if settings.USE_TZ: + value = value.astimezone(timezone.utc).replace(tzinfo=None) + else: + raise ValueError("SQLite backend does not support timezone-aware datetimes when USE_TZ is False.") + + return six.text_type(value) + + def value_to_db_time(self, value): + if value is None: + return None + + # SQLite doesn't support tz-aware datetimes + if timezone.is_aware(value): + raise ValueError("SQLite backend does not support timezone-aware times.") + + return six.text_type(value) + + def get_db_converters(self, expression): + converters = super(DatabaseOperations, self).get_db_converters(expression) + internal_type = expression.output_field.get_internal_type() + if internal_type == 'DateTimeField': + converters.append(self.convert_datetimefield_value) + elif internal_type == 'DateField': + converters.append(self.convert_datefield_value) + elif internal_type == 'TimeField': + converters.append(self.convert_timefield_value) + elif internal_type == 'DecimalField': + converters.append(self.convert_decimalfield_value) + elif internal_type == 'UUIDField': + converters.append(self.convert_uuidfield_value) + return converters + + def convert_decimalfield_value(self, value, expression, context): + return backend_utils.typecast_decimal(expression.output_field.format_number(value)) + + def convert_datefield_value(self, value, expression, context): + if value is not None and not isinstance(value, datetime.date): + value = parse_date(value) + return value + + def convert_datetimefield_value(self, value, expression, context): + if value is not None and not isinstance(value, datetime.datetime): + value = parse_datetime_with_timezone_support(value) + return value + + def convert_timefield_value(self, value, expression, context): + if value is not None and not isinstance(value, datetime.time): + value = parse_time(value) + return value + + def convert_uuidfield_value(self, value, expression, context): + if value is not None: + value = uuid.UUID(value) + return value + + def bulk_insert_sql(self, fields, num_values): + res = [] + res.append("SELECT %s" % ", ".join( + "%%s AS %s" % self.quote_name(f.column) for f in fields + )) + res.extend(["UNION ALL SELECT %s" % ", ".join(["%s"] * len(fields))] * (num_values - 1)) + return " ".join(res) + + def combine_expression(self, connector, sub_expressions): + # SQLite doesn't have a power function, so we fake it with a + # user-defined function django_power that's registered in connect(). + if connector == '^': + return 'django_power(%s)' % ','.join(sub_expressions) + return super(DatabaseOperations, self).combine_expression(connector, sub_expressions) + + def combine_duration_expression(self, connector, sub_expressions): + if connector not in ['+', '-']: + raise utils.DatabaseError('Invalid connector for timedelta: %s.' % connector) + fn_params = ["'%s'" % connector] + sub_expressions + if len(fn_params) > 3: + raise ValueError('Too many params for timedelta operations.') + return "django_format_dtdelta(%s)" % ', '.join(fn_params) + + def integer_field_range(self, internal_type): + # SQLite doesn't enforce any integer constraints + return (None, None) diff --git a/django/db/backends/sqlite3/schema.py b/django/db/backends/sqlite3/schema.py index e0433b0c13..814b94ffee 100644 --- a/django/db/backends/sqlite3/schema.py +++ b/django/db/backends/sqlite3/schema.py @@ -1,10 +1,13 @@ import codecs import copy from decimal import Decimal -from django.utils import six + from django.apps.registry import Apps -from django.db.backends.schema import BaseDatabaseSchemaEditor +from django.db.backends.base.schema import BaseDatabaseSchemaEditor from django.db.models.fields.related import ManyToManyField +from django.utils import six + +import _sqlite3 class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): @@ -13,8 +16,6 @@ class DatabaseSchemaEditor(BaseDatabaseSchemaEditor): sql_create_inline_fk = "REFERENCES %(to_table)s (%(to_column)s)" def quote_value(self, value): - # Inner import to allow nice failure for backend if not present - import _sqlite3 try: value = _sqlite3.adapt(value) except _sqlite3.ProgrammingError: diff --git a/django/db/backends/sqlite3/utils.py b/django/db/backends/sqlite3/utils.py new file mode 100644 index 0000000000..6a8b3b8943 --- /dev/null +++ b/django/db/backends/sqlite3/utils.py @@ -0,0 +1,11 @@ +from django.conf import settings +from django.utils import timezone +from django.utils.dateparse import parse_datetime + + +def parse_datetime_with_timezone_support(value): + dt = parse_datetime(value) + # Confirm that dt is naive before overwriting its tzinfo. + if dt is not None and settings.USE_TZ and timezone.is_naive(dt): + dt = dt.replace(tzinfo=timezone.utc) + return dt diff --git a/docs/ref/migration-operations.txt b/docs/ref/migration-operations.txt index 8e13974cfe..76e8cae8b8 100644 --- a/docs/ref/migration-operations.txt +++ b/docs/ref/migration-operations.txt @@ -272,7 +272,7 @@ if supplied) should be callable objects that accept two arguments; the first is an instance of ``django.apps.registry.Apps`` containing historical models that match the operation's place in the project history, and the second is an instance of :class:`SchemaEditor -`. +`. The optional ``hints`` argument will be passed as ``**hints`` to the :meth:`allow_migrate` method of database routers to assist them in making a diff --git a/docs/ref/schema-editor.txt b/docs/ref/schema-editor.txt index 94456d060e..aa8cd2ef65 100644 --- a/docs/ref/schema-editor.txt +++ b/docs/ref/schema-editor.txt @@ -2,7 +2,7 @@ ``SchemaEditor`` ================ -.. module:: django.db.backends.schema +.. module:: django.db.backends.base.schema .. class:: BaseDatabaseSchemaEditor diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index 95ce56f241..cb2816723f 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -942,6 +942,19 @@ Database backend API The following changes to the database backend API are documented to assist those writing third-party backends in updating their code: +* ``BaseDatabaseXXX`` classes have been moved to ``django.db.backends.base``. + Please import them from the new locations:: + + from django.db.backends.base.base import BaseDatabaseWrapper + from django.db.backends.base.client import BaseDatabaseClient + from django.db.backends.base.creation import BaseDatabaseCreation + from django.db.backends.base.features import BaseDatabaseFeatures + from django.db.backends.base.introspection import BaseDatabaseIntrospection + from django.db.backends.base.introspection import FieldInfo, TableInfo + from django.db.backends.base.operations import BaseDatabaseOperations + from django.db.backends.base.schema import BaseDatabaseSchemaEditor + from django.db.backends.base.validation import BaseDatabaseValidation + * The ``data_types``, ``data_types_suffix``, and ``data_type_check_constraints`` attributes have moved from the ``DatabaseCreation`` class to ``DatabaseWrapper``. diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 9eeb90335c..3d4185e762 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -15,7 +15,7 @@ from django.core.exceptions import ImproperlyConfigured from django.core.management.color import no_style from django.db import (connection, connections, DEFAULT_DB_ALIAS, DatabaseError, IntegrityError, reset_queries, transaction) -from django.db.backends import BaseDatabaseWrapper +from django.db.backends.base.base import BaseDatabaseWrapper from django.db.backends.signals import connection_created from django.db.backends.postgresql_psycopg2 import version as pg_version from django.db.backends.utils import format_number, CursorWrapper