├── .gitignore ├── go.mod ├── v2 ├── go.mod ├── README.md └── paginate │ ├── paginate.go │ └── paginate_test.go ├── v3 ├── go.mod ├── paginate │ ├── new_operators_test.go │ ├── benchmark_test.go │ ├── config.go │ ├── new_operators_bind_test.go │ ├── bind_test.go │ ├── bind.go │ ├── builder_test.go │ └── paginate.go ├── DEBUG_README.md ├── BIND_README.md ├── client │ ├── client.go │ └── client_test.go ├── CLIENT_README.md └── FILTER_README.md ├── assets └── icon.png ├── examples ├── v2 │ ├── go.mod │ ├── go.sum │ └── main.go ├── bind │ ├── go.mod │ ├── go.sum │ └── main.go ├── debug │ ├── go.mod │ ├── go.sum │ └── main.go ├── builder │ ├── go.mod │ ├── go.sum │ └── main.go └── client │ ├── go.mod │ └── main.go ├── .github └── workflows │ └── test.yaml ├── LICENSE └── paginate ├── paginate_test.go └── paginate.go /.gitignore: -------------------------------------------------------------------------------- 1 | cover.html 2 | cover.out -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate 2 | 3 | go 1.18 4 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/v2 2 | 3 | go 1.21.3 4 | -------------------------------------------------------------------------------- /v3/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/v3 2 | 3 | go 1.24.2 4 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/booscaaa/go-paginate/HEAD/assets/icon.png -------------------------------------------------------------------------------- /examples/v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/examples/v2 2 | 3 | go 1.24.2 4 | 5 | require github.com/booscaaa/go-paginate/v3 v3.0.3 6 | -------------------------------------------------------------------------------- /examples/bind/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/examples/bind 2 | 3 | go 1.24.2 4 | 5 | require github.com/booscaaa/go-paginate/v3 v3.0.3 6 | -------------------------------------------------------------------------------- /examples/debug/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/examples/debug 2 | 3 | go 1.24.2 4 | 5 | require github.com/booscaaa/go-paginate/v3 v3.0.3 6 | -------------------------------------------------------------------------------- /examples/builder/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/examples/builder 2 | 3 | go 1.24.2 4 | 5 | require github.com/booscaaa/go-paginate/v3 v3.0.3 6 | -------------------------------------------------------------------------------- /examples/v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/booscaaa/go-paginate/v3 v3.0.3 h1:C7081L9a/ShzYh8Pu7baBymy+izvD2CNA6S5aNRUOJM= 2 | github.com/booscaaa/go-paginate/v3 v3.0.3/go.mod h1:oFrArWjU8s4Jtyw3nVZmIuhd48TJDOBPJJQKKGNwD/s= 3 | -------------------------------------------------------------------------------- /examples/bind/go.sum: -------------------------------------------------------------------------------- 1 | github.com/booscaaa/go-paginate/v3 v3.0.3 h1:C7081L9a/ShzYh8Pu7baBymy+izvD2CNA6S5aNRUOJM= 2 | github.com/booscaaa/go-paginate/v3 v3.0.3/go.mod h1:oFrArWjU8s4Jtyw3nVZmIuhd48TJDOBPJJQKKGNwD/s= 3 | -------------------------------------------------------------------------------- /examples/builder/go.sum: -------------------------------------------------------------------------------- 1 | github.com/booscaaa/go-paginate/v3 v3.0.3 h1:C7081L9a/ShzYh8Pu7baBymy+izvD2CNA6S5aNRUOJM= 2 | github.com/booscaaa/go-paginate/v3 v3.0.3/go.mod h1:oFrArWjU8s4Jtyw3nVZmIuhd48TJDOBPJJQKKGNwD/s= 3 | -------------------------------------------------------------------------------- /examples/debug/go.sum: -------------------------------------------------------------------------------- 1 | github.com/booscaaa/go-paginate/v3 v3.0.3 h1:C7081L9a/ShzYh8Pu7baBymy+izvD2CNA6S5aNRUOJM= 2 | github.com/booscaaa/go-paginate/v3 v3.0.3/go.mod h1:oFrArWjU8s4Jtyw3nVZmIuhd48TJDOBPJJQKKGNwD/s= 3 | -------------------------------------------------------------------------------- /examples/client/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/booscaaa/go-paginate/v3/examples/client 2 | 3 | go 1.24.2 4 | 5 | replace github.com/booscaaa/go-paginate/v3 => ../../v3 6 | 7 | require github.com/booscaaa/go-paginate/v3 v3.0.0 -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | name: Test 3 | jobs: 4 | test: 5 | strategy: 6 | matrix: 7 | go-version: [1.24.x] 8 | os: [ubuntu-latest] 9 | runs-on: ${{ matrix.os }} 10 | steps: 11 | - name: Install Go 12 | uses: actions/setup-go@v2 13 | with: 14 | go-version: ${{ matrix.go-version }} 15 | - name: Checkout code 16 | uses: actions/checkout@v2 17 | - name: Test 18 | run: cd v3 && go mod tidy && go test ./paginate/... -coverprofile=coverage.txt -covermode=atomic 19 | 20 | - name: Upload coverage report 21 | uses: codecov/codecov-action@v1.0.2 22 | with: 23 | token: a84f001b-f42f-4a33-b46a-dc563b8d668f 24 | file: ./v3/coverage.txt 25 | flags: unittests 26 | name: codecov-umbrella 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2025 Vinícius Boscardin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /paginate/paginate_test.go: -------------------------------------------------------------------------------- 1 | package paginate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/booscaaa/go-paginate/paginate" 7 | ) 8 | 9 | type Test struct { 10 | Name string `json:"name" db:"name" paginate:"test.name"` 11 | LastName string `json:"lastName" db:"last_name" paginate:"test.last_name"` 12 | } 13 | 14 | func TestPaginate(t *testing.T) { 15 | queryString := "SELECT t.* FROM test t WHERE 1=1 and ((test.name::TEXT ilike $1) ) ORDER BY name DESC, last_name ASC LIMIT 50 OFFSET 100;" 16 | queryCountString := "SELECT COUNT(t.id) FROM test t WHERE 1=1 and ((test.name::TEXT ilike $1) ) " 17 | 18 | pagin := paginate.Instance(Test{}) 19 | query, queryCount := pagin. 20 | Query("SELECT t.* FROM test t"). 21 | Sort([]string{"name", "lastName"}). 22 | Desc([]string{"true", "false"}). 23 | Page(3). 24 | RowsPerPage(50). 25 | SearchBy("vinicius", "name"). 26 | Select() 27 | 28 | if queryString != *query { 29 | t.Errorf("Wrong query") 30 | return 31 | } 32 | 33 | if queryCountString != *queryCount { 34 | t.Errorf("Wrong query count") 35 | } 36 | } 37 | 38 | func TestPaginateWithArgs(t *testing.T) { 39 | queryString := "SELECT t.* FROM test t WHERE 1=1 and test.name = 'jhon' and ((test.last_name::TEXT ilike $1) ) ORDER BY name DESC, last_name ASC LIMIT 50 OFFSET 100;" 40 | queryCountString := "SELECT COUNT(t.id) FROM test t WHERE 1=1 and test.name = 'jhon' and ((test.last_name::TEXT ilike $1) ) " 41 | 42 | pagin := paginate.Instance(Test{}) 43 | 44 | pagin.Query("SELECT t.* FROM test t"). 45 | Sort([]string{"name", "lastName"}). 46 | Desc([]string{"true", "false"}). 47 | Page(3). 48 | RowsPerPage(50) 49 | 50 | pagin.WhereArgs("and", "test.name = 'jhon'") 51 | pagin.SearchBy("vinicius", []string{"lastName"}...) 52 | query, queryCount := pagin.Select() 53 | 54 | if queryString != *query { 55 | t.Errorf("Wrong query") 56 | } 57 | 58 | if queryCountString != *queryCount { 59 | t.Errorf("Wrong query count") 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /v3/paginate/new_operators_test.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // TestNewOperators tests all the newly implemented operators 8 | func TestNewOperators(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | options []Option 12 | }{ 13 | { 14 | name: "Like operator", 15 | options: []Option{ 16 | WithStruct(TestUser{}), 17 | WithTable("users"), 18 | WithLike(map[string][]string{ 19 | "name": {"john"}, 20 | }), 21 | }, 22 | }, 23 | { 24 | name: "Eq operator", 25 | options: []Option{ 26 | WithStruct(TestUser{}), 27 | WithTable("users"), 28 | WithEq(map[string][]any{ 29 | "status": {"active"}, 30 | }), 31 | }, 32 | }, 33 | { 34 | name: "In operator", 35 | options: []Option{ 36 | WithStruct(TestUser{}), 37 | WithTable("users"), 38 | WithIn(map[string][]any{ 39 | "age": {25, 30, 35}, 40 | }), 41 | }, 42 | }, 43 | { 44 | name: "NotIn operator", 45 | options: []Option{ 46 | WithStruct(TestUser{}), 47 | WithTable("users"), 48 | WithNotIn(map[string][]any{ 49 | "status": {"deleted", "banned"}, 50 | }), 51 | }, 52 | }, 53 | { 54 | name: "Between operator", 55 | options: []Option{ 56 | WithStruct(TestUser{}), 57 | WithTable("users"), 58 | WithBetween(map[string][2]any{ 59 | "age": {18, 65}, 60 | }), 61 | }, 62 | }, 63 | { 64 | name: "IsNull operator", 65 | options: []Option{ 66 | WithStruct(TestUser{}), 67 | WithTable("users"), 68 | WithIsNull([]string{"role_id"}), 69 | }, 70 | }, 71 | { 72 | name: "IsNotNull operator", 73 | options: []Option{ 74 | WithStruct(TestUser{}), 75 | WithTable("users"), 76 | WithIsNotNull([]string{"role_id"}), 77 | }, 78 | }, 79 | } 80 | 81 | for _, tt := range tests { 82 | t.Run(tt.name, func(t *testing.T) { 83 | params, err := NewPaginator(tt.options...) 84 | if err != nil { 85 | t.Fatalf("NewPaginator failed: %v", err) 86 | } 87 | 88 | query, _ := params.GenerateSQL() 89 | if query == "" { 90 | t.Fatal("Generated query is empty") 91 | } 92 | 93 | // Just verify that query generation doesn't fail 94 | t.Logf("Generated query for %s: %s", tt.name, query) 95 | }) 96 | } 97 | } 98 | 99 | // TestBuilderNewOperators tests the builder pattern with new operators 100 | func TestBuilderNewOperators(t *testing.T) { 101 | builder := NewBuilder().Model(TestUser{}).Table("users") 102 | 103 | // Test all new builder methods 104 | builder. 105 | WhereLike("name", "john"). 106 | WhereIn("age", 25, 30, 35). 107 | WhereNotIn("status", "deleted", "banned"). 108 | WhereBetween("age", 18, 65). 109 | WhereIsNull("role_id"). 110 | WhereIsNotNull("email") 111 | 112 | params, err := builder.Build() 113 | if err != nil { 114 | t.Fatalf("Builder.Build() failed: %v", err) 115 | } 116 | 117 | query, _ := params.GenerateSQL() 118 | if query == "" { 119 | t.Fatal("Generated query is empty") 120 | } 121 | 122 | // Just verify that query generation doesn't fail and contains WHERE clause 123 | if !contains(query, "WHERE") { 124 | t.Error("Expected query to contain WHERE clause") 125 | } 126 | 127 | t.Logf("Generated builder query: %s", query) 128 | } 129 | 130 | // Helper function to check if a string contains a substring 131 | func contains(s, substr string) bool { 132 | return len(s) >= len(substr) && (s == substr || 133 | (len(s) > len(substr) && 134 | (s[:len(substr)] == substr || 135 | s[len(s)-len(substr):] == substr || 136 | containsInMiddle(s, substr)))) 137 | } 138 | 139 | func containsInMiddle(s, substr string) bool { 140 | for i := 1; i <= len(s)-len(substr); i++ { 141 | if s[i:i+len(substr)] == substr { 142 | return true 143 | } 144 | } 145 | return false 146 | } -------------------------------------------------------------------------------- /v3/paginate/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | // BenchmarkUser represents a user model for benchmarking 9 | type BenchmarkUser struct { 10 | ID int `json:"id" paginate:"users.id"` 11 | Name string `json:"name" paginate:"users.name"` 12 | Email string `json:"email" paginate:"users.email"` 13 | Age int `json:"age" paginate:"users.age"` 14 | Status string `json:"status" paginate:"users.status"` 15 | Salary int `json:"salary" paginate:"users.salary"` 16 | DeptID int `json:"dept_id" paginate:"users.dept_id"` 17 | CreatedAt string `json:"created_at" paginate:"users.created_at"` 18 | } 19 | 20 | // BenchmarkFluentAPI benchmarks the new fluent API 21 | func BenchmarkFluentAPI(b *testing.B) { 22 | b.ResetTimer() 23 | b.ReportAllocs() 24 | 25 | for i := 0; i < b.N; i++ { 26 | _, _, err := NewBuilder(). 27 | Table("users"). 28 | Model(&BenchmarkUser{}). 29 | Page(2). 30 | Limit(20). 31 | Search("john", "name", "email"). 32 | WhereEquals("status", "active"). 33 | WhereGreaterThan("age", 18). 34 | OrderBy("name"). 35 | OrderByDesc("created_at"). 36 | BuildSQL() 37 | 38 | if err != nil { 39 | b.Fatal(err) 40 | } 41 | } 42 | } 43 | 44 | // BenchmarkTraditionalAPI benchmarks the traditional API 45 | func BenchmarkTraditionalAPI(b *testing.B) { 46 | b.ResetTimer() 47 | b.ReportAllocs() 48 | 49 | for i := 0; i < b.N; i++ { 50 | p, err := NewPaginator( 51 | WithTable("users"), 52 | WithStruct(BenchmarkUser{}), 53 | WithPage(2), 54 | WithItemsPerPage(20), 55 | WithSearch("john"), 56 | WithSearchFields([]string{"name", "email"}), 57 | WithSort([]string{"name", "created_at"}, []string{"true", "false"}), 58 | WithWhereClause("status = ?", "active"), 59 | WithWhereClause("age > ?", 18), 60 | ) 61 | 62 | if err != nil { 63 | b.Fatal(err) 64 | } 65 | 66 | _, _ = p.GenerateSQL() 67 | } 68 | } 69 | 70 | // BenchmarkAutomaticBinding benchmarks the automatic binding feature 71 | func BenchmarkAutomaticBinding(b *testing.B) { 72 | b.ResetTimer() 73 | b.ReportAllocs() 74 | 75 | // Simulate query parameters 76 | queryParams := url.Values{ 77 | "page": {"2"}, 78 | "limit": {"20"}, 79 | "search": {"john"}, 80 | "search_fields": {"name,email"}, 81 | "sort": {"name,-created_at"}, 82 | "likeor[status]": {"active", "pending"}, 83 | "gte[age]": {"18"}, 84 | "columns": {"id,name,email,age"}, 85 | } 86 | 87 | for i := 0; i < b.N; i++ { 88 | // Bind query params to struct 89 | params, err := BindQueryParamsToStruct(queryParams) 90 | if err != nil { 91 | b.Fatal(err) 92 | } 93 | 94 | // Create paginator using traditional API with bound params 95 | options := []Option{ 96 | WithTable("users"), 97 | WithStruct(BenchmarkUser{}), 98 | WithPage(params.Page), 99 | WithItemsPerPage(params.Limit), 100 | WithSearch(params.Search), 101 | WithSearchFields(params.SearchFields), 102 | WithLikeOr(params.LikeOr), 103 | WithGte(params.Gte), 104 | } 105 | 106 | // Add columns if specified 107 | for _, col := range params.Columns { 108 | options = append(options, WithColumn(col)) 109 | } 110 | 111 | p, err := NewPaginator(options...) 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | 116 | _, _ = p.GenerateSQL() 117 | } 118 | } 119 | 120 | // BenchmarkSQLGeneration benchmarks just the SQL generation part 121 | func BenchmarkSQLGeneration(b *testing.B) { 122 | b.ResetTimer() 123 | b.ReportAllocs() 124 | 125 | // Pre-create a paginator to avoid initialization overhead 126 | p, err := NewPaginator( 127 | WithTable("users"), 128 | WithStruct(BenchmarkUser{}), 129 | WithPage(2), 130 | WithItemsPerPage(20), 131 | WithSearch("john"), 132 | WithSearchFields([]string{"name", "email"}), 133 | WithSort([]string{"name", "created_at"}, []string{"true", "false"}), 134 | WithWhereClause("status = ?", "active"), 135 | WithWhereClause("age > ?", 18), 136 | ) 137 | 138 | if err != nil { 139 | b.Fatal(err) 140 | } 141 | 142 | for i := 0; i < b.N; i++ { 143 | _, _ = p.GenerateSQL() 144 | } 145 | } -------------------------------------------------------------------------------- /examples/debug/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "os" 7 | 8 | "github.com/booscaaa/go-paginate/v3/paginate" 9 | ) 10 | 11 | // User represents a user entity 12 | type User struct { 13 | ID int `json:"id" paginate:"id"` 14 | Name string `json:"name" paginate:"name"` 15 | Email string `json:"email" paginate:"email"` 16 | Age int `json:"age" paginate:"age"` 17 | } 18 | 19 | func main() { 20 | // Configure structured logging 21 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 22 | Level: slog.LevelDebug, 23 | })) 24 | slog.SetDefault(logger) 25 | 26 | fmt.Println("=== Go-Paginate Debug Mode Example ===") 27 | fmt.Println() 28 | 29 | // Example 1: Using environment variables 30 | fmt.Println("1. Testing with environment variables:") 31 | os.Setenv("GO_PAGINATE_DEBUG", "true") 32 | os.Setenv("GO_PAGINATE_DEFAULT_LIMIT", "25") 33 | os.Setenv("GO_PAGINATE_MAX_LIMIT", "1000") 34 | 35 | // Reload configuration from environment 36 | paginate.SetDebugMode(true) 37 | paginate.SetDefaultLimit(25) 38 | paginate.SetMaxLimit(1000) 39 | 40 | fmt.Println("Debug mode:", paginate.IsDebugMode()) 41 | fmt.Println("Default limit:", paginate.GetDefaultLimit()) 42 | fmt.Println("Max limit:", paginate.GetMaxLimit()) 43 | fmt.Println() 44 | 45 | // Example 2: Building a simple query 46 | fmt.Println("2. Building a simple query with debug logs:") 47 | builder := paginate.NewBuilder(). 48 | Table("users"). 49 | Model(User{}). 50 | Page(1). 51 | Limit(10). 52 | Search("john", "name", "email"). 53 | OrderBy("name", "ASC") 54 | 55 | sql, args, err := builder.BuildSQL() 56 | if err != nil { 57 | fmt.Printf("Error: %v\n", err) 58 | return 59 | } 60 | 61 | fmt.Printf("Generated SQL: %s\n", sql) 62 | fmt.Printf("Arguments: %v\n", args) 63 | fmt.Println() 64 | 65 | // Example 3: Building a count query 66 | fmt.Println("3. Building a count query with debug logs:") 67 | countSQL, countArgs, err := builder.BuildCountSQL() 68 | if err != nil { 69 | fmt.Printf("Error: %v\n", err) 70 | return 71 | } 72 | 73 | fmt.Printf("Generated Count SQL: %s\n", countSQL) 74 | fmt.Printf("Count Arguments: %v\n", countArgs) 75 | fmt.Println() 76 | 77 | // Example 4: Complex query with filters 78 | fmt.Println("4. Building a complex query with filters:") 79 | complexBuilder := paginate.NewBuilder(). 80 | Table("users"). 81 | Schema("public"). 82 | Model(User{}). 83 | Page(2). 84 | Limit(20). 85 | Select("id", "name", "email", "age"). 86 | Search("developer", "name", "email"). 87 | WhereGreaterThanOrEqual("age", 18). 88 | WhereLessThan("age", 65). 89 | OrderBy("name", "ASC"). 90 | OrderBy("age", "DESC") 91 | 92 | complexSQL, complexArgs, err := complexBuilder.BuildSQL() 93 | if err != nil { 94 | fmt.Printf("Error: %v\n", err) 95 | return 96 | } 97 | 98 | fmt.Printf("Complex SQL: %s\n", complexSQL) 99 | fmt.Printf("Complex Arguments: %v\n", complexArgs) 100 | fmt.Println() 101 | 102 | // Example 5: Testing with debug mode disabled 103 | fmt.Println("5. Testing with debug mode disabled:") 104 | paginate.SetDebugMode(false) 105 | fmt.Println("Debug mode:", paginate.IsDebugMode()) 106 | 107 | // This should not produce debug logs 108 | silentSQL, silentArgs, err := paginate.NewBuilder(). 109 | Table("users"). 110 | Model(User{}). 111 | Page(1). 112 | Limit(5). 113 | BuildSQL() 114 | 115 | if err != nil { 116 | fmt.Printf("Error: %v\n", err) 117 | return 118 | } 119 | 120 | fmt.Printf("Silent SQL (no debug logs): %s\n", silentSQL) 121 | fmt.Printf("Silent Arguments: %v\n", silentArgs) 122 | fmt.Println() 123 | 124 | // Example 6: Re-enable debug mode 125 | fmt.Println("6. Re-enabling debug mode:") 126 | paginate.SetDebugMode(true) 127 | 128 | // This should produce debug logs again 129 | finalSQL, finalArgs, err := paginate.NewBuilder(). 130 | Table("products"). 131 | Model(struct { 132 | ID int `json:"id" paginate:"id"` 133 | Name string `json:"name" paginate:"name"` 134 | Price float64 `json:"price" paginate:"price"` 135 | }{}). 136 | Page(1). 137 | Limit(15). 138 | WhereGreaterThan("price", 10.0). 139 | OrderBy("price", "DESC"). 140 | BuildSQL() 141 | 142 | if err != nil { 143 | fmt.Printf("Error: %v\n", err) 144 | return 145 | } 146 | 147 | fmt.Printf("Final SQL: %s\n", finalSQL) 148 | fmt.Printf("Final Arguments: %v\n", finalArgs) 149 | fmt.Println() 150 | 151 | fmt.Println("=== Debug Mode Example Complete ===") 152 | } 153 | -------------------------------------------------------------------------------- /examples/v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/booscaaa/go-paginate/v3/paginate" 8 | ) 9 | 10 | // User struct used for example. 11 | type User struct { 12 | ID int `json:"id" paginate:"users.id"` 13 | Name string `json:"name" paginate:"users.name"` 14 | Email string `json:"email" paginate:"users.email"` 15 | Age int `json:"age" paginate:"users.age"` 16 | } 17 | 18 | func main() { 19 | // Example 1: LikeOr - search for "vini" OR "joao" in the name field 20 | p1, err := paginate.NewPaginator( 21 | paginate.WithTable("users"), 22 | paginate.WithStruct(User{}), 23 | paginate.WithLikeOr(map[string][]string{ 24 | "name": {"vini", "joao"}, 25 | }), 26 | ) 27 | if err != nil { 28 | log.Fatal(err) 29 | } 30 | 31 | query1, args1 := p1.GenerateSQL() 32 | fmt.Println("Example 1 - LikeOr:") 33 | fmt.Printf("Query: %s\n", query1) 34 | fmt.Printf("Args: %v\n\n", args1) 35 | 36 | // Example 2: LikeAnd - search for "john" AND "doe" in the name field 37 | p2, err := paginate.NewPaginator( 38 | paginate.WithTable("users"), 39 | paginate.WithStruct(User{}), 40 | paginate.WithLikeAnd(map[string][]string{ 41 | "name": {"john", "doe"}, 42 | }), 43 | ) 44 | if err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | query2, args2 := p2.GenerateSQL() 49 | fmt.Println("Example 2 - LikeAnd:") 50 | fmt.Printf("Query: %s\n", query2) 51 | fmt.Printf("Args: %v\n\n", args2) 52 | 53 | // Example 3: EqOr - age equal to 25 OR 30 OR 35 54 | p3, err := paginate.NewPaginator( 55 | paginate.WithTable("users"), 56 | paginate.WithStruct(User{}), 57 | paginate.WithEqOr(map[string][]any{ 58 | "age": {25, 30, 35}, 59 | }), 60 | ) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | 65 | query3, args3 := p3.GenerateSQL() 66 | fmt.Println("Example 3 - EqOr:") 67 | fmt.Printf("Query: %s\n", query3) 68 | fmt.Printf("Args: %v\n\n", args3) 69 | 70 | // Example 4: EqAnd - ID equal to 1 AND 2 (normally doesn't make sense, but it's possible) 71 | p4, err := paginate.NewPaginator( 72 | paginate.WithTable("users"), 73 | paginate.WithStruct(User{}), 74 | paginate.WithEqAnd(map[string][]any{ 75 | "id": {1, 2}, 76 | }), 77 | ) 78 | if err != nil { 79 | log.Fatal(err) 80 | } 81 | 82 | query4, args4 := p4.GenerateSQL() 83 | fmt.Println("Example 4 - EqAnd:") 84 | fmt.Printf("Query: %s\n", query4) 85 | fmt.Printf("Args: %v\n\n", args4) 86 | 87 | // Example 5: Comparison filters (Gte, Gt, Lte, Lt) 88 | p5, err := paginate.NewPaginator( 89 | paginate.WithTable("users"), 90 | paginate.WithStruct(User{}), 91 | paginate.WithGte(map[string]any{"age": 18}), // age >= 18 92 | paginate.WithLte(map[string]any{"age": 65}), // age <= 65 93 | paginate.WithGt(map[string]any{"id": 0}), // id > 0 94 | paginate.WithLt(map[string]any{"id": 1000}), // id < 1000 95 | ) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | query5, args5 := p5.GenerateSQL() 101 | fmt.Println("Example 5 - Comparison Filters:") 102 | fmt.Printf("Query: %s\n", query5) 103 | fmt.Printf("Args: %v\n\n", args5) 104 | 105 | // Example 6: Combining multiple filters 106 | p6, err := paginate.NewPaginator( 107 | paginate.WithTable("users"), 108 | paginate.WithStruct(User{}), 109 | paginate.WithLikeOr(map[string][]string{ 110 | "name": {"vini", "joao"}, 111 | }), 112 | paginate.WithEqOr(map[string][]any{ 113 | "age": {25, 30}, 114 | }), 115 | paginate.WithGte(map[string]any{"id": 1}), 116 | paginate.WithPage(2), 117 | paginate.WithItemsPerPage(5), 118 | ) 119 | if err != nil { 120 | log.Fatal(err) 121 | } 122 | 123 | query6, args6 := p6.GenerateSQL() 124 | fmt.Println("Example 6 - Combined Filters:") 125 | fmt.Printf("Query: %s\n", query6) 126 | fmt.Printf("Args: %v\n\n", args6) 127 | 128 | // Example 7: Using JSON format as requested 129 | // {"likeor": {"nome": ["vini", "joao"]}} 130 | likeOrData := map[string][]string{ 131 | "name": {"vini", "joao"}, 132 | } 133 | 134 | p7, err := paginate.NewPaginator( 135 | paginate.WithTable("users"), 136 | paginate.WithStruct(User{}), 137 | paginate.WithLikeOr(likeOrData), 138 | ) 139 | if err != nil { 140 | log.Fatal(err) 141 | } 142 | 143 | query7, args7 := p7.GenerateSQL() 144 | count7, countArgs7 := p7.GenerateCountQuery() 145 | fmt.Println("Example 7 - JSON Format (likeor):") 146 | fmt.Printf("Query: %s\n", query7) 147 | fmt.Printf("Args: %v\n", args7) 148 | fmt.Printf("Count Query: %s\n", count7) 149 | fmt.Printf("Count Args: %v\n\n", countArgs7) 150 | } 151 | -------------------------------------------------------------------------------- /v3/paginate/config.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "log/slog" 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // GlobalConfig holds the global configuration for go-paginate 10 | type GlobalConfig struct { 11 | DefaultLimit int 12 | MaxLimit int 13 | DebugMode bool 14 | logger *slog.Logger 15 | } 16 | 17 | // globalConfig is the singleton instance 18 | var globalConfig = &GlobalConfig{ 19 | DefaultLimit: 10, // default value 20 | MaxLimit: 100, // default value 21 | DebugMode: false, 22 | logger: slog.Default(), 23 | } 24 | 25 | // init loads configuration from environment variables 26 | func init() { 27 | loadFromEnv() 28 | } 29 | 30 | // loadFromEnv loads configuration from environment variables 31 | func loadFromEnv() { 32 | logger := slog.With("component", "go-paginate-config") 33 | 34 | // Load GO_PAGINATE_DEBUG 35 | if debugStr := os.Getenv("GO_PAGINATE_DEBUG"); debugStr != "" { 36 | if debug, err := strconv.ParseBool(debugStr); err == nil { 37 | globalConfig.DebugMode = debug 38 | logger.Info("Debug mode loaded from environment", 39 | "GO_PAGINATE_DEBUG", debug) 40 | } else { 41 | logger.Warn("Invalid GO_PAGINATE_DEBUG value, using default", 42 | "value", debugStr, 43 | "error", err, 44 | "default", globalConfig.DebugMode) 45 | } 46 | } 47 | 48 | // Load GO_PAGINATE_DEFAULT_LIMIT 49 | if limitStr := os.Getenv("GO_PAGINATE_DEFAULT_LIMIT"); limitStr != "" { 50 | if limit, err := strconv.Atoi(limitStr); err == nil && limit > 0 { 51 | globalConfig.DefaultLimit = limit 52 | logger.Info("Default limit loaded from environment", 53 | "GO_PAGINATE_DEFAULT_LIMIT", limit) 54 | } else { 55 | logger.Warn("Invalid GO_PAGINATE_DEFAULT_LIMIT value, using default", 56 | "value", limitStr, 57 | "error", err, 58 | "default", globalConfig.DefaultLimit) 59 | } 60 | } 61 | 62 | // Load GO_PAGINATE_MAX_LIMIT 63 | if maxLimitStr := os.Getenv("GO_PAGINATE_MAX_LIMIT"); maxLimitStr != "" { 64 | if maxLimit, err := strconv.Atoi(maxLimitStr); err == nil && maxLimit > 0 { 65 | globalConfig.MaxLimit = maxLimit 66 | logger.Info("Max limit loaded from environment", 67 | "GO_PAGINATE_MAX_LIMIT", maxLimit) 68 | } else { 69 | logger.Warn("Invalid GO_PAGINATE_MAX_LIMIT value, using default", 70 | "value", maxLimitStr, 71 | "error", err, 72 | "default", globalConfig.MaxLimit) 73 | } 74 | } 75 | 76 | logger.Info("Go-paginate configuration initialized", 77 | "defaultLimit", globalConfig.DefaultLimit, 78 | "maxLimit", globalConfig.MaxLimit, 79 | "debugMode", globalConfig.DebugMode) 80 | } 81 | 82 | // SetDefaultLimit sets the global default limit 83 | func SetDefaultLimit(limit int) { 84 | logger := slog.With("component", "go-paginate-config") 85 | 86 | if limit <= 0 { 87 | logger.Error("Invalid default limit value, must be greater than 0", 88 | "attempted_value", limit, 89 | "current_value", globalConfig.DefaultLimit) 90 | return 91 | } 92 | 93 | oldValue := globalConfig.DefaultLimit 94 | globalConfig.DefaultLimit = limit 95 | 96 | logger.Info("Default limit updated", 97 | "old_value", oldValue, 98 | "new_value", limit) 99 | } 100 | 101 | // SetMaxLimit sets the global maximum limit 102 | func SetMaxLimit(maxLimit int) { 103 | logger := slog.With("component", "go-paginate-config") 104 | 105 | if maxLimit <= 0 { 106 | logger.Error("Invalid max limit value, must be greater than 0", 107 | "attempted_value", maxLimit, 108 | "current_value", globalConfig.MaxLimit) 109 | return 110 | } 111 | 112 | oldValue := globalConfig.MaxLimit 113 | globalConfig.MaxLimit = maxLimit 114 | 115 | logger.Info("Max limit updated", 116 | "old_value", oldValue, 117 | "new_value", maxLimit) 118 | } 119 | 120 | // SetDebugMode sets the global debug mode 121 | func SetDebugMode(debug bool) { 122 | logger := slog.With("component", "go-paginate-config") 123 | 124 | oldValue := globalConfig.DebugMode 125 | globalConfig.DebugMode = debug 126 | 127 | logger.Info("Debug mode updated", 128 | "old_value", oldValue, 129 | "new_value", debug) 130 | } 131 | 132 | // GetDefaultLimit returns the global default limit 133 | func GetDefaultLimit() int { 134 | return globalConfig.DefaultLimit 135 | } 136 | 137 | // GetMaxLimit returns the global maximum limit 138 | func GetMaxLimit() int { 139 | return globalConfig.MaxLimit 140 | } 141 | 142 | // IsDebugMode returns the global debug mode status 143 | func IsDebugMode() bool { 144 | return globalConfig.DebugMode 145 | } 146 | 147 | // SetLogger sets a custom logger for the configuration 148 | func SetLogger(logger *slog.Logger) { 149 | globalConfig.logger = logger 150 | } 151 | 152 | // logSQL logs SQL queries when debug mode is enabled 153 | func logSQL(operation, query string, args []any) { 154 | if globalConfig.DebugMode { 155 | logger := slog.With("component", "go-paginate-sql") 156 | logger.Info("Generated SQL query", 157 | "operation", operation, 158 | "query", query, 159 | "args", args, 160 | "args_count", len(args)) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /v3/DEBUG_README.md: -------------------------------------------------------------------------------- 1 | # Go-Paginate v3 - Debug Mode 2 | 3 | This document describes the debug functionality implemented in go-paginate v3, which allows structured logging of all generated SQL queries. 4 | 5 | ## 🔧 Configuration 6 | 7 | ### Environment Variables 8 | 9 | ```bash 10 | # Enable debug mode (prints generated SQL) 11 | export GO_PAGINATE_DEBUG=true 12 | 13 | # Set default page limit 14 | export GO_PAGINATE_DEFAULT_LIMIT=25 15 | 16 | # Set maximum page limit 17 | export GO_PAGINATE_MAX_LIMIT=1000 18 | ``` 19 | 20 | ### Global Configuration 21 | 22 | ```go 23 | package main 24 | 25 | import "github.com/booscaaa/go-paginate/v3/paginate" 26 | 27 | func init() { 28 | // Set global configurations 29 | paginate.SetDefaultLimit(25) 30 | paginate.SetMaxLimit(1000) 31 | paginate.SetDebugMode(true) 32 | } 33 | ``` 34 | 35 | ## 📊 Structured Logs 36 | 37 | When debug mode is enabled (`GO_PAGINATE_DEBUG=true` or `paginate.SetDebugMode(true)`), go-paginate will generate structured logs in JSON format for all created SQL queries. 38 | 39 | ### Log Format 40 | 41 | ```json 42 | { 43 | "time": "2025-06-06T09:03:44.087649546-03:00", 44 | "level": "INFO", 45 | "msg": "Generated SQL query", 46 | "component": "go-paginate-sql", 47 | "operation": "BuildSQL", 48 | "query": "SELECT * FROM users WHERE name ILIKE $1 ORDER BY name ASC LIMIT $2 OFFSET $3", 49 | "args": ["john", 10, 0], 50 | "args_count": 3 51 | } 52 | ``` 53 | 54 | ### Log Fields 55 | 56 | - **time**: Log timestamp 57 | - **level**: Log level (INFO for SQL queries) 58 | - **msg**: Descriptive message 59 | - **component**: Component that generated the log (`go-paginate-sql`) 60 | - **operation**: Operation that generated the query: 61 | - `BuildSQL`: Main pagination query 62 | - `BuildCountSQL`: Count query 63 | - `GenerateSQL`: Internally generated query 64 | - `GenerateCountQuery`: Internally generated count query 65 | - `GenerateCountQuery (Vacuum)`: Optimized count query 66 | - **query**: The generated SQL query 67 | - **args**: Array with query arguments 68 | - **args_count**: Total number of arguments 69 | 70 | ## 🚀 Usage Example 71 | 72 | ```go 73 | package main 74 | 75 | import ( 76 | "log/slog" 77 | "os" 78 | "github.com/booscaaa/go-paginate/v3/paginate" 79 | ) 80 | 81 | type User struct { 82 | ID int `json:"id" paginate:"id"` 83 | Name string `json:"name" paginate:"name"` 84 | Email string `json:"email" paginate:"email"` 85 | } 86 | 87 | func main() { 88 | // Configure structured logging 89 | logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ 90 | Level: slog.LevelDebug, 91 | })) 92 | slog.SetDefault(logger) 93 | 94 | // Enable debug mode 95 | paginate.SetDebugMode(true) 96 | 97 | // Build query 98 | sql, args, err := paginate.NewBuilder(). 99 | Table("users"). 100 | Model(User{}). 101 | Page(1). 102 | Limit(10). 103 | Search("john", "name", "email"). 104 | OrderBy("name", "ASC"). 105 | BuildSQL() 106 | 107 | if err != nil { 108 | panic(err) 109 | } 110 | 111 | // Logs will be automatically printed in JSON format 112 | // The query and arguments are also available for use 113 | println("SQL:", sql) 114 | println("Args:", args) 115 | } 116 | ``` 117 | 118 | ## 🔍 Operations that Generate Logs 119 | 120 | ### 1. BuildSQL() 121 | Generates logs for the main pagination query: 122 | ```json 123 | { 124 | "operation": "BuildSQL", 125 | "query": "SELECT * FROM users WHERE name ILIKE $1 LIMIT $2 OFFSET $3", 126 | "args": ["%john%", 10, 0] 127 | } 128 | ``` 129 | 130 | ### 2. BuildCountSQL() 131 | Generates logs for the count query: 132 | ```json 133 | { 134 | "operation": "BuildCountSQL", 135 | "query": "SELECT COUNT(id) FROM users WHERE name ILIKE $1", 136 | "args": ["%john%"] 137 | } 138 | ``` 139 | 140 | ### 3. GenerateSQL() (interno) 141 | Called internally by BuildSQL(): 142 | ```json 143 | { 144 | "operation": "GenerateSQL", 145 | "query": "SELECT * FROM users WHERE name ILIKE $1 LIMIT $2 OFFSET $3", 146 | "args": ["%john%", 10, 0] 147 | } 148 | ``` 149 | 150 | ### 4. GenerateCountQuery() (interno) 151 | Called internally by BuildCountSQL(): 152 | ```json 153 | { 154 | "operation": "GenerateCountQuery", 155 | "query": "SELECT COUNT(id) FROM users WHERE name ILIKE $1", 156 | "args": ["%john%"] 157 | } 158 | ``` 159 | 160 | ### 5. Vacuum Mode 161 | When vacuum mode is enabled: 162 | ```json 163 | { 164 | "operation": "GenerateCountQuery (Vacuum)", 165 | "query": "SELECT count_estimate('SELECT COUNT(1) FROM users WHERE name ILIKE ''$1''');", 166 | "args": ["%john%"] 167 | } 168 | ``` 169 | 170 | ## ⚙️ Advanced Configuration 171 | 172 | ### Custom Logger 173 | 174 | ```go 175 | // Configure custom logger 176 | customLogger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 177 | Level: slog.LevelInfo, 178 | AddSource: true, 179 | })) 180 | 181 | paginate.SetLogger(customLogger) 182 | ``` 183 | 184 | ### Check Configuration Status 185 | 186 | ```go 187 | // Check current configurations 188 | fmt.Println("Debug Mode:", paginate.IsDebugMode()) 189 | fmt.Println("Default Limit:", paginate.GetDefaultLimit()) 190 | fmt.Println("Max Limit:", paginate.GetMaxLimit()) 191 | ``` 192 | 193 | ## 🛡️ Security 194 | 195 | - Logs include query arguments, but these are parameterized and safe against SQL injection 196 | - In production, consider disabling debug mode or configuring the appropriate log level 197 | - Logs may contain sensitive data in arguments - configure appropriately in production environments 198 | 199 | ## 📝 Notes 200 | 201 | - Debug mode uses the `INFO` level to ensure log visibility 202 | - Each operation may generate multiple logs (internal + public) 203 | - Logs are thread-safe and use Go's standard logger (`log/slog`) 204 | - Configuration is global and affects all paginate instances -------------------------------------------------------------------------------- /examples/bind/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/url" 7 | 8 | "github.com/booscaaa/go-paginate/v3/paginate" 9 | ) 10 | 11 | func main() { 12 | fmt.Println("=== Example of Query Parameters Binding ===") 13 | 14 | // Example 1: Using BindQueryParamsToStruct with basic parameters 15 | fmt.Println("\n1. Bind de parâmetros básicos:") 16 | queryString1 := "page=2&limit=25&search=john&search_fields=name,email&vacuum=true" 17 | params1, err := paginate.BindQueryStringToStruct(queryString1) 18 | if err != nil { 19 | log.Fatalf("Error binding: %v", err) 20 | } 21 | 22 | fmt.Printf("Query String: %s\n", queryString1) 23 | fmt.Printf("Resultado:\n") 24 | fmt.Printf(" Page: %d\n", params1.Page) 25 | fmt.Printf(" Limit: %d\n", params1.Limit) 26 | fmt.Printf(" Search: %s\n", params1.Search) 27 | fmt.Printf(" SearchFields: %v\n", params1.SearchFields) 28 | fmt.Printf(" Vacuum: %t\n", params1.Vacuum) 29 | 30 | // Example 2: Using complex parameters with arrays 31 | fmt.Println("\n2. Bind de parâmetros complexos:") 32 | queryString2 := "page=1&likeor[status]=active&likeor[status]=pending&eqor[age]=25&eqor[age]=30>e[created_at]=2023-01-01>[score]=80" 33 | params2, err := paginate.BindQueryStringToStruct(queryString2) 34 | if err != nil { 35 | log.Fatalf("Error binding: %v", err) 36 | } 37 | 38 | fmt.Printf("Query String: %s\n", queryString2) 39 | fmt.Printf("Resultado:\n") 40 | fmt.Printf(" LikeOr: %v\n", params2.LikeOr) 41 | fmt.Printf(" EqOr: %v\n", params2.EqOr) 42 | fmt.Printf(" Gte: %v\n", params2.Gte) 43 | fmt.Printf(" Gt: %v\n", params2.Gt) 44 | 45 | // Example 3: Using url.Values directly 46 | fmt.Println("\n3. Bind usando url.Values:") 47 | queryParams := url.Values{ 48 | "page": {"3"}, 49 | "limit": {"50"}, 50 | "sort_columns": {"name,created_at"}, 51 | "sort_directions": {"ASC,DESC"}, 52 | "likeand[name]": {"admin"}, 53 | "lte[updated_at]": {"2023-12-31"}, 54 | } 55 | 56 | params3, err := paginate.BindQueryParamsToStruct(queryParams) 57 | if err != nil { 58 | log.Fatalf("Error binding: %v", err) 59 | } 60 | 61 | fmt.Printf("Query Params: %v\n", queryParams) 62 | fmt.Printf("Resultado:\n") 63 | fmt.Printf(" Page: %d\n", params3.Page) 64 | fmt.Printf(" Limit: %d\n", params3.Limit) 65 | fmt.Printf(" SortColumns: %v\n", params3.SortColumns) 66 | fmt.Printf(" SortDirections: %v\n", params3.SortDirections) 67 | fmt.Printf(" LikeAnd: %v\n", params3.LikeAnd) 68 | fmt.Printf(" Lte: %v\n", params3.Lte) 69 | 70 | // Example 4: Bind to custom struct 71 | fmt.Println("\n4. Bind para struct customizada:") 72 | type CustomPaginationParams struct { 73 | Page int `query:"page"` 74 | Limit int `query:"limit"` 75 | Search string `query:"search"` 76 | Filters []string `query:"filters"` 77 | Active bool `query:"active"` 78 | } 79 | 80 | customQueryParams := url.Values{ 81 | "page": {"4"}, 82 | "limit": {"100"}, 83 | "search": {"custom search"}, 84 | "filters": {"filter1,filter2,filter3"}, 85 | "active": {"true"}, 86 | } 87 | 88 | customParams := &CustomPaginationParams{} 89 | err = paginate.BindQueryParams(customQueryParams, customParams) 90 | if err != nil { 91 | log.Fatalf("Error binding custom: %v", err) 92 | } 93 | 94 | fmt.Printf("Custom Query Params: %v\n", customQueryParams) 95 | fmt.Printf("Resultado Customizado:\n") 96 | fmt.Printf(" Page: %d\n", customParams.Page) 97 | fmt.Printf(" Limit: %d\n", customParams.Limit) 98 | fmt.Printf(" Search: %s\n", customParams.Search) 99 | fmt.Printf(" Filters: %v\n", customParams.Filters) 100 | fmt.Printf(" Active: %t\n", customParams.Active) 101 | 102 | // Example 5: Simulating use in an HTTP handler 103 | fmt.Println("\n5. Example of usage in HTTP handler:") 104 | simulateHTTPHandler() 105 | } 106 | 107 | // simulateHTTPHandler simulates how to use bind in a real HTTP handler 108 | func simulateHTTPHandler() { 109 | // Simulate an HTTP request URL with new sort pattern 110 | requestURL := "https://api.example.com/users?page=2&limit=20&search=john&search_fields=name,email&likeor[status]=active&likeor[status]=pending>e[age]=18&sort=name&sort=-created_at" 111 | 112 | // Parse the URL 113 | parsedURL, err := url.Parse(requestURL) 114 | if err != nil { 115 | log.Fatalf("Error parsing URL: %v", err) 116 | } 117 | 118 | // Extract query parameters 119 | queryParams := parsedURL.Query() 120 | 121 | // Bind to pagination struct 122 | paginationParams, err := paginate.BindQueryParamsToStruct(queryParams) 123 | if err != nil { 124 | log.Fatalf("Error binding parameters: %v", err) 125 | } 126 | 127 | fmt.Printf("URL simulada: %s\n", requestURL) 128 | fmt.Printf("Parâmetros extraídos:\n") 129 | fmt.Printf(" Page: %d\n", paginationParams.Page) 130 | fmt.Printf(" Limit: %d\n", paginationParams.Limit) 131 | fmt.Printf(" Search: %s\n", paginationParams.Search) 132 | fmt.Printf(" SearchFields: %v\n", paginationParams.SearchFields) 133 | fmt.Printf(" LikeOr: %v\n", paginationParams.LikeOr) 134 | fmt.Printf(" Sort: %v\n", paginationParams.Sort) 135 | fmt.Printf(" SortColumns: %v\n", paginationParams.SortColumns) 136 | fmt.Printf(" SortDirections: %v\n", paginationParams.SortDirections) 137 | fmt.Printf(" Gte: %v\n", paginationParams.Gte) 138 | 139 | // Now you can use these parameters to build your database query 140 | fmt.Println("\n✅ Parameters ready for use in query construction!") 141 | 142 | // Additional example: Demonstrate how to use with FromStruct in builder 143 | fmt.Println("\n6. Example using FromStruct with new sort pattern:") 144 | demonstrateFromStructWithSort(paginationParams) 145 | } 146 | 147 | // demonstrateFromStructWithSort demonstrates how to use FromStruct with the new sort pattern 148 | func demonstrateFromStructWithSort(params *paginate.PaginationParams) { 149 | // Define an example struct for the model 150 | type User struct { 151 | ID int `json:"id" paginate:"id"` 152 | Name string `json:"name" paginate:"name"` 153 | Email string `json:"email" paginate:"email"` 154 | Status string `json:"status" paginate:"status"` 155 | Age int `json:"age" paginate:"age"` 156 | CreatedAt string `json:"created_at" paginate:"created_at"` 157 | } 158 | 159 | // Criar builder e usar FromStruct 160 | builder := paginate.NewBuilder(). 161 | Table("users"). 162 | Model(User{}). 163 | FromStruct(params) 164 | 165 | // Gerar SQL 166 | sql, args, err := builder.BuildSQL() 167 | if err != nil { 168 | log.Fatalf("Error generating SQL: %v", err) 169 | } 170 | 171 | fmt.Printf("SQL gerado: %s\n", sql) 172 | fmt.Printf("Args: %v\n", args) 173 | fmt.Println("\n✅ Sort funcionando corretamente com FromStruct!") 174 | } 175 | -------------------------------------------------------------------------------- /examples/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/booscaaa/go-paginate/v3/client" 10 | ) 11 | 12 | func main() { 13 | fmt.Println("=== Go Paginate v3 Client Examples ===") 14 | fmt.Println() 15 | 16 | // Example 1: Basic pagination 17 | fmt.Println("1. 📄 Basic Pagination:") 18 | basicPaginationExample() 19 | fmt.Println() 20 | 21 | // Example 2: Search and filtering 22 | fmt.Println("2. 🔍 Search and Filtering:") 23 | searchAndFilterExample() 24 | fmt.Println() 25 | 26 | // Example 3: Complex filtering 27 | fmt.Println("3. 🎯 Complex Filtering:") 28 | complexFilteringExample() 29 | fmt.Println() 30 | 31 | // Example 4: Building from existing URL 32 | fmt.Println("4. 🔗 Building from Existing URL:") 33 | buildFromURLExample() 34 | fmt.Println() 35 | 36 | // Example 5: Client cloning and reuse 37 | fmt.Println("5. 📋 Client Cloning and Reuse:") 38 | clientCloningExample() 39 | fmt.Println() 40 | 41 | // Example 6: HTTP client integration 42 | fmt.Println("6. 🌐 HTTP Client Integration:") 43 | httpClientExample() 44 | fmt.Println() 45 | 46 | // Example 7: Query string only 47 | fmt.Println("7. 📝 Query String Only:") 48 | queryStringOnlyExample() 49 | } 50 | 51 | func basicPaginationExample() { 52 | // Create a new client for a users API endpoint 53 | client := client.New("https://api.example.com/users") 54 | 55 | // Build URL with basic pagination 56 | url := client. 57 | Page(2). 58 | Limit(25). 59 | BuildURL() 60 | 61 | fmt.Printf(" URL: %s\n", url) 62 | fmt.Printf(" Query String: %s\n", client.BuildQueryString()) 63 | } 64 | 65 | func searchAndFilterExample() { 66 | // Create client and add search parameters 67 | client := client.New("https://api.example.com/users") 68 | 69 | url := client. 70 | Page(1). 71 | Limit(10). 72 | Search("john"). 73 | SearchFields("name", "email", "username"). 74 | Sort("name", "-created_at"). 75 | Columns("id", "name", "email", "created_at"). 76 | BuildURL() 77 | 78 | fmt.Printf(" URL: %s\n", url) 79 | } 80 | 81 | func complexFilteringExample() { 82 | // Create client with complex filters 83 | client := client.New("https://api.example.com/users") 84 | 85 | url := client. 86 | Page(1). 87 | Limit(20). 88 | // LIKE filters 89 | LikeOr("status", "active", "pending"). 90 | LikeAnd("name", "john", "doe"). 91 | // Equality filters 92 | EqOr("age", 25, 30, 35). 93 | Eq("department", "IT"). 94 | // Comparison filters 95 | Gte("created_at", "2023-01-01"). 96 | Lt("score", 100). 97 | // IN filters 98 | In("role", "admin", "manager", "user"). 99 | NotIn("status", "deleted", "banned"). 100 | // BETWEEN filter 101 | Between("salary", 50000, 150000). 102 | // NULL filters 103 | IsNotNull("email"). 104 | IsNull("deleted_at"). 105 | // Special options 106 | Vacuum(true). 107 | BuildURL() 108 | 109 | fmt.Printf(" URL: %s\n", url) 110 | fmt.Printf(" Query String Length: %d characters\n", len(client.BuildQueryString())) 111 | } 112 | 113 | func buildFromURLExample() { 114 | // Start with an existing URL that has some parameters 115 | existingURL := "https://api.example.com/users?page=1&limit=10&search=existing" 116 | 117 | client, err := client.NewFromURL(existingURL) 118 | if err != nil { 119 | log.Printf("Error creating client from URL: %v", err) 120 | return 121 | } 122 | 123 | // Add more parameters to the existing ones 124 | newURL := client. 125 | Page(3). // This will override the existing page=1 126 | Sort("-created_at"). 127 | Eq("status", "active"). 128 | BuildURL() 129 | 130 | fmt.Printf(" Original URL: %s\n", existingURL) 131 | fmt.Printf(" Modified URL: %s\n", newURL) 132 | } 133 | 134 | func clientCloningExample() { 135 | // Create a base client with common parameters 136 | baseClient := client.New("https://api.example.com/users") 137 | baseClient. 138 | Limit(25). 139 | Columns("id", "name", "email", "created_at"). 140 | Sort("name") 141 | 142 | // Clone for active users 143 | activeUsersClient := baseClient.Clone() 144 | activeUsersURL := activeUsersClient. 145 | Page(1). 146 | Eq("status", "active"). 147 | BuildURL() 148 | 149 | // Clone for inactive users 150 | inactiveUsersClient := baseClient.Clone() 151 | inactiveUsersURL := inactiveUsersClient. 152 | Page(1). 153 | Eq("status", "inactive"). 154 | BuildURL() 155 | 156 | // Clone for admin search 157 | adminSearchClient := baseClient.Clone() 158 | adminSearchURL := adminSearchClient. 159 | Page(1). 160 | Search("admin"). 161 | SearchFields("name", "email"). 162 | Eq("role", "admin"). 163 | BuildURL() 164 | 165 | fmt.Printf(" Active Users: %s\n", activeUsersURL) 166 | fmt.Printf(" Inactive Users: %s\n", inactiveUsersURL) 167 | fmt.Printf(" Admin Search: %s\n", adminSearchURL) 168 | } 169 | 170 | func httpClientExample() { 171 | // Create a client for making HTTP requests 172 | httpClient := &http.Client{} 173 | 174 | // Build URL using go-paginate client 175 | paginateClient := client.New("https://jsonplaceholder.typicode.com/users") 176 | url := paginateClient. 177 | Page(1). 178 | Limit(5). 179 | BuildURL() 180 | 181 | fmt.Printf(" Making request to: %s\n", url) 182 | 183 | // Make the HTTP request 184 | resp, err := httpClient.Get(url) 185 | if err != nil { 186 | fmt.Printf(" Error making request: %v\n", err) 187 | return 188 | } 189 | defer resp.Body.Close() 190 | 191 | fmt.Printf(" Response Status: %s\n", resp.Status) 192 | fmt.Printf(" Content-Type: %s\n", resp.Header.Get("Content-Type")) 193 | 194 | // You can also get just the query parameters for use with other HTTP libraries 195 | params := paginateClient.GetParams() 196 | fmt.Printf(" Query Parameters: %v\n", params) 197 | } 198 | 199 | func queryStringOnlyExample() { 200 | // Sometimes you only need the query string, not the full URL 201 | client := client.New("") // Empty base URL 202 | 203 | queryString := client. 204 | Page(2). 205 | Limit(50). 206 | Search("golang"). 207 | SearchFields("title", "description"). 208 | Sort("-created_at"). 209 | Eq("published", true). 210 | Gte("views", 100). 211 | BuildQueryString() 212 | 213 | fmt.Printf(" Query String: %s\n", queryString) 214 | 215 | // You can use this query string with any base URL 216 | fullURL1 := fmt.Sprintf("https://api1.example.com/posts?%s", queryString) 217 | fullURL2 := fmt.Sprintf("https://api2.example.com/articles?%s", queryString) 218 | 219 | fmt.Printf(" Full URL 1: %s\n", fullURL1) 220 | fmt.Printf(" Full URL 2: %s\n", fullURL2) 221 | 222 | // Parse query string back to url.Values if needed 223 | parsedParams, err := url.ParseQuery(queryString) 224 | if err != nil { 225 | fmt.Printf(" Error parsing query string: %v\n", err) 226 | return 227 | } 228 | 229 | fmt.Printf(" Parsed page parameter: %s\n", parsedParams.Get("page")) 230 | fmt.Printf(" Parsed limit parameter: %s\n", parsedParams.Get("limit")) 231 | } -------------------------------------------------------------------------------- /paginate/paginate.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type Pagination struct { 10 | query string 11 | where string 12 | group string 13 | sort []string 14 | descending []string 15 | page int 16 | itemsPerPage int 17 | search string 18 | statusField string 19 | searchFields []string 20 | structType interface{} 21 | withVacuum bool 22 | } 23 | 24 | func Instance(structType interface{}) Pagination { 25 | pagination := Pagination{ 26 | query: "", 27 | where: " WHERE 1=1 ", 28 | group: " GROUP BY ", 29 | sort: []string{}, 30 | descending: []string{}, 31 | page: 1, 32 | itemsPerPage: 10, 33 | search: "", 34 | statusField: "", 35 | searchFields: []string{}, 36 | structType: structType, 37 | withVacuum: false, 38 | } 39 | 40 | return pagination 41 | } 42 | 43 | func (pagination *Pagination) Query(query string) *Pagination { 44 | pagination.query = query 45 | return pagination 46 | } 47 | 48 | func (pagination *Pagination) WhereArgs(operation, whereArgs string) *Pagination { 49 | pagination.where += fmt.Sprintf(" %s %s ", operation, whereArgs) 50 | 51 | return pagination 52 | } 53 | 54 | func (pagination *Pagination) GroupBy(columns ...string) *Pagination { 55 | 56 | for index, column := range columns { 57 | if index == len(columns)-1 { 58 | pagination.group += column 59 | continue 60 | } 61 | pagination.group += column + ", " 62 | } 63 | 64 | return pagination 65 | } 66 | 67 | func (pagination *Pagination) Desc(desc []string) *Pagination { 68 | pagination.descending = desc 69 | return pagination 70 | } 71 | 72 | func (pagination *Pagination) Sort(sort []string) *Pagination { 73 | pagination.sort = sort 74 | return pagination 75 | } 76 | 77 | func (pagination *Pagination) Page(page int) *Pagination { 78 | pagination.page = page 79 | return pagination 80 | } 81 | 82 | func (pagination *Pagination) RowsPerPage(rows int) *Pagination { 83 | pagination.itemsPerPage = rows 84 | return pagination 85 | } 86 | 87 | func (pagination *Pagination) SearchBy(search string, fields ...string) *Pagination { 88 | pagination.search = search 89 | pagination.searchFields = fields 90 | return pagination 91 | } 92 | 93 | func (pagination *Pagination) WithVacuum() *Pagination { 94 | pagination.withVacuum = true 95 | return pagination 96 | } 97 | 98 | func (pagination Pagination) Select() (*string, *string) { 99 | query := pagination.query 100 | countQuery := generateQueryCount(query, "SELECT", "FROM", pagination.withVacuum) 101 | query += pagination.where 102 | countQuery += pagination.where 103 | 104 | offset := (pagination.page * pagination.itemsPerPage) - pagination.itemsPerPage 105 | 106 | if len(pagination.searchFields) == 0 { 107 | pagination.searchFields = getSearchFieldsBetween(query, "SELECT", "FROM") 108 | } 109 | 110 | var descs []string 111 | 112 | if len(pagination.descending) == 0 { 113 | for range pagination.sort { 114 | descs = append(descs, "ASC") 115 | } 116 | } else { 117 | for _, desc := range pagination.descending { 118 | if desc == "true" { 119 | descs = append(descs, "DESC") 120 | } else { 121 | descs = append(descs, "ASC") 122 | } 123 | } 124 | } 125 | 126 | if pagination.search != "" && len(pagination.searchFields) > 0 { 127 | for i, p := range pagination.searchFields { 128 | if p != "" { 129 | p = getFieldName(p, "json", pagination.structType, "paginate") 130 | if i == 0 { 131 | countQuery += "and ((" + p + "::TEXT ilike $1) " 132 | query += "and ((" + p + "::TEXT ilike $1) " 133 | } else { 134 | countQuery += "or (" + p + "::TEXT ilike $1) " 135 | query += "or (" + p + "::TEXT ilike $1) " 136 | } 137 | } 138 | } 139 | 140 | if len(pagination.searchFields) > 0 { 141 | if pagination.searchFields[0] != "" { 142 | countQuery += ") " 143 | query += ") " 144 | } 145 | } 146 | } 147 | 148 | if len(pagination.sort) > 0 && pagination.sort[0] != "" { 149 | query += `ORDER BY ` 150 | 151 | for s, sort := range pagination.sort { 152 | if s == len(pagination.sort)-1 { 153 | query += getFieldName(sort, "json", pagination.structType, "db") + " " + descs[s] + ` ` 154 | } else { 155 | query += getFieldName(sort, "json", pagination.structType, "db") + " " + descs[s] + `, ` 156 | } 157 | } 158 | } 159 | 160 | if pagination.group != " GROUP BY " { 161 | query += pagination.group 162 | countQuery += pagination.group 163 | } 164 | 165 | if pagination.itemsPerPage > -1 { 166 | query += fmt.Sprintf(" LIMIT %d OFFSET %d;", pagination.itemsPerPage, offset) 167 | } 168 | 169 | if pagination.withVacuum { 170 | countQuery = "SELECT count_estimate($" + strings.ReplaceAll( 171 | countQuery, 172 | "COUNT(dl.id)", 173 | "1", 174 | ) + "$);" 175 | 176 | countQuery = strings.ReplaceAll( 177 | countQuery, 178 | "'", 179 | "''", 180 | ) 181 | 182 | countQuery = strings.ReplaceAll( 183 | countQuery, 184 | "$", 185 | "'", 186 | ) 187 | } 188 | 189 | return &query, &countQuery 190 | } 191 | 192 | func getSearchFieldsBetween(str string, start string, end string) (result []string) { 193 | a := strings.SplitAfterN(str, start, 2) 194 | b := strings.SplitAfterN(a[len(a)-1], end, 2) 195 | fields := strings.Split(strings.Replace((b[0][0:len(b[0])-len(end)]), " ", "", -1), ",") 196 | 197 | searchFields := []string{} 198 | for _, field := range fields { 199 | if !strings.Contains(field, "*") { 200 | searchFields = append(searchFields, field) 201 | } 202 | } 203 | 204 | return searchFields 205 | } 206 | 207 | func generateQueryCount(str string, start string, end string, vacuum bool) (result string) { 208 | a := strings.SplitAfterN(str, start, 2) 209 | b := strings.SplitAfterN(a[len(a)-1], end, 2) 210 | columns := b[0][0 : len(b[0])-len(end)] 211 | 212 | fields := strings.Split(strings.Replace((b[0][0:len(b[0])-len(end)]), " ", "", -1), ",") 213 | 214 | fieldWhithID := "id" 215 | for _, field := range fields { 216 | if !strings.Contains(field, ".*") { 217 | if strings.Contains(field, "id") { 218 | fieldWhithID = field 219 | break 220 | } 221 | } else if strings.Contains(field, ".*") { 222 | fieldWhithID = strings.ReplaceAll(field, ".*", ".id") 223 | break 224 | } 225 | } 226 | 227 | if vacuum { 228 | return strings.ReplaceAll(str, columns, " 1 ") 229 | } 230 | 231 | return strings.ReplaceAll(str, columns, " COUNT("+fieldWhithID+") ") 232 | } 233 | 234 | func getFieldName(tag, key string, s interface{}, keyTarget string) (fieldname string) { 235 | rt := reflect.TypeOf(s) 236 | if rt.Kind() != reflect.Struct { 237 | panic("bad type") 238 | } 239 | for i := 0; i < rt.NumField(); i++ { 240 | f := rt.Field(i) 241 | v := strings.Split(f.Tag.Get(key), ",")[0] // use split to ignore tag "options" like omitempty, etc. 242 | if v == tag { 243 | return f.Tag.Get(keyTarget) 244 | } 245 | } 246 | return "" 247 | } 248 | -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 |
2 |
4 |
5 |
8 |
10 | Complete guide to the 7 new powerful operators added in v3 11 |
12 | 13 | 14 | --- 15 | 16 | ## 🎯 What's New in v3 17 | 18 | Go Paginate v3 introduces **7 powerful new operators** that significantly expand your querying capabilities: 19 | 20 | - ✨ **`Like`** - Simple LIKE pattern matching 21 | - ✨ **`Eq`** - Simple equality operator 22 | - ✨ **`In`** - IN clause for multiple values 23 | - ✨ **`NotIn`** - NOT IN clause for exclusions 24 | - ✨ **`Between`** - BETWEEN clause for ranges 25 | - ✨ **`IsNull`** - IS NULL checks 26 | - ✨ **`IsNotNull`** - IS NOT NULL checks 27 | 28 | These operators complement the existing advanced operators (`LikeOr`, `LikeAnd`, `EqOr`, `EqAnd`, `Gte`, `Gt`, `Lte`, `Lt`) to give you **22+ total filtering options**. 29 | 30 | --- 31 | 32 | ## 📚 Complete Operator Reference 33 | 34 | ### 🆕 New Simple Operators 35 | 36 | #### `Like` - Pattern Matching 37 | ```go 38 | // Builder API 39 | builder.WhereLike("name", "john%") 40 | 41 | // Functional API 42 | paginate.WithLike(map[string][]string{ 43 | "name": {"john%", "jane%"}, 44 | "email": {"%@company.com"}, 45 | }) 46 | 47 | // HTTP Query 48 | // ?like[name]=john%&like[email]=%@company.com 49 | ``` 50 | 51 | #### `Eq` - Simple Equality 52 | ```go 53 | // Builder API 54 | builder.WhereEquals("status", "active") 55 | 56 | // Functional API 57 | paginate.WithEq(map[string][]any{ 58 | "status": {"active", "pending"}, 59 | "role_id": {1, 2, 3}, 60 | }) 61 | 62 | // HTTP Query 63 | // ?eq[status]=active&eq[status]=pending&eq[role_id]=1 64 | ``` 65 | 66 | #### `In` - Multiple Value Matching 67 | ```go 68 | // Builder API 69 | builder.WhereIn("age", 25, 30, 35, 40) 70 | 71 | // Functional API 72 | paginate.WithIn(map[string][]any{ 73 | "age": {25, 30, 35, 40}, 74 | "department_id": {1, 2, 3}, 75 | }) 76 | 77 | // HTTP Query 78 | // ?in[age]=25&in[age]=30&in[age]=35&in[department_id]=1 79 | ``` 80 | 81 | #### `NotIn` - Exclusion Matching 82 | ```go 83 | // Builder API 84 | builder.WhereNotIn("status", "deleted", "banned", "suspended") 85 | 86 | // Functional API 87 | paginate.WithNotIn(map[string][]any{ 88 | "status": {"deleted", "banned"}, 89 | "role_id": {99, 100}, // Exclude admin roles 90 | }) 91 | 92 | // HTTP Query 93 | // ?notin[status]=deleted¬in[status]=banned¬in[role_id]=99 94 | ``` 95 | 96 | #### `Between` - Range Queries 97 | ```go 98 | // Builder API 99 | builder.WhereBetween("age", 18, 65) 100 | builder.WhereBetween("salary", 50000, 150000) 101 | builder.WhereBetween("created_at", "2023-01-01", "2023-12-31") 102 | 103 | // Functional API 104 | paginate.WithBetween(map[string][2]any{ 105 | "age": {18, 65}, 106 | "salary": {50000, 150000}, 107 | "created_at": {"2023-01-01", "2023-12-31"}, 108 | }) 109 | 110 | // HTTP Query 111 | // ?between[age][0]=18&between[age][1]=65&between[salary][0]=50000&between[salary][1]=150000 112 | ``` 113 | 114 | #### `IsNull` - Null Value Checks 115 | ```go 116 | // Builder API 117 | builder.WhereIsNull("deleted_at") 118 | builder.WhereIsNull("archived_at") 119 | 120 | // Functional API 121 | paginate.WithIsNull([]string{"deleted_at", "archived_at"}) 122 | 123 | // HTTP Query 124 | // ?isnull=deleted_at&isnull=archived_at 125 | ``` 126 | 127 | #### `IsNotNull` - Non-Null Value Checks 128 | ```go 129 | // Builder API 130 | builder.WhereIsNotNull("email") 131 | builder.WhereIsNotNull("phone") 132 | 133 | // Functional API 134 | paginate.WithIsNotNull([]string{"email", "phone", "verified_at"}) 135 | 136 | // HTTP Query 137 | // ?isnotnull=email&isnotnull=phone&isnotnull=verified_at 138 | ``` 139 | 140 | --- 141 | 142 | ## 🔥 Real-World Examples 143 | 144 | ### Example 1: User Management with New Operators 145 | 146 | ```go 147 | type User struct { 148 | ID int `json:"id" paginate:"users.id"` 149 | Name string `json:"name" paginate:"users.name"` 150 | Email string `json:"email" paginate:"users.email"` 151 | Age int `json:"age" paginate:"users.age"` 152 | Status string `json:"status" paginate:"users.status"` 153 | RoleID *int `json:"role_id" paginate:"users.role_id"` 154 | Salary int `json:"salary" paginate:"users.salary"` 155 | DeletedAt *string `json:"deleted_at" paginate:"users.deleted_at"` 156 | } 157 | 158 | func advancedUserSearch() { 159 | // Using all new operators together 160 | sql, args, err := paginate.NewBuilder(). 161 | Table("users"). 162 | Model(&User{}). 163 | // Simple pattern matching 164 | WhereLike("name", "john%"). 165 | WhereLike("email", "%@company.com"). 166 | // Multiple value matching 167 | WhereIn("age", 25, 30, 35, 40). 168 | // Exclusions 169 | WhereNotIn("status", "deleted", "banned", "suspended"). 170 | // Range queries 171 | WhereBetween("salary", 50000, 150000). 172 | // Null checks 173 | WhereIsNull("deleted_at"). // Only active users 174 | WhereIsNotNull("email"). // Must have email 175 | WhereIsNotNull("role_id"). // Must have role assigned 176 | OrderBy("name"). 177 | Page(1). 178 | Limit(25). 179 | BuildSQL() 180 | 181 | if err != nil { 182 | log.Fatal(err) 183 | } 184 | 185 | fmt.Printf("SQL: %s\n", sql) 186 | fmt.Printf("Args: %v\n", args) 187 | 188 | // Output SQL: 189 | // SELECT * FROM users WHERE 190 | // (name::TEXT ILIKE $1) AND 191 | // (email::TEXT ILIKE $2) AND 192 | // age IN ($3, $4, $5, $6) AND 193 | // status NOT IN ($7, $8, $9) AND 194 | // salary BETWEEN $10 AND $11 AND 195 | // deleted_at IS NULL AND 196 | // email IS NOT NULL AND 197 | // role_id IS NOT NULL 198 | // ORDER BY name ASC LIMIT 25 OFFSET 0 199 | } 200 | ``` 201 | 202 | ### Example 2: E-commerce Product Filtering 203 | 204 | ```go 205 | type Product struct { 206 | ID int `json:"id" paginate:"products.id"` 207 | Name string `json:"name" paginate:"products.name"` 208 | Description string `json:"description" paginate:"products.description"` 209 | Price float64 `json:"price" paginate:"products.price"` 210 | CategoryID int `json:"category_id" paginate:"products.category_id"` 211 | Brand string `json:"brand" paginate:"products.brand"` 212 | InStock bool `json:"in_stock" paginate:"products.in_stock"` 213 | Rating float64 `json:"rating" paginate:"products.rating"` 214 | Tags string `json:"tags" paginate:"products.tags"` 215 | DiscountID *int `json:"discount_id" paginate:"products.discount_id"` 216 | } 217 | 218 | func productSearch() { 219 | // Complex product filtering 220 | params, err := paginate.NewPaginator( 221 | paginate.WithStruct(Product{}), 222 | paginate.WithTable("products"), 223 | 224 | // Search for electronics or gadgets 225 | paginate.WithLike(map[string][]string{ 226 | "name": {"%electronics%", "%gadget%"}, 227 | "tags": {"%smartphone%", "%laptop%"}, 228 | }), 229 | 230 | // Specific categories 231 | paginate.WithIn(map[string][]any{ 232 | "category_id": {1, 2, 3, 5}, // Electronics categories 233 | }), 234 | 235 | // Exclude certain brands 236 | paginate.WithNotIn(map[string][]any{ 237 | "brand": {"BrandX", "BrandY", "Discontinued"}, 238 | }), 239 | 240 | // Price range 241 | paginate.WithBetween(map[string][2]any{ 242 | "price": {100.0, 2000.0}, 243 | "rating": {3.5, 5.0}, 244 | }), 245 | 246 | // Must be in stock and have description 247 | paginate.WithEq(map[string][]any{ 248 | "in_stock": {true}, 249 | }), 250 | paginate.WithIsNotNull([]string{"description"}), 251 | 252 | // Optional: products with discounts 253 | // paginate.WithIsNotNull([]string{"discount_id"}), 254 | 255 | paginate.WithPage(1), 256 | paginate.WithLimit(20), 257 | paginate.WithSort([]string{"-rating", "price", "name"}), 258 | ) 259 | 260 | if err != nil { 261 | log.Fatal(err) 262 | } 263 | 264 | sql, args := params.GenerateSQL() 265 | fmt.Printf("Product Search SQL: %s\n", sql) 266 | fmt.Printf("Args: %v\n", args) 267 | } 268 | ``` 269 | 270 | ### Example 3: HTTP API Integration 271 | 272 | ```go 273 | func handleUserSearch(w http.ResponseWriter, r *http.Request) { 274 | // Example URL: 275 | // /users?like[name]=john%&in[age]=25&in[age]=30&in[age]=35¬in[status]=deleted¬in[status]=banned&between[salary][0]=50000&between[salary][1]=150000&isnull=deleted_at&isnotnull=email&page=1&limit=25 276 | 277 | // Automatically bind query parameters 278 | params, err := paginate.BindQueryParamsToStruct(r.URL.Query()) 279 | if err != nil { 280 | http.Error(w, "Invalid query parameters", http.StatusBadRequest) 281 | return 282 | } 283 | 284 | // Build query with bound parameters 285 | sql, args, err := paginate.NewBuilder(). 286 | Table("users u"). 287 | Model(&User{}). 288 | LeftJoin("departments d", "u.dept_id = d.id"). 289 | LeftJoin("roles r", "u.role_id = r.id"). 290 | FromStruct(params). 291 | BuildSQL() 292 | 293 | if err != nil { 294 | http.Error(w, "Query build error", http.StatusInternalServerError) 295 | return 296 | } 297 | 298 | // Execute query 299 | rows, err := db.Query(sql, args...) 300 | if err != nil { 301 | http.Error(w, "Database error", http.StatusInternalServerError) 302 | return 303 | } 304 | defer rows.Close() 305 | 306 | // Process results... 307 | var users []User 308 | for rows.Next() { 309 | var user User 310 | // Scan into user struct... 311 | users = append(users, user) 312 | } 313 | 314 | // Return JSON response 315 | w.Header().Set("Content-Type", "application/json") 316 | json.NewEncoder(w).Encode(map[string]interface{}{ 317 | "users": users, 318 | "total": len(users), 319 | "page": params.Page, 320 | "limit": params.Limit, 321 | }) 322 | } 323 | ``` 324 | 325 | --- 326 | 327 | ## 🔄 Migration from v2 328 | 329 | All new operators are **100% backward compatible**. Your existing v2 code will continue to work without any changes. 330 | 331 | ### Adding New Operators to Existing Code 332 | 333 | ```go 334 | // v2 code (still works) 335 | builder := paginate.NewBuilder(). 336 | Table("users"). 337 | Model(&User{}). 338 | WhereEquals("status", "active"). 339 | WhereGreaterThan("age", 18) 340 | 341 | // v3 enhancements (add these) 342 | builder = builder. 343 | WhereLike("name", "john%"). // New! 344 | WhereIn("department_id", 1, 2, 3). // New! 345 | WhereNotIn("role", "admin", "super"). // New! 346 | WhereBetween("salary", 50000, 100000). // New! 347 | WhereIsNull("deleted_at"). // New! 348 | WhereIsNotNull("email") // New! 349 | ``` 350 | 351 | --- 352 | 353 | ## 📊 Performance Notes 354 | 355 | ### Optimized SQL Generation 356 | All new operators generate optimized, parameterized SQL: 357 | 358 | ```sql 359 | -- Like operator 360 | WHERE name::TEXT ILIKE $1 361 | 362 | -- In operator 363 | WHERE age IN ($1, $2, $3, $4) 364 | 365 | -- NotIn operator 366 | WHERE status NOT IN ($1, $2, $3) 367 | 368 | -- Between operator 369 | WHERE salary BETWEEN $1 AND $2 370 | 371 | -- Null checks 372 | WHERE deleted_at IS NULL 373 | WHERE email IS NOT NULL 374 | ``` 375 | 376 | ### Best Practices 377 | 378 | 1. **Use `In` instead of multiple `Eq` conditions** 379 | ```go 380 | // ❌ Less efficient 381 | builder.WhereEquals("status", "active"). 382 | WhereEquals("status", "pending") 383 | 384 | // ✅ More efficient 385 | builder.WhereIn("status", "active", "pending") 386 | ``` 387 | 388 | 2. **Use `Between` for ranges** 389 | ```go 390 | // ❌ Less efficient 391 | builder.WhereGreaterThanOrEqual("age", 18). 392 | WhereLessThanOrEqual("age", 65) 393 | 394 | // ✅ More efficient 395 | builder.WhereBetween("age", 18, 65) 396 | ``` 397 | 398 | 3. **Use `IsNull`/`IsNotNull` for existence checks** 399 | ```go 400 | // ✅ Efficient null checks 401 | builder.WhereIsNull("deleted_at"). // Active records 402 | WhereIsNotNull("email"). // Must have email 403 | WhereIsNotNull("verified_at") // Verified users 404 | ``` 405 | 406 | --- 407 | 408 | ## 🧪 Testing Your Queries 409 | 410 | Use the built-in debug mode to see generated SQL: 411 | 412 | ```go 413 | // Enable debug mode 414 | paginate.SetDebugMode(true) 415 | 416 | // Your query will now log the generated SQL 417 | sql, args, err := paginate.NewBuilder(). 418 | Table("users"). 419 | Model(&User{}). 420 | WhereLike("name", "john%"). 421 | WhereIn("age", 25, 30, 35). 422 | WhereIsNull("deleted_at"). 423 | BuildSQL() 424 | 425 | // Output will include: 426 | // [DEBUG] Generated SQL: SELECT * FROM users WHERE (name::TEXT ILIKE $1) AND age IN ($2, $3, $4) AND deleted_at IS NULL 427 | // [DEBUG] Parameters: [john% 25 30 35] 428 | ``` 429 | 430 | --- 431 | 432 | ## 📝 Complete HTTP Query Examples 433 | 434 | ### Simple Filtering 435 | ``` 436 | /users?like[name]=john%&eq[status]=active&isnotnull=email 437 | ``` 438 | 439 | ### Complex Filtering 440 | ``` 441 | /products?like[name]=%phone%&in[category_id]=1&in[category_id]=2&in[category_id]=3¬in[brand]=BrandX¬in[brand]=BrandY&between[price][0]=100&between[price][1]=1000&eq[in_stock]=true&isnotnull=description&isnull=discontinued_at&sort=-rating&sort=price&page=1&limit=20 442 | ``` 443 | 444 | ### User Management 445 | ``` 446 | /users?like[email]=%@company.com&in[department_id]=1&in[department_id]=2¬in[status]=deleted¬in[status]=suspended&between[age][0]=25&between[age][1]=55&between[salary][0]=50000&between[salary][1]=150000&isnull=deleted_at&isnotnull=role_id&sort=name&sort=-created_at 447 | ``` 448 | 449 | --- 450 | 451 | ## 🎯 Summary 452 | 453 | Go Paginate v3 adds **7 powerful new operators** that make your database queries more expressive and efficient: 454 | 455 | - ✅ **22+ total operators** for comprehensive filtering 456 | - ✅ **100% backward compatible** with v2 457 | - ✅ **Optimized SQL generation** with parameterized queries 458 | - ✅ **Full HTTP binding support** for web APIs 459 | - ✅ **Type-safe** Go interfaces 460 | - ✅ **Thoroughly tested** with comprehensive test coverage 461 | 462 | Upgrade to v3 today and supercharge your pagination capabilities! 463 | 464 | ```bash 465 | go get github.com/booscaaa/go-paginate/v3 466 | ``` 467 | 468 | --- 469 | 470 |471 | Happy Paginating! 🚀 472 |
-------------------------------------------------------------------------------- /v3/paginate/builder_test.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | // TestUser represents a test user model 9 | type TestUser struct { 10 | ID int `json:"id" paginate:"id"` 11 | Name string `json:"name" paginate:"name"` 12 | Email string `json:"email" paginate:"email"` 13 | Age int `json:"age" paginate:"age"` 14 | Status string `json:"status" paginate:"status"` 15 | Salary int `json:"salary" paginate:"salary"` 16 | DeptID int `json:"dept_id" paginate:"dept_id"` 17 | } 18 | 19 | func TestNewBuilder(t *testing.T) { 20 | builder := NewBuilder() 21 | if builder == nil { 22 | t.Fatal("NewBuilder() returned nil") 23 | } 24 | if builder.params == nil { 25 | t.Fatal("Builder params is nil") 26 | } 27 | if builder.params.Page != 1 { 28 | t.Errorf("Expected default page to be 1, got %d", builder.params.Page) 29 | } 30 | if builder.params.ItemsPerPage != 10 { 31 | t.Errorf("Expected default items per page to be 10, got %d", builder.params.ItemsPerPage) 32 | } 33 | } 34 | 35 | func TestBuilderBasicUsage(t *testing.T) { 36 | sql, args, err := NewBuilder(). 37 | Table("users"). 38 | Model(&TestUser{}). 39 | Page(2). 40 | Limit(20). 41 | BuildSQL() 42 | 43 | if err != nil { 44 | t.Fatalf("Unexpected error: %v", err) 45 | } 46 | 47 | expectedSQL := "SELECT * FROM users LIMIT $1 OFFSET $2" 48 | if sql != expectedSQL { 49 | t.Errorf("Expected SQL: %s, got: %s", expectedSQL, sql) 50 | } 51 | 52 | expectedArgs := []any{20, 20} 53 | if len(args) != len(expectedArgs) { 54 | t.Errorf("Expected %d args, got %d", len(expectedArgs), len(args)) 55 | } 56 | } 57 | 58 | // TestPaginationParams represents a test struct for pagination parameters 59 | type TestPaginationParams struct { 60 | Page int `json:"page"` 61 | Limit int `json:"limit"` 62 | Search string `json:"search"` 63 | SearchFields []string `json:"search_fields"` 64 | LikeOr map[string][]string `json:"likeor"` 65 | LikeAnd map[string][]string `json:"likeand"` 66 | EqOr map[string][]any `json:"eqor"` 67 | EqAnd map[string][]any `json:"eqand"` 68 | Gte map[string]any `json:"gte"` 69 | Gt map[string]any `json:"gt"` 70 | Lte map[string]any `json:"lte"` 71 | Lt map[string]any `json:"lt"` 72 | Sort []string `json:"sort"` 73 | } 74 | 75 | func TestFromStruct(t *testing.T) { 76 | // Test basic struct conversion 77 | params := TestPaginationParams{ 78 | Page: 2, 79 | Limit: 25, 80 | Search: "john", 81 | SearchFields: []string{"name", "email"}, 82 | LikeOr: map[string][]string{ 83 | "status": {"active", "pending"}, 84 | }, 85 | Gte: map[string]any{ 86 | "age": 18, 87 | }, 88 | Sort: []string{"name", "-created_at"}, 89 | } 90 | 91 | builder := NewBuilder(). 92 | Table("users"). 93 | Model(&TestUser{}). 94 | FromStruct(params) 95 | 96 | if builder.err != nil { 97 | t.Fatalf("Unexpected error: %v", builder.err) 98 | } 99 | 100 | // Verify the parameters were set correctly 101 | if builder.params.Page != 2 { 102 | t.Errorf("Expected page to be 2, got %d", builder.params.Page) 103 | } 104 | 105 | if builder.params.ItemsPerPage != 25 { 106 | t.Errorf("Expected limit to be 25, got %d", builder.params.ItemsPerPage) 107 | } 108 | 109 | if builder.params.Search != "john" { 110 | t.Errorf("Expected search to be 'john', got '%s'", builder.params.Search) 111 | } 112 | 113 | if len(builder.params.SearchFields) != 2 { 114 | t.Errorf("Expected 2 search fields, got %d", len(builder.params.SearchFields)) 115 | } 116 | 117 | if len(builder.params.LikeOr["status"]) != 2 { 118 | t.Errorf("Expected 2 likeor values for status, got %d", len(builder.params.LikeOr["status"])) 119 | } 120 | 121 | if builder.params.Gte["age"] != 18 { 122 | t.Errorf("Expected gte age to be 18, got %v", builder.params.Gte["age"]) 123 | } 124 | 125 | // Test with pointer to struct 126 | builder2 := NewBuilder(). 127 | Table("users"). 128 | Model(&TestUser{}). 129 | FromStruct(¶ms) 130 | 131 | if builder2.err != nil { 132 | t.Fatalf("Unexpected error with pointer: %v", builder2.err) 133 | } 134 | 135 | if builder2.params.Page != 2 { 136 | t.Errorf("Expected page to be 2 with pointer, got %d", builder2.params.Page) 137 | } 138 | } 139 | 140 | func TestFromStructWithNilAndInvalidTypes(t *testing.T) { 141 | // Test with nil 142 | builder := NewBuilder(). 143 | Table("users"). 144 | Model(&TestUser{}). 145 | FromStruct(nil) 146 | 147 | if builder.err != nil { 148 | t.Fatalf("Unexpected error with nil: %v", builder.err) 149 | } 150 | 151 | // Test with invalid type 152 | builder2 := NewBuilder(). 153 | Table("users"). 154 | Model(&TestUser{}). 155 | FromStruct("not a struct") 156 | 157 | if builder2.err == nil { 158 | t.Fatal("Expected error with invalid type, got nil") 159 | } 160 | 161 | if !strings.Contains(builder2.err.Error(), "expected struct") { 162 | t.Errorf("Expected error message to contain 'expected struct', got: %v", builder2.err) 163 | } 164 | } 165 | 166 | func TestFromStructWithJSONTags(t *testing.T) { 167 | // Test struct with json tags 168 | type CustomParams struct { 169 | PageNumber int `json:"page"` 170 | PageSize int `json:"limit"` 171 | SearchTerm string `json:"search"` 172 | IgnoredField string `json:"-"` 173 | EmptyField string `json:"empty_field"` 174 | } 175 | 176 | params := CustomParams{ 177 | PageNumber: 3, 178 | PageSize: 50, 179 | SearchTerm: "test", 180 | IgnoredField: "should be ignored", 181 | // EmptyField is left empty (zero value) 182 | } 183 | 184 | builder := NewBuilder(). 185 | Table("users"). 186 | Model(&TestUser{}). 187 | FromStruct(params) 188 | 189 | if builder.err != nil { 190 | t.Fatalf("Unexpected error: %v", builder.err) 191 | } 192 | 193 | if builder.params.Page != 3 { 194 | t.Errorf("Expected page to be 3, got %d", builder.params.Page) 195 | } 196 | 197 | if builder.params.ItemsPerPage != 50 { 198 | t.Errorf("Expected limit to be 50, got %d", builder.params.ItemsPerPage) 199 | } 200 | 201 | if builder.params.Search != "test" { 202 | t.Errorf("Expected search to be 'test', got '%s'", builder.params.Search) 203 | } 204 | } 205 | 206 | func TestBuilderSearch(t *testing.T) { 207 | sql, args, err := NewBuilder(). 208 | Table("users"). 209 | Model(&TestUser{}). 210 | Search("john", "name", "email"). 211 | BuildSQL() 212 | 213 | if err != nil { 214 | t.Fatalf("Unexpected error: %v", err) 215 | } 216 | 217 | if !strings.Contains(sql, "WHERE") { 218 | t.Error("Expected SQL to contain WHERE clause") 219 | } 220 | if !strings.Contains(sql, "name") || !strings.Contains(sql, "email") { 221 | t.Error("Expected SQL to contain search fields") 222 | } 223 | if len(args) == 0 { 224 | t.Error("Expected search args") 225 | } 226 | } 227 | 228 | func TestBuilderWhereConditions(t *testing.T) { 229 | sql, args, err := NewBuilder(). 230 | Table("users"). 231 | Model(&TestUser{}). 232 | WhereEquals("status", "active"). 233 | WhereGreaterThan("age", 25). 234 | WhereLessThanOrEqual("salary", 100000). 235 | BuildSQL() 236 | 237 | if err != nil { 238 | t.Fatalf("Unexpected error: %v", err) 239 | } 240 | 241 | if !strings.Contains(sql, "WHERE") { 242 | t.Error("Expected SQL to contain WHERE clause") 243 | } 244 | if len(args) < 3 { 245 | t.Errorf("Expected at least 3 args, got %d", len(args)) 246 | } 247 | } 248 | 249 | func TestBuilderWhereIn(t *testing.T) { 250 | sql, args, err := NewBuilder(). 251 | Table("users"). 252 | Model(&TestUser{}). 253 | WhereIn("dept_id", 1, 2, 3). 254 | BuildSQL() 255 | 256 | if err != nil { 257 | t.Fatalf("Unexpected error: %v", err) 258 | } 259 | 260 | if !strings.Contains(sql, "WHERE") { 261 | t.Error("Expected SQL to contain WHERE clause") 262 | } 263 | if len(args) < 3 { 264 | t.Errorf("Expected at least 3 args, got %d", len(args)) 265 | } 266 | } 267 | 268 | func TestBuilderWhereBetween(t *testing.T) { 269 | sql, args, err := NewBuilder(). 270 | Table("users"). 271 | Model(&TestUser{}). 272 | WhereBetween("age", 18, 65). 273 | BuildSQL() 274 | 275 | if err != nil { 276 | t.Fatalf("Unexpected error: %v", err) 277 | } 278 | 279 | if !strings.Contains(sql, "WHERE") { 280 | t.Error("Expected SQL to contain WHERE clause") 281 | } 282 | if len(args) < 2 { 283 | t.Errorf("Expected at least 2 args, got %d", len(args)) 284 | } 285 | } 286 | 287 | func TestBuilderOrderBy(t *testing.T) { 288 | sql, _, err := NewBuilder(). 289 | Table("users"). 290 | Model(&TestUser{}). 291 | OrderBy("name"). 292 | OrderByDesc("age"). 293 | BuildSQL() 294 | 295 | if err != nil { 296 | t.Fatalf("Unexpected error: %v", err) 297 | } 298 | 299 | if !strings.Contains(sql, "ORDER BY") { 300 | t.Error("Expected SQL to contain ORDER BY clause") 301 | } 302 | if !strings.Contains(sql, "name") { 303 | t.Error("Expected SQL to contain name in ORDER BY") 304 | } 305 | if !strings.Contains(sql, "DESC") { 306 | t.Error("Expected SQL to contain DESC") 307 | } 308 | } 309 | 310 | func TestBuilderJoins(t *testing.T) { 311 | sql, _, err := NewBuilder(). 312 | Table("users u"). 313 | Model(&TestUser{}). 314 | LeftJoin("departments d", "u.dept_id = d.id"). 315 | InnerJoin("roles r", "u.role_id = r.id"). 316 | BuildSQL() 317 | 318 | if err != nil { 319 | t.Fatalf("Unexpected error: %v", err) 320 | } 321 | 322 | if !strings.Contains(sql, "LEFT JOIN") { 323 | t.Error("Expected SQL to contain LEFT JOIN") 324 | } 325 | if !strings.Contains(sql, "INNER JOIN") { 326 | t.Error("Expected SQL to contain INNER JOIN") 327 | } 328 | if !strings.Contains(sql, "departments d") { 329 | t.Error("Expected SQL to contain departments table") 330 | } 331 | if !strings.Contains(sql, "roles r") { 332 | t.Error("Expected SQL to contain roles table") 333 | } 334 | } 335 | 336 | func TestBuilderSelect(t *testing.T) { 337 | sql, _, err := NewBuilder(). 338 | Table("users"). 339 | Model(&TestUser{}). 340 | Select("id", "name", "email"). 341 | BuildSQL() 342 | 343 | if err != nil { 344 | t.Fatalf("Unexpected error: %v", err) 345 | } 346 | 347 | if strings.Contains(sql, "SELECT *") { 348 | t.Error("Expected SQL not to contain SELECT *") 349 | } 350 | if !strings.Contains(sql, "SELECT id, name, email") { 351 | t.Error("Expected SQL to contain specific columns") 352 | } 353 | } 354 | 355 | func TestBuilderFromJSON(t *testing.T) { 356 | jsonQuery := `{ 357 | "page": 2, 358 | "limit": 15, 359 | "search": "john", 360 | "search_fields": ["name", "email"], 361 | "eqor": { 362 | "status": ["active", "pending"] 363 | }, 364 | "gte": { 365 | "age": 18 366 | }, 367 | "sort": ["name", "-created_at"] 368 | }` 369 | 370 | sql, args, err := NewBuilder(). 371 | Table("users"). 372 | Model(&TestUser{}). 373 | FromJSON(jsonQuery). 374 | BuildSQL() 375 | 376 | if err != nil { 377 | t.Fatalf("Unexpected error: %v", err) 378 | } 379 | 380 | if !strings.Contains(sql, "WHERE") { 381 | t.Error("Expected SQL to contain WHERE clause") 382 | } 383 | if !strings.Contains(sql, "ORDER BY") { 384 | t.Error("Expected SQL to contain ORDER BY clause") 385 | } 386 | if !strings.Contains(sql, "LIMIT") { 387 | t.Error("Expected SQL to contain LIMIT clause") 388 | } 389 | if len(args) == 0 { 390 | t.Error("Expected args from JSON query") 391 | } 392 | } 393 | 394 | func TestBuilderFromMap(t *testing.T) { 395 | queryMap := map[string]any{ 396 | "page": 3, 397 | "limit": 25, 398 | "search": "test", 399 | "search_fields": []string{"name", "description"}, 400 | "eqor": map[string]any{ 401 | "category": []string{"tech", "business"}, 402 | }, 403 | "gt": map[string]any{ 404 | "price": 100, 405 | }, 406 | } 407 | 408 | sql, args, err := NewBuilder(). 409 | Table("products"). 410 | Model(&TestUser{}). 411 | FromMap(queryMap). 412 | BuildSQL() 413 | 414 | if err != nil { 415 | t.Fatalf("Unexpected error: %v", err) 416 | } 417 | 418 | if !strings.Contains(sql, "WHERE") { 419 | t.Error("Expected SQL to contain WHERE clause") 420 | } 421 | if !strings.Contains(sql, "LIMIT") { 422 | t.Error("Expected SQL to contain LIMIT clause") 423 | } 424 | if len(args) == 0 { 425 | t.Error("Expected args from map query") 426 | } 427 | } 428 | 429 | func TestBuilderValidation(t *testing.T) { 430 | // Test missing table 431 | _, _, err := NewBuilder(). 432 | Model(&TestUser{}). 433 | BuildSQL() 434 | 435 | if err == nil { 436 | t.Error("Expected error for missing table") 437 | } 438 | 439 | // Test missing model 440 | _, _, err = NewBuilder(). 441 | Table("users"). 442 | BuildSQL() 443 | 444 | if err == nil { 445 | t.Error("Expected error for missing model") 446 | } 447 | 448 | // Test invalid page 449 | builder := NewBuilder(). 450 | Table("users"). 451 | Model(&TestUser{}). 452 | Page(0) 453 | 454 | if builder.err == nil { 455 | t.Error("Expected error for invalid page") 456 | } 457 | 458 | // Test invalid limit 459 | builder = NewBuilder(). 460 | Table("users"). 461 | Model(&TestUser{}). 462 | Limit(0) 463 | 464 | if builder.err == nil { 465 | t.Error("Expected error for invalid limit") 466 | } 467 | } 468 | 469 | func TestBuilderCountSQL(t *testing.T) { 470 | countSQL, countArgs, err := NewBuilder(). 471 | Table("users"). 472 | Model(&TestUser{}). 473 | WhereEquals("status", "active"). 474 | WhereGreaterThan("age", 18). 475 | BuildCountSQL() 476 | 477 | if err != nil { 478 | t.Fatalf("Unexpected error: %v", err) 479 | } 480 | 481 | if !strings.Contains(countSQL, "SELECT COUNT") { 482 | t.Error("Expected count SQL to contain SELECT COUNT") 483 | } 484 | if !strings.Contains(countSQL, "WHERE") { 485 | t.Error("Expected count SQL to contain WHERE clause") 486 | } 487 | if strings.Contains(countSQL, "LIMIT") { 488 | t.Error("Expected count SQL not to contain LIMIT") 489 | } 490 | if strings.Contains(countSQL, "OFFSET") { 491 | t.Error("Expected count SQL not to contain OFFSET") 492 | } 493 | if len(countArgs) == 0 { 494 | t.Error("Expected count args") 495 | } 496 | } 497 | 498 | func TestBuilderComplexQuery(t *testing.T) { 499 | // Test a complex query with multiple conditions 500 | sql, args, err := NewBuilder(). 501 | Table("users u"). 502 | Model(&TestUser{}). 503 | Select("u.id", "u.name", "d.name as dept_name"). 504 | LeftJoin("departments d", "u.dept_id = d.id"). 505 | Search("john", "name", "email"). 506 | WhereEquals("status", "active"). 507 | WhereIn("dept_id", 1, 2, 3). 508 | WhereGreaterThan("age", 25). 509 | WhereLessThanOrEqual("salary", 100000). 510 | Where("u.created_at >= ?", "2023-01-01"). 511 | OrderBy("name"). 512 | OrderByDesc("age"). 513 | Page(2). 514 | Limit(20). 515 | BuildSQL() 516 | 517 | if err != nil { 518 | t.Fatalf("Unexpected error: %v", err) 519 | } 520 | 521 | // Verify all parts are present 522 | if !strings.Contains(sql, "SELECT u.id, u.name, d.name as dept_name") { 523 | t.Error("Expected SQL to contain custom SELECT") 524 | } 525 | if !strings.Contains(sql, "LEFT JOIN departments d") { 526 | t.Error("Expected SQL to contain LEFT JOIN") 527 | } 528 | if !strings.Contains(sql, "WHERE") { 529 | t.Error("Expected SQL to contain WHERE clause") 530 | } 531 | if !strings.Contains(sql, "ORDER BY") { 532 | t.Error("Expected SQL to contain ORDER BY clause") 533 | } 534 | if !strings.Contains(sql, "LIMIT") { 535 | t.Error("Expected SQL to contain LIMIT clause") 536 | } 537 | if !strings.Contains(sql, "OFFSET") { 538 | t.Error("Expected SQL to contain OFFSET clause") 539 | } 540 | 541 | // Should have multiple args 542 | if len(args) < 3 { 543 | t.Errorf("Expected at least 3 args, got %d", len(args)) 544 | } 545 | } 546 | 547 | func TestBuilderErrorHandling(t *testing.T) { 548 | // Test that errors are propagated correctly 549 | builder := NewBuilder(). 550 | Table("users"). 551 | Model(&TestUser{}). 552 | Page(-1) // This should set an error 553 | 554 | if builder.err == nil { 555 | t.Error("Expected error to be set") 556 | } 557 | 558 | // Further operations should not execute 559 | builder = builder.Limit(10).OrderBy("name") 560 | 561 | _, _, err := builder.BuildSQL() 562 | if err == nil { 563 | t.Error("Expected error to be returned from BuildSQL") 564 | } 565 | } 566 | 567 | func TestBuilderChaining(t *testing.T) { 568 | // Test that all methods return the builder for chaining 569 | builder := NewBuilder() 570 | 571 | result := builder. 572 | Table("users"). 573 | Model(&TestUser{}). 574 | Page(1). 575 | Limit(10). 576 | Search("test", "name"). 577 | WhereEquals("status", "active"). 578 | OrderBy("name") 579 | 580 | if result != builder { 581 | t.Error("Expected method chaining to return the same builder instance") 582 | } 583 | } 584 | -------------------------------------------------------------------------------- /v2/paginate/paginate_test.go: -------------------------------------------------------------------------------- 1 | // paginate_test.go 2 | package paginate 3 | 4 | import ( 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | // User struct used for testing. 11 | type User struct { 12 | ID int `json:"id" paginate:"users.id"` 13 | Name string `json:"name" paginate:"users.name"` 14 | Email string `json:"email" paginate:"users.email"` 15 | Age int `json:"age" paginate:"users.age"` 16 | } 17 | 18 | // TestNewPaginator tests the NewPaginator function. 19 | func TestNewPaginator(t *testing.T) { 20 | // Test case: Missing table should return an error. 21 | _, err := NewPaginator( 22 | WithStruct(User{}), 23 | ) 24 | if err == nil || !strings.Contains(err.Error(), "principal table is required") { 25 | t.Errorf("Expected error about missing table, got: %v", err) 26 | } 27 | 28 | // Test case: Missing struct should return an error. 29 | _, err = NewPaginator( 30 | WithTable("users"), 31 | ) 32 | if err == nil || !strings.Contains(err.Error(), "struct is required") { 33 | t.Errorf("Expected error about missing struct, got: %v", err) 34 | } 35 | 36 | // Test case: Valid paginator initialization. 37 | p, err := NewPaginator( 38 | WithTable("users"), 39 | WithStruct(User{}), 40 | ) 41 | if err != nil { 42 | t.Errorf("Unexpected error: %v", err) 43 | } 44 | if p.Page != 1 || p.ItemsPerPage != 10 { 45 | t.Errorf("Unexpected default values: Page=%d, ItemsPerPage=%d", p.Page, p.ItemsPerPage) 46 | } 47 | } 48 | 49 | // TestGenerateSQL tests the GenerateSQL method. 50 | func TestGenerateSQL(t *testing.T) { 51 | p, err := NewPaginator( 52 | WithTable("users"), 53 | WithStruct(User{}), 54 | WithPage(2), 55 | WithItemsPerPage(5), 56 | WithSearch("john"), 57 | WithSearchFields([]string{"name", "email"}), 58 | WithSort([]string{"name"}, []string{"false"}), 59 | WithWhereClause("age > ?", 30), 60 | WithJoin("INNER JOIN orders ON users.id = orders.user_id"), 61 | WithColumn("users.id"), 62 | WithColumn("users.name"), 63 | ) 64 | if err != nil { 65 | t.Fatalf("Unexpected error: %v", err) 66 | } 67 | 68 | query, args := p.GenerateSQL() 69 | expectedQuery := "SELECT users.id, users.name FROM users INNER JOIN orders ON users.id = orders.user_id WHERE (users.name::TEXT ILIKE $1 OR users.email::TEXT ILIKE $2) AND age > $3 ORDER BY users.name ASC LIMIT $4 OFFSET $5" 70 | if query != expectedQuery { 71 | t.Errorf("Expected query:\n%s\nGot:\n%s", expectedQuery, query) 72 | } 73 | 74 | expectedArgs := []interface{}{"%john%", "%john%", 30, 5, 5} 75 | if !reflect.DeepEqual(args, expectedArgs) { 76 | t.Errorf("Expected args: %v\nGot: %v", expectedArgs, args) 77 | } 78 | } 79 | 80 | // TestGenerateCountQuery tests the GenerateCountQuery method. 81 | func TestGenerateCountQuery(t *testing.T) { 82 | p, err := NewPaginator( 83 | WithTable("users"), 84 | WithStruct(User{}), 85 | WithSearch("doe"), 86 | WithSearchFields([]string{"name", "email"}), 87 | WithWhereClause("age > ?", 25), 88 | ) 89 | if err != nil { 90 | t.Fatalf("Unexpected error: %v", err) 91 | } 92 | 93 | query, args := p.GenerateCountQuery() 94 | expectedQuery := "SELECT COUNT(users.id) FROM users WHERE (users.name::TEXT ILIKE $1 OR users.email::TEXT ILIKE $2) AND age > $3" 95 | if query != expectedQuery { 96 | t.Errorf("Expected query:\n%s\nGot:\n%s", expectedQuery, query) 97 | } 98 | 99 | expectedArgs := []interface{}{"%doe%", "%doe%", 25} 100 | if !reflect.DeepEqual(args, expectedArgs) { 101 | t.Errorf("Expected args: %v\nGot: %v", expectedArgs, args) 102 | } 103 | } 104 | 105 | // TestNoOffset tests the NoOffset option. 106 | func TestNoOffset(t *testing.T) { 107 | p, err := NewPaginator( 108 | WithTable("users"), 109 | WithStruct(User{}), 110 | WithNoOffset(true), 111 | ) 112 | if err != nil { 113 | t.Fatalf("Unexpected error: %v", err) 114 | } 115 | 116 | query, args := p.GenerateSQL() 117 | if strings.Contains(query, "OFFSET") { 118 | t.Errorf("Expected query without OFFSET, got: %s", query) 119 | } 120 | if len(args) != 1 || args[0] != 10 { 121 | t.Errorf("Expected args: [10], got: %v", args) 122 | } 123 | } 124 | 125 | // TestVacuumCountQuery tests the Vacuum option in GenerateCountQuery. 126 | func TestVacuumCountQuery(t *testing.T) { 127 | p, err := NewPaginator( 128 | WithTable("users"), 129 | WithStruct(User{}), 130 | WithVacuum(true), 131 | ) 132 | if err != nil { 133 | t.Fatalf("Unexpected error: %v", err) 134 | } 135 | 136 | query, _ := p.GenerateCountQuery() 137 | if !strings.Contains(query, "count_estimate") { 138 | t.Errorf("Expected count_estimate in query, got: %s", query) 139 | } 140 | } 141 | 142 | // TestWithMapArgs tests the WithMapArgs option. 143 | func TestWithMapArgs(t *testing.T) { 144 | mapArgs := map[string]interface{}{ 145 | "key1": "value1", 146 | "key2": 42, 147 | } 148 | p, err := NewPaginator( 149 | WithTable("users"), 150 | WithStruct(User{}), 151 | WithMapArgs(mapArgs), 152 | ) 153 | if err != nil { 154 | t.Fatalf("Unexpected error: %v", err) 155 | } 156 | 157 | if !reflect.DeepEqual(p.MapArgs, mapArgs) { 158 | t.Errorf("Expected MapArgs: %v\nGot: %v", mapArgs, p.MapArgs) 159 | } 160 | } 161 | 162 | // TestWithWhereCombining tests the WhereCombining option. 163 | func TestWithWhereCombining(t *testing.T) { 164 | p, err := NewPaginator( 165 | WithTable("users"), 166 | WithStruct(User{}), 167 | WithWhereCombining("OR"), 168 | WithWhereClause("age > ?", 20), 169 | WithWhereClause("age < ?", 30), 170 | ) 171 | if err != nil { 172 | t.Fatalf("Unexpected error: %v", err) 173 | } 174 | 175 | query, args := p.GenerateSQL() 176 | if !strings.Contains(query, "age > $1 OR age < $2") { 177 | t.Errorf("Expected WHERE clause with OR, got: %s", query) 178 | } 179 | expectedArgs := []interface{}{20, 30, 10, 0} 180 | if !reflect.DeepEqual(args, expectedArgs) { 181 | t.Errorf("Expected args: %v\nGot: %v", expectedArgs, args) 182 | } 183 | } 184 | 185 | // TestGetFieldNameInvalidType tests getFieldName with an invalid type. 186 | func TestGetFieldNameInvalidType(t *testing.T) { 187 | defer func() { 188 | if r := recover(); r == nil { 189 | t.Errorf("Expected panic for invalid type, but did not panic") 190 | } 191 | }() 192 | 193 | getFieldName("id", "json", "paginate", "not a struct") 194 | } 195 | 196 | // TestReplacePlaceholders tests the replacePlaceholders function. 197 | func TestReplacePlaceholders(t *testing.T) { 198 | query := "SELECT * FROM users WHERE name = ? AND age > ?" 199 | args := []interface{}{"John", 30} 200 | expectedQuery := "SELECT * FROM users WHERE name = $1 AND age > $2" 201 | 202 | resultQuery, resultArgs := replacePlaceholders(query, args) 203 | if resultQuery != expectedQuery { 204 | t.Errorf("Expected query:\n%s\nGot:\n%s", expectedQuery, resultQuery) 205 | } 206 | if !reflect.DeepEqual(resultArgs, args) { 207 | t.Errorf("Expected args: %v\nGot: %v", args, resultArgs) 208 | } 209 | } 210 | 211 | // TestReplacePlaceholdersNoPlaceholders tests replacePlaceholders with no placeholders. 212 | func TestReplacePlaceholdersNoPlaceholders(t *testing.T) { 213 | query := "SELECT * FROM users" 214 | args := []interface{}{} 215 | resultQuery, resultArgs := replacePlaceholders(query, args) 216 | if resultQuery != query { 217 | t.Errorf("Expected query unchanged, got: %s", resultQuery) 218 | } 219 | if len(resultArgs) != 0 { 220 | t.Errorf("Expected no args, got: %v", resultArgs) 221 | } 222 | } 223 | 224 | // TestGetFieldName tests the getFieldName function. 225 | func TestGetFieldName(t *testing.T) { 226 | s := User{} 227 | fieldName := getFieldName("name", "json", "paginate", s) 228 | expected := "users.name" 229 | if fieldName != expected { 230 | t.Errorf("Expected field name: %s\nGot: %s", expected, fieldName) 231 | } 232 | 233 | // Test non-existent field. 234 | fieldName = getFieldName("nonexistent", "json", "paginate", s) 235 | if fieldName != "" { 236 | t.Errorf("Expected empty field name, got: %s", fieldName) 237 | } 238 | } 239 | 240 | // TestWithSortInvalidDirections tests WithSort with mismatched directions. 241 | func TestWithSortInvalidDirections(t *testing.T) { 242 | p, err := NewPaginator( 243 | WithTable("users"), 244 | WithStruct(User{}), 245 | WithSort([]string{"name", "age"}, []string{"false"}), 246 | ) 247 | if err != nil { 248 | t.Fatalf("Unexpected error: %v", err) 249 | } 250 | 251 | query, _ := p.GenerateSQL() 252 | if strings.Contains(query, "ORDER BY") { 253 | t.Errorf("Expected no ORDER BY clause due to mismatched sort directions, got: %s", query) 254 | } 255 | } 256 | 257 | // TestWithInvalidSearchFields tests invalid search fields. 258 | func TestWithInvalidSearchFields(t *testing.T) { 259 | p, err := NewPaginator( 260 | WithTable("users"), 261 | WithStruct(User{}), 262 | WithSearch("john"), 263 | WithSearchFields([]string{"nonexistent"}), 264 | ) 265 | if err != nil { 266 | t.Fatalf("Unexpected error: %v", err) 267 | } 268 | 269 | query, args := p.GenerateSQL() 270 | if strings.Contains(query, "ILIKE") { 271 | t.Errorf("Expected no ILIKE clause due to invalid search fields, got: %s", query) 272 | } 273 | if len(args) != 2 { 274 | t.Errorf("Expected no args, got: %v", args) 275 | } 276 | } 277 | 278 | // TestWithEmptySortColumns tests empty sort columns. 279 | func TestWithEmptySortColumns(t *testing.T) { 280 | p, err := NewPaginator( 281 | WithTable("users"), 282 | WithStruct(User{}), 283 | WithSort([]string{}, []string{}), 284 | ) 285 | if err != nil { 286 | t.Fatalf("Unexpected error: %v", err) 287 | } 288 | 289 | query, _ := p.GenerateSQL() 290 | if strings.Contains(query, "ORDER BY") { 291 | t.Errorf("Expected no ORDER BY clause, got: %s", query) 292 | } 293 | } 294 | 295 | // TestSchemaUsage tests usage of the Schema option. 296 | func TestSchemaUsage(t *testing.T) { 297 | p, err := NewPaginator( 298 | WithSchema("public"), 299 | WithTable("users"), 300 | WithStruct(User{}), 301 | ) 302 | if err != nil { 303 | t.Fatalf("Unexpected error: %v", err) 304 | } 305 | 306 | query, _ := p.GenerateSQL() 307 | if !strings.Contains(query, "FROM public.users") { 308 | t.Errorf("Expected schema in FROM clause, got: %s", query) 309 | } 310 | } 311 | 312 | // TestComplexWhereClause tests a complex WHERE clause. 313 | func TestComplexWhereClause(t *testing.T) { 314 | p, err := NewPaginator( 315 | WithTable("users"), 316 | WithStruct(User{}), 317 | WithWhereClause("(age > ? AND age < ?) OR email LIKE ?", 20, 30, "%@example.com"), 318 | ) 319 | if err != nil { 320 | t.Fatalf("Unexpected error: %v", err) 321 | } 322 | 323 | query, args := p.GenerateSQL() 324 | expectedQueryPart := "(age > $1 AND age < $2) OR email LIKE $3" 325 | if !strings.Contains(query, expectedQueryPart) { 326 | t.Errorf("Expected complex WHERE clause, got: %s", query) 327 | } 328 | expectedArgs := []interface{}{20, 30, "%@example.com", 10, 0} 329 | if !reflect.DeepEqual(args, expectedArgs) { 330 | t.Errorf("Expected args: %v\nGot: %v", expectedArgs, args) 331 | } 332 | } 333 | 334 | // TestMultipleJoinsAndPostInstanceWhereClause tests multiple joins and adding a where clause after paginator instance creation. 335 | func TestMultipleJoinsAndPostInstanceWhereClause(t *testing.T) { 336 | // Define structs representing the database tables. 337 | type Order struct { 338 | ID int `json:"id" paginate:"orders.id"` 339 | UserID int `json:"user_id" paginate:"orders.user_id"` 340 | Total float64 `json:"total" paginate:"orders.total"` 341 | } 342 | 343 | type Product struct { 344 | ID int `json:"id" paginate:"products.id"` 345 | OrderID int `json:"order_id" paginate:"products.order_id"` 346 | Name string `json:"name" paginate:"products.name"` 347 | Price float64 `json:"price" paginate:"products.price"` 348 | } 349 | 350 | // Create the paginator instance with initial options. 351 | p, err := NewPaginator( 352 | WithTable("users"), 353 | WithStruct(User{}), 354 | WithColumn("users.id"), 355 | WithColumn("users.name"), 356 | WithJoin("INNER JOIN orders ON users.id = orders.user_id"), 357 | WithJoin("INNER JOIN products ON orders.id = products.order_id"), 358 | ) 359 | if err != nil { 360 | t.Fatalf("Unexpected error: %v", err) 361 | } 362 | 363 | // Add a where clause after the paginator has been created. 364 | WithWhereClause("products.price > ?", 100.0)(p) 365 | WithWhereClause("orders.total < ?", 1000.0)(p) 366 | 367 | // Generate the SQL query and arguments. 368 | query, args := p.GenerateSQL() 369 | 370 | // Expected SQL query. 371 | expectedQuery := "SELECT users.id, users.name FROM users INNER JOIN orders ON users.id = orders.user_id INNER JOIN products ON orders.id = products.order_id WHERE products.price > $1 AND orders.total < $2 LIMIT $3 OFFSET $4" 372 | 373 | // Check if the generated query matches the expected query. 374 | if query != expectedQuery { 375 | t.Errorf("Expected query:\n%s\nGot:\n%s", expectedQuery, query) 376 | } 377 | 378 | // Expected arguments. 379 | expectedArgs := []interface{}{100.0, 1000.0, 10, 0} 380 | 381 | // Check if the generated arguments match the expected arguments. 382 | if !reflect.DeepEqual(args, expectedArgs) { 383 | t.Errorf("Expected args: %v\nGot: %v", expectedArgs, args) 384 | } 385 | } 386 | 387 | // TestFullComplexPaginator tests a paginator using all properties in a complex scenario. 388 | func TestFullComplexPaginator(t *testing.T) { 389 | // Define structs representing the database tables. 390 | type User struct { 391 | ID int `json:"id" paginate:"u.id"` 392 | Name string `json:"name" paginate:"u.name"` 393 | Email string `json:"email" paginate:"u.email"` 394 | Age int `json:"age" paginate:"u.age"` 395 | RoleID int `json:"role_id" paginate:"u.role_id"` 396 | IsActive bool `json:"is_active" paginate:"u.is_active"` 397 | } 398 | 399 | type Role struct { 400 | ID int `json:"id" paginate:"r.id"` 401 | Name string `json:"name" paginate:"r.name"` 402 | } 403 | 404 | type Order struct { 405 | ID int `json:"id" paginate:"o.id"` 406 | UserID int `json:"user_id" paginate:"o.user_id"` 407 | Total float64 `json:"total" paginate:"o.total"` 408 | Status string `json:"status" paginate:"o.status"` 409 | } 410 | 411 | // Initialize MapArgs with custom parameters. 412 | mapArgs := map[string]interface{}{ 413 | "min_age": 25, 414 | "max_age": 35, 415 | "active_only": true, 416 | "statuses": []string{"completed", "shipped"}, 417 | } 418 | 419 | // Create the paginator instance with all properties. 420 | p, err := NewPaginator( 421 | WithSchema("public"), 422 | WithTable("users"), 423 | WithStruct(User{}), 424 | WithPage(3), 425 | WithItemsPerPage(15), 426 | WithSearch("john doe"), 427 | WithSearchFields([]string{"name", "email"}), 428 | WithVacuum(false), 429 | WithNoOffset(false), 430 | WithMapArgs(mapArgs), 431 | WithColumn("u.id"), 432 | WithColumn("u.name"), 433 | WithColumn("u.email"), 434 | WithColumn("r.name AS role_name"), 435 | WithColumn("SUM(o.total) AS total_spent"), 436 | WithJoin("INNER JOIN roles r ON u.role_id = r.id"), 437 | WithJoin("LEFT JOIN orders o ON u.id = o.user_id"), 438 | WithSort([]string{"name"}, []string{"false"}), 439 | ) 440 | if err != nil { 441 | t.Fatalf("Unexpected error: %v", err) 442 | } 443 | 444 | // Add complex where clauses after paginator creation. 445 | WithWhereClause("u.age BETWEEN ? AND ?", mapArgs["min_age"], mapArgs["max_age"])(p) 446 | WithWhereClause("u.is_active = ?", mapArgs["active_only"])(p) 447 | WithWhereClause("o.status IN (?)", mapArgs["statuses"])(p) 448 | WithWhereCombining("AND")(p) 449 | 450 | // Generate the SQL query and arguments. 451 | query, args := p.GenerateSQL() 452 | 453 | // Expected SQL query. 454 | expectedQuery := "SELECT u.id, u.name, u.email, r.name AS role_name, SUM(o.total) AS total_spent FROM public.users INNER JOIN roles r ON u.role_id = r.id LEFT JOIN orders o ON u.id = o.user_id WHERE (u.name::TEXT ILIKE $1 OR u.email::TEXT ILIKE $2) AND u.age BETWEEN $3 AND $4 AND u.is_active = $5 AND o.status IN ($6) ORDER BY u.name ASC LIMIT $7 OFFSET $8" 455 | 456 | // Check if the generated query matches the expected query. 457 | if query != expectedQuery { 458 | t.Errorf("Expected query:\n%s\nGot:\n%s", expectedQuery, query) 459 | } 460 | 461 | // Expected arguments. 462 | expectedArgs := []interface{}{ 463 | "%john doe%", // Search for name 464 | "%john doe%", // Search for email 465 | mapArgs["min_age"], // u.age BETWEEN ? 466 | mapArgs["max_age"], 467 | mapArgs["active_only"], // u.is_active = ? 468 | mapArgs["statuses"], // o.status IN (?) 469 | 15, // LIMIT 470 | 30, // OFFSET (page 3 with 15 items per page) 471 | } 472 | 473 | // Check if the generated arguments match the expected arguments. 474 | if !reflect.DeepEqual(args, expectedArgs) { 475 | t.Errorf("Expected args: %v\nGot: %v", expectedArgs, args) 476 | } 477 | 478 | // Generate the count query. 479 | countQuery, countArgs := p.GenerateCountQuery() 480 | 481 | // Expected count query. 482 | expectedCountQuery := "SELECT COUNT(u.id) FROM public.users INNER JOIN roles r ON u.role_id = r.id LEFT JOIN orders o ON u.id = o.user_id WHERE (u.name::TEXT ILIKE $1 OR u.email::TEXT ILIKE $2) AND u.age BETWEEN $3 AND $4 AND u.is_active = $5 AND o.status IN ($6)" 483 | 484 | // Check if the generated count query matches the expected count query. 485 | if countQuery != expectedCountQuery { 486 | t.Errorf("Expected count query:\n%s\nGot:\n%s", expectedCountQuery, countQuery) 487 | } 488 | 489 | // Check if the generated count arguments match the expected arguments. 490 | if !reflect.DeepEqual(countArgs, expectedArgs[:6]) { 491 | t.Errorf("Expected count args: %v\nGot: %v", expectedArgs[:6], countArgs) 492 | } 493 | } 494 | -------------------------------------------------------------------------------- /v3/paginate/paginate.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "reflect" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // QueryParams contains the parameters for the paginated query. 12 | type QueryParams struct { 13 | Page int 14 | ItemsPerPage int 15 | Search string 16 | SearchFields []string 17 | Vacuum bool 18 | Columns []string 19 | Joins []string 20 | SortColumns []string 21 | SortDirections []string 22 | WhereClauses []string 23 | WhereArgs []any 24 | WhereCombining string 25 | Schema string 26 | Table string 27 | Struct any 28 | MapArgs map[string]any 29 | NoOffset bool 30 | // New filter fields 31 | Like map[string][]string 32 | LikeOr map[string][]string 33 | LikeAnd map[string][]string 34 | Eq map[string][]any 35 | EqOr map[string][]any 36 | EqAnd map[string][]any 37 | Gte map[string]any 38 | Gt map[string]any 39 | Lte map[string]any 40 | Lt map[string]any 41 | In map[string][]any 42 | NotIn map[string][]any 43 | Between map[string][2]any 44 | IsNull []string 45 | IsNotNull []string 46 | } 47 | 48 | // Option is a function that configures options in QueryParams. 49 | type Option func(*QueryParams) 50 | 51 | // WithNoOffset sets the NoOffset option. 52 | func WithNoOffset(noOffset bool) Option { 53 | return func(params *QueryParams) { 54 | params.NoOffset = noOffset 55 | } 56 | } 57 | 58 | // WithMapArgs sets the MapArgs option. 59 | func WithMapArgs(mapArgs map[string]any) Option { 60 | return func(params *QueryParams) { 61 | params.MapArgs = mapArgs 62 | } 63 | } 64 | 65 | // WithStruct sets the Struct option. 66 | func WithStruct(s any) Option { 67 | return func(params *QueryParams) { 68 | params.Struct = s 69 | } 70 | } 71 | 72 | // WithSchema sets the Schema option. 73 | func WithSchema(schema string) Option { 74 | return func(params *QueryParams) { 75 | params.Schema = schema 76 | } 77 | } 78 | 79 | // WithTable sets the Table option. 80 | func WithTable(table string) Option { 81 | return func(params *QueryParams) { 82 | params.Table = table 83 | } 84 | } 85 | 86 | // WithPage sets the Page option. 87 | func WithPage(page int) Option { 88 | return func(params *QueryParams) { 89 | params.Page = page 90 | } 91 | } 92 | 93 | // WithItemsPerPage sets the ItemsPerPage option. 94 | func WithItemsPerPage(itemsPerPage int) Option { 95 | return func(params *QueryParams) { 96 | params.ItemsPerPage = itemsPerPage 97 | } 98 | } 99 | 100 | // WithSearch sets the Search option. 101 | func WithSearch(search string) Option { 102 | return func(params *QueryParams) { 103 | params.Search = search 104 | } 105 | } 106 | 107 | // WithSearchFields sets the SearchFields option. 108 | func WithSearchFields(searchFields []string) Option { 109 | return func(params *QueryParams) { 110 | params.SearchFields = searchFields 111 | } 112 | } 113 | 114 | // WithVacuum sets the Vacuum option. 115 | func WithVacuum(vacuum bool) Option { 116 | return func(params *QueryParams) { 117 | params.Vacuum = vacuum 118 | } 119 | } 120 | 121 | // WithColumn adds a column to the Columns option. 122 | func WithColumn(column string) Option { 123 | return func(params *QueryParams) { 124 | params.Columns = append(params.Columns, column) 125 | } 126 | } 127 | 128 | // WithSort sets the SortColumns and SortDirections options. 129 | func WithSort(sortColumns, sortDirections []string) Option { 130 | return func(params *QueryParams) { 131 | params.SortColumns = sortColumns 132 | params.SortDirections = sortDirections 133 | } 134 | } 135 | 136 | // WithJoin adds a join clause to the Joins option. 137 | func WithJoin(join string) Option { 138 | return func(params *QueryParams) { 139 | params.Joins = append(params.Joins, join) 140 | } 141 | } 142 | 143 | // WithWhereCombining sets the WhereCombining option. 144 | func WithWhereCombining(combining string) Option { 145 | return func(params *QueryParams) { 146 | params.WhereCombining = combining 147 | } 148 | } 149 | 150 | // WithWhereClause adds a where clause and its arguments. 151 | func WithWhereClause(clause string, args ...any) Option { 152 | return func(params *QueryParams) { 153 | params.WhereClauses = append(params.WhereClauses, clause) 154 | params.WhereArgs = append(params.WhereArgs, args...) 155 | } 156 | } 157 | 158 | // WithLike sets the Like filter. 159 | func WithLike(like map[string][]string) Option { 160 | return func(params *QueryParams) { 161 | params.Like = like 162 | } 163 | } 164 | 165 | // WithLikeOr sets the LikeOr filter. 166 | func WithLikeOr(likeOr map[string][]string) Option { 167 | return func(params *QueryParams) { 168 | params.LikeOr = likeOr 169 | } 170 | } 171 | 172 | // WithLikeAnd sets the LikeAnd filter. 173 | func WithLikeAnd(likeAnd map[string][]string) Option { 174 | return func(params *QueryParams) { 175 | params.LikeAnd = likeAnd 176 | } 177 | } 178 | 179 | // WithEq sets the Eq filter. 180 | func WithEq(eq map[string][]any) Option { 181 | return func(params *QueryParams) { 182 | params.Eq = eq 183 | } 184 | } 185 | 186 | // WithEqOr sets the EqOr filter. 187 | func WithEqOr(eqOr map[string][]any) Option { 188 | return func(params *QueryParams) { 189 | params.EqOr = eqOr 190 | } 191 | } 192 | 193 | // WithEqAnd sets the EqAnd filter. 194 | func WithEqAnd(eqAnd map[string][]any) Option { 195 | return func(params *QueryParams) { 196 | params.EqAnd = eqAnd 197 | } 198 | } 199 | 200 | // WithSearchOr is deprecated, use WithLikeOr instead. 201 | func WithSearchOr(searchOr map[string][]string) Option { 202 | return WithLikeOr(searchOr) 203 | } 204 | 205 | // WithSearchAnd is deprecated, use WithLikeAnd instead. 206 | func WithSearchAnd(searchAnd map[string][]string) Option { 207 | return WithLikeAnd(searchAnd) 208 | } 209 | 210 | // WithEqualsOr is deprecated, use WithEqOr instead. 211 | func WithEqualsOr(equalsOr map[string][]any) Option { 212 | return WithEqOr(equalsOr) 213 | } 214 | 215 | // WithEqualsAnd is deprecated, use WithEqAnd instead. 216 | func WithEqualsAnd(equalsAnd map[string][]any) Option { 217 | return WithEqAnd(equalsAnd) 218 | } 219 | 220 | // WithGte sets the Gte (greater than or equal) filter. 221 | func WithGte(gte map[string]any) Option { 222 | return func(params *QueryParams) { 223 | params.Gte = gte 224 | } 225 | } 226 | 227 | // WithGt sets the Gt (greater than) filter. 228 | func WithGt(gt map[string]any) Option { 229 | return func(params *QueryParams) { 230 | params.Gt = gt 231 | } 232 | } 233 | 234 | // WithLte sets the Lte (less than or equal) filter. 235 | func WithLte(lte map[string]any) Option { 236 | return func(params *QueryParams) { 237 | params.Lte = lte 238 | } 239 | } 240 | 241 | // WithLt sets the Lt (less than) filter. 242 | func WithLt(lt map[string]any) Option { 243 | return func(params *QueryParams) { 244 | params.Lt = lt 245 | } 246 | } 247 | 248 | // WithIn sets the In filter. 249 | func WithIn(in map[string][]any) Option { 250 | return func(params *QueryParams) { 251 | params.In = in 252 | } 253 | } 254 | 255 | // WithNotIn sets the NotIn filter. 256 | func WithNotIn(notIn map[string][]any) Option { 257 | return func(params *QueryParams) { 258 | params.NotIn = notIn 259 | } 260 | } 261 | 262 | // WithBetween sets the Between filter. 263 | func WithBetween(between map[string][2]any) Option { 264 | return func(params *QueryParams) { 265 | params.Between = between 266 | } 267 | } 268 | 269 | // WithIsNull sets the IsNull filter. 270 | func WithIsNull(isNull []string) Option { 271 | return func(params *QueryParams) { 272 | params.IsNull = isNull 273 | } 274 | } 275 | 276 | // WithIsNotNull sets the IsNotNull filter. 277 | func WithIsNotNull(isNotNull []string) Option { 278 | return func(params *QueryParams) { 279 | params.IsNotNull = isNotNull 280 | } 281 | } 282 | 283 | // NewPaginator creates a new QueryParams instance with the given options. 284 | func NewPaginator(options ...Option) (*QueryParams, error) { 285 | params := &QueryParams{ 286 | Page: 1, 287 | ItemsPerPage: 10, 288 | WhereCombining: "AND", 289 | NoOffset: false, 290 | } 291 | 292 | // Apply options 293 | for _, option := range options { 294 | option(params) 295 | } 296 | 297 | // Validation 298 | if params.Table == "" { 299 | return nil, errors.New("principal table is required") 300 | } 301 | 302 | if params.Struct == nil { 303 | return nil, errors.New("struct is required") 304 | } 305 | 306 | return params, nil 307 | } 308 | 309 | // GenerateSQL generates the paginated SQL query and its arguments. 310 | func (params *QueryParams) GenerateSQL() (string, []any) { 311 | var clauses []string 312 | var args []any 313 | 314 | // SELECT clause 315 | selectClause := "SELECT " 316 | if len(params.Columns) > 0 { 317 | selectClause += strings.Join(params.Columns, ", ") 318 | } else { 319 | selectClause += "*" 320 | } 321 | clauses = append(clauses, selectClause) 322 | 323 | // FROM clause 324 | fromClause := fmt.Sprintf("FROM %s", params.Table) 325 | if params.Schema != "" { 326 | fromClause = fmt.Sprintf("FROM %s.%s", params.Schema, params.Table) 327 | } 328 | clauses = append(clauses, fromClause) 329 | 330 | // JOIN clauses 331 | if len(params.Joins) > 0 { 332 | clauses = append(clauses, strings.Join(params.Joins, " ")) 333 | } 334 | 335 | // WHERE clause 336 | whereClauses, whereArgs := params.buildWhereClauses() 337 | if len(whereClauses) > 0 { 338 | clauses = append(clauses, "WHERE "+strings.Join(whereClauses, " AND ")) 339 | args = append(args, whereArgs...) 340 | } 341 | 342 | // ORDER BY clause 343 | orderClause := params.buildOrderClause() 344 | if orderClause != "" { 345 | clauses = append(clauses, orderClause) 346 | } 347 | 348 | // LIMIT and OFFSET 349 | limitOffsetClause, limitOffsetArgs := params.buildLimitOffsetClause() 350 | clauses = append(clauses, limitOffsetClause) 351 | args = append(args, limitOffsetArgs...) 352 | 353 | // Combine all clauses 354 | query := strings.Join(clauses, " ") 355 | 356 | // Replace placeholders 357 | query, args = replacePlaceholders(query, args) 358 | 359 | // Log SQL if debug mode is enabled 360 | logSQL("GenerateSQL", query, args) 361 | 362 | return query, args 363 | } 364 | 365 | // GenerateCountQuery generates the SQL query for counting total records. 366 | func (params *QueryParams) GenerateCountQuery() (string, []any) { 367 | var clauses []string 368 | var args []any 369 | 370 | // SELECT COUNT clause 371 | countSelectClause := "SELECT COUNT(id)" 372 | idColumnName := getFieldName("id", "json", "paginate", params.Struct) 373 | if idColumnName != "" { 374 | countSelectClause = fmt.Sprintf("SELECT COUNT(%s)", idColumnName) 375 | } 376 | clauses = append(clauses, countSelectClause) 377 | 378 | // FROM clause 379 | fromClause := fmt.Sprintf("FROM %s", params.Table) 380 | if params.Schema != "" { 381 | fromClause = fmt.Sprintf("FROM %s.%s", params.Schema, params.Table) 382 | } 383 | clauses = append(clauses, fromClause) 384 | 385 | // JOIN clauses 386 | if len(params.Joins) > 0 { 387 | clauses = append(clauses, strings.Join(params.Joins, " ")) 388 | } 389 | 390 | // WHERE clause 391 | whereClauses, whereArgs := params.buildWhereClauses() 392 | if len(whereClauses) > 0 { 393 | clauses = append(clauses, "WHERE "+strings.Join(whereClauses, " AND ")) 394 | args = append(args, whereArgs...) 395 | } 396 | 397 | // Combine all clauses 398 | query := strings.Join(clauses, " ") 399 | 400 | // Replace placeholders 401 | query, args = replacePlaceholders(query, args) 402 | 403 | if params.Vacuum { 404 | countQuery := "SELECT count_estimate('" + query + "');" 405 | countQuery = strings.Replace(countQuery, "COUNT(id)", "1", -1) 406 | re := regexp.MustCompile(`(\$[0-9]+)`) 407 | countQuery = re.ReplaceAllStringFunc(countQuery, func(match string) string { 408 | return "''" + match + "''" 409 | }) 410 | 411 | // Log SQL if debug mode is enabled 412 | logSQL("GenerateCountQuery (Vacuum)", countQuery, args) 413 | 414 | return countQuery, args 415 | } 416 | 417 | // Log SQL if debug mode is enabled 418 | logSQL("GenerateCountQuery", query, args) 419 | 420 | return query, args 421 | } 422 | 423 | // buildWhereClauses constructs the WHERE clauses and arguments. 424 | func (params *QueryParams) buildWhereClauses() ([]string, []any) { 425 | var whereClauses []string 426 | var args []any 427 | 428 | // Search conditions (legacy) 429 | if params.Search != "" && len(params.SearchFields) > 0 { 430 | var searchConditions []string 431 | for _, field := range params.SearchFields { 432 | columnName := getFieldName(field, "json", "paginate", params.Struct) 433 | if columnName != "" { 434 | searchConditions = append(searchConditions, fmt.Sprintf("%s::TEXT ILIKE ?", columnName)) 435 | args = append(args, "%"+params.Search+"%") 436 | } 437 | } 438 | if len(searchConditions) > 0 { 439 | whereClauses = append(whereClauses, "("+strings.Join(searchConditions, " OR ")+")") 440 | } 441 | } 442 | 443 | // Like conditions 444 | if len(params.Like) > 0 { 445 | for field, values := range params.Like { 446 | columnName := getFieldName(field, "json", "paginate", params.Struct) 447 | if columnName != "" && len(values) > 0 { 448 | var searchConditions []string 449 | for _, value := range values { 450 | searchConditions = append(searchConditions, fmt.Sprintf("%s::TEXT ILIKE ?", columnName)) 451 | args = append(args, "%"+value+"%") 452 | } 453 | whereClauses = append(whereClauses, "("+strings.Join(searchConditions, " OR ")+")") 454 | } 455 | } 456 | } 457 | 458 | // LikeOr conditions 459 | if len(params.LikeOr) > 0 { 460 | for field, values := range params.LikeOr { 461 | columnName := getFieldName(field, "json", "paginate", params.Struct) 462 | if columnName != "" && len(values) > 0 { 463 | var searchConditions []string 464 | for _, value := range values { 465 | searchConditions = append(searchConditions, fmt.Sprintf("%s::TEXT ILIKE ?", columnName)) 466 | args = append(args, "%"+value+"%") 467 | } 468 | whereClauses = append(whereClauses, "("+strings.Join(searchConditions, " OR ")+")") 469 | } 470 | } 471 | } 472 | 473 | // LikeAnd conditions 474 | if len(params.LikeAnd) > 0 { 475 | for field, values := range params.LikeAnd { 476 | columnName := getFieldName(field, "json", "paginate", params.Struct) 477 | if columnName != "" && len(values) > 0 { 478 | var searchConditions []string 479 | for _, value := range values { 480 | searchConditions = append(searchConditions, fmt.Sprintf("%s::TEXT ILIKE ?", columnName)) 481 | args = append(args, "%"+value+"%") 482 | } 483 | whereClauses = append(whereClauses, "("+strings.Join(searchConditions, " AND ")+")") 484 | } 485 | } 486 | } 487 | 488 | // Eq conditions 489 | if len(params.Eq) > 0 { 490 | for field, values := range params.Eq { 491 | columnName := getFieldName(field, "json", "paginate", params.Struct) 492 | if columnName != "" && len(values) > 0 { 493 | var equalsConditions []string 494 | for _, value := range values { 495 | equalsConditions = append(equalsConditions, fmt.Sprintf("%s = ?", columnName)) 496 | args = append(args, value) 497 | } 498 | whereClauses = append(whereClauses, "("+strings.Join(equalsConditions, " OR ")+")") 499 | } 500 | } 501 | } 502 | 503 | // EqOr conditions 504 | if len(params.EqOr) > 0 { 505 | for field, values := range params.EqOr { 506 | columnName := getFieldName(field, "json", "paginate", params.Struct) 507 | if columnName != "" && len(values) > 0 { 508 | var equalsConditions []string 509 | for _, value := range values { 510 | equalsConditions = append(equalsConditions, fmt.Sprintf("%s = ?", columnName)) 511 | args = append(args, value) 512 | } 513 | whereClauses = append(whereClauses, "("+strings.Join(equalsConditions, " OR ")+")") 514 | } 515 | } 516 | } 517 | 518 | // EqAnd conditions 519 | if len(params.EqAnd) > 0 { 520 | for field, values := range params.EqAnd { 521 | columnName := getFieldName(field, "json", "paginate", params.Struct) 522 | if columnName != "" && len(values) > 0 { 523 | var equalsConditions []string 524 | for _, value := range values { 525 | equalsConditions = append(equalsConditions, fmt.Sprintf("%s = ?", columnName)) 526 | args = append(args, value) 527 | } 528 | whereClauses = append(whereClauses, "("+strings.Join(equalsConditions, " AND ")+")") 529 | } 530 | } 531 | } 532 | 533 | // Gte conditions 534 | if len(params.Gte) > 0 { 535 | for field, value := range params.Gte { 536 | columnName := getFieldName(field, "json", "paginate", params.Struct) 537 | if columnName != "" { 538 | whereClauses = append(whereClauses, fmt.Sprintf("%s >= ?", columnName)) 539 | args = append(args, value) 540 | } 541 | } 542 | } 543 | 544 | // Gt conditions 545 | if len(params.Gt) > 0 { 546 | for field, value := range params.Gt { 547 | columnName := getFieldName(field, "json", "paginate", params.Struct) 548 | if columnName != "" { 549 | whereClauses = append(whereClauses, fmt.Sprintf("%s > ?", columnName)) 550 | args = append(args, value) 551 | } 552 | } 553 | } 554 | 555 | // Lte conditions 556 | if len(params.Lte) > 0 { 557 | for field, value := range params.Lte { 558 | columnName := getFieldName(field, "json", "paginate", params.Struct) 559 | if columnName != "" { 560 | whereClauses = append(whereClauses, fmt.Sprintf("%s <= ?", columnName)) 561 | args = append(args, value) 562 | } 563 | } 564 | } 565 | 566 | // Lt conditions 567 | if len(params.Lt) > 0 { 568 | for field, value := range params.Lt { 569 | columnName := getFieldName(field, "json", "paginate", params.Struct) 570 | if columnName != "" { 571 | whereClauses = append(whereClauses, fmt.Sprintf("%s < ?", columnName)) 572 | args = append(args, value) 573 | } 574 | } 575 | } 576 | 577 | // In conditions 578 | if len(params.In) > 0 { 579 | for field, values := range params.In { 580 | columnName := getFieldName(field, "json", "paginate", params.Struct) 581 | if columnName != "" && len(values) > 0 { 582 | placeholders := make([]string, len(values)) 583 | for i := range values { 584 | placeholders[i] = "?" 585 | args = append(args, values[i]) 586 | } 587 | whereClauses = append(whereClauses, fmt.Sprintf("%s IN (%s)", columnName, strings.Join(placeholders, ", "))) 588 | } 589 | } 590 | } 591 | 592 | // NotIn conditions 593 | if len(params.NotIn) > 0 { 594 | for field, values := range params.NotIn { 595 | columnName := getFieldName(field, "json", "paginate", params.Struct) 596 | if columnName != "" && len(values) > 0 { 597 | placeholders := make([]string, len(values)) 598 | for i := range values { 599 | placeholders[i] = "?" 600 | args = append(args, values[i]) 601 | } 602 | whereClauses = append(whereClauses, fmt.Sprintf("%s NOT IN (%s)", columnName, strings.Join(placeholders, ", "))) 603 | } 604 | } 605 | } 606 | 607 | // Between conditions 608 | if len(params.Between) > 0 { 609 | for field, values := range params.Between { 610 | columnName := getFieldName(field, "json", "paginate", params.Struct) 611 | if columnName != "" { 612 | whereClauses = append(whereClauses, fmt.Sprintf("%s BETWEEN ? AND ?", columnName)) 613 | args = append(args, values[0], values[1]) 614 | } 615 | } 616 | } 617 | 618 | // IsNull conditions 619 | if len(params.IsNull) > 0 { 620 | for _, field := range params.IsNull { 621 | columnName := getFieldName(field, "json", "paginate", params.Struct) 622 | if columnName != "" { 623 | whereClauses = append(whereClauses, fmt.Sprintf("%s IS NULL", columnName)) 624 | } 625 | } 626 | } 627 | 628 | // IsNotNull conditions 629 | if len(params.IsNotNull) > 0 { 630 | for _, field := range params.IsNotNull { 631 | columnName := getFieldName(field, "json", "paginate", params.Struct) 632 | if columnName != "" { 633 | whereClauses = append(whereClauses, fmt.Sprintf("%s IS NOT NULL", columnName)) 634 | } 635 | } 636 | } 637 | 638 | // Additional WHERE clauses 639 | if len(params.WhereClauses) > 0 { 640 | whereClauses = append(whereClauses, strings.Join(params.WhereClauses, fmt.Sprintf(" %s ", params.WhereCombining))) 641 | args = append(args, params.WhereArgs...) 642 | } 643 | 644 | return whereClauses, args 645 | } 646 | 647 | // buildOrderClause constructs the ORDER BY clause. 648 | func (params *QueryParams) buildOrderClause() string { 649 | if len(params.SortColumns) == 0 || len(params.SortDirections) != len(params.SortColumns) { 650 | return "" 651 | } 652 | 653 | var sortClauses []string 654 | for i, column := range params.SortColumns { 655 | columnName := getFieldName(column, "json", "paginate", params.Struct) 656 | if columnName != "" { 657 | direction := "ASC" 658 | if strings.ToUpper(params.SortDirections[i]) == "DESC" { 659 | direction = "DESC" 660 | } 661 | sortClauses = append(sortClauses, fmt.Sprintf("%s %s", columnName, direction)) 662 | } 663 | } 664 | 665 | if len(sortClauses) > 0 { 666 | return "ORDER BY " + strings.Join(sortClauses, ", ") 667 | } 668 | return "" 669 | } 670 | 671 | // buildLimitOffsetClause constructs the LIMIT and OFFSET clauses. 672 | func (params *QueryParams) buildLimitOffsetClause() (string, []any) { 673 | var clauses []string 674 | var args []any 675 | 676 | clauses = append(clauses, "LIMIT ?") 677 | args = append(args, params.ItemsPerPage) 678 | 679 | if !params.NoOffset { 680 | offset := (params.Page - 1) * params.ItemsPerPage 681 | clauses = append(clauses, "OFFSET ?") 682 | args = append(args, offset) 683 | } 684 | 685 | return strings.Join(clauses, " "), args 686 | } 687 | 688 | // Helper functions 689 | 690 | // replacePlaceholders replaces '?' with positional placeholders like '$1', '$2', etc. 691 | func replacePlaceholders(query string, args []any) (string, []any) { 692 | var newQuery strings.Builder 693 | argIndex := 1 694 | for _, char := range query { 695 | if char == '?' { 696 | newQuery.WriteString(fmt.Sprintf("$%d", argIndex)) 697 | argIndex++ 698 | } else { 699 | newQuery.WriteRune(char) 700 | } 701 | } 702 | return newQuery.String(), args 703 | } 704 | 705 | // getFieldName retrieves the column name from struct tags based on the given key. 706 | func getFieldName(tag, key, keyTarget string, s any) string { 707 | rt := reflect.TypeOf(s) 708 | if rt.Kind() == reflect.Ptr { 709 | rt = rt.Elem() 710 | } 711 | if rt.Kind() != reflect.Struct { 712 | panic("struct type required") 713 | } 714 | for i := 0; i < rt.NumField(); i++ { 715 | field := rt.Field(i) 716 | tagValue := strings.Split(field.Tag.Get(key), ",")[0] 717 | if tagValue == tag { 718 | return field.Tag.Get(keyTarget) 719 | } 720 | } 721 | return "" 722 | } 723 | --------------------------------------------------------------------------------