sqlmodel-ext 0.2.0__py3-none-any.whl → 0.2.1__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.
sqlmodel_ext/__init__.py CHANGED
@@ -20,10 +20,10 @@ Quick start::
20
20
  user = await user.save(session)
21
21
  users = await User.get(session, fetch_mode="all")
22
22
  """
23
- __version__ = "0.2.0"
23
+ __version__ = "0.2.1"
24
24
 
25
25
  # Base
26
- from sqlmodel_ext.base import SQLModelBase
26
+ from sqlmodel_ext.base import SQLModelBase, ExtraIgnoreModelBase
27
27
 
28
28
  # Exceptions
29
29
  from sqlmodel_ext._exceptions import RecordNotFoundError
@@ -39,8 +39,11 @@ from sqlmodel_ext.pagination import (
39
39
  # Mixins
40
40
  from sqlmodel_ext.mixins import (
41
41
  # Table
42
+ SESSION_FOR_UPDATE_KEY,
42
43
  TableBaseMixin,
43
44
  UUIDTableBaseMixin,
45
+ rel,
46
+ cond,
44
47
  # Polymorphic
45
48
  PolymorphicBaseMixin,
46
49
  AutoPolymorphicIdentityMixin,
@@ -53,6 +56,9 @@ from sqlmodel_ext.mixins import (
53
56
  # Relation Preload
54
57
  RelationPreloadMixin,
55
58
  requires_relations,
59
+ requires_for_update,
60
+ # Cached Table
61
+ CachedTableBaseMixin,
56
62
  # Info Response DTOs
57
63
  IntIdInfoMixin,
58
64
  UUIDIdInfoMixin,
@@ -76,20 +82,28 @@ from sqlmodel_ext.field_types import (
76
82
  Str128,
77
83
  Str255,
78
84
  Str256,
85
+ Str500,
86
+ Str512,
87
+ Str2048,
79
88
  Text1K,
80
89
  Text1024,
81
90
  Text2K,
82
91
  Text2500,
83
92
  Text3K,
93
+ Text5K,
84
94
  Text10K,
85
95
  Text60K,
86
96
  Text64K,
87
97
  Text100K,
98
+ Text1M,
88
99
  # Numeric constraints
100
+ INT32_MAX,
89
101
  Port,
90
102
  Percentage,
91
103
  PositiveInt,
92
104
  NonNegativeInt,
105
+ PositiveBigInt,
106
+ NonNegativeBigInt,
93
107
  PositiveFloat,
94
108
  # Custom types
95
109
  IPAddress,
@@ -101,3 +115,12 @@ from sqlmodel_ext.field_types import (
101
115
  validate_not_private_host,
102
116
  ModuleNameMixin,
103
117
  )
118
+
119
+ # Relation Load Checker (static analysis)
120
+ from sqlmodel_ext.relation_load_checker import (
121
+ RelationLoadChecker,
122
+ RelationLoadWarning,
123
+ RelationLoadCheckMiddleware,
124
+ run_model_checks,
125
+ mark_app_check_completed,
126
+ )
sqlmodel_ext/_sa_type.py CHANGED
@@ -97,7 +97,8 @@ def _resolve_annotations(attrs: dict[str, Any]) -> tuple[
97
97
 
98
98
  try:
99
99
  temp_cls = type('AnnotationProxy', (object,), dict(attrs))
100
- temp_cls.__module__ = module_name
100
+ if isinstance(module_name, str):
101
+ temp_cls.__module__ = module_name
101
102
  extras_kw = {'include_extras': True} if sys.version_info >= (3, 10) else {}
102
103
  evaluated = get_type_hints(
103
104
  temp_cls,
@@ -132,6 +133,6 @@ def _evaluate_annotation_from_string(
132
133
  return current_type
133
134
 
134
135
  try:
135
- return eval(expr, globalns, localns)
136
+ return eval(expr, dict(globalns), dict(localns))
136
137
  except (NameError, SyntaxError, AttributeError, TypeError):
137
138
  return current_type
sqlmodel_ext/_utils.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """Internal utility functions."""
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
 
4
- now = lambda: datetime.now()
5
- now_date = lambda: datetime.now().date()
4
+ now = lambda: datetime.now(timezone.utc)
5
+ now_date = lambda: datetime.now(timezone.utc).date()
sqlmodel_ext/base.py CHANGED
@@ -9,11 +9,13 @@ Provides a smart metaclass that handles:
9
9
  - Annotated sa_type extraction and injection
10
10
  - Python 3.14 (PEP 649) compatibility
11
11
  """
