first commit

This commit is contained in:
2026-02-13 15:57:28 +07:00
parent d4248b62e1
commit 7ae8c76a20
26 changed files with 5040 additions and 32 deletions
+149 -2
View File
@@ -61,6 +61,8 @@ const (
OpArrayContains FilterOperator = "_array_contains"
OpArrayNotContains FilterOperator = "_array_ncontains"
OpArrayLength FilterOperator = "_array_length"
OpArrayOverlap FilterOperator = "_array_overlap"
OpArrayContainedBy FilterOperator = "_array_contained_by"
)
// DynamicFilter represents a single filter condition
@@ -627,6 +629,19 @@ func (qb *QueryBuilder) buildCTEClause(ctes []CTE) (string, []interface{}, error
// buildFromClause builds the FROM clause with optional alias
func (qb *QueryBuilder) buildFromClause(table, alias string) string {
// Check if the table name contains a dot (schema.table)
if strings.Contains(table, ".") {
parts := strings.Split(table, ".")
if len(parts) == 2 {
// Quote schema and table separately
fromClause := fmt.Sprintf("%s.%s", qb.escapeIdentifier(parts[0]), qb.escapeIdentifier(parts[1]))
if alias != "" {
fromClause += " " + qb.escapeIdentifier(alias)
}
return fromClause
}
}
fromClause := qb.escapeIdentifier(table)
if alias != "" {
fromClause += " " + qb.escapeIdentifier(alias)
@@ -641,7 +656,18 @@ func (qb *QueryBuilder) buildSingleJoinClause(join Join) (string, string, string
joinType = "INNER"
}
table := qb.escapeIdentifier(join.Table)
var table string
if strings.Contains(join.Table, ".") {
parts := strings.Split(join.Table, ".")
if len(parts) == 2 {
// Quote schema and table separately
table = fmt.Sprintf("%s.%s", qb.escapeIdentifier(parts[0]), qb.escapeIdentifier(parts[1]))
} else {
table = qb.escapeIdentifier(join.Table)
}
} else {
table = qb.escapeIdentifier(join.Table)
}
if join.Alias != "" {
table += " " + qb.escapeIdentifier(join.Alias)
}
@@ -803,7 +829,7 @@ func (qb *QueryBuilder) buildFilterCondition(filter DynamicFilter) (string, []in
switch filter.Operator {
case OpJsonContains, OpJsonNotContains, OpJsonExists, OpJsonNotExists, OpJsonEqual, OpJsonNotEqual:
return qb.buildJsonFilterCondition(filter)
case OpArrayContains, OpArrayNotContains, OpArrayLength:
case OpArrayContains, OpArrayNotContains, OpArrayLength, OpArrayOverlap, OpArrayContainedBy:
return qb.buildArrayFilterCondition(filter)
}
@@ -1051,6 +1077,45 @@ func (qb *QueryBuilder) buildArrayFilterCondition(filter DynamicFilter) (string,
default:
return "", nil, fmt.Errorf("Array operations not supported for database type: %s", qb.dbType)
}
case OpArrayOverlap:
// TAMBAHKAN INI
switch qb.dbType {
case DBTypePostgreSQL:
expr = fmt.Sprintf("%s && ?", column)
args = append(args, filter.Value)
case DBTypeMySQL:
// MySQL doesn't have native array overlap, use JSON_OVERLAPS if available
expr = fmt.Sprintf("JSON_OVERLAPS(%s, ?)", column)
args = append(args, filter.Value)
case DBTypeSQLServer:
// SQL Server workaround using EXISTS and OPENJSON
expr = fmt.Sprintf("EXISTS (SELECT 1 FROM OPENJSON(%s) o1 CROSS JOIN OPENJSON(?) o2 WHERE o1.value = o2.value)", column)
args = append(args, filter.Value)
case DBTypeSQLite:
// SQLite workaround using json_each
expr = fmt.Sprintf("EXISTS (SELECT 1 FROM json_each(%s) j1 CROSS JOIN json_each(?) j2 WHERE j1.value = j2.value)", column)
args = append(args, filter.Value)
default:
return "", nil, fmt.Errorf("Array overlap operations not supported for database type: %s", qb.dbType)
}
case OpArrayContainedBy:
// TAMBAHKAN INI
switch qb.dbType {
case DBTypePostgreSQL:
expr = fmt.Sprintf("%s <@ ?", column)
args = append(args, filter.Value)
case DBTypeMySQL:
expr = fmt.Sprintf("JSON_CONTAINS(?, %s)", column)
args = append(args, filter.Value)
case DBTypeSQLServer:
expr = fmt.Sprintf("NOT EXISTS (SELECT 1 FROM OPENJSON(%s) WHERE value NOT IN (SELECT value FROM OPENJSON(?)))", column)
args = append(args, filter.Value)
case DBTypeSQLite:
expr = fmt.Sprintf("NOT EXISTS (SELECT 1 FROM json_each(%s) j1 WHERE j1.value NOT IN (SELECT j2.value FROM json_each(?) j2))", column)
args = append(args, filter.Value)
default:
return "", nil, fmt.Errorf("Array contained_by operations not supported for database type: %s", qb.dbType)
}
case OpArrayNotContains:
switch qb.dbType {
case DBTypePostgreSQL:
@@ -1114,11 +1179,54 @@ func (qb *QueryBuilder) buildArrayFilterCondition(filter DynamicFilter) (string,
// =============================================================================
func (qb *QueryBuilder) ExecuteQuery(ctx context.Context, db *sqlx.DB, query DynamicQuery, dest interface{}) error {
// sql, args, err := qb.BuildQuery(query)
// if err != nil {
// return err
// }
// start := time.Now()
// err = db.SelectContext(ctx, dest, sql, args...)
// fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
// return err
sql, args, err := qb.BuildQuery(query)
if err != nil {
return err
}
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 := db.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)))
}
fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
return nil
}
}
// Default case: use SelectContext
err = db.SelectContext(ctx, dest, sql, args...)
fmt.Printf("[DEBUG] Query executed in %v\n", time.Since(start))
return err
@@ -2345,3 +2453,42 @@ func (mqb *MongoQueryBuilder) ExecuteDelete(ctx context.Context, collection *mon
fmt.Printf("[DEBUG] MongoDB Delete executed in %v\n", time.Since(start))
return result, err
}
func (qb *QueryBuilder) getOperatorSQL(op FilterOperator) (string, error) {
switch op {
case OpEqual:
return "=", nil
case OpNotEqual:
return "!=", nil
case OpLike:
return "LIKE", nil
case OpILike:
return "ILIKE", nil
case OpIn:
return "IN", nil
case OpNotIn:
return "NOT IN", nil
case OpGreaterThan:
return ">", nil
case OpGreaterThanEqual:
return ">=", nil
case OpLessThan:
return "<", nil
case OpLessThanEqual:
return "<=", nil
case OpNull:
return "IS NULL", nil
case OpNotNull:
return "IS NOT NULL", nil
case OpArrayContains:
return "@>", nil
case OpArrayNotContains:
return "NOT @>", nil
case OpArrayOverlap: // TAMBAHKAN INI
return "&&", nil
case OpArrayContainedBy: // BONUS
return "<@", nil
default:
return "", fmt.Errorf("unsupported operator: %s", op)
}
}