Skip to content

gh-100414: Add SQLite backend to dbm #114481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 48 commits into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
8c19c81
gh-100414: Add SQLite backend to dbm
erlend-aasland Jan 22, 2024
7f32772
Preliminary docs
erlend-aasland Jan 23, 2024
3f8a119
Decode path a little bit earlier
erlend-aasland Jan 23, 2024
4f82c88
Merge branch 'main' into sqlite/dbm
erlend-aasland Jan 23, 2024
38a9b41
Substitutions are already in place
erlend-aasland Jan 23, 2024
db0f148
Use CSV-style table, as the rest of the .rst file does
erlend-aasland Jan 23, 2024
e50d1e9
dbm.sqlite3.error is a subclass of OSError
erlend-aasland Jan 23, 2024
0abd7dc
Don't raise DB API exceptions; test this
erlend-aasland Jan 23, 2024
98fba0f
Pull in main
erlend-aasland Jan 23, 2024
2b97160
Don't panic on double close(); add more tests
erlend-aasland Jan 23, 2024
917bfba
Corruption tests: check all basic operations for flags 'r', 'w', 'c'
erlend-aasland Jan 23, 2024
874fb81
Add read-only specific test
erlend-aasland Jan 23, 2024
b655473
Address review: always import sqlite3; let ImportError deal with miss…
erlend-aasland Jan 24, 2024
95eba93
Test whichdb
erlend-aasland Jan 24, 2024
54751ba
test namespacing
erlend-aasland Jan 24, 2024
906748e
Test that 'c' works even if you already have a database; make sure BU…
erlend-aasland Jan 24, 2024
7320f18
Pull in main
erlend-aasland Jan 24, 2024
c89c5fb
Remove unneeded comma
erlend-aasland Jan 24, 2024
34d8c7f
Close cursors explicitly for each query
erlend-aasland Jan 24, 2024
608c229
Address review from Donghee, Serhiy, and myself:
erlend-aasland Jan 24, 2024
3d53054
Pull in main
erlend-aasland Jan 24, 2024
75f7c6a
Add URI tests, fix whitespace, catch errors during db creation
erlend-aasland Jan 24, 2024
3ac010c
Enable test on Windows
erlend-aasland Jan 24, 2024
08c3848
Use as_uri() instead
erlend-aasland Jan 24, 2024
775ff91
Skip tests with relative paths for now
erlend-aasland Jan 25, 2024
4ba9607
Add prelim docstring for dbm.sqlite3.open
erlend-aasland Jan 25, 2024
dc0ba26
Amend Windows URI expected results
erlend-aasland Jan 25, 2024
37f99ab
For now, just remove the relative URI tests on Windows
erlend-aasland Jan 25, 2024
177b200
Document dbm.sqlite3.open signature
erlend-aasland Jan 25, 2024
1a661a5
Update Lib/test/test_dbm.py
erlend-aasland Jan 25, 2024
fae8603
Update Lib/test/test_dbm.py
erlend-aasland Jan 25, 2024
d5fc39c
Only test URI substitution and normalisation; discard prefix
erlend-aasland Jan 25, 2024
d350ce2
Pull in changes from PR
erlend-aasland Jan 25, 2024
a8db17f
Doc amendments
erlend-aasland Jan 25, 2024
1f87517
Pull in main
erlend-aasland Jan 26, 2024
898dd71
Pull in main
erlend-aasland Jan 26, 2024
bb49fab
Mark up flags as list; it renders better
erlend-aasland Jan 26, 2024
26a2c69
Apply suggestions from code review
erlend-aasland Jan 26, 2024
ae53f95
Address review: use closing() in _execute() wrapper
erlend-aasland Jan 26, 2024
926ef1a
Update Doc/library/dbm.rst
erlend-aasland Jan 26, 2024
c0878d9
Add support for 'mode' param in dbm.sqlite3.open()
erlend-aasland Jan 30, 2024
b7111ee
Pull in main
erlend-aasland Jan 30, 2024
48972a6
Pull in main
erlend-aasland Jan 31, 2024
d6d7c66
Fix test_misuse_reinit()
erlend-aasland Jan 31, 2024
bc849c3
Merge branch 'main' into sqlite/dbm
erlend-aasland Feb 14, 2024
e782fad
Address Serhiy's offline remark: coerce keys/values to bytes
erlend-aasland Feb 14, 2024
b1b9a9b
Align docs to e782fad38f
erlend-aasland Feb 14, 2024
34930cb
Compat with other backends: silently coerce keys to bytes
erlend-aasland Feb 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 70 additions & 2 deletions Doc/library/dbm.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@

--------------

:mod:`dbm` is a generic interface to variants of the DBM database ---
:mod:`dbm.gnu` or :mod:`dbm.ndbm`. If none of these modules is installed, the
:mod:`dbm` is a generic interface to variants of the DBM database:

* :mod:`dbm.sqlite3`
* :mod:`dbm.gnu`
* :mod:`dbm.ndbm`

If none of these modules are installed, the
slow-but-simple implementation in module :mod:`dbm.dumb` will be used. There
is a `third party interface <https://www.jcea.es/programacion/pybsddb.htm>`_ to
the Oracle Berkeley DB.
Expand Down Expand Up @@ -129,6 +134,69 @@ then prints out the contents of the database::

The individual submodules are described in the following sections.

.. Substitutions for the flag docs; all submodules use the same text.

.. |flag_r| replace::
Open existing database for reading only (default)

