Skip to content

feat(db): migrate Slack connection IDs from env name to env id#5564

Draft
pfreixes wants to merge 2 commits intomasterfrom
pfreixes/nan-3540-slack-alert-migration
Draft

feat(db): migrate Slack connection IDs from env name to env id#5564
pfreixes wants to merge 2 commits intomasterfrom
pfreixes/nan-3540-slack-alert-migration

Conversation

@pfreixes
Copy link

@pfreixes pfreixes commented Mar 4, 2026

Adds a knex migration that rewrites Slack notification connection IDs in the admin account from the legacy account-{uuid}-{envName} format to the new account-{uuid}-{envId} format, scoped to environments with slack_notifications = true.

Includes a preview SQL helper to capture before/after state for manual rollback (the knex down is a no-op since new and migrated connections are indistinguishable after the dual-lookup deploy).

⚠️ This one has to be merged first !!! #5559

Adds a knex migration that rewrites Slack notification connection IDs in
the admin account from the legacy `account-{uuid}-{envName}` format to
the new `account-{uuid}-{envId}` format, scoped to environments with
`slack_notifications = true`.

Includes a preview SQL helper to capture before/after state for manual
rollback (the knex down is a no-op since new and migrated connections
are indistinguishable after the dual-lookup deploy).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@linear
Copy link

linear bot commented Mar 4, 2026

@pfreixes pfreixes requested a review from a team March 4, 2026 17:30
@@ -0,0 +1,19 @@
exports.config = { transaction: true };
Copy link
Author

@pfreixes pfreixes Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to move this one into a new PR for first having deployed in prod all stuff that will be required for doing a rollback in case the rename is problematic.

This would allow us to first run the backup and later run the migration.

Note that the new created slack alerts will already follow the new convention after this one is merged, so there is no chance that we could be missing any record in case having to make a rollback of this migration

/cc @TBonnin @marcindobry

Copy link
Collaborator

@TBonnin TBonnin Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you are worrying too much. The update query is atomic and not destroying any info that cannot be reconstructed. I will keep this PR simple with only the migration/sql (and the tests if you want to).
Of course make sure to validate everything works before/after migration locally and in staging.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeps Im gonna do that, indeed Ive already run the query and downloaded the results for getting the connections that will be renamed, so we can use this for reconstructing later

