GoogleCloudPlatform / GoogleCloudPlatform/cloud-spanner-emulator
Incorrect INFORMATION_SCHEMA for Cross-Schema Foreign Keys
- Dominant language
- C++
- Stars
- 334
- Forks
- 77
- Avg merge
- 8m
- Merged PRs (30d)
- 2
Description
This appears to be a regression. This bug is preventing me from upgrading from `1.5.33` where this appears to work correctly.
The following is output working with `1.5.54` in the provided docker image.
There are a few bugs around retrieving metadata from the information schema for foreign keys that reference unique indexes across schemas.
## 1. DDL Setup
To reproduce, create a database with the following DDL. It sets up two schemas: a target schema `other_schema` and a default schema. The default schema has tables that reference a unique index in `other_schema`.
```sql
CREATE SCHEMA `other_schema`;
-- Table in non-default schema with a unique index (non-PK target)
CREATE TABLE other_schema.referenced_table (
id INT64 NOT NULL,
unique_val INT64 NOT NULL,
) PRIMARY KEY (id);
CREATE UNIQUE INDEX other_schema.referenced_table_unique_val_idx
ON other_schema.referenced_table (unique_val);
-- Table in default schema referencing the unique index
CREATE TABLE referring_table (
id INT64 NOT NULL,
ref_val INT64 NOT NULL,
CONSTRAINT fk_ref_val FOREIGN KEY (ref_val) REFERENCES other_schema.referenced_table (unique_val) NOT ENFORCED
) PRIMARY KEY (id);
-- A second table referencing the same unique index (triggers duplicate rows bug)
CREATE TABLE referring_table_2 (
id INT64 NOT NULL,
ref_val INT64 NOT NULL,
CONSTRAINT fk_ref_val_2 FOREIGN KEY (ref_val) REFERENCES other_schema.referenced_table (unique_val) NOT ENFORCED
) PRIMARY KEY (id);
```
---
## 2. Demonstrated Bugs in `INFORMATION_SCHEMA`
After creating the tables, query the metadata tables to see the discrepancies.
### Bug A: Mismatch in Constraint Name and Schema
`TABLE_CONSTRAINTS` prefixes the constraint name with the schema and leaves `CONSTRAINT_SCHEMA` empty, while `REFERENTIAL_CONSTRAINTS` does not.
```sql
-- Query RC
SELECT constraint_name, unique_constraint_schema, unique_constraint_name
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS
WHERE constraint_name = 'fk_ref_val';
```
**Output:**
```
+-----------------+--------------------------+---------------------------------------------------------+
| constraint_name | unique_constraint_schema | unique_constraint_name |
+-----------------+--------------------------+---------------------------------------------------------+
| fk_ref_val | other_schema | IDX_referenced_table_unique_val_U_6279FBB6083F67DA |
+-----------------+--------------------------+---------------------------------------------------------+
```
```sql
-- Query TC
SELECT constraint_schema, constraint_name, table_schema, table_name, constraint_type
FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS
WHERE table_name = 'referenced_table' AND constraint_type = 'UNIQUE';
```
**Output:**
```
+-------------------+-------------------------------------------------------------------------+--------------+------------------+-----------------+
| constraint_schema | constraint_name | table_schema | table_name | constraint_type |
+-------------------+-------------------------------------------------------------------------+--------------+------------------+-----------------+
| | other_schema.IDX_referenced_table_unique_val_U_6279FBB6083F67DA | other_schema | referenced_table | UNIQUE |
| | other_schema.IDX_referenced_table_unique_val_U_6279FBB6083F67DA | other_schema | referenced_table | UNIQUE |
+-------------------+-------------------------------------------------------------------------+--------------+------------------+-----------------+
```
**Discrepancies:**
* `TABLE_CONSTRAINTS.CONSTRAINT_SCHEMA` is **empty string `''`** instead of `'other_schema'`.
* `TABLE_CONSTRAINTS.CONSTRAINT_NAME` is **`other_schema.IDX_...`** (prefixed) but `REFERENTIAL_CONSTRAINTS.UNIQUE_CONSTRAINT_NAME` is **`IDX_...`** (unprefixed).
* `TABLE_CONSTRAINTS` contains **duplicate identical rows** (see Bug C).
---
### Bug B: Mismatch in `KEY_COLUMN_USAGE` Schema
`KEY_COLUMN_USAGE` also leaves the `CONSTRAINT_SCHEMA` empty for unique indexes.
```sql
SELECT constraint_schema, constraint_name, table_schema, table_name, column_name
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE table_name = 'referenced_table' AND constraint_name LIKE 'IDX_%';
```
**Output:**
```
+-------------------+----------------------------------------------------+--------------+------------------+-------------+
| constraint_schema | constraint_name | table_schema | table_name | column_name |
+-------------------+----------------------------------------------------+--------------+------------------+-------------+
| | IDX_referenced_table_unique_val_U_6279FBB6083F67DA | other_schema | referenced_table | unique_val |
| | IDX_referenced_table_unique_val_U_6279FBB6083F67DA | other_schema | referenced_table | unique_val |
+-------------------+----------------------------------------------------+--------------+------------------+-------------+
```
**Discrepancy:**
* `KEY_COLUMN_USAGE.CONSTRAINT_SCHEMA` is **empty string `''`** instead of `'other_schema'`.
---
### Bug C: Duplicate Rows
Having multiple foreign keys referencing the same unique index causes the emulator to duplicate the index representation in `TABLE_CONSTRAINTS` and `KEY_COLUMN_USAGE`.
In the outputs above for **Bug A** and **Bug B**, notice that there are **two identical rows** returned for the unique index. This is because both `referring_table` and `referring_table_2` reference it. Adding more referencing tables increases the duplication.
---
## 3. Impact on Standard Joins
SQLAlchemy (and other tools) reflects foreign keys by joining these tables on catalog, schema, and name:
```sql
SELECT rc.constraint_name, tc_uq.table_name
FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS rc
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS tc_uq
ON tc_uq.constraint_catalog = rc.unique_constraint_catalog
AND tc_uq.constraint_schema = rc.unique_constraint_schema
AND tc_uq.constraint_name = rc.unique_constraint_name
WHERE rc.constraint_name = 'fk_ref_val';
```
In the emulator, this query returns **`0` rows** (instead of `1`), because:
* `tc_uq.constraint_schema` (`''`) != `rc.unique_constraint_schema` (`'other_schema'`)
* `tc_uq.constraint_name` (`'other_schema.IDX_...'`) != `rc.unique_constraint_name` (`'IDX_...'`)
Contributor guide
Assessment
This issue has not been assessed yet.