Repository: googleapis/python-spanner-django

Issue:
django: implement DatabaseIntrospection.get_constraints()
While running https://healthchecks.io/ with this integration, I encountered
```shell
 ...
    state = self.apply_migration(state, migration, fake=fake, fake_initial=fake_initial)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/migrations/executor.py", line 245, in apply_migration
    state = migration.apply(state, schema_editor)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/migrations/migration.py", line 124, in apply
    operation.database_forwards(self.app_label, schema_editor, old_state, project_state)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/migrations/operations/fields.py", line 249, in database_forwards
    schema_editor.alter_field(from_model, from_field, to_field)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/backends/base/schema.py", line 565, in alter_field
    old_db_params, new_db_params, strict)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/backends/base/schema.py", line 573, in _alter_field
    fk_names = self._constraint_names(model, [old_field.column], foreign_key=True)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/backends/base/schema.py", line 1148, in _constraint_names
    constraints = self.connection.introspection.get_constraints(cursor, model._meta.db_table)
  File "/Users/emmanuelodeke/Desktop/django-apps/healthchecks-webapp/hc-venv/lib/python3.7/site-packages/django/db/backends/base/introspection.py", line 169, in get_constraints
    raise NotImplementedError('subclasses of BaseDatabaseIntrospection may require a get_constraints() method')
NotImplementedError: subclasses of BaseDatabaseIntrospection may require a get_constraints() method
```

