Skip to content

Commit eb833ab

Browse files
committed
feat: add relation schema size constraint
1 parent 8c980a4 commit eb833ab

File tree

13 files changed

+224
-201
lines changed

13 files changed

+224
-201
lines changed

src/tgdb/application/common/ports/relations.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66

77

88
@dataclass(frozen=True)
9-
class NotUniqueRelationNumberError(Exception):
10-
relation_number: Number
9+
class NotUniqueRelationNumberError(Exception): ...
1110

1211

1312
@dataclass(frozen=True)
14-
class NoRelationError(Exception):
15-
relation_number: Number
13+
class NoRelationError(Exception): ...
1614

1715

1816
class Relations(ABC):

src/tgdb/application/common/ports/tuples.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
from abc import ABC, abstractmethod
22
from collections.abc import Sequence
3+
from dataclasses import dataclass
34

45
from tgdb.entities.horizon.transaction import TransactionEffect
56
from tgdb.entities.numeration.number import Number
7+
from tgdb.entities.relation.relation import Relation
68
from tgdb.entities.relation.scalar import Scalar
79
from tgdb.entities.relation.tuple import Tuple
810

911

12+
@dataclass(frozen=True)
13+
class OversizedRelationSchemaError(Exception):
14+
schema_size: int
15+
schema_max_size: int
16+
17+
1018
class Tuples(ABC):
1119
@abstractmethod
1220
async def tuples_with_attribute(
@@ -24,3 +32,9 @@ async def map(self, effects: Sequence[TransactionEffect], /) -> None: ...
2432
async def map_idempotently(
2533
self, effects: Sequence[TransactionEffect], /
2634
) -> None: ...
35+
36+
@abstractmethod
37+
async def assert_can_accept_tuples(self, relation: Relation) -> None:
38+
"""
39+
:raises tgdb.application.common.ports.tuples.OversizedRelationSchemaError:
40+
""" # noqa: E501

src/tgdb/application/relation/create_relation.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass
22

33
from tgdb.application.common.ports.relations import Relations
4+
from tgdb.application.common.ports.tuples import Tuples
45
from tgdb.entities.numeration.number import Number
56
from tgdb.entities.relation.relation import Relation
67
from tgdb.entities.relation.schema import Schema
@@ -9,13 +10,17 @@
910
@dataclass(frozen=True)
1011
class CreateRelation:
1112
relations: Relations
13+
tuples: Tuples
1214

1315
async def __call__(
1416
self, relation_number: Number, relation_schema: Schema
1517
) -> None:
1618
"""
19+
:raises tgdb.application.common.ports.tuples.OversizedRelationSchemaError:
1720
:raises tgdb.application.common.ports.relations.NotUniqueRelationNumberError:
1821
""" # noqa: E501
1922

2023
new_relation = Relation.new(relation_number, relation_schema)
24+
await self.tuples.assert_can_accept_tuples(new_relation)
25+
2126
await self.relations.add(new_relation)
Lines changed: 25 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from dataclasses import dataclass
1+
from dataclasses import dataclass, field
22
from datetime import datetime
33
from uuid import UUID
44

@@ -9,84 +9,62 @@
99
class IntDomain:
1010
min: int
1111
max: int
12-
_is_nonable: bool
13-
14-
def type(self) -> type[int]:
15-
return int
16-
17-
def is_nonable(self) -> bool:
18-
return self._is_nonable
12+
is_nonable: bool
1913

2014
def __contains__(self, scalar: Scalar) -> bool:
2115
if scalar is None:
22-
return self._is_nonable
16+
return self.is_nonable
2317

2418
return isinstance(scalar, int) and self.min <= scalar <= self.max
2519

2620

2721
@dataclass(frozen=True)
2822
class StrDomain:
2923
max_len: int
30-
_is_nonable: bool
31-
32-
def type(self) -> type[str]:
33-
return str
34-
35-
def is_nonable(self) -> bool:
36-
return self._is_nonable
24+
is_nonable: bool
3725

3826
def __contains__(self, scalar: Scalar) -> bool:
3927
if scalar is None:
40-
return self._is_nonable
28+
return self.is_nonable
4129

4230
return isinstance(scalar, str) and len(scalar) <= self.max_len
4331

4432

4533
@dataclass(frozen=True)
46-
class SetDomain[T: int | str | datetime | UUID]:
47-
values: tuple[T, ...]
48-
_type: type[T]
49-
_is_nonable: bool
50-
51-
def type(self) -> type[T]:
52-
return self._type
53-
54-
def is_nonable(self) -> bool:
55-
return self._is_nonable
34+
class _TypeDomain:
35+
is_nonable: bool
36+
_type: type = field(init=False)
5637

5738
def __contains__(self, scalar: Scalar) -> bool:
5839
if scalar is None:
59-
return self._is_nonable
40+
return self.is_nonable
6041

61-
return scalar in self.values
42+
return scalar is True or scalar is False
6243

6344

6445
@dataclass(frozen=True)
65-
class TypeDomain[T: bool | datetime | UUID]:
66-
_type: type[T]
67-
_is_nonable: bool
46+
class BoolDomain(_TypeDomain):
47+
_type = bool
6848

69-
def type(self) -> type[T]:
70-
return self._type
7149

72-
def is_nonable(self) -> bool:
73-
return self._is_nonable
50+
@dataclass(frozen=True)
51+
class DatetimeDomain(_TypeDomain):
52+
_type = datetime
53+
54+
55+
@dataclass(frozen=True)
56+
class UuidDomain(_TypeDomain):
57+
_type = UUID
7458

75-
def __contains__(self, scalar: Scalar) -> bool:
76-
if scalar is None:
77-
return self._is_nonable
7859

79-
return isinstance(scalar, self._type)
60+
type SetDomain = tuple[Scalar, ...]
8061

8162

8263
type Domain = (
8364
IntDomain
8465
| StrDomain
85-
| SetDomain[int]
86-
| SetDomain[str]
87-
| SetDomain[datetime]
88-
| SetDomain[UUID]
89-
| TypeDomain[bool]
90-
| TypeDomain[datetime]
91-
| TypeDomain[UUID]
66+
| BoolDomain
67+
| DatetimeDomain
68+
| UuidDomain
69+
| SetDomain
9270
)

src/tgdb/entities/relation/relation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def last_version(self) -> RelationVersion:
7272
else self._initial_version
7373
)
7474