12
+ import logging
13
+ import re
12
14
  import sys
13
15
  import typing
14
16
  from typing import Any, get_args, get_origin
15
17
 
16
- from pydantic import ConfigDict
18
+ from pydantic import ConfigDict, model_validator
17
19
  from pydantic.fields import FieldInfo
18
20
  from pydantic_core import PydanticUndefined as Undefined
19
21
  from sqlalchemy import inspect as sa_inspect
@@ -36,6 +38,61 @@ if sys.version_info >= (3, 14):
36
38
  else:
37
39
  annotationlib = None
38
40
 
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def _make_sti_fk_resolver(
45
+ fk_string: str,
46
+ sa_registry: typing.Any,
47
+ ) -> typing.Callable:
48
+ """
49
+ Convert string-format foreign_keys to a callable for deferred resolution in STI.
50
+
51
+ STI child columns are added to the parent table via _register_sti_columns(),
52
+ but during configure_mappers() they are not yet registered as ColumnProperty.
53
+ SQLAlchemy's string resolution (_GetColumns.__getattr__) looks up columns via
54
+ mapper.all_orm_descriptors, which fails for unregistered STI columns.
55
+
56
+ Solution: convert to callable so configure_mappers() calls it to resolve
57
+ Column objects directly from the table's columns collection (Phase 1 already added them).
58
+
59
+ :param fk_string: String-format foreign_keys, e.g. '[NanoBananaFunction.flash_llm_id]'
60
+ :param sa_registry: SQLAlchemy registry for class-name lookup
61
+ :return: callable returning list of Column objects
62
+ """
63
+ inner = fk_string.strip('[]')
64
+ specs = [s.strip() for s in inner.split(',')]
65
+
66
+ parsed: list[tuple[str, str]] = []
67
+ for spec in specs:
68
+ m = re.match(r'^(\w+)\.(\w+)$', spec)
69
+ if not m:
70
+ return fk_string # type: ignore # cannot parse, return original
71
+ parsed.append((m.group(1), m.group(2)))
72
+
73
+ _registry = sa_registry
74
+
75
+ def _resolve() -> list:
76
+ columns = []
77
+ for cls_name, col_name in parsed:
78
+ for mapper in _registry.mappers:
79
+ if mapper.class_.__name__ == cls_name:
80
+ table = mapper.local_table
81
+ if col_name not in table.c:
82
+ raise RuntimeError(
83
+ f"STI FK resolution failed: column '{col_name}' "
84
+ f"not in table '{table.name}' (class {cls_name})"
85
+ )
86
+ columns.append(table.c[col_name])
87
+ break
88
+ else:
89
+ raise RuntimeError(
90
+ f"STI FK resolution failed: class '{cls_name}' not in SA registry"
91
+ )
92
+ return columns
93
+
94
+ return _resolve
95
+
39
96
 
40
97
  class __DeclarativeMeta(SQLModelMetaclass):
