diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index 4b281f074c8..ab9105bbfdd 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -2205,13 +2205,6 @@ func (q *querier) DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, u return q.db.DeleteApplicationConnectAPIKeysByUserID(ctx, userID) } -func (q *querier) DeleteCachedModuleFilesCreatedBetween(ctx context.Context, arg database.DeleteCachedModuleFilesCreatedBetweenParams) (int64, error) { - if err := q.authorizeContext(ctx, policy.ActionDelete, rbac.ResourceSystem); err != nil { - return 0, err - } - return q.db.DeleteCachedModuleFilesCreatedBetween(ctx, arg) -} - func (q *querier) DeleteChatContextResourcesByChatID(ctx context.Context, chatID uuid.UUID) error { chat, err := q.db.GetChatByID(ctx, chatID) if err != nil { diff --git a/coderd/database/dbauthz/dbauthz_test.go b/coderd/database/dbauthz/dbauthz_test.go index 9f87cfec008..70618754733 100644 --- a/coderd/database/dbauthz/dbauthz_test.go +++ b/coderd/database/dbauthz/dbauthz_test.go @@ -5428,11 +5428,6 @@ func (s *MethodTestSuite) TestSystemFunctions() { dbm.EXPECT().DeleteOldWorkspaceAgentLogs(gomock.Any(), t).Return(int64(0), nil).AnyTimes() check.Args(t).Asserts(rbac.ResourceSystem, policy.ActionDelete) })) - s.Run("DeleteCachedModuleFilesCreatedBetween", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { - arg := database.DeleteCachedModuleFilesCreatedBetweenParams{} - dbm.EXPECT().DeleteCachedModuleFilesCreatedBetween(gomock.Any(), arg).Return(int64(0), nil).AnyTimes() - check.Args(arg).Asserts(rbac.ResourceSystem, policy.ActionDelete) - })) s.Run("InsertWorkspaceAgentStats", s.Mocked(func(dbm *dbmock.MockStore, _ *gofakeit.Faker, check *expects) { arg := database.InsertWorkspaceAgentStatsParams{} dbm.EXPECT().InsertWorkspaceAgentStats(gomock.Any(), arg).Return(xerrors.New("any error")).AnyTimes() diff --git a/coderd/database/dbmetrics/querymetrics.go b/coderd/database/dbmetrics/querymetrics.go index 07b671294d1..0a849734e3f 100644 --- a/coderd/database/dbmetrics/querymetrics.go +++ b/coderd/database/dbmetrics/querymetrics.go @@ -505,14 +505,6 @@ func (m queryMetricsStore) DeleteApplicationConnectAPIKeysByUserID(ctx context.C return r0 } -func (m queryMetricsStore) DeleteCachedModuleFilesCreatedBetween(ctx context.Context, arg database.DeleteCachedModuleFilesCreatedBetweenParams) (int64, error) { - start := time.Now() - r0, r1 := m.s.DeleteCachedModuleFilesCreatedBetween(ctx, arg) - m.queryLatencies.WithLabelValues("DeleteCachedModuleFilesCreatedBetween").Observe(time.Since(start).Seconds()) - m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "DeleteCachedModuleFilesCreatedBetween").Inc() - return r0, r1 -} - func (m queryMetricsStore) DeleteChatContextResourcesByChatID(ctx context.Context, chatID uuid.UUID) error { start := time.Now() r0 := m.s.DeleteChatContextResourcesByChatID(ctx, chatID) diff --git a/coderd/database/dbmock/dbmock.go b/coderd/database/dbmock/dbmock.go index 9185d784019..a4b84ef97e6 100644 --- a/coderd/database/dbmock/dbmock.go +++ b/coderd/database/dbmock/dbmock.go @@ -819,21 +819,6 @@ func (mr *MockStoreMockRecorder) DeleteApplicationConnectAPIKeysByUserID(ctx, us return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteApplicationConnectAPIKeysByUserID", reflect.TypeOf((*MockStore)(nil).DeleteApplicationConnectAPIKeysByUserID), ctx, userID) } -// DeleteCachedModuleFilesCreatedBetween mocks base method. -func (m *MockStore) DeleteCachedModuleFilesCreatedBetween(ctx context.Context, arg database.DeleteCachedModuleFilesCreatedBetweenParams) (int64, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteCachedModuleFilesCreatedBetween", ctx, arg) - ret0, _ := ret[0].(int64) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DeleteCachedModuleFilesCreatedBetween indicates an expected call of DeleteCachedModuleFilesCreatedBetween. -func (mr *MockStoreMockRecorder) DeleteCachedModuleFilesCreatedBetween(ctx, arg any) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteCachedModuleFilesCreatedBetween", reflect.TypeOf((*MockStore)(nil).DeleteCachedModuleFilesCreatedBetween), ctx, arg) -} - // DeleteChatContextResourcesByChatID mocks base method. func (m *MockStore) DeleteChatContextResourcesByChatID(ctx context.Context, chatID uuid.UUID) error { m.ctrl.T.Helper() diff --git a/coderd/database/dbpurge/dbpurge.go b/coderd/database/dbpurge/dbpurge.go index 6f809e68f41..38977e25148 100644 --- a/coderd/database/dbpurge/dbpurge.go +++ b/coderd/database/dbpurge/dbpurge.go @@ -59,20 +59,6 @@ const ( chatSearchBackfillMaxBatches = 5 ) -// Terraform module archives ingested during this window may contain the -// identified upstream module. -// -// This is a one-off cleanup, not a recurring purge. It runs once per coderd -// process because the window is fixed in the past: after a successful pass -// there is nothing left to match. It lives here rather than in a migration -// because migrations cannot be backported. The version table records a single -// high-water mark, so a migration cherry-picked onto a release branch would -// cause later upgrades to skip every migration in between. -var ( - identifiedModuleCacheStart = time.Date(2026, 8, 31, 8, 0, 0, 0, time.UTC) - identifiedModuleCacheEnd = time.Date(2026, 8, 31, 22, 0, 0, 0, time.UTC) -) - type Option func(*instance) // WithClock overrides the clock used by the purger. Defaults to @@ -199,10 +185,6 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time. // after the transaction commits. var staleDrained bool - // Latched after a successful commit so the one-off module cache cleanup - // is attempted again if the transaction rolls back. - ranModuleCachePurge := false - // Start a transaction to grab advisory lock, we don't want to run // multiple purges at the same time (multiple replicas). err := db.InTx(func(tx database.Store) error { @@ -398,20 +380,6 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time. } backfilledChatSearchRows += reindexedChatSearchRows - // One-off cleanup of the identified Terraform module cache. Skipped - // once this process has completed a pass. - var purgedIdentifiedModuleFiles int64 - if !i.identifiedModuleCachePurged { - purgedIdentifiedModuleFiles, err = tx.DeleteCachedModuleFilesCreatedBetween(ctx, database.DeleteCachedModuleFilesCreatedBetweenParams{ - CreatedAtAfter: identifiedModuleCacheStart, - CreatedAtBefore: identifiedModuleCacheEnd, - }) - if err != nil { - return xerrors.Errorf("failed to delete identified module cache files: %w", err) - } - ranModuleCachePurge = true - } - i.logger.Debug(ctx, "purged old database entries", slog.F("workspace_agent_logs", purgedWorkspaceAgentLogs), slog.F("expired_api_keys", expiredAPIKeys), @@ -425,7 +393,6 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time. slog.F("chat_files", purgedChatFiles), slog.F("chat_debug_runs", purgedChatDebugRuns), slog.F("chat_search_rows_backfilled", backfilledChatSearchRows), - slog.F("identified_module_files", purgedIdentifiedModuleFiles), slog.F("duration", i.clk.Since(start)), ) @@ -441,7 +408,6 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time. i.recordsPurged.WithLabelValues("chats").Add(float64(purgedChats)) i.recordsPurged.WithLabelValues("chat_debug_runs").Add(float64(purgedChatDebugRuns)) i.recordsPurged.WithLabelValues("chat_files").Add(float64(purgedChatFiles)) - i.recordsPurged.WithLabelValues("identified_module_files").Add(float64(purgedIdentifiedModuleFiles)) } if i.chatSearchRowsBackfilled != nil { i.chatSearchRowsBackfilled.Add(float64(backfilledChatSearchRows)) @@ -463,10 +429,6 @@ func (i *instance) purgeTick(ctx context.Context, db database.Store, start time. i.chatSearchStaleDrained = true } - if ranModuleCachePurge { - i.identifiedModuleCachePurged = true - } - // Surface the deferred chat-config error so doTick records // the failed iteration metric. if chatConfigErr != nil { @@ -488,11 +450,6 @@ type instance struct { chatSearchBackfillBatchSize int32 chatSearchBackfillMaxBatches int chatSearchStaleDrained bool - - // identifiedModuleCachePurged latches once this process has completed a - // pass of the one-off module cache cleanup. The window is fixed in the - // past, so a completed pass leaves nothing to match on later ticks. - identifiedModuleCachePurged bool } func (i *instance) Close() error { diff --git a/coderd/database/dbpurge/dbpurge_internal_test.go b/coderd/database/dbpurge/dbpurge_internal_test.go index a7fe08d1ebd..f49426e9560 100644 --- a/coderd/database/dbpurge/dbpurge_internal_test.go +++ b/coderd/database/dbpurge/dbpurge_internal_test.go @@ -43,17 +43,3 @@ func TestDBPurgeAuthorization(t *testing.T) { err := inst.purgeTick(ctx, db, now) require.NoError(t, err) } - -// The behavior of the one-off module cache cleanup is covered by -// TestDeleteIdentifiedModuleCacheFiles, which supplies its own window. This -// guards the production constants themselves, which that test no longer reads. -func TestIdentifiedModuleCacheWindow(t *testing.T) { - t.Parallel() - - require.True(t, identifiedModuleCacheStart.Before(identifiedModuleCacheEnd), - "window start must precede window end") - require.Equal(t, time.UTC, identifiedModuleCacheStart.Location(), - "window bounds must be UTC so they do not shift with the host timezone") - require.Equal(t, time.UTC, identifiedModuleCacheEnd.Location(), - "window bounds must be UTC so they do not shift with the host timezone") -} diff --git a/coderd/database/dbpurge/dbpurge_test.go b/coderd/database/dbpurge/dbpurge_test.go index a7f437984ca..d649fc33c92 100644 --- a/coderd/database/dbpurge/dbpurge_test.go +++ b/coderd/database/dbpurge/dbpurge_test.go @@ -4,7 +4,6 @@ import ( "bufio" "bytes" "context" - "crypto/sha256" "database/sql" "encoding/json" "fmt" @@ -258,7 +257,6 @@ func TestMetrics(t *testing.T) { mDB.EXPECT().DeleteOldAuditLogConnectionEvents(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mDB.EXPECT().BackfillChatMessagesSearchTsv(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes() mDB.EXPECT().ReindexStaleChatMessagesSearchTsv(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes() - mDB.EXPECT().DeleteCachedModuleFilesCreatedBetween(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteCachedModuleFilesCreatedBetweenParams{})).Return(int64(0), nil).AnyTimes() mDB.EXPECT().DeleteOldChatDebugRuns(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatDebugRunsParams{})).Return(int64(0), nil).MinTimes(1) mDB.EXPECT().InTx(gomock.Any(), database.DefaultTXOptions().WithID("db_purge")). DoAndReturn(func(f func(database.Store) error, _ *database.TxOptions) error { @@ -312,7 +310,6 @@ func TestMetrics(t *testing.T) { mDB.EXPECT().DeleteOldAuditLogConnectionEvents(gomock.Any(), gomock.Any()).Return(nil).AnyTimes() mDB.EXPECT().BackfillChatMessagesSearchTsv(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes() mDB.EXPECT().ReindexStaleChatMessagesSearchTsv(gomock.Any(), gomock.Any()).Return(int64(0), nil).AnyTimes() - mDB.EXPECT().DeleteCachedModuleFilesCreatedBetween(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteCachedModuleFilesCreatedBetweenParams{})).Return(int64(0), nil).AnyTimes() mDB.EXPECT().DeleteOldChats(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatsParams{})).Return(int64(0), nil).MinTimes(1) mDB.EXPECT().DeleteOldChatFiles(gomock.Any(), gomock.AssignableToTypeOf(database.DeleteOldChatFilesParams{})).Return(int64(0), nil).MinTimes(1) mDB.EXPECT().InTx(gomock.Any(), database.DefaultTXOptions().WithID("db_purge")). @@ -3404,126 +3401,3 @@ func TestBackfillChatMessagesSearchTsv(t *testing.T) { testutil.TryReceive(ctx, t, done) }) } - -//nolint:paralleltest // It uses LockIDDBPurge. -func TestDeleteIdentifiedModuleCacheFiles(t *testing.T) { - ctx := testutil.Context(t, testutil.WaitShort) - clk := quartz.NewMock(t) - clk.Set(dbtime.Now()).MustWait(ctx) - - // The window under test is supplied explicitly rather than copied from the - // production constants, so revising the incident timestamps cannot silently - // invalidate these boundary assertions. - windowStart := time.Date(2026, 8, 31, 8, 0, 0, 0, time.UTC) - windowEnd := time.Date(2026, 8, 31, 22, 0, 0, 0, time.UTC) - inWindow := windowStart.Add(time.Minute) - - db, _ := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure()) - org := dbgen.Organization(t, db, database.Organization{}) - user := dbgen.User(t, db, database.User{}) - _ = dbgen.OrganizationMember(t, db, database.OrganizationMember{UserID: user.ID, OrganizationID: org.ID}) - - logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}) - - mkFile := func(name string, createdBy uuid.UUID, mimetype string, createdAt time.Time) database.File { - file, err := db.InsertFile(ctx, database.InsertFileParams{ - ID: uuid.New(), - Hash: fmt.Sprintf("%x", sha256.Sum256([]byte(name))), - CreatedBy: createdBy, - CreatedAt: createdAt, - Mimetype: mimetype, - Data: []byte{}, - }) - require.NoError(t, err, "insert file %q", name) - return file - } - - // mkVersion creates a template version whose cached module files point at a - // file with the given properties. InsertFile is used directly because - // dbgen.File treats uuid.Nil as unset and substitutes a random creator, - // while uuid.Nil is exactly what identifies a provisionerd module archive. - mkVersion := func(name string, createdBy uuid.UUID, mimetype string, createdAt time.Time) (database.File, database.TemplateVersion) { - file := mkFile(name, createdBy, mimetype, createdAt) - tv := dbgen.TemplateVersion(t, db, database.TemplateVersion{ - Name: name, - OrganizationID: org.ID, - CreatedBy: user.ID, - }) - _ = dbgen.TemplateVersionTerraformValues(t, db, database.TemplateVersionTerraformValue{ - TemplateVersionID: tv.ID, - CachedModuleFiles: uuid.NullUUID{UUID: file.ID, Valid: true}, - }) - return file, tv - } - - // Identified: a provisionerd module archive cached inside the window. - identified, identifiedTV := mkVersion("identified", uuid.Nil, "application/x-tar", inWindow) - // The lower bound is inclusive. - atStart, atStartTV := mkVersion("at-start", uuid.Nil, "application/x-tar", windowStart) - // The upper bound is exclusive, so this archive is known good. - atEnd, atEndTV := mkVersion("at-end", uuid.Nil, "application/x-tar", windowEnd) - // Cached before and after the window. - before, beforeTV := mkVersion("before", uuid.Nil, "application/x-tar", windowStart.Add(-time.Hour)) - after, afterTV := mkVersion("after", uuid.Nil, "application/x-tar", windowEnd.Add(time.Hour)) - // A user-uploaded template tarball shares the mimetype but has a real - // creator, so it must survive even though it is inside the window. - userUpload, userUploadTV := mkVersion("user-upload", user.ID, "application/x-tar", inWindow) - - // An unreferenced archive inside the window. Only archives referenced by a - // template version are in scope. - orphan := mkFile("orphan", uuid.Nil, "application/x-tar", inWindow) - - // when dbpurge runs - tick := awaitDoTicks(ctx, t, clk, 2) - closer := dbpurge.New(ctx, logger, db, &codersdk.DeploymentValues{}, prometheus.NewRegistry(), dbpurge.WithClock(clk)) - defer closer.Close() - tick() // doTick() has now run. - - assertFileDeleted := func(id uuid.UUID, name string) { - t.Helper() - _, err := db.GetFileByID(ctx, id) - require.ErrorIs(t, err, sql.ErrNoRows, "%s should be deleted", name) - } - assertFileExists := func(id uuid.UUID, name string) { - t.Helper() - _, err := db.GetFileByID(ctx, id) - require.NoError(t, err, "%s should be retained", name) - } - // assertCacheRef checks the template version still exists and that its - // module cache reference was cleared only when the file was deleted. - assertCacheRef := func(tv database.TemplateVersion, wantFile uuid.UUID, wantValid bool, name string) { - t.Helper() - values, err := db.GetTemplateVersionTerraformValues(ctx, tv.ID) - require.NoError(t, err, "%s: terraform values row must be retained", name) - require.Equal(t, wantValid, values.CachedModuleFiles.Valid, "%s: cache reference validity", name) - if wantValid { - require.Equal(t, wantFile, values.CachedModuleFiles.UUID, "%s: cache reference target", name) - } - } - - // then the identified archives are deleted and their references cleared - assertFileDeleted(identified.ID, "archive inside the window") - assertCacheRef(identifiedTV, uuid.Nil, false, "archive inside the window") - assertFileDeleted(atStart.ID, "archive at the inclusive lower bound") - assertCacheRef(atStartTV, uuid.Nil, false, "archive at the inclusive lower bound") - - // and everything else is untouched - assertFileExists(atEnd.ID, "archive at the exclusive upper bound") - assertCacheRef(atEndTV, atEnd.ID, true, "archive at the exclusive upper bound") - assertFileExists(before.ID, "archive cached before the window") - assertCacheRef(beforeTV, before.ID, true, "archive cached before the window") - assertFileExists(after.ID, "archive cached after the window") - assertCacheRef(afterTV, after.ID, true, "archive cached after the window") - assertFileExists(userUpload.ID, "user-uploaded tarball") - assertCacheRef(userUploadTV, userUpload.ID, true, "user-uploaded tarball") - assertFileExists(orphan.ID, "unreferenced archive") - - // The cleanup is one-off, not a recurring purge. A second tick must not - // repeat it, so an archive inserted into the window after the first pass - // survives. This documents the latch: the window is fixed in the past and - // nothing can legitimately land in it again. - late, lateTV := mkVersion("late", uuid.Nil, "application/x-tar", inWindow) - tick() - assertFileExists(late.ID, "archive inserted after the one-off pass") - assertCacheRef(lateTV, late.ID, true, "archive inserted after the one-off pass") -} diff --git a/coderd/database/querier.go b/coderd/database/querier.go index cd88b60024f..0772f5f1b15 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -134,12 +134,6 @@ type sqlcQuerier interface { // be recreated. DeleteAllWebpushSubscriptions(ctx context.Context) error DeleteApplicationConnectAPIKeysByUserID(ctx context.Context, userID uuid.UUID) error - // Deletes cached Terraform module archives ingested in the given time range and - // clears the template version references to them. created_by and mimetype - // identify a provisionerd-written module archive, matching the checks in - // provisionerdserver, so user-uploaded template tarballs are never removed. - // Only archives referenced by a template version are considered. - DeleteCachedModuleFilesCreatedBetween(ctx context.Context, arg DeleteCachedModuleFilesCreatedBetweenParams) (int64, error) // Clears a chat's pinned context resources. Used as the first half of a // clear-then-copy re-pin, and on its own when the chat's current agent // has no snapshot. diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fe06b407691..f2701be4f13 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14985,58 +14985,6 @@ func (q *sqlQuerier) UpdateExternalAuthLink(ctx context.Context, arg UpdateExter return i, err } -const deleteCachedModuleFilesCreatedBetween = `-- name: DeleteCachedModuleFilesCreatedBetween :execrows -WITH doomed AS ( - SELECT - files.id - FROM - files - INNER JOIN - template_version_terraform_values - ON template_version_terraform_values.cached_module_files = files.id - WHERE - files.created_by = '00000000-0000-0000-0000-000000000000' - AND files.mimetype = 'application/x-tar' - AND files.created_at >= $1 - AND files.created_at < $2 -), cleared AS ( - -- The foreign key is NO ACTION, so references must be cleared before the - -- files rows can be deleted. Data-modifying CTEs always run to completion, - -- and the constraint is checked at the end of the statement. - UPDATE - template_version_terraform_values - SET - cached_module_files = NULL - WHERE - cached_module_files IN (SELECT id FROM doomed) - RETURNING 1 -) -DELETE FROM - files -USING - doomed -WHERE - files.id = doomed.id -` - -type DeleteCachedModuleFilesCreatedBetweenParams struct { - CreatedAtAfter time.Time `db:"created_at_after" json:"created_at_after"` - CreatedAtBefore time.Time `db:"created_at_before" json:"created_at_before"` -} - -// Deletes cached Terraform module archives ingested in the given time range and -// clears the template version references to them. created_by and mimetype -// identify a provisionerd-written module archive, matching the checks in -// provisionerdserver, so user-uploaded template tarballs are never removed. -// Only archives referenced by a template version are considered. -func (q *sqlQuerier) DeleteCachedModuleFilesCreatedBetween(ctx context.Context, arg DeleteCachedModuleFilesCreatedBetweenParams) (int64, error) { - result, err := q.db.ExecContext(ctx, deleteCachedModuleFilesCreatedBetween, arg.CreatedAtAfter, arg.CreatedAtBefore) - if err != nil { - return 0, err - } - return result.RowsAffected() -} - const getFileByHashAndCreator = `-- name: GetFileByHashAndCreator :one SELECT hash, created_at, created_by, mimetype, data, id diff --git a/coderd/database/queries/files.sql b/coderd/database/queries/files.sql index cefc3a2d02b..cdf6e37ce08 100644 --- a/coderd/database/queries/files.sql +++ b/coderd/database/queries/files.sql @@ -55,41 +55,3 @@ WHERE AND provisioner_jobs.type = 'template_version_import' AND file_id = @file_id ; - --- name: DeleteCachedModuleFilesCreatedBetween :execrows --- Deletes cached Terraform module archives ingested in the given time range and --- clears the template version references to them. created_by and mimetype --- identify a provisionerd-written module archive, matching the checks in --- provisionerdserver, so user-uploaded template tarballs are never removed. --- Only archives referenced by a template version are considered. -WITH doomed AS ( - SELECT - files.id - FROM - files - INNER JOIN - template_version_terraform_values - ON template_version_terraform_values.cached_module_files = files.id - WHERE - files.created_by = '00000000-0000-0000-0000-000000000000' - AND files.mimetype = 'application/x-tar' - AND files.created_at >= @created_at_after - AND files.created_at < @created_at_before -), cleared AS ( - -- The foreign key is NO ACTION, so references must be cleared before the - -- files rows can be deleted. Data-modifying CTEs always run to completion, - -- and the constraint is checked at the end of the statement. - UPDATE - template_version_terraform_values - SET - cached_module_files = NULL - WHERE - cached_module_files IN (SELECT id FROM doomed) - RETURNING 1 -) -DELETE FROM - files -USING - doomed -WHERE - files.id = doomed.id;