├── .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 |

Go Paginate - Go package to generate query pagination

3 |

4 | Reference 5 | Release 6 | Software License 7 | GitHub Workflow Status (with event) 8 | Coverage 9 |

10 |

11 | 12 |
13 | 14 | ## Why? 15 | 16 | This project is part of my personal portfolio, so, I'll be happy if you could provide me any feedback about the project, code, structure or anything that you can report that could make me a better developer! 17 | 18 | Email-me: boscardinvinicius@gmail.com 19 | 20 | Connect with me at [LinkedIn](https://www.linkedin.com/in/booscaaa/). 21 | 22 |
23 | 24 | # Paginate Package Readme 25 | 26 | ## Overview 27 | 28 | The `paginate` package provides a flexible and easy-to-use solution for paginated queries in Go. It allows you to construct paginated queries with various options, including sorting, filtering, and custom column selection. 29 | 30 | ## Usage 31 | 32 | To use the `paginate` package, follow these steps: 33 | 34 | 1. **Import the package:** 35 | 36 | ```go 37 | import "github.com/booscaaa/go-paginate/v2/paginate" 38 | ``` 39 | 40 | 2. **Create a struct to represent your database model.** 41 | 42 | Define a struct that mirrors your database model, with struct tags specifying the corresponding database columns. 43 | 44 | ```go 45 | type MyModel struct { 46 | ID int `json:"id" paginate:"my_table.id"` 47 | CreatedAt time.Time `json:"created_at" paginate:"my_table.created_at"` 48 | Name string `json:"name" paginate:"my_table.name"` 49 | } 50 | ``` 51 | 52 | 3. **Use the `PaginQuery` function to create paginated queries:** 53 | 54 | ```go 55 | // Example usage: 56 | params, err := paginate.PaginQuery( 57 | paginate.WithStruct(MyModel{}), 58 | paginate.WithTable("my_table"), 59 | paginate.WithColumn("my_table.*"), 60 | paginate.WithPage(2), 61 | paginate.WithItemsPerPage(10), 62 | paginate.WithSort([]string{"created_at"}, []string{"true"}), 63 | paginate.WithSearch("example"), 64 | ) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | // Generate SQL and arguments 70 | sql, args := paginate.GenerateSQL(params) 71 | countSQL, countArgs := paginate.GenerateCountQuery(params) 72 | ``` 73 | 74 | The above example will output SQL queries and arguments like: 75 | 76 | ```sql 77 | SELECT my_table.* FROM my_table WHERE (my_table.name::TEXT ILIKE $1) ORDER BY created_at DESC LIMIT $2 OFFSET $3 78 | ``` 79 | 80 | SQL Arguments: 81 | 82 | ``` 83 | [%example% 10 10] 84 | ``` 85 | 86 | Count Query: 87 | 88 | ```sql 89 | SELECT COUNT(id) FROM my_table WHERE (my_table.name::TEXT ILIKE $1) 90 | ``` 91 | 92 | Count Arguments: 93 | 94 | ``` 95 | [%example%] 96 | ``` 97 | 98 | 4. **Options and Customization:** 99 | 100 | You can customize your paginated query using various options such as `WithPage`, `WithItemsPerPage`, `WithSort`, `WithSearch`, `WithSearchFields`, `WithVacuum`, `WithColumn`, `WithJoin`, `WithWhereCombining`, and `WithWhereClause`. These options allow you to tailor your query to specific requirements. 101 | 102 | ```go 103 | // Example options: 104 | options := []paginate.Option{ 105 | paginate.WithPage(2), 106 | paginate.WithItemsPerPage(20), 107 | paginate.WithSort([]string{"created_at"}, []string{"true"}), 108 | paginate.WithSearch("example"), 109 | paginate.WithSearchFields([]string{"name"}), 110 | paginate.WithVacuum(true), 111 | paginate.WithColumn("my_table.*"), 112 | paginate.WithJoin("INNER JOIN other_table ON my_table.id = other_table.my_table_id"), 113 | paginate.WithWhereClause("status = ?", "active"), 114 | } 115 | 116 | params, err := paginate.PaginQuery(options...) 117 | ``` 118 | 119 | 5. **Run your query:** 120 | 121 | Once you've configured your paginated query, use the generated SQL and arguments to execute the query against your database. 122 | 123 | ## Options 124 | 125 | ### `WithNoOffset` 126 | 127 | Disable OFFSET and LIMIT for pagination. Useful for scenarios where OFFSET is not performant. 128 | 129 | ### `WithMapArgs` 130 | 131 | Pass a map of custom arguments to be used in the WHERE clause. 132 | 133 | ### `WithStruct` 134 | 135 | Specify the database model struct to be used for generating SQL queries. 136 | 137 | ### `WithTable` 138 | 139 | Specify the main table for the paginated query. 140 | 141 | ### `WithPage` 142 | 143 | Set the page number for pagination. 144 | 145 | ### `WithItemsPerPage` 146 | 147 | Set the number of items per page. 148 | 149 | ### `WithSearch` 150 | 151 | Specify a search term to filter results. 152 | 153 | ### `WithSearchFields` 154 | 155 | Specify fields to search within. 156 | 157 | ### `WithVacuum` 158 | 159 | Enable or disable VACUUM optimization for the query. 160 | 161 | ### `WithColumn` 162 | 163 | Add a custom column to the SELECT clause. 164 | 165 | ### `WithSort` 166 | 167 | Specify sorting columns and directions. 168 | 169 | ### `WithJoin` 170 | 171 | Add a custom JOIN clause. 172 | 173 | ### `WithWhereCombining` 174 | 175 | Specify the combining operator for multiple WHERE clauses. 176 | 177 | ### `WithWhereClause` 178 | 179 | Add a custom WHERE clause. 180 | 181 | ## Example 182 | 183 | Check the provided example in the code for a comprehensive demonstration of the package's usage. 184 | 185 | ```go 186 | // Example usage: 187 | params, err := paginate.PaginQuery( 188 | // ... (options) 189 | ) 190 | ``` 191 | 192 | ## Contribution 193 | 194 | Feel free to contribute to the `paginate` package by creating issues, submitting pull requests, or providing feedback. Your contributions are highly appreciated! 195 | 196 | ## Contributing 197 | 198 | You can send how many PR's do you want, I'll be glad to analyze and accept them! And if you have any question about the project... 199 | 200 | Email-me: boscardinvinicius@gmail.com 201 | 202 | Connect with me at [LinkedIn](https://www.linkedin.com/in/booscaaa/) 203 | 204 | Thank you! 205 | 206 | ## License 207 | 208 | This project is licensed under the MIT License - see the [LICENSE.md](https://github.com/booscaaa/go-paginate/blob/master/LICENSE) file for details 209 | -------------------------------------------------------------------------------- /v3/BIND_README.md: -------------------------------------------------------------------------------- 1 | # Query Parameters Bind 2 | 3 | This functionality allows binding URL query parameters to pagination structs in a simple and efficient way. 4 | 5 | ## Features 6 | 7 | - ✅ Basic parameter binding (page, limit, search, etc.) 8 | - ✅ Array and slice support 9 | - ✅ Complex parameters with array syntax (`likeor[field]`, `eqor[field]`, etc.) 10 | - ✅ Automatic type conversion (int, bool, string) 11 | - ✅ Custom struct support 12 | - ✅ Type validation 13 | - ✅ Default values 14 | 15 | ## Basic Usage 16 | 17 | ### 1. Bind to PaginationParams (default struct) 18 | 19 | ```go 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "log" 25 | "github.com/booscaaa/go-paginate/v3/paginate" 26 | ) 27 | 28 | func main() { 29 | // From a query string 30 | queryString := "page=2&limit=25&search=john&search_fields=name,email" 31 | params, err := paginate.BindQueryStringToStruct(queryString) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | fmt.Printf("Page: %d\n", params.Page) // 2 37 | fmt.Printf("Limit: %d\n", params.Limit) // 25 38 | fmt.Printf("Search: %s\n", params.Search) // "john" 39 | fmt.Printf("Fields: %v\n", params.SearchFields) // ["name", "email"] 40 | } 41 | ``` 42 | 43 | ### 2. Bind using url.Values 44 | 45 | ```go 46 | import ( 47 | "net/url" 48 | "github.com/booscaaa/go-paginate/v3/paginate" 49 | ) 50 | 51 | func handler() { 52 | queryParams := url.Values{ 53 | "page": {"3"}, 54 | "limit": {"50"}, 55 | "search": {"admin"}, 56 | "vacuum": {"true"}, 57 | } 58 | 59 | params, err := paginate.BindQueryParamsToStruct(queryParams) 60 | if err != nil { 61 | log.Fatal(err) 62 | } 63 | 64 | // Use params... 65 | } 66 | ``` 67 | 68 | ## Supported Parameters 69 | 70 | ### Basic Parameters 71 | 72 | | Parameter | Type | Description | Example | 73 | | ----------------- | -------- | --------------------------- | ------------------------------ | 74 | | `page` | int | Page number | `page=2` | 75 | | `limit` | int | Items per page | `limit=25` | 76 | | `items_per_page` | int | Alias for limit | `items_per_page=25` | 77 | | `search` | string | Search term | `search=john` | 78 | | `search_fields` | []string | Fields for search | `search_fields=name,email` | 79 | | `sort_columns` | []string | Columns for sorting | `sort_columns=name,created_at` | 80 | | `sort_directions` | []string | Sort directions | `sort_directions=ASC,DESC` | 81 | | `columns` | []string | Columns for selection | `columns=id,name,email` | 82 | | `vacuum` | bool | Use count estimation | `vacuum=true` | 83 | | `no_offset` | bool | Disable OFFSET | `no_offset=false` | 84 | 85 | ### Complex Parameters (Array Syntax) 86 | 87 | | Parameter | Type | Description | Example | 88 | | ------------------- | ------------------- | ------------------- | ---------------------------------------------------- | 89 | | `likeor[field]` | map[string][]string | OR search by field | `likeor[status]=active&likeor[status]=pending` | 90 | | `likeand[field]` | map[string][]string | AND search by field | `likeand[name]=john` | 91 | | `eqor[field]` | map[string][]any | OR equality | `eqor[age]=25&eqor[age]=30` | 92 | | `eqand[field]` | map[string][]any | AND equality | `eqand[role]=admin` | 93 | | `gte[field]` | map[string]any | Greater or equal | `gte[age]=18` | 94 | | `gt[field]` | map[string]any | Greater than | `gt[score]=80` | 95 | | `lte[field]` | map[string]any | Less or equal | `lte[price]=100.50` | 96 | | `lt[field]` | map[string]any | Less than | `lt[date]=2023-12-31` | 97 | 98 | ## Advanced Examples 99 | 100 | ### 1. Complex Parameters 101 | 102 | ```go 103 | queryString := "page=1&likeor[status]=active&likeor[status]=pending&eqor[age]=25&eqor[age]=30>e[created_at]=2023-01-01" 104 | params, err := paginate.BindQueryStringToStruct(queryString) 105 | 106 | // Result: 107 | // params.LikeOr["status"] = ["active", "pending"] 108 | // params.EqOr["age"] = [25, 30] 109 | // params.Gte["created_at"] = "2023-01-01" 110 | ``` 111 | 112 | ### 2. Custom Struct 113 | 114 | ```go 115 | type CustomParams struct { 116 | Page int `query:"page"` 117 | Limit int `query:"limit"` 118 | Search string `query:"search"` 119 | Filters []string `query:"filters"` 120 | Active bool `query:"active"` 121 | } 122 | 123 | queryParams := url.Values{ 124 | "page": {"4"}, 125 | "limit": {"100"}, 126 | "search": {"custom"}, 127 | "filters": {"filter1,filter2,filter3"}, 128 | "active": {"true"}, 129 | } 130 | 131 | customParams := &CustomParams{} 132 | err := paginate.BindQueryParams(queryParams, customParams) 133 | ``` 134 | 135 | ### 3. Usage in HTTP Handler 136 | 137 | ```go 138 | func usersHandler(w http.ResponseWriter, r *http.Request) { 139 | // Extract query parameters from request 140 | queryParams := r.URL.Query() 141 | 142 | // Bind to pagination struct 143 | paginationParams, err := paginate.BindQueryParamsToStruct(queryParams) 144 | if err != nil { 145 | http.Error(w, "Invalid parameters", http.StatusBadRequest) 146 | return 147 | } 148 | 149 | // Use the parameters to build the query 150 | // ... 151 | } 152 | ``` 153 | 154 | ## Type Conversion 155 | 156 | The library performs automatic type conversion: 157 | 158 | - **Strings**: Used directly 159 | - **Integers**: Converted with `strconv.Atoi()` 160 | - **Booleans**: Converted with `strconv.ParseBool()` 161 | - **Floats**: Converted with `strconv.ParseFloat()` 162 | - **Slices**: Multiple values or comma-separated values 163 | 164 | ## Error Handling 165 | 166 | - Invalid values are ignored (keeps default value) 167 | - Incompatible types are ignored 168 | - Query string parsing errors are returned 169 | - Invalid targets (non-pointer or non-struct) return error 170 | 171 | ## Default Values 172 | 173 | The `PaginationParams` struct has default values: 174 | 175 | ```go 176 | params := &PaginationParams{ 177 | Page: 1, // default page 178 | Limit: 10, // default limit 179 | ItemsPerPage: 10, // default items per page 180 | } 181 | ``` 182 | 183 | ## Run Example 184 | 185 | To see the functionality in action: 186 | 187 | ```bash 188 | go run example_bind.go 189 | ``` 190 | 191 | ## Run Tests 192 | 193 | ```bash 194 | go test -v ./paginate -run TestBind 195 | ``` 196 | -------------------------------------------------------------------------------- /examples/builder/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 represents a user model 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 | Status string `json:"status" paginate:"users.status"` 17 | Salary int `json:"salary" paginate:"users.salary"` 18 | DeptID int `json:"dept_id" paginate:"users.dept_id"` 19 | DeptName string `json:"dept_name" paginate:"users.dept_name"` 20 | CreatedAt string `json:"created_at" paginate:"users.created_at"` 21 | } 22 | 23 | func main() { 24 | fmt.Println("=== Examples of the New Fluent API ===") 25 | fmt.Println() 26 | 27 | // Example 1: Basic usage 28 | fmt.Println("1. Uso Básico:") 29 | basicExample() 30 | fmt.Println() 31 | 32 | // Example 2: Advanced filters 33 | fmt.Println("2. Advanced Filters:") 34 | advancedFiltersExample() 35 | fmt.Println() 36 | 37 | // Example 3: Joins 38 | fmt.Println("3. Joins:") 39 | joinsExample() 40 | fmt.Println() 41 | 42 | // Example 4: From JSON 43 | fmt.Println("4. A partir de JSON:") 44 | fromJSONExample() 45 | fmt.Println() 46 | 47 | // Example 5: Comparison with old API 48 | fmt.Println("5. Comparação com API Antiga:") 49 | comparisonExample() 50 | fmt.Println() 51 | 52 | // Example 6: Combined complex filters 53 | fmt.Println("6. Combined Complex Filters:") 54 | complexFiltersExample() 55 | } 56 | 57 | func basicExample() { 58 | // Nova API fluente - muito mais simples! 59 | sql, args, err := paginate.NewBuilder(). 60 | Table("users"). 61 | Model(&User{}). 62 | Page(2). 63 | Limit(20). 64 | Search("john", "name", "email"). 65 | OrderBy("name"). 66 | OrderByDesc("created_at"). 67 | BuildSQL() 68 | 69 | if err != nil { 70 | log.Printf("Error: %v", err) 71 | return 72 | } 73 | 74 | fmt.Printf("SQL: %s\n", sql) 75 | fmt.Printf("Args: %v\n", args) 76 | } 77 | 78 | func advancedFiltersExample() { 79 | // Advanced filters with the new API 80 | sql, args, err := paginate.NewBuilder(). 81 | Table("users"). 82 | Model(&User{}). 83 | WhereEquals("status", "active"). 84 | WhereIn("dept_id", 1, 2, 3). 85 | WhereGreaterThan("age", 25). 86 | WhereLessThanOrEqual("salary", 100000). 87 | WhereBetween("created_at", "2023-01-01", "2023-12-31"). 88 | SearchOr("name", "John", "Jane"). 89 | SearchAnd("email", "@company.com"). 90 | BuildSQL() 91 | 92 | if err != nil { 93 | log.Printf("Error: %v", err) 94 | return 95 | } 96 | 97 | fmt.Printf("SQL: %s\n", sql) 98 | fmt.Printf("Args: %v\n", args) 99 | } 100 | 101 | func joinsExample() { 102 | // Joins simplificados 103 | sql, args, err := paginate.NewBuilder(). 104 | Table("users u"). 105 | Model(&User{}). 106 | Select("u.id", "u.name", "u.email", "d.name as dept_name"). 107 | LeftJoin("departments d", "u.dept_id = d.id"). 108 | InnerJoin("roles r", "u.role_id = r.id"). 109 | Where("r.name = ?", "admin"). 110 | WhereEquals("u.status", "active"). 111 | OrderBy("u.name"). 112 | BuildSQL() 113 | 114 | if err != nil { 115 | log.Printf("Error: %v", err) 116 | return 117 | } 118 | 119 | fmt.Printf("SQL: %s\n", sql) 120 | fmt.Printf("Args: %v\n", args) 121 | } 122 | 123 | func fromJSONExample() { 124 | // Build from JSON (useful for REST APIs) 125 | jsonQuery := `{ 126 | "page": 1, 127 | "limit": 10, 128 | "eqor": { 129 | "status": ["active", "pending"] 130 | }, 131 | "likeor": { 132 | "name": ["John", "Jane"], 133 | "email": ["@company.com", "@gmail.com"] 134 | }, 135 | "eqand": { 136 | "email": ["@company.com", "@gmail.com"] 137 | }, 138 | "gte": { 139 | "age": 18 140 | }, 141 | "lte": { 142 | "salary": 150000 143 | }, 144 | "sort": ["-name", "created_at"] 145 | }` 146 | 147 | sql, args, err := paginate.NewBuilder(). 148 | Table("users"). 149 | Model(&User{}). 150 | InnerJoin("departments d", "u.dept_id = d.id"). 151 | FromJSON(jsonQuery). 152 | BuildSQL() 153 | 154 | if err != nil { 155 | log.Printf("Error: %v", err) 156 | return 157 | } 158 | 159 | fmt.Printf("JSON Query: %s\n", jsonQuery) 160 | fmt.Printf("SQL: %s\n", sql) 161 | fmt.Printf("Args: %v\n", args) 162 | } 163 | 164 | func comparisonExample() { 165 | fmt.Println("=== API Antiga ===") 166 | // API antiga - mais verbosa 167 | oldParams, err := paginate.NewPaginator( 168 | paginate.WithTable("users"), 169 | paginate.WithStruct(&User{}), 170 | paginate.WithPage(1), 171 | paginate.WithItemsPerPage(10), 172 | paginate.WithSearch("john"), 173 | paginate.WithSearchFields([]string{"name", "email"}), 174 | paginate.WithEqualsOr(map[string][]any{ 175 | "status": {"active", "pending"}, 176 | }), 177 | paginate.WithGte(map[string]any{ 178 | "age": 18, 179 | }), 180 | ) 181 | 182 | if err != nil { 183 | log.Printf("Error: %v", err) 184 | return 185 | } 186 | 187 | oldSQL, oldArgs := oldParams.GenerateSQL() 188 | fmt.Printf("SQL: %s\n", oldSQL) 189 | fmt.Printf("Args: %v\n", oldArgs) 190 | 191 | fmt.Println("\n=== Nova API Fluente ===") 192 | // Nova API - muito mais limpa! 193 | newSQL, newArgs, err := paginate.NewBuilder(). 194 | Table("users"). 195 | Model(&User{}). 196 | Page(1). 197 | Limit(10). 198 | Search("john", "name", "email"). 199 | WhereIn("status", "active", "pending"). 200 | WhereGreaterThanOrEqual("age", 18). 201 | BuildSQL() 202 | 203 | if err != nil { 204 | log.Printf("Error: %v", err) 205 | return 206 | } 207 | 208 | fmt.Printf("SQL: %s\n", newSQL) 209 | fmt.Printf("Args: %v\n", newArgs) 210 | 211 | // Ambas as APIs geram o mesmo resultado! 212 | fmt.Printf("\nResultados idênticos: %v\n", oldSQL == newSQL) 213 | } 214 | 215 | func complexFiltersExample() { 216 | // Example of very complex filters in a simple way 217 | sql, args, err := paginate.NewBuilder(). 218 | Table("users u"). 219 | Model(&User{}). 220 | Select("u.*", "d.name as department_name", "r.name as role_name"). 221 | LeftJoin("departments d", "u.dept_id = d.id"). 222 | LeftJoin("roles r", "u.role_id = r.id"). 223 | // Search in multiple fields 224 | SearchOr("name", "John", "Jane", "Bob"). 225 | SearchAnd("email", "@company.com"). 226 | // Equality filters 227 | WhereIn("u.status", "active", "pending"). 228 | WhereIn("d.type", "engineering", "product"). 229 | // Comparison filters 230 | WhereGreaterThanOrEqual("u.age", 21). 231 | WhereLessThan("u.age", 65). 232 | WhereGreaterThan("u.salary", 50000). 233 | WhereLessThanOrEqual("u.salary", 200000). 234 | // Custom filters 235 | Where("u.created_at >= ?", "2023-01-01"). 236 | Where("u.last_login_at IS NOT NULL"). 237 | // Ordering 238 | OrderBy("d.name"). 239 | OrderBy("u.name"). 240 | OrderByDesc("u.salary"). 241 | // Pagination 242 | Page(1). 243 | Limit(25). 244 | BuildSQL() 245 | 246 | if err != nil { 247 | log.Printf("Error: %v", err) 248 | return 249 | } 250 | 251 | fmt.Printf("SQL Complexo: %s\n", sql) 252 | fmt.Printf("Args: %v\n", args) 253 | 254 | // We can also generate the count query 255 | countSQL, countArgs, err := paginate.NewBuilder(). 256 | Table("users u"). 257 | Model(&User{}). 258 | LeftJoin("departments d", "u.dept_id = d.id"). 259 | LeftJoin("roles r", "u.role_id = r.id"). 260 | SearchOr("name", "John", "Jane", "Bob"). 261 | SearchAnd("email", "@company.com"). 262 | WhereIn("u.status", "active", "pending"). 263 | WhereIn("d.type", "engineering", "product"). 264 | WhereGreaterThanOrEqual("u.age", 21). 265 | WhereLessThan("u.age", 65). 266 | WhereGreaterThan("u.salary", 50000). 267 | WhereLessThanOrEqual("u.salary", 200000). 268 | Where("u.created_at >= ?", "2023-01-01"). 269 | Where("u.last_login_at IS NOT NULL"). 270 | BuildCountSQL() 271 | 272 | if err != nil { 273 | log.Printf("Error in count query: %v", err) 274 | return 275 | } 276 | 277 | fmt.Printf("\nSQL de Contagem: %s\n", countSQL) 278 | fmt.Printf("Args de Contagem: %v\n", countArgs) 279 | } 280 | -------------------------------------------------------------------------------- /v3/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Client provides methods to build query string parameters compatible with go-paginate 11 | type Client struct { 12 | baseURL string 13 | params url.Values 14 | } 15 | 16 | // New creates a new Client instance 17 | func New(baseURL string) *Client { 18 | return &Client{ 19 | baseURL: baseURL, 20 | params: make(url.Values), 21 | } 22 | } 23 | 24 | // NewFromURL creates a new Client instance from an existing URL with query parameters 25 | func NewFromURL(fullURL string) (*Client, error) { 26 | u, err := url.Parse(fullURL) 27 | if err != nil { 28 | return nil, fmt.Errorf("invalid URL: %w", err) 29 | } 30 | 31 | baseURL := fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, u.Path) 32 | client := &Client{ 33 | baseURL: baseURL, 34 | params: u.Query(), 35 | } 36 | 37 | return client, nil 38 | } 39 | 40 | // Reset clears all query parameters 41 | func (c *Client) Reset() *Client { 42 | c.params = make(url.Values) 43 | return c 44 | } 45 | 46 | // Clone creates a copy of the current client 47 | func (c *Client) Clone() *Client { 48 | newParams := make(url.Values) 49 | for k, v := range c.params { 50 | newParams[k] = append([]string(nil), v...) 51 | } 52 | 53 | return &Client{ 54 | baseURL: c.baseURL, 55 | params: newParams, 56 | } 57 | } 58 | 59 | // Page sets the page number 60 | func (c *Client) Page(page int) *Client { 61 | if page > 0 { 62 | c.params.Set("page", strconv.Itoa(page)) 63 | } 64 | return c 65 | } 66 | 67 | // Limit sets the number of items per page 68 | func (c *Client) Limit(limit int) *Client { 69 | if limit > 0 { 70 | c.params.Set("limit", strconv.Itoa(limit)) 71 | } 72 | return c 73 | } 74 | 75 | // ItemsPerPage is an alias for Limit 76 | func (c *Client) ItemsPerPage(itemsPerPage int) *Client { 77 | return c.Limit(itemsPerPage) 78 | } 79 | 80 | // Search sets the search term 81 | func (c *Client) Search(search string) *Client { 82 | if search != "" { 83 | c.params.Set("search", search) 84 | } 85 | return c 86 | } 87 | 88 | // SearchFields sets the fields to search in 89 | func (c *Client) SearchFields(fields ...string) *Client { 90 | if len(fields) > 0 { 91 | c.params.Set("search_fields", strings.Join(fields, ",")) 92 | } 93 | return c 94 | } 95 | 96 | // Sort sets sorting parameters. Use "-" prefix for descending order 97 | // Example: Sort("name", "-created_at") 98 | func (c *Client) Sort(fields ...string) *Client { 99 | if len(fields) > 0 { 100 | c.params.Del("sort") 101 | for _, field := range fields { 102 | c.params.Add("sort", field) 103 | } 104 | } 105 | return c 106 | } 107 | 108 | // SortColumns sets the columns to sort by 109 | func (c *Client) SortColumns(columns ...string) *Client { 110 | if len(columns) > 0 { 111 | c.params.Set("sort_columns", strings.Join(columns, ",")) 112 | } 113 | return c 114 | } 115 | 116 | // SortDirections sets the sort directions (asc/desc) 117 | func (c *Client) SortDirections(directions ...string) *Client { 118 | if len(directions) > 0 { 119 | c.params.Set("sort_directions", strings.Join(directions, ",")) 120 | } 121 | return c 122 | } 123 | 124 | // Columns sets the columns to select 125 | func (c *Client) Columns(columns ...string) *Client { 126 | if len(columns) > 0 { 127 | c.params.Set("columns", strings.Join(columns, ",")) 128 | } 129 | return c 130 | } 131 | 132 | // Vacuum enables vacuum mode 133 | func (c *Client) Vacuum(enable bool) *Client { 134 | c.params.Set("vacuum", strconv.FormatBool(enable)) 135 | return c 136 | } 137 | 138 | // NoOffset enables no offset mode 139 | func (c *Client) NoOffset(enable bool) *Client { 140 | c.params.Set("no_offset", strconv.FormatBool(enable)) 141 | return c 142 | } 143 | 144 | // Like adds LIKE filter for a field 145 | func (c *Client) Like(field string, values ...string) *Client { 146 | for _, value := range values { 147 | c.params.Add(fmt.Sprintf("like[%s]", field), value) 148 | } 149 | return c 150 | } 151 | 152 | // LikeOr adds LIKE OR filter for a field 153 | func (c *Client) LikeOr(field string, values ...string) *Client { 154 | for _, value := range values { 155 | c.params.Add(fmt.Sprintf("likeor[%s]", field), value) 156 | } 157 | return c 158 | } 159 | 160 | // LikeAnd adds LIKE AND filter for a field 161 | func (c *Client) LikeAnd(field string, values ...string) *Client { 162 | for _, value := range values { 163 | c.params.Add(fmt.Sprintf("likeand[%s]", field), value) 164 | } 165 | return c 166 | } 167 | 168 | // Eq adds equality filter for a field 169 | func (c *Client) Eq(field string, values ...any) *Client { 170 | for _, value := range values { 171 | c.params.Add(fmt.Sprintf("eq[%s]", field), fmt.Sprintf("%v", value)) 172 | } 173 | return c 174 | } 175 | 176 | // EqOr adds equality OR filter for a field 177 | func (c *Client) EqOr(field string, values ...any) *Client { 178 | for _, value := range values { 179 | c.params.Add(fmt.Sprintf("eqor[%s]", field), fmt.Sprintf("%v", value)) 180 | } 181 | return c 182 | } 183 | 184 | // EqAnd adds equality AND filter for a field 185 | func (c *Client) EqAnd(field string, values ...any) *Client { 186 | for _, value := range values { 187 | c.params.Add(fmt.Sprintf("eqand[%s]", field), fmt.Sprintf("%v", value)) 188 | } 189 | return c 190 | } 191 | 192 | // Gte adds greater than or equal filter 193 | func (c *Client) Gte(field string, value any) *Client { 194 | c.params.Set(fmt.Sprintf("gte[%s]", field), fmt.Sprintf("%v", value)) 195 | return c 196 | } 197 | 198 | // Gt adds greater than filter 199 | func (c *Client) Gt(field string, value any) *Client { 200 | c.params.Set(fmt.Sprintf("gt[%s]", field), fmt.Sprintf("%v", value)) 201 | return c 202 | } 203 | 204 | // Lte adds less than or equal filter 205 | func (c *Client) Lte(field string, value any) *Client { 206 | c.params.Set(fmt.Sprintf("lte[%s]", field), fmt.Sprintf("%v", value)) 207 | return c 208 | } 209 | 210 | // Lt adds less than filter 211 | func (c *Client) Lt(field string, value any) *Client { 212 | c.params.Set(fmt.Sprintf("lt[%s]", field), fmt.Sprintf("%v", value)) 213 | return c 214 | } 215 | 216 | // In adds IN filter for a field 217 | func (c *Client) In(field string, values ...any) *Client { 218 | for _, value := range values { 219 | c.params.Add(fmt.Sprintf("in[%s]", field), fmt.Sprintf("%v", value)) 220 | } 221 | return c 222 | } 223 | 224 | // NotIn adds NOT IN filter for a field 225 | func (c *Client) NotIn(field string, values ...any) *Client { 226 | for _, value := range values { 227 | c.params.Add(fmt.Sprintf("notin[%s]", field), fmt.Sprintf("%v", value)) 228 | } 229 | return c 230 | } 231 | 232 | // Between adds BETWEEN filter for a field 233 | func (c *Client) Between(field string, min, max any) *Client { 234 | c.params.Set(fmt.Sprintf("between[%s][0]", field), fmt.Sprintf("%v", min)) 235 | c.params.Set(fmt.Sprintf("between[%s][1]", field), fmt.Sprintf("%v", max)) 236 | return c 237 | } 238 | 239 | // IsNull adds IS NULL filter for a field 240 | func (c *Client) IsNull(fields ...string) *Client { 241 | for _, field := range fields { 242 | c.params.Add("isnull", field) 243 | } 244 | return c 245 | } 246 | 247 | // IsNotNull adds IS NOT NULL filter for a field 248 | func (c *Client) IsNotNull(fields ...string) *Client { 249 | for _, field := range fields { 250 | c.params.Add("isnotnull", field) 251 | } 252 | return c 253 | } 254 | 255 | // BuildURL builds the complete URL with query parameters 256 | func (c *Client) BuildURL() string { 257 | if len(c.params) == 0 { 258 | return c.baseURL 259 | } 260 | return fmt.Sprintf("%s?%s", c.baseURL, c.params.Encode()) 261 | } 262 | 263 | // BuildQueryString builds only the query string part 264 | func (c *Client) BuildQueryString() string { 265 | return c.params.Encode() 266 | } 267 | 268 | // GetParams returns a copy of the current query parameters 269 | func (c *Client) GetParams() url.Values { 270 | params := make(url.Values) 271 | for k, v := range c.params { 272 | params[k] = append([]string(nil), v...) 273 | } 274 | return params 275 | } 276 | 277 | // SetCustomParam sets a custom query parameter 278 | func (c *Client) SetCustomParam(key, value string) *Client { 279 | c.params.Set(key, value) 280 | return c 281 | } 282 | 283 | // AddCustomParam adds a custom query parameter (allows multiple values) 284 | func (c *Client) AddCustomParam(key, value string) *Client { 285 | c.params.Add(key, value) 286 | return c 287 | } 288 | 289 | // RemoveParam removes a query parameter 290 | func (c *Client) RemoveParam(key string) *Client { 291 | c.params.Del(key) 292 | return c 293 | } -------------------------------------------------------------------------------- /v3/CLIENT_README.md: -------------------------------------------------------------------------------- 1 | # Go Paginate v3 Client 2 | 3 | O **Go Paginate Client** é uma biblioteca cliente que permite a outras aplicações Go gerarem facilmente query string parameters compatíveis com a biblioteca go-paginate. Ele fornece uma API fluente e type-safe para construir URLs com parâmetros de paginação, busca e filtros complexos. 4 | 5 | ## 🚀 Instalação 6 | 7 | ```bash 8 | go get github.com/booscaaa/go-paginate/v3 9 | ``` 10 | 11 | ## 📖 Uso Básico 12 | 13 | ```go 14 | package main 15 | 16 | import ( 17 | "fmt" 18 | "github.com/booscaaa/go-paginate/v3/client" 19 | ) 20 | 21 | func main() { 22 | // Criar um novo cliente 23 | c := client.New("https://api.example.com/users") 24 | 25 | // Construir URL com paginação básica 26 | url := c.Page(2).Limit(25).BuildURL() 27 | fmt.Println(url) // https://api.example.com/users?limit=25&page=2 28 | } 29 | ``` 30 | 31 | ## 🔧 Funcionalidades 32 | 33 | ### Paginação Básica 34 | 35 | ```go 36 | client := client.New("https://api.example.com/users") 37 | url := client. 38 | Page(2). // Página 2 39 | Limit(25). // 25 itens por página 40 | BuildURL() 41 | ``` 42 | 43 | ### Busca e Campos de Busca 44 | 45 | ```go 46 | url := client. 47 | Search("john"). // Termo de busca 48 | SearchFields("name", "email", "username"). // Campos para buscar 49 | BuildURL() 50 | ``` 51 | 52 | ### Ordenação 53 | 54 | ```go 55 | url := client. 56 | Sort("name", "-created_at"). // Ordenar por nome (asc) e data de criação (desc) 57 | BuildURL() 58 | 59 | // Ou usando métodos separados 60 | url := client. 61 | SortColumns("name", "created_at"). 62 | SortDirections("asc", "desc"). 63 | BuildURL() 64 | ``` 65 | 66 | ### Seleção de Colunas 67 | 68 | ```go 69 | url := client. 70 | Columns("id", "name", "email", "created_at"). // Selecionar apenas essas colunas 71 | BuildURL() 72 | ``` 73 | 74 | ## 🎯 Filtros Avançados 75 | 76 | ### Filtros LIKE 77 | 78 | ```go 79 | url := client. 80 | Like("name", "john", "jane"). // name LIKE 'john' OR name LIKE 'jane' 81 | LikeOr("status", "active", "pending"). // status LIKE 'active' OR status LIKE 'pending' 82 | LikeAnd("description", "go", "lang"). // description LIKE 'go' AND description LIKE 'lang' 83 | BuildURL() 84 | ``` 85 | 86 | ### Filtros de Igualdade 87 | 88 | ```go 89 | url := client. 90 | Eq("department", "IT"). // department = 'IT' 91 | EqOr("age", 25, 30, 35). // age = 25 OR age = 30 OR age = 35 92 | EqAnd("status", "active", "verified"). // status = 'active' AND status = 'verified' 93 | BuildURL() 94 | ``` 95 | 96 | ### Filtros de Comparação 97 | 98 | ```go 99 | url := client. 100 | Gte("age", 18). // age >= 18 101 | Gt("score", 80). // score > 80 102 | Lte("salary", 100000). // salary <= 100000 103 | Lt("experience", 5). // experience < 5 104 | BuildURL() 105 | ``` 106 | 107 | ### Filtros IN e NOT IN 108 | 109 | ```go 110 | url := client. 111 | In("role", "admin", "manager", "user"). // role IN ('admin', 'manager', 'user') 112 | NotIn("status", "deleted", "banned"). // status NOT IN ('deleted', 'banned') 113 | BuildURL() 114 | ``` 115 | 116 | ### Filtro BETWEEN 117 | 118 | ```go 119 | url := client. 120 | Between("salary", 50000, 150000). // salary BETWEEN 50000 AND 150000 121 | Between("age", 25, 65). // age BETWEEN 25 AND 65 122 | BuildURL() 123 | ``` 124 | 125 | ### Filtros NULL 126 | 127 | ```go 128 | url := client. 129 | IsNull("deleted_at"). // deleted_at IS NULL 130 | IsNotNull("email", "phone"). // email IS NOT NULL AND phone IS NOT NULL 131 | BuildURL() 132 | ``` 133 | 134 | ## 🔄 Funcionalidades Avançadas 135 | 136 | ### Clonagem de Cliente 137 | 138 | ```go 139 | // Criar um cliente base com parâmetros comuns 140 | baseClient := client.New("https://api.example.com/users") 141 | baseClient.Limit(25).Columns("id", "name", "email") 142 | 143 | // Clonar para diferentes casos de uso 144 | activeUsers := baseClient.Clone().Eq("status", "active") 145 | inactiveUsers := baseClient.Clone().Eq("status", "inactive") 146 | 147 | activeURL := activeUsers.BuildURL() 148 | inactiveURL := inactiveUsers.BuildURL() 149 | ``` 150 | 151 | ### Construção a partir de URL Existente 152 | 153 | ```go 154 | // Partir de uma URL existente 155 | existingURL := "https://api.example.com/users?page=1&limit=10" 156 | client, err := client.NewFromURL(existingURL) 157 | if err != nil { 158 | log.Fatal(err) 159 | } 160 | 161 | // Adicionar mais parâmetros 162 | newURL := client.Page(2).Eq("status", "active").BuildURL() 163 | ``` 164 | 165 | ### Reset e Manipulação de Parâmetros 166 | 167 | ```go 168 | client := client.New("https://api.example.com/users") 169 | client.Page(1).Limit(10).Search("test") 170 | 171 | // Limpar todos os parâmetros 172 | client.Reset() 173 | 174 | // Remover parâmetro específico 175 | client.RemoveParam("search") 176 | 177 | // Adicionar parâmetros customizados 178 | client.SetCustomParam("custom", "value") 179 | client.AddCustomParam("multi", "value1") 180 | client.AddCustomParam("multi", "value2") 181 | ``` 182 | 183 | ### Obter Apenas Query String 184 | 185 | ```go 186 | client := client.New("") // URL base vazia 187 | queryString := client. 188 | Page(2). 189 | Limit(50). 190 | Search("golang"). 191 | BuildQueryString() 192 | 193 | // Usar com diferentes URLs base 194 | url1 := fmt.Sprintf("https://api1.com/posts?%s", queryString) 195 | url2 := fmt.Sprintf("https://api2.com/articles?%s", queryString) 196 | ``` 197 | 198 | ## 🌐 Integração com HTTP Clients 199 | 200 | ### Com net/http 201 | 202 | ```go 203 | import ( 204 | "net/http" 205 | "github.com/booscaaa/go-paginate/v3/client" 206 | ) 207 | 208 | func fetchUsers() { 209 | // Construir URL 210 | paginateClient := client.New("https://api.example.com/users") 211 | url := paginateClient. 212 | Page(1). 213 | Limit(10). 214 | Eq("status", "active"). 215 | BuildURL() 216 | 217 | // Fazer requisição HTTP 218 | resp, err := http.Get(url) 219 | if err != nil { 220 | log.Fatal(err) 221 | } 222 | defer resp.Body.Close() 223 | 224 | // Processar resposta... 225 | } 226 | ``` 227 | 228 | ### Com outros HTTP clients 229 | 230 | ```go 231 | // Obter parâmetros como url.Values 232 | client := client.New("https://api.example.com/users") 233 | params := client.Page(1).Limit(10).GetParams() 234 | 235 | // Usar com qualquer biblioteca HTTP que aceite url.Values 236 | // req.URL.RawQuery = params.Encode() 237 | ``` 238 | 239 | ## 🎛️ Opções Especiais 240 | 241 | ```go 242 | url := client. 243 | Vacuum(true). // Habilitar modo vacuum 244 | NoOffset(true). // Habilitar modo no offset 245 | BuildURL() 246 | ``` 247 | 248 | ## 📝 Exemplo Completo 249 | 250 | ```go 251 | package main 252 | 253 | import ( 254 | "fmt" 255 | "log" 256 | "net/http" 257 | "github.com/booscaaa/go-paginate/v3/client" 258 | ) 259 | 260 | func main() { 261 | // Criar cliente com filtros complexos 262 | c := client.New("https://api.example.com/users") 263 | 264 | url := c. 265 | Page(2). 266 | Limit(25). 267 | Search("john"). 268 | SearchFields("name", "email"). 269 | Sort("name", "-created_at"). 270 | LikeOr("status", "active", "pending"). 271 | EqOr("age", 25, 30, 35). 272 | Gte("created_at", "2023-01-01"). 273 | Lt("score", 100). 274 | In("department", "IT", "HR"). 275 | IsNotNull("email"). 276 | Vacuum(true). 277 | BuildURL() 278 | 279 | fmt.Println("Generated URL:", url) 280 | 281 | // Fazer requisição HTTP 282 | resp, err := http.Get(url) 283 | if err != nil { 284 | log.Fatal(err) 285 | } 286 | defer resp.Body.Close() 287 | 288 | fmt.Println("Response Status:", resp.Status) 289 | } 290 | ``` 291 | 292 | ## 🧪 Testes 293 | 294 | Para executar os testes do cliente: 295 | 296 | ```bash 297 | cd v3/client 298 | go test -v 299 | ``` 300 | 301 | ## 📚 Compatibilidade 302 | 303 | O cliente gera query strings totalmente compatíveis com: 304 | - go-paginate v3 `BindQueryParams` 305 | - go-paginate v3 `BindQueryStringToStruct` 306 | - Todos os filtros e operadores suportados pela biblioteca principal 307 | 308 | ## 🔗 Links Úteis 309 | 310 | - [Documentação Principal](../README.md) 311 | - [Exemplos de Bind](../BIND_README.md) 312 | - [Exemplos de Filtros](../FILTER_README.md) 313 | - [Exemplo de Uso do Cliente](../examples/client/main.go) -------------------------------------------------------------------------------- /v3/paginate/new_operators_bind_test.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | // TestUser struct for testing new operators binding 8 | type TestUserBind struct { 9 | ID int `json:"id"` 10 | Name string `json:"name"` 11 | Email string `json:"email"` 12 | Age int `json:"age"` 13 | Active bool `json:"active"` 14 | Category string `json:"category"` 15 | } 16 | 17 | // TestNewOperatorsQueryStringBind tests binding new operators from query string 18 | func TestNewOperatorsQueryStringBind(t *testing.T) { 19 | tests := []struct { 20 | name string 21 | queryString string 22 | validation func(*PaginationParams) bool 23 | }{ 24 | { 25 | name: "Like operator from query string", 26 | queryString: "like[name]=john&like[email]=@example.com", 27 | validation: func(p *PaginationParams) bool { 28 | return len(p.Like) == 2 && 29 | len(p.Like["name"]) == 1 && p.Like["name"][0] == "john" && 30 | len(p.Like["email"]) == 1 && p.Like["email"][0] == "@example.com" 31 | }, 32 | }, 33 | { 34 | name: "Eq operator from query string", 35 | queryString: "eq[age]=25&eq[active]=true", 36 | validation: func(p *PaginationParams) bool { 37 | return len(p.Eq) == 2 && 38 | len(p.Eq["age"]) == 1 && p.Eq["age"][0] == 25 && 39 | len(p.Eq["active"]) == 1 && p.Eq["active"][0] == true 40 | }, 41 | }, 42 | { 43 | name: "In operator from query string", 44 | queryString: "in[category]=admin&in[category]=user&in[age]=25&in[age]=30", 45 | validation: func(p *PaginationParams) bool { 46 | return len(p.In) == 2 && 47 | len(p.In["category"]) == 2 && 48 | len(p.In["age"]) == 2 49 | }, 50 | }, 51 | { 52 | name: "NotIn operator from query string", 53 | queryString: "notin[category]=banned¬in[age]=0", 54 | validation: func(p *PaginationParams) bool { 55 | return len(p.NotIn) == 2 && 56 | len(p.NotIn["category"]) == 1 && p.NotIn["category"][0] == "banned" && 57 | len(p.NotIn["age"]) == 1 && p.NotIn["age"][0] == 0 58 | }, 59 | }, 60 | { 61 | name: "Between operator from query string", 62 | queryString: "between[age]=18&between[age]=65", 63 | validation: func(p *PaginationParams) bool { 64 | return len(p.Between) == 1 && 65 | p.Between["age"][0] == 18 && p.Between["age"][1] == 65 66 | }, 67 | }, 68 | { 69 | name: "IsNull operator from query string", 70 | queryString: "isnull=deleted_at&isnull=archived_at", 71 | validation: func(p *PaginationParams) bool { 72 | return len(p.IsNull) == 2 && 73 | p.IsNull[0] == "deleted_at" && p.IsNull[1] == "archived_at" 74 | }, 75 | }, 76 | { 77 | name: "IsNotNull operator from query string", 78 | queryString: "isnotnull=email&isnotnull=phone", 79 | validation: func(p *PaginationParams) bool { 80 | return len(p.IsNotNull) == 2 && 81 | p.IsNotNull[0] == "email" && p.IsNotNull[1] == "phone" 82 | }, 83 | }, 84 | } 85 | 86 | for _, tt := range tests { 87 | t.Run(tt.name, func(t *testing.T) { 88 | params, err := BindQueryStringToStruct(tt.queryString) 89 | if err != nil { 90 | t.Fatalf("Failed to bind query string: %v", err) 91 | } 92 | 93 | if !tt.validation(params) { 94 | t.Errorf("Validation failed for %s", tt.name) 95 | } 96 | }) 97 | } 98 | } 99 | 100 | // TestNewOperatorsFromJSON tests binding new operators from JSON 101 | func TestNewOperatorsFromJSON(t *testing.T) { 102 | tests := []struct { 103 | name string 104 | jsonData string 105 | validation func(*PaginatorBuilder) bool 106 | }{ 107 | { 108 | name: "All new operators from JSON", 109 | jsonData: `{ 110 | "like": {"name": ["john"], "email": ["@example.com"]}, 111 | "eq": {"age": [25], "active": [true]}, 112 | "in": {"category": ["admin", "user"]}, 113 | "notin": {"status": ["banned", "deleted"]}, 114 | "between": {"age": [18, 65]}, 115 | "isnull": ["deleted_at"], 116 | "isnotnull": ["email"] 117 | }`, 118 | validation: func(b *PaginatorBuilder) bool { 119 | params := b.params 120 | return len(params.Like) > 0 && 121 | len(params.Eq) > 0 && 122 | len(params.In) > 0 && 123 | len(params.NotIn) > 0 && 124 | len(params.Between) > 0 && 125 | len(params.IsNull) > 0 && 126 | len(params.IsNotNull) > 0 127 | }, 128 | }, 129 | } 130 | 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | builder := NewBuilder(). 134 | Model(TestUserBind{}). 135 | Table("users"). 136 | FromJSON(tt.jsonData) 137 | 138 | if builder.err != nil { 139 | t.Fatalf("Failed to bind from JSON: %v", builder.err) 140 | } 141 | 142 | if !tt.validation(builder) { 143 | t.Errorf("Validation failed for %s", tt.name) 144 | } 145 | }) 146 | } 147 | } 148 | 149 | // TestNewOperatorsFromStruct tests binding new operators from struct 150 | func TestNewOperatorsFromStruct(t *testing.T) { 151 | type FilterStruct struct { 152 | Like map[string][]string `json:"like"` 153 | Eq map[string][]any `json:"eq"` 154 | In map[string][]any `json:"in"` 155 | NotIn map[string][]any `json:"notin"` 156 | Between map[string][2]any `json:"between"` 157 | IsNull []string `json:"isnull"` 158 | IsNotNull []string `json:"isnotnull"` 159 | } 160 | 161 | filterData := FilterStruct{ 162 | Like: map[string][]string{ 163 | "name": {"john"}, 164 | "email": {"@example.com"}, 165 | }, 166 | Eq: map[string][]any{ 167 | "age": {25}, 168 | "active": {true}, 169 | }, 170 | In: map[string][]any{ 171 | "category": {"admin", "user"}, 172 | }, 173 | NotIn: map[string][]any{ 174 | "status": {"banned", "deleted"}, 175 | }, 176 | Between: map[string][2]any{ 177 | "age": {18, 65}, 178 | }, 179 | IsNull: []string{"deleted_at"}, 180 | IsNotNull: []string{"email"}, 181 | } 182 | 183 | builder := NewBuilder(). 184 | Model(TestUserBind{}). 185 | Table("users"). 186 | FromStruct(filterData) 187 | 188 | if builder.err != nil { 189 | t.Fatalf("Failed to bind from struct: %v", builder.err) 190 | } 191 | 192 | params := builder.params 193 | if len(params.Like) == 0 || len(params.Eq) == 0 || len(params.In) == 0 || 194 | len(params.NotIn) == 0 || len(params.Between) == 0 || 195 | len(params.IsNull) == 0 || len(params.IsNotNull) == 0 { 196 | t.Error("Not all operators were bound from struct") 197 | } 198 | } 199 | 200 | // TestNewOperatorsFromMap tests binding new operators from map 201 | func TestNewOperatorsFromMap(t *testing.T) { 202 | filterMap := map[string]any{ 203 | "like": map[string]any{ 204 | "name": []string{"john"}, 205 | "email": []string{"@example.com"}, 206 | }, 207 | "eq": map[string]any{ 208 | "age": []any{25}, 209 | "active": []any{true}, 210 | }, 211 | "in": map[string]any{ 212 | "category": []any{"admin", "user"}, 213 | }, 214 | "notin": map[string]any{ 215 | "status": []any{"banned", "deleted"}, 216 | }, 217 | "between": map[string]any{ 218 | "age": []any{18, 65}, 219 | }, 220 | "isnull": []string{"deleted_at"}, 221 | "isnotnull": []string{"email"}, 222 | } 223 | 224 | builder := NewBuilder(). 225 | Model(TestUserBind{}). 226 | Table("users"). 227 | FromMap(filterMap) 228 | 229 | if builder.err != nil { 230 | t.Fatalf("Failed to bind from map: %v", builder.err) 231 | } 232 | 233 | params := builder.params 234 | if len(params.Like) == 0 || len(params.Eq) == 0 || len(params.In) == 0 || 235 | len(params.NotIn) == 0 || len(params.Between) == 0 || 236 | len(params.IsNull) == 0 || len(params.IsNotNull) == 0 { 237 | t.Error("Not all operators were bound from map") 238 | } 239 | } 240 | 241 | // TestNewOperatorsQueryGeneration tests that new operators generate correct SQL 242 | func TestNewOperatorsQueryGeneration(t *testing.T) { 243 | builder := NewBuilder(). 244 | Model(TestUserBind{}). 245 | Table("users"). 246 | WhereLike("name", "john"). 247 | WhereEquals("age", 25). 248 | WhereIn("category", "admin", "user"). 249 | WhereNotIn("status", "banned"). 250 | WhereBetween("age", 18, 65). 251 | WhereIsNull("deleted_at"). 252 | WhereIsNotNull("email") 253 | 254 | sql, args, err := builder.BuildSQL() 255 | if err != nil { 256 | t.Fatalf("Failed to build SQL: %v", err) 257 | } 258 | 259 | if sql == "" { 260 | t.Error("Generated SQL is empty") 261 | } 262 | 263 | if len(args) == 0 { 264 | t.Error("No arguments generated") 265 | } 266 | 267 | // Verify that the query contains WHERE clause (basic validation) 268 | if len(sql) == 0 { 269 | t.Error("Generated SQL should not be empty") 270 | } 271 | 272 | // Basic check that it's a SELECT query with WHERE 273 | if sql[:6] != "SELECT" { 274 | t.Error("Generated SQL should start with SELECT") 275 | } 276 | 277 | t.Logf("Generated SQL: %s", sql) 278 | t.Logf("Generated Args: %v", args) 279 | } 280 | -------------------------------------------------------------------------------- /v3/paginate/bind_test.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | ) 7 | 8 | func TestBindQueryParamsToStruct(t *testing.T) { 9 | // Test basic pagination parameters 10 | queryParams := url.Values{ 11 | "page": {"2"}, 12 | "limit": {"25"}, 13 | "search": {"john"}, 14 | "search_fields": {"name,email"}, 15 | "sort_columns": {"name,created_at"}, 16 | "sort_directions": {"ASC,DESC"}, 17 | "vacuum": {"true"}, 18 | "no_offset": {"false"}, 19 | } 20 | 21 | params, err := BindQueryParamsToStruct(queryParams) 22 | if err != nil { 23 | t.Fatalf("Unexpected error: %v", err) 24 | } 25 | 26 | // Check if parameters were set correctly 27 | if params.Page != 2 { 28 | t.Errorf("Expected page = 2, got %d", params.Page) 29 | } 30 | 31 | if params.Limit != 25 { 32 | t.Errorf("Expected limit = 25, got %d", params.Limit) 33 | } 34 | 35 | if params.Search != "john" { 36 | t.Errorf("Expected search = 'john', got '%s'", params.Search) 37 | } 38 | 39 | if len(params.SearchFields) != 2 { 40 | t.Errorf("Expected 2 search fields, got %d", len(params.SearchFields)) 41 | } 42 | 43 | if params.SearchFields[0] != "name" || params.SearchFields[1] != "email" { 44 | t.Errorf("Expected search fields ['name', 'email'], got %v", params.SearchFields) 45 | } 46 | 47 | if len(params.SortColumns) != 2 { 48 | t.Errorf("Expected 2 sort columns, got %d", len(params.SortColumns)) 49 | } 50 | 51 | if params.SortColumns[0] != "name" || params.SortColumns[1] != "created_at" { 52 | t.Errorf("Expected sort columns ['name', 'created_at'], got %v", params.SortColumns) 53 | } 54 | 55 | if !params.Vacuum { 56 | t.Error("Expected vacuum = true") 57 | } 58 | 59 | if params.NoOffset { 60 | t.Error("Expected no_offset = false") 61 | } 62 | } 63 | 64 | func TestBindQueryParamsWithNestedParameters(t *testing.T) { 65 | // Test nested parameters like likeor[field], eqor[field], etc. 66 | queryParams := url.Values{ 67 | "page": {"1"}, 68 | "limit": {"10"}, 69 | "likeor[status]": {"active", "pending"}, 70 | "likeand[name]": {"john"}, 71 | "eqor[age]": {"25", "30"}, 72 | "eqand[role]": {"admin"}, 73 | "gte[created_at]": {"2023-01-01"}, 74 | "gt[score]": {"80"}, 75 | "lte[updated_at]": {"2023-12-31"}, 76 | "lt[price]": {"100.50"}, 77 | } 78 | 79 | params, err := BindQueryParamsToStruct(queryParams) 80 | if err != nil { 81 | t.Fatalf("Unexpected error: %v", err) 82 | } 83 | 84 | // Check likeor parameters 85 | if len(params.LikeOr["status"]) != 2 { 86 | t.Errorf("Expected 2 likeor values for status, got %d", len(params.LikeOr["status"])) 87 | } 88 | if params.LikeOr["status"][0] != "active" || params.LikeOr["status"][1] != "pending" { 89 | t.Errorf("Expected likeor status ['active', 'pending'], got %v", params.LikeOr["status"]) 90 | } 91 | 92 | // Check likeand parameters 93 | if len(params.LikeAnd["name"]) != 1 { 94 | t.Errorf("Expected 1 likeand value for name, got %d", len(params.LikeAnd["name"])) 95 | } 96 | if params.LikeAnd["name"][0] != "john" { 97 | t.Errorf("Expected likeand name 'john', got '%s'", params.LikeAnd["name"][0]) 98 | } 99 | 100 | // Check eqor parameters 101 | if len(params.EqOr["age"]) != 2 { 102 | t.Errorf("Expected 2 eqor values for age, got %d", len(params.EqOr["age"])) 103 | } 104 | 105 | // Check comparison operators 106 | if params.Gte["created_at"] != "2023-01-01" { 107 | t.Errorf("Expected gte created_at '2023-01-01', got %v", params.Gte["created_at"]) 108 | } 109 | 110 | if params.Gt["score"] != 80 { 111 | t.Errorf("Expected gt score 80, got %v", params.Gt["score"]) 112 | } 113 | 114 | if params.Lte["updated_at"] != "2023-12-31" { 115 | t.Errorf("Expected lte updated_at '2023-12-31', got %v", params.Lte["updated_at"]) 116 | } 117 | 118 | if params.Lt["price"] != 100.5 { 119 | t.Errorf("Expected lt price 100.5, got %v", params.Lt["price"]) 120 | } 121 | } 122 | 123 | func TestBindQueryStringToStruct(t *testing.T) { 124 | // Test parsing from raw query string 125 | queryString := "page=3&limit=50&search=test&search_fields=name,email&likeor[status]=active&likeor[status]=pending>e[age]=18" 126 | 127 | params, err := BindQueryStringToStruct(queryString) 128 | if err != nil { 129 | t.Fatalf("Unexpected error: %v", err) 130 | } 131 | 132 | // Check if parameters were set correctly 133 | if params.Page != 3 { 134 | t.Errorf("Expected page = 3, got %d", params.Page) 135 | } 136 | 137 | if params.Limit != 50 { 138 | t.Errorf("Expected limit = 50, got %d", params.Limit) 139 | } 140 | 141 | if params.Search != "test" { 142 | t.Errorf("Expected search = 'test', got '%s'", params.Search) 143 | } 144 | 145 | if len(params.LikeOr["status"]) != 2 { 146 | t.Errorf("Expected 2 likeor values for status, got %d", len(params.LikeOr["status"])) 147 | } 148 | 149 | if params.Gte["age"] != 18 { 150 | t.Errorf("Expected gte age 18, got %v", params.Gte["age"]) 151 | } 152 | } 153 | 154 | func TestBindQueryParamsWithInvalidValues(t *testing.T) { 155 | // Test with invalid values 156 | queryParams := url.Values{ 157 | "page": {"invalid"}, 158 | "limit": {"abc"}, 159 | "vacuum": {"not_a_bool"}, 160 | } 161 | 162 | params, err := BindQueryParamsToStruct(queryParams) 163 | if err != nil { 164 | t.Fatalf("Unexpected error: %v", err) 165 | } 166 | 167 | // Invalid values should be ignored, defaults should remain 168 | if params.Page != 1 { 169 | t.Errorf("Expected page to remain default (1), got %d", params.Page) 170 | } 171 | 172 | if params.Limit != 10 { 173 | t.Errorf("Expected limit to remain default (10), got %d", params.Limit) 174 | } 175 | 176 | if params.Vacuum { 177 | t.Error("Expected vacuum to remain default (false)") 178 | } 179 | } 180 | 181 | func TestBindQueryParamsWithMultipleValues(t *testing.T) { 182 | // Test parameters with multiple values 183 | queryParams := url.Values{ 184 | "search_fields": {"name", "email", "description"}, 185 | "columns": {"id", "name", "email"}, 186 | } 187 | 188 | params, err := BindQueryParamsToStruct(queryParams) 189 | if err != nil { 190 | t.Fatalf("Unexpected error: %v", err) 191 | } 192 | 193 | // Check if multiple values are handled correctly 194 | if len(params.SearchFields) != 3 { 195 | t.Errorf("Expected 3 search fields, got %d", len(params.SearchFields)) 196 | } 197 | 198 | expectedSearchFields := []string{"name", "email", "description"} 199 | for i, field := range expectedSearchFields { 200 | if i >= len(params.SearchFields) || params.SearchFields[i] != field { 201 | t.Errorf("Expected search field %d to be '%s', got '%s'", i, field, params.SearchFields[i]) 202 | } 203 | } 204 | 205 | if len(params.Columns) != 3 { 206 | t.Errorf("Expected 3 columns, got %d", len(params.Columns)) 207 | } 208 | } 209 | 210 | func TestBindQueryParamsWithItemsPerPage(t *testing.T) { 211 | // Test items_per_page parameter 212 | queryParams := url.Values{ 213 | "items_per_page": {"20"}, 214 | } 215 | 216 | params, err := BindQueryParamsToStruct(queryParams) 217 | if err != nil { 218 | t.Fatalf("Unexpected error: %v", err) 219 | } 220 | 221 | // Check if items_per_page was set and copied to limit 222 | if params.ItemsPerPage != 20 { 223 | t.Errorf("Expected items_per_page = 20, got %d", params.ItemsPerPage) 224 | } 225 | 226 | if params.Limit != 20 { 227 | t.Errorf("Expected limit = 20 (copied from items_per_page), got %d", params.Limit) 228 | } 229 | } 230 | 231 | func TestBindQueryParamsCustomStruct(t *testing.T) { 232 | // Test binding to a custom struct 233 | type CustomParams struct { 234 | Page int `query:"page"` 235 | Limit int `query:"limit"` 236 | Search string `query:"search"` 237 | } 238 | 239 | queryParams := url.Values{ 240 | "page": {"5"}, 241 | "limit": {"100"}, 242 | "search": {"custom"}, 243 | } 244 | 245 | customParams := &CustomParams{} 246 | err := BindQueryParams(queryParams, customParams) 247 | if err != nil { 248 | t.Fatalf("Unexpected error: %v", err) 249 | } 250 | 251 | if customParams.Page != 5 { 252 | t.Errorf("Expected page = 5, got %d", customParams.Page) 253 | } 254 | 255 | if customParams.Limit != 100 { 256 | t.Errorf("Expected limit = 100, got %d", customParams.Limit) 257 | } 258 | 259 | if customParams.Search != "custom" { 260 | t.Errorf("Expected search = 'custom', got '%s'", customParams.Search) 261 | } 262 | } 263 | 264 | func TestBindQueryParamsInvalidTarget(t *testing.T) { 265 | // Test with invalid target (not a pointer to struct) 266 | queryParams := url.Values{"page": {"1"}} 267 | 268 | // Test with non-pointer 269 | var notPointer PaginationParams 270 | err := BindQueryParams(queryParams, notPointer) 271 | if err == nil { 272 | t.Error("Expected error when passing non-pointer") 273 | } 274 | 275 | // Test with pointer to non-struct 276 | var notStruct int 277 | err = BindQueryParams(queryParams, ¬Struct) 278 | if err == nil { 279 | t.Error("Expected error when passing pointer to non-struct") 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /v3/client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestNew(t *testing.T) { 10 | client := New("https://api.example.com/users") 11 | if client.baseURL != "https://api.example.com/users" { 12 | t.Errorf("Expected baseURL to be 'https://api.example.com/users', got '%s'", client.baseURL) 13 | } 14 | if client.params == nil { 15 | t.Error("Expected params to be initialized") 16 | } 17 | } 18 | 19 | func TestNewFromURL(t *testing.T) { 20 | tests := []struct { 21 | name string 22 | input string 23 | expectedURL string 24 | expectedErr bool 25 | }{ 26 | { 27 | name: "valid URL with query params", 28 | input: "https://api.example.com/users?page=2&limit=10", 29 | expectedURL: "https://api.example.com/users", 30 | expectedErr: false, 31 | }, 32 | { 33 | name: "valid URL without query params", 34 | input: "https://api.example.com/users", 35 | expectedURL: "https://api.example.com/users", 36 | expectedErr: false, 37 | }, 38 | { 39 | name: "invalid URL", 40 | input: "://invalid-url", 41 | expectedURL: "", 42 | expectedErr: true, 43 | }, 44 | } 45 | 46 | for _, tt := range tests { 47 | t.Run(tt.name, func(t *testing.T) { 48 | client, err := NewFromURL(tt.input) 49 | if tt.expectedErr { 50 | if err == nil { 51 | t.Error("Expected error but got none") 52 | } 53 | return 54 | } 55 | if err != nil { 56 | t.Errorf("Unexpected error: %v", err) 57 | return 58 | } 59 | if client.baseURL != tt.expectedURL { 60 | t.Errorf("Expected baseURL to be '%s', got '%s'", tt.expectedURL, client.baseURL) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | func TestBasicPagination(t *testing.T) { 67 | client := New("https://api.example.com/users") 68 | url := client.Page(2).Limit(25).BuildURL() 69 | 70 | expected := "https://api.example.com/users?limit=25&page=2" 71 | if url != expected { 72 | t.Errorf("Expected URL '%s', got '%s'", expected, url) 73 | } 74 | } 75 | 76 | func TestSearch(t *testing.T) { 77 | client := New("https://api.example.com/users") 78 | url := client.Search("john").SearchFields("name", "email").BuildURL() 79 | 80 | if !strings.Contains(url, "search=john") { 81 | t.Error("Expected URL to contain 'search=john'") 82 | } 83 | if !strings.Contains(url, "search_fields=name%2Cemail") { 84 | t.Error("Expected URL to contain encoded search fields") 85 | } 86 | } 87 | 88 | func TestSort(t *testing.T) { 89 | client := New("https://api.example.com/users") 90 | url := client.Sort("name", "-created_at").BuildURL() 91 | 92 | if !strings.Contains(url, "sort=name") { 93 | t.Error("Expected URL to contain 'sort=name'") 94 | } 95 | if !strings.Contains(url, "sort=-created_at") { 96 | t.Error("Expected URL to contain 'sort=-created_at'") 97 | } 98 | } 99 | 100 | func TestLikeFilters(t *testing.T) { 101 | client := New("https://api.example.com/users") 102 | url := client.Like("name", "john", "jane").LikeOr("status", "active", "pending").BuildURL() 103 | 104 | if !strings.Contains(url, "like%5Bname%5D=john") { 105 | t.Error("Expected URL to contain encoded like filter") 106 | } 107 | if !strings.Contains(url, "likeor%5Bstatus%5D=active") { 108 | t.Error("Expected URL to contain encoded likeor filter") 109 | } 110 | } 111 | 112 | func TestEqualityFilters(t *testing.T) { 113 | client := New("https://api.example.com/users") 114 | url := client.Eq("age", 25).EqOr("department", "IT", "HR").BuildURL() 115 | 116 | if !strings.Contains(url, "eq%5Bage%5D=25") { 117 | t.Error("Expected URL to contain encoded eq filter") 118 | } 119 | if !strings.Contains(url, "eqor%5Bdepartment%5D=IT") { 120 | t.Error("Expected URL to contain encoded eqor filter") 121 | } 122 | } 123 | 124 | func TestComparisonFilters(t *testing.T) { 125 | client := New("https://api.example.com/users") 126 | url := client.Gte("age", 18).Lt("score", 100).BuildURL() 127 | 128 | if !strings.Contains(url, "gte%5Bage%5D=18") { 129 | t.Error("Expected URL to contain encoded gte filter") 130 | } 131 | if !strings.Contains(url, "lt%5Bscore%5D=100") { 132 | t.Error("Expected URL to contain encoded lt filter") 133 | } 134 | } 135 | 136 | func TestInFilters(t *testing.T) { 137 | client := New("https://api.example.com/users") 138 | url := client.In("status", "active", "pending").NotIn("role", "admin").BuildURL() 139 | 140 | if !strings.Contains(url, "in%5Bstatus%5D=active") { 141 | t.Error("Expected URL to contain encoded in filter") 142 | } 143 | if !strings.Contains(url, "notin%5Brole%5D=admin") { 144 | t.Error("Expected URL to contain encoded notin filter") 145 | } 146 | } 147 | 148 | func TestBetweenFilter(t *testing.T) { 149 | client := New("https://api.example.com/users") 150 | url := client.Between("age", 18, 65).BuildURL() 151 | 152 | if !strings.Contains(url, "between%5Bage%5D%5B0%5D=18") { 153 | t.Error("Expected URL to contain encoded between min filter") 154 | } 155 | if !strings.Contains(url, "between%5Bage%5D%5B1%5D=65") { 156 | t.Error("Expected URL to contain encoded between max filter") 157 | } 158 | } 159 | 160 | func TestNullFilters(t *testing.T) { 161 | client := New("https://api.example.com/users") 162 | url := client.IsNull("deleted_at").IsNotNull("email").BuildURL() 163 | 164 | if !strings.Contains(url, "isnull=deleted_at") { 165 | t.Error("Expected URL to contain isnull filter") 166 | } 167 | if !strings.Contains(url, "isnotnull=email") { 168 | t.Error("Expected URL to contain isnotnull filter") 169 | } 170 | } 171 | 172 | func TestVacuumAndNoOffset(t *testing.T) { 173 | client := New("https://api.example.com/users") 174 | url := client.Vacuum(true).NoOffset(true).BuildURL() 175 | 176 | if !strings.Contains(url, "vacuum=true") { 177 | t.Error("Expected URL to contain vacuum=true") 178 | } 179 | if !strings.Contains(url, "no_offset=true") { 180 | t.Error("Expected URL to contain no_offset=true") 181 | } 182 | } 183 | 184 | func TestColumns(t *testing.T) { 185 | client := New("https://api.example.com/users") 186 | url := client.Columns("id", "name", "email").BuildURL() 187 | 188 | if !strings.Contains(url, "columns=id%2Cname%2Cemail") { 189 | t.Error("Expected URL to contain encoded columns") 190 | } 191 | } 192 | 193 | func TestClone(t *testing.T) { 194 | client := New("https://api.example.com/users") 195 | client.Page(1).Limit(10) 196 | 197 | cloned := client.Clone() 198 | cloned.Page(2).Limit(20) 199 | 200 | originalURL := client.BuildURL() 201 | clonedURL := cloned.BuildURL() 202 | 203 | if originalURL == clonedURL { 204 | t.Error("Expected cloned client to have different parameters") 205 | } 206 | 207 | if !strings.Contains(originalURL, "page=1") { 208 | t.Error("Expected original client to maintain page=1") 209 | } 210 | if !strings.Contains(clonedURL, "page=2") { 211 | t.Error("Expected cloned client to have page=2") 212 | } 213 | } 214 | 215 | func TestReset(t *testing.T) { 216 | client := New("https://api.example.com/users") 217 | client.Page(1).Limit(10).Search("test") 218 | 219 | urlBefore := client.BuildURL() 220 | client.Reset() 221 | urlAfter := client.BuildURL() 222 | 223 | if strings.Contains(urlAfter, "page=") || strings.Contains(urlAfter, "limit=") || strings.Contains(urlAfter, "search=") { 224 | t.Error("Expected all parameters to be cleared after reset") 225 | } 226 | 227 | if urlBefore == urlAfter { 228 | t.Error("Expected URL to change after reset") 229 | } 230 | } 231 | 232 | func TestCustomParams(t *testing.T) { 233 | client := New("https://api.example.com/users") 234 | url := client.SetCustomParam("custom", "value").AddCustomParam("multi", "value1").AddCustomParam("multi", "value2").BuildURL() 235 | 236 | if !strings.Contains(url, "custom=value") { 237 | t.Error("Expected URL to contain custom parameter") 238 | } 239 | if !strings.Contains(url, "multi=value1") || !strings.Contains(url, "multi=value2") { 240 | t.Error("Expected URL to contain multiple values for multi parameter") 241 | } 242 | } 243 | 244 | func TestRemoveParam(t *testing.T) { 245 | client := New("https://api.example.com/users") 246 | client.Page(1).Limit(10).Search("test") 247 | client.RemoveParam("search") 248 | 249 | url := client.BuildURL() 250 | if strings.Contains(url, "search=") { 251 | t.Error("Expected search parameter to be removed") 252 | } 253 | if !strings.Contains(url, "page=1") { 254 | t.Error("Expected page parameter to remain") 255 | } 256 | } 257 | 258 | func TestGetParams(t *testing.T) { 259 | client := New("https://api.example.com/users") 260 | client.Page(1).Limit(10) 261 | 262 | params := client.GetParams() 263 | if params.Get("page") != "1" { 264 | t.Error("Expected page parameter to be '1'") 265 | } 266 | if params.Get("limit") != "10" { 267 | t.Error("Expected limit parameter to be '10'") 268 | } 269 | 270 | // Test that returned params are a copy 271 | params.Set("page", "999") 272 | if client.GetParams().Get("page") == "999" { 273 | t.Error("Expected returned params to be a copy, not reference") 274 | } 275 | } 276 | 277 | func TestBuildQueryString(t *testing.T) { 278 | client := New("https://api.example.com/users") 279 | queryString := client.Page(1).Limit(10).BuildQueryString() 280 | 281 | if !strings.Contains(queryString, "page=1") { 282 | t.Error("Expected query string to contain page=1") 283 | } 284 | if !strings.Contains(queryString, "limit=10") { 285 | t.Error("Expected query string to contain limit=10") 286 | } 287 | if strings.HasPrefix(queryString, "?") { 288 | t.Error("Expected query string to not start with '?'") 289 | } 290 | } 291 | 292 | func TestComplexExample(t *testing.T) { 293 | client := New("https://api.example.com/users") 294 | generatedURL := client. 295 | Page(2). 296 | Limit(25). 297 | Search("john"). 298 | SearchFields("name", "email"). 299 | Sort("name", "-created_at"). 300 | LikeOr("status", "active", "pending"). 301 | EqOr("age", 25, 30). 302 | Gte("created_at", "2023-01-01"). 303 | Lt("score", 100). 304 | In("department", "IT", "HR"). 305 | IsNotNull("email"). 306 | Vacuum(true). 307 | BuildURL() 308 | 309 | // Verify that the URL contains all expected parameters 310 | expectedParams := []string{ 311 | "page=2", 312 | "limit=25", 313 | "search=john", 314 | "vacuum=true", 315 | } 316 | 317 | for _, param := range expectedParams { 318 | if !strings.Contains(generatedURL, param) { 319 | t.Errorf("Expected URL to contain '%s', got: %s", param, generatedURL) 320 | } 321 | } 322 | 323 | // Parse the URL to verify it's valid 324 | parsedURL, err := url.Parse(generatedURL) 325 | if err != nil { 326 | t.Errorf("Generated URL is not valid: %v", err) 327 | } 328 | 329 | if parsedURL.Scheme != "https" { 330 | t.Error("Expected HTTPS scheme") 331 | } 332 | } -------------------------------------------------------------------------------- /v2/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 []interface{} 24 | WhereCombining string 25 | Schema string 26 | Table string 27 | Struct interface{} 28 | MapArgs map[string]interface{} 29 | NoOffset bool 30 | } 31 | 32 | // Option is a function that configures options in QueryParams. 33 | type Option func(*QueryParams) 34 | 35 | // WithNoOffset sets the NoOffset option. 36 | func WithNoOffset(noOffset bool) Option { 37 | return func(params *QueryParams) { 38 | params.NoOffset = noOffset 39 | } 40 | } 41 | 42 | // WithMapArgs sets the MapArgs option. 43 | func WithMapArgs(mapArgs map[string]interface{}) Option { 44 | return func(params *QueryParams) { 45 | params.MapArgs = mapArgs 46 | } 47 | } 48 | 49 | // WithStruct sets the Struct option. 50 | func WithStruct(s interface{}) Option { 51 | return func(params *QueryParams) { 52 | params.Struct = s 53 | } 54 | } 55 | 56 | // WithSchema sets the Schema option. 57 | func WithSchema(schema string) Option { 58 | return func(params *QueryParams) { 59 | params.Schema = schema 60 | } 61 | } 62 | 63 | // WithTable sets the Table option. 64 | func WithTable(table string) Option { 65 | return func(params *QueryParams) { 66 | params.Table = table 67 | } 68 | } 69 | 70 | // WithPage sets the Page option. 71 | func WithPage(page int) Option { 72 | return func(params *QueryParams) { 73 | params.Page = page 74 | } 75 | } 76 | 77 | // WithItemsPerPage sets the ItemsPerPage option. 78 | func WithItemsPerPage(itemsPerPage int) Option { 79 | return func(params *QueryParams) { 80 | params.ItemsPerPage = itemsPerPage 81 | } 82 | } 83 | 84 | // WithSearch sets the Search option. 85 | func WithSearch(search string) Option { 86 | return func(params *QueryParams) { 87 | params.Search = search 88 | } 89 | } 90 | 91 | // WithSearchFields sets the SearchFields option. 92 | func WithSearchFields(searchFields []string) Option { 93 | return func(params *QueryParams) { 94 | params.SearchFields = searchFields 95 | } 96 | } 97 | 98 | // WithVacuum sets the Vacuum option. 99 | func WithVacuum(vacuum bool) Option { 100 | return func(params *QueryParams) { 101 | params.Vacuum = vacuum 102 | } 103 | } 104 | 105 | // WithColumn adds a column to the Columns option. 106 | func WithColumn(column string) Option { 107 | return func(params *QueryParams) { 108 | params.Columns = append(params.Columns, column) 109 | } 110 | } 111 | 112 | // WithSort sets the SortColumns and SortDirections options. 113 | func WithSort(sortColumns, sortDirections []string) Option { 114 | return func(params *QueryParams) { 115 | params.SortColumns = sortColumns 116 | params.SortDirections = sortDirections 117 | } 118 | } 119 | 120 | // WithJoin adds a join clause to the Joins option. 121 | func WithJoin(join string) Option { 122 | return func(params *QueryParams) { 123 | params.Joins = append(params.Joins, join) 124 | } 125 | } 126 | 127 | // WithWhereCombining sets the WhereCombining option. 128 | func WithWhereCombining(combining string) Option { 129 | return func(params *QueryParams) { 130 | params.WhereCombining = combining 131 | } 132 | } 133 | 134 | // WithWhereClause adds a where clause and its arguments. 135 | func WithWhereClause(clause string, args ...interface{}) Option { 136 | return func(params *QueryParams) { 137 | params.WhereClauses = append(params.WhereClauses, clause) 138 | params.WhereArgs = append(params.WhereArgs, args...) 139 | } 140 | } 141 | 142 | // NewPaginator creates a new QueryParams instance with the given options. 143 | func NewPaginator(options ...Option) (*QueryParams, error) { 144 | params := &QueryParams{ 145 | Page: 1, 146 | ItemsPerPage: 10, 147 | WhereCombining: "AND", 148 | NoOffset: false, 149 | } 150 | 151 | // Apply options 152 | for _, option := range options { 153 | option(params) 154 | } 155 | 156 | // Validation 157 | if params.Table == "" { 158 | return nil, errors.New("principal table is required") 159 | } 160 | 161 | if params.Struct == nil { 162 | return nil, errors.New("struct is required") 163 | } 164 | 165 | return params, nil 166 | } 167 | 168 | // GenerateSQL generates the paginated SQL query and its arguments. 169 | func (params *QueryParams) GenerateSQL() (string, []interface{}) { 170 | var clauses []string 171 | var args []interface{} 172 | 173 | // SELECT clause 174 | selectClause := "SELECT " 175 | if len(params.Columns) > 0 { 176 | selectClause += strings.Join(params.Columns, ", ") 177 | } else { 178 | selectClause += "*" 179 | } 180 | clauses = append(clauses, selectClause) 181 | 182 | // FROM clause 183 | fromClause := fmt.Sprintf("FROM %s", params.Table) 184 | if params.Schema != "" { 185 | fromClause = fmt.Sprintf("FROM %s.%s", params.Schema, params.Table) 186 | } 187 | clauses = append(clauses, fromClause) 188 | 189 | // JOIN clauses 190 | if len(params.Joins) > 0 { 191 | clauses = append(clauses, strings.Join(params.Joins, " ")) 192 | } 193 | 194 | // WHERE clause 195 | whereClauses, whereArgs := params.buildWhereClauses() 196 | if len(whereClauses) > 0 { 197 | clauses = append(clauses, "WHERE "+strings.Join(whereClauses, " AND ")) 198 | args = append(args, whereArgs...) 199 | } 200 | 201 | // ORDER BY clause 202 | orderClause := params.buildOrderClause() 203 | if orderClause != "" { 204 | clauses = append(clauses, orderClause) 205 | } 206 | 207 | // LIMIT and OFFSET 208 | limitOffsetClause, limitOffsetArgs := params.buildLimitOffsetClause() 209 | clauses = append(clauses, limitOffsetClause) 210 | args = append(args, limitOffsetArgs...) 211 | 212 | // Combine all clauses 213 | query := strings.Join(clauses, " ") 214 | 215 | // Replace placeholders 216 | query, args = replacePlaceholders(query, args) 217 | return query, args 218 | } 219 | 220 | // GenerateCountQuery generates the SQL query for counting total records. 221 | func (params *QueryParams) GenerateCountQuery() (string, []interface{}) { 222 | var clauses []string 223 | var args []interface{} 224 | 225 | // SELECT COUNT clause 226 | countSelectClause := "SELECT COUNT(id)" 227 | idColumnName := getFieldName("id", "json", "paginate", params.Struct) 228 | if idColumnName != "" { 229 | countSelectClause = fmt.Sprintf("SELECT COUNT(%s)", idColumnName) 230 | } 231 | clauses = append(clauses, countSelectClause) 232 | 233 | // FROM clause 234 | fromClause := fmt.Sprintf("FROM %s", params.Table) 235 | if params.Schema != "" { 236 | fromClause = fmt.Sprintf("FROM %s.%s", params.Schema, params.Table) 237 | } 238 | clauses = append(clauses, fromClause) 239 | 240 | // JOIN clauses 241 | if len(params.Joins) > 0 { 242 | clauses = append(clauses, strings.Join(params.Joins, " ")) 243 | } 244 | 245 | // WHERE clause 246 | whereClauses, whereArgs := params.buildWhereClauses() 247 | if len(whereClauses) > 0 { 248 | clauses = append(clauses, "WHERE "+strings.Join(whereClauses, " AND ")) 249 | args = append(args, whereArgs...) 250 | } 251 | 252 | // Combine all clauses 253 | query := strings.Join(clauses, " ") 254 | 255 | // Replace placeholders 256 | query, args = replacePlaceholders(query, args) 257 | 258 | if params.Vacuum { 259 | countQuery := "SELECT count_estimate('" + query + "');" 260 | countQuery = strings.Replace(countQuery, "COUNT(id)", "1", -1) 261 | re := regexp.MustCompile(`(\$[0-9]+)`) 262 | countQuery = re.ReplaceAllStringFunc(countQuery, func(match string) string { 263 | return "''" + match + "''" 264 | }) 265 | return countQuery, args 266 | } 267 | 268 | return query, args 269 | } 270 | 271 | // buildWhereClauses constructs the WHERE clauses and arguments. 272 | func (params *QueryParams) buildWhereClauses() ([]string, []interface{}) { 273 | var whereClauses []string 274 | var args []interface{} 275 | 276 | // Search conditions 277 | if params.Search != "" && len(params.SearchFields) > 0 { 278 | var searchConditions []string 279 | for _, field := range params.SearchFields { 280 | columnName := getFieldName(field, "json", "paginate", params.Struct) 281 | if columnName != "" { 282 | searchConditions = append(searchConditions, fmt.Sprintf("%s::TEXT ILIKE ?", columnName)) 283 | args = append(args, "%"+params.Search+"%") 284 | } 285 | } 286 | if len(searchConditions) > 0 { 287 | whereClauses = append(whereClauses, "("+strings.Join(searchConditions, " OR ")+")") 288 | } 289 | } 290 | 291 | // Additional WHERE clauses 292 | if len(params.WhereClauses) > 0 { 293 | whereClauses = append(whereClauses, strings.Join(params.WhereClauses, fmt.Sprintf(" %s ", params.WhereCombining))) 294 | args = append(args, params.WhereArgs...) 295 | } 296 | 297 | return whereClauses, args 298 | } 299 | 300 | // buildOrderClause constructs the ORDER BY clause. 301 | func (params *QueryParams) buildOrderClause() string { 302 | 303 | if len(params.SortColumns) == 0 || len(params.SortDirections) != len(params.SortColumns) { 304 | fmt.Println(params.SortColumns) 305 | fmt.Println(params.SortDirections) 306 | return "" 307 | } 308 | 309 | var sortClauses []string 310 | for i, column := range params.SortColumns { 311 | columnName := getFieldName(column, "json", "paginate", params.Struct) 312 | if columnName != "" { 313 | direction := "ASC" 314 | if strings.ToLower(params.SortDirections[i]) == "true" { 315 | direction = "DESC" 316 | } 317 | sortClauses = append(sortClauses, fmt.Sprintf("%s %s", columnName, direction)) 318 | } 319 | } 320 | 321 | fmt.Println(sortClauses) 322 | if len(sortClauses) > 0 { 323 | return "ORDER BY " + strings.Join(sortClauses, ", ") 324 | } 325 | return "" 326 | } 327 | 328 | // buildLimitOffsetClause constructs the LIMIT and OFFSET clauses. 329 | func (params *QueryParams) buildLimitOffsetClause() (string, []interface{}) { 330 | var clauses []string 331 | var args []interface{} 332 | 333 | clauses = append(clauses, "LIMIT ?") 334 | args = append(args, params.ItemsPerPage) 335 | 336 | if !params.NoOffset { 337 | offset := (params.Page - 1) * params.ItemsPerPage 338 | clauses = append(clauses, "OFFSET ?") 339 | args = append(args, offset) 340 | } 341 | 342 | return strings.Join(clauses, " "), args 343 | } 344 | 345 | // Helper functions 346 | 347 | // replacePlaceholders replaces '?' with positional placeholders like '$1', '$2', etc. 348 | func replacePlaceholders(query string, args []interface{}) (string, []interface{}) { 349 | var newQuery strings.Builder 350 | argIndex := 1 351 | for _, char := range query { 352 | if char == '?' { 353 | newQuery.WriteString(fmt.Sprintf("$%d", argIndex)) 354 | argIndex++ 355 | } else { 356 | newQuery.WriteRune(char) 357 | } 358 | } 359 | return newQuery.String(), args 360 | } 361 | 362 | // getFieldName retrieves the column name from struct tags based on the given key. 363 | func getFieldName(tag, key, keyTarget string, s interface{}) string { 364 | rt := reflect.TypeOf(s) 365 | if rt.Kind() == reflect.Ptr { 366 | rt = rt.Elem() 367 | } 368 | if rt.Kind() != reflect.Struct { 369 | panic("struct type required") 370 | } 371 | for i := 0; i < rt.NumField(); i++ { 372 | field := rt.Field(i) 373 | tagValue := strings.Split(field.Tag.Get(key), ",")[0] 374 | if tagValue == tag { 375 | return field.Tag.Get(keyTarget) 376 | } 377 | } 378 | return "" 379 | } 380 | -------------------------------------------------------------------------------- /v3/paginate/bind.go: -------------------------------------------------------------------------------- 1 | package paginate 2 | 3 | import ( 4 | "fmt" 5 | "log/slog" 6 | "net/url" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // PaginationParams represents pagination parameters that can be extracted from query params 13 | type PaginationParams struct { 14 | Page int `query:"page"` 15 | Limit int `query:"limit"` 16 | ItemsPerPage int `query:"items_per_page"` 17 | Search string `query:"search"` 18 | SearchFields []string `query:"search_fields"` 19 | Sort []string `query:"sort"` 20 | SortColumns []string `query:"sort_columns"` 21 | SortDirections []string `query:"sort_directions"` 22 | Columns []string `query:"columns"` 23 | Vacuum bool `query:"vacuum"` 24 | NoOffset bool `query:"no_offset"` 25 | Like map[string][]string `query:"like"` 26 | LikeOr map[string][]string `query:"likeor"` 27 | LikeAnd map[string][]string `query:"likeand"` 28 | Eq map[string][]any `query:"eq"` 29 | EqOr map[string][]any `query:"eqor"` 30 | EqAnd map[string][]any `query:"eqand"` 31 | Gte map[string]any `query:"gte"` 32 | Gt map[string]any `query:"gt"` 33 | Lte map[string]any `query:"lte"` 34 | Lt map[string]any `query:"lt"` 35 | In map[string][]any `query:"in"` 36 | NotIn map[string][]any `query:"notin"` 37 | Between map[string][2]any `query:"between"` 38 | IsNull []string `query:"isnull"` 39 | IsNotNull []string `query:"isnotnull"` 40 | } 41 | 42 | // BindQueryParams binds url.Values to a pagination struct 43 | func BindQueryParams(queryParams url.Values, target any) error { 44 | v := reflect.ValueOf(target) 45 | if v.Kind() != reflect.Ptr || v.Elem().Kind() != reflect.Struct { 46 | return fmt.Errorf("target must be a pointer to struct") 47 | } 48 | 49 | v = v.Elem() 50 | t := v.Type() 51 | 52 | // Inicializar maps se necessário 53 | initializeMaps(v, t) 54 | 55 | for i := range v.NumField() { 56 | field := v.Field(i) 57 | fieldType := t.Field(i) 58 | queryTag := fieldType.Tag.Get("query") 59 | 60 | if queryTag == "" || !field.CanSet() { 61 | continue 62 | } 63 | 64 | // Tratar campos especiais com sintaxe de array 65 | if isMapField(fieldType.Type) { 66 | bindMapField(queryParams, field, queryTag) 67 | continue 68 | } 69 | 70 | // Tratar campos normais 71 | values, exists := queryParams[queryTag] 72 | if !exists || len(values) == 0 { 73 | continue 74 | } 75 | 76 | if err := setFieldValue(field, values); err != nil { 77 | return fmt.Errorf("error setting field %s: %w", fieldType.Name, err) 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // initializeMaps inicializa os campos de map se eles forem nil 85 | func initializeMaps(v reflect.Value, t reflect.Type) { 86 | for i := range v.NumField() { 87 | field := v.Field(i) 88 | fieldType := t.Field(i) 89 | 90 | if !field.CanSet() { 91 | continue 92 | } 93 | 94 | switch fieldType.Type.Kind() { 95 | case reflect.Map: 96 | if field.IsNil() { 97 | field.Set(reflect.MakeMap(fieldType.Type)) 98 | } 99 | } 100 | } 101 | } 102 | 103 | // isMapField verifica se o campo é um map 104 | func isMapField(t reflect.Type) bool { 105 | return t.Kind() == reflect.Map 106 | } 107 | 108 | // bindMapField faz bind de parâmetros com sintaxe de array para campos de map 109 | func bindMapField(queryParams url.Values, field reflect.Value, queryTag string) { 110 | if !field.CanSet() || field.Kind() != reflect.Map { 111 | return 112 | } 113 | 114 | mapType := field.Type() 115 | keyType := mapType.Key() 116 | valueType := mapType.Elem() 117 | 118 | // Procurar por parâmetros com formato: queryTag[key]=value 119 | prefix := queryTag + "[" 120 | for paramName, values := range queryParams { 121 | if strings.HasPrefix(paramName, prefix) && strings.HasSuffix(paramName, "]") { 122 | // Extrair a chave do parâmetro 123 | key := paramName[len(prefix) : len(paramName)-1] 124 | if key == "" { 125 | continue 126 | } 127 | 128 | // Converter a chave para o tipo correto 129 | keyValue := reflect.ValueOf(key) 130 | if keyType.Kind() != reflect.String { 131 | continue // Por enquanto, só suportamos chaves string 132 | } 133 | 134 | // Converter os valores para o tipo correto 135 | var mapValue reflect.Value 136 | switch valueType.Kind() { 137 | case reflect.Array: 138 | // Para arrays fixos como [2]any (usado em Between) 139 | if valueType.Len() == 2 && len(values) >= 2 { 140 | arrayValue := reflect.New(valueType).Elem() 141 | for i := 0; i < 2 && i < len(values); i++ { 142 | value := values[i] 143 | var elem reflect.Value 144 | if valueType.Elem().Kind() == reflect.Interface { 145 | // Try to convert to number first, then boolean, otherwise keep as string 146 | if intVal, err := strconv.Atoi(value); err == nil { 147 | elem = reflect.ValueOf(intVal) 148 | } else if floatVal, err := strconv.ParseFloat(value, 64); err == nil { 149 | elem = reflect.ValueOf(floatVal) 150 | } else if boolVal, err := strconv.ParseBool(value); err == nil { 151 | elem = reflect.ValueOf(boolVal) 152 | } else { 153 | elem = reflect.ValueOf(value) 154 | } 155 | } else { 156 | elem = reflect.ValueOf(value) 157 | } 158 | arrayValue.Index(i).Set(elem) 159 | } 160 | mapValue = arrayValue 161 | } 162 | case reflect.Slice: 163 | // Para []string ou []any 164 | sliceType := valueType.Elem() 165 | slice := reflect.MakeSlice(valueType, 0, len(values)) 166 | for _, value := range values { 167 | var elem reflect.Value 168 | if sliceType.Kind() == reflect.Interface { 169 | // Try to convert to number first, then boolean, otherwise keep as string 170 | if intVal, err := strconv.Atoi(value); err == nil { 171 | elem = reflect.ValueOf(intVal) 172 | } else if floatVal, err := strconv.ParseFloat(value, 64); err == nil { 173 | elem = reflect.ValueOf(floatVal) 174 | } else if boolVal, err := strconv.ParseBool(value); err == nil { 175 | elem = reflect.ValueOf(boolVal) 176 | } else { 177 | elem = reflect.ValueOf(value) 178 | } 179 | } else { 180 | elem = reflect.ValueOf(value) 181 | } 182 | slice = reflect.Append(slice, elem) 183 | } 184 | mapValue = slice 185 | case reflect.Interface: 186 | // Para any, usar o primeiro valor 187 | if len(values) > 0 { 188 | value := values[0] 189 | // Try to convert to number first, then boolean, otherwise keep as string 190 | if intVal, err := strconv.Atoi(value); err == nil { 191 | mapValue = reflect.ValueOf(intVal) 192 | } else if floatVal, err := strconv.ParseFloat(value, 64); err == nil { 193 | mapValue = reflect.ValueOf(floatVal) 194 | } else if boolVal, err := strconv.ParseBool(value); err == nil { 195 | mapValue = reflect.ValueOf(boolVal) 196 | } else { 197 | mapValue = reflect.ValueOf(value) 198 | } 199 | } 200 | default: 201 | continue // Tipo não suportado 202 | } 203 | 204 | if mapValue.IsValid() { 205 | field.SetMapIndex(keyValue, mapValue) 206 | } 207 | } 208 | } 209 | } 210 | 211 | // setFieldValue define o valor de um campo baseado nos valores dos query params 212 | func setFieldValue(field reflect.Value, values []string) error { 213 | if len(values) == 0 { 214 | return nil 215 | } 216 | 217 | switch field.Kind() { 218 | case reflect.String: 219 | field.SetString(values[0]) 220 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 221 | if intVal, err := strconv.ParseInt(values[0], 10, 64); err == nil { 222 | field.SetInt(intVal) 223 | } 224 | case reflect.Bool: 225 | if boolVal, err := strconv.ParseBool(values[0]); err == nil { 226 | field.SetBool(boolVal) 227 | } 228 | case reflect.Slice: 229 | // Para slices, usar todos os valores ou dividir por vírgula se for um único valor 230 | var finalValues []string 231 | if len(values) == 1 && strings.Contains(values[0], ",") { 232 | finalValues = strings.Split(values[0], ",") 233 | // Remover espaços em branco 234 | for i, v := range finalValues { 235 | finalValues[i] = strings.TrimSpace(v) 236 | } 237 | } else { 238 | finalValues = values 239 | } 240 | 241 | slice := reflect.MakeSlice(field.Type(), len(finalValues), len(finalValues)) 242 | for i, value := range finalValues { 243 | slice.Index(i).SetString(value) 244 | } 245 | field.Set(slice) 246 | default: 247 | return fmt.Errorf("unsupported field type: %s", field.Kind()) 248 | } 249 | 250 | return nil 251 | } 252 | 253 | // BindQueryParamsToStruct é uma função de conveniência que cria uma nova instância de PaginationParams 254 | // e faz bind dos query params para ela 255 | func BindQueryParamsToStruct(queryParams url.Values) (*PaginationParams, error) { 256 | params := &PaginationParams{ 257 | Page: 1, // valor padrão 258 | Limit: 10, // valor padrão 259 | ItemsPerPage: 10, // valor padrão 260 | } 261 | 262 | err := BindQueryParams(queryParams, params) 263 | if err != nil { 264 | return nil, err 265 | } 266 | 267 | // If ItemsPerPage was set but Limit wasn't, use ItemsPerPage as Limit 268 | if params.ItemsPerPage != 10 && params.Limit == 10 { 269 | params.Limit = params.ItemsPerPage 270 | } 271 | 272 | return params, nil 273 | } 274 | 275 | // BindQueryStringToStruct faz bind de uma query string para PaginationParams 276 | func BindQueryStringToStruct(queryString string) (*PaginationParams, error) { 277 | queryParams, err := url.ParseQuery(queryString) 278 | if err != nil { 279 | return nil, fmt.Errorf("error parsing query string: %w", err) 280 | } 281 | 282 | return BindQueryParamsToStruct(queryParams) 283 | } 284 | 285 | // NewPaginationParams cria uma nova instância com valores padrão globais 286 | func NewPaginationParams() *PaginationParams { 287 | return &PaginationParams{ 288 | Page: 1, 289 | Limit: GetDefaultLimit(), // Use global default 290 | Like: make(map[string][]string), 291 | LikeOr: make(map[string][]string), 292 | LikeAnd: make(map[string][]string), 293 | Eq: make(map[string][]any), 294 | EqOr: make(map[string][]any), 295 | EqAnd: make(map[string][]any), 296 | Gte: make(map[string]any), 297 | Gt: make(map[string]any), 298 | Lte: make(map[string]any), 299 | Lt: make(map[string]any), 300 | In: make(map[string][]any), 301 | NotIn: make(map[string][]any), 302 | Between: make(map[string][2]any), 303 | IsNull: make([]string, 0), 304 | IsNotNull: make([]string, 0), 305 | } 306 | } 307 | 308 | // setDefaultValues define valores padrão usando configuração global 309 | func setDefaultValues(params *PaginationParams) { 310 | if params.Page == 0 { 311 | params.Page = 1 312 | } 313 | if params.Limit == 0 { 314 | params.Limit = GetDefaultLimit() // Use global default 315 | } 316 | 317 | // Apply global max limit 318 | maxLimit := GetMaxLimit() 319 | if params.Limit > maxLimit { 320 | logger := slog.With("component", "go-paginate-bind") 321 | logger.Warn("Limit exceeds maximum, applying global max limit", 322 | "requested_limit", params.Limit, 323 | "max_limit", maxLimit) 324 | params.Limit = maxLimit 325 | } 326 | 327 | // If ItemsPerPage was set but Limit wasn't, use ItemsPerPage as Limit 328 | if params.ItemsPerPage != GetDefaultLimit() && params.Limit == GetDefaultLimit() { 329 | params.Limit = params.ItemsPerPage 330 | // Apply max limit validation again 331 | if params.Limit > maxLimit { 332 | params.Limit = maxLimit 333 | } 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /v3/FILTER_README.md: -------------------------------------------------------------------------------- 1 | # Go Paginate v3 - New Operators & Filters Guide 2 | 3 |

4 | Go Paginate Logo 5 |

6 | 7 |

8 |

🚀 Go Paginate v3 - New Operators Guide

9 |

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 | --------------------------------------------------------------------------------