sqlite3_hat 0.1__tar.gz → 0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sqlite3_hat
3
- Version: 0.1
3
+ Version: 0.2
4
4
  Summary: Lightweight wrapper with improvements on sqlite3
5
5
  Author-email: Thomas Kluyver <takluyver@fastmail.com>
6
6
  Requires-Python: >=3.12
@@ -17,13 +17,19 @@ module from the standard library.
17
17
  ``sqlite3_hat`` adds (or changes):
18
18
 
19
19
  - Nested transaction support, using SQLite savepoints
20
- - Autocommit is on (see why autocommit)
20
+ - Autocommit is on (see the `docs on autocommit
21
+ <https://sqlite3-hat.readthedocs.io/en/latest/vs-stdlib.html#autocommit>`_ for
22
+ the rationale)
21
23
  - Foreign key enforcement is on
22
24
  - Non-standard double-quoted string syntax is disabled by default
23
25
  - Convenient way to open a database read-only
24
26
  - Parameters can be supplied in t-strings, or as ``*args`` rather than tuples
25
27
  - Shortcut ``.queryone()`` method for getting a single row
28
+ - ``Table`` objects wrapping database tables, and a `dict-style
29
+ interface <https://sqlite3-hat.readthedocs.io/en/latest/mapping.html>`_
30
+ to access a table by a unique key.
26
31
 
27
32
  The API is very similar to ``sqlite3``, but it's not a complete drop-in
28
- replacement.
33
+ replacement. The `porting guide <https://sqlite3-hat.readthedocs.io/en/latest/vs-stdlib.html#porting>`_
34
+ describes a strategy to adopt it in existing code.
29
35
 
