django-libsql-backend 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_libsql/__init__.py +28 -0
- django_libsql/base.py +528 -0
- django_libsql/client.py +20 -0
- django_libsql/creation.py +39 -0
- django_libsql/features.py +81 -0
- django_libsql/introspection.py +20 -0
- django_libsql/operations.py +341 -0
- django_libsql/schema.py +42 -0
- django_libsql_backend-0.1.0.dist-info/METADATA +318 -0
- django_libsql_backend-0.1.0.dist-info/RECORD +13 -0
- django_libsql_backend-0.1.0.dist-info/WHEEL +5 -0
- django_libsql_backend-0.1.0.dist-info/licenses/LICENSE +21 -0
- django_libsql_backend-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Introspection for the libSQL/Turso backend.
|
|
3
|
+
|
|
4
|
+
Delegates to Django's built-in SQLite DatabaseIntrospection via lazy import
|
|
5
|
+
to avoid circular import issues at module-load time.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DatabaseIntrospection:
|
|
10
|
+
"""Proxy that lazily imports SQLite's introspection on first use."""
|
|
11
|
+
|
|
12
|
+
def __init__(self, connection, *args, **kwargs):
|
|
13
|
+
from django.db.backends.sqlite3.introspection import (
|
|
14
|
+
DatabaseIntrospection as _SQLiteIntrospection,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
self._wrapped = _SQLiteIntrospection(connection, *args, **kwargs)
|
|
18
|
+
|
|
19
|
+
def __getattr__(self, name):
|
|
20
|
+
return getattr(self._wrapped, name)
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Operations for the libSQL/Turso backend — SQLite-compatible SQL generation."""
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import decimal
|
|
5
|
+
import uuid
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from itertools import chain
|
|
8
|
+
|
|
9
|
+
from django.conf import settings
|
|
10
|
+
from django.core.exceptions import FieldError
|
|
11
|
+
from django.db import DatabaseError, NotSupportedError, models
|
|
12
|
+
from django.db.backends.base.operations import BaseDatabaseOperations
|
|
13
|
+
from django.db.models.constants import OnConflict
|
|
14
|
+
from django.db.models.expressions import Col
|
|
15
|
+
from django.utils import timezone
|
|
16
|
+
from django.utils.dateparse import parse_date, parse_datetime, parse_time
|
|
17
|
+
from django.utils.functional import cached_property
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DatabaseOperations(BaseDatabaseOperations):
|
|
21
|
+
cast_char_field_without_max_length = "text"
|
|
22
|
+
cast_data_types = {
|
|
23
|
+
"DateField": "TEXT",
|
|
24
|
+
"DateTimeField": "TEXT",
|
|
25
|
+
}
|
|
26
|
+
explain_prefix = "EXPLAIN QUERY PLAN"
|
|
27
|
+
jsonfield_datatype_values = frozenset(["null", "false", "true"])
|
|
28
|
+
|
|
29
|
+
def bulk_batch_size(self, fields, objs):
|
|
30
|
+
max_params = 999
|
|
31
|
+
if len(fields) == 1:
|
|
32
|
+
return 500
|
|
33
|
+
elif len(fields) > 1:
|
|
34
|
+
return max_params // len(fields)
|
|
35
|
+
else:
|
|
36
|
+
return len(objs)
|
|
37
|
+
|
|
38
|
+
def check_expression_support(self, expression):
|
|
39
|
+
bad_fields = (models.DateField, models.DateTimeField, models.TimeField)
|
|
40
|
+
bad_aggregates = (models.Sum, models.Avg, models.Variance, models.StdDev)
|
|
41
|
+
if isinstance(expression, bad_aggregates):
|
|
42
|
+
for expr in expression.get_source_expressions():
|
|
43
|
+
try:
|
|
44
|
+
output_field = expr.output_field
|
|
45
|
+
except (AttributeError, FieldError):
|
|
46
|
+
pass
|
|
47
|
+
else:
|
|
48
|
+
if isinstance(output_field, bad_fields):
|
|
49
|
+
raise NotSupportedError(
|
|
50
|
+
"Sum, Avg, StdDev, Variance aggregations on date/time "
|
|
51
|
+
"fields not supported on SQLite (stored as text)."
|
|
52
|
+
)
|
|
53
|
+
if (
|
|
54
|
+
isinstance(expression, models.Aggregate)
|
|
55
|
+
and expression.distinct
|
|
56
|
+
and len(expression.source_expressions) > 1
|
|
57
|
+
):
|
|
58
|
+
raise NotSupportedError(
|
|
59
|
+
"SQLite doesn't support DISTINCT on aggregate functions "
|
|
60
|
+
"accepting multiple arguments."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def date_extract_sql(self, lookup_type, sql, params):
|
|
64
|
+
return f"django_date_extract(%s, {sql})", (lookup_type.lower(), *params)
|
|
65
|
+
|
|
66
|
+
def format_for_duration_arithmetic(self, sql):
|
|
67
|
+
return sql
|
|
68
|
+
|
|
69
|
+
def date_trunc_sql(self, lookup_type, sql, params, tzname=None):
|
|
70
|
+
return f"django_date_trunc(%s, {sql}, %s, %s)", (
|
|
71
|
+
lookup_type.lower(),
|
|
72
|
+
*params,
|
|
73
|
+
*self._convert_tznames_to_sql(tzname),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def time_trunc_sql(self, lookup_type, sql, params, tzname=None):
|
|
77
|
+
return f"django_time_trunc(%s, {sql}, %s, %s)", (
|
|
78
|
+
lookup_type.lower(),
|
|
79
|
+
*params,
|
|
80
|
+
*self._convert_tznames_to_sql(tzname),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
def _convert_tznames_to_sql(self, tzname):
|
|
84
|
+
if tzname and settings.USE_TZ:
|
|
85
|
+
return tzname, self.connection.timezone_name
|
|
86
|
+
return None, None
|
|
87
|
+
|
|
88
|
+
def datetime_cast_date_sql(self, sql, params, tzname):
|
|
89
|
+
return f"django_datetime_cast_date({sql}, %s, %s)", (
|
|
90
|
+
*params,
|
|
91
|
+
*self._convert_tznames_to_sql(tzname),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def datetime_cast_time_sql(self, sql, params, tzname):
|
|
95
|
+
return f"django_datetime_cast_time({sql}, %s, %s)", (
|
|
96
|
+
*params,
|
|
97
|
+
*self._convert_tznames_to_sql(tzname),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
def datetime_extract_sql(self, lookup_type, sql, params, tzname):
|
|
101
|
+
return f"django_datetime_extract(%s, {sql}, %s, %s)", (
|
|
102
|
+
lookup_type.lower(),
|
|
103
|
+
*params,
|
|
104
|
+
*self._convert_tznames_to_sql(tzname),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def datetime_trunc_sql(self, lookup_type, sql, params, tzname):
|
|
108
|
+
return f"django_datetime_trunc(%s, {sql}, %s, %s)", (
|
|
109
|
+
lookup_type.lower(),
|
|
110
|
+
*params,
|
|
111
|
+
*self._convert_tznames_to_sql(tzname),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def time_extract_sql(self, lookup_type, sql, params):
|
|
115
|
+
return f"django_time_extract(%s, {sql})", (lookup_type.lower(), *params)
|
|
116
|
+
|
|
117
|
+
def pk_default_value(self):
|
|
118
|
+
return "NULL"
|
|
119
|
+
|
|
120
|
+
def quote_name(self, name):
|
|
121
|
+
if name.startswith('"') and name.endswith('"'):
|
|
122
|
+
return name
|
|
123
|
+
return '"%s"' % name
|
|
124
|
+
|
|
125
|
+
def no_limit_value(self):
|
|
126
|
+
return -1
|
|
127
|
+
|
|
128
|
+
def last_executed_query(self, cursor, sql, params):
|
|
129
|
+
return sql % params if params else sql
|
|
130
|
+
|
|
131
|
+
def __references_graph(self, table_name):
|
|
132
|
+
query = """
|
|
133
|
+
WITH tables AS (
|
|
134
|
+
SELECT %s name
|
|
135
|
+
UNION
|
|
136
|
+
SELECT sqlite_master.name
|
|
137
|
+
FROM sqlite_master
|
|
138
|
+
JOIN tables ON (sql REGEXP %s || tables.name || %s)
|
|
139
|
+
) SELECT name FROM tables;
|
|
140
|
+
"""
|
|
141
|
+
params = (
|
|
142
|
+
table_name,
|
|
143
|
+
r'(?i)\s+references\s+("|\')?',
|
|
144
|
+
r'("|\')?\s*\(',
|
|
145
|
+
)
|
|
146
|
+
with self.connection.cursor() as cursor:
|
|
147
|
+
results = cursor.execute(query, params)
|
|
148
|
+
return [row[0] for row in results.fetchall()]
|
|
149
|
+
|
|
150
|
+
@cached_property
|
|
151
|
+
def _references_graph(self):
|
|
152
|
+
return lru_cache(maxsize=512)(self.__references_graph)
|
|
153
|
+
|
|
154
|
+
def sql_flush(self, style, tables, *, reset_sequences=False, allow_cascade=False):
|
|
155
|
+
if tables and allow_cascade:
|
|
156
|
+
tables = set(
|
|
157
|
+
chain.from_iterable(self._references_graph(table) for table in tables)
|
|
158
|
+
)
|
|
159
|
+
sql = [
|
|
160
|
+
"%s %s %s;"
|
|
161
|
+
% (
|
|
162
|
+
style.SQL_KEYWORD("DELETE"),
|
|
163
|
+
style.SQL_KEYWORD("FROM"),
|
|
164
|
+
style.SQL_FIELD(self.quote_name(table)),
|
|
165
|
+
)
|
|
166
|
+
for table in tables
|
|
167
|
+
]
|
|
168
|
+
if reset_sequences:
|
|
169
|
+
sequences = [{"table": table} for table in tables]
|
|
170
|
+
sql.extend(self.sequence_reset_by_name_sql(style, sequences))
|
|
171
|
+
return sql
|
|
172
|
+
|
|
173
|
+
def sequence_reset_by_name_sql(self, style, sequences):
|
|
174
|
+
if not sequences:
|
|
175
|
+
return []
|
|
176
|
+
return [
|
|
177
|
+
"%s %s %s %s = 0 %s %s %s (%s);"
|
|
178
|
+
% (
|
|
179
|
+
style.SQL_KEYWORD("UPDATE"),
|
|
180
|
+
style.SQL_TABLE(self.quote_name("sqlite_sequence")),
|
|
181
|
+
style.SQL_KEYWORD("SET"),
|
|
182
|
+
style.SQL_FIELD(self.quote_name("seq")),
|
|
183
|
+
style.SQL_KEYWORD("WHERE"),
|
|
184
|
+
style.SQL_FIELD(self.quote_name("name")),
|
|
185
|
+
style.SQL_KEYWORD("IN"),
|
|
186
|
+
", ".join(
|
|
187
|
+
["'%s'" % seq["table"] for seq in sequences]
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
]
|
|
191
|
+
|
|
192
|
+
def adapt_datetimefield_value(self, value):
|
|
193
|
+
if value is None:
|
|
194
|
+
return None
|
|
195
|
+
if hasattr(value, "resolve_expression"):
|
|
196
|
+
return value
|
|
197
|
+
if timezone.is_aware(value):
|
|
198
|
+
if settings.USE_TZ:
|
|
199
|
+
value = timezone.make_naive(value, self.connection.timezone)
|
|
200
|
+
else:
|
|
201
|
+
raise ValueError(
|
|
202
|
+
"SQLite does not support timezone-aware datetimes when "
|
|
203
|
+
"USE_TZ is False."
|
|
204
|
+
)
|
|
205
|
+
return str(value)
|
|
206
|
+
|
|
207
|
+
def adapt_timefield_value(self, value):
|
|
208
|
+
if value is None:
|
|
209
|
+
return None
|
|
210
|
+
if hasattr(value, "resolve_expression"):
|
|
211
|
+
return value
|
|
212
|
+
if timezone.is_aware(value):
|
|
213
|
+
raise ValueError("SQLite does not support timezone-aware times.")
|
|
214
|
+
return str(value)
|
|
215
|
+
|
|
216
|
+
def get_db_converters(self, expression):
|
|
217
|
+
converters = super().get_db_converters(expression)
|
|
218
|
+
internal_type = expression.output_field.get_internal_type()
|
|
219
|
+
if internal_type == "DateTimeField":
|
|
220
|
+
converters.append(self.convert_datetimefield_value)
|
|
221
|
+
elif internal_type == "DateField":
|
|
222
|
+
converters.append(self.convert_datefield_value)
|
|
223
|
+
elif internal_type == "TimeField":
|
|
224
|
+
converters.append(self.convert_timefield_value)
|
|
225
|
+
elif internal_type == "DecimalField":
|
|
226
|
+
converters.append(self.get_decimalfield_converter(expression))
|
|
227
|
+
elif internal_type == "UUIDField":
|
|
228
|
+
converters.append(self.convert_uuidfield_value)
|
|
229
|
+
elif internal_type == "BooleanField":
|
|
230
|
+
converters.append(self.convert_booleanfield_value)
|
|
231
|
+
return converters
|
|
232
|
+
|
|
233
|
+
def convert_datetimefield_value(self, value, expression, connection):
|
|
234
|
+
if value is not None:
|
|
235
|
+
if not isinstance(value, datetime.datetime):
|
|
236
|
+
value = parse_datetime(value)
|
|
237
|
+
if settings.USE_TZ and not timezone.is_aware(value):
|
|
238
|
+
value = timezone.make_aware(value, self.connection.timezone)
|
|
239
|
+
return value
|
|
240
|
+
|
|
241
|
+
def convert_datefield_value(self, value, expression, connection):
|
|
242
|
+
if value is not None:
|
|
243
|
+
if not isinstance(value, datetime.date):
|
|
244
|
+
value = parse_date(value)
|
|
245
|
+
return value
|
|
246
|
+
|
|
247
|
+
def convert_timefield_value(self, value, expression, connection):
|
|
248
|
+
if value is not None:
|
|
249
|
+
if not isinstance(value, datetime.time):
|
|
250
|
+
value = parse_time(value)
|
|
251
|
+
return value
|
|
252
|
+
|
|
253
|
+
def get_decimalfield_converter(self, expression):
|
|
254
|
+
create_decimal = decimal.Context(prec=15).create_decimal_from_float
|
|
255
|
+
if isinstance(expression, Col):
|
|
256
|
+
quantize_value = decimal.Decimal(1).scaleb(
|
|
257
|
+
-expression.output_field.decimal_places
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
def converter(value, expression, connection):
|
|
261
|
+
if value is not None:
|
|
262
|
+
return create_decimal(value).quantize(
|
|
263
|
+
quantize_value, context=expression.output_field.context
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
else:
|
|
267
|
+
|
|
268
|
+
def converter(value, expression, connection):
|
|
269
|
+
if value is not None:
|
|
270
|
+
return create_decimal(value)
|
|
271
|
+
|
|
272
|
+
return converter
|
|
273
|
+
|
|
274
|
+
def convert_uuidfield_value(self, value, expression, connection):
|
|
275
|
+
if value is not None:
|
|
276
|
+
value = uuid.UUID(value)
|
|
277
|
+
return value
|
|
278
|
+
|
|
279
|
+
def convert_booleanfield_value(self, value, expression, connection):
|
|
280
|
+
return bool(value) if value in (1, 0) else value
|
|
281
|
+
|
|
282
|
+
def combine_expression(self, connector, sub_expressions):
|
|
283
|
+
if connector == "^":
|
|
284
|
+
return "POWER(%s)" % ",".join(sub_expressions)
|
|
285
|
+
elif connector == "#":
|
|
286
|
+
return "BITXOR(%s)" % ",".join(sub_expressions)
|
|
287
|
+
return super().combine_expression(connector, sub_expressions)
|
|
288
|
+
|
|
289
|
+
def combine_duration_expression(self, connector, sub_expressions):
|
|
290
|
+
if connector not in ["+", "-", "*", "/"]:
|
|
291
|
+
raise DatabaseError("Invalid connector for timedelta: %s." % connector)
|
|
292
|
+
fn_params = ["'%s'" % connector, *sub_expressions]
|
|
293
|
+
if len(fn_params) > 3:
|
|
294
|
+
raise ValueError("Too many params for timedelta operations.")
|
|
295
|
+
return "django_format_dtdelta(%s)" % ", ".join(fn_params)
|
|
296
|
+
|
|
297
|
+
def integer_field_range(self, internal_type):
|
|
298
|
+
if internal_type in [
|
|
299
|
+
"PositiveBigIntegerField",
|
|
300
|
+
"PositiveIntegerField",
|
|
301
|
+
"PositiveSmallIntegerField",
|
|
302
|
+
]:
|
|
303
|
+
return (0, 9223372036854775807)
|
|
304
|
+
return (-9223372036854775808, 9223372036854775807)
|
|
305
|
+
|
|
306
|
+
def subtract_temporals(self, internal_type, lhs, rhs):
|
|
307
|
+
lhs_sql, lhs_params = lhs
|
|
308
|
+
rhs_sql, rhs_params = rhs
|
|
309
|
+
params = (*lhs_params, *rhs_params)
|
|
310
|
+
if internal_type == "TimeField":
|
|
311
|
+
return "django_time_diff(%s, %s)" % (lhs_sql, rhs_sql), params
|
|
312
|
+
return "django_timestamp_diff(%s, %s)" % (lhs_sql, rhs_sql), params
|
|
313
|
+
|
|
314
|
+
def insert_statement(self, on_conflict=None):
|
|
315
|
+
if on_conflict == OnConflict.IGNORE:
|
|
316
|
+
return "INSERT OR IGNORE INTO"
|
|
317
|
+
return super().insert_statement(on_conflict=on_conflict)
|
|
318
|
+
|
|
319
|
+
def on_conflict_suffix_sql(self, fields, on_conflict, update_fields, unique_fields):
|
|
320
|
+
if (
|
|
321
|
+
on_conflict == OnConflict.UPDATE
|
|
322
|
+
and self.connection.features.supports_update_conflicts_with_target
|
|
323
|
+
):
|
|
324
|
+
return "ON CONFLICT(%s) DO UPDATE SET %s" % (
|
|
325
|
+
", ".join(map(self.quote_name, unique_fields)),
|
|
326
|
+
", ".join(
|
|
327
|
+
[
|
|
328
|
+
f"{field} = EXCLUDED.{field}"
|
|
329
|
+
for field in map(self.quote_name, update_fields)
|
|
330
|
+
]
|
|
331
|
+
),
|
|
332
|
+
)
|
|
333
|
+
return super().on_conflict_suffix_sql(
|
|
334
|
+
fields,
|
|
335
|
+
on_conflict,
|
|
336
|
+
update_fields,
|
|
337
|
+
unique_fields,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
def force_group_by(self):
|
|
341
|
+
return []
|
django_libsql/schema.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Schema editor for the libSQL/Turso backend.
|
|
3
|
+
|
|
4
|
+
Delegates to Django's built-in SQLite DatabaseSchemaEditor via lazy import
|
|
5
|
+
to avoid circular import issues at module-load time.
|
|
6
|
+
|
|
7
|
+
Turso's HTTP API is stateless — each request is a separate SQLite connection.
|
|
8
|
+
PRAGMA settings (like foreign_keys) do not persist across requests. Because of
|
|
9
|
+
this, we override __enter__/__exit__ to skip FK constraint toggling, which
|
|
10
|
+
would be meaningless across stateless HTTP calls anyway.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DatabaseSchemaEditor:
|
|
15
|
+
"""Proxy that lazily imports SQLite's schema editor on first use."""
|
|
16
|
+
|
|
17
|
+
def __init__(self, connection, *args, **kwargs):
|
|
18
|
+
from django.db.backends.sqlite3.schema import (
|
|
19
|
+
DatabaseSchemaEditor as _SQLiteSchemaEditor,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
self._wrapped = _SQLiteSchemaEditor(connection, *args, **kwargs)
|
|
23
|
+
|
|
24
|
+
def __getattr__(self, name):
|
|
25
|
+
return getattr(self._wrapped, name)
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
# Skip SQLite's FK-constraint-disabled check. Turso HTTP is stateless:
|
|
29
|
+
# each request is an independent connection, so PRAGMA foreign_keys=OFF
|
|
30
|
+
# on one request has no effect on the next. We bypass the base schema
|
|
31
|
+
# editor's __enter__ entirely and go straight to its parent.
|
|
32
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
33
|
+
|
|
34
|
+
return BaseDatabaseSchemaEditor.__enter__(self._wrapped)
|
|
35
|
+
|
|
36
|
+
def __exit__(self, *args):
|
|
37
|
+
# Skip check_constraints() and enable_constraint_checking() — they
|
|
38
|
+
# would run on new HTTP connections, not the connections that executed
|
|
39
|
+
# the schema changes.
|
|
40
|
+
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
|
|
41
|
+
|
|
42
|
+
return BaseDatabaseSchemaEditor.__exit__(self._wrapped, *args)
|