Documentation for tracer mechanism

This commit is contained in:
Simon Willison 2020-09-07 14:54:07 -07:00
commit f12251092c
12 changed files with 68 additions and 22 deletions

View file

@ -43,6 +43,51 @@ Connections use ``PRAGMA recursive_triggers=on`` by default. If you don't want t
db = Database(memory=True, recursive_triggers=False) db = Database(memory=True, recursive_triggers=False)
.. _python_api_tracing:
Tracing queries
---------------
You can use the ``tracer`` mechanism to see SQL queries that are being executed by SQLite. A tracer is a function that you provide which will be called with ``sql`` and ``params`` arguments every time SQL is executed, for example:
.. code-block:: python
def tracer(sql, params):
print("SQL: {} - params: {}".format(sql, params))
You can pass this function to the ``Database()`` constructor like so:
.. code-block:: python
db = Database(memory=True, tracer=tracer)
You can also turn on a tracer function temporarily for a block of code using the ``with db.tracer(...)`` context manager:
.. code-block:: python
db = Database(memory=True)
# ... later
with db.tracer(tracer):
db["dogs"].insert({"name": "Cleo"})
Queries will be passed to your ``tracer()`` function only for the duration of the ``with`` block.
Executing queries
=================
The ``db.execute()`` and ``db.executescript()`` methods provide wrappers around ``.execute()`` and ``.executescript()`` on the underlying SQLite connection. These wrappers log to the tracer function if one has been registered.
.. code-block:: python
db = Database(memory=True)
db["dogs"].insert({"name": "Cleo"})
db.execute("update dogs set name = 'Cleopaws'")
.. _python_api_table:
Accessing tables
================
Tables are accessed using the indexing operator, like so: Tables are accessed using the indexing operator, like so:
.. code-block:: python .. code-block:: python
@ -930,7 +975,7 @@ For example:
"postalCode": "95018" "postalCode": "95018"
} }
}) })
db.conn.execute(""" db.execute("""
select json_extract(address, '$.addressLocality') select json_extract(address, '$.addressLocality')
from niche_museums from niche_museums
""").fetchall() """).fetchall()
@ -979,8 +1024,8 @@ A more useful example: if you are working with `SpatiaLite <https://www.gaia-gis
places = db["places"].create({"id": int, "name": str,}) places = db["places"].create({"id": int, "name": str,})
# Add a SpatiaLite 'geometry' column: # Add a SpatiaLite 'geometry' column:
db.conn.execute("select InitSpatialMetadata(1)") db.execute("select InitSpatialMetadata(1)")
db.conn.execute( db.execute(
"SELECT AddGeometryColumn('places', 'geometry', 4326, 'MULTIPOLYGON', 2);" "SELECT AddGeometryColumn('places', 'geometry', 4326, 'MULTIPOLYGON', 2);"
) )

View file

@ -771,7 +771,7 @@ def query(
for ext in load_extension: for ext in load_extension:
db.conn.load_extension(ext) db.conn.load_extension(ext)
with db.conn: with db.conn:
cursor = db.conn.execute(sql, dict(param)) cursor = db.execute(sql, dict(param))
if cursor.description is None: if cursor.description is None:
# This was an update/insert # This was an update/insert
headers = ["rows_affected"] headers = ["rows_affected"]

View file

@ -124,9 +124,9 @@ class Database:
else: else:
assert not recreate, "recreate cannot be used with connections, only paths" assert not recreate, "recreate cannot be used with connections, only paths"
self.conn = filename_or_conn self.conn = filename_or_conn
if recursive_triggers:
self.conn.execute("PRAGMA recursive_triggers=on;")
self._tracer = tracer self._tracer = tracer
if recursive_triggers:
self.execute("PRAGMA recursive_triggers=on;")
@contextlib.contextmanager @contextlib.contextmanager
def tracer(self, tracer=None): def tracer(self, tracer=None):
@ -940,7 +940,7 @@ class Table(Queryable):
table=fts_table table=fts_table
) )
) )
self.db.conn.execute( self.db.execute(
textwrap.dedent( textwrap.dedent(
""" """
DELETE FROM [{table}_docsize] WHERE {column} NOT IN ( DELETE FROM [{table}_docsize] WHERE {column} NOT IN (

View file

@ -10,7 +10,7 @@ def fresh_db():
@pytest.fixture @pytest.fixture
def existing_db(): def existing_db():
database = Database(memory=True) database = Database(memory=True)
database.conn.executescript( database.executescript(
""" """
CREATE TABLE foo (text TEXT); CREATE TABLE foo (text TEXT);
INSERT INTO foo (text) values ("one"); INSERT INTO foo (text) values ("one");

View file

@ -396,7 +396,7 @@ def test_enable_fts_with_triggers(db_path):
def search(q): def search(q):
return ( return (
Database(db_path) Database(db_path)
.conn.execute("select c1 from Gosh_fts where c1 match ?", [q]) .execute("select c1 from Gosh_fts where c1 match ?", [q])
.fetchall() .fetchall()
) )
@ -417,7 +417,7 @@ def test_populate_fts(db_path):
def search(q): def search(q):
return ( return (
Database(db_path) Database(db_path)
.conn.execute("select c1 from Gosh_fts where c1 match ?", [q]) .execute("select c1 from Gosh_fts where c1 match ?", [q])
.fetchall() .fetchall()
) )

View file

@ -41,5 +41,5 @@ def test_column_affinity(column_def, expected_type):
@pytest.mark.parametrize("column_def,expected_type", EXAMPLES) @pytest.mark.parametrize("column_def,expected_type", EXAMPLES)
def test_columns_dict(fresh_db, column_def, expected_type): def test_columns_dict(fresh_db, column_def, expected_type):
fresh_db.conn.execute("create table foo (col {})".format(column_def)) fresh_db.execute("create table foo (col {})".format(column_def))
assert {"col": expected_type} == fresh_db["foo"].columns_dict assert {"col": expected_type} == fresh_db["foo"].columns_dict

View file

@ -4,9 +4,9 @@ import pytest
def test_recursive_triggers(): def test_recursive_triggers():
db = Database(memory=True) db = Database(memory=True)
assert db.conn.execute("PRAGMA recursive_triggers").fetchone()[0] assert db.execute("PRAGMA recursive_triggers").fetchone()[0]
def test_recursive_triggers_off(): def test_recursive_triggers_off():
db = Database(memory=True, recursive_triggers=False) db = Database(memory=True, recursive_triggers=False)
assert not db.conn.execute("PRAGMA recursive_triggers").fetchone()[0] assert not db.execute("PRAGMA recursive_triggers").fetchone()[0]

View file

@ -680,7 +680,7 @@ def test_create_index_if_not_exists(fresh_db):
) )
def test_insert_dictionaries_and_lists_as_json(fresh_db, data_structure): def test_insert_dictionaries_and_lists_as_json(fresh_db, data_structure):
fresh_db["test"].insert({"id": 1, "data": data_structure}, pk="id") fresh_db["test"].insert({"id": 1, "data": data_structure}, pk="id")
row = fresh_db.conn.execute("select id, data from test").fetchone() row = fresh_db.execute("select id, data from test").fetchone()
assert row[0] == 1 assert row[0] == 1
assert data_structure == json.loads(row[1]) assert data_structure == json.loads(row[1])

View file

@ -4,7 +4,7 @@ from sqlite_utils.utils import OperationalError
def test_create_view(fresh_db): def test_create_view(fresh_db):
fresh_db.create_view("bar", "select 1 + 1") fresh_db.create_view("bar", "select 1 + 1")
rows = fresh_db.conn.execute("select * from bar").fetchall() rows = fresh_db.execute("select * from bar").fetchall()
assert [(2,)] == rows assert [(2,)] == rows
@ -23,7 +23,7 @@ def test_create_view_ignore(fresh_db):
fresh_db.create_view("bar", "select 1 + 1").create_view( fresh_db.create_view("bar", "select 1 + 1").create_view(
"bar", "select 1 + 2", ignore=True "bar", "select 1 + 2", ignore=True
) )
rows = fresh_db.conn.execute("select * from bar").fetchall() rows = fresh_db.execute("select * from bar").fetchall()
assert [(2,)] == rows assert [(2,)] == rows
@ -31,13 +31,13 @@ def test_create_view_replace(fresh_db):
fresh_db.create_view("bar", "select 1 + 1").create_view( fresh_db.create_view("bar", "select 1 + 1").create_view(
"bar", "select 1 + 2", replace=True "bar", "select 1 + 2", replace=True
) )
rows = fresh_db.conn.execute("select * from bar").fetchall() rows = fresh_db.execute("select * from bar").fetchall()
assert [(3,)] == rows assert [(3,)] == rows
def test_create_view_replace_with_same_does_nothing(fresh_db): def test_create_view_replace_with_same_does_nothing(fresh_db):
fresh_db.create_view("bar", "select 1 + 1") fresh_db.create_view("bar", "select 1 + 1")
initial_version = fresh_db.conn.execute("PRAGMA schema_version").fetchone()[0] initial_version = fresh_db.execute("PRAGMA schema_version").fetchone()[0]
fresh_db.create_view("bar", "select 1 + 1", replace=True) fresh_db.create_view("bar", "select 1 + 1", replace=True)
after_version = fresh_db.conn.execute("PRAGMA schema_version").fetchone()[0] after_version = fresh_db.execute("PRAGMA schema_version").fetchone()[0]
assert after_version == initial_version assert after_version == initial_version

View file

@ -160,7 +160,7 @@ def test_disable_fts(fresh_db, create_triggers):
expected_triggers = set() expected_triggers = set()
assert expected_triggers == set( assert expected_triggers == set(
r[0] r[0]
for r in fresh_db.conn.execute( for r in fresh_db.execute(
"select name from sqlite_master where type = 'trigger'" "select name from sqlite_master where type = 'trigger'"
).fetchall() ).fetchall()
) )
@ -168,7 +168,7 @@ def test_disable_fts(fresh_db, create_triggers):
table.disable_fts() table.disable_fts()
assert ( assert (
0 0
== fresh_db.conn.execute( == fresh_db.execute(
"select count(*) from sqlite_master where type = 'trigger'" "select count(*) from sqlite_master where type = 'trigger'"
).fetchone()[0] ).fetchone()[0]
) )

View file

@ -73,7 +73,7 @@ def test_table_repr(fresh_db):
def test_indexes(fresh_db): def test_indexes(fresh_db):
fresh_db.conn.executescript( fresh_db.executescript(
""" """
create table Gosh (c1 text, c2 text, c3 text); create table Gosh (c1 text, c2 text, c3 text);
create index Gosh_c1 on Gosh(c1); create index Gosh_c1 on Gosh(c1);

View file

@ -11,6 +11,7 @@ def test_tracer():
db["dogs"].enable_fts(["name"]) db["dogs"].enable_fts(["name"])
db["dogs"].search("Cleopaws") db["dogs"].search("Cleopaws")
assert collected == [ assert collected == [
("PRAGMA recursive_triggers=on;", None),
("select name from sqlite_master where type = 'view'", None), ("select name from sqlite_master where type = 'view'", None),
("select name from sqlite_master where type = 'table'", None), ("select name from sqlite_master where type = 'table'", None),
("CREATE TABLE [dogs] (\n [name] TEXT\n);\n ", None), ("CREATE TABLE [dogs] (\n [name] TEXT\n);\n ", None),