@@ -145,6 +145,74 @@ func ValidateColumnName(name string) error {
145145 return nil
146146}
147147
148+ // ValidateJSONBPath validates JSONB path expressions
149+ // Supports expressions like: column->'key'->>'value', column->0->>'nested', etc.
150+ func ValidateJSONBPath (path string ) error {
151+ path = strings .TrimSpace (path )
152+
153+ if len (path ) == 0 {
154+ return fmt .Errorf ("JSONB path cannot be empty" )
155+ }
156+
157+ if len (path ) > 512 { // Allow longer paths for JSONB
158+ return fmt .Errorf ("JSONB path exceeds maximum length (512 chars): %d" , len (path ))
159+ }
160+
161+ // Check for SQL injection patterns (allow single quotes for JSONB keys, but block double quotes and dangerous SQL)
162+ dangerousPatterns := []string {";" , "--" , "/*" , "*/" , "\" " }
163+ for _ , pattern := range dangerousPatterns {
164+ if strings .Contains (path , pattern ) {
165+ return fmt .Errorf ("JSONB path contains potentially dangerous characters: %s" , path )
166+ }
167+ }
168+
169+ // Validate structure: must start with valid column name, followed by JSONB operators
170+ // Find the base column name (everything before the first ->)
171+ operatorIndex := strings .Index (path , "->" )
172+ if operatorIndex == - 1 {
173+ return fmt .Errorf ("JSONB path must contain -> or ->> operator: %s" , path )
174+ }
175+
176+ baseName := path [:operatorIndex ]
177+ if ! validColumnRegex .MatchString (baseName ) {
178+ return fmt .Errorf ("invalid base column in JSONB path: %s" , baseName )
179+ }
180+
181+ // Rest of the path after base column should contain balanced single quotes and valid JSONB operators
182+ // Check for balanced quotes
183+ singleQuoteCount := strings .Count (path , "'" )
184+ if singleQuoteCount % 2 != 0 {
185+ return fmt .Errorf ("unbalanced quotes in JSONB path: %s" , path )
186+ }
187+
188+ return nil
189+ }
190+
191+ // IsJSONBPath checks if a column reference uses JSONB operators
192+ func IsJSONBPath (name string ) bool {
193+ return strings .Contains (name , "->" ) || strings .Contains (name , "->>" )
194+ }
195+
196+ // ValidateAndFormatColumn validates a column name or JSONB path and formats it for SQL
197+ // For simple columns, it quotes the identifier; for JSONB paths, it returns the path as-is (already safe)
198+ func ValidateAndFormatColumn (name string ) (string , error ) {
199+ name = strings .TrimSpace (name )
200+
201+ // Check if it's a JSONB path expression
202+ if IsJSONBPath (name ) {
203+ if err := ValidateJSONBPath (name ); err != nil {
204+ return "" , err
205+ }
206+ return name , nil // Return JSONB path unquoted
207+ }
208+
209+ // Otherwise validate as regular column name
210+ if err := ValidateColumnName (name ); err != nil {
211+ return "" , err
212+ }
213+ return pq .QuoteIdentifier (name ), nil // Quote regular column names
214+ }
215+
148216// SplitQualifiedName splits a qualified table name on dots while respecting quoted identifiers
149217func SplitQualifiedName (qualifiedName string ) ([]string , error ) {
150218 parts := make ([]string , 0 , 2 )
@@ -234,6 +302,12 @@ var allowedOperators = map[string]bool{
234302 "lt" : true , "<" : true , "lte" : true , "<=" : true ,
235303 "like" : true , "ilike" : true , "in" : true , "not_in" : true ,
236304 "is_null" : true , "is_not_null" : true , "any" : true ,
305+ // JSONB operators
306+ "jsonb_contains" : true , // @>
307+ "jsonb_contained" : true , // <@
308+ "jsonb_has_key" : true , // ?
309+ "jsonb_has_any_key" : true , // ?|
310+ "jsonb_has_all_keys" : true , // ?&
237311}
238312
239313// ValidateOperator ensures operator is in the whitelist and safe to use
@@ -721,7 +795,11 @@ func (postgresDbService *PostgresDbService) ToInterfaceSlice(v interface{}) ([]i
721795
722796// BuildSimpleCondition builds conditions for simple operators (=, !=, <, >, etc.)
723797func (postgresDbService * PostgresDbService ) BuildSimpleCondition (filter models.QueryFilter , operator string , argCounter int ) (string , []interface {}, int ) {
724- condition := fmt .Sprintf ("%s %s $%d" , pq .QuoteIdentifier (filter .Column ), operator , argCounter )
798+ formattedColumn , err := ValidateAndFormatColumn (filter .Column )
799+ if err != nil {
800+ return "" , nil , argCounter
801+ }
802+ condition := fmt .Sprintf ("%s %s $%d" , formattedColumn , operator , argCounter )
725803 args := []interface {}{filter .Value }
726804 return condition , args , argCounter + 1
727805}
@@ -733,6 +811,11 @@ func (postgresDbService *PostgresDbService) BuildInCondition(filter models.Query
733811 return "" , nil , argCounter
734812 }
735813
814+ formattedColumn , err := ValidateAndFormatColumn (filter .Column )
815+ if err != nil {
816+ return "" , nil , argCounter
817+ }
818+
736819 placeholders := make ([]string , len (values ))
737820 args := make ([]interface {}, 0 , len (values ))
738821 for i , val := range values {
@@ -745,36 +828,151 @@ func (postgresDbService *PostgresDbService) BuildInCondition(filter models.Query
745828 if useNot {
746829 operator = "NOT IN"
747830 }
748- condition := fmt .Sprintf ("%s %s (%s)" , pq . QuoteIdentifier ( filter . Column ) , operator , strings .Join (placeholders , ", " ))
831+ condition := fmt .Sprintf ("%s %s (%s)" , formattedColumn , operator , strings .Join (placeholders , ", " ))
749832 return condition , args , argCounter
750833}
751834
752835// BuildNullCondition builds conditions for IS NULL/IS NOT NULL operators
753836func (postgresDbService * PostgresDbService ) BuildNullCondition (filter models.QueryFilter , useNot bool , argCounter int ) (string , []interface {}, int ) {
837+ formattedColumn , err := ValidateAndFormatColumn (filter .Column )
838+ if err != nil {
839+ return "" , nil , argCounter
840+ }
754841 operator := "IS NULL"
755842 if useNot {
756843 operator = "IS NOT NULL"
757844 }
758- condition := fmt .Sprintf ("%s %s" , pq . QuoteIdentifier ( filter . Column ) , operator )
845+ condition := fmt .Sprintf ("%s %s" , formattedColumn , operator )
759846 return condition , nil , argCounter
760847}
761848
762849// BuildAnyCondition builds conditions for ANY operator
763850func (postgresDbService * PostgresDbService ) BuildAnyCondition (filter models.QueryFilter , argCounter int ) (string , []interface {}, int ) {
764- condition := fmt .Sprintf ("$%d = ANY(%s)" , argCounter , pq .QuoteIdentifier (filter .Column ))
851+ formattedColumn , err := ValidateAndFormatColumn (filter .Column )
852+ if err != nil {
853+ return "" , nil , argCounter
854+ }
855+ condition := fmt .Sprintf ("$%d = ANY(%s)" , argCounter , formattedColumn )
765856 args := []interface {}{filter .Value }
766857 return condition , args , argCounter + 1
767858}
768859
860+ // BuildJSONBCondition builds conditions for JSONB path queries
861+ // Example: column=["raw_statement"], json_path=["result", "success"], operator="eq", value="true"
862+ // Produces: raw_statement->'result'->>'success' = $1
863+ func (postgresDbService * PostgresDbService ) BuildJSONBCondition (filter models.QueryFilter , argCounter int ) (string , []interface {}, int ) {
864+ // Validate column name
865+ if err := ValidateColumnName (filter .Column ); err != nil {
866+ return "" , nil , argCounter
867+ }
868+
869+ if len (filter .JSONPath ) == 0 {
870+ return "" , nil , argCounter
871+ }
872+
873+ // Build JSONB path expression: column->'key1'->'key2'->>'final_key'
874+ quotedCol := pq .QuoteIdentifier (filter .Column )
875+ pathExpr := quotedCol
876+
877+ // Navigate through the path, using ->> for the last key to extract text
878+ for i , key := range filter .JSONPath {
879+ quotedKey := fmt .Sprintf ("'%s'" , strings .ReplaceAll (key , "'" , "''" )) // SQL escape single quotes
880+ if i == len (filter .JSONPath )- 1 {
881+ // Last key - use ->> to extract as text for comparison
882+ pathExpr += fmt .Sprintf (" ->> %s" , quotedKey )
883+ } else {
884+ // Intermediate keys - use -> to navigate as JSONB
885+ pathExpr += fmt .Sprintf (" -> %s" , quotedKey )
886+ }
887+ }
888+
889+ // Now build the condition using the path expression
890+ operator := strings .ToLower (filter .Operator )
891+ var condition string
892+ var args []interface {}
893+
894+ switch operator {
895+ case "eq" , "=" :
896+ condition = fmt .Sprintf ("%s = $%d" , pathExpr , argCounter )
897+ args = []interface {}{filter .Value }
898+ argCounter ++
899+ case "neq" , "!=" , "<>" :
900+ condition = fmt .Sprintf ("%s != $%d" , pathExpr , argCounter )
901+ args = []interface {}{filter .Value }
902+ argCounter ++
903+ case "gt" , ">" :
904+ condition = fmt .Sprintf ("%s > $%d" , pathExpr , argCounter )
905+ args = []interface {}{filter .Value }
906+ argCounter ++
907+ case "gte" , ">=" :
908+ condition = fmt .Sprintf ("%s >= $%d" , pathExpr , argCounter )
909+ args = []interface {}{filter .Value }
910+ argCounter ++
911+ case "lt" , "<" :
912+ condition = fmt .Sprintf ("%s < $%d" , pathExpr , argCounter )
913+ args = []interface {}{filter .Value }
914+ argCounter ++
915+ case "lte" , "<=" :
916+ condition = fmt .Sprintf ("%s <= $%d" , pathExpr , argCounter )
917+ args = []interface {}{filter .Value }
918+ argCounter ++
919+ case "like" :
920+ condition = fmt .Sprintf ("%s LIKE $%d" , pathExpr , argCounter )
921+ args = []interface {}{filter .Value }
922+ argCounter ++
923+ case "ilike" :
924+ condition = fmt .Sprintf ("%s ILIKE $%d" , pathExpr , argCounter )
925+ args = []interface {}{filter .Value }
926+ argCounter ++
927+ case "in" :
928+ values , ok := postgresDbService .ToInterfaceSlice (filter .Value )
929+ if ! ok || len (values ) == 0 {
930+ return "" , nil , argCounter
931+ }
932+ placeholders := make ([]string , len (values ))
933+ for i , val := range values {
934+ placeholders [i ] = fmt .Sprintf ("$%d" , argCounter )
935+ args = append (args , val )
936+ argCounter ++
937+ }
938+ condition = fmt .Sprintf ("%s IN (%s)" , pathExpr , strings .Join (placeholders , ", " ))
939+ case "not_in" :
940+ values , ok := postgresDbService .ToInterfaceSlice (filter .Value )
941+ if ! ok || len (values ) == 0 {
942+ return "" , nil , argCounter
943+ }
944+ placeholders := make ([]string , len (values ))
945+ for i , val := range values {
946+ placeholders [i ] = fmt .Sprintf ("$%d" , argCounter )
947+ args = append (args , val )
948+ argCounter ++
949+ }
950+ condition = fmt .Sprintf ("%s NOT IN (%s)" , pathExpr , strings .Join (placeholders , ", " ))
951+ case "is_null" :
952+ condition = fmt .Sprintf ("%s IS NULL" , pathExpr )
953+ case "is_not_null" :
954+ condition = fmt .Sprintf ("%s IS NOT NULL" , pathExpr )
955+ default :
956+ // Unknown operator
957+ return "" , nil , argCounter
958+ }
959+
960+ return condition , args , argCounter
961+ }
962+
769963func (postgresDbService * PostgresDbService ) BuildFilterCondition (filter models.QueryFilter , argCounter int ) (string , []interface {}, int ) {
770964 // VALIDATE OPERATOR FIRST - before any SQL string building
771965 if err := ValidateOperator (filter .Operator ); err != nil {
772- // Return empty condition on invalid operator - caller should handle this
773- // or we could return error as fourth return value (future improvement)
966+ // Return empty condition on invalid operator
774967 return "" , nil , argCounter
775968 }
776969
777- // VALIDATE COLUMN NAME - ensure column is safe from SQL injection
970+ // Check if this is a JSONB path query
971+ if len (filter .JSONPath ) > 0 {
972+ return postgresDbService .BuildJSONBCondition (filter , argCounter )
973+ }
974+
975+ // Regular column validation and processing
778976 if err := ValidateColumnName (filter .Column ); err != nil {
779977 // Return empty condition on invalid column
780978 return "" , nil , argCounter
@@ -2740,10 +2938,12 @@ func (r *PostgresDbService) RemoveManyToManyRelations(relationship *models.Relat
27402938// - int: Updated argCounter after consuming parameters
27412939//
27422940// Example output for one-to-many:
2743- // SELECT orders.* FROM orders WHERE orders.user_id = $1
2941+ //
2942+ // SELECT orders.* FROM orders WHERE orders.user_id = $1
27442943//
27452944// Example output for many-to-many:
2746- // SELECT t.* FROM products t INNER JOIN order_items j ON t.id = j.product_id WHERE j.order_id = $1
2945+ //
2946+ // SELECT t.* FROM products t INNER JOIN order_items j ON t.id = j.product_id WHERE j.order_id = $1
27472947func (r * PostgresDbService ) buildRelationshipBaseQuery (relationship * models.RelationshipDefinition , params models.QueryParams , argCounter int ) (string , int ) {
27482948 var query strings.Builder
27492949
0 commit comments