41
98
  """
@@ -61,6 +118,13 @@ class __DeclarativeMeta(SQLModelMetaclass):
61
118
  if is_intended_as_table and 'table' not in kwargs:
62
119
  kwargs['table'] = True
63
120
 
121
+ # 1.5. CachedTableBaseMixin: cache_ttl class keyword -> __cache_ttl__ attribute
122
+ if 'cache_ttl' in kwargs:
123
+ ttl = kwargs.pop('cache_ttl')
124
+ if not isinstance(ttl, int) or ttl <= 0:
125
+ raise ValueError(f"{name}: cache_ttl must be a positive integer, got: {ttl!r}")
126
+ attrs['__cache_ttl__'] = ttl
127
+
64
128
  # 2. Detect STI scenario and preprocess
65
129
  parent_tablename = None
66
130
  for base in bases:
@@ -248,9 +312,21 @@ class __DeclarativeMeta(SQLModelMetaclass):
248
312
  return False
249
313
 
250
314
  if has_different_tablename and parent_tablename:
315
+ # JTI FK must also be primary_key (created by SubclassIdMixin).
316
+ # A FK pointing to the parent table that is NOT a PK (e.g. self-referential
317
+ # parent_transaction_id) should NOT be identified as JTI inheritance.
318
+ def _is_jti_fk(fi: typing.Any) -> bool:
319
+ fk = getattr(fi, 'foreign_key', None)
320
+ pk = getattr(fi, 'primary_key', None)
321
+ return (
322
+ fk is not None
323
+ and isinstance(fk, str)
324
+ and pk is True # PydanticUndefined is truthy, must compare strictly
325
+ and _fk_matches_parent(fk, parent_tablename)
326
+ )
327
+
251
328
  for field_name, field_info in cls.model_fields.items():
252
- fk = getattr(field_info, 'foreign_key', None)
253
- if fk is not None and isinstance(fk, str) and _fk_matches_parent(fk, parent_tablename):
329
+ if _is_jti_fk(field_info):
254
330
  has_fk_to_parent = True
255
331
  break
256
332
 
@@ -258,8 +334,7 @@ class __DeclarativeMeta(SQLModelMetaclass):
258
334
  for base in bases:
259
335
  if hasattr(base, 'model_fields'):
260
336
  for field_name, field_info in base.model_fields.items():
261
- fk = getattr(field_info, 'foreign_key', None)
262
- if fk is not None and isinstance(fk, str) and _fk_matches_parent(fk, parent_tablename):
337
+ if _is_jti_fk(field_info):
263
338
  has_fk_to_parent = True
264
339
  break
265
340
  if has_fk_to_parent:
@@ -367,73 +442,113 @@ class __DeclarativeMeta(SQLModelMetaclass):
367
442
  mapper_args = getattr(cls, '__mapper_args__', {})
368
443
  polymorphic_identity = mapper_args.get('polymorphic_identity')
369
444
 
370
- if polymorphic_identity is not None:
371
- parent_cls = None
372
- for base in bases:
373
- if is_table_model_class(base) and hasattr(base, '__mapper__'):
374
- parent_cls = base
375
- break
445
+ # Support both concrete classes (polymorphic_identity set) and
446
+ # abstract intermediate classes (polymorphic_identity=None, polymorphic_abstract=True)
447
+ parent_cls = None
448
+ for base in bases:
449
+ if is_table_model_class(base) and hasattr(base, '__mapper__'):
450
+ parent_cls = base
451
+ break
376
452
 
377
- if parent_cls is not None:
378
- registry = parent_cls._sa_registry
453
+ if parent_cls is not None:
454
+ registry = parent_cls._sa_registry
379
455
 
380
- rels = getattr(cls, '__sqlmodel_relationships__', {})
381
- own_rels = {}
382
- for rel_name, rel_info in rels.items():
383
- is_inherited = any(
384
- hasattr(base, '__sqlmodel_relationships__') and rel_name in base.__sqlmodel_relationships__
385
- for base in bases
386
- )
387
- if not is_inherited:
388
- own_rels[rel_name] = rel_info
389
-
390
- properties = {}
391
- if own_rels:
392
- for rel_name, rel_info in own_rels.items():
393
- if rel_info.sa_relationship:
394
- properties[rel_name] = rel_info.sa_relationship
395
- else:
396
- raw_ann = cls.__annotations__.get(rel_name)
397
- if raw_ann:
398
- origin = get_origin(raw_ann)
399
- if origin is Mapped:
400
- ann = raw_ann.__args__[0]
401
- else:
402
- ann = raw_ann
403
- relationship_to = get_relationship_to(
404
- name=rel_name, rel_info=rel_info, annotation=ann
405
- )
406
- rel_kwargs: dict[str, typing.Any] = {}
407
- if rel_info.back_populates:
408
- rel_kwargs["back_populates"] = rel_info.back_populates
409
- if rel_info.cascade_delete:
410
- rel_kwargs["cascade"] = "all, delete-orphan"
411
- if rel_info.passive_deletes:
412
- rel_kwargs["passive_deletes"] = rel_info.passive_deletes
413
- if rel_info.link_model:
414
- ins = sa_inspect(rel_info.link_model)
415
- local_table = getattr(ins, "local_table")
416
- if local_table is None:
417
- raise RuntimeError(
418
- f"Could not find secondary table for {rel_name}: {rel_info.link_model}"
419
- )
420
- rel_kwargs["secondary"] = local_table
421
-
422
- rel_args: list[typing.Any] = []
423
- if rel_info.sa_relationship_args:
424
- rel_args.extend(rel_info.sa_relationship_args)
425
- if rel_info.sa_relationship_kwargs:
426
- rel_kwargs.update(rel_info.sa_relationship_kwargs)
427
-
428
- properties[rel_name] = sa_relationship(relationship_to, *rel_args, **rel_kwargs)
429
-
430
- registry.map_imperatively(
431
- cls,
432
- parent_cls.__table__,
433
- inherits=parent_cls,
434
- polymorphic_identity=polymorphic_identity,
435
- properties=properties if properties else None,
456
+ rels = getattr(cls, '__sqlmodel_relationships__', {})
457
+ own_rels = {}
458
+ for rel_name, rel_info in rels.items():
459
+ is_inherited = any(
460
+ hasattr(base, '__sqlmodel_relationships__') and rel_name in base.__sqlmodel_relationships__
461
+ for base in bases
436
462
  )
463
+ if not is_inherited:
464
+ own_rels[rel_name] = rel_info
465
+
466
+ properties = {}
467
+ if own_rels:
468
+ for rel_name, rel_info in own_rels.items():
469
+ if rel_info.sa_relationship:
470
+ properties[rel_name] = rel_info.sa_relationship
471
+ else:
472
+ raw_ann = cls.__annotations__.get(rel_name)
473
+ if raw_ann:
474
+ origin = get_origin(raw_ann)
475
+ if origin is Mapped:
476
+ ann = raw_ann.__args__[0]
477
+ else:
478
+ ann = raw_ann
479
+ relationship_to = get_relationship_to(
480
+ name=rel_name, rel_info=rel_info, annotation=ann
481
+ )
482
+ rel_kwargs: dict[str, typing.Any] = {}
483
+ if rel_info.back_populates:
484
+ rel_kwargs["back_populates"] = rel_info.back_populates
485
+ if rel_info.cascade_delete:
486
+ rel_kwargs["cascade"] = "all, delete-orphan"
487
+ if rel_info.passive_deletes:
488
+ rel_kwargs["passive_deletes"] = rel_info.passive_deletes
489
+ if rel_info.link_model:
490
+ ins = sa_inspect(rel_info.link_model)
491
+ local_table = getattr(ins, "local_table")
492
+ if local_table is None:
493
+ raise RuntimeError(
494
+ f"Could not find secondary table for {rel_name}: {rel_info.link_model}"
495
+ )
496
+ rel_kwargs["secondary"] = local_table
497
+
498
+ rel_args: list[typing.Any] = []
499
+ if rel_info.sa_relationship_args:
500
+ rel_args.extend(rel_info.sa_relationship_args)
501
+ if rel_info.sa_relationship_kwargs:
502
+ rel_kwargs.update(rel_info.sa_relationship_kwargs)
503
+
504
+ # Default lazy='raise_on_sql' for async safety:
505
+ # prevents accidental lazy-loading which causes
506
+ # MissingGreenlet errors in async environments.
507
+ if 'lazy' not in rel_kwargs:
508
+ rel_kwargs['lazy'] = 'raise_on_sql'
509
+
510
+ # STI foreign_keys deferred resolution:
511
+ # STI child columns are not yet registered as ColumnProperty
512
+ # during configure_mappers(), so string foreign_keys fail.
513
+ # Convert to callable for lazy resolution from table columns.
514
+ if 'foreign_keys' in rel_kwargs:
515
+ _fk_val = rel_kwargs['foreign_keys']
516
+ if isinstance(_fk_val, str):
517
+ rel_kwargs['foreign_keys'] = _make_sti_fk_resolver(
518
+ _fk_val, registry
519
+ )
520
+ else:
521
+ # Auto-detect FK ambiguity: when the "many" side STI child
522
+ # has a {rel_name}_id FK field but foreign_keys is not
523
+ # explicitly specified, add a callable to disambiguate.
524
+ _fk_field = f'{rel_name}_id'
525
+ _model_fields = getattr(cls, 'model_fields', None) or {}
526
+ if _fk_field in _model_fields:
527
+ _tbl = parent_cls.__table__
528
+ _fn = _fk_field
529
+ rel_kwargs['foreign_keys'] = (
530
+ lambda _t=_tbl, _f=_fn: [_t.c[_f]]
531
+ )
532
+
533
+ properties[rel_name] = sa_relationship(relationship_to, *rel_args, **rel_kwargs)
534
+
535
+ # Build map_imperatively kwargs conditionally
536
+ map_kwargs: dict[str, typing.Any] = {
537
+ 'inherits': parent_cls,
538
+ 'properties': properties if properties else None,
539
+ }
540
+ if polymorphic_identity is not None:
541
+ map_kwargs['polymorphic_identity'] = polymorphic_identity
542
+ # Abstract intermediate classes (e.g. TencentCompatibleLLM)
543
+ # need polymorphic_abstract=True forwarded to map_imperatively
544
+ if mapper_args.get('polymorphic_abstract'):
545
+ map_kwargs['polymorphic_abstract'] = True
546
+
547
+ registry.map_imperatively(
548
+ cls,
549
+ parent_cls.__table__,
550
+ **map_kwargs,
551
+ )
437
552
 
438
553
  def _setup_relationships(cls, only_these: set[str] | None = None) -> None:
439
554
  """
