1190 lines
38 KiB
Go
1190 lines
38 KiB
Go
package query
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/Masterminds/squirrel"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// SQLQueryBuilder implements QueryBuilder interface for SQL databases
|
|
type SQLQueryBuilder struct {
|
|
dbType DBType
|
|
dialect DatabaseDialect
|
|
sqlBuilder squirrel.StatementBuilderType
|
|
allowedColumns map[string]bool // Security: only allow specified columns
|
|
allowedTables map[string]bool // Security: only allow specified tables
|
|
// Security settings
|
|
enableSecurityChecks bool
|
|
maxAllowedRows int
|
|
// Query logging
|
|
enableQueryLogging bool
|
|
logger Logger
|
|
// Connection timeout settings
|
|
queryTimeout time.Duration
|
|
}
|
|
|
|
// Logger interface for query logging
|
|
type Logger interface {
|
|
Debugf(format string, args ...interface{})
|
|
}
|
|
|
|
// NewSQLQueryBuilder creates a new SQL query builder instance for a specific database type
|
|
func NewSQLQueryBuilder(dbType DBType) *SQLQueryBuilder {
|
|
var placeholderFormat squirrel.PlaceholderFormat
|
|
|
|
switch dbType {
|
|
case DBTypePostgreSQL:
|
|
placeholderFormat = squirrel.Dollar
|
|
case DBTypeMySQL, DBTypeSQLite:
|
|
placeholderFormat = squirrel.Question
|
|
case DBTypeSQLServer:
|
|
placeholderFormat = squirrel.AtP
|
|
default:
|
|
placeholderFormat = squirrel.Question
|
|
}
|
|
|
|
dialect := GetDialect(dbType)
|
|
|
|
return &SQLQueryBuilder{
|
|
dbType: dbType,
|
|
dialect: dialect,
|
|
sqlBuilder: squirrel.StatementBuilder.PlaceholderFormat(placeholderFormat),
|
|
allowedColumns: make(map[string]bool),
|
|
allowedTables: make(map[string]bool),
|
|
enableSecurityChecks: true,
|
|
maxAllowedRows: 10000,
|
|
enableQueryLogging: true,
|
|
queryTimeout: 30 * time.Second,
|
|
}
|
|
}
|
|
|
|
// SetLogger sets the logger for query logging
|
|
func (qb *SQLQueryBuilder) SetLogger(logger Logger) QueryBuilder {
|
|
qb.logger = logger
|
|
return qb
|
|
}
|
|
|
|
// SetSecurityOptions configures security settings
|
|
func (qb *SQLQueryBuilder) SetSecurityOptions(enableChecks bool, maxRows int) QueryBuilder {
|
|
qb.enableSecurityChecks = enableChecks
|
|
qb.maxAllowedRows = maxRows
|
|
return qb
|
|
}
|
|
|
|
// SetAllowedColumns sets the list of allowed columns for security
|
|
func (qb *SQLQueryBuilder) SetAllowedColumns(columns []string) QueryBuilder {
|
|
qb.allowedColumns = make(map[string]bool)
|
|
for _, col := range columns {
|
|
qb.allowedColumns[col] = true
|
|
}
|
|
return qb
|
|
}
|
|
|
|
// SetAllowedTables sets the list of allowed tables for security
|
|
func (qb *SQLQueryBuilder) SetAllowedTables(tables []string) QueryBuilder {
|
|
qb.allowedTables = make(map[string]bool)
|
|
for _, table := range tables {
|
|
qb.allowedTables[table] = true
|
|
}
|
|
return qb
|
|
}
|
|
|
|
// SetQueryLogging enables or disables query logging
|
|
func (qb *SQLQueryBuilder) SetQueryLogging(enable bool) QueryBuilder {
|
|
qb.enableQueryLogging = enable
|
|
return qb
|
|
}
|
|
|
|
// SetQueryTimeout sets the default query timeout
|
|
func (qb *SQLQueryBuilder) SetQueryTimeout(timeout time.Duration) QueryBuilder {
|
|
qb.queryTimeout = timeout
|
|
return qb
|
|
}
|
|
|
|
// logDebug logs debug messages if logging is enabled
|
|
func (qb *SQLQueryBuilder) logDebug(format string, args ...interface{}) {
|
|
if qb.enableQueryLogging {
|
|
if qb.logger != nil {
|
|
qb.logger.Debugf(format, args...)
|
|
} else {
|
|
// Fallback to fmt.Printf if no logger is set
|
|
fmt.Printf("[DEBUG] "+format+"\n", args...)
|
|
}
|
|
}
|
|
}
|
|
|
|
// BuildQuery builds the complete SQL SELECT query with support for CTEs, JOINs, and UNIONs
|
|
func (qb *SQLQueryBuilder) BuildQuery(query DynamicQuery) (string, []interface{}, error) {
|
|
var allArgs []interface{}
|
|
var queryParts []string
|
|
|
|
// --- Langkah 1: Validasi ---
|
|
if qb.enableSecurityChecks && query.Limit > qb.maxAllowedRows {
|
|
return "", nil, fmt.Errorf("requested limit %d exceeds maximum allowed %d", query.Limit, qb.maxAllowedRows)
|
|
}
|
|
if qb.enableSecurityChecks && len(qb.allowedTables) > 0 && !qb.allowedTables[query.From] {
|
|
return "", nil, fmt.Errorf("disallowed table: %s", query.From)
|
|
}
|
|
|
|
// --- Langkah 2: Bangun CTEs (WITH clause) ---
|
|
if len(query.CTEs) > 0 {
|
|
cteClause, cteArgs, err := qb.buildCTEClause(query.CTEs)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build CTE clause: %w", err)
|
|
}
|
|
queryParts = append(queryParts, cteClause)
|
|
allArgs = append(allArgs, cteArgs...)
|
|
}
|
|
|
|
// --- Langkah 3: Bangun Query Utama ---
|
|
// Ini adalah satu-satunya tempat kita memanggil buildMainQuery
|
|
mainQuery, err := qb.buildMainQuery(query)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build main query: %w", err)
|
|
}
|
|
|
|
// Konversi mainQuery (squirrel.SelectBuilder) ke string dan args
|
|
sql, args, err := mainQuery.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to convert main query to SQL: %w", err)
|
|
}
|
|
queryParts = append(queryParts, sql)
|
|
allArgs = append(allArgs, args...)
|
|
|
|
// --- Langkah 4: Terapkan UNIONs ---
|
|
if len(query.Unions) > 0 {
|
|
unionClause, unionArgs, err := qb.buildUnionClause(query.Unions)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build UNION clause: %w", err)
|
|
}
|
|
queryParts = append(queryParts, unionClause)
|
|
allArgs = append(allArgs, unionArgs...)
|
|
}
|
|
|
|
// --- Langkah 5: Gabungkan dan Validasi ---
|
|
finalSQL := strings.Join(queryParts, " ")
|
|
|
|
// Validasi keamanan akhir menggunakan SQL parser
|
|
if qb.enableSecurityChecks {
|
|
if err := qb.validateParsedSQL(finalSQL); err != nil {
|
|
return "", nil, fmt.Errorf("query validation failed: %w", err)
|
|
}
|
|
}
|
|
|
|
qb.logDebug("Final SQL query: %s", finalSQL)
|
|
qb.logDebug("Final query args: %v", allArgs)
|
|
|
|
return finalSQL, allArgs, nil
|
|
}
|
|
|
|
// buildMainQuery builds the main SELECT query
|
|
func (qb *SQLQueryBuilder) buildMainQuery(query DynamicQuery) (squirrel.SelectBuilder, error) {
|
|
fromClause := qb.buildFromClause(query.From, query.Aliases)
|
|
selectFields := qb.buildSelectFields(query.Fields)
|
|
|
|
// Start building the main query
|
|
mainQuery := qb.sqlBuilder.Select(selectFields...).From(fromClause)
|
|
|
|
// Add JOINs
|
|
if len(query.Joins) > 0 {
|
|
mainQuery = qb.applyJoins(mainQuery, query.Joins)
|
|
}
|
|
|
|
// Apply WHERE conditions - HANYA SEKALI
|
|
if len(query.Filters) > 0 {
|
|
// Panggil BuildWhereClause hanya satu kali
|
|
whereClause, whereArgs, err := qb.BuildWhereClause(query.Filters)
|
|
if err != nil {
|
|
return squirrel.SelectBuilder{}, fmt.Errorf("failed to build WHERE clause: %w", err)
|
|
}
|
|
// Terapkan hasilnya ke builder Squirrel hanya satu kali
|
|
mainQuery = mainQuery.Where(whereClause, whereArgs...)
|
|
}
|
|
|
|
// Apply GROUP BY
|
|
if len(query.GroupBy) > 0 {
|
|
mainQuery = mainQuery.GroupBy(qb.buildGroupByColumns(query.GroupBy)...)
|
|
}
|
|
|
|
// Apply HAVING conditions
|
|
if len(query.Having) > 0 {
|
|
havingClause, havingArgs, err := qb.BuildWhereClause(query.Having)
|
|
if err != nil {
|
|
return squirrel.SelectBuilder{}, fmt.Errorf("failed to build HAVING clause: %w", err)
|
|
}
|
|
mainQuery = mainQuery.Having(havingClause, havingArgs...)
|
|
}
|
|
|
|
// Apply ORDER BY
|
|
if len(query.Sort) > 0 {
|
|
for _, sort := range query.Sort {
|
|
column := qb.validateAndEscapeColumn(sort.Column)
|
|
if column == "" {
|
|
continue
|
|
}
|
|
order := "ASC"
|
|
if strings.ToUpper(sort.Order) == "DESC" {
|
|
order = "DESC"
|
|
}
|
|
mainQuery = mainQuery.OrderBy(fmt.Sprintf("%s %s", column, order))
|
|
}
|
|
}
|
|
|
|
// Handle window functions and JSON operations if present
|
|
if len(query.WindowFunctions) > 0 || len(query.JsonOperations) > 0 {
|
|
mainQuery = qb.applyAdvancedFeatures(mainQuery, query, selectFields, fromClause)
|
|
}
|
|
|
|
// Apply pagination with dialect-specific syntax
|
|
if query.Limit > 0 {
|
|
if qb.dbType == DBTypeSQLServer {
|
|
if len(query.Sort) == 0 {
|
|
mainQuery = mainQuery.OrderBy("(SELECT 1)")
|
|
}
|
|
mainQuery = mainQuery.Suffix(fmt.Sprintf("OFFSET %d ROWS FETCH NEXT %d ROWS ONLY", query.Offset, query.Limit))
|
|
} else {
|
|
mainQuery = mainQuery.Limit(uint64(query.Limit))
|
|
if query.Offset > 0 {
|
|
mainQuery = mainQuery.Offset(uint64(query.Offset))
|
|
}
|
|
}
|
|
} else if query.Offset > 0 && qb.dbType != DBTypeSQLServer {
|
|
mainQuery = mainQuery.Offset(uint64(query.Offset))
|
|
}
|
|
|
|
return mainQuery, nil
|
|
}
|
|
|
|
// applyJoins applies JOIN clauses to the query
|
|
func (qb *SQLQueryBuilder) applyJoins(mainQuery squirrel.SelectBuilder, joins []Join) squirrel.SelectBuilder {
|
|
for _, join := range joins {
|
|
// Security check for joined table
|
|
if qb.enableSecurityChecks && len(qb.allowedTables) > 0 && !qb.allowedTables[join.Table] {
|
|
// This should be handled at a higher level, but we'll log it
|
|
qb.logDebug("Warning: disallowed table in join: %s", join.Table)
|
|
continue
|
|
}
|
|
|
|
joinType, tableWithAlias, onClause, joinArgs, err := qb.buildSingleJoinClause(join)
|
|
if err != nil {
|
|
qb.logDebug("Warning: failed to build join clause for table %s: %v", join.Table, err)
|
|
continue
|
|
}
|
|
|
|
joinStr := tableWithAlias + " ON " + onClause
|
|
switch strings.ToUpper(joinType) {
|
|
case "LEFT":
|
|
if join.Lateral {
|
|
mainQuery = mainQuery.LeftJoin("LATERAL "+joinStr, joinArgs...)
|
|
} else {
|
|
mainQuery = mainQuery.LeftJoin(joinStr, joinArgs...)
|
|
}
|
|
case "RIGHT":
|
|
mainQuery = mainQuery.RightJoin(joinStr, joinArgs...)
|
|
case "FULL":
|
|
mainQuery = mainQuery.Join("FULL JOIN "+joinStr, joinArgs...)
|
|
default:
|
|
if join.Lateral {
|
|
mainQuery = mainQuery.Join("LATERAL "+joinStr, joinArgs...)
|
|
} else {
|
|
mainQuery = mainQuery.Join(joinStr, joinArgs...)
|
|
}
|
|
}
|
|
}
|
|
return mainQuery
|
|
}
|
|
|
|
// applyAdvancedFeatures applies window functions and JSON operations
|
|
func (qb *SQLQueryBuilder) applyAdvancedFeatures(mainQuery squirrel.SelectBuilder, query DynamicQuery, selectFields []string, fromClause string) squirrel.SelectBuilder {
|
|
// Rebuild the SELECT clause with window functions and JSON operations
|
|
var finalSelectFields []string
|
|
finalSelectFields = append(finalSelectFields, selectFields...)
|
|
|
|
// Add window functions
|
|
for _, wf := range query.WindowFunctions {
|
|
windowFunc, err := qb.buildWindowFunction(wf)
|
|
if err != nil {
|
|
qb.logDebug("Warning: failed to build window function: %v", err)
|
|
continue
|
|
}
|
|
finalSelectFields = append(finalSelectFields, windowFunc)
|
|
}
|
|
|
|
// Add JSON operations
|
|
for _, jo := range query.JsonOperations {
|
|
jsonExpr, jsonArgs, err := qb.buildJsonOperation(jo)
|
|
if err != nil {
|
|
qb.logDebug("Warning: failed to build JSON operation: %v", err)
|
|
continue
|
|
}
|
|
if jo.Alias != "" {
|
|
jsonExpr += " AS " + qb.dialect.EscapeIdentifier(jo.Alias)
|
|
}
|
|
finalSelectFields = append(finalSelectFields, jsonExpr)
|
|
// Note: JSON args would need to be handled differently in this context
|
|
_ = jsonArgs // Suppress unused warning
|
|
}
|
|
|
|
// Rebuild the query with the complete SELECT clause
|
|
return qb.sqlBuilder.Select(finalSelectFields...).From(fromClause)
|
|
}
|
|
|
|
// buildCTEClause builds the WITH clause for CTEs
|
|
func (qb *SQLQueryBuilder) buildCTEClause(ctes []CTE) (string, []interface{}, error) {
|
|
var cteParts []string
|
|
var allArgs []interface{}
|
|
|
|
for i, cte := range ctes {
|
|
// Security check for CTE table
|
|
if qb.enableSecurityChecks && len(qb.allowedTables) > 0 && !qb.allowedTables[cte.Query.From] {
|
|
return "", nil, fmt.Errorf("disallowed table in CTE: %s", cte.Query.From)
|
|
}
|
|
|
|
cteSQL, cteArgs, err := qb.BuildQuery(cte.Query)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build CTE '%s': %w", cte.Name, err)
|
|
}
|
|
|
|
ctePart := fmt.Sprintf("%s AS (%s)", qb.dialect.EscapeIdentifier(cte.Name), cteSQL)
|
|
cteParts = append(cteParts, ctePart)
|
|
allArgs = append(allArgs, cteArgs...)
|
|
|
|
if i < len(ctes)-1 {
|
|
cteParts = append(cteParts, ",")
|
|
}
|
|
}
|
|
|
|
return "WITH " + strings.Join(cteParts, " "), allArgs, nil
|
|
}
|
|
|
|
// buildUnionClause builds UNION clauses
|
|
func (qb *SQLQueryBuilder) buildUnionClause(unions []Union) (string, []interface{}, error) {
|
|
var unionParts []string
|
|
var allArgs []interface{}
|
|
|
|
for _, union := range unions {
|
|
// Security check for union table
|
|
if qb.enableSecurityChecks && len(qb.allowedTables) > 0 && !qb.allowedTables[union.Query.From] {
|
|
return "", nil, fmt.Errorf("disallowed table in union: %s", union.Query.From)
|
|
}
|
|
|
|
unionSQL, unionArgs, err := qb.BuildQuery(union.Query)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build union query: %w", err)
|
|
}
|
|
|
|
unionType := "UNION"
|
|
if union.Type != "" {
|
|
unionType = strings.ToUpper(union.Type)
|
|
}
|
|
|
|
unionParts = append(unionParts, unionType, unionSQL)
|
|
allArgs = append(allArgs, unionArgs...)
|
|
}
|
|
|
|
return strings.Join(unionParts, " "), allArgs, nil
|
|
}
|
|
|
|
// buildFromClause builds the FROM clause
|
|
func (qb *SQLQueryBuilder) buildFromClause(table, alias string) string {
|
|
fromClause := qb.dialect.EscapeIdentifier(table)
|
|
if alias != "" {
|
|
fromClause += " AS " + qb.dialect.EscapeIdentifier(alias)
|
|
}
|
|
return fromClause
|
|
}
|
|
|
|
// buildSelectFields builds the SELECT fields
|
|
func (qb *SQLQueryBuilder) buildSelectFields(fields []SelectField) []string {
|
|
if len(fields) == 0 {
|
|
return []string{"*"}
|
|
}
|
|
|
|
var selectFields []string
|
|
for _, field := range fields {
|
|
if field.Expression == "" {
|
|
continue
|
|
}
|
|
|
|
selectExpr := field.Expression
|
|
if field.Alias != "" {
|
|
selectExpr += " AS " + qb.dialect.EscapeIdentifier(field.Alias)
|
|
}
|
|
selectFields = append(selectFields, selectExpr)
|
|
}
|
|
|
|
if len(selectFields) == 0 {
|
|
return []string{"*"}
|
|
}
|
|
|
|
return selectFields
|
|
}
|
|
|
|
// buildGroupByColumns builds GROUP BY columns
|
|
func (qb *SQLQueryBuilder) buildGroupByColumns(columns []string) []string {
|
|
var groupByColumns []string
|
|
for _, col := range columns {
|
|
escapedCol := qb.validateAndEscapeColumn(col)
|
|
if escapedCol != "" {
|
|
groupByColumns = append(groupByColumns, escapedCol)
|
|
}
|
|
}
|
|
return groupByColumns
|
|
}
|
|
|
|
// buildSingleJoinClause builds a single JOIN clause
|
|
func (qb *SQLQueryBuilder) buildSingleJoinClause(join Join) (string, string, string, []interface{}, error) {
|
|
joinType := "INNER"
|
|
if join.Type != "" {
|
|
joinType = strings.ToUpper(join.Type)
|
|
}
|
|
|
|
tableWithAlias := qb.dialect.EscapeIdentifier(join.Table)
|
|
if join.Alias != "" {
|
|
tableWithAlias += " AS " + qb.dialect.EscapeIdentifier(join.Alias)
|
|
}
|
|
|
|
onClause, onArgs, err := qb.BuildWhereClause([]FilterGroup{join.OnConditions})
|
|
if err != nil {
|
|
return "", "", "", nil, err
|
|
}
|
|
|
|
return joinType, tableWithAlias, onClause, onArgs, nil
|
|
}
|
|
|
|
// buildWindowFunction builds a window function expression
|
|
func (qb *SQLQueryBuilder) buildWindowFunction(wf WindowFunction) (string, error) {
|
|
var funcExpr string
|
|
|
|
switch strings.ToUpper(wf.Function) {
|
|
case "ROW_NUMBER":
|
|
funcExpr = "ROW_NUMBER()"
|
|
case "RANK":
|
|
funcExpr = "RANK()"
|
|
case "DENSE_RANK":
|
|
funcExpr = "DENSE_RANK()"
|
|
case "LEAD":
|
|
funcExpr = "LEAD()"
|
|
case "LAG":
|
|
funcExpr = "LAG()"
|
|
default:
|
|
return "", fmt.Errorf("unsupported window function: %s", wf.Function)
|
|
}
|
|
|
|
overClause := "OVER ("
|
|
if wf.Over != "" {
|
|
overClause += wf.Over
|
|
}
|
|
if wf.OrderBy != "" {
|
|
if wf.Over != "" {
|
|
overClause += " "
|
|
}
|
|
overClause += "ORDER BY " + wf.OrderBy
|
|
}
|
|
if wf.Frame != "" {
|
|
if wf.Over != "" || wf.OrderBy != "" {
|
|
overClause += " "
|
|
}
|
|
overClause += wf.Frame
|
|
}
|
|
overClause += ")"
|
|
|
|
windowExpr := funcExpr + " " + overClause
|
|
if wf.Alias != "" {
|
|
windowExpr += " AS " + qb.dialect.EscapeIdentifier(wf.Alias)
|
|
}
|
|
|
|
return windowExpr, nil
|
|
}
|
|
|
|
// buildJsonOperation builds a JSON operation expression
|
|
func (qb *SQLQueryBuilder) buildJsonOperation(jo JsonOperation) (string, []interface{}, error) {
|
|
var expr string
|
|
var args []interface{}
|
|
|
|
switch jo.Type {
|
|
case "extract":
|
|
switch qb.dbType {
|
|
case DBTypePostgreSQL:
|
|
expr = fmt.Sprintf("%s->'%s'", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
case DBTypeMySQL:
|
|
expr = fmt.Sprintf("JSON_EXTRACT(%s, '$.%s')", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
case DBTypeSQLServer:
|
|
expr = fmt.Sprintf("JSON_VALUE(%s, '$.%s')", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
case DBTypeSQLite:
|
|
expr = fmt.Sprintf("json_extract(%s, '$.%s')", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
default:
|
|
return "", nil, fmt.Errorf("JSON operations not supported for database type: %s", qb.dbType)
|
|
}
|
|
case "exists":
|
|
switch qb.dbType {
|
|
case DBTypePostgreSQL:
|
|
expr = fmt.Sprintf("jsonb_path_exists(%s, '$.%s')", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
case DBTypeMySQL:
|
|
expr = fmt.Sprintf("JSON_CONTAINS_PATH(%s, 'one', '$.%s')", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
case DBTypeSQLServer:
|
|
expr = fmt.Sprintf("JSON_VALUE(%s, '$.%s') IS NOT NULL", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
case DBTypeSQLite:
|
|
expr = fmt.Sprintf("json_extract(%s, '$.%s') IS NOT NULL", qb.dialect.EscapeIdentifier(jo.Column), jo.Path)
|
|
default:
|
|
return "", nil, fmt.Errorf("JSON operations not supported for database type: %s", qb.dbType)
|
|
}
|
|
default:
|
|
return "", nil, fmt.Errorf("unsupported JSON operation: %s", jo.Type)
|
|
}
|
|
if jo.Alias != "" {
|
|
expr += " AS " + qb.dialect.EscapeIdentifier(jo.Alias)
|
|
}
|
|
return expr, args, nil
|
|
}
|
|
|
|
// BuildCountQuery builds a count query
|
|
func (qb *SQLQueryBuilder) BuildCountQuery(query DynamicQuery) (string, []interface{}, error) {
|
|
// For a count query, we don't need fields, joins, or unions.
|
|
// We only need FROM, WHERE, GROUP BY, HAVING.
|
|
countQuery := DynamicQuery{
|
|
From: query.From,
|
|
Aliases: query.Aliases,
|
|
Filters: query.Filters,
|
|
GroupBy: query.GroupBy,
|
|
Having: query.Having,
|
|
// Joins are important for count with filters on joined tables
|
|
Joins: query.Joins,
|
|
}
|
|
|
|
// Build the base query for the count using Squirrel's From and Join methods
|
|
fromClause := qb.buildFromClause(countQuery.From, countQuery.Aliases)
|
|
baseQuery := qb.sqlBuilder.Select("COUNT(*)").From(fromClause)
|
|
|
|
// Add JOINs using Squirrel's Join method
|
|
if len(countQuery.Joins) > 0 {
|
|
for _, join := range countQuery.Joins {
|
|
// Security check for joined table
|
|
if qb.enableSecurityChecks && len(qb.allowedTables) > 0 && !qb.allowedTables[join.Table] {
|
|
return "", nil, fmt.Errorf("disallowed table in join: %s", join.Table)
|
|
}
|
|
|
|
joinType, tableWithAlias, onClause, joinArgs, err := qb.buildSingleJoinClause(join)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
joinStr := tableWithAlias + " ON " + onClause
|
|
switch strings.ToUpper(joinType) {
|
|
case "LEFT":
|
|
baseQuery = baseQuery.LeftJoin(joinStr, joinArgs...)
|
|
case "RIGHT":
|
|
baseQuery = baseQuery.RightJoin(joinStr, joinArgs...)
|
|
case "FULL":
|
|
baseQuery = baseQuery.Join("FULL JOIN "+joinStr, joinArgs...)
|
|
default:
|
|
baseQuery = baseQuery.Join(joinStr, joinArgs...)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(countQuery.Filters) > 0 {
|
|
whereClause, whereArgs, err := qb.BuildWhereClause(countQuery.Filters)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
baseQuery = baseQuery.Where(whereClause, whereArgs...)
|
|
}
|
|
|
|
if len(countQuery.GroupBy) > 0 {
|
|
baseQuery = baseQuery.GroupBy(qb.buildGroupByColumns(countQuery.GroupBy)...)
|
|
}
|
|
|
|
if len(countQuery.Having) > 0 {
|
|
havingClause, havingArgs, err := qb.BuildWhereClause(countQuery.Having)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
baseQuery = baseQuery.Having(havingClause, havingArgs...)
|
|
}
|
|
|
|
sql, args, err := baseQuery.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build COUNT query: %w", err)
|
|
}
|
|
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] COUNT SQL query: %s\n", sql)
|
|
fmt.Printf("[DEBUG] COUNT query args: %v\n", args)
|
|
}
|
|
return sql, args, nil
|
|
}
|
|
|
|
// BuildInsertQuery builds an INSERT query
|
|
func (qb *SQLQueryBuilder) BuildInsertQuery(table string, data InsertData, returningColumns ...string) (string, []interface{}, error) {
|
|
// Validate columns
|
|
for _, col := range data.Columns {
|
|
if qb.allowedColumns != nil && !qb.allowedColumns[col] {
|
|
return "", nil, fmt.Errorf("disallowed column: %s", col)
|
|
}
|
|
}
|
|
|
|
// Escape table name for case sensitivity
|
|
escapedTable := qb.dialect.EscapeIdentifier(table)
|
|
|
|
// Escape column names for case sensitivity
|
|
escapedColumns := make([]string, len(data.Columns))
|
|
for i, col := range data.Columns {
|
|
escapedColumns[i] = qb.dialect.EscapeIdentifier(col)
|
|
}
|
|
|
|
// Start with basic insert
|
|
insert := qb.sqlBuilder.Insert(escapedTable).Columns(escapedColumns...).Values(data.Values...)
|
|
|
|
// Handle JSON values - we need to modify the insert statement
|
|
if len(data.JsonValues) > 0 {
|
|
// Create a new insert builder with all columns including JSON columns
|
|
allColumns := make([]string, len(escapedColumns))
|
|
copy(allColumns, escapedColumns)
|
|
|
|
allValues := make([]interface{}, len(data.Values))
|
|
copy(allValues, data.Values)
|
|
|
|
for col, val := range data.JsonValues {
|
|
escapedJsonCol := qb.dialect.EscapeIdentifier(col)
|
|
allColumns = append(allColumns, escapedJsonCol)
|
|
jsonVal, err := json.Marshal(val)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
allValues = append(allValues, jsonVal)
|
|
}
|
|
|
|
insert = qb.sqlBuilder.Insert(escapedTable).Columns(allColumns...).Values(allValues...)
|
|
}
|
|
|
|
if len(returningColumns) > 0 {
|
|
if qb.dbType == DBTypePostgreSQL {
|
|
// Escape returning columns
|
|
escapedReturningColumns := make([]string, len(returningColumns))
|
|
for i, col := range returningColumns {
|
|
escapedReturningColumns[i] = qb.dialect.EscapeIdentifier(col)
|
|
}
|
|
insert = insert.Suffix("RETURNING " + strings.Join(escapedReturningColumns, ", "))
|
|
} else {
|
|
return "", nil, fmt.Errorf("RETURNING not supported for database type: %s", qb.dbType)
|
|
}
|
|
}
|
|
|
|
sql, args, err := insert.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build INSERT query: %w", err)
|
|
}
|
|
|
|
return sql, args, nil
|
|
}
|
|
|
|
// BuildUpdateQuery builds an UPDATE query
|
|
func (qb *SQLQueryBuilder) BuildUpdateQuery(table string, updateData UpdateData, filters []FilterGroup, returningColumns ...string) (string, []interface{}, error) {
|
|
// Validate columns
|
|
for _, col := range updateData.Columns {
|
|
if qb.allowedColumns != nil && !qb.allowedColumns[col] {
|
|
return "", nil, fmt.Errorf("disallowed column: %s", col)
|
|
}
|
|
}
|
|
|
|
// Escape table name for case sensitivity
|
|
escapedTable := qb.dialect.EscapeIdentifier(table)
|
|
|
|
// Create set map with proper escaping
|
|
setMap := make(map[string]interface{})
|
|
for i, col := range updateData.Columns {
|
|
escapedCol := qb.dialect.EscapeIdentifier(col)
|
|
setMap[escapedCol] = updateData.Values[i]
|
|
}
|
|
|
|
// Start with basic update
|
|
update := qb.sqlBuilder.Update(escapedTable).SetMap(setMap)
|
|
|
|
// Handle JSON updates if any
|
|
if len(updateData.JsonUpdates) > 0 {
|
|
for col, jsonUpdate := range updateData.JsonUpdates {
|
|
escapedJsonCol := qb.dialect.EscapeIdentifier(col)
|
|
switch qb.dbType {
|
|
case DBTypePostgreSQL:
|
|
jsonVal, err := json.Marshal(jsonUpdate.Value)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
setMap[escapedJsonCol] = squirrel.Expr(fmt.Sprintf("jsonb_set(%s, '%s', ?)", escapedJsonCol, jsonUpdate.Path), jsonVal)
|
|
case DBTypeMySQL:
|
|
jsonVal, err := json.Marshal(jsonUpdate.Value)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
setMap[escapedJsonCol] = squirrel.Expr(fmt.Sprintf("JSON_SET(%s, '%s', ?)", escapedJsonCol, jsonUpdate.Path), jsonVal)
|
|
case DBTypeSQLServer:
|
|
jsonVal, err := json.Marshal(jsonUpdate.Value)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
setMap[escapedJsonCol] = squirrel.Expr(fmt.Sprintf("JSON_MODIFY(%s, '%s', ?)", escapedJsonCol, jsonUpdate.Path), jsonVal)
|
|
case DBTypeSQLite:
|
|
jsonVal, err := json.Marshal(jsonUpdate.Value)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
setMap[escapedJsonCol] = squirrel.Expr(fmt.Sprintf("json_patch(%s, ?)", escapedJsonCol), jsonVal)
|
|
}
|
|
}
|
|
update = qb.sqlBuilder.Update(escapedTable).SetMap(setMap)
|
|
}
|
|
|
|
if len(filters) > 0 {
|
|
whereClause, whereArgs, err := qb.BuildWhereClause(filters)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
update = update.Where(whereClause, whereArgs...)
|
|
}
|
|
|
|
if len(returningColumns) > 0 {
|
|
if qb.dbType == DBTypePostgreSQL {
|
|
escapedReturningColumns := make([]string, len(returningColumns))
|
|
for i, col := range returningColumns {
|
|
escapedReturningColumns[i] = qb.dialect.EscapeIdentifier(col)
|
|
}
|
|
update = update.Suffix("RETURNING " + strings.Join(escapedReturningColumns, ", "))
|
|
} else {
|
|
return "", nil, fmt.Errorf("RETURNING not supported for database type: %s", qb.dbType)
|
|
}
|
|
}
|
|
|
|
sql, args, err := update.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build UPDATE query: %w", err)
|
|
}
|
|
|
|
return sql, args, nil
|
|
}
|
|
|
|
// BuildDeleteQuery builds a DELETE query
|
|
func (qb *SQLQueryBuilder) BuildDeleteQuery(table string, filters []FilterGroup, returningColumns ...string) (string, []interface{}, error) {
|
|
// Escape table name for case sensitivity
|
|
escapedTable := qb.dialect.EscapeIdentifier(table)
|
|
|
|
delete := qb.sqlBuilder.Delete(escapedTable)
|
|
|
|
if len(filters) > 0 {
|
|
whereClause, whereArgs, err := qb.BuildWhereClause(filters)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
delete = delete.Where(whereClause, whereArgs...)
|
|
}
|
|
|
|
if len(returningColumns) > 0 {
|
|
if qb.dbType == DBTypePostgreSQL {
|
|
// Escape returning columns
|
|
escapedReturningColumns := make([]string, len(returningColumns))
|
|
for i, col := range returningColumns {
|
|
escapedReturningColumns[i] = qb.dialect.EscapeIdentifier(col)
|
|
}
|
|
delete = delete.Suffix("RETURNING " + strings.Join(escapedReturningColumns, ", "))
|
|
} else {
|
|
return "", nil, fmt.Errorf("RETURNING not supported for database type: %s", qb.dbType)
|
|
}
|
|
}
|
|
|
|
sql, args, err := delete.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build DELETE query: %w", err)
|
|
}
|
|
|
|
return sql, args, nil
|
|
}
|
|
|
|
// BuildUpsertQuery builds an UPSERT query
|
|
func (qb *SQLQueryBuilder) BuildUpsertQuery(table string, insertData InsertData, conflictColumns []string, updateColumns []string, returningColumns ...string) (string, []interface{}, error) {
|
|
// Validate columns
|
|
for _, col := range insertData.Columns {
|
|
if qb.allowedColumns != nil && !qb.allowedColumns[col] {
|
|
return "", nil, fmt.Errorf("disallowed column: %s", col)
|
|
}
|
|
}
|
|
for _, col := range updateColumns {
|
|
if qb.allowedColumns != nil && !qb.allowedColumns[col] {
|
|
return "", nil, fmt.Errorf("disallowed column: %s", col)
|
|
}
|
|
}
|
|
|
|
// Escape table name for case sensitivity
|
|
escapedTable := qb.dialect.EscapeIdentifier(table)
|
|
|
|
switch qb.dbType {
|
|
case DBTypePostgreSQL:
|
|
// Handle JSON values for PostgreSQL
|
|
allColumns := make([]string, len(insertData.Columns))
|
|
copy(allColumns, insertData.Columns)
|
|
|
|
allValues := make([]interface{}, len(insertData.Values))
|
|
copy(allValues, insertData.Values)
|
|
|
|
for col, val := range insertData.JsonValues {
|
|
allColumns = append(allColumns, col)
|
|
jsonVal, err := json.Marshal(val)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
allValues = append(allValues, jsonVal)
|
|
}
|
|
|
|
insert := qb.sqlBuilder.Insert(escapedTable).Columns(allColumns...).Values(allValues...)
|
|
if len(conflictColumns) > 0 {
|
|
conflictTarget := strings.Join(conflictColumns, ", ")
|
|
setClause := ""
|
|
for _, col := range updateColumns {
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += fmt.Sprintf("%s = EXCLUDED.%s", qb.dialect.EscapeIdentifier(col), qb.dialect.EscapeIdentifier(col))
|
|
}
|
|
insert = insert.Suffix(fmt.Sprintf("ON CONFLICT (%s) DO UPDATE SET %s", conflictTarget, setClause))
|
|
}
|
|
if len(returningColumns) > 0 {
|
|
insert = insert.Suffix("RETURNING " + strings.Join(returningColumns, ", "))
|
|
}
|
|
sql, args, err := insert.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build UPSERT query: %w", err)
|
|
}
|
|
return sql, args, nil
|
|
case DBTypeMySQL:
|
|
// Handle JSON values for MySQL
|
|
allColumns := make([]string, len(insertData.Columns))
|
|
copy(allColumns, insertData.Columns)
|
|
|
|
allValues := make([]interface{}, len(insertData.Values))
|
|
copy(allValues, insertData.Values)
|
|
|
|
for col, val := range insertData.JsonValues {
|
|
allColumns = append(allColumns, col)
|
|
jsonVal, err := json.Marshal(val)
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to marshal JSON value for column %s: %w", col, err)
|
|
}
|
|
allValues = append(allValues, jsonVal)
|
|
}
|
|
|
|
insert := qb.sqlBuilder.Insert(table).Columns(allColumns...).Values(allValues...)
|
|
if len(updateColumns) > 0 {
|
|
setClause := ""
|
|
for _, col := range updateColumns {
|
|
if setClause != "" {
|
|
setClause += ", "
|
|
}
|
|
setClause += fmt.Sprintf("%s = VALUES(%s)", qb.dialect.EscapeIdentifier(col), qb.dialect.EscapeIdentifier(col))
|
|
}
|
|
insert = insert.Suffix(fmt.Sprintf("ON DUPLICATE KEY UPDATE %s", setClause))
|
|
}
|
|
sql, args, err := insert.ToSql()
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("failed to build UPSERT query: %w", err)
|
|
}
|
|
return sql, args, nil
|
|
default:
|
|
return "", nil, fmt.Errorf("UPSERT not supported for database type: %s", qb.dbType)
|
|
}
|
|
}
|
|
|
|
// BuildWhereClause builds WHERE/HAVING conditions from FilterGroups
|
|
func (qb *SQLQueryBuilder) BuildWhereClause(filterGroups []FilterGroup) (string, []interface{}, error) {
|
|
if len(filterGroups) == 0 {
|
|
// Jika tidak ada filter, kembalikan string kosong dan args kosong.
|
|
return "", nil, nil
|
|
}
|
|
|
|
// Slice untuk menampung semua string kondisi dari setiap group.
|
|
var groupConditions []string
|
|
// Slice untuk menampung semua argumen dari semua filter.
|
|
var allArgs []interface{}
|
|
|
|
// Iterasi setiap FilterGroup yang diberikan.
|
|
// Contoh filterGroups: [ {Filters: [filter1, filter2], LogicOp: "AND"}, {Filters: [filter3], LogicOp: "OR"} ]
|
|
for i, group := range filterGroups {
|
|
if len(group.Filters) == 0 {
|
|
// Lewati group yang tidak memiliki filter sama sekali.
|
|
qb.logDebug("Skipping empty filter group at index %d", i)
|
|
continue
|
|
}
|
|
|
|
// Bangun string kondisi untuk group saat ini (misal: "col1 = ? AND col2 LIKE ?")
|
|
// dan kumpulkan argumennya.
|
|
groupCondition, groupArgs, err := qb.buildFilterGroup(group)
|
|
if err != nil {
|
|
// Jika terjadi error saat membangun group, hentikan proses dan kembalikan error.
|
|
return "", nil, fmt.Errorf("failed to build filter group at index %d: %w", i, err)
|
|
}
|
|
|
|
// Tambahkan kondisi group yang sudah dibuat ke daftar.
|
|
// Kita bungkus dengan parentheses untuk memastikan urutan operasi yang benar.
|
|
groupConditions = append(groupConditions, fmt.Sprintf("(%s)", groupCondition))
|
|
// Tambahkan argumen dari group ini ke daftar argumen utama.
|
|
allArgs = append(allArgs, groupArgs...)
|
|
}
|
|
|
|
// Gabungkan semua kondisi group menjadi satu string WHERE.
|
|
// Kita gunakan "AND" sebagai operator default antar group utama.
|
|
// Contoh hasil: ("id" = $1 AND "name" LIKE $2) OR ("status" = $3)
|
|
whereClause := strings.Join(groupConditions, " AND ")
|
|
|
|
qb.logDebug("Final WHERE clause built: %s", whereClause)
|
|
qb.logDebug("Final WHERE args: %v", allArgs)
|
|
|
|
return whereClause, allArgs, nil
|
|
}
|
|
|
|
// buildFilterGroup membangun satu string kondisi dari sebuah FilterGroup.
|
|
// Ini adalah pembantu untuk BuildWhereClause.
|
|
func (qb *SQLQueryBuilder) buildFilterGroup(group FilterGroup) (string, []interface{}, error) {
|
|
var conditions []string
|
|
var args []interface{}
|
|
|
|
// Tentukan operator logika untuk group ini. Defaultnya adalah "AND".
|
|
logicOp := "AND"
|
|
if group.LogicOp != "" {
|
|
logicOp = strings.ToUpper(group.LogicOp)
|
|
}
|
|
|
|
// Iterasi setiap DynamicFilter dalam group.
|
|
// Contoh group.Filters: [ {Column: "id", Operator: "_eq", Value: 5}, {Column: "name", Operator: "_like", Value: "test"} ]
|
|
for i, filter := range group.Filters {
|
|
// Bangun kondisi individual untuk setiap filter (misal: "id" = $1).
|
|
condition, filterArgs, err := qb.buildFilterCondition(filter)
|
|
if err != nil {
|
|
// Jika gagal membangun kondisi filter, hentikan proses.
|
|
return "", nil, fmt.Errorf("failed to build filter condition at index %d: %w", i, err)
|
|
}
|
|
|
|
// Jika ini bukan filter pertama, tambahkan operator logika sebelum kondisi.
|
|
if len(conditions) > 0 {
|
|
conditions = append(conditions, logicOp)
|
|
}
|
|
// Tambahkan string kondisi.
|
|
conditions = append(conditions, condition)
|
|
// Tambahkan argumen.
|
|
args = append(args, filterArgs...)
|
|
}
|
|
|
|
// Gabungkan semua kondisi dalam group dengan spasi.
|
|
// Contoh hasil: "id" = $1 AND "name" LIKE $2
|
|
groupClause := strings.Join(conditions, " ")
|
|
return groupClause, args, nil
|
|
}
|
|
|
|
// ExecuteQuery executes a query with parameters and returns rows
|
|
func (qb *SQLQueryBuilder) ExecuteQuery(ctx context.Context, exec sqlx.ExtContext, query DynamicQuery, dest interface{}) error {
|
|
sql, args, err := qb.BuildQuery(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
// Check if dest is a pointer to a slice of maps
|
|
destValue := reflect.ValueOf(dest)
|
|
if destValue.Kind() != reflect.Ptr || destValue.IsNil() {
|
|
return fmt.Errorf("dest must be a non-nil pointer")
|
|
}
|
|
|
|
destElem := destValue.Elem()
|
|
if destElem.Kind() == reflect.Slice {
|
|
sliceType := destElem.Type().Elem()
|
|
if sliceType.Kind() == reflect.Map &&
|
|
sliceType.Key().Kind() == reflect.String &&
|
|
sliceType.Elem().Kind() == reflect.Interface {
|
|
|
|
// Handle slice of map[string]interface{}
|
|
rows, err := exec.QueryxContext(ctx, sql, args...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
row := make(map[string]interface{})
|
|
if err := rows.MapScan(row); err != nil {
|
|
return err
|
|
}
|
|
destElem.Set(reflect.Append(destElem, reflect.ValueOf(row)))
|
|
}
|
|
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Default case: use SelectContext
|
|
err = sqlx.SelectContext(ctx, exec, dest, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ExecuteQueryRow executes a query with parameters and returns a single row
|
|
func (qb *SQLQueryBuilder) ExecuteQueryRow(ctx context.Context, exec sqlx.ExtContext, query DynamicQuery, dest interface{}) error {
|
|
sql, args, err := qb.BuildQuery(query)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
err = sqlx.GetContext(ctx, exec, dest, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] QueryRow executed in %v\n", time.Since(start))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ExecuteCount executes a count query
|
|
func (qb *SQLQueryBuilder) ExecuteCount(ctx context.Context, exec sqlx.ExtContext, query DynamicQuery) (int64, error) {
|
|
sql, args, err := qb.BuildCountQuery(query)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
var count int64
|
|
err = sqlx.GetContext(ctx, exec, &count, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Count query executed in %v\n", time.Since(start))
|
|
}
|
|
return count, err
|
|
}
|
|
|
|
// ExecuteInsert executes an insert operation
|
|
func (qb *SQLQueryBuilder) ExecuteInsert(ctx context.Context, exec sqlx.ExtContext, table string, data InsertData, returningColumns ...string) (sql.Result, error) {
|
|
sql, args, err := qb.BuildInsertQuery(table, data, returningColumns...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
result, err := exec.ExecContext(ctx, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Insert executed in %v\n", time.Since(start))
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// ExecuteUpdate executes an update operation
|
|
func (qb *SQLQueryBuilder) ExecuteUpdate(ctx context.Context, exec sqlx.ExtContext, table string, updateData UpdateData, filters []FilterGroup, returningColumns ...string) (sql.Result, error) {
|
|
sql, args, err := qb.BuildUpdateQuery(table, updateData, filters, returningColumns...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
result, err := exec.ExecContext(ctx, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Update executed in %v\n", time.Since(start))
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// ExecuteDelete executes a delete operation
|
|
func (qb *SQLQueryBuilder) ExecuteDelete(ctx context.Context, exec sqlx.ExtContext, table string, filters []FilterGroup, returningColumns ...string) (sql.Result, error) {
|
|
sql, args, err := qb.BuildDeleteQuery(table, filters, returningColumns...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
result, err := exec.ExecContext(ctx, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Delete executed in %v\n", time.Since(start))
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// ExecuteUpsert executes an upsert operation
|
|
func (qb *SQLQueryBuilder) ExecuteUpsert(ctx context.Context, exec sqlx.ExtContext, table string, insertData InsertData, conflictColumns []string, updateColumns []string, returningColumns ...string) (sql.Result, error) {
|
|
sql, args, err := qb.BuildUpsertQuery(table, insertData, conflictColumns, updateColumns, returningColumns...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set timeout if not already in context
|
|
if _, hasDeadline := ctx.Deadline(); !hasDeadline && qb.queryTimeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, qb.queryTimeout)
|
|
defer cancel()
|
|
}
|
|
|
|
start := time.Now()
|
|
result, err := exec.ExecContext(ctx, sql, args...)
|
|
if qb.enableQueryLogging {
|
|
fmt.Printf("[DEBUG] Upsert executed in %v\n", time.Since(start))
|
|
}
|
|
return result, err
|
|
}
|
|
|
|
// buildSetMap builds a map for SetMap from UpdateData
|
|
func (qb *SQLQueryBuilder) buildSetMap(updateData UpdateData) map[string]interface{} {
|
|
setMap := make(map[string]interface{})
|
|
for i, col := range updateData.Columns {
|
|
// PERBAIKAN: Pastikan kolom yang benar di-escape
|
|
escapedCol := qb.validateAndEscapeColumn(col)
|
|
if escapedCol != "" {
|
|
setMap[escapedCol] = updateData.Values[i]
|
|
}
|
|
}
|
|
return setMap
|
|
}
|