Skip to content

Commit 9139e84

Browse files
committed
Add sqlalchemy example
1 parent aca1e40 commit 9139e84

File tree

2 files changed

+188
-0
lines changed

2 files changed

+188
-0
lines changed

examples/sqlalchemy/dialect.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
__version__ = "0.1.0-pre"
9+
10+
_registry.register(
11+
"sqlite.libsql", "sqlalchemy_libsql", "SQLiteDialect_libsql"
12+
)
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 = "wss"
25+
else:
26+
scheme = "ws"
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(
113+
"check_same_thread", not self._is_url_file_db(url)
114+
)
115+
116+
return ([connect_url], libsql_opts)
117+
118+
119+
dialect = SQLiteDialect_libsql

examples/sqlalchemy/example.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/usr/bin/env python3
2+
3+
import dialect
4+
5+
from typing import List
6+
from typing import Optional
7+
from sqlalchemy import ForeignKey
8+
from sqlalchemy import String
9+
from sqlalchemy.orm import DeclarativeBase
10+
from sqlalchemy.orm import Mapped
11+
from sqlalchemy.orm import mapped_column
12+
from sqlalchemy.orm import relationship
13+
14+
class Base(DeclarativeBase):
15+
pass
16+
17+
class User(Base):
18+
__tablename__ = "user_account"
19+
id: Mapped[int] = mapped_column(primary_key=True)
20+
name: Mapped[str] = mapped_column(String(30))
21+
fullname: Mapped[Optional[str]]
22+
addresses: Mapped[List["Address"]] = relationship(
23+
back_populates="user", cascade="all, delete-orphan"
24+
)
25+
def __repr__(self) -> str:
26+
return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"
27+
28+
class Address(Base):
29+
__tablename__ = "address"
30+
id: Mapped[int] = mapped_column(primary_key=True)
31+
email_address: Mapped[str]
32+
user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
33+
user: Mapped["User"] = relationship(back_populates="addresses")
34+
def __repr__(self) -> str:
35+
return f"Address(id={self.id!r}, email_address={self.email_address!r})"
36+
37+
from sqlalchemy import create_engine
38+
engine = create_engine("sqlite+libsql://", echo=True)
39+
40+
Base.metadata.create_all(engine)
41+
42+
from sqlalchemy.orm import Session
43+
44+
with Session(engine) as session:
45+
spongebob = User(
46+
name="spongebob",
47+
fullname="Spongebob Squarepants",
48+
addresses=[Address(email_address="[email protected]")],
49+
)
50+
sandy = User(
51+
name="sandy",
52+
fullname="Sandy Cheeks",
53+
addresses=[
54+
Address(email_address="[email protected]"),
55+
Address(email_address="[email protected]"),
56+
],
57+
)
58+
patrick = User(name="patrick", fullname="Patrick Star")
59+
session.add_all([spongebob, sandy, patrick])
60+
session.commit()
61+
62+
from sqlalchemy import select
63+
64+
session = Session(engine)
65+
66+
stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))
67+
68+
for user in session.scalars(stmt):
69+
print(user)

0 commit comments

Comments
 (0)