dclassql 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,669 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ import sys
5
+ import threading
6
+ from dataclasses import MISSING, dataclass, fields, is_dataclass
7
+ from typing import (
8
+ Any,
9
+ Callable,
10
+ Mapping,
11
+ Protocol,
12
+ Sequence,
13
+ cast,
14
+ runtime_checkable,
15
+ get_origin,
16
+ )
17
+
18
+ from weakref import WeakKeyDictionary
19
+
20
+ from pypika import Query, Table
21
+ from pypika.enums import Order
22
+ from pypika.terms import Parameter
23
+
24
+
25
+ ConnectionFactory = Callable[[], sqlite3.Connection]
26
+
27
+
28
+ @dataclass(slots=True)
29
+ class RelationSpec:
30
+ name: str
31
+ table_name: str
32
+ table_module: str
33
+ many: bool
34
+ mapping: tuple[tuple[str, str], ...]
35
+
36
+
37
+ @runtime_checkable
38
+ class TableProtocol[ModelT, InsertT, WhereT](Protocol):
39
+ def __init__(self, backend: BackendProtocol[ModelT, InsertT, WhereT]) -> None: ...
40
+
41
+ model: type[ModelT]
42
+ insert_model: type[InsertT]
43
+ columns: tuple[str, ...]
44
+ auto_increment_columns: tuple[str, ...]
45
+ primary_key: tuple[str, ...]
46
+ relations: tuple[RelationSpec, ...]
47
+
48
+
49
+ @runtime_checkable
50
+ class BackendProtocol[ModelT, InsertT, WhereT](Protocol):
51
+ def insert(self, table: TableProtocol[ModelT, InsertT, WhereT], data: InsertT | Mapping[str, object]) -> ModelT: ...
52
+
53
+ def insert_many(
54
+ self,
55
+ table: TableProtocol[ModelT, InsertT, WhereT],
56
+ data: Sequence[InsertT | Mapping[str, object]],
57
+ *,
58
+ batch_size: int | None = None,
59
+ ) -> list[ModelT]: ...
60
+
61
+ def find_many(
62
+ self,
63
+ table: TableProtocol[ModelT, InsertT, WhereT],
64
+ *,
65
+ where: WhereT | None = None,
66
+ include: Mapping[str, bool] | None = None,
67
+ order_by: Sequence[tuple[str, str]] | None = None,
68
+ take: int | None = None,
69
+ skip: int | None = None,
70
+ ) -> list[ModelT]: ...
71
+
72
+ def find_first(
73
+ self,
74
+ table: TableProtocol[ModelT, InsertT, WhereT],
75
+ *,
76
+ where: WhereT | None = None,
77
+ include: Mapping[str, bool] | None = None,
78
+ order_by: Sequence[tuple[str, str]] | None = None,
79
+ skip: int | None = None,
80
+ ) -> ModelT | None: ...
81
+
82
+
83
+ @dataclass(slots=True)
84
+ class LazyRelationState:
85
+ attribute: str
86
+ backend: BackendProtocol[Any, Any, Mapping[str, object]]
87
+ table_cls: type[Any]
88
+ mapping: tuple[tuple[str, str], ...]
89
+ many: bool
90
+ loaded: bool = False
91
+ value: Any = None
92
+ loading: bool = False
93
+ _LAZY_CLASS_CACHE: dict[type[Any], type[Any]] = {}
94
+ _LAZY_RELATION_STATE: WeakKeyDictionary[Any, dict[str, LazyRelationState]] = WeakKeyDictionary()
95
+
96
+
97
+ def _resolve_lazy_relation(instance: Any, state: LazyRelationState) -> Any:
98
+ if state.loaded:
99
+ return state.value
100
+ if state.loading:
101
+ return state.value
102
+ state.loading = True
103
+ state.value = [] if state.many else None
104
+ where: dict[str, object] = {}
105
+ for owner_column, target_column in state.mapping:
106
+ owner_value = getattr(instance, owner_column, None)
107
+ if owner_value is None:
108
+ value: Any = [] if state.many else None
109
+ state.loaded = True
110
+ state.value = value
111
+ object.__setattr__(instance, state.attribute, value)
112
+ state.loading = False
113
+ return value
114
+ where[target_column] = owner_value
115
+
116
+ table = state.table_cls(state.backend)
117
+ if state.many:
118
+ loaded = table.find_many(where=cast(Mapping[str, object], where))
119
+ else:
120
+ loaded = table.find_first(where=cast(Mapping[str, object], where))
121
+
122
+ if state.many and loaded is None:
123
+ loaded = []
124
+
125
+ state.loaded = True
126
+ state.value = loaded
127
+ state.loading = False
128
+ object.__setattr__(instance, state.attribute, loaded)
129
+ return loaded
130
+
131
+
132
+ def _ensure_lazy_model_class(model_cls: type[Any]) -> type[Any]:
133
+ cached = _LAZY_CLASS_CACHE.get(model_cls)
134
+ if cached is not None:
135
+ return cached
136
+
137
+ base_getattribute = cast(Callable[[Any, str], Any], model_cls.__getattribute__)
138
+ base_setattr = cast(Callable[[Any, str, Any], None], model_cls.__setattr__)
139
+
140
+ def __getattribute__(self: Any, name: str) -> Any:
141
+ state_map = _LAZY_RELATION_STATE.get(self)
142
+ if state_map is not None:
143
+ state = state_map.get(name)
144
+ if state is not None:
145
+ if not state.loaded:
146
+ _resolve_lazy_relation(self, state)
147
+ return state.value
148
+ return base_getattribute(self, name)
149
+
150
+ def __setattr__(self: Any, name: str, value: Any) -> None:
151
+ state_map = _LAZY_RELATION_STATE.get(self)
152
+ if state_map is not None:
153
+ state = state_map.get(name)
154
+ if state is not None:
155
+ state.loaded = True
156
+ state.value = value
157
+ base_setattr(self, name, value)
158
+
159
+ namespace: dict[str, Any] = {
160
+ "__slots__": (),
161
+ "__lazy_base__": model_cls,
162
+ "__getattribute__": __getattribute__,
163
+ "__setattr__": __setattr__,
164
+ "__hash__": object.__hash__,
165
+ }
166
+ lazy_cls = type(f"{model_cls.__name__}LazyRuntime", (model_cls,), namespace)
167
+ try:
168
+ lazy_cls.__name__ = model_cls.__name__
169
+ except AttributeError:
170
+ pass
171
+ try:
172
+ lazy_cls.__qualname__ = model_cls.__qualname__
173
+ except AttributeError:
174
+ pass
175
+ lazy_cls.__module__ = model_cls.__module__
176
+ _LAZY_CLASS_CACHE[model_cls] = lazy_cls
177
+ return lazy_cls
178
+
179
+
180
+ def _find_backref_relations(
181
+ table: TableProtocol[Any, Any, Any],
182
+ existing_names: set[str],
183
+ ) -> list[RelationSpec]:
184
+ model = table.model
185
+ module_name = table.__class__.__module__
186
+ module = sys.modules.get(module_name)
187
+ if module is None:
188
+ return []
189
+
190
+ extras: list[RelationSpec] = []
191
+ for attr in dir(module):
192
+ candidate = getattr(module, attr, None)
193
+ if not isinstance(candidate, type):
194
+ continue
195
+ if candidate is table.__class__:
196
+ continue
197
+ foreign_keys = getattr(candidate, "foreign_keys", None)
198
+ if not foreign_keys:
199
+ continue
200
+ candidate_model = getattr(candidate, "model", None)
201
+ if candidate_model is None:
202
+ continue
203
+ for fk in foreign_keys:
204
+ if fk.backref is None:
205
+ continue
206
+ if fk.remote_model is not model:
207
+ continue
208
+ name = fk.backref
209
+ if name in existing_names:
210
+ continue
211
+ mapping_pairs: tuple[tuple[str, str], ...] = tuple(
212
+ (remote, local) for remote, local in zip(fk.remote_columns, fk.local_columns)
213
+ )
214
+ primary_key = getattr(candidate, "primary_key", ())
215
+ many = tuple(primary_key) != tuple(fk.local_columns)
216
+ extras.append(
217
+ RelationSpec(
218
+ name=name,
219
+ table_name=candidate.__name__,
220
+ table_module=candidate.__module__,
221
+ many=many,
222
+ mapping=mapping_pairs,
223
+ )
224
+ )
225
+ existing_names.add(name)
226
+ return extras
227
+
228
+
229
+ class SQLiteBackend[ModelT, InsertT, WhereT: Mapping[str, object]](BackendProtocol[ModelT, InsertT, WhereT]):
230
+ def __init__(self, source: sqlite3.Connection | ConnectionFactory | "SQLiteBackend") -> None:
231
+ if isinstance(source, SQLiteBackend):
232
+ self._factory: ConnectionFactory | None = source._factory
233
+ self._connection: sqlite3.Connection | None = source._connection
234
+ self._local = source._local
235
+ self._identity_map = source._identity_map
236
+ elif isinstance(source, sqlite3.Connection):
237
+ self._factory = None
238
+ self._connection = source
239
+ self._ensure_row_factory(self._connection)
240
+ self._local = threading.local()
241
+ self._identity_map: dict[tuple[type[Any], tuple[Any, ...]], Any] = {}
242
+ elif callable(source):
243
+ self._factory = source
244
+ self._connection = None
245
+ self._local = threading.local()
246
+ self._identity_map = {}
247
+ else:
248
+ raise TypeError("SQLite backend source must be connection or callable returning connection")
249
+
250
+ def insert(self, table: TableProtocol[ModelT, InsertT, WhereT], data: InsertT | Mapping[str, object]) -> ModelT:
251
+ payload = self._normalize_insert_payload(table, data)
252
+ if not payload:
253
+ raise ValueError("Insert payload cannot be empty")
254
+
255
+ table_name = table.model.__name__
256
+ sql_table = Table(table_name)
257
+ columns = list(payload.keys())
258
+ params = [payload[name] for name in columns]
259
+
260
+ insert_query = (
261
+ Query.into(sql_table)
262
+ .columns(*columns)
263
+ .insert(*(Parameter("?") for _ in columns))
264
+ )
265
+ sql = insert_query.get_sql(quote_char='"') + ";"
266
+ cursor = self._execute(sql, params)
267
+
268
+ pk_filter: dict[str, object] = {}
269
+ auto_increment = set(getattr(table, "auto_increment_columns", ()))
270
+ primary_key = getattr(table, "primary_key", ())
271
+ if not primary_key:
272
+ raise ValueError(f"Table {table_name} does not define primary key")
273
+ if len(primary_key) == 1:
274
+ pk = primary_key[0]
275
+ value = payload.get(pk)
276
+ if value is None and pk in auto_increment:
277
+ value = cursor.lastrowid
278
+ if value is None:
279
+ raise ValueError(f"Primary key column '{pk}' is null after insert")
280
+ pk_filter[pk] = value
281
+ else:
282
+ for pk in primary_key:
283
+ value = payload.get(pk)
284
+ if value is None:
285
+ raise ValueError(f"Composite key requires '{pk}' value")
286
+ pk_filter[pk] = value
287
+
288
+ result = self._fetch_single(table, pk_filter, include=None)
289
+ self._invalidate_backrefs(table, result)
290
+ return result
291
+
292
+ def insert_many(
293
+ self,
294
+ table: TableProtocol[ModelT, InsertT, WhereT],
295
+ data: Sequence[InsertT | Mapping[str, object]],
296
+ *,
297
+ batch_size: int | None = None,
298
+ ) -> list[ModelT]:
299
+ items = list(data)
300
+ if not items:
301
+ return []
302
+
303
+ payloads = [self._normalize_insert_payload(table, item) for item in items]
304
+ columns = list(payloads[0].keys())
305
+ if not columns:
306
+ raise ValueError("Insert payload cannot be empty")
307
+
308
+ params_matrix = [[payload.get(column) for column in columns] for payload in payloads]
309
+ table_name = table.model.__name__
310
+ sql_table = Table(table_name)
311
+ insert_query = (
312
+ Query.into(sql_table)
313
+ .columns(*columns)
314
+ .insert(*(Parameter("?") for _ in columns))
315
+ )
316
+ sql = insert_query.get_sql(quote_char='"') + ';'
317
+
318
+ pk_columns = list(table.primary_key)
319
+ if not pk_columns:
320
+ raise ValueError(f"Table {table_name} does not define primary key")
321
+ auto_increment = set(table.auto_increment_columns)
322
+
323
+ results: list[ModelT] = []
324
+ step = batch_size if batch_size and batch_size > 0 else len(payloads)
325
+ start = 0
326
+ connection = self._acquire_connection()
327
+ while start < len(payloads):
328
+ end = min(start + step, len(payloads))
329
+ subset_payloads = payloads[start:end]
330
+ subset_params = params_matrix[start:end]
331
+ if not subset_params:
332
+ start = end
333
+ continue
334
+ cursor = connection.executemany(sql, [tuple(param) for param in subset_params])
335
+ connection.commit()
336
+ generated_start: int | None = None
337
+ if len(pk_columns) == 1 and pk_columns[0] in auto_increment:
338
+ last_rowid_result = connection.execute("SELECT last_insert_rowid()").fetchone()
339
+ if last_rowid_result is None or last_rowid_result[0] is None:
340
+ raise RuntimeError("Unable to determine lastrowid for bulk insert")
341
+ last_id = int(last_rowid_result[0])
342
+ generated_start = last_id - len(subset_payloads) + 1
343
+
344
+ for offset, payload in enumerate(subset_payloads):
345
+ pk_filter: dict[str, object] = {}
346
+ for pk in pk_columns:
347
+ value = payload.get(pk)
348
+ if value is None and generated_start is not None and pk == pk_columns[0]:
349
+ value = generated_start + offset
350
+ payload[pk] = value
351
+ if value is None:
352
+ raise ValueError(f"Primary key column '{pk}' is null after insert")
353
+ pk_filter[pk] = value
354
+ instance = self._fetch_single(table, pk_filter, include=None)
355
+ self._invalidate_backrefs(table, instance)
356
+ results.append(instance)
357
+ start = end
358
+ return results
359
+
360
+ def find_many(
361
+ self,
362
+ table: TableProtocol[ModelT, InsertT, WhereT],
363
+ *,
364
+ where: WhereT | None = None,
365
+ include: Mapping[str, bool] | None = None,
366
+ order_by: Sequence[tuple[str, str]] | None = None,
367
+ take: int | None = None,
368
+ skip: int | None = None,
369
+ ) -> list[ModelT]:
370
+ table_name = table.model.__name__
371
+ sql_table = Table(table_name)
372
+ select_query = Query.from_(sql_table).select(*[sql_table.field(col) for col in table.columns])
373
+ params: list[Any] = []
374
+
375
+ if where:
376
+ for column, value in where.items():
377
+ if column not in table.columns:
378
+ raise KeyError(f"Unknown column '{column}' in where clause")
379
+ field = sql_table.field(column)
380
+ if value is None:
381
+ select_query = select_query.where(field.isnull())
382
+ else:
383
+ select_query = select_query.where(field == Parameter("?"))
384
+ params.append(value)
385
+
386
+ if order_by:
387
+ for column, direction in order_by:
388
+ if column not in table.columns:
389
+ raise KeyError(f"Unknown column '{column}' in order_by clause")
390
+ direction_lower = direction.lower()
391
+ if direction_lower not in {"asc", "desc"}:
392
+ raise ValueError("order_by direction must be 'asc' or 'desc'")
393
+ select_query = select_query.orderby(sql_table.field(column), order=Order[direction_lower])
394
+
395
+ if skip is not None:
396
+ select_query = select_query.offset(skip)
397
+ if take is not None:
398
+ select_query = select_query.limit(take)
399
+
400
+ sql = select_query.get_sql(quote_char='"') + ";"
401
+ rows = self._fetch_all(sql, params)
402
+ include_map = include or {}
403
+ return [self._row_to_model(table, row, include_map) for row in rows]
404
+
405
+ def find_first(
406
+ self,
407
+ table: TableProtocol[ModelT, InsertT, WhereT],
408
+ *,
409
+ where: WhereT | None = None,
410
+ include: Mapping[str, bool] | None = None,
411
+ order_by: Sequence[tuple[str, str]] | None = None,
412
+ skip: int | None = None,
413
+ ) -> ModelT | None:
414
+ results = self.find_many(
415
+ table,
416
+ where=where,
417
+ include=include,
418
+ order_by=order_by,
419
+ take=1,
420
+ skip=skip,
421
+ )
422
+ return results[0] if results else None
423
+
424
+ def _fetch_single(
425
+ self,
426
+ table: TableProtocol[ModelT, InsertT, WhereT],
427
+ where: Mapping[str, object],
428
+ include: Mapping[str, bool] | None,
429
+ ) -> ModelT:
430
+ results = self.find_many(table, where=cast(WhereT, where), include=include)
431
+ if not results:
432
+ raise RuntimeError("Inserted row could not be reloaded")
433
+ return results[0]
434
+
435
+ def _normalize_insert_payload(
436
+ self,
437
+ table: TableProtocol[ModelT, InsertT, WhereT],
438
+ data: InsertT | Mapping[str, object],
439
+ ) -> dict[str, object]:
440
+ allowed = set(table.columns)
441
+ if isinstance(data, dict):
442
+ return {k: v for k, v in data.items() if k in allowed}
443
+ insert_model = getattr(table, "insert_model", None)
444
+ if insert_model and isinstance(data, insert_model):
445
+ return {column: getattr(data, column) for column in table.columns if hasattr(data, column)}
446
+ if is_dataclass(data):
447
+ return {column: getattr(data, column) for column in table.columns if hasattr(data, column)}
448
+ raise TypeError("Unsupported insert payload type")
449
+
450
+ def _fetch_all(self, sql: str, params: Sequence[Any]) -> list[sqlite3.Row]:
451
+ cursor = self._execute(sql, params)
452
+ return cursor.fetchall()
453
+
454
+ def _execute(self, sql: str, params: Sequence[Any], auto_commit: bool = True) -> sqlite3.Cursor:
455
+ connection = self._acquire_connection()
456
+ cursor = connection.execute(sql, tuple(params))
457
+ if auto_commit:
458
+ connection.commit()
459
+ return cursor
460
+
461
+ def _acquire_connection(self) -> sqlite3.Connection:
462
+ if self._factory is None:
463
+ assert self._connection is not None
464
+ self._ensure_row_factory(self._connection)
465
+ return self._connection
466
+
467
+ connection = getattr(self._local, "connection", None)
468
+ if connection is None:
469
+ connection = self._factory()
470
+ if not isinstance(connection, sqlite3.Connection):
471
+ raise TypeError("SQLite backend factory must return sqlite3.Connection")
472
+ self._ensure_row_factory(connection)
473
+ self._local.connection = connection
474
+ return connection
475
+
476
+ @staticmethod
477
+ def _ensure_row_factory(connection: sqlite3.Connection) -> None:
478
+ if connection.row_factory is None:
479
+ connection.row_factory = sqlite3.Row
480
+
481
+ def _row_to_model(
482
+ self,
483
+ table: TableProtocol[ModelT, Any, Any],
484
+ row: sqlite3.Row,
485
+ include_map: Mapping[str, bool],
486
+ ) -> ModelT:
487
+ key = self._identity_key(table, row)
488
+ if key is not None:
489
+ cached = self._identity_map.get(key)
490
+ else:
491
+ cached = None
492
+ model = table.model
493
+ if cached is None:
494
+ if is_dataclass(model):
495
+ values: dict[str, Any] = {column: row[column] for column in table.columns}
496
+ instance = model.__new__(model)
497
+ for field in fields(model):
498
+ if field.name in values:
499
+ value = values[field.name]
500
+ elif field.default is not MISSING:
501
+ value = field.default
502
+ elif field.default_factory is not MISSING: # type: ignore[attr-defined]
503
+ value = field.default_factory() # type: ignore[misc]
504
+ else:
505
+ origin = get_origin(field.type)
506
+ if origin in (list, set, frozenset):
507
+ value = origin() # type: ignore[call-arg]
508
+ else:
509
+ value = None
510
+ object.__setattr__(instance, field.name, value)
511
+ else:
512
+ instance = cast(ModelT, model(**{column: row[column] for column in table.columns}))
513
+ if key is not None:
514
+ self._identity_map[key] = instance
515
+ else:
516
+ instance = cast(ModelT, cached)
517
+ for column in table.columns:
518
+ object.__setattr__(instance, column, row[column])
519
+ self._attach_relations(table, instance, include_map)
520
+ return instance
521
+
522
+ def _invalidate_backrefs(
523
+ self,
524
+ table: TableProtocol[ModelT, Any, Any],
525
+ instance: ModelT,
526
+ ) -> None:
527
+ foreign_keys = getattr(table, "foreign_keys", ())
528
+ if not foreign_keys:
529
+ return
530
+ for fk in foreign_keys:
531
+ backref = getattr(fk, "backref", None)
532
+ if not backref:
533
+ continue
534
+ remote_model = fk.remote_model
535
+ if remote_model is None:
536
+ continue
537
+ key_values: list[Any] = []
538
+ for local_col, remote_col in zip(fk.local_columns, fk.remote_columns):
539
+ value = getattr(instance, local_col, None)
540
+ if value is None:
541
+ key_values = []
542
+ break
543
+ key_values.append(value)
544
+ if not key_values:
545
+ continue
546
+ identity_key = (remote_model, tuple(key_values))
547
+ owner = self._identity_map.get(identity_key)
548
+ if owner is None:
549
+ continue
550
+ state_map = _LAZY_RELATION_STATE.get(owner)
551
+ if state_map is None:
552
+ continue
553
+ state = state_map.get(backref)
554
+ if state is None:
555
+ continue
556
+ state.loaded = False
557
+ state.loading = False
558
+ state.value = [] if state.many else None
559
+
560
+ def _identity_key(
561
+ self,
562
+ table: TableProtocol[ModelT, Any, Any],
563
+ row: sqlite3.Row,
564
+ ) -> tuple[type[Any], tuple[Any, ...]] | None:
565
+ pk_columns = getattr(table, "primary_key", ())
566
+ if not pk_columns:
567
+ return None
568
+ values: list[Any] = []
569
+ for column in pk_columns:
570
+ value = row[column]
571
+ if value is None:
572
+ return None
573
+ values.append(value)
574
+ return (table.model, tuple(values))
575
+
576
+ def _attach_relations(
577
+ self,
578
+ table: TableProtocol[ModelT, Any, Any],
579
+ instance: ModelT,
580
+ include_map: Mapping[str, bool],
581
+ ) -> None:
582
+ relations_attr = cast(Sequence[Any], getattr(table, "relations", ()))
583
+ relations: list[RelationSpec] = []
584
+ for entry in relations_attr:
585
+ if isinstance(entry, RelationSpec):
586
+ relations.append(entry)
587
+ elif isinstance(entry, dict):
588
+ relations.append(
589
+ RelationSpec(
590
+ name=entry['name'],
591
+ table_name=entry['table_name'],
592
+ table_module=entry.get('table_module', table.__class__.__module__),
593
+ many=bool(entry['many']),
594
+ mapping=tuple(tuple(pair) for pair in entry['mapping']),
595
+ )
596
+ )
597
+ else:
598
+ raise TypeError("Unsupported relation specification")
599
+
600
+ existing_names: set[str] = {spec.name for spec in relations}
601
+ relations.extend(_find_backref_relations(table, existing_names))
602
+ if not relations:
603
+ return
604
+ backend_for_lazy = cast(BackendProtocol[Any, Any, Mapping[str, object]], self)
605
+
606
+ base_model_cls = cast(type[Any], getattr(instance.__class__, "__lazy_base__", instance.__class__))
607
+ lazy_cls = _ensure_lazy_model_class(base_model_cls)
608
+ if instance.__class__ is not lazy_cls:
609
+ instance.__class__ = lazy_cls
610
+
611
+ states = _LAZY_RELATION_STATE.get(instance)
612
+ if states is None:
613
+ states = {}
614
+
615
+ for spec in relations:
616
+ name = spec.name
617
+ table_module_name = spec.table_module or table.__class__.__module__
618
+ module = sys.modules.get(table_module_name)
619
+ if module is None:
620
+ raise RuntimeError(f"Module '{table_module_name}' not loaded for relation '{name}'")
621
+ table_cls_name = spec.table_name
622
+ table_cls = getattr(module, table_cls_name)
623
+ mapping = spec.mapping
624
+ many = spec.many
625
+ existing_state = states.get(name)
626
+ if existing_state is None:
627
+ state = LazyRelationState(
628
+ attribute=name,
629
+ backend=backend_for_lazy,
630
+ table_cls=cast(type[Any], table_cls),
631
+ mapping=mapping,
632
+ many=many,
633
+ )
634
+ states[name] = state
635
+ else:
636
+ existing_state.backend = backend_for_lazy
637
+ existing_state.table_cls = cast(type[Any], table_cls)
638
+ existing_state.mapping = mapping
639
+ existing_state.many = many
640
+ state = existing_state
641
+
642
+ if not states:
643
+ return
644
+ _LAZY_RELATION_STATE[instance] = states
645
+
646
+ for name, state in states.items():
647
+ if include_map.get(name):
648
+ _resolve_lazy_relation(instance, state)
649
+
650
+ def close(self) -> None:
651
+ if self._factory is None:
652
+ if self._connection is not None:
653
+ self._connection.close()
654
+ self._connection = None
655
+ self._identity_map.clear()
656
+ return
657
+ connection = getattr(self._local, "connection", None)
658
+ if connection is not None:
659
+ connection.close()
660
+ delattr(self._local, "connection")
661
+ self._identity_map.clear()
662
+
663
+
664
+ def create_backend(provider: str, connection: Any) -> BackendProtocol[Any, Any, Mapping[str, object]]:
665
+ if isinstance(connection, SQLiteBackend):
666
+ return connection
667
+ if provider == "sqlite":
668
+ return SQLiteBackend(connection)
669
+ raise ValueError(f"Unsupported provider '{provider}'")
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+ from pathlib import Path
5
+ from typing import Any
6
+ from urllib.parse import urlparse
7
+
8
+
9
+ def resolve_sqlite_path(url: str | None) -> str:
10
+ if not url:
11
+ raise ValueError("SQLite datasource must specify a url, e.g. sqlite:///path/to.db")
12
+
13
+ parsed = urlparse(url)
14
+ if parsed.scheme != "sqlite":
15
+ raise ValueError(f"Unsupported sqlite url '{url}'")
16
+
17
+ if parsed.path in {":memory:", "/:memory:"}:
18
+ return ":memory:"
19
+
20
+ if parsed.netloc:
21
+ raise ValueError(f"Unsupported sqlite netloc '{parsed.netloc}' in url '{url}'")
22
+
23
+ path = parsed.path
24
+ if not path:
25
+ return ":memory:"
26
+
27
+ if path.startswith("//"):
28
+ target = Path(path[1:])
29
+ return target.as_posix()
30
+
31
+ if path.startswith("/"):
32
+ path = path[1:]
33
+ target = Path(path)
34
+ return target.as_posix()
35
+
36
+
37
+ def open_sqlite_connection(url: str | None) -> sqlite3.Connection:
38
+ path = resolve_sqlite_path(url)
39
+ return sqlite3.connect(path, check_same_thread=False)