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.
@@ -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 []
@@ -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)