djangodjango-modelsdjango-rest-frameworkdjango-querysetdjango-q

How to create dynamic queries with Django Q objects from parenthesis inside a string


I don't know if the title of the question is formed well enough. But essentially I would like to be able to do something like this from front end :

(name="abc" OR name="xyz") AND (status="active" OR (status="available" AND age=30))

I want to the user to send this string. I will parse it in backend and form a query.

I have looked at this answer and this but couldn't figure out how to solve the parenthesis here.

I am thinking about using a stack (the way we solve infix expressions) to do this, but don't want to go that long route unless I am sure there isn't another/ready solution available. If someone can do this with that method, would be great too.


Solution

  • So I got it done using by changing the 'infix expression solver' a bit.

    Here's the code:

    def complex_filter_by_string(mystr):
    """
    Filters string by using Q objects and infix expression solver.
    The input string should be of the type ( name = abc OR name = xyz ) AND ( other = fgh ) Or ( other_one = xyz )
    Notice the spaces after each 'word'. We are splitting the string by space, so space is necessary.
    """
    tokens = mystr.split()
    
    # stack to store integer values.
    values = []
    
    # stack to store operators.
    ops = []
    i = 0
    while i < len(tokens):
    
        # Current token is a whitespace,
        # skip it.
        if tokens[i] == ' ':
            i += 1
            continue
    
        # Current token is an opening
        # brace, push it to 'ops'
        elif tokens[i] == '(':
            ops.append(tokens[i])
    
        # Current token is =, convert it to a Q object and push
        # it to stack for q objects.
        elif tokens[i] == "=":
    
            # if token equals '=' . It means we will have tokens[i-1]=attribute and tokens[i+1]=value (this is a constraint on string)
            obj = {}
            key = tokens[i-1]
            value = tokens[i+1]
            if isinstance(value,str):
                i+=1
                while i+1 < len(tokens) and (tokens[i+1] != 'OR' and tokens[i+1] != 'AND' and tokens[i+1] != ')'):
                    value= value + " " + tokens[i+1]
                    i+=1
                value = value.rstrip()
            obj[key]=value
            val = Q(**obj)
            values.append(val)
        
            
        # Closing brace encountered,
        # solve entire brace.
        elif tokens[i] == ')':
        
            while len(ops) != 0 and ops[-1] != '(':
            
                val2 = values.pop()
                val1 = values.pop()
                op = ops.pop()
                
                values.append(applyOp(val1, val2, op))
            
            # pop opening brace.
            ops.pop()
        
        # Current token is an operator.
        elif tokens[i] == "OR" or tokens[i] == "AND":
        
            # While top of 'ops' has same or
            # greater precedence to current
            # token, which is an operator.
            # Apply operator on top of 'ops'
            # to top two elements in values stack.
            while (len(ops) != 0 and
                precedence(ops[-1]) >=
                precedence(tokens[i])):
                        
                val2 = values.pop()
                val1 = values.pop()
                op = ops.pop()
                
                values.append(applyOp(val1, val2, op))
            
            # Push current token to 'ops'.
            ops.append(tokens[i])
        
        i += 1
    
    # Entire expression has been parsed
    # at this point, apply remaining ops
    # to remaining values.
    while len(ops) != 0:
        
        val2 = values.pop()
        val1 = values.pop()
        op = ops.pop()
                
        values.append(applyOp(val1, val2, op))
    
    # Top of 'values' contains result,
    # return it.
    return values[-1]
    

    This will return a Q object which you can pass onto your model lie MyModel.objects.filter(q_obj)

    There are a few restrictions on the string though. The main restriction is 'there should be a whitespace after each keyword'. A keyword can be ' OR , AND , attribute , value , = , open_parenthesis '.

    So the string that i posted in question should be : ( name = abc OR name = xyz ) AND (status = active OR ( status = available AND age = 30 ))