From 3d041d34d5ee8234e0b955d2d1697f0756d1ffa5 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 2 Jan 2021 20:15:04 -0800 Subject: [PATCH] Renamed db.escape() to db.quote() and documented it Closes #217 --- docs/python-api.rst | 26 ++++++++++++++++++++++++++ sqlite_utils/cli.py | 2 +- sqlite_utils/db.py | 10 ++++------ tests/test_create.py | 7 +++++++ 4 files changed, 38 insertions(+), 7 deletions(-) diff --git a/docs/python-api.rst b/docs/python-api.rst index 1fc5ba8..a8fb705 100644 --- a/docs/python-api.rst +++ b/docs/python-api.rst @@ -87,6 +87,15 @@ The ``db.execute()`` and ``db.executescript()`` methods provide wrappers around db["dogs"].insert({"name": "Cleo"}) db.execute("update dogs set name = 'Cleopaws'") +You can pass parameters as an optional second argument, using either a list or a dictionary. These will be correctly quoted and escaped. + +.. code-block:: python + + # Using ? and a list: + db.execute("update dogs set name = ?", ["Cleopaws"]) + # Or using :name and a dictionary: + db.execute("update dogs set name = :name", {"name": "Cleopaws"}) + .. _python_api_table: Accessing tables @@ -1913,3 +1922,20 @@ If you want to deliberately replace the registered function with a new implement @db.register_function(deterministic=True, replace=True) def reverse_string(s): return s[::-1] + +.. _python_api_quote: + +Quoting strings for use in SQL +============================== + +In almost all cases you should pass values to your SQL queries using the optional ``parameters`` argument to ``db.execute()``, as described in :ref:`python_api_execute`. + +If that option isn't relevant to your use-case you can to quote a string for use with SQLite using the ``db.quote()`` method, like so: + +:: + + >>> db = Database(memory=True) + >>> db.quote("hello") + "'hello'" + >>> db.quote("hello'this'has'quotes") + "'hello''this''has''quotes'" diff --git a/sqlite_utils/cli.py b/sqlite_utils/cli.py index b56e557..4a3035b 100644 --- a/sqlite_utils/cli.py +++ b/sqlite_utils/cli.py @@ -1127,7 +1127,7 @@ def triggers( "Show triggers configured in this database" sql = "select name, tbl_name as [table], sql from sqlite_master where type = 'trigger'" if tables: - quote = sqlite_utils.Database(memory=True).escape + quote = sqlite_utils.Database(memory=True).quote sql += " and [table] in ({})".format( ", ".join(quote(table) for table in tables) ) diff --git a/sqlite_utils/db.py b/sqlite_utils/db.py index 3118138..2fedc4f 100644 --- a/sqlite_utils/db.py +++ b/sqlite_utils/db.py @@ -228,7 +228,7 @@ class Database: klass = View if table_name in self.view_names() else Table return klass(self, table_name, **kwargs) - def escape(self, value): + def quote(self, value): # Normally we would use .execute(sql, [params]) for escaping, but # occasionally that isn't available - most notable when we need # to include a "... DEFAULT 'value'" in a column definition. @@ -417,7 +417,7 @@ class Database: column_extras.append("NOT NULL") if column_name in defaults and defaults[column_name] is not None: column_extras.append( - "DEFAULT {}".format(self.escape(defaults[column_name])) + "DEFAULT {}".format(self.quote(defaults[column_name])) ) if column_name in foreign_keys_by_column: column_extras.append( @@ -1107,9 +1107,7 @@ class Table(Queryable): col_type = str not_null_sql = None if not_null_default is not None: - not_null_sql = "NOT NULL DEFAULT {}".format( - self.db.escape(not_null_default) - ) + not_null_sql = "NOT NULL DEFAULT {}".format(self.db.quote(not_null_default)) sql = "ALTER TABLE [{table}] ADD COLUMN [{col_name}] {col_type}{not_null_default};".format( table=self.name, col_name=col_name, @@ -1227,7 +1225,7 @@ class Table(Queryable): create_counts_table=self.db._counts_table_create, counts_table=self.db._counts_table_name, table=self.name, - table_quoted=self.db.escape(self.name), + table_quoted=self.db.quote(self.name), ) ) with self.db.conn: diff --git a/tests/test_create.py b/tests/test_create.py index 83936e4..a658757 100644 --- a/tests/test_create.py +++ b/tests/test_create.py @@ -922,3 +922,10 @@ def test_create_with_nested_bytes(fresh_db): record = {"id": 1, "data": {"foo": b"bytes"}} fresh_db["t"].insert(record) assert [{"id": 1, "data": '{"foo": "b\'bytes\'"}'}] == list(fresh_db["t"].rows) + + +@pytest.mark.parametrize( + "input,expected", [("hello", "'hello'"), ("hello'there'", "'hello''there'''")] +) +def test_quote(fresh_db, input, expected): + assert fresh_db.quote(input) == expected