@@ -0,0 +1,21 @@
1
+ ``sqlite3_hat`` is a small, pure Python wrapper to improve the ``sqlite3``
2
+ module from the standard library.
3
+
4
+ ``sqlite3_hat`` adds (or changes):
5
+
6
+ - Nested transaction support, using SQLite savepoints
7
+ - Autocommit is on (see the `docs on autocommit
8
+ <https://sqlite3-hat.readthedocs.io/en/latest/vs-stdlib.html#autocommit>`_ for
9
+ the rationale)
10
+ - Foreign key enforcement is on
11
+ - Non-standard double-quoted string syntax is disabled by default
12
+ - Convenient way to open a database read-only
13
+ - Parameters can be supplied in t-strings, or as ``*args`` rather than tuples
14
+ - Shortcut ``.queryone()`` method for getting a single row
15
+ - ``Table`` objects wrapping database tables, and a `dict-style
16
+ interface <https://sqlite3-hat.readthedocs.io/en/latest/mapping.html>`_
17
+ to access a table by a unique key.
18
+
19
+ The API is very similar to ``sqlite3``, but it's not a complete drop-in
20
+ replacement. The `porting guide <https://sqlite3-hat.readthedocs.io/en/latest/vs-stdlib.html#porting>`_
21
+ describes a strategy to adopt it in existing code.
@@ -0,0 +1,20 @@
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = .
9
+ BUILDDIR = _build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
@@ -0,0 +1,74 @@
1
+ API Reference
2
+ =============
3
+
4
+ .. currentmodule:: sqlite3_hat
5
+
6
+ The API is similar to :mod:`sqlite3`.
7
+
8
+ .. autofunction:: connect
9
+
10
+ .. autofunction:: open
11
+
12
+ Usage::
13
+
14
+ with sqlite3_hat.open("foo.db") as conn:
15
+ conn.execute(...)
16
+
17
+ .. class:: Connection
18
+
19
+ The primary interface to an open SQLite database.
20
+ Use :func:`connect` or :func:`open` to make a Connection object.
21
+
22
+ .. automethod:: execute
23
+ .. automethod:: executescript
24
+ .. automethod:: executemany
25
+ .. automethod:: queryone
26
+
27
+ .. automethod:: savepoint
28
+
29
+ .. seealso::
30
+ `Savepoints in SQLite docs <https://sqlite.org/lang_savepoint.html>`_
31
+
32
+ .. automethod:: transaction
33
+
34
+ .. seealso::
35
+ `Transactions in SQLite docs <https://sqlite.org/lang_transaction.html>`_
36
+
37
+ .. automethod:: commit
38
+ .. automethod:: rollback
39
+
40
+ .. automethod:: table
41
+
42
+ .. class:: Table
43
+
44
+ A wrapper for a database table. Call :meth:`Connection.table` to make a
45
+ Table object.
46
+
47
+ .. attribute:: columns
48
+
49
+ A tuple of selected column names. Initially this is all columns except for
50
+ hidden columns in virtual tables - the same columns you'd get with
51
+ ``SELECT *``. Use :meth:`select_columns` to get a different selection.
52
+
53
+ .. automethod:: select_columns
54
+ .. automethod:: nrows
55
+ .. automethod:: read_cursor
56
+ .. automethod:: insert
57
+ .. automethod:: insertmany
58
+ .. automethod:: mapping
59
+
60
+ Other functions
61
+ ---------------
62
+
63
+ .. autofunction:: quote_name
64
+
65
+ Exceptions
66
+ ----------
67
+
68
+ :exc:`sqlite3.Error` and its subclasses can all be raised by this module.
69
+
70
+ .. autoexception:: NoResultsError
71
+
72
+ .. autoexception:: TableNameError
73
+
74
+ .. autoexception:: ColumnNameError
@@ -0,0 +1,34 @@
1
+ # Configuration file for the Sphinx documentation builder.
2
+ #
3
+ # For the full list of built-in configuration values, see the documentation:
4
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html
5
+
6
+ # -- Project information -----------------------------------------------------
7
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
8
+
9
+ project = 'sqlite3_hat'
10
+ copyright = '2026, Thomas Kluyver'
11
+ author = 'Thomas Kluyver'
12
+
13
+ # -- General configuration ---------------------------------------------------
14
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
15
+
16
+ import os, sys
17
+ sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
18
+
19
+ extensions = [
20
+ "sphinx.ext.doctest",
21
+ "sphinx.ext.autodoc",
22
+ "sphinx.ext.intersphinx",
23
+ ]
24
+
25
+ templates_path = ['_templates']
26
+ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
27
+
28
+ intersphinx_mapping = {'python': ('https://docs.python.org/3', None)}
29
+
30
+ # -- Options for HTML output -------------------------------------------------
31
+ # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
32
+
33
+ html_theme = 'alabaster'
34
+ html_static_path = ['_static']
@@ -1,15 +1,33 @@
1
+ sqlite3_hat
2
+ ============
3
+
1
4
  ``sqlite3_hat`` is a small, pure Python wrapper to improve the ``sqlite3``
2
5
  module from the standard library.
3
6
 
4
7
  ``sqlite3_hat`` adds (or changes):
5
8
 
6
9
  - Nested transaction support, using SQLite savepoints
7
- - Autocommit is on (see why autocommit)
10
+ - Autocommit is on (see :ref:`autocommit` for rationale)
8
11
  - Foreign key enforcement is on
9
12
  - Non-standard double-quoted string syntax is disabled by default
10
13
  - Convenient way to open a database read-only
11
14
  - Parameters can be supplied in t-strings, or as ``*args`` rather than tuples
12
15
  - Shortcut ``.queryone()`` method for getting a single row
16
+ - :class:`~.Table` objects wrapping database tables, and a :doc:`dict-style
17
+ interface <mapping>` to access a table by a unique key.
18
+
19
+ Install it with::
20
+
21
+ pip install sqlite3_hat
13
22
 
14
23
  The API is very similar to ``sqlite3``, but it's not a complete drop-in
