mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-01 19:47:37 +03:00
122 lines
4.6 KiB
Go
122 lines
4.6 KiB
Go
package persistence
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"github.com/navidrome/navidrome/db"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// When creating migrations that change existing columns, it is easy to miss the original collation of a column.
|
|
// These tests enforce that the required collation of the columns and indexes in the database are kept in place.
|
|
// This is important to ensure that the database can perform fast case-insensitive searches and sorts.
|
|
var _ = Describe("Collation", func() {
|
|
conn := db.Db()
|
|
DescribeTable("Column collation",
|
|
func(table, column string) {
|
|
Expect(checkCollation(conn, table, column)).To(Succeed())
|
|
},
|
|
Entry("artist.order_artist_name", "artist", "order_artist_name"),
|
|
Entry("artist.sort_artist_name", "artist", "sort_artist_name"),
|
|
Entry("album.order_album_name", "album", "order_album_name"),
|
|
Entry("album.order_album_artist_name", "album", "order_album_artist_name"),
|
|
Entry("album.sort_album_name", "album", "sort_album_name"),
|
|
Entry("album.sort_album_artist_name", "album", "sort_album_artist_name"),
|
|
Entry("media_file.order_title", "media_file", "order_title"),
|
|
Entry("media_file.order_album_name", "media_file", "order_album_name"),
|
|
Entry("media_file.order_artist_name", "media_file", "order_artist_name"),
|
|
Entry("media_file.sort_title", "media_file", "sort_title"),
|
|
Entry("media_file.sort_album_name", "media_file", "sort_album_name"),
|
|
Entry("media_file.sort_artist_name", "media_file", "sort_artist_name"),
|
|
Entry("radio.name", "radio", "name"),
|
|
Entry("user.name", "user", "name"),
|
|
)
|
|
|
|
DescribeTable("Index collation",
|
|
func(table, column string) {
|
|
Expect(checkIndexUsage(conn, table, column)).To(Succeed())
|
|
},
|
|
Entry("artist.order_artist_name", "artist", "order_artist_name collate nocase"),
|
|
Entry("artist.sort_artist_name", "artist", "coalesce(nullif(sort_artist_name,''),order_artist_name) collate nocase"),
|
|
Entry("album.order_album_name", "album", "order_album_name collate nocase"),
|
|
Entry("album.order_album_artist_name", "album", "order_album_artist_name collate nocase"),
|
|
Entry("album.sort_album_name", "album", "coalesce(nullif(sort_album_name,''),order_album_name) collate nocase"),
|
|
Entry("album.sort_album_artist_name", "album", "coalesce(nullif(sort_album_artist_name,''),order_album_artist_name) collate nocase"),
|
|
Entry("media_file.order_title", "media_file", "order_title collate nocase"),
|
|
Entry("media_file.order_album_name", "media_file", "order_album_name collate nocase"),
|
|
Entry("media_file.order_artist_name", "media_file", "order_artist_name collate nocase"),
|
|
Entry("media_file.sort_title", "media_file", "coalesce(nullif(sort_title,''),order_title) collate nocase"),
|
|
Entry("media_file.sort_album_name", "media_file", "coalesce(nullif(sort_album_name,''),order_album_name) collate nocase"),
|
|
Entry("media_file.sort_artist_name", "media_file", "coalesce(nullif(sort_artist_name,''),order_artist_name) collate nocase"),
|
|
Entry("media_file.path", "media_file", "path collate nocase"),
|
|
Entry("radio.name", "radio", "name collate nocase"),
|
|
Entry("user.user_name", "user", "user_name collate nocase"),
|
|
)
|
|
})
|
|
|
|
func checkIndexUsage(conn *sql.DB, table string, column string) error {
|
|
rows, err := conn.Query(fmt.Sprintf(`
|
|
explain query plan select * from %[1]s
|
|
where %[2]s = 'test'
|
|
order by %[2]s`, table, column))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rows.Next() {
|
|
var dummy int
|
|
var detail string
|
|
err = rows.Scan(&dummy, &dummy, &dummy, &detail)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if ok, _ := regexp.MatchString("SEARCH.*USING INDEX", detail); ok {
|
|
return nil
|
|
} else {
|
|
return fmt.Errorf("INDEX for '%s' not used: %s", column, detail)
|
|
}
|
|
}
|
|
return errors.New("no rows returned")
|
|
}
|
|
|
|
func checkCollation(conn *sql.DB, table string, column string) error {
|
|
rows, err := conn.Query(fmt.Sprintf("SELECT sql FROM sqlite_master WHERE type='table' AND tbl_name='%s'", table))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
err = rows.Err()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if rows.Next() {
|
|
var res string
|
|
err = rows.Scan(&res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
re := regexp.MustCompile(fmt.Sprintf(`(?i)\b%s\b.*varchar`, column))
|
|
if !re.MatchString(res) {
|
|
return fmt.Errorf("column '%s' not found in table '%s'", column, table)
|
|
}
|
|
re = regexp.MustCompile(fmt.Sprintf(`(?i)\b%s\b.*collate\s+NOCASE`, column))
|
|
if re.MatchString(res) {
|
|
return nil
|
|
}
|
|
} else {
|
|
return fmt.Errorf("table '%s' not found", table)
|
|
}
|
|
return fmt.Errorf("column '%s' in table '%s' does not have NOCASE collation", column, table)
|
|
}
|