Skip to content

Commit 70d9d42

Browse files
committed
initial commit
0 parents  commit 70d9d42

File tree

11 files changed

+571
-0
lines changed

11 files changed

+571
-0
lines changed

.envrc

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
has nix && use flake

.github/workflows/test.yml

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Test
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
pull_request:
7+
branches: [ main ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install .[dev]
28+
29+
- name: Run tests
30+
run: |
31+
pytest --cov=libsql_sqlalchemy tests/

.gitignore

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.direnv
2+
3+
*.egg-info/
4+
*.egg
5+
__pycache__/
6+
*.py[cod]
7+
*$py.class
8+
dist/
9+
build/
10+
.pytest_cache/
11+
.coverage
12+
htmlcov/

.python-version

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.12

README.md

Whitespace-only changes.

flake.lock

+61
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
description = "A very basic flake";
3+
4+
inputs = {
5+
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
6+
flake-utils = {
7+
url = "github:numtide/flake-utils";
8+
};
9+
};
10+
11+
outputs = { self, nixpkgs, flake-utils }:
12+
flake-utils.lib.eachDefaultSystem (system:
13+
let
14+
pkgs = import nixpkgs { inherit system; };
15+
in
16+
{
17+
formatter = pkgs.nixpkgs-fmt;
18+
devShells.default = with pkgs; mkShell {
19+
buildInputs = [
20+
ruff
21+
uv
22+
poetry
23+
python312
24+
python312Packages.pip
25+
python312Packages.virtualenv
26+
python312Packages.pytest
27+
python312Packages.pyperf
28+
];
29+
shellHook = ''
30+
virtualenv .venv
31+
source .venv/bin/activate
32+
'';
33+
};
34+
});
35+
}

libsql_sqlalchemy/__init__.py

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import os
2+
import urllib.parse
3+
4+
from sqlalchemy import util
5+
from sqlalchemy.dialects import registry as _registry
6+
from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite
7+
8+
from libsql_experimental import Connection
9+
10+
__version__ = "0.1.0-pre"
11+
12+
_registry.register("sqlite.libsql", "libsql_sqlalchemy", "SQLiteDialect_libsql")
13+
14+
15+
def _build_connection_url(url, query, secure):
16+
# sorting of keys is for unit test support
17+
query_str = urllib.parse.urlencode(sorted(query.items()))
18+
19+
if not url.host:
20+
if query_str:
21+
return f"{url.database}?{query_str}"
22+
return url.database
23+
elif secure: # yes, pop to remove
24+
scheme = "https"
25+
else:
26+
scheme = "http"
27+
28+
if url.username and url.password:
29+
netloc = f"{url.username}:{url.password}@{url.host}"
30+
elif url.username:
31+
netloc = f"{url.username}@{url.host}"
32+
else:
33+
netloc = url.host
34+
35+
if url.port:
36+
netloc += f":{url.port}"
37+
38+
return urllib.parse.urlunsplit(
39+
(
40+
scheme,
41+
netloc,
42+
url.database or "",
43+
query_str,
44+
"", # fragment
45+
)
46+
)
47+
48+
49+
class SQLiteDialect_libsql(SQLiteDialect_pysqlite):
50+
driver = "libsql"
51+
# need to be set explicitly
52+
supports_statement_cache = SQLiteDialect_pysqlite.supports_statement_cache
53+
54+
@classmethod
55+
def import_dbapi(cls):
56+
import libsql_experimental as libsql
57+
58+
return libsql
59+
60+
def on_connect(self):
61+
import libsql_experimental as libsql
62+
63+
sqlite3_connect = super().on_connect()
64+
65+
def connect(conn):
66+
# LibSQL: there is no support for create_function()
67+
if isinstance(conn, Connection):
68+
return
69+
return sqlite3_connect(conn)
70+
71+
return connect
72+
73+
def create_connect_args(self, url):
74+
pysqlite_args = (
75+
("uri", bool),
76+
("timeout", float),
77+
("isolation_level", str),
78+
("detect_types", int),
79+
("check_same_thread", bool),
80+
("cached_statements", int),
81+
("secure", bool), # LibSQL extra, selects between ws and wss
82+
)
83+
opts = url.query
84+
libsql_opts = {}
85+
for key, type_ in pysqlite_args:
86+
util.coerce_kw_type(opts, key, type_, dest=libsql_opts)
87+
88+
if url.host:
89+
libsql_opts["uri"] = True
90+
91+
if libsql_opts.get("uri", False):
92+
uri_opts = dict(opts)
93+
# here, we are actually separating the parameters that go to
94+
# sqlite3/pysqlite vs. those that go the SQLite URI. What if
95+
# two names conflict? again, this seems to be not the case right
96+
# now, and in the case that new names are added to
97+
# either side which overlap, again the sqlite3/pysqlite parameters
98+
# can be passed through connect_args instead of in the URL.
99+
# If SQLite native URIs add a parameter like "timeout" that
100+
# we already have listed here for the python driver, then we need
101+
# to adjust for that here.
102+
for key, type_ in pysqlite_args:
103+
uri_opts.pop(key, None)
104+
105+
secure = libsql_opts.pop("secure", False)
106+
connect_url = _build_connection_url(url, uri_opts, secure)
107+
else:
108+
connect_url = url.database or ":memory:"
109+
if connect_url != ":memory:":
110+
connect_url = os.path.abspath(connect_url)
111+
112+
libsql_opts.setdefault("check_same_thread", not self._is_url_file_db(url))
113+
114+
return ([connect_url], libsql_opts)
115+
116+
117+
dialect = SQLiteDialect_libsql

pyproject.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[project]
2+
name = "libsql-sqlalchemy"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"libsql-experimental>=0.0.47",
9+
"sqlalchemy>=2.0.0",
10+
]
11+
12+
[dependency-groups]
13+
dev = [
14+
"pytest>=8.3.5",
15+
"pytest-cov>=4.1.0",
16+
]
17+
18+
[project.entry-points."sqlalchemy.dialects"]
19+
"sqlite.libsql" = "libsql_sqlalchemy:SQLiteDialect_libsql"

tests/test_basic.py

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import pytest
2+
from sqlalchemy import create_engine, Column, Integer, String, text
3+
from sqlalchemy.orm import declarative_base, sessionmaker
4+
5+
Base = declarative_base()
6+
7+
8+
class User(Base):
9+
__tablename__ = "users"
10+
11+
id = Column(Integer, primary_key=True)
12+
name = Column(String)
13+
email = Column(String)
14+
15+
16+
@pytest.fixture
17+
def engine():
18+
return create_engine("sqlite+libsql://")
19+
20+
21+
@pytest.fixture
22+
def session(engine):
23+
Base.metadata.create_all(engine)
24+
Session = sessionmaker(bind=engine)
25+
session = Session()
26+
yield session
27+
session.close()
28+
Base.metadata.drop_all(engine)
29+
30+
31+
def test_connection(engine):
32+
with engine.connect() as conn:
33+
result = conn.execute(text("SELECT 1"))
34+
assert result.scalar() == 1
35+
36+
37+
def test_create_table(session):
38+
with session.connection() as conn:
39+
result = conn.execute(
40+
text("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
41+
)
42+
assert result.scalar() == "users"
43+
44+
45+
def test_insert_and_query(session):
46+
user = User(name="Test User", email="[email protected]")
47+
session.add(user)
48+
session.commit()
49+
50+
queried_user = session.query(User).first()
51+
assert queried_user.name == "Test User"
52+
assert queried_user.email == "[email protected]"
53+
54+
55+
def test_update(session):
56+
user = User(name="Test User", email="[email protected]")
57+
session.add(user)
58+
session.commit()
59+
60+
user.name = "Updated User"
61+
session.commit()
62+
63+
updated_user = session.query(User).first()
64+
assert updated_user.name == "Updated User"
65+
66+
67+
def test_delete(session):
68+
user = User(name="Test User", email="[email protected]")
69+
session.add(user)
70+
session.commit()
71+
72+
session.delete(user)
73+
session.commit()
74+
75+
assert session.query(User).count() == 0

0 commit comments

Comments
 (0)