duckling-orm 0.0.3__tar.gz → 0.0.4__tar.gz
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.
- {duckling_orm-0.0.3/src/duckling_orm.egg-info → duckling_orm-0.0.4}/PKG-INFO +29 -2
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/README.md +28 -1
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/pyproject.toml +2 -1
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/__init__.py +3 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/document.py +64 -16
- duckling_orm-0.0.4/src/duckling/ids.py +26 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4/src/duckling_orm.egg-info}/PKG-INFO +29 -2
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling_orm.egg-info/SOURCES.txt +2 -0
- duckling_orm-0.0.4/tests/test_custom_ids.py +84 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/LICENSE +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/setup.cfg +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/connection.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/exceptions.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/fields.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/init.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/operators.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling/query.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling_orm.egg-info/dependency_links.txt +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling_orm.egg-info/requires.txt +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/src/duckling_orm.egg-info/top_level.txt +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/tests/test_connection.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/tests/test_document.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/tests/test_fields.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/tests/test_init.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/tests/test_operators.py +0 -0
- {duckling_orm-0.0.3 → duckling_orm-0.0.4}/tests/test_query.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckling-orm
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A Beanie-inspired ORM for DuckDB — async-first, Pydantic-powered.
|
|
5
5
|
Author: Duckling Contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -108,7 +108,8 @@ init_duckling_sync(database="app.db", document_models=[User])
|
|
|
108
108
|
|
|
109
109
|
### Defining Models
|
|
110
110
|
|
|
111
|
-
Duckling models are Pydantic `BaseModel` subclasses
|
|
111
|
+
Duckling models are Pydantic `BaseModel` subclasses. By default, `id` is an
|
|
112
|
+
auto-generated, auto-incrementing integer primary key:
|
|
112
113
|
|
|
113
114
|
```python
|
|
114
115
|
from duckling import Document, IndexSpec
|
|
@@ -129,6 +130,32 @@ class Product(Document):
|
|
|
129
130
|
|
|
130
131
|
**Supported types:** `str`, `int`, `float`, `bool`, `bytes`, `datetime.date`, `datetime.datetime`, `datetime.time`, `uuid.UUID`, `Optional[T]`, `List[T]` (→ JSON), `dict` (→ JSON), nested Pydantic models (→ JSON), `Enum`.
|
|
131
132
|
|
|
133
|
+
### Custom Primary Keys (UUID, ULID, string)
|
|
134
|
+
|
|
135
|
+
Override `id` with any type other than `int` to opt out of the auto-increment
|
|
136
|
+
sequence and use your own primary key — a UUID, a sortable ULID, or a plain
|
|
137
|
+
caller-supplied string:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import uuid
|
|
141
|
+
from pydantic import Field
|
|
142
|
+
from duckling import Document, generate_ulid
|
|
143
|
+
|
|
144
|
+
class Session(Document):
|
|
145
|
+
id: str = Field(default_factory=generate_ulid) # sortable string PK
|
|
146
|
+
user_email: str
|
|
147
|
+
|
|
148
|
+
class ApiKey(Document):
|
|
149
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID PK
|
|
150
|
+
label: str
|
|
151
|
+
|
|
152
|
+
class Tag(Document):
|
|
153
|
+
id: str # no default — caller must supply an id
|
|
154
|
+
name: str
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Inserting a document whose `id` already exists raises `DocumentAlreadyExists`.
|
|
158
|
+
|
|
132
159
|
### Indexed Fields
|
|
133
160
|
|
|
134
161
|
```python
|
|
@@ -77,7 +77,8 @@ init_duckling_sync(database="app.db", document_models=[User])
|
|
|
77
77
|
|
|
78
78
|
### Defining Models
|
|
79
79
|
|
|
80
|
-
Duckling models are Pydantic `BaseModel` subclasses
|
|
80
|
+
Duckling models are Pydantic `BaseModel` subclasses. By default, `id` is an
|
|
81
|
+
auto-generated, auto-incrementing integer primary key:
|
|
81
82
|
|
|
82
83
|
```python
|
|
83
84
|
from duckling import Document, IndexSpec
|
|
@@ -98,6 +99,32 @@ class Product(Document):
|
|
|
98
99
|
|
|
99
100
|
**Supported types:** `str`, `int`, `float`, `bool`, `bytes`, `datetime.date`, `datetime.datetime`, `datetime.time`, `uuid.UUID`, `Optional[T]`, `List[T]` (→ JSON), `dict` (→ JSON), nested Pydantic models (→ JSON), `Enum`.
|
|
100
101
|
|
|
102
|
+
### Custom Primary Keys (UUID, ULID, string)
|
|
103
|
+
|
|
104
|
+
Override `id` with any type other than `int` to opt out of the auto-increment
|
|
105
|
+
sequence and use your own primary key — a UUID, a sortable ULID, or a plain
|
|
106
|
+
caller-supplied string:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
import uuid
|
|
110
|
+
from pydantic import Field
|
|
111
|
+
from duckling import Document, generate_ulid
|
|
112
|
+
|
|
113
|
+
class Session(Document):
|
|
114
|
+
id: str = Field(default_factory=generate_ulid) # sortable string PK
|
|
115
|
+
user_email: str
|
|
116
|
+
|
|
117
|
+
class ApiKey(Document):
|
|
118
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID PK
|
|
119
|
+
label: str
|
|
120
|
+
|
|
121
|
+
class Tag(Document):
|
|
122
|
+
id: str # no default — caller must supply an id
|
|
123
|
+
name: str
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Inserting a document whose `id` already exists raises `DocumentAlreadyExists`.
|
|
127
|
+
|
|
101
128
|
### Indexed Fields
|
|
102
129
|
|
|
103
130
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "duckling-orm"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.4"
|
|
8
8
|
description = "A Beanie-inspired ORM for DuckDB — async-first, Pydantic-powered."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "Apache-2.0"}
|
|
@@ -44,6 +44,7 @@ dev = [
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
|
|
47
48
|
[tool.setuptools.packages.find]
|
|
48
49
|
where = ["src"]
|
|
49
50
|
include = ["duckling*"]
|
|
@@ -40,6 +40,7 @@ from .fields import (
|
|
|
40
40
|
IndexSpec,
|
|
41
41
|
SortDirection,
|
|
42
42
|
)
|
|
43
|
+
from .ids import generate_ulid
|
|
43
44
|
from .init import init_duckling, init_duckling_sync
|
|
44
45
|
from .operators import (
|
|
45
46
|
And,
|
|
@@ -84,6 +85,8 @@ __all__ = [
|
|
|
84
85
|
"SortDirection",
|
|
85
86
|
"FieldProxy",
|
|
86
87
|
"Expression",
|
|
88
|
+
# IDs
|
|
89
|
+
"generate_ulid",
|
|
87
90
|
# Query
|
|
88
91
|
"FindQuery",
|
|
89
92
|
# Operators
|
|
@@ -46,10 +46,11 @@ from typing import (
|
|
|
46
46
|
get_type_hints,
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
+
import duckdb
|
|
49
50
|
from pydantic import BaseModel, ConfigDict
|
|
50
51
|
|
|
51
52
|
from .connection import DucklingSession, get_session
|
|
52
|
-
from .exceptions import DocumentNotFound, InvalidQueryError
|
|
53
|
+
from .exceptions import DocumentAlreadyExists, DocumentNotFound, InvalidQueryError
|
|
53
54
|
from .fields import Expression, FieldProxy, IndexSpec, SortDirection
|
|
54
55
|
from .query import FindQuery
|
|
55
56
|
|
|
@@ -287,6 +288,35 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
287
288
|
indexed.append((name, arg))
|
|
288
289
|
return indexed
|
|
289
290
|
|
|
291
|
+
# ── Primary key strategy ──────────────────
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def _is_auto_increment_id(cls) -> bool:
|
|
295
|
+
"""
|
|
296
|
+
True if the `id` field resolves to `int` (the default primary key
|
|
297
|
+
strategy: an auto-incrementing INTEGER backed by a DuckDB SEQUENCE).
|
|
298
|
+
|
|
299
|
+
Any other resolved type (str, uuid.UUID, ...) is treated as a
|
|
300
|
+
caller/default_factory-supplied primary key with no sequence.
|
|
301
|
+
"""
|
|
302
|
+
import typing
|
|
303
|
+
|
|
304
|
+
hints = get_type_hints(cls, include_extras=True)
|
|
305
|
+
py_type = hints.get("id", int)
|
|
306
|
+
|
|
307
|
+
origin = get_origin(py_type)
|
|
308
|
+
if origin is getattr(typing, "Annotated", None):
|
|
309
|
+
py_type = get_args(py_type)[0]
|
|
310
|
+
origin = get_origin(py_type)
|
|
311
|
+
|
|
312
|
+
args = get_args(py_type)
|
|
313
|
+
if args:
|
|
314
|
+
non_none = [a for a in args if a is not type(None)]
|
|
315
|
+
if non_none:
|
|
316
|
+
py_type = non_none[0]
|
|
317
|
+
|
|
318
|
+
return py_type is int
|
|
319
|
+
|
|
290
320
|
# ── Table creation ────────────────────────
|
|
291
321
|
|
|
292
322
|
@classmethod
|
|
@@ -296,24 +326,28 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
296
326
|
col_types = cls._get_column_types()
|
|
297
327
|
indexed = dict(cls._get_indexed_fields())
|
|
298
328
|
|
|
329
|
+
if cls._is_auto_increment_id():
|
|
330
|
+
id_def = f'"id" INTEGER PRIMARY KEY DEFAULT(nextval(\'seq_{table}_id\'))'
|
|
331
|
+
else:
|
|
332
|
+
id_def = f'"id" {col_types["id"]} PRIMARY KEY'
|
|
333
|
+
|
|
299
334
|
columns = []
|
|
300
335
|
for col_name, col_type in col_types.items():
|
|
301
|
-
parts = [f'"{col_name}"', col_type]
|
|
302
336
|
if col_name == "id":
|
|
303
|
-
|
|
304
|
-
|
|
337
|
+
continue
|
|
338
|
+
parts = [f'"{col_name}"', col_type]
|
|
305
339
|
if col_name in indexed and indexed[col_name].unique:
|
|
306
340
|
parts.append("UNIQUE")
|
|
307
341
|
columns.append(" ".join(parts))
|
|
308
342
|
|
|
309
|
-
|
|
310
|
-
col_defs = [f'"id" INTEGER PRIMARY KEY DEFAULT(nextval(\'seq_{table}_id\'))']
|
|
311
|
-
col_defs.extend(columns)
|
|
343
|
+
col_defs = [id_def] + columns
|
|
312
344
|
|
|
313
345
|
return f'CREATE TABLE IF NOT EXISTS "{table}" (\n ' + ",\n ".join(col_defs) + "\n)"
|
|
314
346
|
|
|
315
347
|
@classmethod
|
|
316
|
-
def _build_sequence_sql(cls) -> str:
|
|
348
|
+
def _build_sequence_sql(cls) -> Optional[str]:
|
|
349
|
+
if not cls._is_auto_increment_id():
|
|
350
|
+
return None
|
|
317
351
|
table = cls._get_table_name()
|
|
318
352
|
return f"CREATE SEQUENCE IF NOT EXISTS seq_{table}_id START 1"
|
|
319
353
|
|
|
@@ -321,7 +355,9 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
321
355
|
def _create_table_sync(cls) -> None:
|
|
322
356
|
"""Create the table synchronously."""
|
|
323
357
|
session = get_session()
|
|
324
|
-
|
|
358
|
+
seq_sql = cls._build_sequence_sql()
|
|
359
|
+
if seq_sql:
|
|
360
|
+
session.execute(seq_sql)
|
|
325
361
|
session.execute(cls._build_create_table_sql())
|
|
326
362
|
|
|
327
363
|
# Create indexes
|
|
@@ -370,8 +406,10 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
370
406
|
table = self._get_table_name()
|
|
371
407
|
data = self._to_row_dict()
|
|
372
408
|
|
|
373
|
-
# Remove id if None
|
|
374
|
-
|
|
409
|
+
# Remove id if None so the auto-increment sequence default applies.
|
|
410
|
+
# A non-auto-increment (str/UUID) id left None is kept so the
|
|
411
|
+
# missing-primary-key error surfaces from the database.
|
|
412
|
+
if self._is_auto_increment_id() and data.get("id") is None:
|
|
375
413
|
data.pop("id", None)
|
|
376
414
|
|
|
377
415
|
columns = list(data.keys())
|
|
@@ -381,7 +419,12 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
381
419
|
|
|
382
420
|
sql = f'INSERT INTO "{table}" ({col_str}) VALUES ({placeholders}) RETURNING "id"'
|
|
383
421
|
|
|
384
|
-
|
|
422
|
+
try:
|
|
423
|
+
row = await session.async_fetchone(sql, values)
|
|
424
|
+
except duckdb.ConstraintException as e:
|
|
425
|
+
raise DocumentAlreadyExists(
|
|
426
|
+
f"{self.__class__.__name__} with id={data.get('id')!r} already exists"
|
|
427
|
+
) from e
|
|
385
428
|
if row:
|
|
386
429
|
self.id = row[0]
|
|
387
430
|
return self
|
|
@@ -392,7 +435,7 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
392
435
|
table = self._get_table_name()
|
|
393
436
|
data = self._to_row_dict()
|
|
394
437
|
|
|
395
|
-
if data.get("id") is None:
|
|
438
|
+
if self._is_auto_increment_id() and data.get("id") is None:
|
|
396
439
|
data.pop("id", None)
|
|
397
440
|
|
|
398
441
|
columns = list(data.keys())
|
|
@@ -401,7 +444,12 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
401
444
|
values = [data[c] for c in columns]
|
|
402
445
|
|
|
403
446
|
sql = f'INSERT INTO "{table}" ({col_str}) VALUES ({placeholders}) RETURNING "id"'
|
|
404
|
-
|
|
447
|
+
try:
|
|
448
|
+
row = session.fetchone(sql, values)
|
|
449
|
+
except duckdb.ConstraintException as e:
|
|
450
|
+
raise DocumentAlreadyExists(
|
|
451
|
+
f"{self.__class__.__name__} with id={data.get('id')!r} already exists"
|
|
452
|
+
) from e
|
|
405
453
|
if row:
|
|
406
454
|
self.id = row[0]
|
|
407
455
|
return self
|
|
@@ -541,7 +589,7 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
541
589
|
# ── Query: get by id ──────────────────────
|
|
542
590
|
|
|
543
591
|
@classmethod
|
|
544
|
-
async def get(cls: Type[T], doc_id:
|
|
592
|
+
async def get(cls: Type[T], doc_id: Any) -> Optional[T]:
|
|
545
593
|
"""Get a document by its primary key id."""
|
|
546
594
|
session = get_session()
|
|
547
595
|
table = cls._get_table_name()
|
|
@@ -553,7 +601,7 @@ class Document(BaseModel, metaclass=DocumentMeta):
|
|
|
553
601
|
return cls._from_row(row, cls._get_column_names())
|
|
554
602
|
|
|
555
603
|
@classmethod
|
|
556
|
-
def get_sync(cls: Type[T], doc_id:
|
|
604
|
+
def get_sync(cls: Type[T], doc_id: Any) -> Optional[T]:
|
|
557
605
|
"""Get a document by id synchronously."""
|
|
558
606
|
session = get_session()
|
|
559
607
|
table = cls._get_table_name()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""ID generation helpers for custom primary keys."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
_CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def generate_ulid() -> str:
|
|
10
|
+
"""
|
|
11
|
+
Generate a 26-character ULID (48-bit ms timestamp + 80-bit randomness),
|
|
12
|
+
lexicographically sortable by creation time.
|
|
13
|
+
|
|
14
|
+
Usage:
|
|
15
|
+
class Session(Document):
|
|
16
|
+
id: str = Field(default_factory=generate_ulid)
|
|
17
|
+
"""
|
|
18
|
+
ms = int(time.time() * 1000)
|
|
19
|
+
randomness = int.from_bytes(os.urandom(10), "big")
|
|
20
|
+
value = (ms << 80) | randomness
|
|
21
|
+
|
|
22
|
+
chars = []
|
|
23
|
+
for _ in range(26):
|
|
24
|
+
value, rem = divmod(value, 32)
|
|
25
|
+
chars.append(_CROCKFORD[rem])
|
|
26
|
+
return "".join(reversed(chars))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: duckling-orm
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: A Beanie-inspired ORM for DuckDB — async-first, Pydantic-powered.
|
|
5
5
|
Author: Duckling Contributors
|
|
6
6
|
License: Apache-2.0
|
|
@@ -108,7 +108,8 @@ init_duckling_sync(database="app.db", document_models=[User])
|
|
|
108
108
|
|
|
109
109
|
### Defining Models
|
|
110
110
|
|
|
111
|
-
Duckling models are Pydantic `BaseModel` subclasses
|
|
111
|
+
Duckling models are Pydantic `BaseModel` subclasses. By default, `id` is an
|
|
112
|
+
auto-generated, auto-incrementing integer primary key:
|
|
112
113
|
|
|
113
114
|
```python
|
|
114
115
|
from duckling import Document, IndexSpec
|
|
@@ -129,6 +130,32 @@ class Product(Document):
|
|
|
129
130
|
|
|
130
131
|
**Supported types:** `str`, `int`, `float`, `bool`, `bytes`, `datetime.date`, `datetime.datetime`, `datetime.time`, `uuid.UUID`, `Optional[T]`, `List[T]` (→ JSON), `dict` (→ JSON), nested Pydantic models (→ JSON), `Enum`.
|
|
131
132
|
|
|
133
|
+
### Custom Primary Keys (UUID, ULID, string)
|
|
134
|
+
|
|
135
|
+
Override `id` with any type other than `int` to opt out of the auto-increment
|
|
136
|
+
sequence and use your own primary key — a UUID, a sortable ULID, or a plain
|
|
137
|
+
caller-supplied string:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
import uuid
|
|
141
|
+
from pydantic import Field
|
|
142
|
+
from duckling import Document, generate_ulid
|
|
143
|
+
|
|
144
|
+
class Session(Document):
|
|
145
|
+
id: str = Field(default_factory=generate_ulid) # sortable string PK
|
|
146
|
+
user_email: str
|
|
147
|
+
|
|
148
|
+
class ApiKey(Document):
|
|
149
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4) # UUID PK
|
|
150
|
+
label: str
|
|
151
|
+
|
|
152
|
+
class Tag(Document):
|
|
153
|
+
id: str # no default — caller must supply an id
|
|
154
|
+
name: str
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Inserting a document whose `id` already exists raises `DocumentAlreadyExists`.
|
|
158
|
+
|
|
132
159
|
### Indexed Fields
|
|
133
160
|
|
|
134
161
|
```python
|
|
@@ -6,6 +6,7 @@ src/duckling/connection.py
|
|
|
6
6
|
src/duckling/document.py
|
|
7
7
|
src/duckling/exceptions.py
|
|
8
8
|
src/duckling/fields.py
|
|
9
|
+
src/duckling/ids.py
|
|
9
10
|
src/duckling/init.py
|
|
10
11
|
src/duckling/operators.py
|
|
11
12
|
src/duckling/query.py
|
|
@@ -15,6 +16,7 @@ src/duckling_orm.egg-info/dependency_links.txt
|
|
|
15
16
|
src/duckling_orm.egg-info/requires.txt
|
|
16
17
|
src/duckling_orm.egg-info/top_level.txt
|
|
17
18
|
tests/test_connection.py
|
|
19
|
+
tests/test_custom_ids.py
|
|
18
20
|
tests/test_document.py
|
|
19
21
|
tests/test_fields.py
|
|
20
22
|
tests/test_init.py
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Tests for custom (non auto-increment) primary keys — str, UUID, ULID."""
|
|
2
|
+
|
|
3
|
+
import uuid
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from duckling import DocumentAlreadyExists, get_session
|
|
8
|
+
|
|
9
|
+
from .models import ApiKey, Session, Tag
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestSyncCustomIds:
|
|
13
|
+
def test_ulid_id_is_generated_and_unique(self, sync_db):
|
|
14
|
+
a = Session(user_email="a@test.com")
|
|
15
|
+
b = Session(user_email="b@test.com")
|
|
16
|
+
a.insert_sync()
|
|
17
|
+
b.insert_sync()
|
|
18
|
+
|
|
19
|
+
assert isinstance(a.id, str) and len(a.id) == 26
|
|
20
|
+
assert a.id != b.id
|
|
21
|
+
|
|
22
|
+
def test_uuid_id_round_trip(self, sync_db):
|
|
23
|
+
key = ApiKey(label="prod")
|
|
24
|
+
key.insert_sync()
|
|
25
|
+
assert isinstance(key.id, uuid.UUID)
|
|
26
|
+
|
|
27
|
+
found = ApiKey.get_sync(key.id)
|
|
28
|
+
assert found is not None
|
|
29
|
+
assert found.label == "prod"
|
|
30
|
+
|
|
31
|
+
def test_caller_supplied_id_round_trip(self, sync_db):
|
|
32
|
+
tag = Tag(id="billing", name="Billing")
|
|
33
|
+
tag.insert_sync()
|
|
34
|
+
|
|
35
|
+
found = Tag.get_sync("billing")
|
|
36
|
+
assert found is not None
|
|
37
|
+
assert found.name == "Billing"
|
|
38
|
+
|
|
39
|
+
def test_duplicate_caller_supplied_id_raises(self, sync_db):
|
|
40
|
+
Tag(id="dup", name="First").insert_sync()
|
|
41
|
+
with pytest.raises(DocumentAlreadyExists):
|
|
42
|
+
Tag(id="dup", name="Second").insert_sync()
|
|
43
|
+
|
|
44
|
+
def test_no_sequence_created_for_string_id_model(self, sync_db):
|
|
45
|
+
session = get_session()
|
|
46
|
+
rows = session.fetchall(
|
|
47
|
+
"SELECT sequence_name FROM duckdb_sequences() WHERE sequence_name = ?",
|
|
48
|
+
["seq_tags_id"],
|
|
49
|
+
)
|
|
50
|
+
assert rows == []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class TestAsyncCustomIds:
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_ulid_id_is_generated_and_unique(self, async_db):
|
|
56
|
+
a = Session(user_email="a@test.com")
|
|
57
|
+
b = Session(user_email="b@test.com")
|
|
58
|
+
await a.insert()
|
|
59
|
+
await b.insert()
|
|
60
|
+
|
|
61
|
+
assert isinstance(a.id, str) and len(a.id) == 26
|
|
62
|
+
assert a.id != b.id
|
|
63
|
+
|
|
64
|
+
@pytest.mark.asyncio
|
|
65
|
+
async def test_duplicate_caller_supplied_id_raises(self, async_db):
|
|
66
|
+
await Tag(id="dup", name="First").insert()
|
|
67
|
+
with pytest.raises(DocumentAlreadyExists):
|
|
68
|
+
await Tag(id="dup", name="Second").insert()
|
|
69
|
+
|
|
70
|
+
@pytest.mark.asyncio
|
|
71
|
+
async def test_recreate_tables_with_mixed_pk_types(self):
|
|
72
|
+
from duckling import get_session, init_duckling
|
|
73
|
+
|
|
74
|
+
from .models import ALL_MODELS
|
|
75
|
+
|
|
76
|
+
session = await init_duckling(database=":memory:", document_models=ALL_MODELS)
|
|
77
|
+
conn = session.connection
|
|
78
|
+
|
|
79
|
+
# Reuse the same live connection so recreate_tables actually drops
|
|
80
|
+
# and rebuilds tables/sequences that exist, instead of a fresh :memory: db.
|
|
81
|
+
await init_duckling(
|
|
82
|
+
connection=conn, document_models=ALL_MODELS, recreate_tables=True
|
|
83
|
+
)
|
|
84
|
+
assert get_session().connection is conn
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|