feat: add foreign keys to stopwatch & tracked_time tables (#9373)
Adds four foreign keys: - stopwatch -- issue_id -> issue, user_id -> user - tracked_time -- issue_id -> issue, user_id -> user The majority of work encompassed in this PR is updating testing and support infrastructure to support foreign keys: - `models/db/foreign_keys.go` adds new capabilities to sort registered tables into the right insertion order to avoid violating foreign keys - `RecreateTables`, used by migration testing and the `doctor recreate-table` CLI, has been updated to support tables with foreign keys; new restrictions require that FK-related tables be rebuilt at the same time - test fixture data is inserted in foreign-key order, and deleted in the reverse An upgrade to xorm v1.3.9-forgejo.2 is incorporated in this PR, as two unexpected behaviors in the foreign key schema management were discovered during development of the updated `RecreateTables` routine. Work in this PR is laid out to be reviewed easier commit-by-commit. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests - I added test coverage for Go changes... - [x] in their respective `*_test.go` for unit tests. - [x] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I added test coverage for JavaScript changes... - [ ] in `web_src/js/*.test.js` if it can be unit tested. - [ ] in `tests/e2e/*.test.e2e.js` if it requires interactions with a live Forgejo server (see also the [developer guide for JavaScript testing](https://codeberg.org/forgejo/forgejo/src/branch/forgejo/tests/e2e/README.md#end-to-end-tests)). ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [ ] I do not want this change to show in the release notes. - [ ] I want the title to show in the release notes with a link to this pull request. - [x] I want the content of the `release-notes/<pull request number>.md` to be be used for the release notes instead of the title. Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/9373 Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org> Reviewed-by: Gusted <gusted@noreply.codeberg.org> Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Co-committed-by: Mathieu Fenniak <mathieu@fenniak.net>
This commit is contained in:
committed by
Earl Warren
parent
ee4f9b02f9
commit
187ad99f3c
@@ -119,6 +119,8 @@ var migrations = []*Migration{
|
||||
NewMigration("Migrate `data` column of `secret` table to store keying material", MigrateActionSecretsToKeying),
|
||||
// v39 -> v40
|
||||
NewMigration("Add index for release sha1", AddIndexForReleaseSha1),
|
||||
// v40 -> v41
|
||||
NewMigration("Add foreign keys to stopwatch & tracked_time", AddForeignKeysStopwatchTrackedTime),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package forgejo_migrations
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"forgejo.org/modules/log"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func syncDoctorForeignKey(x *xorm.Engine, beans []any) error {
|
||||
for _, bean := range beans {
|
||||
// Sync() drops indexes by default, which will cause unnecessary rebuilding of indexes when syncDoctorForeignKey
|
||||
// is used with partial bean definitions; so we disable that option
|
||||
_, err := x.SyncWithOptions(xorm.SyncOptions{IgnoreDropIndices: true}, bean)
|
||||
if err != nil {
|
||||
if errors.Is(err, xorm.ErrForeignKeyViolation) {
|
||||
tableName := x.TableName(bean)
|
||||
log.Error(
|
||||
"Foreign key creation on table %s failed. Run `forgejo doctor check --all` to identify the orphaned records preventing this foreign key from being created. Error was: %v",
|
||||
tableName, err)
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AddForeignKeysStopwatchTrackedTime(x *xorm.Engine) error {
|
||||
type Stopwatch struct {
|
||||
IssueID int64 `xorm:"INDEX REFERENCES(issue, id)"`
|
||||
UserID int64 `xorm:"INDEX REFERENCES(user, id)"`
|
||||
}
|
||||
type TrackedTime struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
IssueID int64 `xorm:"INDEX REFERENCES(issue, id)"`
|
||||
UserID int64 `xorm:"INDEX REFERENCES(user, id)"`
|
||||
}
|
||||
|
||||
// TrackedTime.UserID used to be an intentionally dangling reference if a user was deleted, in order to maintain the
|
||||
// time that was tracked against an issue. With the addition of a foreign key, we set UserID to NULL where the user
|
||||
// doesn't exist instead of leaving it pointing to an invalid record:
|
||||
var trackedTime []TrackedTime
|
||||
err := x.Table("tracked_time").
|
||||
Join("LEFT", "`user`", "`tracked_time`.user_id = `user`.id").
|
||||
Where(builder.IsNull{"`user`.id"}).
|
||||
Find(&trackedTime)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tt := range trackedTime {
|
||||
affected, err := x.Table(&TrackedTime{}).Where("id = ?", tt.ID).Update(map[string]any{"user_id": nil})
|
||||
if err != nil {
|
||||
return err
|
||||
} else if affected != 1 {
|
||||
return fmt.Errorf("expected to update 1 tracked_time record with ID %d, but actually affected %d records", tt.ID, affected)
|
||||
}
|
||||
}
|
||||
|
||||
return syncDoctorForeignKey(x, []any{
|
||||
new(Stopwatch),
|
||||
new(TrackedTime),
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user