Compare commits

..

3 Commits

Author SHA1 Message Date
Hayden Hargreaves
9398e06943 (FIX): Removed legacy code from search API. 2026-02-03 22:58:07 -07:00
Hayden Hargreaves
4bd4c4ca26 (FEAT): First squirrel implementation. Search! 2026-02-03 22:56:26 -07:00
Hayden Hargreaves
5037b0c2db (FEAT): SQLX migration. 2026-02-03 22:33:55 -07:00
7 changed files with 130 additions and 140 deletions

4
go.mod
View File

@ -15,6 +15,7 @@ require (
require (
cloud.google.com/go/compute/metadata v0.3.0 // indirect
github.com/Masterminds/squirrel v1.5.4 // indirect
github.com/bytedance/sonic v1.13.2 // indirect
github.com/bytedance/sonic/loader v0.2.4 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
@ -25,10 +26,13 @@ require (
github.com/go-playground/validator/v10 v10.26.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jmoiron/sqlx v1.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect

12
go.sum
View File

@ -1,5 +1,8 @@
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=
github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=
github.com/a-h/templ v0.3.898 h1:g9oxL/dmM6tvwRe2egJS8hBDQTncokbMoOFk1oJMX7s=
github.com/a-h/templ v0.3.898/go.mod h1:oLBbZVQ6//Q6zpvSMPTuBK0F3qOtBdFBcGRspcT+VNQ=
github.com/a-h/templ v0.3.920 h1:IQjjTu4KGrYreHo/ewzSeS8uefecisPayIIc9VflLSE=
@ -48,6 +51,7 @@ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBEx
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
@ -65,6 +69,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -78,12 +84,17 @@ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -99,6 +110,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View File

@ -1,7 +1,6 @@
package server
import (
"database/sql"
"fmt"
"net/http"
"os"
@ -18,6 +17,7 @@ import (
"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
@ -26,7 +26,7 @@ type Server struct {
port int
Router *gin.Engine
config cors.Config
DB *sql.DB
DB *sqlx.DB
deps domain.InjectedDependencies
logs []logging.Logger
cleanupFuncs []func() error
@ -132,7 +132,7 @@ func (s *Server) Setup() *Server {
auth.NewGoogleConfig(redirectUrl, clientId, clientSecret, scope)
// SETUP DATABASE
db, err := sql.Open("postgres", cfg.DatabaseUrl)
db, err := sqlx.Open("postgres", cfg.DatabaseUrl)
if err != nil {
panic("Could not connect to database: " + err.Error())
}

View File

@ -7,11 +7,12 @@ import (
"time"
domain "github.com/haydenhargreaves/Potion/internal/domain/engagement"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
type EngagementRepository struct {
db *sql.DB
db *sqlx.DB
}
// Compile-time check to ensure the EngagementRepository implements domain.EngagementRepository
@ -19,7 +20,7 @@ var _ domain.EngagementRepository = (*EngagementRepository)(nil)
// NewUserRepository creates a user repository object which is used by the user service to access
// the database. Any user related database operations will take place in this repository.
func NewEngagementRepository(db *sql.DB) domain.EngagementRepository {
func NewEngagementRepository(db *sqlx.DB) domain.EngagementRepository {
return &EngagementRepository{db: db}
}

View File

@ -8,12 +8,14 @@ import (
"strings"
"time"
sq "github.com/Masterminds/squirrel"
domain "github.com/haydenhargreaves/Potion/internal/domain/recipe"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
type RecipeRepository struct {
db *sql.DB
db *sqlx.DB
}
// Compile-time check to ensure the RecipeRepository implements domain.RecipeRepository
@ -21,7 +23,7 @@ var _ domain.RecipeRepository = (*RecipeRepository)(nil)
// NewRecipeRepository creates a user repository object which is used by the user service to access
// the database. Any recipe related database operations will take place in this repository.
func NewRecipeRepository(db *sql.DB) domain.RecipeRepository {
func NewRecipeRepository(db *sqlx.DB) domain.RecipeRepository {
return &RecipeRepository{db: db}
}
@ -338,198 +340,168 @@ func isBitActive(bits, pos int) bool {
// 12/28/25: This function has changed, now longer returns the recipes, but their IDs for fetching
// elsewhere.
//
// 2/3/26: Refactored this large function to use Squirrel for simpler generation. Reduced line count by 50,
// but this is still insane. We need to clean this up.
//
// This function will only return recipes that are not deleted. Any recipes marked deleted will be ignored
// and the standard "not-found" error will be returned.
func (r *RecipeRepository) SearchRecipes(filters domain.SearchFilters, userId *int, favorites bool) ([]int, error) {
// Compute meals type filters (there are 7 bits)
var mealConditions []string
// Begin creating the query
psql := sq.StatementBuilder.PlaceholderFormat(sq.Dollar)
query := psql.Select("r.id").From("recipes r")
// Only select fields where the recipe ID can be found in the favorites table (mapped to user ID)
if favorites && userId != nil {
query = query.
Join("favorites f ON f.recipeId = r.id").
Where(sq.Eq{"f.userid": *userId})
}
// Compute and add meal type filters (7 bit options)
var mealCategories []string
for i := range 7 {
if isBitActive(filters.MealType, i) {
mealConditions = append(mealConditions, fmt.Sprintf("category = '%s'", domain.ParseMeal(i)))
mealCategories = append(mealCategories, string(domain.ParseMeal(i)))
}
}
// Compute time filters (there are 5 bits)
var timeConditions []string
if len(mealCategories) > 0 {
query = query.Where(sq.Eq{"category": mealCategories})
}
// Compute and add time filters (5 bit options)
var timeOr sq.Or
for i := range 5 {
var cond string
if isBitActive(filters.Time, i) {
switch i {
case 0:
cond = "(duration->>'total')::int < 15"
timeOr = append(timeOr, sq.Lt{"(duration->>'total')::int": 15})
case 1:
cond = "(duration->>'total')::int BETWEEN 15 AND 30"
timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 15 AND 30"))
case 2:
cond = "(duration->>'total')::int BETWEEN 30 AND 60"
timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 30 AND 60"))
case 3:
cond = "(duration->>'total')::int BETWEEN 60 AND 120"
timeOr = append(timeOr, sq.Expr("(duration->>'total')::int BETWEEN 60 AND 120"))
case 4:
cond = "(duration->>'total')::int > 120"
timeOr = append(timeOr, sq.Gt{"(duration->>'total')::int": 120})
}
timeConditions = append(timeConditions, cond)
}
}
// Compute difficulty filters (there are 5 bits)
var difficultyConditions []string
if len(timeOr) > 0 {
query = query.Where(timeOr)
}
// Compute and add difficulty filters (5 bit options)
var difficulties []int
for i := range 5 {
if isBitActive(filters.Difficulty, i) {
cond := fmt.Sprintf("difficulty = '%d'", i+1)
difficultyConditions = append(difficultyConditions, cond)
difficulties = append(difficulties, i+1)
}
}
// Compute serving size filters (there are 5 bits)
var servingConditions []string
if len(difficulties) > 0 {
query = query.Where(sq.Eq{"difficulty": difficulties})
}
// Compute and add serving size filters (5 bit options)
var servingOr sq.Or
for i := range 5 {
var cond string
if isBitActive(filters.ServingSize, i) {
switch i {
case 0:
cond = "serves BETWEEN 1 AND 2"
servingOr = append(servingOr, sq.Expr("serves BETWEEN 1 AND 2"))
case 1:
cond = "serves BETWEEN 2 AND 4"
servingOr = append(servingOr, sq.Expr("serves BETWEEN 2 AND 4"))
case 2:
cond = "serves BETWEEN 4 AND 6"
servingOr = append(servingOr, sq.Expr("serves BETWEEN 4 AND 6"))
case 3:
cond = "serves BETWEEN 6 AND 8"
servingOr = append(servingOr, sq.Expr("serves BETWEEN 6 AND 8"))
case 4:
cond = "serves > 8"
servingOr = append(servingOr, sq.Gt{"serves": 8})
}
servingConditions = append(servingConditions, cond)
}
}
// Merge condition strings
mealString := fmt.Sprintf("(%s)", strings.Join(mealConditions, " OR "))
timeString := fmt.Sprintf("(%s)", strings.Join(timeConditions, " OR "))
difficultyString := fmt.Sprintf("(%s)", strings.Join(difficultyConditions, " OR "))
servingString := fmt.Sprintf("(%s)", strings.Join(servingConditions, " OR "))
// Combine condition strings
var conditions []string
if len(mealConditions) > 0 {
conditions = append(conditions, mealString)
}
if len(timeConditions) > 0 {
conditions = append(conditions, timeString)
}
if len(difficultyConditions) > 0 {
conditions = append(conditions, difficultyString)
}
if len(servingConditions) > 0 {
conditions = append(conditions, servingString)
if len(servingOr) > 0 {
query = query.Where(servingOr)
}
// Define columns to select
columns := []string{
"r.id",
}
// Create search vector query with SAFE parameterization
var orderBy string = ""
var searchQuery string = ""
// Handle search with full-text search and ILIKE fallback
if filters.Search != "" {
spl := strings.Split(filters.Search, " ")
var cleaned []string
// Use a string replacer for safety
// Sanitize search terms
replacer := strings.NewReplacer(
"'", "",
"-", "",
"&", "",
"|", "",
"!", "",
":", "", // Remove colons to prevent tsquery syntax injection
":", "",
"(", "",
")", "",
)
for i := range len(spl) {
q := strings.TrimSpace(replacer.Replace(spl[i]))
if q != "" {
// Add :* suffix for prefix matching
cleaned = append(cleaned, q+":*")
}
}
// Join with OR operator for full-text search
vector_query := strings.Join(cleaned, " | ")
searchQuery = vector_query
// Full-text search with prefix matching
searchCondition := fmt.Sprintf("r.search_vector @@ to_tsquery('english', '%s')", vector_query)
// Add fallback ILIKE for true substring matching
// This catches cases where "pan" is inside "pancake" but not at word boundaries
var ilikeConditions []string
for _, term := range spl {
cleanTerm := strings.TrimSpace(replacer.Replace(term))
if cleanTerm != "" {
ilikeConditions = append(ilikeConditions, fmt.Sprintf("(r.title ILIKE '%%%s%%' OR r.description ILIKE '%%%s%%')", cleanTerm, cleanTerm))
q := strings.TrimSpace(replacer.Replace(term))
if q != "" {
cleaned = append(cleaned, q+":*") // Add prefix matching
}
}
if len(ilikeConditions) > 0 {
searchCondition = fmt.Sprintf("(%s OR %s)", searchCondition, strings.Join(ilikeConditions, " OR "))
if len(cleaned) > 0 {
vectorQuery := strings.Join(cleaned, " | ")
// Build search condition as raw SQL expression
// We'll use sq.Expr for the entire OR clause
var searchConditions []string
var searchArgs []interface{}
// Full-text search
searchConditions = append(searchConditions, "r.search_vector @@ to_tsquery('english', ?)")
searchArgs = append(searchArgs, vectorQuery)
// ILIKE fallback for substring matching
for _, term := range spl {
cleanTerm := strings.TrimSpace(replacer.Replace(term))
if cleanTerm != "" {
searchConditions = append(searchConditions, "r.title ILIKE ?")
searchArgs = append(searchArgs, "%"+cleanTerm+"%")
searchConditions = append(searchConditions, "r.description ILIKE ?")
searchArgs = append(searchArgs, "%"+cleanTerm+"%")
}
}
// Combine all conditions with OR
searchExpr := fmt.Sprintf("(%s)", strings.Join(searchConditions, " OR "))
query = query.Where(sq.Expr(searchExpr, searchArgs...))
// Add ordering for search results
query = query.
OrderBy(fmt.Sprintf("CASE WHEN r.search_vector @@ to_tsquery('english', '%s') THEN 1 ELSE 2 END", vectorQuery)).
OrderBy(fmt.Sprintf("ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC", vectorQuery)).
OrderBy(fmt.Sprintf("ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC", vectorQuery))
}
conditions = append(conditions, searchCondition)
// Ranking with preference for full-text matches
orderBy = fmt.Sprintf(`
ORDER BY
CASE
WHEN r.search_vector @@ to_tsquery('english', '%s') THEN 1
ELSE 2
END,
ts_rank(r.search_vector, to_tsquery('english', '%s')) DESC,
ts_rank_cd(r.search_vector, to_tsquery('english', '%s')) DESC
`, searchQuery, searchQuery, searchQuery)
}
// Generate the query
var query string
if favorites && userId != nil {
query = fmt.Sprintf(
"SELECT %s FROM recipes r JOIN favorites f ON f.recipeId = r.id",
strings.Join(columns, ","),
)
conditions = append(conditions, fmt.Sprintf("f.userid = %d", *userId))
} else {
query = fmt.Sprintf("SELECT %s FROM recipes r", strings.Join(columns, ","))
}
// Exclude deleted recipes
query = query.Where(sq.Eq{"deleted": false})
// Convert and append conditions if provided
conditions = append(conditions, "deleted = false")
if len(conditions) > 0 {
conditionsString := fmt.Sprintf("WHERE %s", strings.Join(conditions, " AND "))
query = fmt.Sprintf("%s %s", query, conditionsString)
}
// Append sorting order if exists
if len(orderBy) > 0 {
query = fmt.Sprintf("%s %s", query, orderBy)
}
// Finish it off with a semicolon!
query += ";"
// Execute the query
rows, err := r.db.Query(query)
sql, args, err := query.ToSql()
if err != nil {
return []int{}, fmt.Errorf("failed to query recipes: %w", err)
return nil, fmt.Errorf("[ERROR] Failed to build query: %w", err)
}
defer rows.Close()
fmt.Println(sql)
fmt.Println(args)
// Execute query using SQLX
var ids []int
for rows.Next() {
var id int
if err := rows.Scan(&id); err != nil {
return []int{}, fmt.Errorf("failed to extract ID: %s\n", err.Error())
}
ids = append(ids, id)
if err = r.db.Select(&ids, sql, args...); err != nil {
return nil, fmt.Errorf("[ERROR] Failed to query recipes: %w", err)
}
return ids, nil

View File

@ -5,11 +5,12 @@ import (
"fmt"
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
)
type UserRepository struct {
db *sql.DB
db *sqlx.DB
}
// Compile-time check to ensure the UserRepository implements domain.UserRepository
@ -17,7 +18,7 @@ var _ domain.UserRepository = (*UserRepository)(nil)
// NewUserRepository creates a user repository object which is used by the user service to access
// the database. Any user related database operations will take place in this repository.
func NewUserRepository(db *sql.DB) domain.UserRepository {
func NewUserRepository(db *sqlx.DB) domain.UserRepository {
return &UserRepository{db: db}
}

View File

@ -1,21 +1,21 @@
package loggers
import (
"database/sql"
"fmt"
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
"github.com/jmoiron/sqlx"
)
type DatabaseLogger struct {
db *sql.DB
db *sqlx.DB
table string
filter logging.LogLevel
}
var _ logging.Logger = (*DatabaseLogger)(nil)
func NewDatabaseLogger(conn *sql.DB, table string, filter logging.LogLevel) (logging.Logger, error) {
func NewDatabaseLogger(conn *sqlx.DB, table string, filter logging.LogLevel) (logging.Logger, error) {
if conn == nil {
return &DatabaseLogger{}, fmt.Errorf("Connection is nil, something is very wrong.")
}
@ -45,7 +45,7 @@ func NewDatabaseLogger(conn *sql.DB, table string, filter logging.LogLevel) (log
// tableExists queries a database connection and returns whether the table name provided
// exists on the table.
func tableExists(conn *sql.DB, tableName string) (bool, error) {
func tableExists(conn *sqlx.DB, tableName string) (bool, error) {
var exists bool
err := conn.QueryRow(`
SELECT EXISTS (