fix(catalog-backend): fix irreversible database migration down functions

Five migration down functions were broken, causing the SQL report to flag
them as not reversible. All issues were pre-existing bugs that were never
caught because the down migrations had never been exercised under the
reversibility checker.

- 20241003170511: add .notNullable() to both up and down when altering
  locations.target, preventing the NOT NULL constraint from being silently
  dropped by knex's .alter() when widening varchar(255) to text.
- 20220116144621: implement a real down function that recreates the three
  dropped legacy tables (entities, entities_search, entities_relations)
  with correct columns and indices.
- 20210302150147: drop dependent tables in the correct order to avoid a
  FK constraint violation, and fix a typo (references → refresh_state_references).
- 20201005122705: drop full_name from entities (not entities_search), and
  restore entities_unique_name with the correct column order (kind, name, namespace).
- 20200702153613: use table.integer('generation') instead of table.string()
  in the down function to restore the correct column type.

Signed-off-by: Fredrik Adelöw <freben@spotify.com>
Made-with: Cursor
This commit is contained in:
Fredrik Adelöw
2026-04-14 14:54:42 +02:00
parent 3ef6078b70
commit b33f8450b5
7 changed files with 102 additions and 35 deletions
@@ -0,0 +1,11 @@
---
'@backstage/plugin-catalog-backend': patch
---
Fixed several database migration `down` functions that were not properly reversible, causing the SQL report to show warnings:
- `20241003170511_alter_target_in_locations.js`: both `up` and `down` now include `.notNullable()` when altering the `locations.target` column, preventing the `NOT NULL` constraint from being accidentally dropped when widening the column type from `varchar(255)` to `text`.
- `20220116144621_remove_legacy.js`: the `down` function now properly recreates the three dropped legacy tables (`entities`, `entities_search`, `entities_relations`) with correct columns and indices.
- `20210302150147_refresh_state.js`: the `down` function now drops dependent tables in the correct order (avoiding a FK constraint violation) and fixes a typo where the table was referred to as `references` instead of `refresh_state_references`.
- `20201005122705_add_entity_full_name.js`: the `down` function now drops the `full_name` column from `entities` (not `entities_search`), and restores the `entities_unique_name` index with the correct column order `(kind, name, namespace)`.
- `20200702153613_entities.js`: the `down` function now uses `table.integer('generation')` instead of `table.string('generation')`, restoring the correct column type.
@@ -169,7 +169,7 @@ exports.down = async function down(knex) {
'An opaque string that changes for each update operation to any part of the entity, including metadata.',
);
table
.string('generation')
.integer('generation')
.notNullable()
.unsigned()
.comment(
@@ -52,12 +52,12 @@ exports.down = async function down(knex) {
await knex.schema.alterTable('entities', table => {
// https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.18/#objectmeta-v1-meta
table.dropUnique([], 'entities_unique_full_name');
table.unique(['kind', 'namespace', 'name'], {
table.unique(['kind', 'name', 'namespace'], {
indexName: 'entities_unique_name',
});
});
await knex.schema.alterTable('entities_search', table => {
await knex.schema.alterTable('entities', table => {
table.dropColumn('full_name');
});
};
@@ -175,33 +175,10 @@ exports.up = async function up(knex) {
* @param {import('knex').Knex} knex
*/
exports.down = async function down(knex) {
await knex.schema.alterTable('refresh_state_references', table => {
table.dropIndex([], 'refresh_state_references_source_key_idx');
table.dropIndex([], 'refresh_state_references_source_entity_ref_idx');
table.dropIndex([], 'refresh_state_references_target_entity_ref_idx');
});
await knex.schema.alterTable('refresh_state', table => {
table.dropUnique([], 'refresh_state_entity_ref_uniq');
table.dropIndex([], 'refresh_state_entity_id_idx');
table.dropIndex([], 'refresh_state_entity_ref_idx');
table.dropIndex([], 'refresh_state_next_update_at_idx');
});
await knex.schema.alterTable('final_entities', table => {
table.dropIndex([], 'final_entities_entity_id_idx');
});
await knex.schema.alterTable('relations', table => {
table.index('source_entity_ref', 'relations_source_entity_ref_idx');
table.index('originating_entity_id', 'relations_source_entity_id_idx');
});
await knex.schema.alterTable('search', table => {
table.dropIndex([], 'search_entity_id_idx');
table.dropIndex([], 'search_key_idx');
table.dropIndex([], 'search_value_idx');
});
// Drop tables that have FK constraints referencing refresh_state first.
await knex.schema.dropTable('refresh_state_references');
await knex.schema.dropTable('search');
await knex.schema.dropTable('final_entities');
await knex.schema.dropTable('relations');
await knex.schema.dropTable('references');
await knex.schema.dropTable('refresh_state');
};
@@ -25,4 +25,86 @@ exports.up = async function up(knex) {
await knex.schema.dropTable('entities');
};
exports.down = async function down() {};
exports.down = async function down(knex) {
await knex.schema.createTable('entities', table => {
table.comment('All entities currently stored in the catalog');
table.uuid('id').primary().comment('Auto-generated ID of the entity');
table
.uuid('location_id')
.references('id')
.inTable('locations')
.nullable()
.comment('The location that originated the entity');
table
.string('etag')
.notNullable()
.comment(
'An opaque string that changes for each update operation to any part of the entity, including metadata.',
);
table
.integer('generation')
.notNullable()
.unsigned()
.comment(
'A positive nonzero number that indicates the current generation of data for this entity; the value is incremented each time the spec changes.',
);
table
.string('full_name')
.notNullable()
.comment('The full name of the entity');
table
.text('data')
.notNullable()
.comment('The entire JSON data blob of the entity');
table.unique(['full_name'], { indexName: 'entities_unique_full_name' });
table.index('location_id', 'entity_location_id_idx');
});
await knex.schema.createTable('entities_search', table => {
table.comment(
'Flattened key-values from the entities, used for quick filtering',
);
table
.uuid('entity_id')
.references('id')
.inTable('entities')
.onDelete('CASCADE')
.comment('The entity that matches this key/value');
table
.string('key')
.notNullable()
.comment('A key that occurs in the entity');
table
.string('value')
.nullable()
.comment('The corresponding value to match on');
table.index(['key'], 'entities_search_key');
table.index(['value'], 'entities_search_value');
table.index(['entity_id'], 'entity_id_idx');
});
await knex.schema.createTable('entities_relations', table => {
table.comment('All relations between entities in the catalog');
table
.uuid('originating_entity_id')
.references('id')
.inTable('entities')
.onDelete('CASCADE')
.notNullable()
.comment('The entity that provided the relation');
table
.string('source_full_name')
.notNullable()
.comment('The full name of the source entity of the relation');
table
.string('type')
.notNullable()
.comment('The type of the relation between the entities');
table
.string('target_full_name')
.notNullable()
.comment('The full name of the target entity of the relation');
table.index('source_full_name', 'source_full_name_idx');
table.index('originating_entity_id', 'originating_entity_id_idx');
});
};
@@ -22,7 +22,7 @@
*/
exports.up = async function up(knex) {
await knex.schema.alterTable('locations', table => {
table.text('target').alter();
table.text('target').notNullable().alter();
});
};
@@ -42,6 +42,6 @@ exports.down = async function down(knex) {
}
await knex.schema.alterTable('locations', table => {
table.string('target').alter();
table.string('target').notNullable().alter();
});
};
+1 -4
View File
@@ -2,9 +2,6 @@
> Do not edit this file. It is a report generated by `yarn build:api-reports`
> [!WARNING]
> Failed to migrate down from '20241003170511_alter_target_in_locations.js'
## Sequences
- `location_update_log_id_seq` (bigint)
@@ -47,7 +44,7 @@
| --------------------- | ------------------- | -------- | ---------- | ------- |
| `id` | `uuid` | false | - | - |
| `location_entity_ref` | `character varying` | false | 255 | - |
| `target` | `text` | true | - | - |
| `target` | `text` | false | - | - |
| `type` | `character varying` | false | 255 | - |
### Indices