tapestry-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.
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/PKG-INFO +1 -1
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/pyproject.toml +1 -1
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/__init__.py +2 -2
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/table.py +86 -6
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/README.md +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/base.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/edge.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/engine.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/field.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/field.pyi +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/node.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/py.typed +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/query.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/tokenizer.py +0 -0
- {tapestry_orm-0.0.3 → tapestry_orm-0.0.4}/src/tapestry/utils.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
__all__ = ["Base", "Text", "create_engine", "create_engine_context", "Engine", "Reference", "Node", "Edge", "Q"]
|
|
1
|
+
__all__ = ["Base", "Text", "create_engine", "create_engine_context", "Engine", "Reference", "Indexed", "Node", "Edge", "Q"]
|
|
2
2
|
|
|
3
3
|
from .query import Q
|
|
4
4
|
from .base import Base
|
|
5
5
|
from .node import Node
|
|
6
6
|
from .edge import Edge
|
|
7
7
|
from .tokenizer import Text
|
|
8
|
-
from .table import Reference
|
|
8
|
+
from .table import Reference, Indexed
|
|
9
9
|
from .engine import create_engine, create_engine_context, Engine
|
|
@@ -17,6 +17,26 @@ from .utils import flatten_type
|
|
|
17
17
|
from .tokenizer import Text, Tokenizer, FrenchTokenizer
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class Indexed:
|
|
22
|
+
"""Opt-in index annotation for model fields.
|
|
23
|
+
|
|
24
|
+
Use with ``typing.Annotated`` to request a database index on a field::
|
|
25
|
+
|
|
26
|
+
from typing import Annotated
|
|
27
|
+
from tapestry import Node, Indexed
|
|
28
|
+
|
|
29
|
+
class Person(Node):
|
|
30
|
+
ext_id: Annotated[str | None, Indexed(unique=True)] = None
|
|
31
|
+
party: Annotated[str | None, Indexed()] = None
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
unique: If True, the index enforces uniqueness.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
unique: bool = False
|
|
38
|
+
|
|
39
|
+
|
|
20
40
|
@dataclass(frozen=True)
|
|
21
41
|
class Link:
|
|
22
42
|
_in: type[Any]
|
|
@@ -86,11 +106,18 @@ class Table:
|
|
|
86
106
|
relation: Link | None = None
|
|
87
107
|
index: dict[str, str] = field(default_factory=dict)
|
|
88
108
|
tokenizers: tuple[Tokenizer, ...] = ()
|
|
109
|
+
record_link_fields: tuple[str, ...] = ()
|
|
110
|
+
indexed_fields: dict[str, bool] = field(default_factory=dict)
|
|
89
111
|
|
|
90
112
|
def __post_init__(self):
|
|
91
113
|
indices = {}
|
|
92
114
|
tokenizers = []
|
|
115
|
+
record_links = []
|
|
116
|
+
opt_in: dict[str, bool] = {}
|
|
117
|
+
|
|
93
118
|
for field in self.fields:
|
|
119
|
+
if field.name in ("id", "is_stub_"):
|
|
120
|
+
continue
|
|
94
121
|
if field.field_type:
|
|
95
122
|
for tp in tuple(flatten_type(field.field_type, base_type=Text)):
|
|
96
123
|
if inspect.isclass(tp) and issubclass(tp, Text):
|
|
@@ -105,9 +132,44 @@ class Table:
|
|
|
105
132
|
if not args or not issubclass(args[0], self.base_class):
|
|
106
133
|
raise TypeError("References must point to a Node class")
|
|
107
134
|
|
|
135
|
+
# Detect record link fields (foreign keys)
|
|
136
|
+
if self._is_record_link_type(field.field_type):
|
|
137
|
+
record_links.append(field.name)
|
|
138
|
+
|
|
139
|
+
# Detect opt-in Indexed metadata from Pydantic FieldInfo
|
|
140
|
+
if hasattr(self.model_class, "model_fields"):
|
|
141
|
+
for fname, finfo in self.model_class.model_fields.items():
|
|
142
|
+
if hasattr(finfo, "metadata") and finfo.metadata:
|
|
143
|
+
for meta in finfo.metadata:
|
|
144
|
+
if isinstance(meta, Indexed):
|
|
145
|
+
opt_in[fname] = meta.unique
|
|
146
|
+
break
|
|
147
|
+
|
|
108
148
|
object.__setattr__(self, "index", indices)
|
|
109
149
|
object.__setattr__(self, "tokenizers", tuple(tokenizers))
|
|
110
150
|
object.__setattr__(self, "name_to_type", {f.name: f.field_type for f in self.fields})
|
|
151
|
+
object.__setattr__(self, "record_link_fields", tuple(record_links))
|
|
152
|
+
object.__setattr__(self, "indexed_fields", opt_in)
|
|
153
|
+
|
|
154
|
+
def _is_record_link_type(self, tp: Any) -> bool:
|
|
155
|
+
"""Check whether *tp* resolves to a direct record link.
|
|
156
|
+
|
|
157
|
+
Returns ``True`` when the type is (or contains via Union) a subclass
|
|
158
|
+
of ``self.base_class``, but does **not** recurse into generic
|
|
159
|
+
containers like ``list[...]`` or ``set[...]``.
|
|
160
|
+
"""
|
|
161
|
+
if tp is type(None):
|
|
162
|
+
return False
|
|
163
|
+
try:
|
|
164
|
+
if inspect.isclass(tp) and issubclass(tp, self.base_class):
|
|
165
|
+
return True
|
|
166
|
+
except TypeError:
|
|
167
|
+
return False
|
|
168
|
+
origin = get_origin(tp)
|
|
169
|
+
args = get_args(tp)
|
|
170
|
+
if origin is types.UnionType or (origin is not None and hasattr(origin, "__name__") and origin.__name__ == "UnionType"):
|
|
171
|
+
return any(self._is_record_link_type(a) for a in (args or ()))
|
|
172
|
+
return False
|
|
111
173
|
|
|
112
174
|
# -------------------- helpers to map python type -> surreal type ----------
|
|
113
175
|
|
|
@@ -438,7 +500,7 @@ class Table:
|
|
|
438
500
|
return getattr(tp, "__name__", str(tp)).lower()
|
|
439
501
|
|
|
440
502
|
def generate_table_sql(self) -> str:
|
|
441
|
-
stmts = []
|
|
503
|
+
stmts: list[str] = []
|
|
442
504
|
if self.relation:
|
|
443
505
|
in_tbl = self._pytype_to_surreal("_in", self.relation._in)
|
|
444
506
|
out_tbl = self._pytype_to_surreal("_out", self.relation._out)
|
|
@@ -458,8 +520,26 @@ class Table:
|
|
|
458
520
|
surreal_t = self._pytype_to_surreal(ft.name, ft.field_type)
|
|
459
521
|
stmts.append(f"DEFINE FIELD {self._quote_ident(ft.name)} ON {self.name} TYPE {surreal_t};")
|
|
460
522
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
523
|
+
# -- Indexes -----------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
# Auto-index edge in/out fields
|
|
526
|
+
if self.relation is not None:
|
|
527
|
+
stmts.append(f"DEFINE INDEX idx_{self.name}_in ON TABLE {self.name} FIELDS in;")
|
|
528
|
+
stmts.append(f"DEFINE INDEX idx_{self.name}_out ON TABLE {self.name} FIELDS out;")
|
|
529
|
+
|
|
530
|
+
# Auto-index record link fields (foreign keys)
|
|
531
|
+
for field_name in self.record_link_fields:
|
|
532
|
+
stmts.append(f"DEFINE INDEX idx_{self.name}_{field_name} ON TABLE {self.name} FIELDS {field_name};")
|
|
533
|
+
|
|
534
|
+
# Opt-in indexed fields
|
|
535
|
+
for field_name, unique in self.indexed_fields.items():
|
|
536
|
+
unique_suffix = " UNIQUE" if unique else ""
|
|
537
|
+
stmts.append(f"DEFINE INDEX idx_{self.name}_{field_name} ON TABLE {self.name} FIELDS {field_name}{unique_suffix};")
|
|
538
|
+
|
|
539
|
+
# Fulltext search indexes (preserve existing naming convention)
|
|
540
|
+
for field, analyzer in self.index.items():
|
|
541
|
+
stmts.append(
|
|
542
|
+
f"DEFINE INDEX {field}_search ON TABLE {self.name} FIELDS {field} FULLTEXT ANALYZER {analyzer} BM25 HIGHLIGHTS;"
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
return "\n".join(stmts)
|
|
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
|