This commit is contained in:
Keith Martin 2026-01-22 04:51:53 +01:00 committed by GitHub
commit d7125fc997
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 404 additions and 244 deletions

View file

@ -23,6 +23,7 @@ import (
clusternode "github.com/photoprism/photoprism/internal/service/cluster/node"
"github.com/photoprism/photoprism/internal/service/cluster/theme"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/http/header"
"github.com/photoprism/photoprism/pkg/log/status"
@ -449,7 +450,7 @@ func persistRegisterResponse(conf *config.Config, resp *cluster.RegisterResponse
// DB settings (MySQL/MariaDB only)
if resp.Database.Name != "" && resp.Database.User != "" {
updates.SetDatabaseDriver(config.MySQL)
updates.SetDatabaseDriver(enum.MySQL)
updates.SetDatabaseName(resp.Database.Name)
updates.SetDatabaseServer(fmt.Sprintf("%s:%d", resp.Database.Host, resp.Database.Port))
updates.SetDatabaseUser(resp.Database.User)

View file

@ -61,6 +61,7 @@ import (
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/checksum"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/i18n"
"github.com/photoprism/photoprism/pkg/rnd"
@ -688,7 +689,7 @@ func (c *Config) IndexWorkers() int {
}
// Limit number of workers when using SQLite3 to avoid database locking issues.
if c.DatabaseDriver() == SQLite3 && (cores >= 8 && c.options.IndexWorkers <= 0 || c.options.IndexWorkers > 4) {
if c.DatabaseDriver() == enum.SQLite3 && (cores >= 8 && c.options.IndexWorkers <= 0 || c.options.IndexWorkers > 4) {
return 4
}

View file

@ -22,35 +22,26 @@ import (
"github.com/photoprism/photoprism/internal/service/cluster"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/dsn"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/txt"
)
// SQL Databases.
// TODO: PostgreSQL support requires upgrading GORM, so generic column data types can be used.
const (
Auto = "auto"
MySQL = dsn.DriverMySQL
MariaDB = dsn.DriverMariaDB
Postgres = dsn.DriverPostgres
SQLite3 = dsn.DriverSQLite3
)
// DatabaseDriver returns the database driver name.
func (c *Config) DatabaseDriver() string {
c.normalizeDatabaseDSN()
switch strings.ToLower(c.options.DatabaseDriver) {
case MySQL, MariaDB:
c.options.DatabaseDriver = MySQL
case SQLite3, "sqlite", "test", "file", "":
c.options.DatabaseDriver = SQLite3
case enum.MySQL, enum.MariaDB:
c.options.DatabaseDriver = enum.MySQL
case enum.SQLite3, "sqlite", "test", "file", "":
c.options.DatabaseDriver = enum.SQLite3
case "tidb":
log.Warnf("config: database driver 'tidb' is deprecated, using sqlite")
c.options.DatabaseDriver = SQLite3
c.options.DatabaseDriver = enum.SQLite3
c.options.DatabaseDSN = ""
default:
log.Warnf("config: unsupported database driver %s, using sqlite", c.options.DatabaseDriver)
c.options.DatabaseDriver = SQLite3
c.options.DatabaseDriver = enum.SQLite3
c.options.DatabaseDSN = ""
}
@ -60,9 +51,9 @@ func (c *Config) DatabaseDriver() string {
// DatabaseDriverName returns the formatted database driver name.
func (c *Config) DatabaseDriverName() string {
switch c.DatabaseDriver() {
case MySQL, MariaDB:
case enum.MySQL, enum.MariaDB:
return "MariaDB"
case SQLite3, "sqlite", "test", "file", "":
case enum.SQLite3, "sqlite", "test", "file", "":
return "SQLite"
case "tidb":
return "TiDB"
@ -92,7 +83,7 @@ func (c *Config) DatabaseSsl() bool {
}
switch c.DatabaseDriver() {
case MySQL:
case enum.MySQL:
// see https://mariadb.org/mission-impossible-zero-configuration-ssl/
return c.IsDatabaseVersion("v11.4")
default:
@ -115,7 +106,7 @@ func (c *Config) DatabaseDSN() string {
// Generate matching database DSN based on the configured database driver.
if c.NoDatabaseDSN() {
switch c.DatabaseDriver() {
case MySQL:
case enum.MySQL:
databaseServer := c.DatabaseServer()
// Connect via Unix Domain Socket?
@ -132,10 +123,10 @@ func (c *Config) DatabaseDSN() string {
c.DatabasePassword(),
databaseServer,
c.DatabaseName(),
dsn.Params[dsn.DriverMySQL],
dsn.Params[enum.MySQL],
c.DatabaseTimeout(),
)
case Postgres:
case enum.Postgres:
return fmt.Sprintf(
"user=%s password=%s dbname=%s host=%s port=%d connect_timeout=%d %s",
c.DatabaseUser(),
@ -144,10 +135,10 @@ func (c *Config) DatabaseDSN() string {
c.DatabaseHost(),
c.DatabasePort(),
c.DatabaseTimeout(),
dsn.Params[dsn.DriverPostgres],
dsn.Params[enum.Postgres],
)
case SQLite3:
return filepath.Join(c.StoragePath(), fmt.Sprintf("index.db?%s", dsn.Params[dsn.DriverSQLite3]))
case enum.SQLite3:
return filepath.Join(c.StoragePath(), fmt.Sprintf("index.db?%s", dsn.Params[enum.SQLite3]))
default:
log.Errorf("config: empty database dsn")
return ""
@ -155,11 +146,11 @@ func (c *Config) DatabaseDSN() string {
}
// If missing, add the required parameters to the configured MySQL/MariaDB DSN.
if c.DatabaseDriver() == MySQL && !strings.Contains(c.options.DatabaseDSN, "?") {
if c.DatabaseDriver() == enum.MySQL && !strings.Contains(c.options.DatabaseDSN, "?") {
c.options.DatabaseDSN = fmt.Sprintf(
"%s?%s&timeout=%ds",
c.options.DatabaseDSN,
dsn.Params[dsn.DriverMySQL],
dsn.Params[enum.MySQL],
c.DatabaseTimeout())
}
@ -181,7 +172,7 @@ func (c *Config) HasDatabaseDSN() bool {
// ReportDatabaseDSN checks if the database data source name (DSN) should be reported
// instead of database name, server, user, and password.
func (c *Config) ReportDatabaseDSN() bool {
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return true
}
@ -192,7 +183,7 @@ func (c *Config) ReportDatabaseDSN() bool {
func (c *Config) ParseDatabaseDSN() {
if c.NoDatabaseDSN() {
return
} else if c.options.DatabaseServer != "" && c.DatabaseDriver() == SQLite3 {
} else if c.options.DatabaseServer != "" && c.DatabaseDriver() == enum.SQLite3 {
return
}
@ -214,7 +205,7 @@ func (c *Config) DatabaseFile() string {
func (c *Config) DatabaseServer() string {
c.ParseDatabaseDSN()
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return ""
} else if c.options.DatabaseServer == "" {
return localhost
@ -227,7 +218,7 @@ func (c *Config) DatabaseServer() string {
func (c *Config) DatabaseHost() string {
c.ParseDatabaseDSN()
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return ""
}
@ -239,7 +230,7 @@ func (c *Config) DatabaseHost() string {
func (c *Config) DatabasePort() int {
c.ParseDatabaseDSN()
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return 0
}
@ -249,7 +240,7 @@ func (c *Config) DatabasePort() int {
// DatabasePortString the database server port as string.
func (c *Config) DatabasePortString() string {
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return ""
}
@ -260,7 +251,7 @@ func (c *Config) DatabasePortString() string {
func (c *Config) DatabaseName() string {
c.ParseDatabaseDSN()
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return c.DatabaseDSN()
} else if c.options.DatabaseName == "" {
return "photoprism"
@ -271,7 +262,7 @@ func (c *Config) DatabaseName() string {
// DatabaseUser returns the database user name.
func (c *Config) DatabaseUser() string {
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return ""
}
@ -286,7 +277,7 @@ func (c *Config) DatabaseUser() string {
// DatabasePassword returns the database user password.
func (c *Config) DatabasePassword() string {
if c.DatabaseDriver() == SQLite3 {
if c.DatabaseDriver() == enum.SQLite3 {
return ""
}
@ -358,7 +349,7 @@ func (c *Config) DatabaseProvisionPrefix() string {
// ShouldAutoRotateDatabase decides whether callers should request DB rotation automatically.
// It is used by both the CLI and node bootstrap to avoid unnecessary provisioning calls.
func (c *Config) ShouldAutoRotateDatabase() bool {
if c.Portal() || c.DatabaseDriver() != MySQL {
if c.Portal() || c.DatabaseDriver() != enum.MySQL {
return false
}
@ -440,11 +431,11 @@ func (c *Config) CloseDb() error {
// SetDbOptions sets the database collation to unicode if supported.
func (c *Config) SetDbOptions() {
switch c.DatabaseDriver() {
case MySQL, MariaDB:
case enum.MySQL, enum.MariaDB:
c.Db().Set("gorm:table_options", "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci")
case Postgres:
case enum.Postgres:
// Ignore for now.
case SQLite3:
case enum.SQLite3:
// Not required as Unicode is default.
}
}
@ -515,7 +506,7 @@ func (c *Config) checkDb(db *gorm.DB) error {
}
switch c.DatabaseDriver() {
case MySQL:
case enum.MySQL:
type Res struct {
Value string `gorm:"column:Value;"`
}
@ -544,7 +535,7 @@ func (c *Config) checkDb(db *gorm.DB) error {
case !c.IsDatabaseVersion("v10.5.12"):
return fmt.Errorf("config: MariaDB %s is not supported, see https://docs.photoprism.app/getting-started/#databases", c.dbVersion)
}
case SQLite3:
case enum.SQLite3:
type Res struct {
Value string `gorm:"column:Value;"`
}

View file

@ -6,6 +6,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/internal/service/cluster"
)
@ -26,16 +28,16 @@ func TestConfig_DatabaseDriver(t *testing.T) {
c := NewConfig(CliTestContext())
resetDatabaseOptions(c)
assert.Equal(t, SQLite3, c.DatabaseDriver())
assert.Equal(t, enum.SQLite3, c.DatabaseDriver())
})
t.Run("NormalizesDeprecatedDSN", func(t *testing.T) {
c := NewConfig(CliTestContext())
resetDatabaseOptions(c)
c.options.DatabaseDriver = MySQL
c.options.DatabaseDriver = enum.MySQL
c.options.Deprecated.DatabaseDsn = "user:pass@tcp(localhost:3306)/photoprism"
assert.Equal(t, MySQL, c.DatabaseDriver())
assert.Equal(t, enum.MySQL, c.DatabaseDriver())
assert.Equal(t, "user:pass@tcp(localhost:3306)/photoprism", c.options.DatabaseDSN)
assert.Empty(t, c.options.Deprecated.DatabaseDsn)
})
@ -65,7 +67,7 @@ func TestConfig_normalizeDatabaseDSN(t *testing.T) {
c := NewConfig(CliTestContext())
c.options.Deprecated.DatabaseDsn = "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true"
c.options.DatabaseDriver = MySQL
c.options.DatabaseDriver = enum.MySQL
assert.Equal(t, "honeypot:1234", c.DatabaseServer())
assert.Equal(t, "honeypot", c.DatabaseHost())
@ -79,7 +81,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) {
c := NewConfig(CliTestContext())
c.options.DatabaseDSN = "foo:b@r@tcp(honeypot:1234)/baz?charset=utf8mb4,utf8&parseTime=true"
c.options.DatabaseDriver = SQLite3
c.options.DatabaseDriver = enum.SQLite3
assert.Equal(t, "", c.DatabaseServer())
assert.Equal(t, "", c.DatabaseHost())
@ -88,7 +90,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) {
assert.Equal(t, "", c.DatabaseUser())
assert.Equal(t, "", c.DatabasePassword())
c.options.DatabaseDriver = MySQL
c.options.DatabaseDriver = enum.MySQL
assert.Equal(t, "honeypot:1234", c.DatabaseServer())
assert.Equal(t, "honeypot", c.DatabaseHost())
@ -97,7 +99,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) {
assert.Equal(t, "foo", c.DatabaseUser())
assert.Equal(t, "b@r", c.DatabasePassword())
c.options.DatabaseDriver = SQLite3
c.options.DatabaseDriver = enum.SQLite3
assert.Equal(t, "", c.DatabaseServer())
assert.Equal(t, "", c.DatabaseHost())
@ -110,7 +112,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) {
target := NewConfig(CliTestContext())
resetDatabaseOptions(target)
target.options.DatabaseDriver = MySQL
target.options.DatabaseDriver = enum.MySQL
target.options.DatabaseServer = "db.internal:3306"
target.options.DatabaseName = "photoprism"
target.options.DatabaseUser = "app"
@ -129,7 +131,7 @@ func TestConfig_ParseDatabaseDSN(t *testing.T) {
cfg := NewConfig(CliTestContext())
resetDatabaseOptions(cfg)
cfg.options.DatabaseDriver = SQLite3
cfg.options.DatabaseDriver = enum.SQLite3
cfg.options.DatabaseDSN = "file:/data/app.db?_busy_timeout=5000"
cfg.options.DatabaseServer = "/tmp/mysql.sock"
cfg.options.DatabaseName = "existing-name"
@ -191,9 +193,9 @@ func TestConfig_DatabasePassword(t *testing.T) {
// Test setting the password via secret file.
_ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "testdata/secret_database")
assert.Equal(t, "", c.DatabasePassword())
c.Options().DatabaseDriver = MySQL
c.Options().DatabaseDriver = enum.MySQL
assert.Equal(t, "StoryOfAmélie", c.DatabasePassword())
c.Options().DatabaseDriver = SQLite3
c.Options().DatabaseDriver = enum.SQLite3
_ = os.Setenv(FlagFileVar("DATABASE_PASSWORD"), "")
assert.Equal(t, "", c.DatabasePassword())
@ -222,19 +224,19 @@ func TestShouldAutoRotateDatabase(t *testing.T) {
t.Run("PortalAlwaysFalse", func(t *testing.T) {
conf := NewMinimalTestConfig(t.TempDir())
conf.Options().NodeRole = cluster.RolePortal
conf.Options().DatabaseDriver = MySQL
conf.Options().DatabaseDriver = enum.MySQL
assert.False(t, conf.ShouldAutoRotateDatabase())
})
t.Run("NonMySQLDriverFalse", func(t *testing.T) {
conf := NewMinimalTestConfig(t.TempDir())
conf.Options().DatabaseDriver = SQLite3
conf.Options().DatabaseDriver = enum.SQLite3
assert.False(t, conf.ShouldAutoRotateDatabase())
})
t.Run("MySQLMissingFieldsTrue", func(t *testing.T) {
conf := NewMinimalTestConfig(t.TempDir())
conf.Options().DatabaseDriver = MySQL
conf.Options().DatabaseDriver = enum.MySQL
conf.Options().DatabaseName = "photoprism"
conf.Options().DatabaseUser = ""
conf.Options().DatabasePassword = ""
@ -246,7 +248,7 @@ func TestConfig_DatabaseDSN(t *testing.T) {
c := NewConfig(CliTestContext())
resetDatabaseOptions(c)
driver := c.DatabaseDriver()
assert.Equal(t, SQLite3, driver)
assert.Equal(t, enum.SQLite3, driver)
c.options.DatabaseDSN = ""
c.options.DatabaseDriver = "MariaDB"
assert.Equal(t, "photoprism:@tcp(localhost)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=15s", c.DatabaseDSN())
@ -263,7 +265,7 @@ func TestConfig_DatabaseDSN(t *testing.T) {
conf := NewConfig(CliTestContext())
resetDatabaseOptions(conf)
conf.options.DatabaseDriver = MySQL
conf.options.DatabaseDriver = enum.MySQL
conf.options.DatabaseServer = "proxy.internal:6032"
conf.options.DatabaseName = "tenantdb"
conf.options.DatabaseUser = "tenant"
@ -279,7 +281,7 @@ func TestConfig_DatabaseDSN(t *testing.T) {
conf := NewConfig(CliTestContext())
resetDatabaseOptions(conf)
conf.options.DatabaseDriver = MySQL
conf.options.DatabaseDriver = enum.MySQL
conf.options.DatabaseServer = "/var/run/mysql.sock"
conf.options.DatabaseName = "tenantdb"
conf.options.DatabaseUser = "tenant"
@ -305,7 +307,7 @@ func TestConfig_DatabaseDSNFlags(t *testing.T) {
conf := NewConfig(CliTestContext())
resetDatabaseOptions(conf)
conf.options.DatabaseDriver = MySQL
conf.options.DatabaseDriver = enum.MySQL
conf.options.Deprecated.DatabaseDsn = "user:pass@tcp(db.internal:3306)/photoprism"
assert.False(t, conf.NoDatabaseDSN())
@ -319,10 +321,10 @@ func TestConfig_ReportDatabaseDSN(t *testing.T) {
conf := NewConfig(CliTestContext())
resetDatabaseOptions(conf)
assert.Equal(t, SQLite3, conf.DatabaseDriver())
assert.Equal(t, enum.SQLite3, conf.DatabaseDriver())
assert.True(t, conf.ReportDatabaseDSN())
conf.options.DatabaseDriver = MySQL
conf.options.DatabaseDriver = enum.MySQL
conf.options.DatabaseDSN = ""
assert.False(t, conf.ReportDatabaseDSN())
@ -335,7 +337,7 @@ func TestConfig_DatabaseFile(t *testing.T) {
// Ensure SQLite defaults
resetDatabaseOptions(c)
driver := c.DatabaseDriver()
assert.Equal(t, SQLite3, driver)
assert.Equal(t, enum.SQLite3, driver)
c.options.DatabaseDSN = ""
assert.Equal(t, ProjectRoot+"/storage/testdata/index.db", c.DatabaseFile())
assert.Equal(t, ProjectRoot+"/storage/testdata/index.db?_busy_timeout=5000", c.DatabaseDSN())

View file

@ -6,6 +6,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/enum"
)
func TestConfig_ConvertSize(t *testing.T) {
@ -48,7 +49,7 @@ func TestConfig_ThumbFilter(t *testing.T) {
assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter())
c.options.ThumbFilter = "linear"
assert.Equal(t, thumb.ResampleLinear, c.ThumbFilter())
c.options.ThumbFilter = Auto
c.options.ThumbFilter = enum.Auto
assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter())
c.options.ThumbFilter = ""
assert.Equal(t, thumb.ResampleLanczos, c.ThumbFilter())
@ -92,7 +93,7 @@ func TestConfig_PngSize(t *testing.T) {
func TestConfig_ThumbLibrary(t *testing.T) {
c := NewConfig(CliTestContext())
assert.False(t, c.DisableVips())
c.options.ThumbLibrary = Auto
c.options.ThumbLibrary = enum.Auto
assert.Equal(t, "vips", c.ThumbLibrary())
c.options.DisableVips = true
assert.Equal(t, "imaging", c.ThumbLibrary())

View file

@ -15,6 +15,7 @@ import (
"github.com/photoprism/photoprism/internal/service/hub/places"
"github.com/photoprism/photoprism/internal/thumb"
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/http/header"
"github.com/photoprism/photoprism/pkg/http/scheme"
@ -941,7 +942,7 @@ var Flags = CliFlags{
Flag: &cli.StringFlag{
Name: "database-provision-driver",
Usage: "auto-provisioning `DRIVER` (auto, mysql)",
Value: Auto,
Value: enum.Auto,
EnvVars: EnvVars("DATABASE_PROVISION_DRIVER"),
Hidden: true,
}}, {
@ -1113,7 +1114,7 @@ var Flags = CliFlags{
Name: "thumb-library",
Aliases: []string{"thumbs"},
Usage: "image processing `LIBRARY` to be used for generating thumbnails (auto, imaging, vips)",
Value: Auto,
Value: enum.Auto,
EnvVars: EnvVars("THUMB_LIBRARY"),
}}, {
Flag: &cli.StringFlag{

View file

@ -5,6 +5,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/enum"
)
func TestConfig_Report(t *testing.T) {
@ -34,7 +36,7 @@ func TestConfig_ReportDatabaseSection(t *testing.T) {
rows, _ := conf.Report()
values := collect(rows)
assert.Equal(t, SQLite3, values["database-driver"])
assert.Equal(t, enum.SQLite3, values["database-driver"])
assert.Equal(t, conf.DatabaseDSN(), values["database-dsn"])
_, hasName := values["database-name"]
assert.False(t, hasName)
@ -43,7 +45,7 @@ func TestConfig_ReportDatabaseSection(t *testing.T) {
conf := NewConfig(CliTestContext())
resetDatabaseOptions(conf)
conf.options.DatabaseDriver = MySQL
conf.options.DatabaseDriver = enum.MySQL
conf.options.DatabaseServer = "db.internal:3306"
conf.options.DatabaseName = "photoprism"
conf.options.DatabaseUser = "app"
@ -52,7 +54,7 @@ func TestConfig_ReportDatabaseSection(t *testing.T) {
rows, _ := conf.Report()
values := collect(rows)
assert.Equal(t, MySQL, values["database-driver"])
assert.Equal(t, enum.MySQL, values["database-driver"])
assert.Equal(t, "photoprism", values["database-name"])
assert.Equal(t, "db.internal:3306", values["database-server"])
assert.Equal(t, "db.internal", values["database-host"])
@ -66,13 +68,13 @@ func TestConfig_ReportDatabaseSection(t *testing.T) {
conf := NewConfig(CliTestContext())
resetDatabaseOptions(conf)
conf.options.DatabaseDriver = MySQL
conf.options.DatabaseDriver = enum.MySQL
conf.options.DatabaseDSN = "user:pass@tcp(db.internal:3306)/photoprism"
rows, _ := conf.Report()
values := collect(rows)
assert.Equal(t, MySQL, values["database-driver"])
assert.Equal(t, enum.MySQL, values["database-driver"])
assert.Equal(t, "user:***@tcp(db.internal:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true&timeout=15s", values["database-dsn"])
_, hasName := values["database-name"]
assert.False(t, hasName)

View file

@ -23,7 +23,7 @@ import (
"github.com/photoprism/photoprism/pkg/authn"
"github.com/photoprism/photoprism/pkg/capture"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/dsn"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt/report"
@ -127,19 +127,19 @@ func NewTestOptionsForPath(dbName, dataPath string) *Options {
// Set default test database driver.
if testDriver == "test" || testDriver == "sqlite" || testDriver == "" || testDsn == "" {
testDriver = dsn.DriverSQLite3
testDriver = enum.SQLite3
}
// Set default database DSN.
if testDriver == dsn.DriverSQLite3 {
if testDriver == enum.SQLite3 {
if testDsn == "" && dbName != "" {
if testDsn = fmt.Sprintf(".%s.db", clean.TypeLower(dbName)); !fs.FileExists(testDsn) {
log.Tracef("sqlite: test database %s does not already exist", clean.Log(testDsn))
} else if err := os.Remove(testDsn); err != nil {
log.Errorf("sqlite: failed to remove existing test database %s (%s)", clean.Log(testDsn), err)
}
} else if testDsn == "" || testDsn == dsn.SQLiteTestDB {
testDsn = dsn.SQLiteTestDB
} else if testDsn == "" || testDsn == enum.SQLiteTestDB {
testDsn = enum.SQLiteTestDB
if !fs.FileExists(testDsn) {
log.Tracef("sqlite: test database %s does not already exist", clean.Log(testDsn))
} else if err := os.Remove(testDsn); err != nil {
@ -205,7 +205,7 @@ func NewTestOptionsError() *Options {
OriginalsPath: dataPath + "/originals",
ImportPath: dataPath + "/import",
TempPath: dataPath + "/temp",
DatabaseDriver: SQLite3,
DatabaseDriver: enum.SQLite3,
DatabaseDSN: ".test-error.db",
}
@ -244,7 +244,7 @@ func NewMinimalTestConfigWithDb(dbName, dataPath string) *Config {
cachedDb := false
// Try to restore test db from cache.
if len(testDbCache) > 0 && c.DatabaseDriver() == SQLite3 && !fs.FileExists(c.DatabaseDSN()) {
if len(testDbCache) > 0 && c.DatabaseDriver() == enum.SQLite3 && !fs.FileExists(c.DatabaseDSN()) {
if err := os.WriteFile(c.DatabaseDSN(), testDbCache, fs.ModeFile); err != nil {
log.Warnf("config: %s (restore test database)", err)
} else {
@ -264,7 +264,7 @@ func NewMinimalTestConfigWithDb(dbName, dataPath string) *Config {
c.InitTestDb()
if testDbCache == nil && c.DatabaseDriver() == SQLite3 && fs.FileExistsNotEmpty(c.DatabaseDSN()) {
if testDbCache == nil && c.DatabaseDriver() == enum.SQLite3 && fs.FileExistsNotEmpty(c.DatabaseDSN()) {
testDbMutex.Lock()
defer testDbMutex.Unlock()

View file

@ -8,16 +8,6 @@ import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/photoprism/photoprism/pkg/dsn"
)
// Supported test databases.
const (
MySQL = dsn.DriverMySQL
SQLite3 = dsn.DriverSQLite3
SQLiteTestDB = ".test.db"
SQLiteMemoryDSN = ":memory:?cache=shared"
)
// dbConn is the global gorm.DB connection provider.

View file

@ -11,6 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/time/unix"
)
@ -103,7 +104,7 @@ func UpdateSubjectCounts(public bool) (err error) {
condition := gorm.Expr("subj_type = ?", SubjPerson)
switch DbDialect() {
case MySQL:
case enum.MySQL:
res = Db().Exec(`UPDATE ? LEFT JOIN (
SELECT m.subj_uid, COUNT(DISTINCT f.id) AS subj_files, COUNT(DISTINCT f.photo_id) AS subj_photos
FROM files f
@ -114,7 +115,7 @@ func UpdateSubjectCounts(public bool) (err error) {
SET subjects.file_count = CASE WHEN b.subj_files IS NULL THEN 0 ELSE b.subj_files END,
subjects.photo_count = CASE WHEN b.subj_photos IS NULL THEN 0 ELSE b.subj_photos END
WHERE ?`, gorm.Expr(subjTable), photosJoin, condition)
case SQLite3:
case enum.SQLite3:
// Update files count.
res = Db().Table(subjTable).
UpdateColumn("file_count", gorm.Expr("(SELECT COUNT(DISTINCT f.id)"+
@ -187,7 +188,7 @@ func UpdateLabelCounts() (err error) {
start := time.Now()
var res *gorm.DB
if IsDialect(MySQL) {
if IsDialect(enum.MySQL) {
res = Db().Exec(`UPDATE labels LEFT JOIN (
SELECT p2.label_id, COUNT(DISTINCT photo_id) AS label_photos FROM (
SELECT pl.label_id as label_id, p.id AS photo_id FROM photos p
@ -201,7 +202,7 @@ func UpdateLabelCounts() (err error) {
) p2 GROUP BY p2.label_id
) b ON b.label_id = labels.id
SET photo_count = CASE WHEN b.label_photos IS NULL THEN 0 ELSE b.label_photos END`)
} else if IsDialect(SQLite3) {
} else if IsDialect(enum.SQLite3) {
res = Db().
Table("labels").
UpdateColumn("photo_count",

View file

@ -6,6 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/entity/migrate"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
)
@ -55,13 +56,13 @@ func InitTestDb(driver, dbDsn string) *DbConn {
// Set default test database driver.
if driver == "test" || driver == "sqlite" || driver == "" || dbDsn == "" {
driver = SQLite3
driver = enum.SQLite3
}
// Set default database DSN.
if driver == SQLite3 {
if dbDsn == "" || dbDsn == SQLiteTestDB {
dbDsn = SQLiteTestDB
if driver == enum.SQLite3 {
if dbDsn == "" || dbDsn == enum.SQLiteTestDB {
dbDsn = enum.SQLiteTestDB
if !fs.FileExists(dbDsn) {
log.Debugf("sqlite: test database %s does not already exist", clean.Log(dbDsn))
} else if err := os.Remove(dbDsn); err != nil {

View file

@ -11,6 +11,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/ai/face"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -340,7 +341,7 @@ func (m *Face) RefreshPhotos() error {
var err error
switch DbDialect() {
case MySQL:
case enum.MySQL:
update := fmt.Sprintf(`UPDATE photos p JOIN files f ON f.photo_id = p.id JOIN %s m ON m.file_uid = f.file_uid
SET p.checked_at = NULL WHERE m.face_id = ?`, Marker{}.TableName())
err = UnscopedDb().Exec(update, m.ID).Error

View file

@ -17,6 +17,7 @@ import (
"github.com/photoprism/photoprism/internal/ai/face"
"github.com/photoprism/photoprism/internal/config/customize"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/media/colors"
@ -129,7 +130,7 @@ func (m File) RegenerateIndex() {
}
switch DbDialect() {
case MySQL:
case enum.MySQL:
Log("files", "regenerate photo_taken_at",
Db().Exec("UPDATE files JOIN ? p ON p.id = files.photo_id SET files.photo_taken_at = p.taken_at_local WHERE ?",
gorm.Expr(photosTable), updateWhere).Error)
@ -141,7 +142,7 @@ func (m File) RegenerateIndex() {
Log("files", "regenerate time_index",
Db().Exec("UPDATE files SET time_index = CASE WHEN media_id IS NOT NULL AND photo_taken_at IS NOT NULL THEN CONCAT(100000000000000 - CAST(photo_taken_at AS UNSIGNED), '-', media_id) ELSE NULL END WHERE ?",
updateWhere).Error)
case SQLite3:
case enum.SQLite3:
Log("files", "regenerate photo_taken_at",
Db().Exec("UPDATE files SET photo_taken_at = (SELECT p.taken_at_local FROM ? p WHERE p.id = photo_id) WHERE ?",
gorm.Expr(photosTable), updateWhere).Error)

View file

@ -14,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/thumb/crop"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -575,7 +576,7 @@ func (m *Marker) RefreshPhotos() error {
var err error
switch DbDialect() {
case MySQL:
case enum.MySQL:
err = UnscopedDb().Exec(`UPDATE photos p JOIN files f ON f.photo_id = p.id
JOIN ? m ON m.file_uid = f.file_uid SET p.checked_at = NULL
WHERE m.marker_uid = ?`,

View file

@ -1,19 +1,17 @@
package migrate
import "sync"
import (
"sync"
// Supported database dialects.
const (
MySQL = "mysql"
SQLite3 = "sqlite3"
"github.com/photoprism/photoprism/pkg/enum"
)
var Dialects = map[string]Migrations{
MySQL: DialectMySQL,
SQLite3: DialectSQLite3,
enum.MySQL: DialectMySQL,
enum.SQLite3: DialectSQLite3,
}
var once = map[string]*sync.Once{
MySQL: {},
SQLite3: {},
enum.MySQL: {},
enum.SQLite3: {},
}

View file

@ -6,6 +6,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/entity/migrate"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/jinzhu/gorm"
)
@ -17,7 +18,7 @@ func TestMySQL8(t *testing.T) {
t.Skip("skipping MySQL 8 test: PHOTOPRISM_TEST_DSN_MYSQL8 is not set")
}
dbDriver := MySQL
dbDriver := enum.MySQL
db, err := gorm.Open(dbDriver, dbDsn)
if err != nil || db == nil {

View file

@ -6,6 +6,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/geo"
"github.com/photoprism/photoprism/pkg/txt"
@ -99,14 +100,14 @@ func (m *Photo) EstimateLocation(force bool) {
var mostRecent Photos
switch DbDialect() {
case MySQL:
case enum.MySQL:
err = UnscopedDb().
Where("photo_lat <> 0 AND photo_lng <> 0").
Where("place_src <> '' AND place_src <> ? AND place_id IS NOT NULL AND place_id <> '' AND place_id <> 'zz'", SrcEstimate).
Where("taken_src <> '' AND taken_at BETWEEN CAST(? AS DATETIME) AND CAST(? AS DATETIME)", rangeMin, rangeMax).
Order(gorm.Expr("ABS(TIMESTAMPDIFF(SECOND, taken_at, ?))", m.TakenAt)).Limit(2).
Preload("Place").Find(&mostRecent).Error
case SQLite3:
case enum.SQLite3:
err = UnscopedDb().
Where("photo_lat <> 0 AND photo_lng <> 0").
Where("place_src <> '' AND place_src <> ? AND place_id IS NOT NULL AND place_id <> '' AND place_id <> 'zz'", SrcEstimate).

View file

@ -5,6 +5,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -108,11 +109,11 @@ func (m *Photo) Merge(mergeMeta, mergeUuid bool) (original Photo, merged Photos,
logResult(UnscopedDb().Exec("UPDATE photos SET photo_quality = -1, deleted_at = ? WHERE id = ?", Now(), merge.ID))
switch DbDialect() {
case MySQL:
case enum.MySQL:
logResult(UnscopedDb().Exec("UPDATE IGNORE photos_keywords SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID))
logResult(UnscopedDb().Exec("UPDATE IGNORE photos_labels SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID))
logResult(UnscopedDb().Exec("UPDATE IGNORE photos_albums SET photo_uid = ? WHERE photo_uid = ?", original.PhotoUID, merge.PhotoUID))
case SQLite3:
case enum.SQLite3:
logResult(UnscopedDb().Exec("UPDATE OR IGNORE photos_keywords SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID))
logResult(UnscopedDb().Exec("UPDATE OR IGNORE photos_labels SET photo_id = ? WHERE photo_id = ?", original.ID, merge.ID))
logResult(UnscopedDb().Exec("UPDATE OR IGNORE photos_albums SET photo_uid = ? WHERE photo_uid = ?", original.PhotoUID, merge.PhotoUID))

View file

@ -10,6 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/entity/sortby"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/media"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -120,7 +121,7 @@ func UpdateAlbumDates() error {
defer mutex.Index.Unlock()
switch DbDialect() {
case MySQL:
case enum.MySQL:
return UnscopedDb().Exec(`UPDATE albums INNER JOIN (
SELECT photo_path, MAX(taken_at_local) AS taken_max
FROM photos WHERE taken_src = 'meta' AND photos.photo_quality >= 3 AND photos.deleted_at IS NULL

View file

@ -12,6 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/media"
)
@ -45,7 +46,7 @@ func UpdateAlbumManualCovers(albums ...entity.Album) (err error) {
condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumManual, entity.SrcAuto)
switch DbDialect() {
case MySQL:
case enum.MySQL:
res = Db().Exec(`UPDATE albums LEFT JOIN (
SELECT p2.album_uid, f.file_hash FROM files f, (
SELECT pa.album_uid, max(p.id) AS photo_id FROM photos p
@ -54,7 +55,7 @@ func UpdateAlbumManualCovers(albums ...entity.Album) (err error) {
GROUP BY pa.album_uid) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?)
) b ON b.album_uid = albums.album_uid
SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition)
case SQLite3:
case enum.SQLite3:
res = Db().Table(entity.Album{}.TableName()).
UpdateColumn("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f
@ -108,7 +109,7 @@ func UpdateAlbumFolderCovers(albums ...entity.Album) (err error) {
condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumFolder, entity.SrcAuto)
switch DbDialect() {
case MySQL:
case enum.MySQL:
res = Db().Exec(`UPDATE albums LEFT JOIN (
SELECT p2.photo_path, f.file_hash FROM files f, (
SELECT p.photo_path, max(p.id) AS photo_id FROM photos p
@ -116,7 +117,7 @@ func UpdateAlbumFolderCovers(albums ...entity.Album) (err error) {
GROUP BY p.photo_path) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?)
) b ON b.photo_path = albums.album_path
SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition)
case SQLite3:
case enum.SQLite3:
res = Db().Table(entity.Album{}.TableName()).UpdateColumn("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f,(
SELECT p.photo_path, max(p.id) AS photo_id FROM photos p
@ -171,7 +172,7 @@ func UpdateAlbumMonthCovers(albums ...entity.Album) (err error) {
condition := gorm.Expr("album_type = ? AND thumb_src = ?", entity.AlbumMonth, entity.SrcAuto)
switch DbDialect() {
case MySQL:
case enum.MySQL:
res = Db().Exec(`UPDATE albums LEFT JOIN (
SELECT p2.photo_year, p2.photo_month, f.file_hash FROM files f, (
SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p
@ -179,7 +180,7 @@ func UpdateAlbumMonthCovers(albums ...entity.Album) (err error) {
GROUP BY p.photo_year, p.photo_month) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?)
) b ON b.photo_year = albums.album_year AND b.photo_month = albums.album_month
SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition)
case SQLite3:
case enum.SQLite3:
res = Db().Table(entity.Album{}.TableName()).UpdateColumn("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f,(
SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p
@ -322,7 +323,7 @@ func refreshFolderAlbumCover(album entity.Album) error {
}
switch DbDialect() {
case MySQL:
case enum.MySQL:
res := Db().Exec(`UPDATE albums LEFT JOIN (
SELECT p2.photo_path, f.file_hash FROM files f, (
SELECT p.photo_path, max(p.id) AS photo_id FROM photos p
@ -338,7 +339,7 @@ func refreshFolderAlbumCover(album entity.Album) error {
)
return res.Error
case SQLite3:
case enum.SQLite3:
res := Db().Table(entity.Album{}.TableName()).
Where("album_uid = ? AND album_type = ? AND thumb_src = ?", album.AlbumUID, entity.AlbumFolder, entity.SrcAuto).
UpdateColumn("thumb", gorm.Expr(`(
@ -366,7 +367,7 @@ func refreshMonthAlbumCover(album entity.Album) error {
}
switch DbDialect() {
case MySQL:
case enum.MySQL:
res := Db().Exec(`UPDATE albums LEFT JOIN (
SELECT p2.photo_year, p2.photo_month, f.file_hash FROM files f, (
SELECT p.photo_year, p.photo_month, max(p.id) AS photo_id FROM photos p
@ -383,7 +384,7 @@ func refreshMonthAlbumCover(album entity.Album) error {
)
return res.Error
case SQLite3:
case enum.SQLite3:
res := Db().Table(entity.Album{}.TableName()).
Where("album_uid = ? AND album_type = ? AND thumb_src = ?", album.AlbumUID, entity.AlbumMonth, entity.SrcAuto).
UpdateColumn("thumb", gorm.Expr(`(
@ -416,7 +417,7 @@ func UpdateLabelCovers() (err error) {
condition := gorm.Expr("thumb_src = ?", entity.SrcAuto)
switch DbDialect() {
case MySQL:
case enum.MySQL:
res = Db().Exec(`UPDATE labels LEFT JOIN (
SELECT p2.label_id, f.file_hash FROM files f, (
SELECT pl.label_id as label_id, max(p.id) AS photo_id FROM photos p
@ -432,7 +433,7 @@ func UpdateLabelCovers() (err error) {
) p2 WHERE p2.photo_id = f.photo_id AND f.file_primary = 1 AND f.file_error = '' AND f.file_type IN (?) AND f.file_missing = 0
) b ON b.label_id = labels.id
SET thumb = b.file_hash WHERE ?`, media.PreviewExpr, condition)
case SQLite3:
case enum.SQLite3:
res = Db().Table(entity.Label{}.TableName()).UpdateColumn("thumb", gorm.Expr(`(
SELECT f.file_hash FROM files f
JOIN photos_labels pl ON pl.label_id = labels.id AND pl.photo_id = f.photo_id AND pl.uncertainty < 100
@ -494,7 +495,7 @@ func UpdateSubjectCovers(public bool) (err error) {
// Compose SQL update query.
switch DbDialect() {
case MySQL:
case enum.MySQL:
res = Db().Exec(`UPDATE subjects LEFT JOIN (
SELECT m.subj_uid, m.q, MAX(m.thumb) AS marker_thumb
FROM markers m
@ -508,7 +509,7 @@ func UpdateSubjectCovers(public bool) (err error) {
photosJoin,
condition,
)
case SQLite3:
case enum.SQLite3:
// from := gorm.Expr(fmt.Sprintf("%s m WHERE m.subj_uid = %s.subj_uid ", markerTable, subjTable))
res = Db().Table(entity.Subject{}.TableName()).UpdateColumn("thumb",
gorm.Expr(`(

View file

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/internal/entity"
@ -98,9 +99,9 @@ func SelectedFiles(frm form.Selection, o FileSelection) (results entity.Files, e
var concat string
switch DbDialect() {
case MySQL:
case enum.MySQL:
concat = "CONCAT(a.path, '/%')"
case SQLite3:
case enum.SQLite3:
concat = "a.path || '/%'"
default:
return results, fmt.Errorf("unknown sql dialect: %s", DbDialect())

View file

@ -5,6 +5,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/mutex"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/media"
)
@ -75,7 +76,7 @@ func UpdateFolderDates() error {
defer mutex.Index.Unlock()
switch DbDialect() {
case MySQL:
case enum.MySQL:
return UnscopedDb().Exec(`UPDATE folders
INNER JOIN
(SELECT photo_path, MAX(taken_at_local) AS taken_max

View file

@ -6,6 +6,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/enum"
)
// SelectedPhotos finds photos based on the given selection form, e.g. for adding them to an album.
@ -24,9 +25,9 @@ func SelectedPhotos(frm form.Selection) (results entity.Photos, err error) {
var concat string
switch DbDialect() {
case MySQL:
case enum.MySQL:
concat = "CONCAT(a.path, '/%')"
case SQLite3:
case enum.SQLite3:
concat = "a.path || '/%'"
default:
return results, fmt.Errorf("unknown sql dialect: %s", DbDialect())

View file

@ -29,16 +29,11 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/enum"
)
var log = event.Log
// Supported database dialect identifiers.
const (
MySQL = "mysql"
SQLite3 = "sqlite3"
)
// Cols represents a list of database columns.
type Cols []string
@ -79,7 +74,7 @@ func DbDialect() string {
// BatchSize returns the maximum query parameter number based on the current sql database dialect.
func BatchSize() int {
switch DbDialect() {
case SQLite3:
case enum.SQLite3:
return 333
default:
return 1000

View file

@ -14,7 +14,7 @@ import (
// expressions. It strips operators that we don't expect to persist in the
// statement and lets callers provide their own surrounding wildcards.
func Like(s string) string {
return strings.Trim(clean.SqlString(s), " |&*%")
return strings.Trim(clean.SQLString(s, Db().Dialect().GetName()), " |&*%")
}
// LikeAny builds OR-chained LIKE predicates for a text column. The input string

View file

@ -8,6 +8,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/txt"
)
@ -16,8 +17,11 @@ func TestLike(t *testing.T) {
assert.Equal(t, "", Like(""))
})
t.Run("Special", func(t *testing.T) {
s := " ' \" \t \n %_''"
exp := "'' \"\" %_''''"
s := " ' \" \t \n %_''\\"
exp := "'' \" %_''''\\"
if entity.DbDialect() == enum.MySQL {
exp = "'' \" %_''''\\\\"
}
result := Like(s)
t.Logf("String..: %s", s)
t.Logf("Expected: %s", exp)
@ -135,7 +139,7 @@ func TestLikeAnyWord(t *testing.T) {
t.Fatalf("two where conditions expected: %#v", w)
} else {
assert.Equal(t, "k.keyword LIKE 'spoon%' OR k.keyword LIKE 'table%'", w[0])
assert.Equal(t, "k.keyword LIKE '\"\"us''a%'", w[1])
assert.Equal(t, "k.keyword LIKE '\"us''a%'", w[1])
}
})
}

View file

@ -4,21 +4,18 @@ import (
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
_ "github.com/jinzhu/gorm/dialects/sqlite"
)
const (
MySQL = "mysql"
SQLite3 = "sqlite3"
"github.com/photoprism/photoprism/pkg/enum"
)
// RandomExpr returns the name of the random function depending on the SQL dialect.
func RandomExpr(dialect gorm.Dialect) *gorm.SqlExpr {
switch dialect.GetName() {
case MySQL:
case enum.MySQL:
// A seed integer can be passed as an argument, e.g. "RAND(2342)", to generate
// reproducible pseudo-random values, see https://mariadb.com/kb/en/rand/.
return gorm.Expr("RAND()")
case SQLite3:
case enum.SQLite3:
// SQLite does not support specifying a seed to generate a deterministic sequence
// of pseudo-random values, see https://www.sqlite.org/lang_corefunc.html#random.
return gorm.Expr("RANDOM()")

View file

@ -5,12 +5,14 @@ import (
"github.com/jinzhu/gorm"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/stretchr/testify/assert"
)
func TestRandomExpr(t *testing.T) {
mysql, _ := gorm.GetDialect(MySQL)
sqlite3, _ := gorm.GetDialect(SQLite3)
mysql, _ := gorm.GetDialect(enum.MySQL)
sqlite3, _ := gorm.GetDialect(enum.SQLite3)
assert.Equal(t, gorm.Expr("RAND()"), RandomExpr(mysql))
assert.Equal(t, gorm.Expr("RANDOM()"), RandomExpr(sqlite3))

View file

@ -11,6 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/photoprism/photoprism/pkg/txt"
)
@ -490,7 +491,7 @@ func (m *Subject) RefreshPhotos() error {
var err error
switch DbDialect() {
case MySQL:
case enum.MySQL:
update := fmt.Sprintf(`UPDATE photos p JOIN files f ON f.photo_id = p.id JOIN %s m ON m.file_uid = f.file_uid
SET p.checked_at = NULL WHERE m.subj_uid = ?`, Marker{}.TableName())
err = UnscopedDb().Exec(update, m.SubjUID).Error

View file

@ -15,10 +15,10 @@ import (
"github.com/dustin/go-humanize/english"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/photoprism/get"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
)
@ -73,7 +73,7 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er
var cmd *exec.Cmd
switch c.DatabaseDriver() {
case config.MySQL, config.MariaDB:
case enum.MySQL, enum.MariaDB:
// Connect via Unix Domain Socket?
if socketName := c.DatabaseServer(); strings.HasPrefix(socketName, "/") {
cmd = exec.Command( // #nosec G204 database connection parameters from trusted config
@ -112,7 +112,7 @@ func Database(backupPath, fileName string, toStdOut, force bool, retain int) (er
c.DatabaseName(),
)
}
case config.SQLite3:
case enum.SQLite3:
if !fs.FileExistsNotEmpty(c.DatabaseFile()) {
return fmt.Errorf("sqlite database file %s not found", clean.LogQuote(c.DatabaseFile()))
}
@ -254,7 +254,7 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er
var cmd *exec.Cmd
switch c.DatabaseDriver() {
case config.MySQL, config.MariaDB:
case enum.MySQL, enum.MariaDB:
// Connect via Unix Domain Socket?
if socketName := c.DatabaseServer(); strings.HasPrefix(socketName, "/") {
cmd = exec.Command( // #nosec G204 database connection parameters from config
@ -296,7 +296,7 @@ func RestoreDatabase(backupPath, fileName string, fromStdIn, force bool) (err er
c.DatabaseName(),
)
}
case config.SQLite3:
case enum.SQLite3:
log.Infoln("restore: dropping existing sqlite database tables")
tables.Drop(c.Db())
cmd = exec.Command( // #nosec G204 sqlite restore uses configured binary and db path

View file

@ -14,7 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service/cluster"
"github.com/photoprism/photoprism/pkg/dsn"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -61,7 +61,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
Secrets: &cluster.RegisterSecrets{ClientSecret: cluster.ExampleClientSecret},
JWKSUrl: jwksURL,
Database: cluster.RegisterDatabase{
Driver: dsn.DriverMySQL,
Driver: enum.MySQL,
Host: "db.local",
Port: 3306,
Name: "pp_db",
@ -92,7 +92,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
expectedAppName = c.About()
expectedAppVersion = c.Version()
// Gate rotate=true: driver mysql and no DSN/fields.
c.Options().DatabaseDriver = dsn.DriverMySQL
c.Options().DatabaseDriver = enum.MySQL
c.Options().DatabaseDSN = ""
c.Options().DatabaseName = ""
c.Options().DatabaseUser = ""
@ -105,7 +105,7 @@ func TestRegister_PersistSecretAndDB(t *testing.T) {
assert.Equal(t, cluster.ExampleClientSecret, c.NodeClientSecret())
// DSN branch should be preferred and persisted.
assert.Contains(t, c.Options().DatabaseDSN, "@tcp(db.local:3306)/pp_db")
assert.Equal(t, dsn.DriverMySQL, c.Options().DatabaseDriver)
assert.Equal(t, enum.MySQL, c.Options().DatabaseDriver)
assert.Equal(t, srv.URL+"/.well-known/jwks.json", c.JWKSUrl())
assert.Equal(t, "192.0.2.0/24", c.ClusterCIDR())
}
@ -277,7 +277,7 @@ func TestRegister_SQLite_NoDBPersist(t *testing.T) {
// NodeClientSecret should persist, but DB should remain SQLite (no DSN update).
assert.Equal(t, cluster.ExampleClientSecret, c.NodeClientSecret())
assert.Equal(t, config.SQLite3, c.DatabaseDriver())
assert.Equal(t, enum.SQLite3, c.DatabaseDriver())
assert.Equal(t, origDSN, c.Options().DatabaseDSN)
assert.Equal(t, srv.URL+"/.well-known/jwks.json", c.JWKSUrl())
assert.Equal(t, "203.0.113.0/24", c.ClusterCIDR())

View file

@ -8,7 +8,7 @@ import (
"time"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/dsn"
"github.com/photoprism/photoprism/pkg/enum"
)
// Credentials contains the connection details returned when ensuring a node database.
@ -35,9 +35,9 @@ func EnsureCredentials(ctx context.Context, conf *config.Config, nodeUUID, nodeN
driver := strings.ToLower(DatabaseDriver)
switch driver {
case dsn.DriverMySQL, dsn.DriverMariaDB:
case enum.MySQL, enum.MariaDB:
// ok
case dsn.DriverSQLite3, dsn.DriverPostgres:
case enum.SQLite3, enum.Postgres:
return out, false, errors.New("database must be MySQL/MariaDB for auto-provisioning")
default:
// Driver is configured externally for the provisioner (decoupled from app config).

View file

@ -10,6 +10,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service/cluster"
"github.com/photoprism/photoprism/pkg/dsn"
"github.com/photoprism/photoprism/pkg/enum"
"github.com/photoprism/photoprism/pkg/rnd"
)
@ -64,9 +65,9 @@ func GenerateCredentials(conf *config.Config, nodeUUID, nodeName string) (dbName
func BuildDSN(driver, host string, port int, user, pass, name string) string {
d := strings.ToLower(driver)
switch d {
case dsn.DriverMySQL, dsn.DriverMariaDB:
case enum.MySQL, enum.MariaDB:
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?%s",
user, pass, host, port, name, dsn.Params[dsn.DriverMySQL],
user, pass, host, port, name, dsn.Params[enum.MySQL],
)
default:
log.Warnf("provisioner: unsupported driver %q, falling back to mysql DSN format", driver)

View file

@ -9,6 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/service/cluster"
"github.com/photoprism/photoprism/pkg/enum"
)
func TestGenerateCredentials_StabilityAndBudgets(t *testing.T) {
@ -103,7 +104,7 @@ func TestEnsureCredentials_SqliteRejected(t *testing.T) {
ctx := context.Background()
c := config.NewConfig(config.CliTestContext())
origDriver := DatabaseDriver
DatabaseDriver = config.SQLite3
DatabaseDriver = enum.SQLite3
t.Cleanup(func() { DatabaseDriver = origDriver })
_, _, err := EnsureCredentials(ctx, c, "11111111-1111-4111-8111-111111111111", "pp-node-01", false)

View file

@ -1,24 +1,35 @@
package clean
// SqlSpecial checks if the byte must be escaped/omitted in SQL.
func SqlSpecial(b byte) (special bool, omit bool) {
import "github.com/photoprism/photoprism/pkg/enum"
// SQLSpecial checks if the byte must be escaped/omitted in SQL.
func SQLSpecial(b byte, dialect string) (special bool, omit bool) {
if b < 32 {
return true, true
}
switch b {
case '"', '\'', '\\':
return true, false
default:
return false, false
if dialect == enum.MySQL {
switch b {
case '\'', '\\':
return true, false
default:
return false, false
}
} else {
switch b {
case '\'':
return true, false
default:
return false, false
}
}
}
// SqlString escapes a string for use in an SQL query.
func SqlString(s string) string {
// SQLString escapes a string for use in an SQL query.
func SQLString(s string, dialect string) string {
var i int
for i = 0; i < len(s); i++ {
if found, _ := SqlSpecial(s[i]); found {
if found, _ := SQLSpecial(s[i], dialect); found {
break
}
}
@ -35,7 +46,7 @@ func SqlString(s string) string {
j := i
for ; i < len(s); i++ {
if special, omit := SqlSpecial(s[i]); omit {
if special, omit := SQLSpecial(s[i], dialect); omit {
// Omit control characters.
continue
} else if special {

View file

@ -4,81 +4,210 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/enum"
)
func TestSqlSpecial(t *testing.T) {
t.Run("Special", func(t *testing.T) {
if s, o := SqlSpecial(1); !s {
func TestSQLSpecial(t *testing.T) {
t.Run("Special MySQL", func(t *testing.T) {
if s, o := SQLSpecial(1, enum.MySQL); !s {
t.Error("char is special")
} else if !o {
t.Error("\" must be omitted")
}
if s, o := SqlSpecial(31); !s {
if s, o := SQLSpecial(31, enum.MySQL); !s {
t.Error("char is special")
} else if !o {
t.Error("\" must be omitted")
}
if s, o := SqlSpecial('\\'); !s {
if s, o := SQLSpecial('\\', enum.MySQL); !s {
t.Error("\\ is special")
} else if o {
t.Error("\\ must not be omitted")
}
if s, o := SqlSpecial('\''); !s {
if s, o := SQLSpecial('\'', enum.MySQL); !s {
t.Error("' is special")
} else if o {
t.Error("' must not be omitted")
}
})
t.Run("Special SQLite", func(t *testing.T) {
if s, o := SQLSpecial(1, enum.SQLite3); !s {
t.Error("char is special")
} else if !o {
t.Error("\" must be omitted")
}
if s, o := SqlSpecial('"'); !s {
t.Error("\" is special")
if s, o := SQLSpecial(31, enum.SQLite3); !s {
t.Error("char is special")
} else if !o {
t.Error("\" must be omitted")
}
if s, o := SQLSpecial('\'', enum.SQLite3); !s {
t.Error("' is special")
} else if o {
t.Error("\" must not be omitted")
t.Error("' must not be omitted")
}
})
t.Run("NotSpecial", func(t *testing.T) {
if s, o := SqlSpecial(32); s {
t.Run("Special Postgres", func(t *testing.T) {
if s, o := SQLSpecial(1, enum.Postgres); !s {
t.Error("char is special")
} else if !o {
t.Error("\" must be omitted")
}
if s, o := SQLSpecial(31, enum.Postgres); !s {
t.Error("char is special")
} else if !o {
t.Error("\" must be omitted")
}
if s, o := SQLSpecial('\'', enum.Postgres); !s {
t.Error("' is special")
} else if o {
t.Error("' must not be omitted")
}
})
t.Run("NotSpecial MySQL", func(t *testing.T) {
if s, o := SQLSpecial(32, enum.MySQL); s {
t.Error("space is not special")
} else if o {
t.Error("space must not be omitted")
}
if s, o := SqlSpecial('A'); s {
if s, o := SQLSpecial('A', enum.MySQL); s {
t.Error("A is not special")
} else if o {
t.Error("A must not be omitted")
}
if s, o := SqlSpecial('a'); s {
if s, o := SQLSpecial('a', enum.MySQL); s {
t.Error("a is not special")
} else if o {
t.Error("a must not be omitted")
}
if s, o := SqlSpecial('_'); s {
if s, o := SQLSpecial('_', enum.MySQL); s {
t.Error("_ is not special")
} else if o {
t.Error("_ must not be omitted")
}
if s, o := SQLSpecial('"', enum.MySQL); s {
t.Error("\" is not special")
} else if o {
t.Error("\" must not be omitted")
}
})
t.Run("NotSpecial SQLite", func(t *testing.T) {
if s, o := SQLSpecial(32, enum.SQLite3); s {
t.Error("space is not special")
} else if o {
t.Error("space must not be omitted")
}
if s, o := SQLSpecial('A', enum.SQLite3); s {
t.Error("A is not special")
} else if o {
t.Error("A must not be omitted")
}
if s, o := SQLSpecial('a', enum.SQLite3); s {
t.Error("a is not special")
} else if o {
t.Error("a must not be omitted")
}
if s, o := SQLSpecial('_', enum.SQLite3); s {
t.Error("_ is not special")
} else if o {
t.Error("_ must not be omitted")
}
if s, o := SQLSpecial('"', enum.SQLite3); s {
t.Error("\" is not special")
} else if o {
t.Error("\" must not be omitted")
}
if s, o := SQLSpecial('\\', enum.SQLite3); s {
t.Error("\\ is not special")
} else if o {
t.Error("\\ must not be omitted")
}
})
t.Run("NotSpecial Postgres", func(t *testing.T) {
if s, o := SQLSpecial(32, enum.Postgres); s {
t.Error("space is not special")
} else if o {
t.Error("space must not be omitted")
}
if s, o := SQLSpecial('A', enum.Postgres); s {
t.Error("A is not special")
} else if o {
t.Error("A must not be omitted")
}
if s, o := SQLSpecial('a', enum.Postgres); s {
t.Error("a is not special")
} else if o {
t.Error("a must not be omitted")
}
if s, o := SQLSpecial('_', enum.Postgres); s {
t.Error("_ is not special")
} else if o {
t.Error("_ must not be omitted")
}
if s, o := SQLSpecial('"', enum.Postgres); s {
t.Error("\" is not special")
} else if o {
t.Error("\" must not be omitted")
}
if s, o := SQLSpecial('\\', enum.Postgres); s {
t.Error("\\ is not special")
} else if o {
t.Error("\\ must not be omitted")
}
})
}
func TestSqlString(t *testing.T) {
t.Run("Empty", func(t *testing.T) {
assert.Equal(t, "", SqlString(""))
assert.Equal(t, "", SQLString("", enum.MySQL))
assert.Equal(t, "", SQLString("", enum.SQLite3))
assert.Equal(t, "", SQLString("", enum.Postgres))
})
t.Run("Special", func(t *testing.T) {
s := "' \" \t \n %_''"
exp := "'' \"\" %_''''"
result := SqlString(s)
s := "' \" \t \n %_''\\"
exp := "'' \" %_''''\\\\"
result := SQLString(s, enum.MySQL)
t.Logf("String..: %s", s)
t.Logf("Expected: %s", exp)
t.Logf("Result..: %s", result)
assert.Equal(t, exp, result)
exp = "'' \" %_''''\\"
result = SQLString(s, enum.SQLite3)
t.Logf("String..: %s", s)
t.Logf("Expected: %s", exp)
t.Logf("Result..: %s", result)
assert.Equal(t, exp, result)
exp = "'' \" %_''''\\"
result = SQLString(s, enum.Postgres)
t.Logf("String..: %s", s)
t.Logf("Expected: %s", exp)
t.Logf("Result..: %s", result)
assert.Equal(t, exp, result)
})
t.Run("Alnum", func(t *testing.T) {
assert.Equal(t, "123ABCabc", SqlString("123ABCabc"))
assert.Equal(t, "123ABCabc", SQLString("123ABCabc", enum.MySQL))
})
}

View file

@ -1,23 +1,11 @@
package dsn
// SQL database drivers.
const (
DriverMySQL = "mysql"
DriverMariaDB = "mariadb"
DriverPostgres = "postgres"
DriverSQLite3 = "sqlite3"
)
// SQLite default DSNs.
const (
SQLiteTestDB = ".test.db"
SQLiteMemory = ":memory:"
)
import "github.com/photoprism/photoprism/pkg/enum"
// Params maps required DSN parameters by driver type.
var Params = Values{
DriverMySQL: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
DriverMariaDB: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
DriverPostgres: "sslmode=disable TimeZone=UTC",
DriverSQLite3: "_busy_timeout=5000",
enum.MySQL: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
enum.MariaDB: "charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
enum.Postgres: "sslmode=disable TimeZone=UTC",
enum.SQLite3: "_busy_timeout=5000",
}

View file

@ -31,6 +31,8 @@ import (
"strconv"
"strings"
"unicode"
"github.com/photoprism/photoprism/pkg/enum"
)
// dsnPattern is a regular expression matching a database DSN string.
@ -75,7 +77,7 @@ func (d *DSN) MaskPassword() (s string) {
}
// Mask password in PostgreSQL-style DSN.
if d.Driver == DriverPostgres || strings.Contains(s, "password=") {
if d.Driver == enum.Postgres || strings.Contains(s, "password=") {
return dsnPostgresPasswordPattern.ReplaceAllStringFunc(s, func(segment string) string {
matches := dsnPostgresPasswordPattern.FindStringSubmatch(segment)
if len(matches) != 3 {
@ -107,7 +109,7 @@ func (d *DSN) MaskPassword() (s string) {
// Host the database server host.
func (d *DSN) Host() string {
if d.Driver == DriverSQLite3 {
if d.Driver == enum.SQLite3 {
return ""
}
@ -117,16 +119,17 @@ func (d *DSN) Host() string {
// Port the database server port.
func (d *DSN) Port() int {
if d.Driver == DriverSQLite3 {
switch d.Driver {
case enum.SQLite3:
return 0
}
defaultPort := 0
switch d.Driver {
case DriverMySQL, DriverMariaDB:
case enum.MySQL, enum.MariaDB:
defaultPort = 3306
case DriverPostgres:
case enum.Postgres:
defaultPort = 5432
}
@ -249,7 +252,7 @@ func (d *DSN) parsePostgres() bool {
}
}
d.Driver = DriverPostgres
d.Driver = enum.Postgres
d.User = values["user"]
d.Password = values["password"]
d.Name = name
@ -371,13 +374,13 @@ func (d *DSN) detectDriver() {
switch driver {
case "postgres", "postgresql":
d.Driver = DriverPostgres
d.Driver = enum.Postgres
return
case "mysql", "mariadb":
d.Driver = DriverMySQL
d.Driver = enum.MySQL
return
case "sqlite", "sqlite3", "file":
d.Driver = DriverSQLite3
d.Driver = enum.SQLite3
return
}
@ -389,26 +392,26 @@ func (d *DSN) detectDriver() {
lower := strings.ToLower(d.DSN)
if strings.Contains(lower, "postgres://") || strings.Contains(lower, "postgresql://") {
d.Driver = DriverPostgres
d.Driver = enum.Postgres
return
}
if d.Net == "tcp" || d.Net == "unix" || strings.Contains(lower, "@tcp(") || strings.Contains(lower, "@unix(") {
d.Driver = DriverMySQL
d.Driver = enum.MySQL
return
}
if strings.HasPrefix(lower, "file:") || strings.HasSuffix(lower, ".db") || strings.HasSuffix(strings.ToLower(d.Name), ".db") {
d.Driver = DriverSQLite3
d.Driver = enum.SQLite3
return
}
if strings.Contains(lower, " host=") && strings.Contains(lower, " dbname=") {
d.Driver = DriverPostgres
d.Driver = enum.Postgres
return
}
if d.Server != "" && (strings.Contains(d.Server, ":") || d.Net != "") && d.Driver == "" {
d.Driver = DriverMySQL
d.Driver = enum.MySQL
}
}

View file

@ -4,6 +4,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/enum"
)
func TestDSN_HostAndPort(t *testing.T) {
@ -95,7 +97,7 @@ func TestDSN_ParsePostgres(t *testing.T) {
in: "user=alice password=s3cr3t dbname=app",
want: DSN{
DSN: "user=alice password=s3cr3t dbname=app",
Driver: DriverPostgres,
Driver: enum.Postgres,
User: "alice",
Password: "s3cr3t",
Name: "app",
@ -107,7 +109,7 @@ func TestDSN_ParsePostgres(t *testing.T) {
in: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require",
want: DSN{
DSN: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require",
Driver: DriverPostgres,
Driver: enum.Postgres,
User: "alice",
Password: "s3cr3t",
Server: "db.internal:5432",
@ -121,7 +123,7 @@ func TestDSN_ParsePostgres(t *testing.T) {
in: `user="alice" password="s ec ret" dbname="app" host=db.internal`,
want: DSN{
DSN: `user="alice" password="s ec ret" dbname="app" host=db.internal`,
Driver: DriverPostgres,
Driver: enum.Postgres,
User: "alice",
Password: "s ec ret",
Server: "db.internal",

View file

@ -4,6 +4,8 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/enum"
)
func TestParse(t *testing.T) {
@ -17,7 +19,7 @@ func TestParse(t *testing.T) {
in: "user:secret@tcp(localhost:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
want: DSN{
DSN: "user:secret@tcp(localhost:3306)/photoprism?charset=utf8mb4,utf8&collation=utf8mb4_unicode_ci&parseTime=true",
Driver: DriverMySQL,
Driver: enum.MySQL,
User: "user",
Password: "secret",
Net: "tcp",
@ -31,7 +33,7 @@ func TestParse(t *testing.T) {
in: "mysql://user:secret@localhost:3306/photoprism?parseTime=true",
want: DSN{
DSN: "mysql://user:secret@localhost:3306/photoprism?parseTime=true",
Driver: DriverMySQL,
Driver: enum.MySQL,
User: "user",
Password: "secret",
Server: "localhost:3306",
@ -44,7 +46,7 @@ func TestParse(t *testing.T) {
in: "user:secret@unix(/var/run/mysql.sock)/photoprism",
want: DSN{
DSN: "user:secret@unix(/var/run/mysql.sock)/photoprism",
Driver: DriverMySQL,
Driver: enum.MySQL,
User: "user",
Password: "secret",
Net: "unix",
@ -57,7 +59,7 @@ func TestParse(t *testing.T) {
in: "file:/data/index.db?_busy_timeout=5000",
want: DSN{
DSN: "file:/data/index.db?_busy_timeout=5000",
Driver: DriverSQLite3,
Driver: enum.SQLite3,
Server: "file:/data",
Name: "index.db",
Params: "_busy_timeout=5000",
@ -68,7 +70,7 @@ func TestParse(t *testing.T) {
in: "/index.db?_busy_timeout=5000",
want: DSN{
DSN: "/index.db?_busy_timeout=5000",
Driver: DriverSQLite3,
Driver: enum.SQLite3,
Server: "",
Name: "index.db",
Params: "_busy_timeout=5000",
@ -79,7 +81,7 @@ func TestParse(t *testing.T) {
in: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require",
want: DSN{
DSN: "user=alice password=s3cr3t dbname=app host=db.internal port=5432 connect_timeout=5 sslmode=require",
Driver: DriverPostgres,
Driver: enum.Postgres,
User: "alice",
Password: "s3cr3t",
Server: "db.internal:5432",

View file

@ -24,3 +24,8 @@ Additional information can be found in our Developer Guide:
<https://docs.photoprism.app/developer-guide/>
*/
package enum
// Automatically choose the appropriate "thing"
const (
Auto = "auto"
)

19
pkg/enum/sql.go Normal file
View file

@ -0,0 +1,19 @@
package enum
// Supported database(s)
const (
MySQL = "mysql"
MariaDB = "mariadb"
SQLite3 = "sqlite3"
)
// Future database(s)
const (
Postgres = "postgres"
)
// Test database(s)
const (
SQLiteTestDB = ".test.db"
SQLiteMemoryDSN = ":memory:?cache=shared"
)