of which that `get_constraints()` method is expected to be of the form https://github.com/django/django/blob/f283ffaa84ef0a558eb466b8fc3fae7e6fbb547c/django/db/backends/base/introspection.py#L157-L164
![Screen Shot 2020-02-26 at 9 51 10 PM](https://user-images.githubusercontent.com/4898263/75416023-1ec06580-58e2-11ea-8887-69664305f7b3.png)


and here is a full reference to Cloud Spanner's INFORMATION SCHEMA https://cloud.google.com/spanner/docs/information-schema


Discussion:
We can't return foreign key constraints yet (#313) but we should be able to return other constraints like indexes, right?
Oh, yes we can! There is INFORMATION_SCHEMA https://cloud.google.com/spanner/docs/information-schema#information_schematable_constraints. For some reason I wasn't watching properly and I marked this issue as blocked, but I wasn't supposed to. I'll fix that.

Resolving patch:
diff --git a/django_spanner/features.py b/django_spanner/features.py
--- a/django_spanner/features.py
+++ b/django_spanner/features.py
@@ -303,11 +303,6 @@ class DatabaseFeatures(BaseDatabaseFeatures):
         # DatabaseIntrospection.get_key_columns() is only required if this
         # backend needs it (which it currently doesn't).
         'introspection.tests.IntrospectionTests.test_get_key_columns',
-        # DatabaseIntrospection.get_constraints() isn't implemented:
-        # https://github.com/orijtech/django-spanner/issues/294
-        'introspection.tests.IntrospectionTests.test_get_constraints',
-        'introspection.tests.IntrospectionTests.test_get_constraints_index_types',
-        'introspection.tests.IntrospectionTests.test_get_constraints_indexes_orders',
         # DatabaseIntrospection.get_table_description() isn't fully implemented:
         # https://github.com/orijtech/django-spanner/issues/248
         'introspection.tests.IntrospectionTests.test_get_table_description_col_lengths',
diff --git a/django_spanner/introspection.py b/django_spanner/introspection.py
--- a/django_spanner/introspection.py
+++ b/django_spanner/introspection.py
@@ -97,3 +97,112 @@ def get_primary_key_column(self, cursor, table_name):
             ''' % self.connection.ops.quote_name(table_name),
         )
         return results[0][0] if results else None
+
+    def get_constraints(self, cursor, table_name):
+        constraints = {}
+        quoted_table_name = self.connection.ops.quote_name(table_name)
+
+        # Firstly populate all available constraints and their columns.
+        constraint_columns = cursor.run_sql_in_snapshot(
+            '''
+            SELECT
+                CONSTRAINT_NAME, COLUMN_NAME
+            FROM
+                INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE
+               WHERE TABLE_NAME="{table}"'''.format(table=quoted_table_name),
+        )
+        for constraint, column_name in constraint_columns:
+            if constraint not in constraints:
+                constraints[constraint] = {
+                    'check': False,
+                    'columns': [],
+                    'foreign_key': None,
+                    'index': False,
+                    'orders': [],
+                    'primary_key': False,
+                    'type': None,
+                    'unique': False,
+                }
+
+            constraints[constraint]['columns'].append(column_name)
+
+        # Add the various constraints by type.
+        constraint_types = cursor.run_sql_in_snapshot(
+            '''
+            SELECT
+                CONSTRAINT_NAME, CONSTRAINT_TYPE
+            FROM
+                INFORMATION_SCHEMA.TABLE_CONSTRAINTS
+            WHERE
+                TABLE_NAME="{table}"'''.format(table=quoted_table_name),
+        )
+        for constraint, constraint_type in constraint_types:
+            already_added = constraint in constraints
+            if constraint_type == 'FOREIGN KEY':
+                # We don't yet support anything related to FOREIGN KEY.
+                # See https://github.com/orijtech/django-spanner/issues/313.
+                if already_added:
+                    del constraints[constraint]
+                continue
+
+            if not already_added:
+                constraints[constraint] = {
+                    'check': False,
+                    'columns': [],
+                    'foreign_key': None,
+                    'index': False,
+                    'orders': [],
+                    'primary_key': False,
+                    'type': None,
+                    'unique': False,
+                }
+
+            is_primary_key = constraint_type == 'PRIMARY KEY'
+            constraints[constraint]['check'] = constraint_type == 'CHECK'
+            constraints[constraint]['index'] = constraint_type == 'INDEX'
+            constraints[constraint]['unique'] = constraint_type == 'UNIQUE' or is_primary_key
+            constraints[constraint]['primary_key'] = is_primary_key
+
+        # Add the indices.
+        indexes = cursor.run_sql_in_snapshot(
+            '''
+            SELECT
+                idx.INDEX_NAME, idx_col.COLUMN_NAME, idx_col.COLUMN_ORDERING, idx.INDEX_TYPE, idx.IS_UNIQUE
+            FROM
+                INFORMATION_SCHEMA.INDEXES AS idx
+            RIGHT JOIN
+                INFORMATION_SCHEMA.INDEX_COLUMNS AS idx_col
+            ON
+                idx_col.INDEX_NAME = idx.INDEX_NAME AND idx_col.TABLE_NAME="{table}"
+            WHERE
+                idx.TABLE_NAME="{table}"
+            ORDER BY
+                idx_col.ORDINAL_POSITION
+            '''.format(table=quoted_table_name),
+        )
+        for index_name, column_name, ordering, index_type, is_unique in indexes:
+            if index_name not in constraints:
+                constraints[index_name] = {
+                    'check': False,
+                    'columns': [],
+                    'foreign_key': None,
+                    'index': False,
+                    'orders': [],
+                    'primary_key': False,
+                    'type': None,
+                    'unique': False,
+                }
+
+            constraints[index_name]['columns'].append(column_name)
+            constraints[index_name]['index'] = True
+            constraints[index_name]['orders'].append(ordering)
+            # Index_type for PRIMARY KEY is 'PRIMARY_KEY' and NOT 'PRIMARY KEY'
+            constraints[index_name]['primary_key'] = index_type == 'PRIMARY_KEY'
+            # Cloud Spanner doesn't expose the index's data structure. However, Django
+            # expects last segment after '_', of the suffix of the index name, to be the type.
+            underscore_split = index_name.split('_')
+            index_type = underscore_split[-1] if len(underscore_split) > 1 else None
+            constraints[index_name]['type'] = index_type
+            constraints[index_name]['unique'] = is_unique
+
+        return constraints


Test patch: