Skip to content

Commit 872a273

Browse files
committed
Add backend support for PostgreSQL
1 parent 9727ce1 commit 872a273

File tree

16 files changed

+186
-49
lines changed

16 files changed

+186
-49
lines changed

.github/workflows/main.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ jobs:
3737
max-parallel: 4
3838
matrix:
3939
python-version: [3.7, 3.8, 3.9, '3.10', '3.11']
40+
services:
41+
postgresql:
42+
image: postgres:16
43+
ports:
44+
- 5432:5432
45+
env:
46+
POSTGRES_HOST_AUTH_METHOD: trust
4047
steps:
4148
- uses: actions/checkout@v3
4249
- name: Set up Python ${{ matrix.python-version }}
@@ -66,6 +73,13 @@ jobs:
6673
ls -l dist
6774
documentation:
6875
runs-on: ubuntu-latest
76+
services:
77+
postgresql:
78+
image: postgres:16
79+
ports:
80+
- 5432:5432
81+
env:
82+
POSTGRES_HOST_AUTH_METHOD: trust
6983
steps:
7084
- uses: actions/checkout@v3
7185
- name: Setup python

README.rst

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,23 @@ Usage
7979
Please access http://localhost:8000/search.html
8080
8181
82+
Development
83+
===========
84+
85+
Install package in development mode::
86+
87+
pip install --editable='.[cli,docs,test]' --prefer-binary
88+
89+
Start PostgreSQL server::
90+
91+
docker run --rm -it --publish=5432:5432 --env "POSTGRES_HOST_AUTH_METHOD=trust" postgres:16 postgres -c log_statement=all
92+
93+
Invoke software tests::
94+
95+
export POSTGRES_LOG_STATEMENT=all
96+
pytest -vvv
97+
98+
8299
.. _atsphinx-sqlite3fts: https://pypi.org/project/atsphinx-sqlite3fts/
83100
.. _Kazuya Takei: https://github.com/attakei
84101
.. _readthedocs-sphinx-search: https://github.com/readthedocs/readthedocs-sphinx-search

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
}
3333
# atsphinx-sqlite3fts
3434
sqlite3fts_use_search_html = True
35+
sqlite3fts_database_url = "postgresql://postgres@localhost:5432"
3536

3637

3738
def setup(app): # noqa: D103

docs/getting-started.rst

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,11 @@ You can build database by ``sqlite`` builder.
4747

4848
.. code-block:: console
4949
50-
make sqlite
51-
sqlite3 _build/sqlite/db.sqlite
50+
make fts-index
51+
52+
.. code-block:: console
53+
54+
psql postgresql://postgres@localhost:5432/ --command 'SELECT * FROM document;'
5255
5356
.. code-block:: sqlite3
5457

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dynamic = ["version", "description"]
3535
dependencies = [
3636
"docutils",
3737
"peewee",
38+
"psycopg2[binary]",
3839
"sphinx<7",
3940
]
4041

src/atsphinx/sqlite3fts/__init__.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Sphinx document searcher using SQLite3."""
1+
"""Sphinx document searcher using SQL database."""
22
from sphinx.application import Sphinx
33

44
from . import builders, events
@@ -10,9 +10,10 @@ def setup(app: Sphinx):
1010
"""Entrypoint as Sphinx extension."""
1111
app.add_config_value("sqlite3fts_exclude_pages", [], "env")
1212
app.add_config_value("sqlite3fts_use_search_html", False, "env")
13-
app.add_builder(builders.SqliteBuilder)
13+
app.add_config_value("sqlite3fts_database_url", None, "env")
14+
app.add_builder(builders.FtsIndexer)
1415
app.connect("config-inited", events.setup_search_html)
15-
app.connect("builder-inited", events.configure_database)
16+
app.connect("config-inited", events.configure_database)
1617
app.connect("html-page-context", events.register_document)
1718
app.connect("build-finished", events.save_database)
1819
return {

src/atsphinx/sqlite3fts/builders.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
from . import models, services
88

99

10-
class SqliteBuilder(Builder):
10+
class FtsIndexer(Builder):
1111
"""Single database generation builder.
1212
1313
This is custom builder to generate only SQLite database file
1414
"""
1515

16-
name = "sqlite"
16+
name = "fts-index"
1717
allow_parallel = True
1818

1919
def get_target_uri(self, docname: str, typ: str = None) -> str: # noqa: D102
@@ -23,7 +23,11 @@ def get_outdated_docs(self) -> str: # noqa: D102
2323
return "db.sqlite"
2424

2525
def prepare_writing(self, docnames: Set[str]) -> None: # noqa: D102
26-
pass
26+
from atsphinx.sqlite3fts.models import Content, Document, Section
27+
28+
Document.truncate_table(cascade=True)
29+
Section.truncate_table(cascade=True)
30+
Content.truncate_table(cascade=True)
2731

2832
def write_doc(self, docname: str, doctree: nodes.document) -> None:
2933
"""Register content of document into database.

src/atsphinx/sqlite3fts/events.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,23 @@ def _generate_search_html(app: Sphinx):
3333
app.connect("html-collect-pages", _generate_search_html)
3434

3535

36-
def configure_database(app: Sphinx):
37-
"""Connect database for project output."""
36+
def configure_database(app: Sphinx, config: Config):
37+
"""
38+
Connect database for project output.
39+
40+
TODO: Add support for multiple database backends?
41+
"""
42+
# SQLite
43+
"""
3844
db_path = Path(app.outdir) / "db.sqlite"
3945
if db_path.exists():
4046
db_path.unlink()
41-
models.initialize(db_path)
47+
models.initialize("sqlite", db_path)
48+
"""
49+
# PostgreSQL
50+
if not app.config.sqlite3fts_database_url:
51+
raise ValueError("Configuring database failed")
52+
models.initialize("postgresql", app.config.sqlite3fts_database_url)
4253

4354

4455
def register_document(

src/atsphinx/sqlite3fts/models.py

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,49 @@
66
77
TODO: Add support for multiple database backends?
88
"""
9+
import os
910
from pathlib import Path
1011
from typing import Iterable
1112

12-
from playhouse import sqlite_ext
13+
from playhouse import postgres_ext as ext
1314

14-
db_proxy = sqlite_ext.DatabaseProxy()
15+
db_proxy = ext.DatabaseProxy()
1516

1617

17-
class Document(sqlite_ext.Model):
18+
class Document(ext.Model):
1819
"""Document main model."""
1920

20-
page = sqlite_ext.TextField(null=False, unique=True)
21-
title = sqlite_ext.TextField(null=False)
21+
page = ext.TextField(null=False, unique=True)
22+
title = ext.TextField(null=False)
2223

2324
class Meta: # noqa: D106
2425
database = db_proxy
2526

2627

27-
class Section(sqlite_ext.Model):
28+
class Section(ext.Model):
2829
"""Section unit of document."""
2930

30-
document = sqlite_ext.ForeignKeyField(Document)
31-
root = sqlite_ext.BooleanField(default=False, null=False)
32-
ref = sqlite_ext.TextField(null=False)
33-
title = sqlite_ext.TextField(null=False)
34-
body = sqlite_ext.TextField(null=False)
31+
document = ext.ForeignKeyField(Document)
32+
root = ext.BooleanField(default=False, null=False)
33+
ref = ext.TextField(null=False)
34+
title = ext.TextField(null=False)
35+
body = ext.TextField(null=False)
3536

3637
class Meta: # noqa: D106
3738
database = db_proxy
3839

3940

40-
class Content(sqlite_ext.FTS5Model):
41+
class Content(ext.Model):
4142
"""Searching model."""
4243

43-
rowid = sqlite_ext.RowIDField()
44-
title = sqlite_ext.SearchField()
45-
body = sqlite_ext.SearchField()
44+
rowid = ext.IntegerField()
45+
title = ext.TextField()
46+
body = ext.TextField()
4647

4748
class Meta: # noqa: D106
4849
database = db_proxy
49-
options = {"tokenize": "trigram"}
50+
# TODO: This is an option from SQLite, it does not work on other DBMS.
51+
# options = {"tokenize": "trigram"}
5052

5153

5254
def store_document(document: Document, sections: Iterable[Section]):
@@ -74,16 +76,25 @@ def search_documents(keyword: str) -> Iterable[Section]:
7476
)
7577

7678

77-
def bind(db_path: Path):
79+
def bind(db_type: str, db_path: Path):
7880
"""Bind connection.
7981
8082
This works only set db into proxy, not included creating tables.
8183
"""
82-
db = sqlite_ext.SqliteExtDatabase(db_path)
84+
if db_type == "sqlite":
85+
db = ext.SqliteExtDatabase(db_path)
86+
elif db_type == "postgresql":
87+
db = ext.PostgresqlExtDatabase(db_path)
88+
if "POSTGRES_LOG_STATEMENT" in os.environ:
89+
db.execute_sql(
90+
f"SET log_statement='{os.environ['POSTGRES_LOG_STATEMENT']}';"
91+
)
92+
else:
93+
raise ValueError(f"Unknown database type: {db_type}")
8394
db_proxy.initialize(db)
8495

8596

86-
def initialize(db_path: Path):
97+
def initialize(db_type: str, db_path: Path):
8798
"""Bind connection and create tables."""
88-
bind(db_path)
99+
bind(db_type, db_path)
89100
db_proxy.create_tables([Document, Section, Content])

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Test package."""

tests/conftest.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,40 @@
11
"""Configuration for pytest."""
2+
import os
3+
24
import pytest
35
from sphinx.testing.path import path
46

7+
from tests.util import Database
8+
59
pytest_plugins = "sphinx.testing.fixtures"
610
collect_ignore = ["roots"]
711

812

13+
@pytest.fixture(scope="session")
14+
def database_dsn():
15+
"""Pytest fixture providing the database connection string for software tests."""
16+
return "postgresql://postgres@localhost:5432"
17+
18+
19+
@pytest.fixture(scope="session")
20+
def database(database_dsn):
21+
"""Pytest fixture returning a database wrapper object."""
22+
return Database(database_dsn)
23+
24+
25+
@pytest.fixture
26+
def conn(database):
27+
"""
28+
Pytest fixture returning a database wrapper object, with content cleared.
29+
30+
This is intended to provide each test case with a blank slate.
31+
"""
32+
if "POSTGRES_LOG_STATEMENT" in os.environ:
33+
database.execute(f"SET log_statement='{os.environ['POSTGRES_LOG_STATEMENT']}';")
34+
database.reset()
35+
return database
36+
37+
938
@pytest.fixture(scope="session")
1039
def rootdir():
1140
"""Set root directory to use testing sphinx project."""

tests/roots/test-default/conf.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
"atsphinx.sqlite3fts",
44
]
55

