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,28 @@
1
+ """
2
+ Django database backend for libSQL/Turso.
3
+
4
+ Provides a Django database backend that communicates with remote libSQL/SQLite
5
+ databases via Turso's HTTP REST API (Hrana protocol over HTTP).
6
+
7
+ Usage in Django settings.py::
8
+
9
+ DATABASES = {
10
+ "default": {
11
+ "ENGINE": "django_libsql",
12
+ "NAME": "https://your-database.turso.io",
13
+ "AUTH_TOKEN": "your-jwt-auth-token",
14
+ "OPTIONS": {
15
+ "timeout": 30,
16
+ },
17
+ }
18
+ }
19
+
20
+ The ``NAME`` can be a full URL (``https://db-name.turso.io``) or a bare
21
+ hostname (``db-name.turso.io``) — ``https://`` is prepended automatically.
22
+ """
23
+
24
+ __version__ = "0.1.0"
25
+
26
+ from .base import DatabaseWrapper
27
+
28
+ __all__ = ["DatabaseWrapper", "__version__"]
django_libsql/base.py ADDED
@@ -0,0 +1,528 @@
1
+ """
2
+ Django database backend for libSQL/Turso.
3
+
4
+ Supports both remote Turso/libSQL databases via HTTP REST API and local
5
+ SQLite files. Connection type is auto-detected from the NAME setting.
6
+
7
+ Remote (Turso HTTP):
8
+ Each HTTP request is an independent SQLite connection — there is no
9
+ persistent session. Transactions, savepoints, and connection-stateful
10
+ PRAGMAs behave accordingly.
11
+
12
+ Local (sqlite3 file):
13
+ Uses Python's built-in sqlite3 module. Full transaction support, WAL
14
+ mode, and persistent PRAGMA state within a connection.
15
+ """
16
+
17
+ import json
18
+ import sqlite3
19
+ import urllib.request
20
+ import urllib.error
21
+ import re
22
+ from collections.abc import Mapping
23
+
24
+ from sqlite3 import dbapi2 as Database
25
+
26
+ from django.core.exceptions import ImproperlyConfigured
27
+ from django.db.backends.base.base import BaseDatabaseWrapper
28
+ from django.utils.asyncio import async_unsafe
29
+
30
+ from .features import DatabaseFeatures
31
+ from .operations import DatabaseOperations
32
+ from .client import DatabaseClient
33
+ from .creation import DatabaseCreation
34
+ from .introspection import DatabaseIntrospection
35
+ from .schema import DatabaseSchemaEditor
36
+
37
+ FORMAT_QMARK_REGEX = re.compile(r"(?<!%)%s")
38
+
39
+
40
+ def _is_local_name(name):
41
+ """Return True if NAME looks like a local file path, False if remote URL."""
42
+ if not name:
43
+ return False
44
+ if name.startswith(("http://", "https://", "libsql://")):
45
+ return False
46
+ if name.startswith(("/", ".")):
47
+ return True
48
+ if name.endswith((".sqlite3", ".db", ".sqlite", ".s3db", ".sl3")):
49
+ return True
50
+ # Bare hostname like "db.turso.io" — has dots, no path separators,
51
+ # and does not match any known file extension above.
52
+ if "." in name and "/" not in name and "\\" not in name:
53
+ return False
54
+ return True
55
+
56
+
57
+ def _py_value_to_turso_type(value):
58
+ """Convert a Python value to a Turso typed-value dict."""
59
+ if value is None:
60
+ return {"type": "null"}
61
+ if isinstance(value, bool):
62
+ return {"type": "integer", "value": "1" if value else "0"}
63
+ if isinstance(value, int):
64
+ return {"type": "integer", "value": str(value)}
65
+ if isinstance(value, float):
66
+ return {"type": "real", "value": value}
67
+ if isinstance(value, (bytes, memoryview, bytearray)):
68
+ import base64
69
+ return {
70
+ "type": "blob",
71
+ "value": base64.b64encode(bytes(value)).decode("ascii"),
72
+ }
73
+ return {"type": "text", "value": str(value)}
74
+
75
+
76
+ def _turso_value_to_py(cell):
77
+ """Convert a Turso typed-value dict to a Python value."""
78
+ ctype = cell.get("type", "text")
79
+ value = cell.get("value")
80
+ if ctype == "null" or value is None or (ctype == "text" and value == "NULL"):
81
+ return None if ctype == "null" else value
82
+ if ctype == "integer":
83
+ return int(value)
84
+ if ctype == "real":
85
+ return float(value)
86
+ if ctype == "blob":
87
+ import base64
88
+ return base64.b64decode(value)
89
+ return value
90
+
91
+
92
+ def _build_turso_args(params):
93
+ """Build Turso-format args list from Python params."""
94
+ if params is None:
95
+ return None
96
+ if isinstance(params, Mapping):
97
+ raise NotImplementedError("Named parameters not supported; use qmark style")
98
+ return [_py_value_to_turso_type(p) for p in params]
99
+
100
+
101
+ class TursoHTTPConnection:
102
+ """Minimal HTTP connection to Turso's REST API."""
103
+
104
+ def __init__(self, base_url, auth_token, timeout=30):
105
+ self.base_url = base_url.rstrip("/")
106
+ self.auth_token = auth_token
107
+ self.timeout = timeout
108
+
109
+ def request(self, path, body):
110
+ url = f"{self.base_url}{path}"
111
+ data = json.dumps(body).encode("utf-8")
112
+ req = urllib.request.Request(
113
+ url,
114
+ data=data,
115
+ headers={
116
+ "Content-Type": "application/json",
117
+ "Authorization": f"Bearer {self.auth_token}",
118
+ },
119
+ )
120
+ try:
121
+ resp = urllib.request.urlopen(req, timeout=self.timeout)
122
+ return json.loads(resp.read())
123
+ except urllib.error.HTTPError as e:
124
+ body = e.read().decode(errors="replace")
125
+ raise RuntimeError(f"Turso HTTP {e.code}: {body}") from e
126
+ except urllib.error.URLError as e:
127
+ raise RuntimeError(f"Turso connection error: {e.reason}") from e
128
+
129
+
130
+ class TursoCursor:
131
+ """DB-API 2.0 compatible cursor that calls Turso HTTP API."""
132
+
133
+ def __init__(self, connection):
134
+ self.connection = connection
135
+ self._rows = []
136
+ self._columns = ()
137
+ self._index = 0
138
+ self.rowcount = -1
139
+ self.lastrowid = None
140
+ self.description = None
141
+ self._closed = False
142
+
143
+ def _convert_query(self, query):
144
+ """Convert Django format-style %s to qmark ? for Turso."""
145
+ return FORMAT_QMARK_REGEX.sub("?", query).replace("%%", "%")
146
+
147
+ def execute(self, sql, params=None):
148
+ if self._closed:
149
+ raise RuntimeError("Cursor is closed")
150
+ sql = self._convert_query(sql)
151
+ payload = {"stmt": {"sql": sql}}
152
+ if params:
153
+ payload["stmt"]["args"] = _build_turso_args(params)
154
+
155
+ data = self.connection.request("/v1/execute", payload)
156
+ result = data.get("result", {})
157
+
158
+ self._columns = tuple(c["name"] for c in result.get("cols", []))
159
+ self._rows = [
160
+ tuple(_turso_value_to_py(cell) for cell in row)
161
+ for row in result.get("rows", [])
162
+ ]
163
+ self._index = 0
164
+ self.rowcount = result.get("affected_row_count", -1)
165
+ self.lastrowid = result.get("last_insert_rowid")
166
+
167
+ if self._columns:
168
+ self.description = [
169
+ (name, None, None, None, None, None, None) for name in self._columns
170
+ ]
171
+ return self
172
+
173
+ def executemany(self, sql, param_list):
174
+ """Use batch endpoint for multiple parameter sets."""
175
+ if self._closed:
176
+ raise RuntimeError("Cursor is closed")
177
+ sql = self._convert_query(sql)
178
+ steps = [
179
+ {"stmt": {"sql": sql, "args": _build_turso_args(p) or []}}
180
+ for p in param_list
181
+ ]
182
+ data = self.connection.request("/v1/batch", {"batch": {"steps": steps}})
183
+ result = data.get("result", {})
184
+ self.rowcount = 0
185
+ for step_result in result.get("step_results", []):
186
+ self.rowcount += step_result.get("affected_row_count", 0)
187
+ self.lastrowid = None
188
+ self._rows = []
189
+ self._columns = ()
190
+ self._index = 0
191
+ self.description = None
192
+ return self
193
+
194
+ def fetchone(self):
195
+ if self._index < len(self._rows):
196
+ row = self._rows[self._index]
197
+ self._index += 1
198
+ return row
199
+ return None
200
+
201
+ def fetchall(self):
202
+ rows = self._rows[self._index:]
203
+ self._index = len(self._rows)
204
+ return rows
205
+
206
+ def fetchmany(self, size=None):
207
+ if size is None:
208
+ size = 1
209
+ rows = self._rows[self._index:self._index + size]
210
+ self._index += len(rows)
211
+ return rows
212
+
213
+ def close(self):
214
+ self._closed = True
215
+
216
+ @property
217
+ def closed(self):
218
+ return self._closed
219
+
220
+ def __iter__(self):
221
+ return self
222
+
223
+ def __next__(self):
224
+ row = self.fetchone()
225
+ if row is None:
226
+ raise StopIteration
227
+ return row
228
+
229
+
230
+ class LocalSQLiteCursor:
231
+ """DB-API 2.0 compatible cursor wrapping a local sqlite3.Cursor."""
232
+
233
+ def __init__(self, sqlite_conn):
234
+ self._cursor = sqlite_conn.cursor()
235
+ self._closed = False
236
+
237
+ def _convert_query(self, query):
238
+ return FORMAT_QMARK_REGEX.sub("?", query).replace("%%", "%")
239
+
240
+ def execute(self, sql, params=None):
241
+ if self._closed:
242
+ raise RuntimeError("Cursor is closed")
243
+ sql = self._convert_query(sql)
244
+ if params:
245
+ self._cursor.execute(sql, params)
246
+ else:
247
+ self._cursor.execute(sql)
248
+ return self
249
+
250
+ def executemany(self, sql, param_list):
251
+ if self._closed:
252
+ raise RuntimeError("Cursor is closed")
253
+ sql = self._convert_query(sql)
254
+ self._cursor.executemany(sql, param_list)
255
+ return self
256
+
257
+ def fetchone(self):
258
+ return self._cursor.fetchone()
259
+
260
+ def fetchall(self):
261
+ return self._cursor.fetchall()
262
+
263
+ def fetchmany(self, size=None):
264
+ return self._cursor.fetchmany(size)
265
+
266
+ def close(self):
267
+ self._closed = True
268
+ self._cursor.close()
269
+
270
+ @property
271
+ def closed(self):
272
+ return self._closed
273
+
274
+ @property
275
+ def rowcount(self):
276
+ return self._cursor.rowcount
277
+
278
+ @property
279
+ def lastrowid(self):
280
+ return self._cursor.lastrowid
281
+
282
+ @property
283
+ def description(self):
284
+ return self._cursor.description
285
+
286
+ def __iter__(self):
287
+ return self
288
+
289
+ def __next__(self):
290
+ row = self.fetchone()
291
+ if row is None:
292
+ raise StopIteration
293
+ return row
294
+
295
+
296
+ class DatabaseWrapper(BaseDatabaseWrapper):
297
+ vendor = "libsql"
298
+ display_name = "libSQL (Turso)"
299
+ Database = Database
300
+
301
+ data_types = {
302
+ "AutoField": "integer",
303
+ "BigAutoField": "integer",
304
+ "BinaryField": "BLOB",
305
+ "BooleanField": "bool",
306
+ "CharField": "varchar(%(max_length)s)",
307
+ "DateField": "date",
308
+ "DateTimeField": "datetime",
309
+ "DecimalField": "decimal",
310
+ "DurationField": "bigint",
311
+ "FileField": "varchar(%(max_length)s)",
312
+ "FilePathField": "varchar(%(max_length)s)",
313
+ "FloatField": "real",
314
+ "IntegerField": "integer",
315
+ "BigIntegerField": "bigint",
316
+ "IPAddressField": "char(15)",
317
+ "GenericIPAddressField": "char(39)",
318
+ "JSONField": "text",
319
+ "PositiveBigIntegerField": "bigint unsigned",
320
+ "PositiveIntegerField": "integer unsigned",
321
+ "PositiveSmallIntegerField": "smallint unsigned",
322
+ "SlugField": "varchar(%(max_length)s)",
323
+ "SmallAutoField": "integer",
324
+ "SmallIntegerField": "smallint",
325
+ "TextField": "text",
326
+ "TimeField": "time",
327
+ "UUIDField": "char(32)",
328
+ }
329
+ data_type_check_constraints = {
330
+ "PositiveBigIntegerField": '"%(column)s" >= 0',
331
+ "JSONField": '(JSON_VALID("%(column)s") OR "%(column)s" IS NULL)',
332
+ "PositiveIntegerField": '"%(column)s" >= 0',
333
+ "PositiveSmallIntegerField": '"%(column)s" >= 0',
334
+ }
335
+ data_types_suffix = {
336
+ "AutoField": "AUTOINCREMENT",
337
+ "BigAutoField": "AUTOINCREMENT",
338
+ "SmallAutoField": "AUTOINCREMENT",
339
+ }
340
+ operators = {
341
+ "exact": "= %s",
342
+ "iexact": "LIKE %s ESCAPE '\\'",
343
+ "contains": "LIKE %s ESCAPE '\\'",
344
+ "icontains": "LIKE %s ESCAPE '\\'",
345
+ "regex": "REGEXP %s",
346
+ "iregex": "REGEXP '(?i)' || %s",
347
+ "gt": "> %s",
348
+ "gte": ">= %s",
349
+ "lt": "< %s",
350
+ "lte": "<= %s",
351
+ "startswith": "LIKE %s ESCAPE '\\'",
352
+ "endswith": "LIKE %s ESCAPE '\\'",
353
+ "istartswith": "LIKE %s ESCAPE '\\'",
354
+ "iendswith": "LIKE %s ESCAPE '\\'",
355
+ }
356
+ pattern_esc = (
357
+ r"REPLACE(REPLACE(REPLACE({}, '\', '\\'), '%%', '\%%'), '_', '\_')"
358
+ )
359
+ pattern_ops = {
360
+ "contains": r"LIKE '%%' || {} || '%%' ESCAPE '\'",
361
+ "icontains": r"LIKE '%%' || UPPER({}) || '%%' ESCAPE '\'",
362
+ "startswith": r"LIKE {} || '%%' ESCAPE '\'",
363
+ "istartswith": r"LIKE UPPER({}) || '%%' ESCAPE '\'",
364
+ "endswith": r"LIKE '%%' || {} ESCAPE '\'",
365
+ "iendswith": r"LIKE '%%' || UPPER({}) ESCAPE '\'",
366
+ }
367
+
368
+ SchemaEditorClass = DatabaseSchemaEditor
369
+ client_class = DatabaseClient
370
+ creation_class = DatabaseCreation
371
+ features_class = DatabaseFeatures
372
+ introspection_class = DatabaseIntrospection
373
+ ops_class = DatabaseOperations
374
+
375
+ def __init__(self, settings_dict, alias="default"):
376
+ super().__init__(settings_dict, alias)
377
+ self._http_connection = None
378
+
379
+ def get_connection_params(self):
380
+ settings_dict = self.settings_dict
381
+ name = settings_dict.get("NAME")
382
+ if not name:
383
+ # Django's _nodb_cursor passes NAME=None for test DB teardown.
384
+ # Default to in-memory local SQLite.
385
+ return {"is_local": True, "filepath": ":memory:"}
386
+ if _is_local_name(name):
387
+ # sqlite3.connect() handles relative paths natively — no
388
+ # need to resolve against BASE_DIR.
389
+ return {"is_local": True, "filepath": name}
390
+ # Remote: convert libsql:// → https://, add https:// to bare hostnames
391
+ if name.startswith("libsql://"):
392
+ url = name.replace("libsql://", "https://", 1)
393
+ elif "://" in name:
394
+ url = name
395
+ else:
396
+ url = f"https://{name}"
397
+ return {
398
+ "is_local": False,
399
+ "url": url,
400
+ "auth_token": settings_dict.get("AUTH_TOKEN", ""),
401
+ "timeout": settings_dict.get("OPTIONS", {}).get("timeout", 30),
402
+ }
403
+
404
+ @async_unsafe
405
+ def get_new_connection(self, conn_params):
406
+ if conn_params["is_local"]:
407
+ conn = sqlite3.connect(
408
+ conn_params["filepath"],
409
+ check_same_thread=False,
410
+ )
411
+ conn.execute("PRAGMA journal_mode=WAL")
412
+ conn.execute("PRAGMA foreign_keys=ON")
413
+ return conn
414
+ return TursoHTTPConnection(
415
+ base_url=conn_params["url"],
416
+ auth_token=conn_params["auth_token"],
417
+ timeout=conn_params["timeout"],
418
+ )
419
+
420
+ def init_connection_state(self):
421
+ """Initialize database connection settings."""
422
+ pass
423
+
424
+ @async_unsafe
425
+ def create_cursor(self, name=None):
426
+ if self.connection is None:
427
+ raise RuntimeError("No connection established")
428
+ if isinstance(self.connection, TursoHTTPConnection):
429
+ return TursoCursor(self.connection)
430
+ return LocalSQLiteCursor(self.connection)
431
+
432
+ def is_usable(self):
433
+ if self.connection is None:
434
+ return False
435
+ try:
436
+ if isinstance(self.connection, TursoHTTPConnection):
437
+ self.connection.request("/v1/execute", {"stmt": {"sql": "SELECT 1"}})
438
+ else:
439
+ self.connection.execute("SELECT 1")
440
+ return True
441
+ except Exception:
442
+ return False
443
+
444
+ def _close(self):
445
+ if self.connection is not None:
446
+ if isinstance(self.connection, TursoHTTPConnection):
447
+ # HTTP connections are stateless — nothing to close.
448
+ pass
449
+ else:
450
+ self.connection.close()
451
+
452
+ def _set_autocommit(self, autocommit):
453
+ # For local SQLite, setting isolation_level=None enables autocommit
454
+ # mode, while setting it to 'DEFERRED' starts implicit transactions.
455
+ if self.connection is not None and not isinstance(
456
+ self.connection, TursoHTTPConnection
457
+ ):
458
+ if autocommit:
459
+ self.connection.isolation_level = None
460
+ else:
461
+ self.connection.isolation_level = "DEFERRED"
462
+
463
+ def _start_transaction_under_autocommit(self):
464
+ """Start an explicit transaction while staying in autocommit mode."""
465
+ if self.connection is not None and not isinstance(
466
+ self.connection, TursoHTTPConnection
467
+ ):
468
+ self.connection.execute("BEGIN")
469
+
470
+ def _commit(self):
471
+ if self.connection is not None and not isinstance(
472
+ self.connection, TursoHTTPConnection
473
+ ):
474
+ self.connection.commit()
475
+
476
+ def _rollback(self):
477
+ if self.connection is not None and not isinstance(
478
+ self.connection, TursoHTTPConnection
479
+ ):
480
+ self.connection.rollback()
481
+
482
+ def disable_constraint_checking(self):
483
+ """Disable FK checks via PRAGMA. Returns True if successfully disabled."""
484
+ with self.cursor() as cursor:
485
+ cursor.execute("PRAGMA foreign_keys")
486
+ was_enabled = bool(cursor.fetchone()[0])
487
+ if was_enabled:
488
+ cursor.execute("PRAGMA foreign_keys = OFF")
489
+ return was_enabled
490
+
491
+ def enable_constraint_checking(self):
492
+ """Re-enable FK checks."""
493
+ with self.cursor() as cursor:
494
+ cursor.execute("PRAGMA foreign_keys = ON")
495
+
496
+ def check_constraints(self, table_names=None):
497
+ with self.cursor() as cursor:
498
+ if table_names is None:
499
+ violations = cursor.execute("PRAGMA foreign_key_check").fetchall()
500
+ else:
501
+ from itertools import chain
502
+ violations = chain.from_iterable(
503
+ cursor.execute(
504
+ 'PRAGMA foreign_key_check("%s")' % table_name
505
+ ).fetchall()
506
+ for table_name in table_names
507
+ )
508
+ for (table_name, rowid, ref_table, fk_idx) in violations:
509
+ raise RuntimeError(
510
+ f"Foreign key violation in table '{table_name}', "
511
+ f"rowid={rowid}"
512
+ )
513
+
514
+ def is_in_memory_db(self):
515
+ return False
516
+
517
+ def get_database_version(self):
518
+ if self.connection is not None and not isinstance(
519
+ self.connection, TursoHTTPConnection
520
+ ):
521
+ return sqlite3.sqlite_version_info[:3]
522
+ with self.cursor() as cursor:
523
+ cursor.execute("SELECT sqlite_version()")
524
+ row = cursor.fetchone()
525
+ if row:
526
+ parts = row[0].split(".")
527
+ return tuple(int(p) for p in parts[:3])
528
+ return (3, 0, 0)
@@ -0,0 +1,20 @@
1
+ """
2
+ Database client for the libSQL/Turso backend.
3
+
4
+ Provides a shell entry point for Turso databases.
5
+ """
6
+
7
+ from django.db.backends.base.client import BaseDatabaseClient
8
+
9
+
10
+ class DatabaseClient(BaseDatabaseClient):
11
+ executable_name = "turso"
12
+
13
+ @classmethod
14
+ def settings_to_cmd_args_env(cls, settings_dict, parameters):
15
+ args = [cls.executable_name, "db", "shell", settings_dict["NAME"]]
16
+ if settings_dict.get("AUTH_TOKEN"):
17
+ args.extend(["--token", settings_dict["AUTH_TOKEN"]])
18
+ if parameters:
19
+ args.extend(parameters)
20
+ return args, None
@@ -0,0 +1,39 @@
1
+ """
2
+ Database creation for the libSQL/Turso backend.
3
+
4
+ Since Turso databases are provisioned externally (not created via
5
+ Django), this module provides basic test-database creation that
6
+ reuses the production connection or creates a separate database.
7
+ """
8
+
9
+ from django.db.backends.base.creation import BaseDatabaseCreation
10
+
11
+
12
+ class DatabaseCreation(BaseDatabaseCreation):
13
+ def _get_test_db_name(self):
14
+ return self.connection.settings_dict["NAME"]
15
+
16
+ def create_test_db(self, verbosity=1, autoclobber=False, keepdb=False):
17
+ if not keepdb:
18
+ self._destroy_test_db(verbosity=verbosity)
19
+ return self.connection.settings_dict["NAME"]
20
+
21
+ def destroy_test_db(self, old_database_name, verbosity=1, keepdb=False):
22
+ self._destroy_test_db(verbosity=verbosity)
23
+
24
+ def _destroy_test_db(self, verbosity=1):
25
+ with self.connection._nodb_cursor() as cursor:
26
+ cursor.execute(
27
+ "SELECT name FROM sqlite_master WHERE type='table' AND "
28
+ "name NOT LIKE 'sqlite_%%' AND name NOT LIKE '_%%'"
29
+ )
30
+ tables = [row[0] for row in cursor.fetchall()]
31
+ for table in tables:
32
+ cursor.execute(f'DROP TABLE IF EXISTS "{table}"')
33
+
34
+ def test_db_signature(self):
35
+ settings_dict = self.connection.settings_dict
36
+ return (
37
+ self.connection.settings_dict["NAME"],
38
+ settings_dict.get("AUTH_TOKEN", ""),
39
+ )
@@ -0,0 +1,81 @@
1
+ """Features for the libSQL/Turso backend — SQLite-compatible, remote HTTP."""
2
+
3
+ import operator
4
+
5
+ from django.db.backends.base.features import BaseDatabaseFeatures
6
+ from django.utils.functional import cached_property
7
+
8
+
9
+ class DatabaseFeatures(BaseDatabaseFeatures):
10
+ minimum_database_version = (3, 31)
11
+ test_db_allows_multiple_connections = True
12
+ supports_unspecified_pk = True
13
+ supports_timezones = False
14
+ atomic_transactions = False
15
+ can_rollback_ddl = True
16
+ can_create_inline_fk = False
17
+ requires_literal_defaults = True
18
+ can_clone_databases = False
19
+ supports_temporal_subtraction = True
20
+ ignores_table_name_case = True
21
+ supports_cast_with_precision = False
22
+ time_cast_precision = 3
23
+ can_release_savepoints = True
24
+ has_case_insensitive_like = True
25
+ supports_parentheses_in_compound = False
26
+ can_defer_constraint_checks = True
27
+ supports_over_clause = True
28
+ supports_frame_range_fixed_distance = True
29
+ supports_frame_exclusion = True
30
+ supports_aggregate_filter_clause = True
31
+ supports_aggregate_order_by_clause = True
32
+ supports_json_field_contains = False
33
+ supports_update_conflicts = True
34
+ supports_update_conflicts_with_target = True
35
+ order_by_nulls_first = True
36
+ supports_index_on_text_field = True
37
+ supports_stored_generated_columns = True
38
+ supports_virtual_generated_columns = True
39
+ can_alter_table_drop_column = True
40
+ supports_transactions = True
41
+ supports_unlimited_charfield = True
42
+ supports_any_value = True
43
+ supports_aggregate_distinct_multiple_argument = False
44
+ supports_default_keyword_in_insert = False
45
+ insert_test_table_with_defaults = 'INSERT INTO {} ("null") VALUES (1)'
46
+
47
+ test_collations = {
48
+ "ci": "nocase",
49
+ "cs": "binary",
50
+ "non_default": "nocase",
51
+ }
52
+ django_test_expected_failures = set()
53
+
54
+ @cached_property
55
+ def introspected_field_types(self):
56
+ return {
57
+ **super().introspected_field_types,
58
+ "BigAutoField": "AutoField",
59
+ "DurationField": "BigIntegerField",
60
+ "GenericIPAddressField": "CharField",
61
+ "SmallAutoField": "AutoField",
62
+ }
63
+
64
+ @cached_property
65
+ def supports_json_field(self):
66
+ return True
67
+
68
+ can_introspect_json_field = property(operator.attrgetter("supports_json_field"))
69
+ has_json_object_function = property(operator.attrgetter("supports_json_field"))
70
+
71
+ @cached_property
72
+ def can_return_columns_from_insert(self):
73
+ return True
74
+
75
+ can_return_rows_from_bulk_insert = property(
76
+ operator.attrgetter("can_return_columns_from_insert")
77
+ )
78
+
79
+ can_return_rows_from_update = property(
80
+ operator.attrgetter("can_return_columns_from_insert")
81
+ )