diff --git a/db/mysql.go b/db/mysql.go index 7cacd79..1d70b78 100644 --- a/db/mysql.go +++ b/db/mysql.go @@ -39,7 +39,7 @@ var ( re: regexp.MustCompile(`(\?\s*,\s*)+`), to: "..., ", }, { - re: regexp.MustCompile(`(\(..., \?\)\s*,\s*)+`), + re: regexp.MustCompile(`(\(\.\.\., \?\)\s*,\s*)+`), to: "..., ", }} mysqlNormalizeCacheLocker = &sync.RWMutex{} diff --git a/db/postgres.go b/db/postgres.go new file mode 100644 index 0000000..933243f --- /dev/null +++ b/db/postgres.go @@ -0,0 +1,88 @@ +package isudb + +import ( + "database/sql" + "regexp" + "strings" + "sync" + + "github.com/lib/pq" +) + +func init() { + sql.Register("isupostgres", wrapDriver(pq.Driver{}, postgresSegmentBuilder{})) +} + +type postgresSegmentBuilder struct{} + +func (postgresSegmentBuilder) driver() string { + return "postgres" +} + +func (psb postgresSegmentBuilder) parseDSN(dsn string) *measureSegment { + strConfig, err := pq.ParseURL(dsn) + if err != nil { + strConfig = dsn + } + + splitedConfigs := strings.Split(strConfig, " ") + host := "localhost" + port := "5432" + for _, config := range splitedConfigs { + switch { + case strings.HasPrefix(config, "host="): + host = strings.TrimPrefix(config, "host=") + case strings.HasPrefix(config, "port="): + port = strings.TrimPrefix(config, "port=") + } + } + + return &measureSegment{ + driver: psb.driver(), + addr: host + ":" + port, + normalizer: psb.normalizer, + } +} + +var ( + postgresReList = []struct { + re *regexp.Regexp + to string + }{{ + re: regexp.MustCompile(`(\$(\d*)\s*,\s*)+\$(\d*)`), + to: "..., ?", + }, { + re: regexp.MustCompile(`(\(..., \?\)\s*,\s*)+`), + to: "..., ", + }} + postgresNormalizeCacheLocker = &sync.RWMutex{} + postgresNormalizeCache = make(map[string]string, 50) +) + +func (postgresSegmentBuilder) normalizer(query string) string { + var ( + normalizedQuery string + ok bool + ) + func() { + postgresNormalizeCacheLocker.RLock() + defer postgresNormalizeCacheLocker.RUnlock() + normalizedQuery, ok = postgresNormalizeCache[query] + }() + if ok { + return normalizedQuery + } + + normalizedQuery = query + for _, re := range postgresReList { + normalizedQuery = re.re.ReplaceAllString(normalizedQuery, re.to) + } + + func() { + postgresNormalizeCacheLocker.Lock() + defer postgresNormalizeCacheLocker.Unlock() + postgresNormalizeCache[query] = normalizedQuery + }() + + return normalizedQuery +} diff --git a/db/postgres_test.go b/db/postgres_test.go new file mode 100644 index 0000000..a914876 --- /dev/null +++ b/db/postgres_test.go @@ -0,0 +1,42 @@ +package isudb + +import ( + "fmt" + "strings" + "testing" +) + +func BenchmarkPostgresNormalizer(b *testing.B) { + psb := postgresSegmentBuilder{} + + queryPart := fmt.Sprintf("(%s$2)", strings.Repeat("$1, ", 5)) + query := fmt.Sprintf("INSERT INTO users (name, email, password, salt, created_at, updated_at) VALUES %s", strings.Repeat(queryPart+", ", 999)+queryPart) + + for i := 0; i < b.N; i++ { + psb.normalizer(query) + } +} + +func TestPostgresNormalizer(t *testing.T) { + psb := postgresSegmentBuilder{} + + tests := []string{ + "$1", + } + + for _, test := range tests { + test := test + t.Run(test, func(t *testing.T) { + t.Parallel() + + queryPart := fmt.Sprintf("(%s%s)", strings.Repeat(fmt.Sprintf("%s, ", test), 5), test) + query := fmt.Sprintf("INSERT INTO users (name, email, password, salt, created_at, updated_at) VALUES %s", strings.Repeat(queryPart+", ", 999)+queryPart) + + normalizedQuery := psb.normalizer(query) + + if normalizedQuery != "INSERT INTO users (name, email, password, salt, created_at, updated_at) VALUES ..., (..., ?)" { + t.Errorf("unexpected query: %s", normalizedQuery) + } + }) + } +} diff --git a/db/sql.go b/db/sql.go index 272129d..c530576 100644 --- a/db/sql.go +++ b/db/sql.go @@ -56,6 +56,10 @@ func DBMetricsSetup[T interface { if isutools.Enable { openDriverName = "isusqlite3" } + case "postgres": + if isutools.Enable { + openDriverName = "isupostgres" + } } CONNECT: diff --git a/db/sqlite3.go b/db/sqlite3.go index 3b4e0ec..a05a10a 100644 --- a/db/sqlite3.go +++ b/db/sqlite3.go @@ -31,10 +31,10 @@ var ( re *regexp.Regexp to string }{{ - re: regexp.MustCompile(`((?:\?(\d*)|[@:$][0-9A-Fa-f]+)\s*,\s*)+`), - to: "..., ", + re: regexp.MustCompile(`((?:\?(\d*)|[@:$][0-9A-Fa-f]+)\s*,\s*)+(?:\?(\d*)|[@:$][0-9A-Fa-f]+)`), + to: "..., ?", }, { - re: regexp.MustCompile(`(\(\.\.\., ((\?[0-9]*)|[@:$][0-9A-Fa-f]+)\)\s*,\s*)+`), + re: regexp.MustCompile(`(\(\.\.\., \?\)\s*,\s*)+`), to: "..., ", }} sqlite3NormalizeCacheLocker = &sync.RWMutex{} diff --git a/db/sqlite_test.go b/db/sqlite_test.go index 0c4a9ad..9962eda 100644 --- a/db/sqlite_test.go +++ b/db/sqlite_test.go @@ -9,7 +9,7 @@ import ( func BenchmarkSQLite3Normalizer(b *testing.B) { ssb := sqlite3SegmentBuilder{} - queryPart := fmt.Sprintf("(%s?)", strings.Repeat("?, ", 5)) + queryPart := fmt.Sprintf("(%s?2)", strings.Repeat("?1, ", 5)) query := fmt.Sprintf("INSERT INTO users (name, email, password, salt, created_at, updated_at) VALUES %s", strings.Repeat(queryPart+", ", 999)+queryPart) for i := 0; i < b.N; i++ { @@ -38,7 +38,7 @@ func TestSQLite3Normalizer(t *testing.T) { normalizedQuery := ssb.normalizer(query) - if normalizedQuery != fmt.Sprintf("INSERT INTO users (name, email, password, salt, created_at, updated_at) VALUES ..., (..., %s)", test) { + if normalizedQuery != "INSERT INTO users (name, email, password, salt, created_at, updated_at) VALUES ..., (..., ?)" { t.Errorf("unexpected query: %s", normalizedQuery) } }) diff --git a/go.mod b/go.mod index acb6e51..6b89ef6 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,7 @@ require ( github.com/gofiber/fiber/v2 v2.49.2 github.com/golang/protobuf v1.5.3 // indirect github.com/labstack/echo/v4 v4.11.1 + github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.17 github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mazrean/iwrapper v0.0.0 diff --git a/go.sum b/go.sum index b2b75a2..e8e6d41 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,8 @@ github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8 github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=