@@ -481,6 +596,11 @@ class __DeclarativeMeta(SQLModelMetaclass):
481
596
  if rel_info.sa_relationship_kwargs:
482
597
  rel_kwargs.update(rel_info.sa_relationship_kwargs)
483
598
 
599
+ # Default lazy='raise_on_sql' for async safety: prevents accidental
600
+ # lazy-loading which causes MissingGreenlet errors in async environments.
601
+ if 'lazy' not in rel_kwargs:
602
+ rel_kwargs['lazy'] = 'raise_on_sql'
603
+
484
604
  rel_value = sa_relationship(relationship_to, *rel_args, **rel_kwargs)
485
605
  setattr(cls, rel_name, rel_value)
486
606
 
@@ -499,3 +619,53 @@ class SQLModelBase(SQLModel, metaclass=__DeclarativeMeta):
499
619
  """Get the set of computed_field names for this model class."""
500
620
  fields = cls.model_computed_fields
501
621
  return set(fields.keys()) if fields else set()
622
+
623
+
624
+ class ExtraIgnoreModelBase(SQLModelBase):
625
+ """
626
+ Model base class that ignores unknown fields (extra='ignore').
627
+
628
+ Unlike SQLModelBase (extra='forbid'), this class silently ignores undeclared
629
+ fields and logs a WARNING for discoverability.
630
+
631
+ Use for:
632
+ - Third-party API responses (where the schema may change without notice)
633
+ - Client WebSocket message envelopes (protocol-level field validation)
634
+ - Any model parsing external JSON input (including nested sub-models)
635
+
636
+ Do NOT use for: request models that we construct and send to external services
637
+ (those should keep 'forbid' to catch mistakes).
638
+ """
639
+
640
+ model_config = ConfigDict(
641
+ use_attribute_docstrings=True, validate_by_name=True, extra='ignore',
642
+ )
643
+
644
+ @model_validator(mode='before')
645
+ @classmethod
646
+ def _warn_unknown_fields(cls, data: Any) -> Any:
647
+ """
648
+ Detect and warn about unknown fields in incoming data.
649
+
650
+ Logs a WARNING before Pydantic's extra='ignore' discards unknown fields,
651
+ helping developers notice third-party API changes and add field definitions.
652
+ """
653
+ if not isinstance(data, dict):
654
+ return data
655
+ accepted: set[str] = set()
656
+ for name, field_info in cls.model_fields.items():
657
+ accepted.add(name)
658
+ if field_info.alias:
659
+ accepted.add(field_info.alias)
660
+ if field_info.validation_alias and isinstance(field_info.validation_alias, str):
661
+ accepted.add(field_info.validation_alias)
662
+ unknown = set(data.keys()) - accepted
663
+ if unknown:
664
+ total = len(unknown)
665
+ sample = [name[:64] for name in sorted(unknown)[:5]]
666
+ logger.warning(
667
+ "External input contains unknown fields | model=%s "
668
+ "unknown_count=%d sample_fields=%s",
669
+ cls.__name__, total, sample,
670
+ )
671
+ return data
@@ -7,6 +7,8 @@ all compatible with Pydantic validation and SQLAlchemy column mapping.
7
7
  from pathlib import Path
8
8
  from typing import Annotated, TypeAlias
9
9
 
10
+ from pydantic import StringConstraints
11
+ from sqlalchemy import BigInteger
10
12
  from sqlmodel import Field
11
13
 
12
14
  from ._internal.path import _DirectoryPathHandler, _FilePathHandler
@@ -42,61 +44,79 @@ while behaving as a ``pathlib.Path`` in Python code.
42
44
  # Field Constraint Type Aliases (Annotated Style)
43
45
  # ---------------------------------------------------------------------------
44
46
 
47
+ _NO_NULL_BYTE = StringConstraints(pattern=r'^[^\x00]*$')
48
+ """PostgreSQL rejects null bytes in text columns. pydantic-core compiles the regex once with zero Python overhead."""
49
+
45
50
  # String length constraints
46
- Str24: TypeAlias = Annotated[str, Field(max_length=24)]
51
+ Str24: TypeAlias = Annotated[str, Field(max_length=24), _NO_NULL_BYTE]
47
52
  """24-character string field"""
48
53
 
49
- Str32: TypeAlias = Annotated[str, Field(max_length=32)]
54
+ Str32: TypeAlias = Annotated[str, Field(max_length=32), _NO_NULL_BYTE]
50
55
  """32-character string field"""
51
56
 
52
- Str36: TypeAlias = Annotated[str, Field(max_length=36)]
57
+ Str36: TypeAlias = Annotated[str, Field(max_length=36), _NO_NULL_BYTE]
53
58
  """36-character string field (UUID standard format length)"""
54
59
 
55
- Str48: TypeAlias = Annotated[str, Field(max_length=48)]
60
+ Str48: TypeAlias = Annotated[str, Field(max_length=48), _NO_NULL_BYTE]
56
61
  """48-character string field"""
57
62
 
58
- Str64: TypeAlias = Annotated[str, Field(max_length=64)]
63
+ Str64: TypeAlias = Annotated[str, Field(max_length=64), _NO_NULL_BYTE]
59
64
  """64-character string field"""
60
65
 
61
- Str100: TypeAlias = Annotated[str, Field(max_length=100)]
66
+ Str100: TypeAlias = Annotated[str, Field(max_length=100), _NO_NULL_BYTE]
62
67
  """100-character string field"""
63
68
 
64
- Str128: TypeAlias = Annotated[str, Field(max_length=128)]
69
+ Str128: TypeAlias = Annotated[str, Field(max_length=128), _NO_NULL_BYTE]
65
70
  """128-character string field"""
66
71
 
67
- Str255: TypeAlias = Annotated[str, Field(max_length=255)]
72
+ Str255: TypeAlias = Annotated[str, Field(max_length=255), _NO_NULL_BYTE]
68
73
  """255-character string field"""
69
74
 
70
- Str256: TypeAlias = Annotated[str, Field(max_length=256)]
75
+ Str256: TypeAlias = Annotated[str, Field(max_length=256), _NO_NULL_BYTE]
71
76
  """256-character string field"""
72
77
 
73
- Text1K: TypeAlias = Annotated[str, Field(max_length=1000)]
78
+ Str500: TypeAlias = Annotated[str, Field(max_length=500), _NO_NULL_BYTE]
79
+ """500-character string field"""
80
+
81
+ Str512: TypeAlias = Annotated[str, Field(max_length=512), _NO_NULL_BYTE]
82
+ """512-character string field"""
83
+
84
+ Str2048: TypeAlias = Annotated[str, Field(max_length=2048), _NO_NULL_BYTE]
85
+ """2048-character string field (URLs etc.)"""
86
+
87
+ Text1K: TypeAlias = Annotated[str, Field(max_length=1000), _NO_NULL_BYTE]
74
88
  """1000-character text field"""
75
89
 