15
- replacement.
24
+ replacement. See :ref:`porting` for how to adopt it in existing code.
25
+
26
+ .. toctree::
27
+ :maxdepth: 2
28
+ :caption: Contents:
29
+
30
+ tutorial.rst
31
+ mapping.rst
32
+ vs-stdlib.rst
33
+ api.rst
@@ -0,0 +1,35 @@
1
+ @ECHO OFF
2
+
3
+ pushd %~dp0
4
+
5
+ REM Command file for Sphinx documentation
6
+
7
+ if "%SPHINXBUILD%" == "" (
8
+ set SPHINXBUILD=sphinx-build
9
+ )
10
+ set SOURCEDIR=.
11
+ set BUILDDIR=_build
12
+
13
+ %SPHINXBUILD% >NUL 2>NUL
14
+ if errorlevel 9009 (
15
+ echo.
16
+ echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
17
+ echo.installed, then set the SPHINXBUILD environment variable to point
18
+ echo.to the full path of the 'sphinx-build' executable. Alternatively you
19
+ echo.may add the Sphinx directory to PATH.
20
+ echo.
21
+ echo.If you don't have Sphinx installed, grab it from
22
+ echo.https://www.sphinx-doc.org/
23
+ exit /b 1
24
+ )
25
+
26
+ if "%1" == "" goto help
27
+
28
+ %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29
+ goto end
30
+
31
+ :help
32
+ %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33
+
34
+ :end
35
+ popd
@@ -0,0 +1,112 @@
1
+ Mapping interface
2
+ =================
3
+
4
+ A lot of simple data access is based on unique IDs. The mapping interface
5
+ lets you use a database table like a dictionary:
6
+
7
+ .. testsetup:: mapping
8
+
9
+ import sqlite3_hat
10
+ conn = sqlite3_hat.connect(":memory:")
11
+
12
+ .. testcode:: mapping
13
+
14
+ conn.execute("""CREATE TABLE settings(
15
+ name TEXT PRIMARY KEY NOT NULL,
16
+ value,
17
+ user_set INTEGER DEFAULT 0
18
+ )""")
19
+ settings = conn.table("settings").mapping()
20
+
21
+ # name : (value, user_set)
22
+ settings["theme"] = ("light", 0)
23
+ settings.update(view_width=(800, 0), view_height=(600, 0))
24
+
25
+ if settings["theme"][0] == "light":
26
+ ...
27
+
28
+ .. testcode:: mapping
29
+ :hide:
30
+
31
+ assert settings["theme"] == ("light", 0)
32
+ assert settings["view_width"][0] == 800
33
+
34
+ You can use almost all dictionary methods; more precisely, the mapping object
35
+ implements the :class:`~collections.abc.MutableMapping` interface.
36
+
37
+ By default, the keys are the table's primary key column (or columns), and the
38
+ values are all the other columns. But you can select specific columns:
39
+
40
+ .. testcode:: mapping
41
+
42
+ setting_values = conn.table("settings").mapping(value="value")
43
+
44
+ setting_values["time_zone"] = "UTC"
45
+
46
+ if setting_values.get("theme", "light") == "dark":
47
+ ...
48
+
49
+ .. note::
50
+
51
+ Selecting columns for values, as shown here, is mostly simple.
52
+
53
+ You can also select different columns for keys, but the selected column(s)
54
+ should have a UNIQUE constraint (PRIMARY KEY also implies uniqueness).
55
+
56
+ You should not use ``None`` in keys, because a column with a UNIQUE
57
+ constraint can still have multiple NULL values. If possible, use a NOT NULL
58
+ constraint on key columns to ensure no NULLs creep in.
59
+
60
+ Transactions
61
+ ------------
62
+
63
+ Mapping objects work with :ref:`database transactions <tutorial-transactions>`.
64
+ If a transaction is open, any changes you make to the mapping are only saved
65
+ when the transaction is committed.
66
+
67
+ .. testcode:: mapping
68
+
69
+ with conn:
70
+ settings["theme"] = ("high-contrast", 1)
71
+ settings["font_size"] = ("14pt", 1)
72
+
73
+ # Leaving the 'with conn' block commits the changes
74
+
75
+ Each database method is atomic, and will act on a consistent view of the
76
+ database. For instance, ``settings.update()`` will either make all the given
77
+ updates, or none of them.
78
+
79
+ .. testcode:: mapping
80
+
81
+ # Don't do this:
82
+ if 'theme' in settings:
83
+ # between these steps, another process could delete the entry
84
+ del settings['theme']
85
+
86
+ # Do this instead:
87
+ settings.pop('theme', None)
88
+
89
+ Iterators
90
+ ---------
91
+
92
+ Iterating over keys, values or items in the mapping is *lazy*, loading the data
93
+ as needed rather than reading it all up front. However, it means keeping a read
94
+ transaction open on the database, which may block writing until it finishes.
95
+ So if the code in the loop might be slow, it's a good idea to read all the
96
+ entries into a list before looping:
97
+
98
+ .. testsetup:: mapping
99
+
100
+ def user_confirm(prompt): return
101
+ def settings_input_generator(): return []
102
+
103
+ .. testcode:: mapping
104
+
105
+ # list() collects the data in memory & finishes reading
106
+ for k, v in list(setting_values.items()):
107
+ user_confirm(f"Setting {k} is set to {v} - OK?")
108
+
109
+ Writing data with ``mapping.update(slow_iterator)`` can be even worse, because
110
+ this keeps a write transaction open until the iterator finishes, which can block
111
+ both readers and writers. Try to either group changes in memory before calling
112
+ ``.update()``, or split it up into smaller changes.
@@ -0,0 +1,278 @@
1
+ Getting started with sqlite3_hat
2
+ =================================
3
+
4
+ This guide is about writing new code. See :ref:`porting` to adapt existing code
5
+ using ``sqlite3``.
6
+
7
+ .. _tutorial-connect:
8
+
9
+ Opening a database
10
+ ------------------
11
+
12
+ Everything you'll do with SQLite starts with opening a database file:
13
+
14
+ ::
15
+
16
+ import sqlite3_hat
17
+ conn = sqlite3_hat.connect("library.sqlite")
18
+
19
+ The default will create this file if it doesn't exist. If you don't want this,
20
+ pass ``mode="ro"`` (read-only) or ``mode="rw"`` (read-write).
21
+
22
+ The special filename ``:memory:`` will create a transient in-memory database.
23
+ This is especially useful for writing tests.
24
+
25
+ .. _tutorial-schema:
26
+
27
+ Setting up tables and indexes
28
+ -----------------------------
29
+
30
+ For many database use cases, the next step is to define your *schema*, the
31
+ tables, indexes, etc. that structure your data. Of course, you can skip this
32
+ if you're opening a database created by other code.
33
+
34
+ .. testsetup:: tutorial
35
+
36
+ import sqlite3_hat
37
+ conn = sqlite3_hat.connect(":memory:")
38
+
39
+ .. testcode:: tutorial
40
+
41
+ conn.executescript("""
42
+ CREATE TABLE IF NOT EXISTS book(
43
+ id INTEGER PRIMARY KEY,
44
+ title TEXT,
45
+ author TEXT
46
+ );
47
+ CREATE TABLE IF NOT EXISTS borrower(
48
+ id INTEGER PRIMARY KEY,
49
+ name TEXT,
50
+ card_num TEXT UNIQUE
51
+ );
52
+ CREATE TABLE IF NOT EXISTS loan(
53
+ book INTEGER REFERENCES book(id) ON DELETE CASCADE,
54
+ -- SQL comments look like this!
55
+ -- RESTRICT forbids deleting a borrower with loans
56
+ borrower INTEGER REFERENCES borrower(id) ON DELETE RESTRICT,
57
+ due TEXT -- ISO format: YYYY-MM-DD
58
+ );
59
+ CREATE INDEX IF NOT EXISTS loan_by_book ON loan(book);
60
+ CREATE INDEX IF NOT EXISTS loan_by_borrower ON loan(borrower);
61
+ """)
62
+
63
+ Looking up data in a table with many rows is much faster if there's an index
64
+ on the field being used. PRIMARY KEY and UNIQUE columns get indexes
65
+ automatically, and you can choose to create indexes for other columns, as shown
66
+ above.
67
+
68
+ Designing a good schema for your data is a big topic, which I won't attempt to
69
+ cover here. You may well also need to think about *migrations*, how to modify
70
+ the structure of an existing database as your code evolves.
71
+
72
+ .. note::
73
+
74
+ The examples here use capitals for SQL keywords like ``CREATE``, and
75
+ lowercase for names like ``book``. This is a common convention, but it's
76
+ not required.
77
+
78
+ The SQL commands for creating objects are:
79
+
80
+ - `CREATE TABLE <https://sqlite.org/lang_createtable.html>`_
81
+ - `CREATE INDEX <https://sqlite.org/lang_createindex.html>`_
82
+ - `CREATE VIRTUAL TABLE <https://sqlite.org/lang_createvtab.html>`_ (used with extensions like full text search)
83
+ - `CREATE VIEW <https://sqlite.org/lang_createview.html>`_ (views are like tables generated from SQL queries)
84
+ - `CREATE TRIGGER <https://sqlite.org/lang_createtrigger.html>`_ (triggers can do things when data is modified)
85
+
86
+ .. _tutorial-write:
87
+
88
+ Writing data
89
+ ------------
90
+
91
+ The key SQL statements for writing are `INSERT <https://sqlite.org/lang_insert.html>`_
92
+ and `UPDATE <https://sqlite.org/lang_update.html>`_:
93
+
94
+ .. testcode:: tutorial
95
+
96
+ cur = conn.execute(
97
+ "INSERT INTO borrower(name, card_num) VALUES (?, ?)",
98
+ "Thomas Klyuver", "D98765"
99
+ )
100
+ borrower_id = cur.lastrowid
101
+
102
+ # Fix a typo
103
+ conn.execute(
104
+ "UPDATE borrower SET name=? WHERE id=?", "Thomas Kluyver", borrower_id
105
+ )
106
+
107
+ The ``cursor.lastrowid`` attribute gives you the *row ID* after a successful
108
+ insert. Because we defined the ``id`` column in the table as
109
+ `INTEGER PRIMARY KEY <https://sqlite.org/lang_createtable.html#rowids_and_the_integer_primary_key>`_,
110
+ the row ID is also the value in this column.
111
+
112
+ It's often useful to use the ``executemany()`` method when writing to make
113
+ a batch of changes efficiently:
114
+
115
+ .. testcode:: tutorial
116
+
117
+ conn.executemany("INSERT INTO book(title, author) VALUES (?, ?)", [
118
+ ("Northern Lights", "Philip Pullman"),
119
+ ("Men at Arms", "Terry Pratchett"),
120
+ ])
121
+
122
+ Finally, you can also `DELETE <https://sqlite.org/lang_delete.html>`_ rows::
123
+
124
+ conn.execute("DELETE FROM borrowers WHERE id=?", borrower_id)
125
+
126
+ .. _tutorial-params:
127
+
128
+ Parameters
129
+ ----------
130
+
131
+ Use placeholders, like the ``?`` shown above, and parameters to combine data with
132
+ the SQL code. There are two styles for this:
133
+
134
+ .. testcode:: tutorial
135
+
136
+ # Parameters by order
137
+ conn.execute("INSERT INTO book(title, author) VALUES (?, ?)",
138
+ "4.50 from Paddington", "Agatha Christie")
139
+
140
+ # Parameters by name
141
+ conn.execute("INSERT INTO book(title, author) VALUES (:title, :author)",
142
+ {"title": "Death in the Clouds", "author": "Agatha Christie"})
143
+
144
+
145
+ **Don't** be tempted to build a string of SQL including the data. That is how
146
+ you get *SQL injection* vulnerabilities, a serious and sadly common kind of
147
+ security issue. Even if you don't think this could matter in your context, get
148
+ into the habit of using placeholders by default, and learn to see it as a code
149
+ smell if SQL is built with string formatting.
150
+
151
+ Using placeholders can also be more efficient: as the actual SQL is not
152
+ changing, SQLite can cache and reuse the parsed statement. In a simple test
153
+ storing 1000 numbers, using placeholders was twice as fast as formatting the
154
+ numbers into the SQL.
155
+
156
+ Template strings
157
+ ~~~~~~~~~~~~~~~~
158
+
159
+ You can also use template strings to put values into SQL safely on Python 3.14
160
+ or above:
161
+
162
+ .. testcode:: tutorial
163
+
164
+ author = "J.R.R. Tolkein"
165
+ conn.execute(
166
+ t"INSERT INTO book(title, author) VALUES ({'The Hobbit'}, {author})"
167
+ )
168
+
169
+ This uses the same placeholder & parameter mechanism to combine the data with
170
+ the SQL. So it's safe against SQL injection, but can only be used in the same
171
+ places that parameters work; e.g. you can't use it to fill in a table or column
172
+ name.
173
+
174
+ You can also use template strings with ``conn.executemany()`` with this
175
+ comprehension style:
176
+
177
+ .. testcode:: tutorial
178
+
179
+ lotr_books = [
180
+ "The Fellowship of the Ring",
181
+ "The Two Towers",
182
+ "The Return of the King",
183
+ ]
184
+ conn.executemany([
185
+ t"INSERT INTO book(title, author) VALUES ({title}, {author})"
186
+ for title in lotr_books
187
+ ])
188
+
189
+ Only the parameter parts can vary within an ``executemany()`` call - the SQL
190
+ template must be the same in all entries.
191
+
192
+ .. _tutorial-transactions:
193
+
194
+ Transactions
195
+ ------------
196
+
197
+ The modifications above will all take effect as soon as they finish.
198
+ But you can also wrap a group of changes in a transaction. This achieves two
199
+ things:
200
+
201
+ 1. If there's an error during a transaction, it can be rolled back. The changes
202
+ within the transaction are either all saved, or none of them are (the
203
+ transaction is *atomic*).
204
+ 2. Making multiple changes within a transaction is more efficient than letting
205
+ SQLite commit each change separately.
206
+
207
+ For our library example, let's imagine members of another library are allowed to
208
+ borrow books, and we've decided to create a temporary borrower in our database
209
+ to track each outside loan. When the book is returned, the temporary borrower
210
+ should be deleted again.
211
+
212
+ Transactions can be made using the connection object as a context manager:
213
+
214
+ .. testcode:: tutorial
215
+ :hide:
216
+
217
+ def is_temp(borrower_id): return True # To make the example below work
218
+
219
+ .. testcode:: tutorial
220
+
221
+ # Borrow
222
+ with conn:
223
+ cur = conn.execute("INSERT INTO borrower(name) VALUES (?)",
224
+ "Card #1234 from Othersville library",)
225
+ conn.execute("INSERT INTO loan(book, borrower) VALUES (?, ?)",
226
+ 4, cur.lastrowid)
227
+
228
+ # Return
229
+ borrower_id, = conn.queryone(
230
+ "SELECT borrower FROM loan WHERE book=?", 4
231
+ )
232
+ with conn:
233
+ conn.execute("DELETE FROM loan WHERE book=?", 4)
234
+ if is_temp(borrower_id):
235
+ conn.execute(
236
+ "DELETE FROM borrower WHERE id=?", borrower_id,
237
+ )
238
+
239
+ The transaction is committed at the end of the ``with`` block if there's no
240
+ exception, or rolled back if an exception is raised.
241
+
242
+ Transactions can be nested, and each level of transaction can be committed or
243
+ rolled back. Only when the outermost transaction is committed are any changes
244
+ visible to other connections. See `Savepoints in the SQLite docs
245
+ <https://sqlite.org/lang_savepoint.html>`_ for more about how nested
246
+ transactions work.
247
+
248
+
249
+ .. _tutorial-read:
250
+
251
+ Reading data
252
+ ------------
253
+
254
+ The `SELECT <https://sqlite.org/lang_select.html>`_ statement is the workhorse
255
+ of getting data out of the database:
256
+
257
+ .. testcode:: tutorial
258
+
259
+ rows = conn.execute(
260
+ "SELECT title FROM book WHERE author=?", "Agatha Christie"
261
+ ).fetchall()
262
+ for title, in rows:
263
+ print(title)
264
+
265
+ .. testoutput:: tutorial
266
+
267
+ 4.50 from Paddington
268
+ Death in the Clouds
269
+
270
+ The ``queryone`` method is a shortcut for reading a single row, and will raise
271
+ a clear error if no match is returned:
272
+
273
+ .. testcode:: tutorial
274
+
275
+ num_books = conn.queryone("SELECT count(*) FROM book")[0]
276
+ borrower_id, name = conn.queryone(
277
+ "SELECT id, name FROM borrower WHERE card_num=?", "D98765"
278
+ )