Small DSL: Converting JSON to MSSQL query Scenario

Once upon a time, there was this human (ahem, me, ahem) who tried (and succeeded) to build a small program which would convert JSON to a MSSQL query using Python 3.x.



How does it work?

The user makes a POST request with a JSON (in the code, I've used a JSON file) which looks like this:

{
  "endpoint": "rfc",
  "expression": [
    {
      "field": "first_name",
      "operator": "EQ",
      "value": "Dixie"
    },
    "AND",
    [
      {
        "field": "last_name",
        "operator": "NEQ",
        "value": "Smith"
      },
      "OR",
      {
        "field": "middle_name",
        "operator": "EQ",
        "value": "Sam"
      }
    ]
  ],
  "limit": "2"
}


Now, the key point in this story is the expression which is being translated to a query, using recursion, as follows:

def process(data):
    """
    :param data: JSON Object (dict).
    :return: where clause (str) built from data
    """
    where_clause = ""
    if isinstance(data, list):
        for part in data:
            if part not in LOGICAL_OPERATORS:
                where_clause += " ({}) ".format(process(part))
            else:
                where_clause += process(part)
    elif isinstance(data, dict):
        where_clause += " {} {} '{}'".format(data["field"], COMPARISON_OPERATORS[data["operator"]], data["value"])
    elif isinstance(data, str):
        return data
    return where_clause


The entire code, is as follows:

import json

LOGICAL_OPERATORS = ("AND", "OR")

COMPARISON_OPERATORS = {
    "LT": "&lt;",
    "GT": "&gt;",
    "LTE": "&lt;=",
    "GTE": "&gt;=",
    "EQ": "=",
    "NEQ": "!="
}


def get_data(file):
    """
    :param file: JSON file.
    :return: data as a json object (dict).
    """
    with open(file) as data_file:
        return json.load(data_file)


def process(data):
    """
    :param data: JSON Object (dict).
    :return: where clause (str) built from data
    """
    where_clause = ""
    if isinstance(data, list):
        for part in data:
            if part not in LOGICAL_OPERATORS:
                where_clause += " ({}) ".format(process(part))
            else:
                where_clause += process(part)
    elif isinstance(data, dict):
        where_clause += " {} {} '{}'".format(data["field"], COMPARISON_OPERATORS[data["operator"]], data["value"])
    elif isinstance(data, str):
        return data
    return where_clause


def main():
    expression = get_data("other.json")["expression"]
    where_clause = process(expression)

    return "SELECT * FROM table WHERE {}".format(where_clause)

if __name__ == '__main__':
    print(main())




Questions:


Will the recursive function scale good on a larger expression? Now, that expression can go in depth as long as the memory goes. Should I add a limit to it?
Did I miss something? Maybe an edge case?
Is there an already builtin in out there for this?
Escaping and SQL injections
One of the major problems in the presented code is that the code is vulnerable to SQL injection attacks and does not properly sanitize, validate and escape the field values.
For example, if a last_name contains a single quote, for example O'Reilly:
{
    &quot;field&quot;: &quot;last_name&quot;,
    &quot;operator&quot;: &quot;NEQ&quot;,
    &quot;value&quot;: &quot;O'Reilly&quot;
},

The generated query would be:
SELECT * FROM table WHERE  ( first_name = 'Dixie') AND ( ( last_name != 'O'Reilly') OR ( middle_name = 'Sam') ) 

which is syntactically incorrect query and would raise an error if executed (which may be a sign of not properly sanitized inputs for a potential &quot;bad&quot; user).
Then, we can try:
{
  &quot;endpoint&quot;: &quot;rfc&quot;,
  &quot;expression&quot;: [
    {
      &quot;field&quot;: &quot;first_name&quot;,
      &quot;operator&quot;: &quot;EQ&quot;,
      &quot;value&quot;: &quot;' or '1' = '1&quot;
    }
  ],
  &quot;limit&quot;: &quot;2&quot;
}

which will result into:
SELECT * FROM table WHERE  ( first_name = '' or '1' = '1') 

which will match everything in the table.
Possible to solution to the problem
A possible solution to the problem would be to use parameterized queries and named placeholders where names would come from the field parameter (assuming it's unique). Something along these lines (please test thoroughly):
def process(data, parameters={}):
    &quot;&quot;&quot;
    :param data: JSON Object (dict). 
    :param parameters: dict.
    :return: where clause (str) built from data
    &quot;&quot;&quot;
    where_clause = &quot;&quot;
    if isinstance(data, list):
        for part in data:
            if part not in LOGICAL_OPERATORS:
                where_clause += &quot; ({}) &quot;.format(process(part, parameters))
            else:
                where_clause += process(part, parameters)
    elif isinstance(data, dict):
        where_clause += &quot; {} {} %({})s &quot;.format(data[&quot;field&quot;], COMPARISON_OPERATORS[data[&quot;operator&quot;]], data[&quot;field&quot;])
        parameters[data[&quot;field&quot;]] = data[&quot;value&quot;]
    elif isinstance(data, str):
        return data
    return where_clause


def main():
    expression = get_data(&quot;other.json&quot;)[&quot;expression&quot;]
    parameters = {}
    where_clause = process(expression, parameters)

    return &quot;SELECT * FROM table WHERE {}&quot;.format(where_clause), parameters



Is there an already builtin in out there for this?

I would look into using ORMs like SQLAlchemy to generate queries. This way you would not have to deal with SQL at all, working with Python abstraction layer only.