From 7ab2539449a38bc170be69805bbde65dc5e67f56 Mon Sep 17 00:00:00 2001 From: Stephanie Hingtgen Date: Mon, 6 Jan 2025 07:58:16 -0700 Subject: [PATCH] Dashboard tags: add dashboard_uid and org_id (#98500) --- pkg/services/dashboards/database/database.go | 26 ++++++--- .../dashboards/database/database_test.go | 20 ++++++- .../sqlstore/migrations/dashboard_mig.go | 55 +++++++++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 5ed1db5160e..6c3c789284e 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -20,6 +20,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/permissions" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/services/star" @@ -41,9 +42,11 @@ type dashboardStore struct { // SQL bean helper to save tags type dashboardTag struct { - Id int64 - DashboardId int64 - Term string + Id int64 + OrgID int64 `xorm:"org_id"` + DashboardId int64 + DashboardUID string `xorm:"dashboard_uid"` + Term string } // DashboardStore implements the Store interface @@ -57,6 +60,13 @@ func ProvideDashboardStore(sqlStore db.DB, cfg *setting.Cfg, features featuremgm return nil, err } + // fill out dashboard_uid and org_id for dashboard_tags + // need to run this at startup in case any downgrade happened after the initial migration + err = migrations.RunDashboardTagMigrations(sqlStore.GetEngine().NewSession(), sqlStore.GetDialect().DriverName()) + if err != nil { + s.log.Error("Failed to run dashboard_tag migrations", "err", err) + } + if err := quotaService.RegisterQuotaReporter("a.NewUsageReporter{ TargetSrv: dashboards.QuotaTargetSrv, DefaultLimits: defaultLimits, @@ -438,7 +448,7 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE } // delete existing tags - if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_id=?", dash.ID); err != nil { + if _, err = sess.Exec("DELETE FROM dashboard_tag WHERE dashboard_uid=? AND org_id=?", dash.UID, dash.OrgID); err != nil { return nil, err } @@ -446,7 +456,7 @@ func saveDashboard(sess *db.Session, cmd *dashboards.SaveDashboardCommand, emitE tags := dash.GetTags() if len(tags) > 0 { for _, tag := range tags { - if _, err := sess.Insert(dashboardTag{DashboardId: dash.ID, Term: tag}); err != nil { + if _, err := sess.Insert(dashboardTag{DashboardId: dash.ID, Term: tag, OrgID: dash.OrgID, DashboardUID: dash.UID}); err != nil { return nil, err } } @@ -604,7 +614,7 @@ func (d *dashboardStore) deleteDashboard(cmd *dashboards.DeleteDashboardCommand, } sqlStatements := []statement{ - {SQL: "DELETE FROM dashboard_tag WHERE dashboard_id = ? ", args: []any{dashboard.ID}}, + {SQL: "DELETE FROM dashboard_tag WHERE dashboard_uid = ? AND org_id = ?", args: []any{dashboard.UID, dashboard.OrgID}}, {SQL: "DELETE FROM star WHERE dashboard_id = ? ", args: []any{dashboard.ID}}, {SQL: "DELETE FROM dashboard WHERE id = ?", args: []any{dashboard.ID}}, {SQL: "DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?", args: []any{dashboard.ID}}, @@ -935,8 +945,8 @@ func (d *dashboardStore) GetDashboardTags(ctx context.Context, query *dashboards COUNT(*) as count, term FROM dashboard - INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id - WHERE dashboard.org_id=? + INNER JOIN dashboard_tag on dashboard_tag.dashboard_uid = dashboard.uid + WHERE dashboard_tag.org_id=? GROUP BY term ORDER BY term` diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index c76cd51efa6..fac6028f4df 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -213,15 +213,31 @@ func TestIntegrationDashboardDataAccess(t *testing.T) { assert.Equal(t, len(queryResult), 2) }) - t.Run("Should be able to delete dashboard", func(t *testing.T) { + t.Run("Should be able to delete dashboard and associated tags", func(t *testing.T) { setup() dash := insertTestDashboard(t, dashboardStore, "delete me", 1, 0, "", false, "delete this") - err := dashboardStore.DeleteDashboard(context.Background(), &dashboards.DeleteDashboardCommand{ + tags, err := dashboardStore.GetDashboardTags(context.Background(), &dashboards.GetDashboardTagsQuery{OrgID: 1}) + require.NoError(t, err) + terms := make([]string, len(tags)) + for i, tag := range tags { + terms[i] = tag.Term + } + require.Contains(t, terms, "delete this") + + err = dashboardStore.DeleteDashboard(context.Background(), &dashboards.DeleteDashboardCommand{ ID: dash.ID, OrgID: 1, }) require.NoError(t, err) + + tags, err = dashboardStore.GetDashboardTags(context.Background(), &dashboards.GetDashboardTagsQuery{OrgID: 1}) + require.NoError(t, err) + terms = make([]string, len(tags)) + for i, tag := range tags { + terms[i] = tag.Term + } + require.NotContains(t, terms, "delete this") }) t.Run("Should be able to create dashboard", func(t *testing.T) { diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index 46bdd1c886a..44465386a65 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -1,7 +1,10 @@ package migrations import ( + "fmt" + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "xorm.io/xorm" ) func addDashboardMigration(mg *Migrator) { @@ -244,4 +247,56 @@ func addDashboardMigration(mg *Migrator) { Cols: []string{"deleted"}, Type: IndexType, })) + + mg.AddMigration("Add column dashboard_uid in dashboard_tag", NewAddColumnMigration(dashboardTagV1, &Column{ + Name: "dashboard_uid", Type: DB_NVarchar, Length: 40, Nullable: true, + })) + mg.AddMigration("Add column org_id in dashboard_tag", NewAddColumnMigration(dashboardTagV1, &Column{ + Name: "org_id", Type: DB_BigInt, Nullable: true, Default: "1", + })) + + mg.AddMigration("Add missing dashboard_uid and org_id to dashboard_tag", &FillDashbordUIDAndOrgIDMigration{}) +} + +type FillDashbordUIDAndOrgIDMigration struct { + MigrationBase +} + +func (m *FillDashbordUIDAndOrgIDMigration) SQL(dialect Dialect) string { + return "code migration" +} + +func (m *FillDashbordUIDAndOrgIDMigration) Exec(sess *xorm.Session, mg *Migrator) error { + return RunDashboardTagMigrations(sess, mg.Dialect.DriverName()) +} + +func RunDashboardTagMigrations(sess *xorm.Session, driverName string) error { + // sqlite + sql := `UPDATE dashboard_tag + SET + dashboard_uid = (SELECT uid FROM dashboard WHERE dashboard.id = dashboard_tag.dashboard_id), + org_id = (SELECT org_id FROM dashboard WHERE dashboard.id = dashboard_tag.dashboard_id) + WHERE + (dashboard_uid IS NULL OR org_id IS NULL) + AND EXISTS (SELECT 1 FROM dashboard WHERE dashboard.id = dashboard_tag.dashboard_id);` + if driverName == Postgres { + sql = `UPDATE dashboard_tag + SET dashboard_uid = dashboard.uid, + org_id = dashboard.org_id + FROM dashboard + WHERE dashboard_tag.dashboard_id = dashboard.id + AND (dashboard_tag.dashboard_uid IS NULL OR dashboard_tag.org_id IS NULL);` + } else if driverName == MySQL { + sql = `UPDATE dashboard_tag + LEFT JOIN dashboard ON dashboard_tag.dashboard_id = dashboard.id + SET dashboard_tag.dashboard_uid = dashboard.uid, + dashboard_tag.org_id = dashboard.org_id + WHERE dashboard_tag.dashboard_uid IS NULL OR dashboard_tag.org_id IS NULL;` + } + + if _, err := sess.Exec(sql); err != nil { + return fmt.Errorf("failed to set dashboard_uid and org_id in dashboard_tag: %w", err) + } + + return nil }