update builder

This commit is contained in:
2025-10-23 05:37:33 +07:00
parent a5523d11a3
commit d4638dddc8
4 changed files with 3329 additions and 96 deletions

View File

File diff suppressed because it is too large Load Diff

View File

@@ -61,12 +61,23 @@ type SortField struct {
Order string `json:"order"` // ASC, DESC
}
// UpdateData represents data for UPDATE operations
type UpdateData struct {
Columns []string `json:"columns"`
Values []interface{} `json:"values"`
}
// InsertData represents data for INSERT operations
type InsertData struct {
Columns []string `json:"columns"`
Values []interface{} `json:"values"`
}
// QueryBuilder builds SQL queries from dynamic filters
type QueryBuilder struct {
tableName string
columnMapping map[string]string // Maps API field names to DB column names
allowedColumns map[string]bool // Security: only allow specified columns
// PERUBAHAN 1: Hapus paramCounter dan mu untuk membuat QueryBuilder stateless dan thread-safe.
}
// NewQueryBuilder creates a new query builder instance
@@ -85,8 +96,6 @@ func (qb *QueryBuilder) SetColumnMapping(mapping map[string]string) *QueryBuilde
}
// SetAllowedColumns sets the list of allowed columns for security
// PERUBAHAN 3: Nama kolom di sini seharusnya adalah nama kolom ASLI di database
// untuk pemeriksaan keamanan yang lebih konsisten.
func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
qb.allowedColumns = make(map[string]bool)
for _, col := range columns {
@@ -95,10 +104,8 @@ func (qb *QueryBuilder) SetAllowedColumns(columns []string) *QueryBuilder {
return qb
}
// BuildQuery builds the complete SQL query
// BuildQuery builds the complete SQL SELECT query
func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
// PERUBAHAN 1: paramCounter sekarang lokal untuk fungsi ini.
// Ini membuat QueryBuilder aman untuk digunakan secara konkuren (thread-safe).
paramCounter := 0
args := []interface{}{}
@@ -164,6 +171,197 @@ func (qb *QueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, e
return sql, args, nil
}
// BuildInsertQuery builds an INSERT query
func (qb *QueryBuilder) BuildInsertQuery(data InsertData, returningColumns ...string) (string, []interface{}, error) {
if len(data.Columns) == 0 || len(data.Values) == 0 {
return "", nil, fmt.Errorf("no columns or values provided for INSERT")
}
if len(data.Columns) != len(data.Values) {
return "", nil, fmt.Errorf("columns and values count mismatch for INSERT")
}
paramCounter := 0
args := []interface{}{}
// Build column names
var columns []string
for _, col := range data.Columns {
mappedCol := qb.mapAndValidateColumn(col)
if mappedCol == "" {
continue // Skip invalid columns
}
columns = append(columns, fmt.Sprintf(`"%s"`, mappedCol))
}
if len(columns) == 0 {
return "", nil, fmt.Errorf("no valid columns provided for INSERT")
}
// Build value placeholders
var placeholders []string
for i := 0; i < len(columns); i++ {
paramCounter++
placeholders = append(placeholders, fmt.Sprintf("$%d", paramCounter))
args = append(args, data.Values[i])
}
// Build INSERT clause
insertClause := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
qb.tableName,
strings.Join(columns, ", "),
strings.Join(placeholders, ", "),
)
// Build RETURNING clause if specified
returningClause := qb.buildReturningClause(returningColumns)
// Combine all parts
sqlParts := []string{insertClause}
if returningClause != "" {
sqlParts = append(sqlParts, returningClause)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}
// BuildUpdateQuery builds an UPDATE query
func (qb *QueryBuilder) BuildUpdateQuery(data UpdateData, filters []FilterGroup, returningColumns ...string) (string, []interface{}, error) {
if len(data.Columns) == 0 || len(data.Values) == 0 {
return "", nil, fmt.Errorf("no columns or values provided for UPDATE")
}
if len(data.Columns) != len(data.Values) {
return "", nil, fmt.Errorf("columns and values count mismatch for UPDATE")
}
paramCounter := 0
args := []interface{}{}
// Build SET clause
var setParts []string
for i, col := range data.Columns {
mappedCol := qb.mapAndValidateColumn(col)
if mappedCol == "" {
continue // Skip invalid columns
}
paramCounter++
setParts = append(setParts, fmt.Sprintf(`"%s" = $%d`, mappedCol, paramCounter))
args = append(args, data.Values[i])
}
if len(setParts) == 0 {
return "", nil, fmt.Errorf("no valid columns provided for UPDATE")
}
setClause := "SET " + strings.Join(setParts, ", ")
// Build WHERE clause
whereClause, whereArgs, err := qb.buildWhereClause(filters, &paramCounter)
if err != nil {
return "", nil, err
}
args = append(args, whereArgs...)
// Build RETURNING clause if specified
returningClause := qb.buildReturningClause(returningColumns)
// Combine all parts
sqlParts := []string{
fmt.Sprintf("UPDATE %s", qb.tableName),
setClause,
}
if whereClause != "" {
sqlParts = append(sqlParts, "WHERE "+whereClause)
}
if returningClause != "" {
sqlParts = append(sqlParts, returningClause)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}
// BuildDeleteQuery builds a DELETE query
func (qb *QueryBuilder) BuildDeleteQuery(filters []FilterGroup, returningColumns ...string) (string, []interface{}, error) {
paramCounter := 0
args := []interface{}{}
// Build DELETE clause
deleteClause := fmt.Sprintf("DELETE FROM %s", qb.tableName)
// Build WHERE clause
whereClause, whereArgs, err := qb.buildWhereClause(filters, &paramCounter)
if err != nil {
return "", nil, err
}
args = append(args, whereArgs...)
// Build RETURNING clause if specified
returningClause := qb.buildReturningClause(returningColumns)
// Combine all parts
sqlParts := []string{deleteClause}
if whereClause != "" {
sqlParts = append(sqlParts, "WHERE "+whereClause)
}
if returningClause != "" {
sqlParts = append(sqlParts, returningClause)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}
// BuildCountQuery builds a count query
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
paramCounter := 0
args := []interface{}{}
// Build FROM clause
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
// Build WHERE clause
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters, &paramCounter)
if err != nil {
return "", nil, err
}
args = append(args, whereArgs...)
// Build GROUP BY clause
groupClause := qb.buildGroupByClause(query.GroupBy)
// Build HAVING clause
havingClause, havingArgs, err := qb.buildHavingClause(query.Having, &paramCounter)
if err != nil {
return "", nil, err
}
args = append(args, havingArgs...)
// Combine parts
sqlParts := []string{"SELECT COUNT(*)", fromClause}
if whereClause != "" {
sqlParts = append(sqlParts, "WHERE "+whereClause)
}
if groupClause != "" {
sqlParts = append(sqlParts, groupClause)
}
if havingClause != "" {
sqlParts = append(sqlParts, "HAVING "+havingClause)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}
// buildSelectClause builds the SELECT part of the query
func (qb *QueryBuilder) buildSelectClause(fields []string) string {
if len(fields) == 0 || (len(fields) == 1 && fields[0] == "*") {
@@ -182,15 +380,9 @@ func (qb *QueryBuilder) buildSelectClause(fields []string) string {
continue
}
// PERUBAHAN 3: Lakukan mapping terlebih dahulu, lalu pemeriksaan keamanan.
mappedCol := field
if mapped, exists := qb.columnMapping[field]; exists {
mappedCol = mapped
}
// Security check: hanya izinkan kolom yang sudah ditentukan (cek nama kolom DB)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[mappedCol] {
continue // Lewati kolom yang tidak diizinkan
mappedCol := qb.mapAndValidateColumn(field)
if mappedCol == "" {
continue // Skip invalid columns
}
selectedFields = append(selectedFields, fmt.Sprintf(`"%s"`, mappedCol))
@@ -203,6 +395,55 @@ func (qb *QueryBuilder) buildSelectClause(fields []string) string {
return "SELECT " + strings.Join(selectedFields, ", ")
}
// buildReturningClause builds the RETURNING part of the query
func (qb *QueryBuilder) buildReturningClause(columns []string) string {
if len(columns) == 0 {
return ""
}
var returningFields []string
for _, field := range columns {
if field == "*" {
returningFields = append(returningFields, "*")
continue
}
mappedCol := qb.mapAndValidateColumn(field)
if mappedCol == "" {
continue // Skip invalid columns
}
returningFields = append(returningFields, fmt.Sprintf(`"%s"`, mappedCol))
}
if len(returningFields) == 0 {
return ""
}
return "RETURNING " + strings.Join(returningFields, ", ")
}
// mapAndValidateColumn maps a column name and validates it
func (qb *QueryBuilder) mapAndValidateColumn(field string) string {
// Map the column name
mappedCol := field
if mapped, exists := qb.columnMapping[field]; exists {
mappedCol = mapped
}
// Security check: only allow specified columns
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[mappedCol] {
return "" // Skip invalid columns
}
// Additional security: Validate column name format
if !qb.isValidColumnName(mappedCol) {
return "" // Skip invalid columns
}
return mappedCol
}
// buildWhereClause builds the WHERE part of the query
func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup, paramCounter *int) (string, []interface{}, error) {
if len(filterGroups) == 0 {
@@ -213,7 +454,6 @@ func (qb *QueryBuilder) buildWhereClause(filterGroups []FilterGroup, paramCounte
var allArgs []interface{}
for i, group := range filterGroups {
// PERUBAHAN 2: Tambahkan tanda kurung untuk setiap grup untuk memastikan urutan operasi yang benar.
groupCondition, groupArgs, err := qb.buildFilterGroup(group, paramCounter)
if err != nil {
return "", nil, err
@@ -271,20 +511,10 @@ func (qb *QueryBuilder) buildFilterGroup(group FilterGroup, paramCounter *int) (
// buildFilterCondition builds a single filter condition
func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter *int) (string, []interface{}, error) {
// PERUBAHAN 3: Lakukan mapping terlebih dahulu, lalu pemeriksaan keamanan.
column := filter.Column
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check (cek nama kolom DB hasil mapping)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
return "", nil, nil
}
// Additional security: Validate column name format
if !qb.isValidColumnName(column) {
return "", nil, fmt.Errorf("invalid column name: %s", column)
// Map and validate the column name
column := qb.mapAndValidateColumn(filter.Column)
if column == "" {
return "", nil, nil // Skip invalid columns
}
// Wrap column name in quotes for PostgreSQL
@@ -292,7 +522,6 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter
switch filter.Operator {
case OpEqual:
// PERUBAHAN 4: Tangani nilai nil secara eksplisit untuk operator kesetaraan.
if filter.Value == nil {
return fmt.Sprintf("%s IS NULL", column), nil, nil
}
@@ -300,7 +529,6 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter, paramCounter
return fmt.Sprintf("%s = $%d", column, *paramCounter), []interface{}{filter.Value}, nil
case OpNotEqual:
// PERUBAHAN 4: Tangani nilai nil secara eksplisit untuk operator ketidaksamaan.
if filter.Value == nil {
return fmt.Sprintf("%s IS NOT NULL", column), nil, nil
}
@@ -492,15 +720,9 @@ func (qb *QueryBuilder) buildOrderClause(sortFields []SortField) string {
var orderParts []string
for _, sort := range sortFields {
// PERUBAHAN 3: Lakukan mapping dan pemeriksaan keamanan.
column := sort.Column
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check (cek nama kolom DB hasil mapping)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
continue
column := qb.mapAndValidateColumn(sort.Column)
if column == "" {
continue // Skip invalid columns
}
order := "ASC"
@@ -526,15 +748,9 @@ func (qb *QueryBuilder) buildGroupByClause(groupFields []string) string {
var groupParts []string
for _, field := range groupFields {
// PERUBAHAN 3: Lakukan mapping dan pemeriksaan keamanan.
column := field
if mappedCol, exists := qb.columnMapping[column]; exists {
column = mappedCol
}
// Security check (cek nama kolom DB hasil mapping)
if len(qb.allowedColumns) > 0 && !qb.allowedColumns[column] {
continue
column := qb.mapAndValidateColumn(field)
if column == "" {
continue // Skip invalid columns
}
groupParts = append(groupParts, fmt.Sprintf(`"%s"`, column))
@@ -556,51 +772,6 @@ func (qb *QueryBuilder) buildHavingClause(havingGroups []FilterGroup, paramCount
return qb.buildWhereClause(havingGroups, paramCounter)
}
// BuildCountQuery builds a count query
func (qb *QueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
// PERUBAHAN 1: paramCounter lokal.
paramCounter := 0
args := []interface{}{}
// Build FROM clause
fromClause := fmt.Sprintf("FROM %s", qb.tableName)
// Build WHERE clause
whereClause, whereArgs, err := qb.buildWhereClause(query.Filters, &paramCounter)
if err != nil {
return "", nil, err
}
args = append(args, whereArgs...)
// Build GROUP BY clause
groupClause := qb.buildGroupByClause(query.GroupBy)
// Build HAVING clause
havingClause, havingArgs, err := qb.buildHavingClause(query.Having, &paramCounter)
if err != nil {
return "", nil, err
}
args = append(args, havingArgs...)
// Combine parts
sqlParts := []string{"SELECT COUNT(*)", fromClause}
if whereClause != "" {
sqlParts = append(sqlParts, "WHERE "+whereClause)
}
if groupClause != "" {
sqlParts = append(sqlParts, groupClause)
}
if havingClause != "" {
sqlParts = append(sqlParts, "HAVING "+havingClause)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}
// isValidColumnName validates column name format to prevent SQL injection
func (qb *QueryBuilder) isValidColumnName(column string) bool {
if column == "" {
@@ -608,7 +779,6 @@ func (qb *QueryBuilder) isValidColumnName(column string) bool {
}
// Allow only alphanumeric characters, underscores, and dots (for table.column format)
// This is more restrictive than before for better security
for _, r := range column {
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') ||
(r >= '0' && r <= '9') || r == '_' || r == '.') {
@@ -632,3 +802,96 @@ func (qb *QueryBuilder) isValidColumnName(column string) bool {
return true
}
// BuildUpsertQuery builds an UPSERT (INSERT ... ON CONFLICT UPDATE) query for PostgreSQL
func (qb *QueryBuilder) BuildUpsertQuery(data InsertData, conflictColumns []string, updateData UpdateData, returningColumns ...string) (string, []interface{}, error) {
if len(data.Columns) == 0 || len(data.Values) == 0 {
return "", nil, fmt.Errorf("no columns or values provided for UPSERT")
}
if len(data.Columns) != len(data.Values) {
return "", nil, fmt.Errorf("columns and values count mismatch for UPSERT")
}
if len(conflictColumns) == 0 {
return "", nil, fmt.Errorf("no conflict columns provided for UPSERT")
}
paramCounter := 0
args := []interface{}{}
// Build column names
var columns []string
for _, col := range data.Columns {
mappedCol := qb.mapAndValidateColumn(col)
if mappedCol == "" {
continue // Skip invalid columns
}
columns = append(columns, fmt.Sprintf(`"%s"`, mappedCol))
}
if len(columns) == 0 {
return "", nil, fmt.Errorf("no valid columns provided for UPSERT")
}
// Build value placeholders
var placeholders []string
for i := 0; i < len(columns); i++ {
paramCounter++
placeholders = append(placeholders, fmt.Sprintf("$%d", paramCounter))
args = append(args, data.Values[i])
}
// Build INSERT clause
insertClause := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
qb.tableName,
strings.Join(columns, ", "),
strings.Join(placeholders, ", "),
)
// Build ON CONFLICT clause
var conflictCols []string
for _, col := range conflictColumns {
mappedCol := qb.mapAndValidateColumn(col)
if mappedCol == "" {
continue // Skip invalid columns
}
conflictCols = append(conflictCols, fmt.Sprintf(`"%s"`, mappedCol))
}
if len(conflictCols) == 0 {
return "", nil, fmt.Errorf("no valid conflict columns provided for UPSERT")
}
conflictClause := fmt.Sprintf("ON CONFLICT (%s)", strings.Join(conflictCols, ", "))
// Build UPDATE clause
var updateParts []string
for i, col := range updateData.Columns {
mappedCol := qb.mapAndValidateColumn(col)
if mappedCol == "" {
continue // Skip invalid columns
}
paramCounter++
updateParts = append(updateParts, fmt.Sprintf(`"%s" = $%d`, mappedCol, paramCounter))
args = append(args, updateData.Values[i])
}
if len(updateParts) == 0 {
return "", nil, fmt.Errorf("no valid update columns provided for UPSERT")
}
updateClause := "DO UPDATE SET " + strings.Join(updateParts, ", ")
// Build RETURNING clause if specified
returningClause := qb.buildReturningClause(returningColumns)
// Combine all parts
sqlParts := []string{insertClause, conflictClause, updateClause}
if returningClause != "" {
sqlParts = append(sqlParts, returningClause)
}
sql := strings.Join(sqlParts, " ")
return sql, args, nil
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,220 @@
package main
import (
"fmt"
"log"
"net/url"
"your_module_path/utils" // Ganti dengan path modul Anda
)
func main() {
// --- 1. Setup QueryBuilder ---
// Kita akan menggunakan PostgreSQL untuk contoh ini.
// Untuk database lain, cukup ganti DBTypePostgreSQL menjadi DBTypeMySQL, dll.
qb := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL).
SetColumnMapping(map[string]string{
// API Field -> DB Column
"userId": "id",
"username": "user_name",
"email": "email_address",
"isActive": "is_active",
"createdAt": "created_at",
}).
SetAllowedColumns([]string{
"id", "user_name", "email_address", "is_active", "created_at", "updated_at",
})
fmt.Println("=== QUERY BUILDER EXAMPLES ===\n")
// --- 2. CREATE (INSERT) ---
fmt.Println("--- CREATE (INSERT) ---")
// Contoh data user baru
newUser := utils.InsertData{
Columns: []string{"username", "email", "is_active"},
Values: []interface{}{"john_doe", "john.doe@example.com", true},
}
// Build INSERT query
sql, args, err := qb.BuildInsertQuery(newUser, "id", "created_at")
if err != nil {
log.Fatalf("Failed to build INSERT query: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: INSERT INTO users (user_name, email_address, is_active) VALUES ($1, $2, $3) RETURNING id, created_at
// Args: [john_doe john.doe@example.com true]
// --- 3. UPDATE ---
fmt.Println("--- UPDATE ---")
// Data yang akan diupdate untuk user dengan ID 1
updateData := utils.UpdateData{
Columns: []string{"username", "is_active"},
Values: []interface{}{"john_doe_updated", false},
}
// Filter untuk menentukan user mana yang akan diupdate
filters := []utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "userId",
Operator: utils.OpEqual,
Value: 1,
}},
}}
// Build UPDATE query
sql, args, err = qb.BuildUpdateQuery(updateData, filters, "updated_at")
if err != nil {
log.Fatalf("Failed to build UPDATE query: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: UPDATE "users" SET "user_name" = $1, "is_active" = $2 WHERE ("id" = $3) RETURNING updated_at
// Args: [john_doe_updated false 1]
// --- 4. DELETE ---
fmt.Println("--- DELETE ---")
// Filter untuk menghapus user yang tidak aktif
deleteFilters := []utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "isActive",
Operator: utils.OpEqual,
Value: false,
}},
}}
// Build DELETE query
sql, args, err = qb.BuildDeleteQuery(deleteFilters, "id")
if err != nil {
log.Fatalf("Failed to build DELETE query: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: DELETE FROM "users" WHERE ("is_active" = $1) RETURNING id
// Args: [false]
// --- 5. UPSERT (INSERT ... ON CONFLICT) ---
fmt.Println("--- UPSERT ---")
// Data untuk upsert (insert atau update jika sudah ada)
upsertData := utils.InsertData{
Columns: []string{"id", "username", "email"},
Values: []interface{}{1, "unique_user", "unique@example.com"}, // ID 1 mungkin sudah ada
}
// Kolom yang menjadi penentu konflik (misalnya, primary key atau unique key)
conflictColumns := []string{"id"}
// Data yang akan diupdate jika terjadi konflik
upsertUpdateData := utils.UpdateData{
Columns: []string{"username", "email"},
Values: []interface{}{"unique_user_updated", "updated@example.com"},
}
// Build UPSERT query
sql, args, err = qb.BuildUpsertQuery(upsertData, conflictColumns, upsertUpdateData, "updated_at")
if err != nil {
log.Fatalf("Failed to build UPSERT query: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: INSERT INTO users (id, user_name, email_address) VALUES ($1, $2, $3) ON CONFLICT (id) DO UPDATE SET "user_name" = EXCLUDED.user_name, "email_address" = EXCLUDED.email_address RETURNING updated_at
// Args: [1 unique_user unique@example.com unique_user_updated updated@example.com]
// --- 6. SELECT dengan JOIN ---
fmt.Println("--- SELECT with JOIN ---")
// Reset builder untuk query baru
qbJoin := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL).
SetColumnMapping(map[string]string{"userId": "id", "username": "user_name"}).
SetAllowedColumns([]string{"id", "user_name", "profile_id"}).
Join("INNER", "profiles", "users.id = profiles.user_id") // Tambahkan JOIN
// Query untuk mendapatkan user dan profilnya
joinQuery := utils.DynamicQuery{
Fields: []string{"users.id", "user_name", "profiles.bio"},
Filters: []utils.FilterGroup{{
Filters: []utils.DynamicFilter{{
Column: "isActive",
Operator: utils.OpEqual,
Value: true,
}},
}},
Limit: 10,
}
sql, args, err = qbJoin.BuildQuery(joinQuery)
if err != nil {
log.Fatalf("Failed to build JOIN query: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: SELECT "users"."id", "user_name", "profiles"."bio" FROM "users" INNER JOIN "profiles" ON users.id = profiles.user_id WHERE ("is_active" = $1) LIMIT 10
// Args: [true]
// --- 7. SELECT dengan UNION ---
fmt.Println("--- SELECT with UNION ---")
// Buat dua query builder untuk dua tabel berbeda
qbActive := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL)
qbArchived := utils.NewQueryBuilder("archived_users", utils.DBTypePostgreSQL)
// Query pertama: user aktif dari tabel 'users'
query1 := qbActive.sqlBuilder.Select("id", "user_name", "'active' as status").Where("is_active = ?", true)
// Query kedua: user aktif dari tabel 'archived_users'
query2 := qbArchived.sqlBuilder.Select("id", "user_name", "'archived' as status").Where("is_active = ?", true)
// Gabungkan dengan UNION
unionQuery := query1.Union(query2)
sql, args, err = unionQuery.ToSql()
if err != nil {
log.Fatalf("Failed to build UNION query: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: SELECT id, user_name, 'active' as status FROM users WHERE is_active = $1 UNION SELECT id, user_name, 'archived' as status FROM archived_users WHERE is_active = $2
// Args: [true true]
// --- 8. Integrasi dengan QueryParser (Web Context) ---
fmt.Println("--- Integration with QueryParser ---")
// Simulasi query parameter dari URL: /users?fields=id,username&filter[username][_contains]=john&sort=-createdAt&limit=5
mockURLValues := url.Values{
"fields": []string{"id", "username"},
"filter[username][_contains]": []string{"john"},
"sort": []string{"-createdAt"},
"limit": []string{"5"},
}
parser := utils.NewQueryParser()
dynamicQuery, err := parser.ParseQuery(mockURLValues)
if err != nil {
log.Fatalf("Failed to parse query: %v", err)
}
// Build query dari hasil parsing
qbParser := utils.NewQueryBuilder("users", utils.DBTypePostgreSQL).
SetColumnMapping(map[string]string{"username": "user_name", "createdAt": "created_at"}).
SetAllowedColumns([]string{"id", "user_name", "created_at"})
sql, args, err = qbParser.BuildQuery(dynamicQuery)
if err != nil {
log.Fatalf("Failed to build query from parser: %v", err)
}
fmt.Printf("SQL: %s\n", sql)
fmt.Printf("Args: %v\n\n", args)
// Output (PostgreSQL):
// SQL: SELECT "id", "user_name" FROM "users" WHERE ("user_name" ILIKE $1) ORDER BY "created_at" DESC LIMIT 5
// Args: [%john%]
}