Merge branch 'master' into develop
This commit is contained in:
@@ -2,9 +2,11 @@ package sqlstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
@@ -13,19 +15,94 @@ type SqlAnnotationRepo struct {
|
||||
|
||||
func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
|
||||
tags := models.ParseTagPairs(item.Tags)
|
||||
item.Tags = models.JoinTagPairs(tags)
|
||||
if _, err := sess.Table("annotation").Insert(item); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
if tags, err := r.ensureTagsExist(sess, tags); err != nil {
|
||||
return err
|
||||
} else {
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", item.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Will insert if needed any new key/value pars and return ids
|
||||
func (r *SqlAnnotationRepo) ensureTagsExist(sess *DBSession, tags []*models.Tag) ([]*models.Tag, error) {
|
||||
for _, tag := range tags {
|
||||
var existingTag models.Tag
|
||||
|
||||
// check if it exists
|
||||
if exists, err := sess.Table("tag").Where("key=? AND value=?", tag.Key, tag.Value).Get(&existingTag); err != nil {
|
||||
return nil, err
|
||||
} else if exists {
|
||||
tag.Id = existingTag.Id
|
||||
} else {
|
||||
if _, err := sess.Table("tag").Insert(tag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
var (
|
||||
isExist bool
|
||||
err error
|
||||
)
|
||||
existing := new(annotations.Item)
|
||||
|
||||
if _, err := sess.Table("annotation").Id(item.Id).Update(item); err != nil {
|
||||
if item.Id == 0 && item.RegionId != 0 {
|
||||
// Update region end time
|
||||
isExist, err = sess.Table("annotation").Where("region_id=? AND id!=? AND org_id=?", item.RegionId, item.RegionId, item.OrgId).Get(existing)
|
||||
} else {
|
||||
isExist, err = sess.Table("annotation").Where("id=? AND org_id=?", item.Id, item.OrgId).Get(existing)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isExist {
|
||||
return errors.New("Annotation not found")
|
||||
}
|
||||
|
||||
existing.Epoch = item.Epoch
|
||||
existing.Text = item.Text
|
||||
if item.RegionId != 0 {
|
||||
existing.RegionId = item.RegionId
|
||||
}
|
||||
|
||||
if item.Tags != nil {
|
||||
if tags, err := r.ensureTagsExist(sess, models.ParseTagPairs(item.Tags)); err != nil {
|
||||
return err
|
||||
} else {
|
||||
if _, err := sess.Exec("DELETE FROM annotation_tag WHERE annotation_id = ?", existing.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, tag := range tags {
|
||||
if _, err := sess.Exec("INSERT INTO annotation_tag (annotation_id, tag_id) VALUES(?,?)", existing.Id, tag.Id); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
existing.Tags = item.Tags
|
||||
|
||||
if _, err := sess.Table("annotation").Id(existing.Id).Cols("epoch", "text", "region_id", "tags").Update(existing); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -33,51 +110,79 @@ func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
|
||||
})
|
||||
}
|
||||
|
||||
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
|
||||
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.ItemDTO, error) {
|
||||
var sql bytes.Buffer
|
||||
params := make([]interface{}, 0)
|
||||
|
||||
sql.WriteString(`SELECT *
|
||||
from annotation
|
||||
`)
|
||||
sql.WriteString(`
|
||||
SELECT
|
||||
annotation.id,
|
||||
annotation.epoch as time,
|
||||
annotation.dashboard_id,
|
||||
annotation.panel_id,
|
||||
annotation.new_state,
|
||||
annotation.prev_state,
|
||||
annotation.alert_id,
|
||||
annotation.region_id,
|
||||
annotation.text,
|
||||
annotation.tags,
|
||||
annotation.data,
|
||||
usr.email,
|
||||
usr.login,
|
||||
alert.name as alert_name
|
||||
FROM annotation
|
||||
LEFT OUTER JOIN ` + dialect.Quote("user") + ` as usr on usr.id = annotation.user_id
|
||||
LEFT OUTER JOIN alert on alert.id = annotation.alert_id
|
||||
`)
|
||||
|
||||
sql.WriteString(`WHERE org_id = ?`)
|
||||
sql.WriteString(`WHERE annotation.org_id = ?`)
|
||||
params = append(params, query.OrgId)
|
||||
|
||||
if query.AlertId != 0 {
|
||||
sql.WriteString(` AND alert_id = ?`)
|
||||
params = append(params, query.AlertId)
|
||||
}
|
||||
|
||||
if query.AlertId != 0 {
|
||||
sql.WriteString(` AND alert_id = ?`)
|
||||
sql.WriteString(` AND annotation.alert_id = ?`)
|
||||
params = append(params, query.AlertId)
|
||||
}
|
||||
|
||||
if query.DashboardId != 0 {
|
||||
sql.WriteString(` AND dashboard_id = ?`)
|
||||
sql.WriteString(` AND annotation.dashboard_id = ?`)
|
||||
params = append(params, query.DashboardId)
|
||||
}
|
||||
|
||||
if query.PanelId != 0 {
|
||||
sql.WriteString(` AND panel_id = ?`)
|
||||
sql.WriteString(` AND annotation.panel_id = ?`)
|
||||
params = append(params, query.PanelId)
|
||||
}
|
||||
|
||||
if query.From > 0 && query.To > 0 {
|
||||
sql.WriteString(` AND epoch BETWEEN ? AND ?`)
|
||||
sql.WriteString(` AND annotation.epoch BETWEEN ? AND ?`)
|
||||
params = append(params, query.From, query.To)
|
||||
}
|
||||
|
||||
if query.Type != "" {
|
||||
sql.WriteString(` AND type = ?`)
|
||||
params = append(params, string(query.Type))
|
||||
}
|
||||
if len(query.Tags) > 0 {
|
||||
keyValueFilters := []string{}
|
||||
|
||||
if len(query.NewState) > 0 {
|
||||
sql.WriteString(` AND new_state IN (?` + strings.Repeat(",?", len(query.NewState)-1) + ")")
|
||||
for _, v := range query.NewState {
|
||||
params = append(params, v)
|
||||
tags := models.ParseTagPairs(query.Tags)
|
||||
for _, tag := range tags {
|
||||
if tag.Value == "" {
|
||||
keyValueFilters = append(keyValueFilters, "(tag.key = ?)")
|
||||
params = append(params, tag.Key)
|
||||
} else {
|
||||
keyValueFilters = append(keyValueFilters, "(tag.key = ? AND tag.value = ?)")
|
||||
params = append(params, tag.Key, tag.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(tags) > 0 {
|
||||
tagsSubQuery := fmt.Sprintf(`
|
||||
SELECT SUM(1) FROM annotation_tag at
|
||||
INNER JOIN tag on tag.id = at.tag_id
|
||||
WHERE at.annotation_id = annotation.id
|
||||
AND (
|
||||
%s
|
||||
)
|
||||
`, strings.Join(keyValueFilters, " OR "))
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" AND (%s) = %d ", tagsSubQuery, len(tags)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +192,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
|
||||
sql.WriteString(fmt.Sprintf(" ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||
|
||||
items := make([]*annotations.Item, 0)
|
||||
items := make([]*annotations.ItemDTO, 0)
|
||||
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -97,11 +202,31 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
||||
|
||||
func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
var (
|
||||
sql string
|
||||
annoTagSql string
|
||||
queryParams []interface{}
|
||||
)
|
||||
|
||||
sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
|
||||
if params.RegionId != 0 {
|
||||
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE region_id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE region_id = ?"
|
||||
queryParams = []interface{}{params.RegionId}
|
||||
} else if params.Id != 0 {
|
||||
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE id = ?"
|
||||
queryParams = []interface{}{params.Id}
|
||||
} else {
|
||||
annoTagSql = "DELETE FROM annotation_tag WHERE annotation_id IN (SELECT id FROM annotation WHERE dashboard_id = ? AND panel_id = ?)"
|
||||
sql = "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
|
||||
queryParams = []interface{}{params.DashboardId, params.PanelId}
|
||||
}
|
||||
|
||||
_, err := sess.Exec(sql, params.DashboardId, params.PanelId)
|
||||
if err != nil {
|
||||
if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(sql, queryParams...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
)
|
||||
|
||||
func TestSavingTags(t *testing.T) {
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
repo := SqlAnnotationRepo{}
|
||||
|
||||
Convey("Can save tags", func() {
|
||||
tagPairs := []*models.Tag{
|
||||
{Key: "outage"},
|
||||
{Key: "type", Value: "outage"},
|
||||
{Key: "server", Value: "server-1"},
|
||||
{Key: "error"},
|
||||
}
|
||||
tags, err := repo.ensureTagsExist(newSession(), tagPairs)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(len(tags), ShouldEqual, 4)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestAnnotations(t *testing.T) {
|
||||
Convey("Testing annotation saving/loading", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
repo := SqlAnnotationRepo{}
|
||||
|
||||
Convey("Can save annotation", func() {
|
||||
err := repo.Save(&annotations.Item{
|
||||
OrgId: 1,
|
||||
UserId: 1,
|
||||
DashboardId: 1,
|
||||
Text: "hello",
|
||||
Epoch: 10,
|
||||
Tags: []string{"outage", "error", "type:outage", "server:server-1"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can query for annotation", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 1)
|
||||
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Tags, ShouldResemble, []string{"outage", "error", "type:outage", "server:server-1"})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Should not find any when item is outside time range", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 12,
|
||||
To: 15,
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 0)
|
||||
})
|
||||
|
||||
Convey("Should not find one when tag filter does not match", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"asd"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 0)
|
||||
})
|
||||
|
||||
Convey("Should find one when all tag filters does match", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"outage", "error"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 1)
|
||||
})
|
||||
|
||||
Convey("Should find one when all key value tag filters does match", func() {
|
||||
items, err := repo.Find(&annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 1,
|
||||
To: 15,
|
||||
Tags: []string{"type:outage", "server:server-1"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(items, ShouldHaveLength, 1)
|
||||
})
|
||||
|
||||
Convey("Can update annotation and remove all tags", func() {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
}
|
||||
items, err := repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Update(&annotations.Item{
|
||||
Id: annotationId,
|
||||
OrgId: 1,
|
||||
Text: "something new",
|
||||
Tags: []string{},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
items, err = repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Id, ShouldEqual, annotationId)
|
||||
So(len(items[0].Tags), ShouldEqual, 0)
|
||||
So(items[0].Text, ShouldEqual, "something new")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can update annotation with new tags", func() {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
}
|
||||
items, err := repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Update(&annotations.Item{
|
||||
Id: annotationId,
|
||||
OrgId: 1,
|
||||
Text: "something new",
|
||||
Tags: []string{"newtag1", "newtag2"},
|
||||
})
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
items, err = repo.Find(query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can read tags", func() {
|
||||
So(items[0].Id, ShouldEqual, annotationId)
|
||||
So(items[0].Tags, ShouldResemble, []string{"newtag1", "newtag2"})
|
||||
So(items[0].Text, ShouldEqual, "something new")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can delete annotation", func() {
|
||||
query := &annotations.ItemQuery{
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
From: 0,
|
||||
To: 15,
|
||||
}
|
||||
items, err := repo.Find(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
annotationId := items[0].Id
|
||||
|
||||
err = repo.Delete(&annotations.DeleteParams{Id: annotationId})
|
||||
|
||||
items, err = repo.Find(query)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Should be deleted", func() {
|
||||
So(len(items), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -378,6 +378,7 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard WHERE folder_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
|
||||
@@ -57,4 +57,37 @@ func addAnnotationMig(mg *Migrator) {
|
||||
mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{
|
||||
Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0",
|
||||
}))
|
||||
|
||||
categoryIdIndex := &Index{Cols: []string{"org_id", "category_id"}, Type: IndexType}
|
||||
mg.AddMigration("Drop category_id index", NewDropIndexMigration(table, categoryIdIndex))
|
||||
|
||||
mg.AddMigration("Add column tags to annotation table", NewAddColumnMigration(table, &Column{
|
||||
Name: "tags", Type: DB_NVarchar, Nullable: true, Length: 500,
|
||||
}))
|
||||
|
||||
///
|
||||
/// Annotation tag
|
||||
///
|
||||
annotationTagTable := Table{
|
||||
Name: "annotation_tag",
|
||||
Columns: []*Column{
|
||||
{Name: "annotation_id", Type: DB_BigInt, Nullable: false},
|
||||
{Name: "tag_id", Type: DB_BigInt, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"annotation_id", "tag_id"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
mg.AddMigration("Create annotation_tag table v2", NewAddTableMigration(annotationTagTable))
|
||||
mg.AddMigration("Add unique index annotation_tag.annotation_id_tag_id", NewAddIndexMigration(annotationTagTable, annotationTagTable.Indices[0]))
|
||||
|
||||
//
|
||||
// clear alert text
|
||||
//
|
||||
updateTextFieldSql := "UPDATE annotation SET TEXT = '' WHERE alert_id > 0"
|
||||
mg.AddMigration("Update alert annotations and set TEXT to empty", new(RawSqlMigration).
|
||||
Sqlite(updateTextFieldSql).
|
||||
Postgres(updateTextFieldSql).
|
||||
Mysql(updateTextFieldSql))
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ func AddMigrations(mg *Migrator) {
|
||||
addDashboardVersionMigration(mg)
|
||||
addUserGroupMigrations(mg)
|
||||
addDashboardAclMigrations(mg)
|
||||
addTagMigration(mg)
|
||||
}
|
||||
|
||||
func addMigrationLogMigrations(mg *Migrator) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
package migrations
|
||||
|
||||
import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
|
||||
|
||||
func addTagMigration(mg *Migrator) {
|
||||
|
||||
tagTable := Table{
|
||||
Name: "tag",
|
||||
Columns: []*Column{
|
||||
{Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
|
||||
{Name: "key", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||
{Name: "value", Type: DB_NVarchar, Length: 100, Nullable: false},
|
||||
},
|
||||
Indices: []*Index{
|
||||
{Cols: []string{"key", "value"}, Type: UniqueIndex},
|
||||
},
|
||||
}
|
||||
|
||||
// create table
|
||||
mg.AddMigration("create tag table", NewAddTableMigration(tagTable))
|
||||
|
||||
// create indices
|
||||
mg.AddMigration("add index tag.key_value", NewAddIndexMigration(tagTable, tagTable.Indices[0]))
|
||||
}
|
||||
@@ -104,7 +104,7 @@ func (db *Postgres) SqlType(c *Column) string {
|
||||
|
||||
func (db *Postgres) TableCheckSql(tableName string) (string, []interface{}) {
|
||||
args := []interface{}{"grafana", tableName}
|
||||
sql := "SELECT `TABLE_NAME` from `INFORMATION_SCHEMA`.`TABLES` WHERE `TABLE_SCHEMA`=? and `TABLE_NAME`=?"
|
||||
sql := "SELECT table_name FROM information_schema.tables WHERE table_schema=? and table_name=?"
|
||||
return sql, args
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user