75-
def last_version_id(self) -> RelationSchemaID:
75+
def last_version_schema_id(self) -> RelationSchemaID:
7676
return RelationSchemaID(self._number, self.last_version().number)
7777

7878
def recent_versions(

src/tgdb/infrastructure/adapters/relations.py

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,21 @@ class InMemoryRelations(Relations):
2020
_db: InMemoryDb[Relation]
2121

2222
async def relation(self, relation_number: Number) -> Relation:
23-
"""
24-
:raises tgdb.application.common.ports.relations.NoRelationError:
25-
"""
26-
2723
relation = self._db.select_one(
2824
lambda it: it.number() == relation_number
2925
)
3026
if relation is None:
31-
raise NoRelationError(relation_number)
27+
raise NoRelationError
3228

3329
return relation
3430

3531
async def add(self, relation: Relation) -> None:
36-
"""
37-
:raises tgdb.application.common.ports.relations.NotUniqueRelationNumberError:
38-
""" # noqa: E501
39-
4032
selected_relation = self._db.select_one(
4133
lambda it: it.number() == relation.number()
4234
)
4335

4436
if selected_relation is not None:
45-
raise NotUniqueRelationNumberError(relation.number())
37+
raise NotUniqueRelationNumberError
4638

4739
self._db.insert(relation)
4840

@@ -75,7 +67,7 @@ async def relation(self, relation_number: Number) -> Relation:
7567
)
7668

7769
if relation is None:
78-
raise NoRelationError(relation_number)
70+
raise NoRelationError
7971

8072
return relation
8173

@@ -89,7 +81,7 @@ async def add(self, relation: Relation) -> None:
8981
)
9082

9183
if selected_relation is not None:
92-
raise NotUniqueRelationNumberError(relation.number())
84+
raise NotUniqueRelationNumberError
9385

9486
self._cached_relations.insert(relation)
9587

src/tgdb/infrastructure/adapters/tuples.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@
44

55
from in_memory_db import InMemoryDb
66

7-
from tgdb.application.common.ports.tuples import Tuples
7+
from tgdb.application.common.ports.tuples import (
8+
OversizedRelationSchemaError,
9+
Tuples,
10+
)
811
from tgdb.entities.horizon.transaction import (
912
TransactionEffect,
1013
TransactionScalarEffect,
1114
)
1215
from tgdb.entities.numeration.number import Number
16+
from tgdb.entities.relation.relation import Relation
1317
from tgdb.entities.relation.scalar import Scalar
1418
from tgdb.entities.relation.tuple import Tuple
1519
from tgdb.entities.relation.tuple_effect import (
@@ -18,13 +22,19 @@
1822
MutatedTuple,
1923
NewTuple,
2024
)
21-
from tgdb.infrastructure.telethon.in_telegram_heap import InTelegramHeap
25+
from tgdb.infrastructure.telethon.in_telegram_heap import (
26+
InTelegramHeap,
27+
UnacceptableTupleError,
28+
)
2229

2330

2431
@dataclass(frozen=True, unsafe_hash=False)
2532
class InMemoryTuples(Tuples):
2633
_db: InMemoryDb[Tuple]
2734

35+
async def assert_can_accept_tuples(self, relation: Relation) -> None:
36+
...
37+
2838
async def tuples_with_attribute(
2939
self,
3040
relation_number: Number,
@@ -88,6 +98,15 @@ async def _map_one_idempotently(self, effect: TransactionEffect) -> None:
8898
class InTelegramHeapTuples(Tuples):
8999
_heap: InTelegramHeap
90100

101+
async def assert_can_accept_tuples(self, relation: Relation) -> None:
102+
try:
103+
self._heap.assert_can_accept_tuples_of_relation(relation)
104+
except UnacceptableTupleError as error:
105+
raise OversizedRelationSchemaError(
106+
error.encoded_tuple_len,
107+
self._heap.tuple_max_len(),
108+
) from error
109+
91110
async def tuples_with_attribute(
92111
self,
93112
relation_number: Number,

src/tgdb/infrastructure/heap_tuple_encoding.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
from datetime import datetime, timedelta, timezone
12
from enum import Enum
3+
from uuid import UUID
24

35
from tgdb.entities.numeration.number import Number
6+
from tgdb.entities.relation.domain import (
7+
BoolDomain,
8+
DatetimeDomain,
9+
Domain,
10+
IntDomain,
11+
StrDomain,
12+
UuidDomain,
13+
)
414
from tgdb.entities.relation.relation import RelationSchemaID
515
from tgdb.entities.relation.scalar import Scalar
16+
from tgdb.entities.relation.schema import Schema
617
from tgdb.entities.relation.tuple import TID, Tuple
718
from tgdb.infrastructure.primitive_encoding import (
819
ReversibleTranslationTable,
@@ -11,6 +22,7 @@
1122
decoded_uuid,
1223
encoded_int,
1324
encoded_primitive_with_type,
25+
encoded_primitive_without_type,
1426
encoded_uuid,
1527
)
1628

@@ -27,6 +39,16 @@ class Separator(Enum):
2739

2840

2941
class HeapTupleEncoding:
42+
@staticmethod
43+
def largest_tuple(
44+
schema: Schema,
45+
schema_id: RelationSchemaID,
46+
) -> Tuple:
47+
xid, schema_id = _HeapTupleMetadataEncoding.largest_metadata(schema_id)
48+
scalars = map(_HeapTupleAttributeEncoding.largest_scalar, schema)
49+
50+
return Tuple(xid, schema_id, tuple(scalars))
51+
3052
@staticmethod
3153
def encoded_tuple(tuple_: Tuple) -> str:
3254
encoded_metadata = _HeapTupleMetadataEncoding.encoded_metadata(
@@ -111,6 +133,10 @@ def decoded_metadata(encoded_metadata: str) -> _HeapTupleMetadata:
111133
def id_of_encoded_tuple_with_tid(tid: TID) -> str:
112134
return f"{Separator.top_metadata.value}{encoded_uuid(tid)}"
113135

136+
@staticmethod
137+
def largest_metadata(schema_id: RelationSchemaID) -> _HeapTupleMetadata:
138+
return UUID(int=0), schema_id
139+
114140

115141
class _HeapTupleAttributeEncoding:
116142
@staticmethod
@@ -132,3 +158,25 @@ def decoded_scalar(encoded_attribute: str) -> Scalar:
132158
)
133159

134160
return decoded_primitive_with_type(encoded_scalar, heap_tuple_table)
161+
162+
@staticmethod
163+
def largest_scalar(domain: Domain) -> Scalar:
164+
match domain:
165+
case IntDomain():
166+
return max(domain.min, domain.max, key=lambda it: len(str(it)))
167+
case StrDomain():
168+
return "x" * domain.max_len
169+
case BoolDomain():
170+
return True
171+
case DatetimeDomain():
172+
tzinfo = timezone(timedelta(hours=14))
173+
return datetime(9999, 12, 31, 23, 59, 59, 999999, tzinfo=tzinfo)
174+
case UuidDomain():
175+
return UUID(int=0)
176+
case tuple():
177+
return max(
178+
domain,
179+
key=lambda it: (
180+
encoded_primitive_without_type(it, heap_tuple_table)
181+
)
182+
)

0 commit comments

Comments
 (0)