diff --git a/cel2sql.go b/cel2sql.go index 07246fb..5827061 100644 --- a/cel2sql.go +++ b/cel2sql.go @@ -8,6 +8,7 @@ import ( "log/slog" "math" "regexp" + "slices" "strconv" "strings" "time" @@ -172,8 +173,8 @@ func WithMaxOutputLength(maxLength int) ConvertOption { // Result represents the output of a CEL to SQL conversion with parameterized queries. // It contains the SQL string with placeholders ($1, $2, etc.) and the corresponding parameter values. type Result struct { - SQL string // The generated SQL WHERE clause with placeholders - Parameters []interface{} // Parameter values in order ($1, $2, etc.) + SQL string // The generated SQL WHERE clause with placeholders + Parameters []any // Parameter values in order ($1, $2, etc.) } // Convert converts a CEL AST to a PostgreSQL SQL WHERE clause condition. @@ -312,13 +313,13 @@ type converter struct { schemas map[string]pg.Schema ctx context.Context logger *slog.Logger - depth int // Current recursion depth - maxDepth int // Maximum allowed recursion depth - maxOutputLen int // Maximum allowed SQL output length - comprehensionDepth int // Current comprehension nesting depth - parameterize bool // Enable parameterized output - parameters []interface{} // Collected parameters for parameterized queries - paramCount int // Parameter counter for placeholders ($1, $2, etc.) + depth int // Current recursion depth + maxDepth int // Maximum allowed recursion depth + maxOutputLen int // Maximum allowed SQL output length + comprehensionDepth int // Current comprehension nesting depth + parameterize bool // Enable parameterized output + parameters []any // Collected parameters for parameterized queries + paramCount int // Parameter counter for placeholders ($1, $2, etc.) } // checkContext checks if the context has been cancelled or expired. @@ -1293,10 +1294,7 @@ func (con *converter) callSubstring(target *exprpb.Expr, args []*exprpb.Expr) er if endConst := endExpr.GetConstExpr(); endConst != nil { start := startConst.GetInt64Value() end := endConst.GetInt64Value() - length := end - start - if length < 0 { - length = 0 - } + length := max(end-start, 0) con.str.WriteString(strconv.FormatInt(length, 10)) } else { // End is dynamic, start is constant @@ -2524,10 +2522,8 @@ func (con *converter) isDirectJSONFieldAccess(operand *exprpb.Expr, _ string) bo // Check if the parent field is a known JSON column jsonFields := []string{"metadata", "properties", "content", "structure", "taxonomy", "analytics", "classification"} - for _, jsonField := range jsonFields { - if parentField == jsonField { - return true - } + if slices.Contains(jsonFields, parentField) { + return true } } diff --git a/errors.go b/errors.go index 0aa1e79..92115f3 100644 --- a/errors.go +++ b/errors.go @@ -105,7 +105,7 @@ func newConversionError(userMsg string, internalDetails string) *ConversionError } // newConversionErrorf creates a ConversionError with formatted internal details -func newConversionErrorf(userMsg string, internalFormat string, args ...interface{}) *ConversionError { +func newConversionErrorf(userMsg string, internalFormat string, args ...any) *ConversionError { return &ConversionError{ UserMessage: userMsg, InternalDetails: fmt.Sprintf(internalFormat, args...), diff --git a/json.go b/json.go index a98c550..5867bf8 100644 --- a/json.go +++ b/json.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log/slog" + "slices" exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1" ) @@ -85,26 +86,14 @@ func (con *converter) needsNumericCasting(identName string) bool { // Common iteration variable names that come from numeric JSON arrays numericIterationVars := []string{"score", "value", "num", "amount", "count", "level"} - for _, numericVar := range numericIterationVars { - if identName == numericVar { - return true - } - } - - return false + return slices.Contains(numericIterationVars, identName) } // isNumericJSONField checks if a JSON field name typically contains numeric values func (con *converter) isNumericJSONField(fieldName string) bool { numericFields := []string{"level", "score", "value", "count", "amount", "price", "rating", "age", "size", "capacity", "megapixels", "cores", "threads", "ram", "storage", "vram", "weight", "frequency", "helpful"} - for _, numericField := range numericFields { - if fieldName == numericField { - return true - } - } - - return false + return slices.Contains(numericFields, fieldName) } // isNestedJSONAccess checks if this is nested JSON field access like settings.permissions @@ -184,10 +173,8 @@ func (con *converter) isJSONObjectFieldAccess(expr *exprpb.Expr) bool { jsonObjectVars := []string{"attr", "item", "element", "obj", "feature", "review"} identName := identExpr.GetName() - for _, jsonVar := range jsonObjectVars { - if identName == jsonVar { - return true - } + if slices.Contains(jsonObjectVars, identName) { + return true } } } diff --git a/parameterized_integration_test.go b/parameterized_integration_test.go index 6af5395..c1a49dd 100644 --- a/parameterized_integration_test.go +++ b/parameterized_integration_test.go @@ -1023,7 +1023,7 @@ func TestParameterizedQueryPerformance(t *testing.T) { const iterations = 100 var totalDuration time.Duration - for i := 0; i < iterations; i++ { + for range iterations { start := time.Now() var count int err := db.QueryRow(query, result.Parameters...).Scan(&count) diff --git a/parameterized_test.go b/parameterized_test.go index b6e0f41..1cb2768 100644 --- a/parameterized_test.go +++ b/parameterized_test.go @@ -30,7 +30,7 @@ func TestConvertParameterized(t *testing.T) { celExpr string wantSQL string wantParamCount int - wantParams []interface{} + wantParams []any }{ // String parameters { @@ -38,21 +38,21 @@ func TestConvertParameterized(t *testing.T) { celExpr: `name == "John"`, wantSQL: "name = $1", wantParamCount: 1, - wantParams: []interface{}{"John"}, + wantParams: []any{"John"}, }, { name: "multiple string parameters", celExpr: `name == "John" && name != "Jane"`, wantSQL: "name = $1 AND name != $2", wantParamCount: 2, - wantParams: []interface{}{"John", "Jane"}, + wantParams: []any{"John", "Jane"}, }, { name: "string with escaped quotes", celExpr: `name == "O'Brien"`, wantSQL: "name = $1", wantParamCount: 1, - wantParams: []interface{}{"O'Brien"}, + wantParams: []any{"O'Brien"}, }, // Integer parameters @@ -61,21 +61,21 @@ func TestConvertParameterized(t *testing.T) { celExpr: `age == 18`, wantSQL: "age = $1", wantParamCount: 1, - wantParams: []interface{}{int64(18)}, + wantParams: []any{int64(18)}, }, { name: "integer comparison", celExpr: `age > 21 && age < 65`, wantSQL: "age > $1 AND age < $2", wantParamCount: 2, - wantParams: []interface{}{int64(21), int64(65)}, + wantParams: []any{int64(21), int64(65)}, }, { name: "negative integer", celExpr: `age == -5`, wantSQL: "age = $1", wantParamCount: 1, - wantParams: []interface{}{int64(-5)}, + wantParams: []any{int64(-5)}, }, // Double parameters @@ -84,14 +84,14 @@ func TestConvertParameterized(t *testing.T) { celExpr: `salary == 50000.50`, wantSQL: "salary = $1", wantParamCount: 1, - wantParams: []interface{}{50000.50}, + wantParams: []any{50000.50}, }, { name: "double comparison", celExpr: `salary >= 30000.0 && salary <= 100000.0`, wantSQL: "salary >= $1 AND salary <= $2", wantParamCount: 2, - wantParams: []interface{}{30000.0, 100000.0}, + wantParams: []any{30000.0, 100000.0}, }, // Boolean and NULL constants (kept inline) @@ -114,7 +114,7 @@ func TestConvertParameterized(t *testing.T) { celExpr: `active == true && age == 18`, wantSQL: "active IS TRUE AND age = $1", wantParamCount: 1, - wantParams: []interface{}{int64(18)}, + wantParams: []any{int64(18)}, }, // Bytes parameters @@ -123,7 +123,7 @@ func TestConvertParameterized(t *testing.T) { celExpr: `data == b"hello"`, wantSQL: "data = $1", wantParamCount: 1, - wantParams: []interface{}{[]byte("hello")}, + wantParams: []any{[]byte("hello")}, }, // Complex expressions with multiple parameters @@ -132,21 +132,21 @@ func TestConvertParameterized(t *testing.T) { celExpr: `name == "John" && age >= 18 && salary > 50000.0`, wantSQL: "name = $1 AND age >= $2 AND salary > $3", wantParamCount: 3, - wantParams: []interface{}{"John", int64(18), 50000.0}, + wantParams: []any{"John", int64(18), 50000.0}, }, { name: "complex OR expression", celExpr: `name == "John" || name == "Jane" || age == 25`, wantSQL: "name = $1 OR name = $2 OR age = $3", wantParamCount: 3, - wantParams: []interface{}{"John", "Jane", int64(25)}, + wantParams: []any{"John", "Jane", int64(25)}, }, { name: "nested parentheses with params", celExpr: `(name == "John" && age == 18) || (name == "Jane" && age == 21)`, wantSQL: "name = $1 AND age = $2 OR name = $3 AND age = $4", wantParamCount: 4, - wantParams: []interface{}{"John", int64(18), "Jane", int64(21)}, + wantParams: []any{"John", int64(18), "Jane", int64(21)}, }, // Parameter ordering test @@ -155,7 +155,7 @@ func TestConvertParameterized(t *testing.T) { celExpr: `name == "First" && age == 1 && salary == 100.0 && name != "Second"`, wantSQL: "name = $1 AND age = $2 AND salary = $3 AND name != $4", wantParamCount: 4, - wantParams: []interface{}{"First", int64(1), 100.0, "Second"}, + wantParams: []any{"First", int64(1), 100.0, "Second"}, }, // Empty parameter list @@ -188,7 +188,7 @@ func TestConvertParameterized(t *testing.T) { celExpr: `name.contains("oh")`, wantSQL: "POSITION($1 IN name) > 0", wantParamCount: 1, - wantParams: []interface{}{"oh"}, + wantParams: []any{"oh"}, }, // IN operator with parameters @@ -197,14 +197,14 @@ func TestConvertParameterized(t *testing.T) { celExpr: `age in [18, 21, 25]`, wantSQL: "age = ANY(ARRAY[$1, $2, $3])", wantParamCount: 3, - wantParams: []interface{}{int64(18), int64(21), int64(25)}, + wantParams: []any{int64(18), int64(21), int64(25)}, }, { name: "string IN with array literal", celExpr: `name in ["John", "Jane", "Bob"]`, wantSQL: "name = ANY(ARRAY[$1, $2, $3])", wantParamCount: 3, - wantParams: []interface{}{"John", "Jane", "Bob"}, + wantParams: []any{"John", "Jane", "Bob"}, }, // Ternary operator with parameters @@ -213,7 +213,7 @@ func TestConvertParameterized(t *testing.T) { celExpr: `age > 18 ? "adult" : "minor"`, wantSQL: "CASE WHEN age > $1 THEN $2 ELSE $3 END", wantParamCount: 3, - wantParams: []interface{}{int64(18), "adult", "minor"}, + wantParams: []any{int64(18), "adult", "minor"}, }, // Type casting with parameters @@ -222,7 +222,7 @@ func TestConvertParameterized(t *testing.T) { celExpr: `string(age) == "18"`, wantSQL: "CAST(age AS TEXT) = $1", wantParamCount: 1, - wantParams: []interface{}{"18"}, + wantParams: []any{"18"}, }, } @@ -273,28 +273,28 @@ func TestConvertParameterized_JSONFields(t *testing.T) { celExpr string wantSQL string wantParamCount int - wantParams []interface{} + wantParams []any }{ { name: "JSON field comparison with parameter", celExpr: `usr.metadata.username == "john_doe"`, wantSQL: "usr.metadata->>'username' = $1", wantParamCount: 1, - wantParams: []interface{}{"john_doe"}, + wantParams: []any{"john_doe"}, }, { name: "nested JSON field comparison", celExpr: `usr.metadata.settings.theme == "dark"`, wantSQL: "usr.metadata->'settings'->>'theme' = $1", wantParamCount: 1, - wantParams: []interface{}{"dark"}, + wantParams: []any{"dark"}, }, { name: "JSON and regular field with parameters", celExpr: `usr.name == "John" && usr.metadata.age == "25"`, wantSQL: "usr.name = $1 AND usr.metadata->>'age' = $2", wantParamCount: 2, - wantParams: []interface{}{"John", "25"}, + wantParams: []any{"John", "25"}, }, } @@ -339,35 +339,35 @@ func TestConvertParameterized_Comprehensions(t *testing.T) { celExpr string wantSQL string wantParamCount int - wantParams []interface{} + wantParams []any }{ { name: "all() with parameterized predicate", celExpr: `scores.all(x, x > 50)`, wantSQL: "NOT EXISTS (SELECT 1 FROM UNNEST(scores) AS x WHERE NOT (x > $1))", wantParamCount: 1, - wantParams: []interface{}{int64(50)}, + wantParams: []any{int64(50)}, }, { name: "exists() with parameterized predicate", celExpr: `scores.exists(x, x == 100)`, wantSQL: "EXISTS (SELECT 1 FROM UNNEST(scores) AS x WHERE x = $1)", wantParamCount: 1, - wantParams: []interface{}{int64(100)}, + wantParams: []any{int64(100)}, }, { name: "exists_one() with parameterized predicate", celExpr: `scores.exists_one(x, x == 42)`, wantSQL: "(SELECT COUNT(*) FROM UNNEST(scores) AS x WHERE x = $1) = 1", wantParamCount: 1, - wantParams: []interface{}{int64(42)}, + wantParams: []any{int64(42)}, }, { name: "map() with parameterized transform", celExpr: `scores.map(x, x + 10).exists(y, y == 110)`, wantSQL: "EXISTS (SELECT 1 FROM UNNEST(ARRAY(SELECT x + $1 FROM UNNEST(scores) AS x)) AS y WHERE y = $2)", wantParamCount: 2, - wantParams: []interface{}{int64(10), int64(110)}, + wantParams: []any{int64(10), int64(110)}, }, } diff --git a/pg/provider_test.go b/pg/provider_test.go index 2a746a8..da0f8cb 100644 --- a/pg/provider_test.go +++ b/pg/provider_test.go @@ -348,7 +348,7 @@ func Test_typeProvider_PostgreSQLTypes(t *testing.T) { func BenchmarkFieldLookup_Small(b *testing.B) { // 10 fields - small schema fields := make([]pg.FieldSchema, 10) - for i := 0; i < 10; i++ { + for i := range 10 { fields[i] = pg.FieldSchema{ Name: "field_" + string(rune('a'+i)), Type: "text", @@ -369,7 +369,7 @@ func BenchmarkFieldLookup_Small(b *testing.B) { func BenchmarkFieldLookup_Medium(b *testing.B) { // 100 fields - medium schema fields := make([]pg.FieldSchema, 100) - for i := 0; i < 100; i++ { + for i := range 100 { fields[i] = pg.FieldSchema{ Name: "field_" + string(rune('0'+i%10)) + string(rune('0'+i/10)), Type: "text", @@ -390,7 +390,7 @@ func BenchmarkFieldLookup_Medium(b *testing.B) { func BenchmarkFieldLookup_Large(b *testing.B) { // 1000 fields - large schema (real-world worst case) fields := make([]pg.FieldSchema, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { fields[i] = pg.FieldSchema{ Name: "field_" + string(rune('0'+i%10)) + string(rune('0'+(i/10)%10)) + string(rune('0'+i/100)), Type: "text", @@ -410,7 +410,7 @@ func BenchmarkFieldLookup_Large(b *testing.B) { func BenchmarkFieldNames_Small(b *testing.B) { fields := make([]pg.FieldSchema, 10) - for i := 0; i < 10; i++ { + for i := range 10 { fields[i] = pg.FieldSchema{ Name: "field_" + string(rune('a'+i)), Type: "text", @@ -429,7 +429,7 @@ func BenchmarkFieldNames_Small(b *testing.B) { func BenchmarkFieldNames_Large(b *testing.B) { fields := make([]pg.FieldSchema, 1000) - for i := 0; i < 1000; i++ { + for i := range 1000 { fields[i] = pg.FieldSchema{ Name: "field_" + string(rune('0'+i%10)) + string(rune('0'+(i/10)%10)) + string(rune('0'+i/100)), Type: "text", diff --git a/pg/provider_testcontainer_test.go b/pg/provider_testcontainer_test.go index 7e02c6f..558589f 100644 --- a/pg/provider_testcontainer_test.go +++ b/pg/provider_testcontainer_test.go @@ -923,7 +923,7 @@ func TestLoadTableSchema_JsonComprehensions(t *testing.T) { for rows.Next() { var id int var name string - var tags, scores interface{} + var tags, scores any err = rows.Scan(&id, &name, &tags, &scores) require.NoError(t, err) t.Logf("User %d: %s, tags: %v (type: %T), scores: %v (type: %T)", id, name, tags, tags, scores, scores) diff --git a/recursion_depth_test.go b/recursion_depth_test.go index a69a866..63fff18 100644 --- a/recursion_depth_test.go +++ b/recursion_depth_test.go @@ -169,7 +169,7 @@ func TestMaxDepthWithOtherOptions(t *testing.T) { func TestRecursionDepthErrorMessage(t *testing.T) { // Verify error message format expr := "x" - for i := 0; i < 150; i++ { + for range 150 { expr = "(" + expr + " + 1)" } @@ -207,7 +207,7 @@ func TestDeeplyNestedExpressions(t *testing.T) { name: "nested AND conditions", buildExpr: func() string { expr := "x > 0" - for i := 0; i < 60; i++ { + for range 60 { expr = "(" + expr + " && x < 100)" } return expr @@ -219,7 +219,7 @@ func TestDeeplyNestedExpressions(t *testing.T) { name: "nested OR conditions", buildExpr: func() string { expr := "x == 1" - for i := 0; i < 60; i++ { + for range 60 { expr = "(" + expr + " || x == 2)" } return expr @@ -231,7 +231,7 @@ func TestDeeplyNestedExpressions(t *testing.T) { name: "nested function calls", buildExpr: func() string { expr := "x" - for i := 0; i < 60; i++ { + for range 60 { expr = "int(" + expr + ")" } return expr + " > 0" @@ -243,7 +243,7 @@ func TestDeeplyNestedExpressions(t *testing.T) { name: "deeply nested ternary", buildExpr: func() string { expr := "x" - for i := 0; i < 25; i++ { + for range 25 { expr = "(" + expr + " > 0 ? 1 : 0)" } return expr + " == 1" @@ -255,7 +255,7 @@ func TestDeeplyNestedExpressions(t *testing.T) { name: "exceeds limit nested arithmetic", buildExpr: func() string { expr := "x" - for i := 0; i < 150; i++ { + for range 150 { expr = "(" + expr + " + 1)" } return expr + " > 0" @@ -310,7 +310,7 @@ func TestDepthResetBetweenCalls(t *testing.T) { // Build an expression at 75% of default limit expr := "x" - for i := 0; i < 75; i++ { + for range 75 { expr = "(" + expr + " + 1)" }