6+
sqlite3fts_database_url = "postgresql://postgres@localhost:5432"
7+
68
# To skip toctree
79
rst_prolog = """
810
:orphan:

tests/test_builders.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11
"""Test cases for custom builders."""
2-
import sqlite3
3-
from pathlib import Path
4-
52
import pytest
63
from sphinx.testing.util import SphinxTestApp
74

85

9-
@pytest.mark.sphinx("sqlite", testroot="default")
10-
def test___work_builder(app: SphinxTestApp, status, warning): # noqa
6+
@pytest.mark.sphinx("fts-index", testroot="default")
7+
def test___work_builder(app: SphinxTestApp, status, warning, conn): # noqa
118
app.build()
12-
db_path = Path(app.outdir) / "db.sqlite"
13-
assert db_path.exists()
14-
conn = sqlite3.connect(db_path)
159
assert len(conn.execute("SELECT * FROM document").fetchall()) > 0
1610
assert len(conn.execute("SELECT * FROM section").fetchall()) > 0
1711
assert len(conn.execute("SELECT * FROM content").fetchall()) > 0

tests/test_events.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,9 @@
11
"""Test cases for result of events."""
2-
import sqlite3
3-
from pathlib import Path
4-
52
import pytest
63
from sphinx.testing.util import SphinxTestApp
74

85

96
@pytest.mark.sphinx("html", testroot="default")
10-
def test___work_builder(app: SphinxTestApp, status, warning): # noqa
7+
def test___work_builder(app: SphinxTestApp, status, warning, conn): # noqa
118
app.build()
12-
db_path = Path(app.outdir) / "db.sqlite"
13-
assert db_path.exists()
14-
conn = sqlite3.connect(db_path)
159
assert len(conn.execute("SELECT * FROM document").fetchall()) > 0

tests/test_services.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
RST_DIR = Path(__file__).parent / "rst"
1111

1212

13-
@pytest.mark.sphinx("sqlite", testroot="default")
13+
@pytest.mark.sphinx("fts-index", testroot="default")
1414
def test_single_section(app: SphinxTestApp): # noqa
1515
rst_path = RST_DIR / "single-section.rst"
1616
doctree = parse(app, rst_path.read_text())
@@ -24,7 +24,7 @@ def test_single_section(app: SphinxTestApp): # noqa
2424
assert sections[0].root
2525

2626

27-
@pytest.mark.sphinx("sqlite", testroot="default")
27+
@pytest.mark.sphinx("fts-index", testroot="default")
2828
def test_sub_sections(app: SphinxTestApp): # noqa
2929
rst_path = RST_DIR / "sub-sections.rst"
3030
doctree = parse(app, rst_path.read_text())
@@ -35,7 +35,7 @@ def test_sub_sections(app: SphinxTestApp): # noqa
3535
assert not sections[1].root
3636

3737

38-
@pytest.mark.sphinx("sqlite", testroot="default")
38+
@pytest.mark.sphinx("fts-index", testroot="default")
3939
def test_multiple_sub_sections(app: SphinxTestApp): # noqa
4040
rst_path = RST_DIR / "multiple-sub-sections.rst"
4141
doctree = parse(app, rst_path.read_text())

0 commit comments

Comments
 (0)