ethyca-fides 2.63.0rc0__py2.py3-none-any.whl → 2.63.0rc2__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/METADATA +1 -1
  2. {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/RECORD +99 -94
  3. fides/_version.py +3 -3
  4. fides/api/alembic/migrations/versions/5474a47c77da_create_staged_resource_ancestor_link_table.py +72 -0
  5. fides/api/alembic/migrations/versions/bf713b5a021d_staged_resource_ancestor_link_data_.py +250 -0
  6. fides/api/alembic/migrations/versions/c586a56c25e7_remove_child_diff_statuses.py +40 -0
  7. fides/api/main.py +4 -0
  8. fides/api/migrations/post_upgrade_index_creation.py +269 -0
  9. fides/api/models/detection_discovery.py +123 -23
  10. fides/api/service/privacy_request/request_service.py +4 -15
  11. fides/api/util/lock.py +44 -0
  12. fides/ui-build/static/admin/404.html +1 -1
  13. fides/ui-build/static/admin/_next/static/{LY2HTf0jAydL8pMpld53D → Fb70i-8GI-owNAvgEJWhA}/_buildManifest.js +1 -1
  14. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-781808bca01f8048.js +1 -0
  15. fides/ui-build/static/admin/add-systems/manual.html +1 -1
  16. fides/ui-build/static/admin/add-systems/multiple.html +1 -1
  17. fides/ui-build/static/admin/add-systems.html +1 -1
  18. fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
  19. fides/ui-build/static/admin/consent/configure.html +1 -1
  20. fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
  21. fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
  22. fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
  23. fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
  24. fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
  25. fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
  26. fides/ui-build/static/admin/consent/properties.html +1 -1
  27. fides/ui-build/static/admin/consent/reporting.html +1 -1
  28. fides/ui-build/static/admin/consent.html +1 -1
  29. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
  30. fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
  31. fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
  32. fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
  33. fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
  34. fides/ui-build/static/admin/data-catalog.html +1 -1
  35. fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
  36. fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
  37. fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
  38. fides/ui-build/static/admin/data-discovery/activity.html +1 -1
  39. fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
  40. fides/ui-build/static/admin/data-discovery/detection.html +1 -1
  41. fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
  42. fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
  43. fides/ui-build/static/admin/datamap.html +1 -1
  44. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
  45. fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
  46. fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
  47. fides/ui-build/static/admin/dataset/new.html +1 -1
  48. fides/ui-build/static/admin/dataset.html +1 -1
  49. fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
  50. fides/ui-build/static/admin/datastore-connection/new.html +1 -1
  51. fides/ui-build/static/admin/datastore-connection.html +1 -1
  52. fides/ui-build/static/admin/index.html +1 -1
  53. fides/ui-build/static/admin/integrations/[id].html +1 -1
  54. fides/ui-build/static/admin/integrations.html +1 -1
  55. fides/ui-build/static/admin/login/[provider].html +1 -1
  56. fides/ui-build/static/admin/login.html +1 -1
  57. fides/ui-build/static/admin/messaging/[id].html +1 -1
  58. fides/ui-build/static/admin/messaging/add-template.html +1 -1
  59. fides/ui-build/static/admin/messaging.html +1 -1
  60. fides/ui-build/static/admin/poc/ant-components.html +1 -1
  61. fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
  62. fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
  63. fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
  64. fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
  65. fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
  66. fides/ui-build/static/admin/poc/forms.html +1 -1
  67. fides/ui-build/static/admin/poc/table-migration.html +1 -1
  68. fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
  69. fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
  70. fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
  71. fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
  72. fides/ui-build/static/admin/privacy-requests.html +1 -1
  73. fides/ui-build/static/admin/properties/[id].html +1 -1
  74. fides/ui-build/static/admin/properties/add-property.html +1 -1
  75. fides/ui-build/static/admin/properties.html +1 -1
  76. fides/ui-build/static/admin/reporting/datamap.html +1 -1
  77. fides/ui-build/static/admin/settings/about/alpha.html +1 -1
  78. fides/ui-build/static/admin/settings/about.html +1 -1
  79. fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
  80. fides/ui-build/static/admin/settings/consent.html +1 -1
  81. fides/ui-build/static/admin/settings/custom-fields.html +1 -1
  82. fides/ui-build/static/admin/settings/domain-records.html +1 -1
  83. fides/ui-build/static/admin/settings/domains.html +1 -1
  84. fides/ui-build/static/admin/settings/email-templates.html +1 -1
  85. fides/ui-build/static/admin/settings/locations.html +1 -1
  86. fides/ui-build/static/admin/settings/organization.html +1 -1
  87. fides/ui-build/static/admin/settings/regulations.html +1 -1
  88. fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
  89. fides/ui-build/static/admin/systems/configure/[id].html +1 -1
  90. fides/ui-build/static/admin/systems.html +1 -1
  91. fides/ui-build/static/admin/taxonomy.html +1 -1
  92. fides/ui-build/static/admin/user-management/new.html +1 -1
  93. fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
  94. fides/ui-build/static/admin/user-management.html +1 -1
  95. fides/ui-build/static/admin/_next/static/chunks/pages/integrations-b582e348423e5bf4.js +0 -1
  96. {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/WHEEL +0 -0
  97. {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/entry_points.txt +0 -0
  98. {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/licenses/LICENSE +0 -0
  99. {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/top_level.txt +0 -0
  100. /fides/ui-build/static/admin/_next/static/{LY2HTf0jAydL8pMpld53D → Fb70i-8GI-owNAvgEJWhA}/_ssgManifest.js +0 -0
@@ -0,0 +1,250 @@
1
+ """
2
+ Staged resource ancestor link data migration.
3
+
4
+ Indexes and constraints are created on this table _after_ data is populated.
5
+ If > 1,000,000 stagedresourceancestor records are created as part
6
+ of the data migration, the migration will skip creating the indexes
7
+ and constraints, and will instead log a message
8
+ indicating that index migration should be performed as part of app runtime.
9
+
10
+ The data migration has the following steps:
11
+ - Query for all staged resources and their children
12
+ - Build a list in-memory of all ancestor-descendant pairs to insert by recursively
13
+ processing each resource's children
14
+ - Write the ancestor links to a CSV string in-memory
15
+ - Copy the CSV string into the stagedresourceancestor table via a PostgreSQL COPY
16
+ command
17
+ - Create the indexes and constraints on the stagedresourceancestor table if
18
+ < 1,000,000 ancestor links are created
19
+
20
+
21
+ Revision ID: bf713b5a021d
22
+ Revises: 5474a47c77da
23
+ Create Date: 2025-06-03 09:44:58.769535
24
+
25
+ """
26
+
27
+ import csv
28
+ import uuid
29
+ from io import StringIO
30
+ from pathlib import Path
31
+
32
+ import sqlalchemy as sa
33
+ from alembic import op
34
+ from loguru import logger
35
+
36
+ # revision identifiers, used by Alembic.
37
+ revision = "bf713b5a021d"
38
+ down_revision = "5474a47c77da"
39
+
40
+ branch_labels = None
41
+ depends_on = None
42
+
43
+
44
+ def upgrade():
45
+ logger.info("Populating staged resource ancestor links...")
46
+
47
+ conn = op.get_bind()
48
+
49
+ # Get all resources and their children in batches
50
+ BATCH_SIZE = 500000
51
+
52
+ # Query resources in batches using yield_per
53
+ resources_query = sa.text(
54
+ """
55
+ SELECT urn, children
56
+ FROM stagedresource
57
+ """
58
+ )
59
+ resource_children = {}
60
+
61
+ # process resources and populate resource_children map in batches
62
+ # to limit memory usage
63
+ for batch in (
64
+ conn.execution_options(stream_results=True)
65
+ .execute(resources_query)
66
+ .yield_per(BATCH_SIZE)
67
+ .partitions()
68
+ ):
69
+ logger.info(f"Processing batch of {BATCH_SIZE} resources")
70
+
71
+ # Build resource -> children map for this batch
72
+ for result in batch:
73
+ urn = result.urn
74
+ children = result.children
75
+ if children:
76
+ resource_children[urn] = children
77
+
78
+ # Build list of ancestor-descendant pairs
79
+ ancestor_links = []
80
+
81
+ def process_children(ancestor_urn, children, visited=None):
82
+ """Recursively process children and collect ancestor links"""
83
+ if visited is None:
84
+ visited = set()
85
+
86
+ for child_urn in children:
87
+ if child_urn not in visited:
88
+ visited.add(child_urn)
89
+ # Add direct ancestor link
90
+ ancestor_links.append(
91
+ {
92
+ "id": f"srl_{uuid.uuid4()}",
93
+ "ancestor_urn": ancestor_urn,
94
+ "descendant_urn": child_urn,
95
+ }
96
+ )
97
+
98
+ # Recursively process this child's children
99
+ if child_urn in resource_children:
100
+ process_children(
101
+ ancestor_urn, resource_children[child_urn], visited
102
+ )
103
+
104
+ logger.info(
105
+ f"Recursively processing {len(resource_children)} resources for ancestor links in current batch"
106
+ )
107
+
108
+ # Process each resource's children recursively
109
+ for ancestor_urn, children in resource_children.items():
110
+ process_children(ancestor_urn, children)
111
+
112
+ # remove the resource_children map from memory
113
+ del resource_children
114
+
115
+ ancestor_links_count = len(ancestor_links)
116
+ logger.info(f"Found {ancestor_links_count} ancestor links in current batch")
117
+
118
+ if ancestor_links_count > 0:
119
+ # Create temporary CSV file
120
+ temp_csv_path = Path("staged_resource_ancestors.csv")
121
+ with open(temp_csv_path, "w", newline="") as csv_file:
122
+ writer = csv.DictWriter(
123
+ csv_file, fieldnames=["id", "ancestor_urn", "descendant_urn"]
124
+ )
125
+ writer.writeheader()
126
+ logger.info(f"Writing {ancestor_links_count} ancestor links to CSV file")
127
+ writer.writerows(ancestor_links)
128
+
129
+ del ancestor_links
130
+
131
+ # Copy all data from CSV file into table
132
+ logger.info(
133
+ f"Copying {ancestor_links_count} ancestor links from CSV file into stagedresourceancestor table..."
134
+ )
135
+ with open(temp_csv_path, "r") as csv_file:
136
+ copy_query = """
137
+ COPY stagedresourceancestor (id, ancestor_urn, descendant_urn)
138
+ FROM STDIN
139
+ WITH (FORMAT CSV, HEADER TRUE)
140
+ """
141
+ conn.connection.cursor().copy_expert(copy_query, csv_file)
142
+
143
+ # Clean up temp file
144
+ temp_csv_path.unlink()
145
+
146
+ logger.info(
147
+ f"Completed copying all ancestor links. Total ancestor links created: {ancestor_links_count}"
148
+ )
149
+
150
+ if ancestor_links_count < 1000000:
151
+
152
+ logger.info("Creating primary key index on stagedresourceancestor table...")
153
+
154
+ op.create_index(
155
+ "ix_staged_resource_ancestor_pkey",
156
+ "stagedresourceancestor",
157
+ ["id"],
158
+ unique=True,
159
+ )
160
+
161
+ logger.info("Completed creating primary key index")
162
+
163
+ logger.info("Creating foreign key constraints stagedresourceancestor table...")
164
+
165
+ op.create_foreign_key(
166
+ "fk_staged_resource_ancestor_ancestor",
167
+ "stagedresourceancestor",
168
+ "stagedresource",
169
+ ["ancestor_urn"],
170
+ ["urn"],
171
+ ondelete="CASCADE",
172
+ )
173
+
174
+ op.create_foreign_key(
175
+ "fk_staged_resource_ancestor_descendant",
176
+ "stagedresourceancestor",
177
+ "stagedresource",
178
+ ["descendant_urn"],
179
+ ["urn"],
180
+ ondelete="CASCADE",
181
+ )
182
+
183
+ logger.info("Completed creating foreign key constraints")
184
+
185
+ logger.info("Creating unique constraint on stagedresourceancestor table...")
186
+
187
+ op.create_unique_constraint(
188
+ "uq_staged_resource_ancestor",
189
+ "stagedresourceancestor",
190
+ ["ancestor_urn", "descendant_urn"],
191
+ )
192
+
193
+ logger.info("Completed creating unique constraint")
194
+
195
+ logger.info("Creating indexes on stagedresourceancestor table...")
196
+
197
+ op.create_index(
198
+ "ix_staged_resource_ancestor_ancestor",
199
+ "stagedresourceancestor",
200
+ ["ancestor_urn"],
201
+ unique=False,
202
+ )
203
+ op.create_index(
204
+ "ix_staged_resource_ancestor_descendant",
205
+ "stagedresourceancestor",
206
+ ["descendant_urn"],
207
+ unique=False,
208
+ )
209
+
210
+ logger.info("Completed creating indexes on stagedresourceancestor table")
211
+ else:
212
+ logger.info(
213
+ "Skipping creation of primary key index, foreign key constraints, unique constraint, and indexes on stagedresourceancestor table because there are more than 1,000,000 ancestor links. These will be populated as part of the `post_upgrade_index_creation.py` task kicked off during application startup."
214
+ )
215
+
216
+
217
+ def downgrade():
218
+ logger.info(
219
+ "Downgrading staged resource ancestor link data migration, populating child_diff_statuses..."
220
+ )
221
+
222
+ # Get child diff statuses for each ancestor
223
+ conn = op.get_bind()
224
+ child_diff_statuses_query = """
225
+ UPDATE stagedresource
226
+ SET child_diff_statuses = child_statuses.status_map
227
+ FROM (
228
+ SELECT
229
+ stagedresourceancestor.ancestor_urn,
230
+ jsonb_object_agg(distinct(stagedresource.diff_status), True) as status_map
231
+ FROM stagedresourceancestor
232
+ JOIN stagedresource ON stagedresourceancestor.descendant_urn = stagedresource.urn
233
+ GROUP BY stagedresourceancestor.ancestor_urn
234
+ ) AS child_statuses
235
+ WHERE stagedresource.urn = child_statuses.ancestor_urn
236
+ """
237
+ result = conn.execute(child_diff_statuses_query)
238
+ updated_rows = result.rowcount
239
+
240
+ logger.info(
241
+ f"Downgraded staged resource ancestor link data migration, completed populating child_diff_statuses for {updated_rows} rows"
242
+ )
243
+
244
+ # Intentionally not dropping the stagedresourceancestor indexes here because they
245
+ # may not have been created as part of the data migration above. If they were not created,
246
+ # then the downgrade would fail trying to drop the non-existent indexes.
247
+
248
+ # The indexes and constraints will be dropped as part of the overall table drop that's done
249
+ # as part of the downgrade in the `5474a47c77da_create_staged_resource_ancestor_link_table.py`
250
+ # migration.
@@ -0,0 +1,40 @@
1
+ """
2
+ Removes the stagedresource.child_diff_statuses column.
3
+
4
+ This migration is run _after_ the data migration
5
+ that populates the staged resource ancestor link table,
6
+ to prevent unintended data loss.
7
+
8
+ Revision ID: c586a56c25e7
9
+ Revises: bf713b5a021d
10
+ Create Date: 2025-06-03 09:47:11.389652
11
+
12
+ """
13
+
14
+ import sqlalchemy as sa
15
+ from alembic import op
16
+ from sqlalchemy.dialects import postgresql
17
+
18
+ # revision identifiers, used by Alembic.
19
+ revision = "c586a56c25e7"
20
+ down_revision = "bf713b5a021d"
21
+ branch_labels = None
22
+ depends_on = None
23
+
24
+
25
+ def upgrade():
26
+ # remove the StagedResource.child_diff_statuses column as it's no longer needed
27
+ op.drop_column("stagedresource", "child_diff_statuses")
28
+
29
+
30
+ def downgrade():
31
+ # re-add the StagedResource.child_diff_statuses column
32
+ op.add_column(
33
+ "stagedresource",
34
+ sa.Column(
35
+ "child_diff_statuses",
36
+ postgresql.JSONB(astext_type=sa.Text()),
37
+ server_default="{}",
38
+ nullable=False,
39
+ ),
40
+ )
fides/api/main.py CHANGED
@@ -33,6 +33,9 @@ from fides.api.common_exceptions import MalisciousUrlException
33
33
  from fides.api.cryptography.identity_salt import get_identity_salt
34
34
  from fides.api.middleware import handle_audit_log_resource
35
35
  from fides.api.migrations.hash_migration_job import initiate_bcrypt_migration_task
36
+ from fides.api.migrations.post_upgrade_index_creation import (
37
+ initiate_post_upgrade_index_creation,
38
+ )
36
39
  from fides.api.schemas.analytics import Event, ExtraData
37
40
 
38
41
  # pylint: disable=wildcard-import, unused-wildcard-import
@@ -97,6 +100,7 @@ async def lifespan(wrapped_app: FastAPI) -> AsyncGenerator[None, None]:
97
100
  initiate_scheduled_dsr_data_removal()
98
101
  initiate_interrupted_task_requeue_poll()
99
102
  initiate_bcrypt_migration_task()
103
+ initiate_post_upgrade_index_creation()
100
104
 
101
105
  logger.debug("Sending startup analytics events...")
102
106
  # Avoid circular imports
@@ -0,0 +1,269 @@
1
+ import json
2
+ from typing import Dict, List
3
+
4
+ from loguru import logger
5
+ from redis.lock import Lock
6
+ from sqlalchemy import text
7
+ from sqlalchemy.orm import Session
8
+
9
+ from fides.api.db.session import get_db_session
10
+ from fides.api.tasks.scheduled.scheduler import scheduler
11
+ from fides.api.util.lock import redis_lock
12
+ from fides.config import CONFIG
13
+
14
+ """
15
+ This utility is used to detect indices or constraints that were deferred as part
16
+ of a standard Fides/Alembic migration.
17
+
18
+ The standard approach for creating indices or constraints on large tables is a blocking operation,
19
+ meaning reads and write can't run while the indices and constraints are being created.
20
+ A non-blocking approach is to use the CONCURRENTLY keyword for index creation then adding
21
+ the constraints with ADD CONSTRAINT USING INDEX.
22
+
23
+ However these approaches cannot run in a transaction, which is what Fides/Alembic uses to
24
+ apply migrations. The approach here is to run these commands after the application has started
25
+ up and commit the changes immediately, outside of a transaction.
26
+
27
+ The commands that need to run are denoted in the `TABLE_OBJECT_MAP`. Indices that are used
28
+ to create constraints also include the `constraint_name` value since either the presence of
29
+ the index or the constraint can be used to determine if an index has executed.
30
+ When using the ADD CONSTRAINT USING INDEX syntax, the index specified in the command is
31
+ automatically renamed to match the name of the constraint being added.
32
+ This means that after the constraint is created, the index will have the same name as the constraint.
33
+ """
34
+
35
+ POST_UPGRADE_INDEX_CREATION = "post_upgrade_index_creation"
36
+
37
+ TABLE_OBJECT_MAP: Dict[str, List[Dict[str, str]]] = {
38
+ "currentprivacypreferencev2": [
39
+ {
40
+ "name": "ix_currentprivacypreferencev2_email_property_id",
41
+ "statement": "CREATE UNIQUE INDEX CONCURRENTLY ix_currentprivacypreferencev2_email_property_id ON currentprivacypreferencev2 (email, property_id)",
42
+ "type": "index",
43
+ "constraint_name": "last_saved_for_email_per_property_id",
44
+ },
45
+ {
46
+ "name": "ix_currentprivacypreferencev2_external_id_property_id",
47
+ "statement": "CREATE UNIQUE INDEX CONCURRENTLY ix_currentprivacypreferencev2_external_id_property_id ON currentprivacypreferencev2 (external_id, property_id)",
48
+ "type": "index",
49
+ "constraint_name": "last_saved_for_external_id_per_property_id",
50
+ },
51
+ {
52
+ "name": "ix_currentprivacypreferencev2_fides_user_device_property_id",
53
+ "statement": "CREATE UNIQUE INDEX CONCURRENTLY ix_currentprivacypreferencev2_fides_user_device_property_id ON currentprivacypreferencev2 (fides_user_device, property_id)",
54
+ "type": "index",
55
+ "constraint_name": "last_saved_for_fides_user_device_per_property_id",
56
+ },
57
+ {
58
+ "name": "ix_currentprivacypreferencev2_phone_number_property_id",
59
+ "statement": "CREATE UNIQUE INDEX CONCURRENTLY ix_currentprivacypreferencev2_phone_number_property_id ON currentprivacypreferencev2 (phone_number, property_id)",
60
+ "type": "index",
61
+ "constraint_name": "last_saved_for_phone_number_per_property_id",
62
+ },
63
+ {
64
+ "name": "ix_currentprivacypreferencev2_hashed_external_id",
65
+ "statement": "CREATE INDEX CONCURRENTLY ix_currentprivacypreferencev2_hashed_external_id ON currentprivacypreferencev2 (hashed_external_id)",
66
+ "type": "index",
67
+ },
68
+ {
69
+ "name": "last_saved_for_email_per_property_id",
70
+ "statement": "ALTER TABLE currentprivacypreferencev2 ADD CONSTRAINT last_saved_for_email_per_property_id UNIQUE USING INDEX ix_currentprivacypreferencev2_email_property_id",
71
+ "type": "constraint",
72
+ },
73
+ {
74
+ "name": "last_saved_for_external_id_per_property_id",
75
+ "statement": "ALTER TABLE currentprivacypreferencev2 ADD CONSTRAINT last_saved_for_external_id_per_property_id UNIQUE USING INDEX ix_currentprivacypreferencev2_external_id_property_id",
76
+ "type": "constraint",
77
+ },
78
+ {
79
+ "name": "last_saved_for_fides_user_device_per_property_id",
80
+ "statement": "ALTER TABLE currentprivacypreferencev2 ADD CONSTRAINT last_saved_for_fides_user_device_per_property_id UNIQUE USING INDEX ix_currentprivacypreferencev2_fides_user_device_property_id",
81
+ "type": "constraint",
82
+ },
83
+ {
84
+ "name": "last_saved_for_phone_number_per_property_id",
85
+ "statement": "ALTER TABLE currentprivacypreferencev2 ADD CONSTRAINT last_saved_for_phone_number_per_property_id UNIQUE USING INDEX ix_currentprivacypreferencev2_phone_number_property_id",
86
+ "type": "constraint",
87
+ },
88
+ ],
89
+ "privacypreferencehistory": [
90
+ {
91
+ "name": "ix_privacypreferencehistory_hashed_external_id",
92
+ "statement": "CREATE INDEX CONCURRENTLY ix_privacypreferencehistory_hashed_external_id ON privacypreferencehistory (hashed_external_id)",
93
+ "type": "index",
94
+ },
95
+ ],
96
+ "servednoticehistory": [
97
+ {
98
+ "name": "ix_servednoticehistory_hashed_external_id",
99
+ "statement": "CREATE INDEX CONCURRENTLY ix_servednoticehistory_hashed_external_id ON servednoticehistory (hashed_external_id)",
100
+ "type": "index",
101
+ },
102
+ ],
103
+ "stagedresourceancestor": [
104
+ # primary key index
105
+ {
106
+ "name": "ix_staged_resource_ancestor_pkey",
107
+ "statement": "CREATE UNIQUE INDEX CONCURRENTLY ix_staged_resource_ancestor_pkey ON stagedresourceancestor (id)",
108
+ "type": "index",
109
+ },
110
+ # unique constraint + index
111
+ {
112
+ "name": "ix_staged_resource_ancestor_unique",
113
+ "statement": "CREATE UNIQUE INDEX CONCURRENTLY ix_staged_resource_ancestor_unique ON stagedresourceancestor (ancestor_urn, descendant_urn)",
114
+ "type": "index",
115
+ "constraint_name": "uq_staged_resource_ancestor",
116
+ },
117
+ {
118
+ "name": "uq_staged_resource_ancestor",
119
+ "statement": "ALTER TABLE stagedresourceancestor ADD CONSTRAINT uq_staged_resource_ancestor UNIQUE USING INDEX ix_staged_resource_ancestor_unique",
120
+ "type": "constraint",
121
+ },
122
+ # foreign key constraints
123
+ {
124
+ "name": "fk_staged_resource_ancestor_ancestor",
125
+ "statement": "ALTER TABLE stagedresourceancestor ADD CONSTRAINT fk_staged_resource_ancestor_ancestor FOREIGN KEY (ancestor_urn) REFERENCES stagedresource (urn) ON DELETE CASCADE",
126
+ "type": "constraint",
127
+ },
128
+ {
129
+ "name": "fk_staged_resource_ancestor_descendant",
130
+ "statement": "ALTER TABLE stagedresourceancestor ADD CONSTRAINT fk_staged_resource_ancestor_descendant FOREIGN KEY (descendant_urn) REFERENCES stagedresource (urn) ON DELETE CASCADE",
131
+ "type": "constraint",
132
+ },
133
+ # column indexes
134
+ {
135
+ "name": "ix_staged_resource_ancestor_ancestor",
136
+ "statement": "CREATE INDEX CONCURRENTLY ix_staged_resource_ancestor_ancestor ON stagedresourceancestor (ancestor_urn)",
137
+ "type": "index",
138
+ },
139
+ {
140
+ "name": "ix_staged_resource_ancestor_descendant",
141
+ "statement": "CREATE INDEX CONCURRENTLY ix_staged_resource_ancestor_descendant ON stagedresourceancestor (descendant_urn)",
142
+ "type": "index",
143
+ },
144
+ ],
145
+ }
146
+
147
+
148
+ def check_object_exists(db: Session, object_name: str) -> bool:
149
+ """Checks Postgres' system catalogs to verify the existence of a given index or constraint in a specific table."""
150
+ object_query = text(
151
+ """
152
+ SELECT EXISTS (
153
+ SELECT 1
154
+ FROM pg_indexes
155
+ WHERE indexname = :object_name
156
+ ) OR EXISTS (
157
+ SELECT 1
158
+ FROM pg_constraint
159
+ WHERE conname = :object_name
160
+ )
161
+ """
162
+ )
163
+ result = db.execute(
164
+ object_query,
165
+ {
166
+ "object_name": object_name,
167
+ },
168
+ ).scalar()
169
+ return result
170
+
171
+
172
+ def create_object(db: Session, object_statement: str, object_name: str) -> None:
173
+ """Executes the index or constraint creation statement."""
174
+ logger.info(f"Creating index/constraint object: '{object_name}'...")
175
+ with db.bind.connect().execution_options(
176
+ isolation_level="AUTOCOMMIT"
177
+ ) as connection:
178
+ connection.execute(text(object_statement))
179
+ logger.info(f"Successfully created index/constraint object: '{object_name}'")
180
+
181
+
182
+ def check_and_create_objects(
183
+ db: Session, table_object_map: Dict[str, List[Dict[str, str]]], lock: Lock
184
+ ) -> Dict[str, str]:
185
+ """Returns a dictionary of any indices or constraints that are in the process of being created."""
186
+ object_info: Dict[str, str] = {}
187
+ for _, objects in table_object_map.items():
188
+ for object_data in objects:
189
+ object_name = object_data["name"]
190
+ object_statement = object_data["statement"]
191
+ object_type = object_data["type"]
192
+ object_exists = check_object_exists(db, object_name)
193
+
194
+ if not object_exists:
195
+ if object_type == "index":
196
+ constraint_name = object_data.get("constraint_name")
197
+ if constraint_name:
198
+ constraint_exists = check_object_exists(db, constraint_name)
199
+ if constraint_exists:
200
+ logger.debug(
201
+ f"Constraint {constraint_name} already exists, skipping index creation for {object_name}"
202
+ )
203
+ continue
204
+
205
+ create_object(db, object_statement, object_name)
206
+ object_info[object_name] = "in progress"
207
+ else:
208
+ logger.debug(
209
+ f"Object {object_name} already exists, skipping index/constraint creation"
210
+ )
211
+ lock.reacquire()
212
+
213
+ return object_info
214
+
215
+
216
+ # Lock key for the post upgrade index creation
217
+ POST_UPGRADE_INDEX_CREATION_LOCK = "post_upgrade_index_creation_lock"
218
+ # The timeout of the lock for the post upgrade index creation, in seconds
219
+ POST_UPGRADE_INDEX_CREATION_LOCK_TIMEOUT_SECONDS = 600 # 10 minutes
220
+
221
+
222
+ def post_upgrade_index_creation_task() -> None:
223
+ """
224
+ Task for creating indices and constraints that were deferred
225
+ as part of a standard Fides/Alembic migration.
226
+
227
+ This task is to be kicked off as a background task during application startup,
228
+ after standard migrations have been applied.
229
+
230
+ If all specified indexes and constraints are created, the task is effectively
231
+ a no-op, as it checks for the presence of the objects in the database before
232
+ creating them.
233
+ """
234
+ with redis_lock(
235
+ POST_UPGRADE_INDEX_CREATION_LOCK,
236
+ POST_UPGRADE_INDEX_CREATION_LOCK_TIMEOUT_SECONDS,
237
+ ) as lock:
238
+ if not lock:
239
+ return
240
+
241
+ SessionLocal = get_db_session(CONFIG)
242
+ with SessionLocal() as db:
243
+ object_info: Dict[str, str] = check_and_create_objects(
244
+ db, TABLE_OBJECT_MAP, lock
245
+ )
246
+ if object_info:
247
+ logger.info(
248
+ f"Post upgrade index creation output: {json.dumps(object_info)}"
249
+ )
250
+ else:
251
+ logger.debug("All indices and constraints created")
252
+
253
+
254
+ def initiate_post_upgrade_index_creation() -> None:
255
+ """Initiates scheduler to migrate all non-credential tables using a bcrypt hash to use a SHA-256 hash"""
256
+
257
+ if CONFIG.test_mode:
258
+ logger.debug("Skipping post upgrade index creation in test mode")
259
+ return
260
+
261
+ assert (
262
+ scheduler.running
263
+ ), "Scheduler is not running! Cannot migrate tables with bcrypt hashes."
264
+
265
+ logger.info("Initiating scheduler for hash migration")
266
+ scheduler.add_job(
267
+ func=post_upgrade_index_creation_task,
268
+ id=POST_UPGRADE_INDEX_CREATION,
269
+ )