Fixed #32234 -- Made inspectdb inform about composite primary keys.
This commit is contained in:
parent
ebf25555bb
commit
295249c901
|
@ -106,11 +106,14 @@ class Command(BaseCommand):
|
||||||
)
|
)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
constraints = {}
|
constraints = {}
|
||||||
primary_key_column = (
|
primary_key_columns = (
|
||||||
connection.introspection.get_primary_key_column(
|
connection.introspection.get_primary_key_columns(
|
||||||
cursor, table_name
|
cursor, table_name
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
primary_key_column = (
|
||||||
|
primary_key_columns[0] if primary_key_columns else None
|
||||||
|
)
|
||||||
unique_columns = [
|
unique_columns = [
|
||||||
c["columns"][0]
|
c["columns"][0]
|
||||||
for c in constraints.values()
|
for c in constraints.values()
|
||||||
|
@ -150,6 +153,12 @@ class Command(BaseCommand):
|
||||||
# Add primary_key and unique, if necessary.
|
# Add primary_key and unique, if necessary.
|
||||||
if column_name == primary_key_column:
|
if column_name == primary_key_column:
|
||||||
extra_params["primary_key"] = True
|
extra_params["primary_key"] = True
|
||||||
|
if len(primary_key_columns) > 1:
|
||||||
|
comment_notes.append(
|
||||||
|
"The composite primary key (%s) found, that is not "
|
||||||
|
"supported. The first column is selected."
|
||||||
|
% ", ".join(primary_key_columns)
|
||||||
|
)
|
||||||
elif column_name in unique_columns:
|
elif column_name in unique_columns:
|
||||||
extra_params["unique"] = True
|
extra_params["unique"] = True
|
||||||
|
|
||||||
|
|
|
@ -260,6 +260,10 @@ class BaseDatabaseFeatures:
|
||||||
create_test_procedure_without_params_sql = None
|
create_test_procedure_without_params_sql = None
|
||||||
create_test_procedure_with_int_param_sql = None
|
create_test_procedure_with_int_param_sql = None
|
||||||
|
|
||||||
|
# SQL to create a table with a composite primary key for use by the Django
|
||||||
|
# test suite.
|
||||||
|
create_test_table_with_composite_primary_key = None
|
||||||
|
|
||||||
# Does the backend support keyword parameters for cursor.callproc()?
|
# Does the backend support keyword parameters for cursor.callproc()?
|
||||||
supports_callproc_kwargs = False
|
supports_callproc_kwargs = False
|
||||||
|
|
||||||
|
|
|
@ -177,9 +177,14 @@ class BaseDatabaseIntrospection:
|
||||||
"""
|
"""
|
||||||
Return the name of the primary key column for the given table.
|
Return the name of the primary key column for the given table.
|
||||||
"""
|
"""
|
||||||
|
columns = self.get_primary_key_columns(cursor, table_name)
|
||||||
|
return columns[0] if columns else None
|
||||||
|
|
||||||
|
def get_primary_key_columns(self, cursor, table_name):
|
||||||
|
"""Return a list of primary key columns for the given table."""
|
||||||
for constraint in self.get_constraints(cursor, table_name).values():
|
for constraint in self.get_constraints(cursor, table_name).values():
|
||||||
if constraint["primary_key"]:
|
if constraint["primary_key"]:
|
||||||
return constraint["columns"][0]
|
return constraint["columns"]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_constraints(self, cursor, table_name):
|
def get_constraints(self, cursor, table_name):
|
||||||
|
|
|
@ -39,6 +39,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
SET V_I = P_I;
|
SET V_I = P_I;
|
||||||
END;
|
END;
|
||||||
"""
|
"""
|
||||||
|
create_test_table_with_composite_primary_key = """
|
||||||
|
CREATE TABLE test_table_composite_pk (
|
||||||
|
column_1 INTEGER NOT NULL,
|
||||||
|
column_2 INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(column_1, column_2)
|
||||||
|
)
|
||||||
|
"""
|
||||||
# Neither MySQL nor MariaDB support partial indexes.
|
# Neither MySQL nor MariaDB support partial indexes.
|
||||||
supports_partial_indexes = False
|
supports_partial_indexes = False
|
||||||
# COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
|
# COLLATE must be wrapped in parentheses because MySQL treats COLLATE as an
|
||||||
|
|
|
@ -54,6 +54,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
V_I := P_I;
|
V_I := P_I;
|
||||||
END;
|
END;
|
||||||
"""
|
"""
|
||||||
|
create_test_table_with_composite_primary_key = """
|
||||||
|
CREATE TABLE test_table_composite_pk (
|
||||||
|
column_1 NUMBER(11) NOT NULL,
|
||||||
|
column_2 NUMBER(11) NOT NULL,
|
||||||
|
PRIMARY KEY (column_1, column_2)
|
||||||
|
)
|
||||||
|
"""
|
||||||
supports_callproc_kwargs = True
|
supports_callproc_kwargs = True
|
||||||
supports_over_clause = True
|
supports_over_clause = True
|
||||||
supports_frame_range_fixed_distance = True
|
supports_frame_range_fixed_distance = True
|
||||||
|
|
|
@ -248,7 +248,7 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
for field_name, rel_table_name, rel_field_name in cursor.fetchall()
|
for field_name, rel_table_name, rel_field_name in cursor.fetchall()
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_primary_key_column(self, cursor, table_name):
|
def get_primary_key_columns(self, cursor, table_name):
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"""
|
"""
|
||||||
SELECT
|
SELECT
|
||||||
|
@ -259,13 +259,13 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
WHERE
|
WHERE
|
||||||
user_constraints.constraint_name = cols.constraint_name AND
|
user_constraints.constraint_name = cols.constraint_name AND
|
||||||
user_constraints.constraint_type = 'P' AND
|
user_constraints.constraint_type = 'P' AND
|
||||||
user_constraints.table_name = UPPER(%s) AND
|
user_constraints.table_name = UPPER(%s)
|
||||||
cols.position = 1
|
ORDER BY
|
||||||
|
cols.position
|
||||||
""",
|
""",
|
||||||
[table_name],
|
[table_name],
|
||||||
)
|
)
|
||||||
row = cursor.fetchone()
|
return [self.identifier_converter(row[0]) for row in cursor.fetchall()]
|
||||||
return self.identifier_converter(row[0]) if row else None
|
|
||||||
|
|
||||||
def get_constraints(self, cursor, table_name):
|
def get_constraints(self, cursor, table_name):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -49,6 +49,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
V_I := P_I;
|
V_I := P_I;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;"""
|
$$ LANGUAGE plpgsql;"""
|
||||||
|
create_test_table_with_composite_primary_key = """
|
||||||
|
CREATE TABLE test_table_composite_pk (
|
||||||
|
column_1 INTEGER NOT NULL,
|
||||||
|
column_2 INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(column_1, column_2)
|
||||||
|
)
|
||||||
|
"""
|
||||||
requires_casted_case_in_updates = True
|
requires_casted_case_in_updates = True
|
||||||
supports_over_clause = True
|
supports_over_clause = True
|
||||||
only_supports_unbounded_with_preceding_and_following = True
|
only_supports_unbounded_with_preceding_and_following = True
|
||||||
|
|
|
@ -55,6 +55,13 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
# Date/DateTime fields and timedeltas.
|
# Date/DateTime fields and timedeltas.
|
||||||
"expressions.tests.FTimeDeltaTests.test_mixed_comparisons1",
|
"expressions.tests.FTimeDeltaTests.test_mixed_comparisons1",
|
||||||
}
|
}
|
||||||
|
create_test_table_with_composite_primary_key = """
|
||||||
|
CREATE TABLE test_table_composite_pk (
|
||||||
|
column_1 INTEGER NOT NULL,
|
||||||
|
column_2 INTEGER NOT NULL,
|
||||||
|
PRIMARY KEY(column_1, column_2)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def django_test_skips(self):
|
def django_test_skips(self):
|
||||||
|
|
|
@ -156,15 +156,11 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
) in cursor.fetchall()
|
) in cursor.fetchall()
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_primary_key_column(self, cursor, table_name):
|
def get_primary_key_columns(self, cursor, table_name):
|
||||||
"""Return the column name of the primary key for the given table."""
|
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"PRAGMA table_info(%s)" % self.connection.ops.quote_name(table_name)
|
"PRAGMA table_info(%s)" % self.connection.ops.quote_name(table_name)
|
||||||
)
|
)
|
||||||
for _, name, *_, pk in cursor.fetchall():
|
return [name for _, name, *_, pk in cursor.fetchall() if pk]
|
||||||
if pk:
|
|
||||||
return name
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _parse_column_or_constraint_definition(self, tokens, columns):
|
def _parse_column_or_constraint_definition(self, tokens, columns):
|
||||||
token = None
|
token = None
|
||||||
|
@ -372,14 +368,14 @@ class DatabaseIntrospection(BaseDatabaseIntrospection):
|
||||||
if orders is not None:
|
if orders is not None:
|
||||||
constraints[index]["orders"] = orders
|
constraints[index]["orders"] = orders
|
||||||
# Get the PK
|
# Get the PK
|
||||||
pk_column = self.get_primary_key_column(cursor, table_name)
|
pk_columns = self.get_primary_key_columns(cursor, table_name)
|
||||||
if pk_column:
|
if pk_columns:
|
||||||
# SQLite doesn't actually give a name to the PK constraint,
|
# SQLite doesn't actually give a name to the PK constraint,
|
||||||
# so we invent one. This is fine, as the SQLite backend never
|
# so we invent one. This is fine, as the SQLite backend never
|
||||||
# deletes PK constraints by name, as you can't delete constraints
|
# deletes PK constraints by name, as you can't delete constraints
|
||||||
# in SQLite; we remake the table with a new PK instead.
|
# in SQLite; we remake the table with a new PK instead.
|
||||||
constraints["__primary__"] = {
|
constraints["__primary__"] = {
|
||||||
"columns": [pk_column],
|
"columns": pk_columns,
|
||||||
"primary_key": True,
|
"primary_key": True,
|
||||||
"unique": False, # It's not actually a unique constraint.
|
"unique": False, # It's not actually a unique constraint.
|
||||||
"foreign_key": None,
|
"foreign_key": None,
|
||||||
|
|
|
@ -585,3 +585,32 @@ class InspectDBTransactionalTests(TransactionTestCase):
|
||||||
)
|
)
|
||||||
cursor.execute("DROP SERVER IF EXISTS inspectdb_server")
|
cursor.execute("DROP SERVER IF EXISTS inspectdb_server")
|
||||||
cursor.execute("DROP EXTENSION IF EXISTS file_fdw")
|
cursor.execute("DROP EXTENSION IF EXISTS file_fdw")
|
||||||
|
|
||||||
|
@skipUnlessDBFeature("create_test_table_with_composite_primary_key")
|
||||||
|
def test_composite_primary_key(self):
|
||||||
|
table_name = "test_table_composite_pk"
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute(
|
||||||
|
connection.features.create_test_table_with_composite_primary_key
|
||||||
|
)
|
||||||
|
out = StringIO()
|
||||||
|
if connection.vendor == "sqlite":
|
||||||
|
field_type = connection.features.introspected_field_types["AutoField"]
|
||||||
|
else:
|
||||||
|
field_type = connection.features.introspected_field_types["IntegerField"]
|
||||||
|
try:
|
||||||
|
call_command("inspectdb", table_name, stdout=out)
|
||||||
|
output = out.getvalue()
|
||||||
|
self.assertIn(
|
||||||
|
f"column_1 = models.{field_type}(primary_key=True) # The composite "
|
||||||
|
f"primary key (column_1, column_2) found, that is not supported. The "
|
||||||
|
f"first column is selected.",
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"column_2 = models.IntegerField()",
|
||||||
|
output,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
with connection.cursor() as cursor:
|
||||||
|
cursor.execute("DROP TABLE %s" % table_name)
|
||||||
|
|
Loading…
Reference in New Issue