Repository: tobymao/sqlglot

Issue:
Add Support for Filling in Placeholders
It would be great to be able to perform substitutions on placeholders.


Discussion:
Just hacked this together for my own project. `#clone()` would also be good addition.

```
    def bindparams(self, **bindparams):
        cloned_expression = sg.parse_one(self.expression.sql()) # clone hack

        for placeholder in cloned_expression.find_all(sg.expressions.Placeholder):
            if placeholder.name in bindparams:
                placeholder.replace(sg.parse_one(str(bindparams[placeholder.name])))

        return Query(self.db,
            cloned_expression
        )
```

Resolving patch:
diff --git a/sqlglot/expressions.py b/sqlglot/expressions.py
--- a/sqlglot/expressions.py
+++ b/sqlglot/expressions.py
@@ -3518,6 +3518,41 @@ def _replace_tables(node):
     return expression.transform(_replace_tables)
 
 
+def replace_placeholders(expression, *args, **kwargs):
+    """Replace placeholders in an expression.
+
+    Args:
+        expression (sqlglot.Expression): Expression node to be transformed and replaced
+        args: Positional names that will substitute unnamed placeholders in the given order
+        kwargs: Keyword arguments that will substitute named placeholders
+
+    Examples:
+        >>> from sqlglot import exp, parse_one
+        >>> replace_placeholders(
+        ...     parse_one("select * from :tbl where ? = ?"), "a", "b", tbl="foo"
+        ... ).sql()
+        'SELECT * FROM foo WHERE a = b'
+
+    Returns:
+        The mapped expression
+    """
+
+    def _replace_placeholders(node, args, **kwargs):
+        if isinstance(node, Placeholder):
+            if node.name:
+                new_name = kwargs.get(node.name)
+                if new_name:
+                    return to_identifier(new_name)
+            else:
+                try:
+                    return to_identifier(next(args))
+                except StopIteration:
+                    pass
+        return node
+
+    return expression.transform(_replace_placeholders, iter(args), **kwargs)
+
+
 TRUE = Boolean(this=True)
 FALSE = Boolean(this=False)
 NULL = Null()


Test patch:
diff --git a/tests/test_expressions.py b/tests/test_expressions.py
--- a/tests/test_expressions.py
+++ b/tests/test_expressions.py
@@ -135,6 +135,53 @@ def test_replace_tables(self):
             "SELECT * FROM a1 AS a JOIN b.a JOIN c.a2 JOIN d2 JOIN e.a",
         )
 
+    def test_replace_placeholders(self):
+        self.assertEqual(
+            exp.replace_placeholders(
+                parse_one("select * from :tbl1 JOIN :tbl2 ON :col1 = :col2 WHERE :col3 > 100"),
+                tbl1="foo",
+                tbl2="bar",
+                col1="a",
+                col2="b",
+                col3="c",
+            ).sql(),
+            "SELECT * FROM foo JOIN bar ON a = b WHERE c > 100",
+        )
+        self.assertEqual(
+            exp.replace_placeholders(
+                parse_one("select * from ? JOIN ? ON ? = ? WHERE ? > 100"),
+                "foo",
+                "bar",
+                "a",
+                "b",
+                "c",
+            ).sql(),
+            "SELECT * FROM foo JOIN bar ON a = b WHERE c > 100",
+        )
+        self.assertEqual(
+            exp.replace_placeholders(
+                parse_one("select * from ? WHERE ? > 100"),
+                "foo",
+            ).sql(),
+            "SELECT * FROM foo WHERE ? > 100",
+        )
+        self.assertEqual(
+            exp.replace_placeholders(parse_one("select * from :name WHERE ? > 100"), another_name="bla").sql(),
+            "SELECT * FROM :name WHERE ? > 100",
+        )
+        self.assertEqual(
+            exp.replace_placeholders(
+                parse_one("select * from (SELECT :col1 FROM ?) WHERE :col2 > 100"),
+                "tbl1",
+                "tbl2",
+                "tbl3",
+                col1="a",
+                col2="b",
+                col3="c",
+            ).sql(),
+            "SELECT * FROM (SELECT a FROM tbl1) WHERE b > 100",
+        )
+
     def test_named_selects(self):
         expression = parse_one("SELECT a, b AS B, c + d AS e, *, 'zz', 'zz' AS z FROM foo as bar, baz")
         self.assertEqual(expression.named_selects, ["a", "B", "e", "*", "zz", "z"])