sqliter-py 0.12.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.
- sqliter/__init__.py +9 -0
- sqliter/constants.py +45 -0
- sqliter/exceptions.py +198 -0
- sqliter/helpers.py +100 -0
- sqliter/model/__init__.py +46 -0
- sqliter/model/foreign_key.py +153 -0
- sqliter/model/model.py +236 -0
- sqliter/model/unique.py +28 -0
- sqliter/py.typed +0 -0
- sqliter/query/__init__.py +9 -0
- sqliter/query/query.py +891 -0
- sqliter/sqliter.py +1087 -0
- sqliter_py-0.12.0.dist-info/METADATA +209 -0
- sqliter_py-0.12.0.dist-info/RECORD +15 -0
- sqliter_py-0.12.0.dist-info/WHEEL +4 -0
sqliter/__init__.py
ADDED
sqliter/constants.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Constant values and mappings used throughout SQLiter.
|
|
2
|
+
|
|
3
|
+
This module defines constant dictionaries that map SQLiter-specific
|
|
4
|
+
concepts to their SQLite equivalents. It includes mappings for query
|
|
5
|
+
operators and data types, which are crucial for translating between
|
|
6
|
+
Pydantic models and SQLite database operations.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import datetime
|
|
10
|
+
|
|
11
|
+
# A dictionary mapping SQLiter filter operators to their corresponding SQL
|
|
12
|
+
# operators.
|
|
13
|
+
OPERATOR_MAPPING = {
|
|
14
|
+
"__lt": "<",
|
|
15
|
+
"__lte": "<=",
|
|
16
|
+
"__gt": ">",
|
|
17
|
+
"__gte": ">=",
|
|
18
|
+
"__eq": "=",
|
|
19
|
+
"__ne": "!=",
|
|
20
|
+
"__in": "IN",
|
|
21
|
+
"__not_in": "NOT IN",
|
|
22
|
+
"__isnull": "IS NULL",
|
|
23
|
+
"__notnull": "IS NOT NULL",
|
|
24
|
+
"__startswith": "LIKE",
|
|
25
|
+
"__endswith": "LIKE",
|
|
26
|
+
"__contains": "LIKE",
|
|
27
|
+
"__istartswith": "LIKE",
|
|
28
|
+
"__iendswith": "LIKE",
|
|
29
|
+
"__icontains": "LIKE",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# A dictionary mapping Python types to their corresponding SQLite column types.
|
|
33
|
+
SQLITE_TYPE_MAPPING = {
|
|
34
|
+
int: "INTEGER",
|
|
35
|
+
float: "REAL",
|
|
36
|
+
str: "TEXT",
|
|
37
|
+
bool: "INTEGER", # SQLite stores booleans as integers (0 or 1)
|
|
38
|
+
bytes: "BLOB",
|
|
39
|
+
datetime.datetime: "INTEGER", # Store as Unix timestamp
|
|
40
|
+
datetime.date: "INTEGER", # Store as Unix timestamp
|
|
41
|
+
list: "BLOB",
|
|
42
|
+
dict: "BLOB",
|
|
43
|
+
set: "BLOB",
|
|
44
|
+
tuple: "BLOB",
|
|
45
|
+
}
|
sqliter/exceptions.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""Custom exception classes for SQLiter error handling.
|
|
2
|
+
|
|
3
|
+
This module defines a hierarchy of exception classes specific to
|
|
4
|
+
SQLiter operations. These exceptions provide detailed error information
|
|
5
|
+
for various scenarios such as connection issues, invalid queries,
|
|
6
|
+
and CRUD operation failures, enabling more precise error handling
|
|
7
|
+
in applications using SQLiter.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
import traceback
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SqliterError(Exception):
|
|
16
|
+
"""Base exception class for all SQLiter-specific errors.
|
|
17
|
+
|
|
18
|
+
This class serves as the parent for all custom exceptions in SQLiter,
|
|
19
|
+
providing a consistent interface and message formatting.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
message_template (str): A template string for the error message.
|
|
23
|
+
original_exception (Exception): The original exception that was caught.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
message_template: str = "An error occurred in the SQLiter package."
|
|
27
|
+
|
|
28
|
+
def __init__(self, *args: object) -> None:
|
|
29
|
+
"""Format the message using the provided arguments.
|
|
30
|
+
|
|
31
|
+
We also capture (and display) the current exception context and chain
|
|
32
|
+
any previous exceptions.
|
|
33
|
+
|
|
34
|
+
:param args: Arguments to format into the message template
|
|
35
|
+
"""
|
|
36
|
+
if args:
|
|
37
|
+
message = self.message_template.format(*args)
|
|
38
|
+
else:
|
|
39
|
+
message = (
|
|
40
|
+
self.message_template.replace("'{}'", "")
|
|
41
|
+
.replace(":", "")
|
|
42
|
+
.strip()
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Capture the current exception context
|
|
46
|
+
self.original_exception = sys.exc_info()[1]
|
|
47
|
+
|
|
48
|
+
# If there's an active exception, append its information to our message
|
|
49
|
+
if self.original_exception:
|
|
50
|
+
original_type = type(self.original_exception).__name__
|
|
51
|
+
original_module = type(self.original_exception).__module__
|
|
52
|
+
|
|
53
|
+
# Get the traceback of the original exception
|
|
54
|
+
tb = traceback.extract_tb(self.original_exception.__traceback__)
|
|
55
|
+
if tb:
|
|
56
|
+
last_frame = tb[-1]
|
|
57
|
+
file_path = os.path.relpath(last_frame.filename)
|
|
58
|
+
line_number = last_frame.lineno
|
|
59
|
+
location = f"{file_path}:{line_number}"
|
|
60
|
+
else:
|
|
61
|
+
location = "unknown location"
|
|
62
|
+
|
|
63
|
+
message += (
|
|
64
|
+
f"\n --> {original_module}.{original_type} "
|
|
65
|
+
f"from {location}: {self.original_exception}"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Call the parent constructor with our formatted message
|
|
69
|
+
super().__init__(message)
|
|
70
|
+
|
|
71
|
+
# Explicitly chain exceptions if there's an active one
|
|
72
|
+
if self.original_exception:
|
|
73
|
+
self.__cause__ = self.original_exception
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class DatabaseConnectionError(SqliterError):
|
|
77
|
+
"""Exception raised when a database connection cannot be established."""
|
|
78
|
+
|
|
79
|
+
message_template = "Failed to connect to the database: '{}'"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class InvalidOffsetError(SqliterError):
|
|
83
|
+
"""Exception raised when an invalid offset value is provided."""
|
|
84
|
+
|
|
85
|
+
message_template = (
|
|
86
|
+
"Invalid offset value: '{}'. Offset must be a positive integer."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class InvalidOrderError(SqliterError):
|
|
91
|
+
"""Exception raised when an invalid order specification is provided."""
|
|
92
|
+
|
|
93
|
+
message_template = "Invalid order value - {}"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class TableCreationError(SqliterError):
|
|
97
|
+
"""Exception raised when a table cannot be created in the database."""
|
|
98
|
+
|
|
99
|
+
message_template = "Failed to create the table: '{}'"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class RecordInsertionError(SqliterError):
|
|
103
|
+
"""Exception raised when a record cannot be inserted into the database."""
|
|
104
|
+
|
|
105
|
+
message_template = "Failed to insert record into table: '{}'"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class RecordUpdateError(SqliterError):
|
|
109
|
+
"""Exception raised when a record cannot be updated in the database."""
|
|
110
|
+
|
|
111
|
+
message_template = "Failed to update record in table: '{}'"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class RecordNotFoundError(SqliterError):
|
|
115
|
+
"""Exception raised when a requested record is not found in the database."""
|
|
116
|
+
|
|
117
|
+
message_template = "Failed to find that record in the table (key '{}') "
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class RecordFetchError(SqliterError):
|
|
121
|
+
"""Exception raised on an error fetching records from the database."""
|
|
122
|
+
|
|
123
|
+
message_template = "Failed to fetch record from table: '{}'"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class RecordDeletionError(SqliterError):
|
|
127
|
+
"""Exception raised when a record cannot be deleted from the database."""
|
|
128
|
+
|
|
129
|
+
message_template = "Failed to delete record from table: '{}'"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class InvalidFilterError(SqliterError):
|
|
133
|
+
"""Exception raised when an invalid filter is applied to a query."""
|
|
134
|
+
|
|
135
|
+
message_template = "Failed to apply filter: invalid field '{}'"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class TableDeletionError(SqliterError):
|
|
139
|
+
"""Raised when a table cannot be deleted from the database."""
|
|
140
|
+
|
|
141
|
+
message_template = "Failed to delete the table: '{}'"
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class SqlExecutionError(SqliterError):
|
|
145
|
+
"""Raised when an SQL execution fails."""
|
|
146
|
+
|
|
147
|
+
message_template = "Failed to execute SQL: '{}'"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class InvalidIndexError(SqliterError):
|
|
151
|
+
"""Exception raised when an invalid index field is specified.
|
|
152
|
+
|
|
153
|
+
This error is triggered if one or more fields specified for an index
|
|
154
|
+
do not exist in the model's fields.
|
|
155
|
+
|
|
156
|
+
Attributes:
|
|
157
|
+
invalid_fields (list[str]): The list of fields that were invalid.
|
|
158
|
+
model_class (str): The name of the model where the error occurred.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
message_template = "Invalid fields for indexing in model '{}': {}"
|
|
162
|
+
|
|
163
|
+
def __init__(self, invalid_fields: list[str], model_class: str) -> None:
|
|
164
|
+
"""Tidy up the error message by joining the invalid fields."""
|
|
165
|
+
# Join invalid fields into a comma-separated string
|
|
166
|
+
invalid_fields_str = ", ".join(invalid_fields)
|
|
167
|
+
# Pass the formatted message to the parent class
|
|
168
|
+
super().__init__(model_class, invalid_fields_str)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class ForeignKeyError(SqliterError):
|
|
172
|
+
"""Base exception for foreign key related errors."""
|
|
173
|
+
|
|
174
|
+
message_template = "Foreign key error: {}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class ForeignKeyConstraintError(ForeignKeyError):
|
|
178
|
+
"""Raised when a foreign key constraint is violated.
|
|
179
|
+
|
|
180
|
+
This error occurs when attempting to insert/update a record with a
|
|
181
|
+
foreign key value that doesn't exist in the referenced table, or
|
|
182
|
+
when attempting to delete a record that is still referenced.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
message_template = (
|
|
186
|
+
"Foreign key constraint violation: Cannot {} record - "
|
|
187
|
+
"referenced record {}"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class InvalidForeignKeyError(ForeignKeyError):
|
|
192
|
+
"""Raised when an invalid foreign key configuration is detected.
|
|
193
|
+
|
|
194
|
+
This error occurs when defining a foreign key with invalid parameters,
|
|
195
|
+
such as using SET NULL without null=True.
|
|
196
|
+
"""
|
|
197
|
+
|
|
198
|
+
message_template = "Invalid foreign key configuration: {}"
|
sqliter/helpers.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Utility functions for SQLiter internal operations.
|
|
2
|
+
|
|
3
|
+
This module provides helper functions used across the SQLiter library,
|
|
4
|
+
primarily for type inference and mapping between Python and SQLite
|
|
5
|
+
data types. These utilities support the core functionality of model
|
|
6
|
+
to database schema translation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import datetime
|
|
12
|
+
from typing import Union
|
|
13
|
+
|
|
14
|
+
from sqliter.constants import SQLITE_TYPE_MAPPING
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def infer_sqlite_type(field_type: Union[type, None]) -> str:
|
|
18
|
+
"""Infer the SQLite column type based on the Python type.
|
|
19
|
+
|
|
20
|
+
This function maps Python types to their corresponding SQLite column
|
|
21
|
+
types. It's used when creating database tables to ensure that the
|
|
22
|
+
correct SQLite types are used for each field.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
field_type: The Python type of the field, or None.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
A string representing the corresponding SQLite column type.
|
|
29
|
+
|
|
30
|
+
Note:
|
|
31
|
+
If the input type is None or not recognized, it defaults to 'TEXT'.
|
|
32
|
+
"""
|
|
33
|
+
# If field_type is None, default to TEXT
|
|
34
|
+
if field_type is None:
|
|
35
|
+
return "TEXT"
|
|
36
|
+
|
|
37
|
+
# Map the simplified type to an SQLite type
|
|
38
|
+
return SQLITE_TYPE_MAPPING.get(field_type, "TEXT")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def to_unix_timestamp(value: datetime.date | datetime.datetime) -> int:
|
|
42
|
+
"""Convert datetime or date to a Unix timestamp in UTC.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
value: The datetime or date object to convert.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
An integer Unix timestamp.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
TypeError: If the value is not a datetime or date object.
|
|
52
|
+
"""
|
|
53
|
+
if isinstance(value, datetime.datetime):
|
|
54
|
+
# If no timezone is provided, assume local time and convert to UTC
|
|
55
|
+
if value.tzinfo is None:
|
|
56
|
+
value = value.astimezone() # Convert to user's local timezone
|
|
57
|
+
# Convert to UTC before storing
|
|
58
|
+
value = value.astimezone(datetime.timezone.utc)
|
|
59
|
+
return int(value.timestamp())
|
|
60
|
+
if isinstance(value, datetime.date):
|
|
61
|
+
# Convert date to datetime at midnight in UTC
|
|
62
|
+
dt = datetime.datetime.combine(
|
|
63
|
+
value, datetime.time(0, 0), tzinfo=datetime.timezone.utc
|
|
64
|
+
)
|
|
65
|
+
return int(dt.timestamp())
|
|
66
|
+
|
|
67
|
+
err_msg = "Expected datetime or date object."
|
|
68
|
+
raise TypeError(err_msg)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def from_unix_timestamp(
|
|
72
|
+
value: int, to_type: type, *, localize: bool = True
|
|
73
|
+
) -> datetime.date | datetime.datetime:
|
|
74
|
+
"""Convert a Unix timestamp to datetime or date, optionally to local time.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
value: The Unix timestamp as an integer.
|
|
78
|
+
to_type: The expected output type, either datetime or date.
|
|
79
|
+
localize: If True, convert the datetime to the user's local timezone.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
The corresponding datetime or date object.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
TypeError: If to_type is not datetime or date.
|
|
86
|
+
"""
|
|
87
|
+
if to_type is datetime.datetime:
|
|
88
|
+
# Convert the Unix timestamp to UTC datetime
|
|
89
|
+
dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
|
|
90
|
+
# Convert to local time if requested
|
|
91
|
+
return dt.astimezone() if localize else dt
|
|
92
|
+
if to_type is datetime.date:
|
|
93
|
+
# Convert to UTC datetime first
|
|
94
|
+
dt = datetime.datetime.fromtimestamp(value, tz=datetime.timezone.utc)
|
|
95
|
+
# Convert to local time if requested, then return the date part
|
|
96
|
+
dt_local = dt.astimezone() if localize else dt
|
|
97
|
+
return dt_local.date() # Extract the date part
|
|
98
|
+
|
|
99
|
+
err_msg = "Expected datetime or date type."
|
|
100
|
+
raise TypeError(err_msg)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""This module provides the base model class for SQLiter database models.
|
|
2
|
+
|
|
3
|
+
It exports the BaseDBModel class, which is used to define database
|
|
4
|
+
models in SQLiter applications, and the unique function, which is used to
|
|
5
|
+
define unique constraints on model fields.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import warnings
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from typing_extensions import deprecated
|
|
12
|
+
|
|
13
|
+
from .foreign_key import ForeignKey, ForeignKeyInfo, get_foreign_key_info
|
|
14
|
+
from .model import BaseDBModel, SerializableField
|
|
15
|
+
from .unique import unique
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@deprecated("Use 'unique' instead. Will be removed in a future version.")
|
|
19
|
+
def Unique(default: Any = ..., **kwargs: Any) -> Any: # noqa: ANN401, N802
|
|
20
|
+
"""Deprecated: Use 'unique' instead. Will be removed in a future version.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
default: The default value for the field.
|
|
24
|
+
**kwargs: Additional keyword arguments to pass to Field.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
A Field with unique metadata attached.
|
|
28
|
+
"""
|
|
29
|
+
warnings.warn(
|
|
30
|
+
"Unique is deprecated and will be removed in a future version. "
|
|
31
|
+
"Use 'unique' instead.",
|
|
32
|
+
DeprecationWarning,
|
|
33
|
+
stacklevel=2,
|
|
34
|
+
)
|
|
35
|
+
return unique(default=default, **kwargs)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
"BaseDBModel",
|
|
40
|
+
"ForeignKey",
|
|
41
|
+
"ForeignKeyInfo",
|
|
42
|
+
"SerializableField",
|
|
43
|
+
"Unique",
|
|
44
|
+
"get_foreign_key_info",
|
|
45
|
+
"unique",
|
|
46
|
+
]
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""Foreign key support for SQLiter ORM.
|
|
2
|
+
|
|
3
|
+
This module provides the ForeignKey factory function and ForeignKeyInfo
|
|
4
|
+
dataclass for defining foreign key relationships between models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional
|
|
11
|
+
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
|
|
14
|
+
from sqliter.exceptions import InvalidForeignKeyError
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
17
|
+
from pydantic.fields import FieldInfo
|
|
18
|
+
|
|
19
|
+
from sqliter.model.model import BaseDBModel
|
|
20
|
+
|
|
21
|
+
# Type alias for foreign key actions
|
|
22
|
+
FKAction = Literal["CASCADE", "SET NULL", "RESTRICT", "NO ACTION"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ForeignKeyInfo:
|
|
27
|
+
"""Metadata about a foreign key relationship.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
to_model: The target model class that this foreign key references.
|
|
31
|
+
on_delete: Action to take when the referenced record is deleted.
|
|
32
|
+
on_update: Action to take when the referenced record's PK is updated.
|
|
33
|
+
null: Whether the foreign key field can be NULL.
|
|
34
|
+
unique: Whether the foreign key field must be unique (one-to-one).
|
|
35
|
+
related_name: Optional name for the reverse relationship (Phase 2).
|
|
36
|
+
db_column: Optional custom column name in the database.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
to_model: type[BaseDBModel]
|
|
40
|
+
on_delete: FKAction
|
|
41
|
+
on_update: FKAction
|
|
42
|
+
null: bool
|
|
43
|
+
unique: bool
|
|
44
|
+
related_name: Optional[str]
|
|
45
|
+
db_column: Optional[str]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def ForeignKey( # noqa: N802, PLR0913
|
|
49
|
+
to: type[BaseDBModel],
|
|
50
|
+
*,
|
|
51
|
+
on_delete: FKAction = "RESTRICT",
|
|
52
|
+
on_update: FKAction = "RESTRICT",
|
|
53
|
+
null: bool = False,
|
|
54
|
+
unique: bool = False,
|
|
55
|
+
related_name: Optional[str] = None,
|
|
56
|
+
db_column: Optional[str] = None,
|
|
57
|
+
default: Any = ..., # noqa: ANN401
|
|
58
|
+
**kwargs: Any, # noqa: ANN401
|
|
59
|
+
) -> Any: # noqa: ANN401
|
|
60
|
+
"""Create a foreign key field.
|
|
61
|
+
|
|
62
|
+
This function creates a Pydantic Field with foreign key metadata stored
|
|
63
|
+
in json_schema_extra. In Phase 1, this provides FK constraint support.
|
|
64
|
+
Phase 2 will add descriptor support for `book.author` style access.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
to: The target model class that this foreign key references.
|
|
68
|
+
on_delete: Action when referenced record is deleted.
|
|
69
|
+
- CASCADE: Delete this record too.
|
|
70
|
+
- SET NULL: Set this field to NULL (requires null=True).
|
|
71
|
+
- RESTRICT: Prevent deletion if references exist (default).
|
|
72
|
+
- NO ACTION: Similar to RESTRICT in SQLite.
|
|
73
|
+
on_update: Action when referenced record's PK is updated.
|
|
74
|
+
- CASCADE: Update this field to the new PK value.
|
|
75
|
+
- SET NULL: Set this field to NULL (requires null=True).
|
|
76
|
+
- RESTRICT: Prevent update if references exist (default).
|
|
77
|
+
- NO ACTION: Similar to RESTRICT in SQLite.
|
|
78
|
+
null: Whether the foreign key field can be NULL. Default is False.
|
|
79
|
+
unique: Whether the field must be unique (creates one-to-one
|
|
80
|
+
relationship). Default is False.
|
|
81
|
+
related_name: Optional name for the reverse relationship. Reserved
|
|
82
|
+
for Phase 2 implementation.
|
|
83
|
+
db_column: Optional custom column name. If not specified, defaults
|
|
84
|
+
to `{field_name}_id`.
|
|
85
|
+
default: Default value for the field. If null=True and no default
|
|
86
|
+
is provided, defaults to None.
|
|
87
|
+
**kwargs: Additional keyword arguments passed to Pydantic Field.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A Pydantic Field with foreign key metadata.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
InvalidForeignKeyError: If SET NULL action is used without null=True.
|
|
94
|
+
|
|
95
|
+
Example:
|
|
96
|
+
>>> class Book(BaseDBModel):
|
|
97
|
+
... title: str
|
|
98
|
+
... author_id: int = ForeignKey(Author, on_delete="CASCADE")
|
|
99
|
+
"""
|
|
100
|
+
# Validate SET NULL requires null=True
|
|
101
|
+
if on_delete == "SET NULL" and not null:
|
|
102
|
+
msg = "on_delete='SET NULL' requires null=True"
|
|
103
|
+
raise InvalidForeignKeyError(msg)
|
|
104
|
+
if on_update == "SET NULL" and not null:
|
|
105
|
+
msg = "on_update='SET NULL' requires null=True"
|
|
106
|
+
raise InvalidForeignKeyError(msg)
|
|
107
|
+
|
|
108
|
+
# Handle existing json_schema_extra
|
|
109
|
+
existing_extra = kwargs.pop("json_schema_extra", {})
|
|
110
|
+
if not isinstance(existing_extra, dict):
|
|
111
|
+
existing_extra = {}
|
|
112
|
+
|
|
113
|
+
# Create ForeignKeyInfo metadata
|
|
114
|
+
fk_info = ForeignKeyInfo(
|
|
115
|
+
to_model=to,
|
|
116
|
+
on_delete=on_delete,
|
|
117
|
+
on_update=on_update,
|
|
118
|
+
null=null,
|
|
119
|
+
unique=unique,
|
|
120
|
+
related_name=related_name,
|
|
121
|
+
db_column=db_column,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Store FK metadata in json_schema_extra
|
|
125
|
+
existing_extra["foreign_key"] = fk_info
|
|
126
|
+
if unique:
|
|
127
|
+
existing_extra["unique"] = True
|
|
128
|
+
|
|
129
|
+
# Set default value
|
|
130
|
+
if default is ... and "default_factory" not in kwargs:
|
|
131
|
+
default = None if null else ...
|
|
132
|
+
|
|
133
|
+
return Field(default=default, json_schema_extra=existing_extra, **kwargs)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def get_foreign_key_info(field_info: FieldInfo) -> Optional[ForeignKeyInfo]:
|
|
137
|
+
"""Extract ForeignKeyInfo from a field if it's a foreign key.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
field_info: The Pydantic FieldInfo to examine.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
The ForeignKeyInfo if the field is a foreign key, None otherwise.
|
|
144
|
+
"""
|
|
145
|
+
if not hasattr(field_info, "json_schema_extra"):
|
|
146
|
+
return None
|
|
147
|
+
extra = field_info.json_schema_extra
|
|
148
|
+
if not isinstance(extra, dict):
|
|
149
|
+
return None
|
|
150
|
+
fk_info = extra.get("foreign_key")
|
|
151
|
+
if isinstance(fk_info, ForeignKeyInfo):
|
|
152
|
+
return fk_info
|
|
153
|
+
return None
|