update builder
This commit is contained in:
1611
internal/handlers/retribusi/sqruel
Normal file
1611
internal/handlers/retribusi/sqruel
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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, ¶mCounter)
|
||||
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, ¶mCounter)
|
||||
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, ¶mCounter)
|
||||
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, ¶mCounter)
|
||||
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, ¶mCounter)
|
||||
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, ¶mCounter)
|
||||
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
|
||||
}
|
||||
|
||||
1139
internal/utils/query/builder.go
Normal file
1139
internal/utils/query/builder.go
Normal file
File diff suppressed because it is too large
Load Diff
220
internal/utils/query/exemple.go
Normal file
220
internal/utils/query/exemple.go
Normal 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%]
|
||||
}
|
||||
Reference in New Issue
Block a user