Comment on lines +71 to +76
AND c.environment_id IN (
SELECT e2.id
FROM _nango_environments AS e2
JOIN _nango_accounts AS a2 ON e2.account_id = a2.id
WHERE a2.uuid = '${adminUUID}'
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can simplify and avoid the subquery with the join I think

Suggested change
AND c.environment_id IN (
SELECT e2.id
FROM _nango_environments AS e2
JOIN _nango_accounts AS a2 ON e2.account_id = a2.id
WHERE a2.uuid = '${adminUUID}'
)
AND c.environment_id = e.id
AND a.uuid = '${adminUUID}'

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I mistaken this will prune this JOIN FROM _nango_environments AS e JOIN _nango_accounts AS a ON a.id = e.account_id Not achieving what we wanted, see the reproduction that I did in local:

ango=# SELECT c.id, c.connection_id FROM _nango_connections AS c JOIN _nango_environments AS e ON true JOIN _nango_accounts AS a ON a.id = e.account_id WHERE c.connection_id = 'account-' ||
nango-#   a.uuid || '-' || e.name AND c.provider_config_key = 'slack' AND c.environment_id IN (SELECT e2.id FROM _nango_environments AS e2 JOIN _nango_accounts AS a2 ON e2.account_id = a2.id WHERE
nango(#    a2.uuid = '5f589ece-7f5a-41cd-a7f0-d5a3b9114b00') AND e.slack_notifications = true AND c.deleted = false;
 id |                   connection_id
----+---------------------------------------------------
  6 | account-ea0fb22f-515f-4aae-9767-7b8651d5c506-dev2
(1 row)

nango=# SELECT c.id, c.connection_id FROM _nango_connections AS c JOIN _nango_environments AS e ON e.id = c.environment_id JOIN _nango_accounts AS a ON a.id = e.account_id WHERE c.connection_id
nango-#   = 'account-' || a.uuid || '-' || e.name AND c.provider_config_key = 'slack' AND a.uuid = '5f589ece-7f5a-41cd-a7f0-d5a3b9114b00' AND e.slack_notifications = true AND c.deleted = false;
 id | connection_id
----+---------------
(0 rows)

Im not super happy with the original query, since it will expand the initial set of rows to any permutation of (c, e, a) and then will filter the ones that match the name, the ones that are from the root account, the ones that are slack name, etc.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oks finnaly manged to run the SELECT for getting a plan and check how much this update could cost :

explain analyze SELECT count(*)
  FROM _nango_connections AS c
  JOIN _nango_environments AS e ON true
  JOIN _nango_accounts AS a ON a.id = e.account_id
  WHERE c.connection_id = 'account-' || a.uuid || '-' || e.name
    AND c.provider_config_key = 'slack'
    AND c.environment_id IN (
        SELECT e2.id FROM _nango_environments AS e2
        JOIN _nango_accounts AS a2 ON e2.account_id = a2.id
        WHERE a2.uuid = 'b209cb00-81f6-4b98-8395-29d553be7348'
    )
    AND e.slack_notifications = true
    AND c.deleted = false;

QUERY PLAN
Aggregate  (cost=1915.61..1915.62 rows=1 width=8) (actual time=1126.026..1126.030 rows=1 loops=1)
  ->  Hash Join  (cost=797.08..1915.60 rows=1 width=0) (actual time=6.555..1125.916 rows=337 loops=1)
        Hash Cond: (e.account_id = a.id)
        Join Filter: ((c.connection_id)::text = ((('account-'::text || (a.uuid)::text) || '-'::text) || (e.name)::text))
        Rows Removed by Join Filter: 126031
        ->  Nested Loop  (cost=335.59..1453.15 rows=366 width=45) (actual time=0.897..1074.595 rows=126368 loops=1)
              ->  Nested Loop  (cost=335.59..356.10 rows=1 width=37) (actual time=0.849..1.887 rows=359 loops=1)
                    ->  Unique  (cost=335.16..335.17 rows=2 width=4) (actual time=0.824..0.833 rows=3 loops=1)
                          ->  Sort  (cost=335.16..335.17 rows=2 width=4) (actual time=0.823..0.828 rows=3 loops=1)
                                Sort Key: e2.id
                                Sort Method: quicksort  Memory: 25kB
                                ->  Nested Loop  (cost=0.29..335.15 rows=2 width=4) (actual time=0.592..0.820 rows=3 loops=1)
                                      ->  Seq Scan on _nango_accounts a2  (cost=0.00..326.16 rows=1 width=4) (actual time=0.581..0.804 rows=1 loops=1)
                                            Filter: (uuid = 'b209cb00-81f6-4b98-8395-29d553be7348'::uuid)
                                            Rows Removed by Filter: 13525
                                      ->  Index Scan using _nango_environments_account_id_index on _nango_environments e2  (cost=0.29..8.97 rows=2 width=8) (actual time=0.009..0.012 rows=3 loops=1)
                                            Index Cond: (account_id = a2.id)
                    ->  Index Only Scan using idx_connections_envid_connectionid_provider_where_deleted on _nango_connections c  (cost=0.42..10.36 rows=10 width=41) (actual time=0.015..0.318 rows=120 loops=3)
                          Index Cond: ((environment_id = e2.id) AND (provider_config_key = 'slack'::text))
                          Heap Fetches: 60
              ->  Seq Scan on _nango_environments e  (cost=0.00..1093.39 rows=366 width=8) (actual time=0.041..2.954 rows=352 loops=359)
                    Filter: slack_notifications
                    Rows Removed by Filter: 27050
        ->  Hash  (cost=292.33..292.33 rows=13533 width=20) (actual time=3.071..3.071 rows=13526 loops=1)
              Buckets: 16384  Batches: 1  Memory Usage: 815kB
              ->  Seq Scan on _nango_accounts a  (cost=0.00..292.33 rows=13533 width=20) (actual time=0.005..1.570 rows=13526 loops=1)
Planning Time: 0.683 ms
Execution Time: 1126.099 ms

Execution Time: 1126.099 ms Im not super conderned

RETURNING c.id
)
SELECT
(SELECT COUNT(*) FROM updated) AS updated_rows;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UPDATE queries already returned the number of rows being touched. I don't think you need the CTE and count

Preview was already run and output saved externally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants