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.
- {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/METADATA +1 -1
- {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/RECORD +99 -94
- fides/_version.py +3 -3
- fides/api/alembic/migrations/versions/5474a47c77da_create_staged_resource_ancestor_link_table.py +72 -0
- fides/api/alembic/migrations/versions/bf713b5a021d_staged_resource_ancestor_link_data_.py +250 -0
- fides/api/alembic/migrations/versions/c586a56c25e7_remove_child_diff_statuses.py +40 -0
- fides/api/main.py +4 -0
- fides/api/migrations/post_upgrade_index_creation.py +269 -0
- fides/api/models/detection_discovery.py +123 -23
- fides/api/service/privacy_request/request_service.py +4 -15
- fides/api/util/lock.py +44 -0
- fides/ui-build/static/admin/404.html +1 -1
- fides/ui-build/static/admin/_next/static/{LY2HTf0jAydL8pMpld53D → Fb70i-8GI-owNAvgEJWhA}/_buildManifest.js +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations-781808bca01f8048.js +1 -0
- fides/ui-build/static/admin/add-systems/manual.html +1 -1
- fides/ui-build/static/admin/add-systems/multiple.html +1 -1
- fides/ui-build/static/admin/add-systems.html +1 -1
- fides/ui-build/static/admin/consent/configure/add-vendors.html +1 -1
- fides/ui-build/static/admin/consent/configure.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-experience.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/[id].html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices/new.html +1 -1
- fides/ui-build/static/admin/consent/privacy-notices.html +1 -1
- fides/ui-build/static/admin/consent/properties.html +1 -1
- fides/ui-build/static/admin/consent/reporting.html +1 -1
- fides/ui-build/static/admin/consent.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn]/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects/[projectUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/projects.html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-catalog/[systemId]/resources.html +1 -1
- fides/ui-build/static/admin/data-catalog.html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId]/[systemId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center/[monitorId].html +1 -1
- fides/ui-build/static/admin/data-discovery/action-center.html +1 -1
- fides/ui-build/static/admin/data-discovery/activity.html +1 -1
- fides/ui-build/static/admin/data-discovery/detection/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/detection.html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery/[resourceUrn].html +1 -1
- fides/ui-build/static/admin/data-discovery/discovery.html +1 -1
- fides/ui-build/static/admin/datamap.html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName]/[...subfieldNames].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId]/[collectionName].html +1 -1
- fides/ui-build/static/admin/dataset/[datasetId].html +1 -1
- fides/ui-build/static/admin/dataset/new.html +1 -1
- fides/ui-build/static/admin/dataset.html +1 -1
- fides/ui-build/static/admin/datastore-connection/[id].html +1 -1
- fides/ui-build/static/admin/datastore-connection/new.html +1 -1
- fides/ui-build/static/admin/datastore-connection.html +1 -1
- fides/ui-build/static/admin/index.html +1 -1
- fides/ui-build/static/admin/integrations/[id].html +1 -1
- fides/ui-build/static/admin/integrations.html +1 -1
- fides/ui-build/static/admin/login/[provider].html +1 -1
- fides/ui-build/static/admin/login.html +1 -1
- fides/ui-build/static/admin/messaging/[id].html +1 -1
- fides/ui-build/static/admin/messaging/add-template.html +1 -1
- fides/ui-build/static/admin/messaging.html +1 -1
- fides/ui-build/static/admin/poc/ant-components.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/AntForm.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikAntFormItem.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikControlled.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikField.html +1 -1
- fides/ui-build/static/admin/poc/form-experiments/FormikSpreadField.html +1 -1
- fides/ui-build/static/admin/poc/forms.html +1 -1
- fides/ui-build/static/admin/poc/table-migration.html +1 -1
- fides/ui-build/static/admin/privacy-requests/[id].html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/messaging.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure/storage.html +1 -1
- fides/ui-build/static/admin/privacy-requests/configure.html +1 -1
- fides/ui-build/static/admin/privacy-requests.html +1 -1
- fides/ui-build/static/admin/properties/[id].html +1 -1
- fides/ui-build/static/admin/properties/add-property.html +1 -1
- fides/ui-build/static/admin/properties.html +1 -1
- fides/ui-build/static/admin/reporting/datamap.html +1 -1
- fides/ui-build/static/admin/settings/about/alpha.html +1 -1
- fides/ui-build/static/admin/settings/about.html +1 -1
- fides/ui-build/static/admin/settings/consent/[configuration_id]/[purpose_id].html +1 -1
- fides/ui-build/static/admin/settings/consent.html +1 -1
- fides/ui-build/static/admin/settings/custom-fields.html +1 -1
- fides/ui-build/static/admin/settings/domain-records.html +1 -1
- fides/ui-build/static/admin/settings/domains.html +1 -1
- fides/ui-build/static/admin/settings/email-templates.html +1 -1
- fides/ui-build/static/admin/settings/locations.html +1 -1
- fides/ui-build/static/admin/settings/organization.html +1 -1
- fides/ui-build/static/admin/settings/regulations.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id]/test-datasets.html +1 -1
- fides/ui-build/static/admin/systems/configure/[id].html +1 -1
- fides/ui-build/static/admin/systems.html +1 -1
- fides/ui-build/static/admin/taxonomy.html +1 -1
- fides/ui-build/static/admin/user-management/new.html +1 -1
- fides/ui-build/static/admin/user-management/profile/[id].html +1 -1
- fides/ui-build/static/admin/user-management.html +1 -1
- fides/ui-build/static/admin/_next/static/chunks/pages/integrations-b582e348423e5bf4.js +0 -1
- {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/WHEEL +0 -0
- {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/entry_points.txt +0 -0
- {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/licenses/LICENSE +0 -0
- {ethyca_fides-2.63.0rc0.dist-info → ethyca_fides-2.63.0rc2.dist-info}/top_level.txt +0 -0
- /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
|
+
)
|