.. |flag_w| replace::
Open existing database for reading and writing

.. |flag_c| replace::
Open database for reading and writing, creating it if it doesn't exist.

.. |flag_n| replace::
Always create a new, empty database, open for reading and writing.

:mod:`dbm.sqlite3` --- SQLite backend for dbm
----------------------------------------------

.. module:: dbm.sqlite3
:platform: All
:synopsis: SQLite backend for dbm

.. versionadded:: 3.13

**Source code:** :source:`Lib/dbm/sqlite3.py`

--------------

This module uses the standard library :mod:`sqlite3` to provide an
SQLite backend for the :mod:`dbm` module.
The files created by :mod:`dbm.sqlite3` can thus be opened by :mod:`sqlite3`,
or any other SQLite browser, including the SQLite CLI.

.. function:: open(filename, flag="r", mode=None)

Open an SQLite database.
The returned object, behaves like a :term:`mapping`,
but also implements a :meth:`!close` method
and supports a "closing" context manager via the :keyword:`with` keyword.

Neither keys nor values are coerced to a specific type
before being stored in the database.

:param filename:
The path to the database to be opened.
:type filename: :term:`path-like object`

:param str flag:

.. list-table::
:header-rows: 1

* - ``'r'``
- |flag_r|
* - ``'w'``
- |flag_w|
* - ``'c'``
- |flag_c|
* - ``'n'``
- |flag_n|

:param mode:
This parameter is ignored by the :mod:`!dbm.sqlite3` module.


:mod:`dbm.gnu` --- GNU's reinterpretation of dbm
------------------------------------------------
Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.13.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ dbm
from the database.
(Contributed by Donghee Na in :gh:`107122`.)

* Add new :mod:`dbm.sqlite3` backend.
(Contributed by Raymond Hettinger and Erlend E. Aasland in :gh:`100414`.)

doctest
-------

Expand Down
11 changes: 11 additions & 0 deletions Lib/dbm/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ class error(Exception):
except ImportError:
ndbm = None

try:
import sqlite3
_names.insert(0, 'dbm.sqlite3')
_has_sqlite3 = True
except ImportError:
_has_sqlite3 = False


def open(file, flag='r', mode=0o666):
"""Open or create database at path given by *file*.
Expand Down Expand Up @@ -164,6 +171,10 @@ def whichdb(filename):
if len(s) != 4:
return ""

# Check for SQLite3 header string.
if s16 == b"SQLite format 3\0":
return "dbm.sqlite3" if _has_sqlite3 else ""

# Convert to 4-byte int in native byte order -- return "" if impossible
try:
(magic,) = struct.unpack("=l", s)
Expand Down
104 changes: 104 additions & 0 deletions Lib/dbm/sqlite3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import os
import sqlite3
from contextlib import suppress, closing
from collections.abc import MutableMapping

BUILD_TABLE = """
CREATE TABLE Dict (
key TEXT NOT NULL,
value BLOB NOT NULL,
PRIMARY KEY (key)
)
"""
GET_SIZE = "SELECT COUNT (key) FROM Dict"
LOOKUP_KEY = "SELECT value FROM Dict WHERE key = ? LIMIT 1"
STORE_KV = "REPLACE INTO Dict (key, value) VALUES (?, ?)"
DELETE_KEY = "DELETE FROM Dict WHERE key = ?"
ITER_KEYS = "SELECT key FROM Dict"


def _normalize_uri_path(path):
path = path.replace("?", "%3f")
path = path.replace("#", "%23")
while "//" in path:
path = path.replace("//", "/")
return path


class _Database(MutableMapping):

def __init__(self, path, flag):
match flag:
case "r":
flag = "ro"
case "w":
flag = "rw"
case "c":
flag = "rwc"
case "n":
flag = "rwc"
try:
os.remove(path)
except FileNotFoundError:
pass
case _:
raise ValueError("Flag must be one of 'r', 'w', 'c', or 'n', not",
repr(flag))

# We use the URI format when opening the database.
path = os.fsdecode(path)
if not path or path == ":memory:":
uri = "file:?mode=memory"
else:
path = _normalize_uri_path(path)
uri = f"file:{path}?mode={flag}"

self.cx = sqlite3.connect(uri, autocommit=True, uri=True)
self.cx.execute("PRAGMA journal_mode = wal")
with suppress(sqlite3.OperationalError):
self.cx.execute(BUILD_TABLE)

def __len__(self):
return self.cx.execute(GET_SIZE).fetchone()[0]

def __getitem__(self, key):
rows = [row for row in self.cx.execute(LOOKUP_KEY, (key,))]
if not rows:
raise KeyError(key)
assert len(rows) == 1
row = rows[0]
return row[0]

def __setitem__(self, key, value):
self.cx.execute(STORE_KV, (key, value));

def __delitem__(self, key):
if key not in self:
raise KeyError(key)
self.cx.execute(DELETE_KEY, (key,));

def __iter__(self):
with closing(self.cx.execute(ITER_KEYS)) as cu:
for row in cu:
yield row[0]

def close(self):
self.cx.close()
self.cx = None

def keys(self):
return list(super().keys())

def __enter__(self):
return self

def __exit__(self, *args):
self.close()


class error(OSError, sqlite3.Error):
pass


def open(path, flag="r", mode=None):
return _Database(path, flag)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :mod:`dbm.sqlite3` as a backend to :mod:`dbm`.
Patch by Raymond Hettinger and Erlend E. Aasland.
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy