iceaxe 0.7.0.dev1__cp312-cp312-macosx_11_0_arm64.whl → 0.7.0.dev2__cp312-cp312-macosx_11_0_arm64.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.
Potentially problematic release.
This version of iceaxe might be problematic. Click here for more details.
- iceaxe/__tests__/schemas/test_db_memory_serializer.py +61 -0
- iceaxe/__tests__/schemas/test_db_stubs.py +165 -1
- iceaxe/base.py +1 -1
- iceaxe/schemas/actions.py +2 -2
- iceaxe/schemas/db_memory_serializer.py +85 -3
- iceaxe/schemas/db_stubs.py +113 -0
- iceaxe/session.py +1 -1
- {iceaxe-0.7.0.dev1.dist-info → iceaxe-0.7.0.dev2.dist-info}/METADATA +2 -3
- {iceaxe-0.7.0.dev1.dist-info → iceaxe-0.7.0.dev2.dist-info}/RECORD +12 -12
- {iceaxe-0.7.0.dev1.dist-info → iceaxe-0.7.0.dev2.dist-info}/WHEEL +0 -0
- {iceaxe-0.7.0.dev1.dist-info → iceaxe-0.7.0.dev2.dist-info}/licenses/LICENSE +0 -0
- {iceaxe-0.7.0.dev1.dist-info → iceaxe-0.7.0.dev2.dist-info}/top_level.txt +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from datetime import date, datetime, time, timedelta
|
|
2
3
|
from enum import Enum, IntEnum, StrEnum
|
|
3
4
|
from typing import Generic, Sequence, TypeVar
|
|
@@ -20,6 +21,7 @@ from iceaxe.schemas.actions import (
|
|
|
20
21
|
DryRunComment,
|
|
21
22
|
)
|
|
22
23
|
from iceaxe.schemas.db_memory_serializer import (
|
|
24
|
+
CompositePrimaryKeyConstraintError,
|
|
23
25
|
DatabaseHandler,
|
|
24
26
|
DatabaseMemorySerializer,
|
|
25
27
|
)
|
|
@@ -1462,3 +1464,62 @@ def test_foreign_key_actions():
|
|
|
1462
1464
|
assert fk_constraint.foreign_key_constraint.target_columns == frozenset({"id"})
|
|
1463
1465
|
assert fk_constraint.foreign_key_constraint.on_delete == "CASCADE"
|
|
1464
1466
|
assert fk_constraint.foreign_key_constraint.on_update == "CASCADE"
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
def test_multiple_primary_keys_foreign_key_error():
|
|
1470
|
+
"""
|
|
1471
|
+
Test that when a model has multiple primary keys and foreign key constraints,
|
|
1472
|
+
we get a helpful error message explaining the issue.
|
|
1473
|
+
"""
|
|
1474
|
+
|
|
1475
|
+
class User(TableBase):
|
|
1476
|
+
id: int = Field(primary_key=True)
|
|
1477
|
+
tenant_id: int = Field(primary_key=True) # Composite primary key
|
|
1478
|
+
name: str
|
|
1479
|
+
|
|
1480
|
+
class Topic(TableBase):
|
|
1481
|
+
id: str = Field(primary_key=True)
|
|
1482
|
+
tenant_id: int = Field(primary_key=True) # Composite primary key
|
|
1483
|
+
title: str
|
|
1484
|
+
|
|
1485
|
+
class Rec(TableBase):
|
|
1486
|
+
id: int = Field(primary_key=True, default=None)
|
|
1487
|
+
creator_id: int = Field(
|
|
1488
|
+
foreign_key="user.id"
|
|
1489
|
+
) # This will fail because user is leveraging our synthetic primary key
|
|
1490
|
+
topic_id: str = Field(
|
|
1491
|
+
foreign_key="topic.id"
|
|
1492
|
+
) # This will fail because topic is leveraging our synthetic primary key
|
|
1493
|
+
|
|
1494
|
+
migrator = DatabaseMemorySerializer()
|
|
1495
|
+
|
|
1496
|
+
with pytest.raises(CompositePrimaryKeyConstraintError) as exc_info:
|
|
1497
|
+
db_objects = list(migrator.delegate([User, Topic, Rec]))
|
|
1498
|
+
migrator.order_db_objects(db_objects)
|
|
1499
|
+
|
|
1500
|
+
# Check that the exception has the expected attributes
|
|
1501
|
+
assert exc_info.value.missing_constraints == [("user", "id")]
|
|
1502
|
+
|
|
1503
|
+
|
|
1504
|
+
def test_multiple_primary_keys_warning():
|
|
1505
|
+
"""
|
|
1506
|
+
Test that when a model has multiple primary keys, we get a warning.
|
|
1507
|
+
"""
|
|
1508
|
+
|
|
1509
|
+
class ExampleModel(TableBase):
|
|
1510
|
+
value_a: int = Field(primary_key=True)
|
|
1511
|
+
value_b: int = Field(primary_key=True)
|
|
1512
|
+
|
|
1513
|
+
migrator = DatabaseMemorySerializer()
|
|
1514
|
+
|
|
1515
|
+
with warnings.catch_warnings(record=True) as w:
|
|
1516
|
+
warnings.simplefilter("always")
|
|
1517
|
+
list(migrator.delegate([ExampleModel]))
|
|
1518
|
+
|
|
1519
|
+
# Check that a warning was issued
|
|
1520
|
+
assert len(w) == 1
|
|
1521
|
+
assert issubclass(w[0].category, UserWarning)
|
|
1522
|
+
warning_message = str(w[0].message)
|
|
1523
|
+
assert "multiple fields marked as primary_key=True" in warning_message
|
|
1524
|
+
assert "composite primary key constraint" in warning_message
|
|
1525
|
+
assert "Consider using only one primary key field" in warning_message
|
|
@@ -1,4 +1,168 @@
|
|
|
1
|
-
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from iceaxe.schemas.db_stubs import ConstraintPointerInfo, DBObjectPointer, DBType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MockDBObjectPointer(DBObjectPointer):
|
|
7
|
+
"""Mock implementation of DBObjectPointer for testing parser methods."""
|
|
8
|
+
|
|
9
|
+
representation_str: str
|
|
10
|
+
|
|
11
|
+
def representation(self) -> str:
|
|
12
|
+
return self.representation_str
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.mark.parametrize(
|
|
16
|
+
"representation_str,expected_result",
|
|
17
|
+
[
|
|
18
|
+
# Valid constraint pointer formats
|
|
19
|
+
(
|
|
20
|
+
"users.['id'].PRIMARY KEY",
|
|
21
|
+
ConstraintPointerInfo("users", ["id"], "PRIMARY KEY"),
|
|
22
|
+
),
|
|
23
|
+
(
|
|
24
|
+
"orders.['user_id', 'product_id'].UNIQUE",
|
|
25
|
+
ConstraintPointerInfo("orders", ["user_id", "product_id"], "UNIQUE"),
|
|
26
|
+
),
|
|
27
|
+
(
|
|
28
|
+
"products.['name'].INDEX",
|
|
29
|
+
ConstraintPointerInfo("products", ["name"], "INDEX"),
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
"table_name.['col1', 'col2', 'col3'].FOREIGN KEY",
|
|
33
|
+
ConstraintPointerInfo(
|
|
34
|
+
"table_name", ["col1", "col2", "col3"], "FOREIGN KEY"
|
|
35
|
+
),
|
|
36
|
+
),
|
|
37
|
+
# Single quotes
|
|
38
|
+
("users.['email'].UNIQUE", ConstraintPointerInfo("users", ["email"], "UNIQUE")),
|
|
39
|
+
# Double quotes
|
|
40
|
+
('users.["email"].UNIQUE', ConstraintPointerInfo("users", ["email"], "UNIQUE")),
|
|
41
|
+
# Mixed quotes
|
|
42
|
+
(
|
|
43
|
+
"users.[\"col1\", 'col2'].UNIQUE",
|
|
44
|
+
ConstraintPointerInfo("users", ["col1", "col2"], "UNIQUE"),
|
|
45
|
+
),
|
|
46
|
+
# Extra whitespace
|
|
47
|
+
(
|
|
48
|
+
"users.[ 'col1' , 'col2' ].UNIQUE",
|
|
49
|
+
ConstraintPointerInfo("users", ["col1", "col2"], "UNIQUE"),
|
|
50
|
+
),
|
|
51
|
+
# Empty column list
|
|
52
|
+
("users.[].CHECK", ConstraintPointerInfo("users", [], "CHECK")),
|
|
53
|
+
# Schema-qualified table names (dots in table names are valid when representing schema.table)
|
|
54
|
+
(
|
|
55
|
+
"public.users.['column'].PRIMARY KEY",
|
|
56
|
+
ConstraintPointerInfo("public.users", ["column"], "PRIMARY KEY"),
|
|
57
|
+
),
|
|
58
|
+
# Complex constraint types
|
|
59
|
+
(
|
|
60
|
+
"users.['id'].PRIMARY KEY AUTOINCREMENT",
|
|
61
|
+
ConstraintPointerInfo("users", ["id"], "PRIMARY KEY AUTOINCREMENT"),
|
|
62
|
+
),
|
|
63
|
+
# Table names with underscores and numbers (valid PostgreSQL identifiers)
|
|
64
|
+
(
|
|
65
|
+
"user_table_2.['id'].PRIMARY KEY",
|
|
66
|
+
ConstraintPointerInfo("user_table_2", ["id"], "PRIMARY KEY"),
|
|
67
|
+
),
|
|
68
|
+
# Column names with underscores and numbers
|
|
69
|
+
(
|
|
70
|
+
"users.['user_id_2', 'created_at'].UNIQUE",
|
|
71
|
+
ConstraintPointerInfo("users", ["user_id_2", "created_at"], "UNIQUE"),
|
|
72
|
+
),
|
|
73
|
+
# Invalid formats that should return None
|
|
74
|
+
("users.column.UNIQUE", None), # Missing brackets
|
|
75
|
+
("users.['column']", None), # Missing constraint type
|
|
76
|
+
("['column'].UNIQUE", None), # Missing table name
|
|
77
|
+
("users", None), # Just table name
|
|
78
|
+
("", None), # Empty string
|
|
79
|
+
("users.column", None), # Simple table.column format
|
|
80
|
+
("invalid_format", None), # Random string
|
|
81
|
+
# Malformed bracket syntax
|
|
82
|
+
("users.[column].UNIQUE", None), # Missing quotes in brackets
|
|
83
|
+
("users.['column.UNIQUE", None), # Unclosed bracket
|
|
84
|
+
("users.column'].UNIQUE", None), # Missing opening bracket
|
|
85
|
+
],
|
|
86
|
+
)
|
|
87
|
+
def test_parse_constraint_pointer(
|
|
88
|
+
representation_str: str, expected_result: ConstraintPointerInfo | None
|
|
89
|
+
):
|
|
90
|
+
"""Test parsing of constraint pointer representations."""
|
|
91
|
+
pointer = MockDBObjectPointer(representation_str=representation_str)
|
|
92
|
+
result = pointer.parse_constraint_pointer()
|
|
93
|
+
|
|
94
|
+
if expected_result is None:
|
|
95
|
+
assert result is None
|
|
96
|
+
else:
|
|
97
|
+
assert result is not None
|
|
98
|
+
assert result.table_name == expected_result.table_name
|
|
99
|
+
assert result.column_names == expected_result.column_names
|
|
100
|
+
assert result.constraint_type == expected_result.constraint_type
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.parametrize(
|
|
104
|
+
"representation_str,expected_table_name",
|
|
105
|
+
[
|
|
106
|
+
# Constraint pointer formats
|
|
107
|
+
("users.['id'].PRIMARY KEY", "users"),
|
|
108
|
+
("orders.['user_id', 'product_id'].UNIQUE", "orders"),
|
|
109
|
+
("public.users.['column'].INDEX", "public.users"),
|
|
110
|
+
# Simple table.column formats
|
|
111
|
+
("users.email", "users"),
|
|
112
|
+
("products.name", "products"),
|
|
113
|
+
("public.users.column", "public.users"), # Schema.table.column format
|
|
114
|
+
# Edge cases
|
|
115
|
+
("table_only", "table_only"),
|
|
116
|
+
("", None), # Empty string should return None
|
|
117
|
+
("users.['id'].PRIMARY KEY", "users"), # Constraint format takes precedence
|
|
118
|
+
# Complex table names with underscores and numbers
|
|
119
|
+
("user_table_123.column", "user_table_123"),
|
|
120
|
+
("schema_1.table_2.column", "schema_1.table_2"),
|
|
121
|
+
# Multiple dots in representation (should extract the table part correctly)
|
|
122
|
+
("very.long.schema.table.['col'].UNIQUE", "very.long.schema.table"),
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
def test_get_table_name(representation_str: str, expected_table_name: str | None):
|
|
126
|
+
"""Test extraction of table names from pointer representations."""
|
|
127
|
+
pointer = MockDBObjectPointer(representation_str=representation_str)
|
|
128
|
+
result = pointer.get_table_name()
|
|
129
|
+
assert result == expected_table_name
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@pytest.mark.parametrize(
|
|
133
|
+
"representation_str,expected_column_names",
|
|
134
|
+
[
|
|
135
|
+
# Constraint pointer formats
|
|
136
|
+
("users.['id'].PRIMARY KEY", ["id"]),
|
|
137
|
+
("orders.['user_id', 'product_id'].UNIQUE", ["user_id", "product_id"]),
|
|
138
|
+
("products.['name', 'category', 'price'].INDEX", ["name", "category", "price"]),
|
|
139
|
+
("users.[].CHECK", []), # Empty column list
|
|
140
|
+
# Simple table.column formats
|
|
141
|
+
("users.email", ["email"]),
|
|
142
|
+
("products.name", ["name"]),
|
|
143
|
+
("public.users.column", ["column"]), # Schema.table.column format
|
|
144
|
+
# Edge cases
|
|
145
|
+
("table_only", []), # No columns
|
|
146
|
+
("", []), # Empty string
|
|
147
|
+
# Whitespace handling
|
|
148
|
+
("users.[ 'col1' , 'col2' ].UNIQUE", ["col1", "col2"]),
|
|
149
|
+
# Quote handling
|
|
150
|
+
("users.[\"col1\", 'col2'].UNIQUE", ["col1", "col2"]),
|
|
151
|
+
# Column names with underscores and numbers
|
|
152
|
+
(
|
|
153
|
+
"users.['user_id_2', 'created_at_timestamp'].UNIQUE",
|
|
154
|
+
["user_id_2", "created_at_timestamp"],
|
|
155
|
+
),
|
|
156
|
+
# Complex schema.table.column cases
|
|
157
|
+
("schema.table.column_name", ["column_name"]),
|
|
158
|
+
("very.long.schema.table.column", ["column"]),
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
def test_get_column_names(representation_str: str, expected_column_names: list[str]):
|
|
162
|
+
"""Test extraction of column names from pointer representations."""
|
|
163
|
+
pointer = MockDBObjectPointer(representation_str=representation_str)
|
|
164
|
+
result = pointer.get_column_names()
|
|
165
|
+
assert result == expected_column_names
|
|
2
166
|
|
|
3
167
|
|
|
4
168
|
def test_merge_type_columns():
|
iceaxe/base.py
CHANGED
|
@@ -273,7 +273,7 @@ class TableBase(BaseModel, metaclass=DBModelMetaclass):
|
|
|
273
273
|
:param name: Attribute name
|
|
274
274
|
:param value: New value
|
|
275
275
|
"""
|
|
276
|
-
if name in self.model_fields:
|
|
276
|
+
if name in self.__class__.model_fields:
|
|
277
277
|
self.modified_attrs[name] = value
|
|
278
278
|
for callback in self.modified_attrs_callbacks:
|
|
279
279
|
callback(self)
|
iceaxe/schemas/actions.py
CHANGED
|
@@ -299,7 +299,7 @@ class DatabaseActions:
|
|
|
299
299
|
- Scalar to array types (INTEGER → INTEGER[])
|
|
300
300
|
- Custom enum conversions (VARCHAR/TEXT → custom enum)
|
|
301
301
|
- Compatible numeric conversions (INTEGER → BIGINT)
|
|
302
|
-
|
|
302
|
+
|
|
303
303
|
When autocast=False, PostgreSQL will only allow the type change if it's
|
|
304
304
|
compatible without explicit casting, which may fail for many conversions.
|
|
305
305
|
|
|
@@ -308,7 +308,7 @@ class DatabaseActions:
|
|
|
308
308
|
await actor.modify_column_type(
|
|
309
309
|
"products", "price", ColumnType.INTEGER, autocast=True
|
|
310
310
|
)
|
|
311
|
-
|
|
311
|
+
|
|
312
312
|
# Manual migration with custom control
|
|
313
313
|
await actor.modify_column_type(
|
|
314
314
|
"products", "price", ColumnType.INTEGER, autocast=False
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from datetime import date, datetime, time, timedelta
|
|
3
4
|
from inspect import isgenerator
|
|
@@ -54,6 +55,47 @@ from iceaxe.typing import (
|
|
|
54
55
|
NodeYieldType = Union[DBObject, DBObjectPointer, "NodeDefinition"]
|
|
55
56
|
|
|
56
57
|
|
|
58
|
+
class CompositePrimaryKeyConstraintError(ValueError):
|
|
59
|
+
"""
|
|
60
|
+
Raised when foreign key constraints cannot be resolved due to composite primary keys.
|
|
61
|
+
|
|
62
|
+
This occurs when a table has multiple fields marked as primary_key=True, creating
|
|
63
|
+
a composite primary key constraint, but foreign key constraints expect individual
|
|
64
|
+
primary key constraints on the target columns.
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, missing_constraints: list[tuple[str, str]], base_message: str):
|
|
69
|
+
self.missing_constraints = missing_constraints
|
|
70
|
+
self.base_message = base_message
|
|
71
|
+
|
|
72
|
+
# Construct the detailed error message
|
|
73
|
+
error_msg = base_message
|
|
74
|
+
|
|
75
|
+
if missing_constraints:
|
|
76
|
+
error_msg += "\n\nThis error commonly occurs when you have multiple fields marked as primary_key=True in your model."
|
|
77
|
+
error_msg += "\nIceaxe creates a single composite primary key constraint, but foreign key constraints"
|
|
78
|
+
error_msg += (
|
|
79
|
+
"\nexpect individual primary key constraints on the target columns."
|
|
80
|
+
)
|
|
81
|
+
error_msg += "\n\nFor a detailed explanation of why this happens and how to fix it, see:"
|
|
82
|
+
error_msg += "\nhttps://mountaineer.sh/iceaxe/guides/relationships#composite-primary-keys-and-foreign-key-constraints"
|
|
83
|
+
error_msg += "\n\nTo fix this issue, choose one of these approaches:"
|
|
84
|
+
error_msg += "\n\nRecommended: Modify the current table"
|
|
85
|
+
error_msg += (
|
|
86
|
+
"\n - Keep only one field as primary_key=True (e.g., just 'id')"
|
|
87
|
+
)
|
|
88
|
+
error_msg += "\n - Add a UniqueConstraint if you need uniqueness across multiple fields"
|
|
89
|
+
error_msg += "\n - This is usually the better design pattern"
|
|
90
|
+
|
|
91
|
+
# Show specific table/column combinations that are missing
|
|
92
|
+
error_msg += "\n\nCurrently missing individual primary key constraints:"
|
|
93
|
+
for table_name, column_name in missing_constraints:
|
|
94
|
+
error_msg += f"\n - Table '{table_name}' needs a primary key on column '{column_name}'"
|
|
95
|
+
|
|
96
|
+
super().__init__(error_msg)
|
|
97
|
+
|
|
98
|
+
|
|
57
99
|
@dataclass
|
|
58
100
|
class NodeDefinition:
|
|
59
101
|
node: DBObject
|
|
@@ -125,9 +167,37 @@ class DatabaseMemorySerializer:
|
|
|
125
167
|
pointer.representation() in db_objects_by_name
|
|
126
168
|
for pointer in dep.pointers
|
|
127
169
|
):
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
170
|
+
# Create a more helpful error message for common cases
|
|
171
|
+
missing_pointers = [
|
|
172
|
+
p.representation() for p in dep.pointers
|
|
173
|
+
]
|
|
174
|
+
error_msg = f"None of the OR pointers {missing_pointers} found in the defined database objects"
|
|
175
|
+
|
|
176
|
+
# Check if this is the common case of multiple primary keys causing foreign key issues
|
|
177
|
+
primary_key_pointers = []
|
|
178
|
+
for p in dep.pointers:
|
|
179
|
+
parsed = p.parse_constraint_pointer()
|
|
180
|
+
if parsed and parsed.constraint_type == "PRIMARY KEY":
|
|
181
|
+
primary_key_pointers.append(p)
|
|
182
|
+
|
|
183
|
+
if primary_key_pointers:
|
|
184
|
+
# Extract table and column info from the primary key pointers
|
|
185
|
+
primary_key_info: list[tuple[str, str]] = []
|
|
186
|
+
for pointer in primary_key_pointers:
|
|
187
|
+
table_name = pointer.get_table_name()
|
|
188
|
+
column_names = pointer.get_column_names()
|
|
189
|
+
|
|
190
|
+
if table_name and column_names:
|
|
191
|
+
for column_name in column_names:
|
|
192
|
+
primary_key_info.append(
|
|
193
|
+
(table_name, column_name)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if primary_key_info:
|
|
197
|
+
raise CompositePrimaryKeyConstraintError(
|
|
198
|
+
primary_key_info, error_msg
|
|
199
|
+
)
|
|
200
|
+
raise ValueError(error_msg)
|
|
131
201
|
elif dep.representation() not in db_objects_by_name:
|
|
132
202
|
raise ValueError(
|
|
133
203
|
f"Pointer {dep.representation()} not found in the defined database objects"
|
|
@@ -544,6 +614,18 @@ class DatabaseHandler:
|
|
|
544
614
|
if not keys:
|
|
545
615
|
return
|
|
546
616
|
|
|
617
|
+
# Warn users about potential issues with multiple primary keys
|
|
618
|
+
if len(keys) > 1:
|
|
619
|
+
column_names = [key for key, _ in keys]
|
|
620
|
+
warnings.warn(
|
|
621
|
+
f"Table '{table.get_table_name()}' has multiple fields marked as primary_key=True: {column_names}. "
|
|
622
|
+
f"This creates a composite primary key constraint, which may cause issues with foreign key "
|
|
623
|
+
f"constraints that expect individual primary keys on target columns. "
|
|
624
|
+
f"Consider using only one primary key field and adding UniqueConstraint for uniqueness instead.",
|
|
625
|
+
UserWarning,
|
|
626
|
+
stacklevel=3,
|
|
627
|
+
)
|
|
628
|
+
|
|
547
629
|
columns = [key for key, _ in keys]
|
|
548
630
|
yield from self._yield_nodes(
|
|
549
631
|
DBConstraint(
|
iceaxe/schemas/db_stubs.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from abc import abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
2
4
|
from typing import Self, Union
|
|
3
5
|
|
|
4
6
|
from pydantic import BaseModel, Field, model_validator
|
|
@@ -12,6 +14,15 @@ from iceaxe.schemas.actions import (
|
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
@dataclass
|
|
18
|
+
class ConstraintPointerInfo:
|
|
19
|
+
"""Information parsed from a constraint pointer representation."""
|
|
20
|
+
|
|
21
|
+
table_name: str
|
|
22
|
+
column_names: list[str]
|
|
23
|
+
constraint_type: str
|
|
24
|
+
|
|
25
|
+
|
|
15
26
|
class DBObject(BaseModel):
|
|
16
27
|
"""
|
|
17
28
|
A subclass for all models that are intended to store
|
|
@@ -86,6 +97,108 @@ class DBObjectPointer(BaseModel):
|
|
|
86
97
|
def representation(self) -> str:
|
|
87
98
|
pass
|
|
88
99
|
|
|
100
|
+
def parse_constraint_pointer(self) -> ConstraintPointerInfo | None:
|
|
101
|
+
"""
|
|
102
|
+
Parse a constraint pointer representation into its components.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
ConstraintPointerInfo | None: Parsed constraint information or None if not a constraint pointer
|
|
106
|
+
|
|
107
|
+
Examples:
|
|
108
|
+
"table.['column'].PRIMARY KEY" -> ConstraintPointerInfo("table", ["column"], "PRIMARY KEY")
|
|
109
|
+
"table.['col1', 'col2'].UNIQUE" -> ConstraintPointerInfo("table", ["col1", "col2"], "UNIQUE")
|
|
110
|
+
"""
|
|
111
|
+
representation = self.representation()
|
|
112
|
+
|
|
113
|
+
# Pattern to match: table_name.[column_list].constraint_type
|
|
114
|
+
# where column_list can be ['col'] or ['col1', 'col2', ...]
|
|
115
|
+
# The table_name can contain dots (for schema.table), so we need to be more careful
|
|
116
|
+
# We look for the pattern .[...]. to identify where the column list starts
|
|
117
|
+
pattern = r"^(.+)\.(\[.*?\])\.(.+)$"
|
|
118
|
+
match = re.match(pattern, representation)
|
|
119
|
+
|
|
120
|
+
if not match:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
table_name, columns_part, constraint_type = match.groups()
|
|
124
|
+
|
|
125
|
+
# Validate that the column list contains properly quoted column names or is empty
|
|
126
|
+
# Remove brackets and check the content
|
|
127
|
+
columns_str = columns_part.strip("[]")
|
|
128
|
+
if not columns_str:
|
|
129
|
+
# Empty column list is valid
|
|
130
|
+
return ConstraintPointerInfo(table_name, [], constraint_type)
|
|
131
|
+
|
|
132
|
+
# Split by comma and validate each column name is properly quoted
|
|
133
|
+
columns = []
|
|
134
|
+
for col in columns_str.split(","):
|
|
135
|
+
col = col.strip()
|
|
136
|
+
# Check if the column is properly quoted (single or double quotes)
|
|
137
|
+
if (col.startswith("'") and col.endswith("'")) or (
|
|
138
|
+
col.startswith('"') and col.endswith('"')
|
|
139
|
+
):
|
|
140
|
+
# Remove quotes and add to list
|
|
141
|
+
col_name = col[1:-1]
|
|
142
|
+
if col_name: # Don't add empty column names
|
|
143
|
+
columns.append(col_name)
|
|
144
|
+
else:
|
|
145
|
+
# Column is not properly quoted, this is not a valid constraint pointer
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
return ConstraintPointerInfo(table_name, columns, constraint_type)
|
|
149
|
+
|
|
150
|
+
def get_table_name(self) -> str | None:
|
|
151
|
+
"""
|
|
152
|
+
Extract the table name from the pointer representation.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
str | None: The table name if it can be parsed, None otherwise
|
|
156
|
+
"""
|
|
157
|
+
# Try constraint pointer format first
|
|
158
|
+
parsed = self.parse_constraint_pointer()
|
|
159
|
+
if parsed is not None:
|
|
160
|
+
return parsed.table_name
|
|
161
|
+
|
|
162
|
+
# Try simple table.column format
|
|
163
|
+
representation = self.representation()
|
|
164
|
+
if not representation:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
parts = representation.split(".")
|
|
168
|
+
if len(parts) >= 2:
|
|
169
|
+
# For schema.table.column format, take all parts except the last one
|
|
170
|
+
return ".".join(parts[:-1])
|
|
171
|
+
elif len(parts) == 1:
|
|
172
|
+
# Just a table name
|
|
173
|
+
return parts[0]
|
|
174
|
+
else:
|
|
175
|
+
return None
|
|
176
|
+
|
|
177
|
+
def get_column_names(self) -> list[str]:
|
|
178
|
+
"""
|
|
179
|
+
Extract column names from the pointer representation.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
list[str]: List of column names if they can be parsed, empty list otherwise
|
|
183
|
+
"""
|
|
184
|
+
# Try constraint pointer format first
|
|
185
|
+
parsed = self.parse_constraint_pointer()
|
|
186
|
+
if parsed is not None:
|
|
187
|
+
return parsed.column_names
|
|
188
|
+
|
|
189
|
+
# Try simple table.column format
|
|
190
|
+
representation = self.representation()
|
|
191
|
+
if not representation:
|
|
192
|
+
return []
|
|
193
|
+
|
|
194
|
+
parts = representation.split(".")
|
|
195
|
+
if len(parts) >= 2:
|
|
196
|
+
# For schema.table.column format, take the last part as the column name
|
|
197
|
+
return [parts[-1]]
|
|
198
|
+
else:
|
|
199
|
+
# Just a table name, no columns
|
|
200
|
+
return []
|
|
201
|
+
|
|
89
202
|
|
|
90
203
|
class DBTable(DBObject):
|
|
91
204
|
table_name: str
|
iceaxe/session.py
CHANGED
|
@@ -593,7 +593,7 @@ class DBConnection:
|
|
|
593
593
|
modified_attrs = frozenset(
|
|
594
594
|
k
|
|
595
595
|
for k, v in obj.get_modified_attributes().items()
|
|
596
|
-
if not obj.model_fields[k].exclude
|
|
596
|
+
if not obj.__class__.model_fields[k].exclude
|
|
597
597
|
)
|
|
598
598
|
if modified_attrs:
|
|
599
599
|
updates_by_fields[modified_attrs].append(obj)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iceaxe
|
|
3
|
-
Version: 0.7.0.
|
|
3
|
+
Version: 0.7.0.dev2
|
|
4
4
|
Summary: A modern, fast ORM for Python.
|
|
5
5
|
Author-email: Pierce Freeman <pierce@freeman.vc>
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -15,8 +15,7 @@ Dynamic: license-file
|
|
|
15
15
|
|
|
16
16
|

|
|
17
17
|
|
|
18
|
-
](https://github.com/piercefreeman/iceaxe/actions)
|
|
18
|
+
 [](https://github.com/piercefreeman/iceaxe/actions)
|
|
20
19
|
|
|
21
20
|
A modern, fast ORM for Python. We have the following goals:
|
|
22
21
|
|
|
@@ -5,7 +5,7 @@ iceaxe/sql_types.py,sha256=fEdDPeb7QctFfdWEn1rLqY1b2EccM61UEAaqXdyz3jo,2607
|
|
|
5
5
|
iceaxe/alias_values.py,sha256=gvtaYJLdMkSUBS99GlFGDMul4jlOs_lxnmgRae7OrKI,1978
|
|
6
6
|
iceaxe/session_optimized.pyx,sha256=5sPvWKuEZKzwaOFLE4knU8kRVWt0JCVTugfQCTpRPmo,8375
|
|
7
7
|
iceaxe/io.py,sha256=af-ZJKiP0yOtLCe2dG0jMOBffbAvZ1BMSHG6lVjQ1Bc,3889
|
|
8
|
-
iceaxe/session.py,sha256=
|
|
8
|
+
iceaxe/session.py,sha256=0EFbCHA2XBiMS7IT6Cb0-HLE0Q-t2nsWXbe9DdvKyQQ,32616
|
|
9
9
|
iceaxe/__init__.py,sha256=zIq6C5rZY3ERBeOfZBIInM78RvR35ub5oKVw_jj1VO0,614
|
|
10
10
|
iceaxe/modifications.py,sha256=xTs3pdOpeVm_lwKL0DkHVm9a3Kn-HIJtUTTNEwpcUrs,6588
|
|
11
11
|
iceaxe/comparison.py,sha256=fpXZHQrVirTiSox6vpYvTnWJjGJyuZGoEu-98YmuK2g,17005
|
|
@@ -16,7 +16,7 @@ iceaxe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
16
16
|
iceaxe/queries.py,sha256=ZPiJYG_hyRWSUjaC7-gfscr2h4GbGaNT67uyjaScr4A,44461
|
|
17
17
|
iceaxe/typing.py,sha256=s1rCCE2Pcs5Fxt1zY76UppTc9LiXyydzksTm_k2BUmw,1917
|
|
18
18
|
iceaxe/generics.py,sha256=TGI4EIshzwwRoVi1eoux0Dn64xcWvbErSyxd_wK-iHc,4891
|
|
19
|
-
iceaxe/base.py,sha256=
|
|
19
|
+
iceaxe/base.py,sha256=o2c3RIqUnoK1SKB7mkEetcKKdPMtTHBvOawGYCm8usM,11387
|
|
20
20
|
iceaxe/session_optimized.cpython-312-darwin.so,sha256=pm_UfdLXczkxclLYL1B5ATTNOaGBbLe0Xxa6UfQ76Xg,171320
|
|
21
21
|
iceaxe/mountaineer/config.py,sha256=sdzNbrXkLPw1W3RUE2Gsq1m076lvjhw9wGxX1AHEWbw,1404
|
|
22
22
|
iceaxe/mountaineer/__init__.py,sha256=psc2Tj25KHoqmL9kCatZ31Y96ilamERI2Rn0dqQX0zM,298
|
|
@@ -30,12 +30,12 @@ iceaxe/migrations/cli.py,sha256=FzejSzE1n5FOkhtyGXLuRKWOTR4DlP6xTPxdqqzTCC0,8802
|
|
|
30
30
|
iceaxe/migrations/client_io.py,sha256=f-yJJaaB4yfNlyHk2ou9eKrWIiSzB11rIoSlhDXwQLo,2314
|
|
31
31
|
iceaxe/migrations/migrator.py,sha256=Z1-baX2qAvFaAVYC2oFv0VkFHO6XZRYYqKz9wRke8yo,3038
|
|
32
32
|
iceaxe/migrations/migration.py,sha256=HYtFx_AlmUQF1RJFulAFKaM2XLS1NmynufvjUbTJBec,2370
|
|
33
|
-
iceaxe/schemas/actions.py,sha256=
|
|
33
|
+
iceaxe/schemas/actions.py,sha256=70aHXmWkJx2E4emH1na5OET-m8rPIdEvCUkL1mvB3Ps,30046
|
|
34
34
|
iceaxe/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
35
35
|
iceaxe/schemas/db_serializer.py,sha256=SpMxo_f66R3rKl7uEiRIwS5ZoBCe9ETIz4R__KBT5fA,13070
|
|
36
36
|
iceaxe/schemas/cli.py,sha256=G-ySDV1sIvqDLln23Xc6eKlEQw5-ZydVAzFPgGQGeo4,1019
|
|
37
|
-
iceaxe/schemas/db_stubs.py,sha256=
|
|
38
|
-
iceaxe/schemas/db_memory_serializer.py,sha256=
|
|
37
|
+
iceaxe/schemas/db_stubs.py,sha256=B2JdnrChqKUMZwNPAtzVkC2bHQfoN_bFczL-mZZy4_A,17849
|
|
38
|
+
iceaxe/schemas/db_memory_serializer.py,sha256=44jf2eI2YMuQpS3XB1UxIl43u1pv8EOZXbjZ1M-O5GY,29079
|
|
39
39
|
iceaxe/__tests__/conftest.py,sha256=S_leF61hosjmXjYnvammgKckdl8w70LiwHJQzU7i2Us,4010
|
|
40
40
|
iceaxe/__tests__/test_session.py,sha256=ZuUe7B_H-aW4Ym3Wx7WhBDhcyIlE7jMcpeD1jOkYyJs,49199
|
|
41
41
|
iceaxe/__tests__/test_modifications.py,sha256=beljqGGTc-Mg4sogyvUSNCOYp57AjNRi41e-vdMqMOM,5061
|
|
@@ -59,16 +59,16 @@ iceaxe/__tests__/migrations/test_generator.py,sha256=Z9GlZCCWwbysUO-y3S6R76PodY_
|
|
|
59
59
|
iceaxe/__tests__/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
60
60
|
iceaxe/__tests__/migrations/test_generics.py,sha256=JuaniWOsfQasGF-pPx7pyyXEuNO9sfFtQaIfGntqNCQ,2654
|
|
61
61
|
iceaxe/__tests__/schemas/test_actions.py,sha256=-lM3Kz1R0_hJsQNd3A0NGe83cFPuSpcgMS5Z9yIRGmU,38340
|
|
62
|
-
iceaxe/__tests__/schemas/test_db_memory_serializer.py,sha256=
|
|
62
|
+
iceaxe/__tests__/schemas/test_db_memory_serializer.py,sha256=6zAIuY-K4XKzLSHaCUq_UE3sa3h66hPzl3BPlF-nMVE,47103
|
|
63
63
|
iceaxe/__tests__/schemas/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
64
|
-
iceaxe/__tests__/schemas/test_db_stubs.py,sha256
|
|
64
|
+
iceaxe/__tests__/schemas/test_db_stubs.py,sha256=Bh_bPeKvz9bQGM6-03iqs2PXS2Ik_klzO508fHzm-wE,7456
|
|
65
65
|
iceaxe/__tests__/schemas/test_db_serializer.py,sha256=InW5x51f73Zgdw8KDK6G0hUbi64FYzVbu8TmFa0VH8g,10752
|
|
66
66
|
iceaxe/__tests__/schemas/test_cli.py,sha256=mQe0GKGPwwJ3foJCEDy4N-uERfjI_eU1QLDk0AaT588,738
|
|
67
67
|
iceaxe/__tests__/benchmarks/test_bulk_insert.py,sha256=XsMsmGD0y4BQGXKvciPZ_cW1WzYjqW7rKyo2XfgfDjA,1284
|
|
68
68
|
iceaxe/__tests__/benchmarks/test_select.py,sha256=ZyIDvuJA0j4g3kMnoF4eaxO4D-f0BAxznpZMfkETQU0,3984
|
|
69
69
|
iceaxe/__tests__/benchmarks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
70
|
-
iceaxe-0.7.0.
|
|
71
|
-
iceaxe-0.7.0.
|
|
72
|
-
iceaxe-0.7.0.
|
|
73
|
-
iceaxe-0.7.0.
|
|
74
|
-
iceaxe-0.7.0.
|
|
70
|
+
iceaxe-0.7.0.dev2.dist-info/RECORD,,
|
|
71
|
+
iceaxe-0.7.0.dev2.dist-info/WHEEL,sha256=CltXN3lQvXbHxKDtiDwW0RNzF8s2WyBuPbOAX_ZeQlA,109
|
|
72
|
+
iceaxe-0.7.0.dev2.dist-info/top_level.txt,sha256=pTQaTX1areyLx9BHkSfYqSEU0r6dHJMxjxMme0CK9xw,7
|
|
73
|
+
iceaxe-0.7.0.dev2.dist-info/METADATA,sha256=IFs9P6ZHyIdws_g7gHNs28I3DH1d2OrGAfCeqId20KQ,9148
|
|
74
|
+
iceaxe-0.7.0.dev2.dist-info/licenses/LICENSE,sha256=DgU6htH6BJg7h_6609WHBMKEUN_Y04BDN_vTTrmp5Pg,1071
|
|
File without changes
|
|
File without changes
|
|
File without changes
|