76
- Text1024: TypeAlias = Annotated[str, Field(max_length=1024)]
90
+ Text1024: TypeAlias = Annotated[str, Field(max_length=1024), _NO_NULL_BYTE]
77
91
  """1024-character text field"""
78
92
 
79
- Text2K: TypeAlias = Annotated[str, Field(max_length=2000)]
93
+ Text2K: TypeAlias = Annotated[str, Field(max_length=2000), _NO_NULL_BYTE]
80
94
  """2000-character text field"""
81
95
 
82
- Text2500: TypeAlias = Annotated[str, Field(max_length=2500)]
96
+ Text2500: TypeAlias = Annotated[str, Field(max_length=2500), _NO_NULL_BYTE]
83
97
  """2500-character text field"""
84
98
 
85
- Text3K: TypeAlias = Annotated[str, Field(max_length=3000)]
99
+ Text3K: TypeAlias = Annotated[str, Field(max_length=3000), _NO_NULL_BYTE]
86
100
  """3000-character text field"""
87
101
 
88
- Text10K: TypeAlias = Annotated[str, Field(max_length=10000)]
102
+ Text5K: TypeAlias = Annotated[str, Field(max_length=5000), _NO_NULL_BYTE]
103
+ """5000-character text field"""
104
+
105
+ Text10K: TypeAlias = Annotated[str, Field(max_length=10000), _NO_NULL_BYTE]
89
106
  """10000-character text field"""
90
107
 
91
- Text60K: TypeAlias = Annotated[str, Field(max_length=60000)]
108
+ Text60K: TypeAlias = Annotated[str, Field(max_length=60000), _NO_NULL_BYTE]
92
109
  """60000-character text field"""
93
110
 
94
- Text64K: TypeAlias = Annotated[str, Field(max_length=65536)]
111
+ Text64K: TypeAlias = Annotated[str, Field(max_length=65536), _NO_NULL_BYTE]
95
112
  """65536-character text field"""
96
113
 
97
- Text100K: TypeAlias = Annotated[str, Field(max_length=100000)]
114
+ Text100K: TypeAlias = Annotated[str, Field(max_length=100000), _NO_NULL_BYTE]
98
115
  """100000-character text field"""
99
116
 
117
+ Text1M: TypeAlias = Annotated[str, Field(max_length=1000000), _NO_NULL_BYTE]
118
+ """1000000-character text field (tool call parameters, tool responses, etc.)"""
119
+
100
120
  # Numeric range constraints
101
121
  Port: TypeAlias = Annotated[int, Field(ge=1, le=65535)]
102
122
  """Port number (1-65535)"""
@@ -104,11 +124,20 @@ Port: TypeAlias = Annotated[int, Field(ge=1, le=65535)]
104
124
  Percentage: TypeAlias = Annotated[int, Field(ge=0, le=100)]
105
125
  """Percentage (0-100)"""
106
126
 
107
- PositiveInt: TypeAlias = Annotated[int, Field(ge=1)]
108
- """Positive integer (>=1)"""
127
+ INT32_MAX = 2147483647
128
+ """Maximum value for PostgreSQL INTEGER column (2^31-1)"""
129
+
130
+ PositiveInt: TypeAlias = Annotated[int, Field(ge=1, le=INT32_MAX)]
131
+ """Positive integer (1 to 2147483647, fits PostgreSQL INTEGER)"""
132
+
133
+ NonNegativeInt: TypeAlias = Annotated[int, Field(ge=0, le=INT32_MAX)]
134
+ """Non-negative integer (0 to 2147483647, fits PostgreSQL INTEGER)"""
135
+
136
+ PositiveBigInt: TypeAlias = Annotated[int, Field(ge=1, sa_type=BigInteger)]
137
+ """Positive big integer (>=1, BigInteger storage)"""
109
138
 
110
- NonNegativeInt: TypeAlias = Annotated[int, Field(ge=0)]
111
- """Non-negative integer (>=0)"""
139
+ NonNegativeBigInt: TypeAlias = Annotated[int, Field(ge=0, sa_type=BigInteger)]
140
+ """Non-negative big integer (>=0, BigInteger storage)"""
112
141
 
113
142
  PositiveFloat: TypeAlias = Annotated[float, Field(gt=0.0)]
114
143
  """Positive float (>0)"""