|
1 | | -r""" |
2 | | -.. dialect:: sqlite+libsql |
3 | | - :name: libsql |
4 | | - :dbapi: libsql_client.dbapi2 |
5 | | - :connectstring: sqlite+libsql://your-db.your-server.com?authToken=JWT_HERE&secure=true |
6 | | - :url: https://github.com/libsql/libsql-client-py/ |
7 | | -
|
8 | | - Note that this driver is based on the standard SQLAlchemy ``pysqlite`` |
9 | | - dialect, the only change is how to connect, accepting remote URL in |
10 | | - addition to the file dialects |
11 | | -
|
12 | | -Driver |
13 | | ------- |
14 | | -
|
15 | | -The ``libsql_client.dbapi2`` offers compatibility with standard library's |
16 | | -``sqlite3.dbapi2``. For local files or ``:memory:``, the standard library |
17 | | -connection is used. Whenever a host is provided, then the connection |
18 | | -will use LibSQL network protocol via ``ws`` (WebSocket) or ``wss`` |
19 | | -(secure WebSocket), the decision depends on the presence of ``secure=true`` |
20 | | -query parameter. |
21 | | -
|
22 | | -Connect Strings |
23 | | ---------------- |
24 | | -
|
25 | | -In addition to `Pysqlite |
26 | | -<https://docs.sqlalchemy.org/en/20/dialects/sqlite.html#connect-strings>`_, |
27 | | -this driver accepts URL with user, password, hostname and port. |
28 | | -
|
29 | | -These will use the LibSQL network protocol on top of WebSockets. The selection |
30 | | -between ``ws://`` and ``wss://` (secure) is defined by the query/search |
31 | | -parameter ``secure=true``. It defaults to ``secure=false``. |
32 | | -
|
33 | | -If the given URL provides a hostname, then it will default to ``uri=true``. |
34 | | -
|
35 | | -""" # noqa: E501 |
36 | | - |
37 | | -import os |
38 | | -import urllib.parse |
39 | | - |
40 | | -from sqlalchemy import util |
41 | 1 | from sqlalchemy.dialects import registry as _registry |
42 | | -from sqlalchemy.dialects.sqlite.pysqlite import SQLiteDialect_pysqlite |
| 2 | +from sqlalchemy_libsql.libsql import SQLiteDialect_libsql |
43 | 3 |
|
44 | 4 | __version__ = "0.1.0-pre" |
45 | 5 |
|
46 | 6 | _registry.register( |
47 | 7 | "sqlite.libsql", "sqlalchemy_libsql", "SQLiteDialect_libsql" |
48 | 8 | ) |
49 | | - |
50 | | - |
51 | | -def _build_connection_url(url, query, secure): |
52 | | - # sorting of keys is for unit test support |
53 | | - query_str = urllib.parse.urlencode(sorted(query.items())) |
54 | | - |
55 | | - if not url.host: |
56 | | - if query_str: |
57 | | - return f"{url.database}?{query_str}" |
58 | | - return url.database |
59 | | - elif secure: # yes, pop to remove |
60 | | - scheme = "wss" |
61 | | - else: |
62 | | - scheme = "ws" |
63 | | - |
64 | | - if url.username and url.password: |
65 | | - netloc = f"{url.username}:{url.password}@{url.host}" |
66 | | - elif url.username: |
67 | | - netloc = f"{url.username}@{url.host}" |
68 | | - else: |
69 | | - netloc = url.host |
70 | | - |
71 | | - if url.port: |
72 | | - netloc += f":{url.port}" |
73 | | - |
74 | | - return urllib.parse.urlunsplit( |
75 | | - ( |
76 | | - scheme, |
77 | | - netloc, |
78 | | - url.database or "", |
79 | | - query_str, |
80 | | - "", # fragment |
81 | | - ) |
82 | | - ) |
83 | | - |
84 | | - |
85 | | -class SQLiteDialect_libsql(SQLiteDialect_pysqlite): |
86 | | - driver = "libsql" |
87 | | - # need to be set explicitly |
88 | | - supports_statement_cache = SQLiteDialect_pysqlite.supports_statement_cache |
89 | | - |
90 | | - @classmethod |
91 | | - def import_dbapi(cls): |
92 | | - from libsql_client import dbapi2 as libsql_client |
93 | | - |
94 | | - return libsql_client |
95 | | - |
96 | | - def on_connect(self): |
97 | | - from libsql_client.dbapi2 import Connection |
98 | | - |
99 | | - sqlite3_connect = super().on_connect() |
100 | | - |
101 | | - def connect(conn): |
102 | | - # LibSQL: there is no support for create_function() |
103 | | - if isinstance(conn, Connection): |
104 | | - return |
105 | | - return sqlite3_connect(conn) |
106 | | - |
107 | | - return connect |
108 | | - |
109 | | - def create_connect_args(self, url): |
110 | | - pysqlite_args = ( |
111 | | - ("uri", bool), |
112 | | - ("timeout", float), |
113 | | - ("isolation_level", str), |
114 | | - ("detect_types", int), |
115 | | - ("check_same_thread", bool), |
116 | | - ("cached_statements", int), |
117 | | - ("secure", bool), # LibSQL extra, selects between ws and wss |
118 | | - ) |
119 | | - opts = url.query |
120 | | - libsql_opts = {} |
121 | | - for key, type_ in pysqlite_args: |
122 | | - util.coerce_kw_type(opts, key, type_, dest=libsql_opts) |
123 | | - |
124 | | - if url.host: |
125 | | - libsql_opts["uri"] = True |
126 | | - |
127 | | - if libsql_opts.get("uri", False): |
128 | | - uri_opts = dict(opts) |
129 | | - # here, we are actually separating the parameters that go to |
130 | | - # sqlite3/pysqlite vs. those that go the SQLite URI. What if |
131 | | - # two names conflict? again, this seems to be not the case right |
132 | | - # now, and in the case that new names are added to |
133 | | - # either side which overlap, again the sqlite3/pysqlite parameters |
134 | | - # can be passed through connect_args instead of in the URL. |
135 | | - # If SQLite native URIs add a parameter like "timeout" that |
136 | | - # we already have listed here for the python driver, then we need |
137 | | - # to adjust for that here. |
138 | | - for key, type_ in pysqlite_args: |
139 | | - uri_opts.pop(key, None) |
140 | | - |
141 | | - secure = libsql_opts.pop("secure", False) |
142 | | - connect_url = _build_connection_url(url, uri_opts, secure) |
143 | | - else: |
144 | | - connect_url = url.database or ":memory:" |
145 | | - if connect_url != ":memory:": |
146 | | - connect_url = os.path.abspath(connect_url) |
147 | | - |
148 | | - libsql_opts.setdefault( |
149 | | - "check_same_thread", not self._is_url_file_db(url) |
150 | | - ) |
151 | | - |
152 | | - return ([connect_url], libsql_opts) |
153 | | - |
154 | | - |
155 | | -dialect = SQLiteDialect_libsql |
0 commit comments