Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 172 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Generates GraphQL types, inputs, queries and resolvers directly from SQLAlchemy
- [Mapping SQLAlchemy Models](#mapping-sqlalchemy-models)
- [Resolver Generation](#resolver-generation)
- [Pagination](#pagination)
- [Ordering](#ordering)
- [Filtering](#filtering)
- [Aggregations](#aggregations)
- [Mutations](#mutations)
Expand Down Expand Up @@ -567,20 +568,25 @@ def farms(self) -> str:
Strawchemy supports offset-based pagination out of the box.

<details>
<summary>Pagination example:</summary>
<summary>Pagination examples</summary>

Enable pagination on fields:
### Field-Level Pagination

Enable pagination on specific fields:

```python
from strawchemy.schema.pagination import DefaultOffsetPagination


@strawberry.type
class Query:
# Enable pagination with default settings
# Enable pagination with default settings (limit=100, offset=0)
users: list[UserType] = strawchemy.field(pagination=True)
# Customize pagination defaults
users_custom_pagination: list[UserType] = strawchemy.field(pagination=DefaultOffsetPagination(limit=20))

# Customize pagination defaults for this specific field
users_custom: list[UserType] = strawchemy.field(
pagination=DefaultOffsetPagination(limit=20, offset=10)
)
```

In your GraphQL queries, you can use the `offset` and `limit` parameters:
Expand All @@ -594,10 +600,40 @@ In your GraphQL queries, you can use the `offset` and `limit` parameters:
}
```

You can also enable pagination for nested relationships:
### Config-Level Pagination

Enable pagination globally for all list fields:

```python
@strawchemy.type(User, include="all", child_pagination=True)
from strawchemy import Strawchemy, StrawchemyConfig

strawchemy = Strawchemy(
StrawchemyConfig(
"postgresql",
pagination="all", # Enable on all list fields
pagination_default_limit=100, # Default limit
pagination_default_offset=0, # Default offset
)
)


@strawchemy.type(User, include="all")
class UserType:
pass


@strawberry.type
class Query:
# This field automatically has pagination enabled
users: list[UserType] = strawchemy.field()
```

### Type-level pagination

Enable pagination for nested relationships from a specific type:

```python
@strawchemy.type(User, include="all", paginate="all")
class UserType:
pass
```
Expand All @@ -619,6 +655,118 @@ Then in your GraphQL queries:

</details>

## Ordering

Strawchemy provides flexible ordering capabilities for query results.

<details>
<summary>Ordering examples</summary>

### Field-Level Ordering

Define ordering inputs and use them on specific fields:

```python
# Create order by input
@strawchemy.order(User, include="all")
class UserOrderBy:
pass


@strawberry.type
class Query:
users: list[UserType] = strawchemy.field(order_by=UserOrderBy)
```

Query with ordering:

```graphql
{
users(orderBy: [{ name: ASC }, { createdAt: DESC }]) {
id
name
createdAt
}
}
```

Available ordering options:

- `ASC` - Ascending order
- `DESC` - Descending order
- `ASC_NULLS_FIRST` - Ascending with nulls first
- `ASC_NULLS_LAST` - Ascending with nulls last
- `DESC_NULLS_FIRST` - Descending with nulls first
- `DESC_NULLS_LAST` - Descending with nulls last

### Type-Level Ordering

Enable ordering automatically on a type:

```python
@strawchemy.type(User, include="all", order="all")
class UserType:
pass
```

This automatically generates and applies an order by input for all fields using this type.

### Config-Level Ordering

Enable ordering globally for all list fields:

```python
from strawchemy import Strawchemy, StrawchemyConfig

strawchemy = Strawchemy(
StrawchemyConfig(
"postgresql",
order_by="all", # Enable ordering on all list fields
)
)


@strawchemy.type(User, include="all")
class UserType:
pass


@strawberry.type
class Query:
# This field automatically has ordering enabled
users: list[UserType] = strawchemy.field()
```

With this configuration, all list fields will automatically have an `orderBy` argument without needing to specify it per
field.

### Nested Relationship Ordering

Order nested relationships:

```python
@strawchemy.type(User, include="all", order="all")
class UserType:
pass
```

Query with nested ordering:

```graphql
{
users(orderBy: [{ name: ASC }]) {
id
name
posts(orderBy: [{ title: ASC }]) {
id
title
}
}
}
```

</details>

## Filtering

Strawchemy provides powerful filtering capabilities.
Expand Down Expand Up @@ -1950,18 +2098,20 @@ Configuration is made by passing a `StrawchemyConfig` to the `Strawchemy` instan

### Configuration Options

| Option | Type | Default | Description |
|----------------------------|-------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `dialect` | `SupportedDialect` | | Database dialect to use. Supported dialects are "postgresql", "mysql", "sqlite". |
| `session_getter` | `Callable[[Info], Session]` | `default_session_getter` | Function to retrieve SQLAlchemy session from strawberry `Info` object. By default, it retrieves the session from `info.context.session`. |
| `auto_snake_case` | `bool` | `True` | Automatically convert snake cased names to camel case in GraphQL schema. |
| `repository_type` | `type[Repository] \| StrawchemySyncRepository` | `StrawchemySyncRepository` | Repository class to use for auto resolvers. |
| `filter_overrides` | `OrderedDict[tuple[type, ...], type[SQLAlchemyFilterBase]]` | `None` | Override default filters with custom filters. This allows you to provide custom filter implementations for specific column types. |
| `execution_options` | `dict[str, Any]` | `None` | SQLAlchemy execution options for repository operations. These options are passed to the SQLAlchemy `execution_options()` method. |
| `pagination_default_limit` | `int` | `100` | Default pagination limit when `pagination=True`. |
| `pagination` | `bool` | `False` | Enable/disable pagination on list resolvers by default. |
| `default_id_field_name` | `str` | `"id"` | Name for primary key fields arguments on primary key resolvers. |
| `deterministic_ordering` | `bool` | `True` | Force deterministic ordering for list resolvers. |
| Option | Type | Default | Description |
|-----------------------------|-------------------------------------------------------------|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `dialect` | `SupportedDialect` | | Database dialect to use. Supported dialects are "postgresql", "mysql", "sqlite". |
| `session_getter` | `Callable[[Info], Session]` | `default_session_getter` | Function to retrieve SQLAlchemy session from strawberry `Info` object. By default, it retrieves the session from `info.context.session`. |
| `auto_snake_case` | `bool` | `True` | Automatically convert snake cased names to camel case in GraphQL schema. |
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix hyphenation in configuration table.

"snake cased names" should be hyphenated as "snake-cased names" for proper English grammar.

🔎 Proposed fix
-| `auto_snake_case`           | `bool`                                                      | `True`                     | Automatically convert snake cased names to camel case in GraphQL schema.                                                                 |
+| `auto_snake_case`           | `bool`                                                      | `True`                     | Automatically convert snake-cased names to camel case in GraphQL schema.                                                                 |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `auto_snake_case` | `bool` | `True` | Automatically convert snake cased names to camel case in GraphQL schema. |
| `auto_snake_case` | `bool` | `True` | Automatically convert snake-cased names to camel case in GraphQL schema. |
🧰 Tools
🪛 LanguageTool

[grammar] ~2105-~2105: Use a hyphen to join words.
Context: ... | Automatically convert snake cased names to camel case in GraphQL sch...

(QB_NEW_EN_HYPHEN)

🤖 Prompt for AI Agents
In README.md around line 2105, the phrase "snake cased names" in the
configuration table should be corrected to "snake-cased names"; update the table
cell text to use the hyphenated form "snake-cased names" so the grammar is
correct while preserving the rest of the sentence and formatting.

| `repository_type` | `type[Repository] \| StrawchemySyncRepository` | `StrawchemySyncRepository` | Repository class to use for auto resolvers. |
| `filter_overrides` | `OrderedDict[tuple[type, ...], type[SQLAlchemyFilterBase]]` | `None` | Override default filters with custom filters. This allows you to provide custom filter implementations for specific column types. |
| `execution_options` | `dict[str, Any]` | `None` | SQLAlchemy execution options for repository operations. These options are passed to the SQLAlchemy `execution_options()` method. |
| `default_id_field_name` | `str` | `"id"` | Name for primary key fields arguments on primary key resolvers. |
| `deterministic_ordering` | `bool` | `True` | Force deterministic ordering for list resolvers. |
| `pagination` | `Literal["all"] \| None` | `None` | Enable/disable pagination on list resolvers by default. Set to `"all"` to enable pagination on all list fields. |
| `order_by` | `Literal["all"] \| None` | `None` | Enable/disable order by on list resolvers by default. Set to `"all"` to enable ordering on all list fields. |
| `pagination_default_limit` | `int` | `100` | Default pagination limit when `pagination=True`. |
| `pagination_default_offset` | `int` | `0` | Default pagination offset when `pagination=True`. |

### Example

Expand All @@ -1980,8 +2130,10 @@ strawchemy = Strawchemy(
"postgresql",
session_getter=get_session_from_context,
auto_snake_case=True,
pagination=True,
pagination="all",
pagination_default_limit=50,
pagination_default_offset=0,
order_by="all",
default_id_field_name="pk",
)
)
Expand Down
4 changes: 2 additions & 2 deletions examples/testapp/testapp/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class TicketOrder: ...
class TicketFilter: ...


@strawchemy.type(Ticket, include="all", filter_input=TicketFilter, order_by=TicketOrder, override=True)
@strawchemy.type(Ticket, include="all", filter_input=TicketFilter, order=TicketOrder, override=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for order_by usage in strawchemy.type() and strawchemy.field() calls
echo "=== Type decorator usage with order_by ==="
rg -nP '@strawchemy\.type\([^)]*order_by\s*=' --type=py

echo -e "\n=== Field usage with order_by ==="
rg -nP 'strawchemy\.field\([^)]*order_by\s*=' --type=py

echo -e "\n=== Type decorator usage with order= (new pattern) ==="
rg -nP '@strawchemy\.type\([^)]*order\s*=' --type=py | head -20

echo -e "\n=== Field usage with order= (new pattern) ==="
rg -nP 'strawchemy\.field\([^)]*order\s*=' --type=py | head -20

Repository: gazorby/strawchemy

Length of output: 3173


Update field-level API to use order= parameter for consistency.

The type decorator has been migrated to use order=, but strawchemy.field() calls still use order_by=. This creates an API inconsistency that needs to be resolved:

  • Type-level: Uses order= (e.g., line 23 of examples/testapp/testapp/types.py)
  • Field-level: Still uses order_by= (found in 7 test files including tests/unit/schemas/order/field_order_by_specific_fields.py:18)

Update all field-level calls to use order= to match the type-level API.

🤖 Prompt for AI Agents
In @examples/testapp/testapp/types.py at line 23, The project mixes two ordering
parameters: the type decorator uses order= (e.g., @strawchemy.type(Ticket,
include="all", filter_input=TicketFilter, order=TicketOrder, override=True))
while field-level calls still use order_by=; update all strawchemy.field(...)
usages to replace order_by= with order= so the field-level API matches the
type-level API (search for strawchemy.field in tests and examples, e.g.,
tests/unit/schemas/order/field_order_by_specific_fields.py and
examples/testapp/testapp/types.py references to
Ticket/TicketFilter/TicketOrder).

class TicketType: ...


Expand Down Expand Up @@ -55,7 +55,7 @@ class ProjectOrder: ...
class ProjectFilter: ...


@strawchemy.type(Project, include="all", filter_input=ProjectFilter, order_by=ProjectOrder, override=True)
@strawchemy.type(Project, include="all", filter_input=ProjectFilter, order=ProjectOrder, override=True)
class ProjectType: ...


Expand Down
19 changes: 15 additions & 4 deletions src/strawchemy/config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from strawchemy.utils.strawberry import default_session_getter

if TYPE_CHECKING:
from strawchemy.dto.types import FieldIterable, IncludeFields
from strawchemy.repository.typing import AnySessionGetter, FilterMap
from strawchemy.typing import AnyRepositoryType, SupportedDialect

Expand Down Expand Up @@ -43,14 +44,24 @@ class StrawchemyConfig:
"""Override default filters with custom filters."""
execution_options: dict[str, Any] | None = None
"""SQLAlchemy execution options for strawberry operations."""
pagination_default_limit: int = 100
"""Default pagination limit when `pagination=True`."""
pagination: bool = False
"""Enable/disable pagination on list resolvers."""
default_id_field_name: str = "id"
"""Name for primary key fields arguments on primary key resolvers."""
deterministic_ordering: bool = True
"""Force deterministic ordering for list resolvers."""
include: IncludeFields = "all"
"""Globally included fields."""
exclude: FieldIterable | None = None
"""Globally included fields."""
pagination: IncludeFields | None = None
"""Enable/disable pagination on list resolvers."""
order_by: IncludeFields | None = None
"""Enable/disable order by on list resolvers."""
distinct_on: IncludeFields | None = None
"""Enable/disable order by onelist resolvers."""
pagination_default_limit: int = 100
"""Default pagination limit when `pagination=True`."""
pagination_default_offset: int = 0
"""Default pagination offset when `pagination=True`."""
Comment on lines +62 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inline docstrings reference outdated API.

The docstrings on lines 55 and 57 mention pagination=True, but the new API uses pagination="all".

🔎 Proposed fix
     pagination_default_limit: int = 100
-    """Default pagination limit when `pagination=True`."""
+    """Default pagination limit when pagination is enabled."""
     pagination_default_offset: int = 0
-    """Default pagination offset when `pagination=True`."""
+    """Default pagination offset when pagination is enabled."""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pagination_default_limit: int = 100
"""Default pagination limit when `pagination=True`."""
pagination_default_offset: int = 0
"""Default pagination offset when `pagination=True`."""
pagination_default_limit: int = 100
"""Default pagination limit when pagination is enabled."""
pagination_default_offset: int = 0
"""Default pagination offset when pagination is enabled."""
🤖 Prompt for AI Agents
In src/strawchemy/config/base.py around lines 54 to 57, the inline docstrings
reference the outdated API value `pagination=True`; update both docstrings to
reference the new API value `pagination="all"` (e.g., change `when
`pagination=True`` to `when `pagination="all"``) so they accurately reflect the
current parameter semantics and quoting style.

Comment on lines +52 to +65
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix documentation errors in field docstrings.

There are two issues in the docstrings:

  1. Line 54: The exclude field docstring says "Globally included fields" but should describe excluded fields
  2. Line 60: Typo "onelist" should be "on list"
📝 Proposed fix
     include: IncludeFields = "all"
     """Globally included fields."""
     exclude: FieldIterable | None = None
-    """Globally included fields."""
+    """Globally excluded fields."""
     pagination: IncludeFields | None = None
     """Enable/disable pagination on list resolvers."""
     order_by: IncludeFields | None = None
     """Enable/disable order by on list resolvers."""
     distinct_on: IncludeFields | None = None
-    """Enable/disable order by onelist resolvers."""
+    """Enable/disable distinct on for list resolvers."""
     pagination_default_limit: int = 100
     """Default pagination limit when `pagination=True`."""
     pagination_default_offset: int = 0
     """Default pagination offset when `pagination=True`."""
🤖 Prompt for AI Agents
In @src/strawchemy/config/base.py around lines 51 - 64, Update the field
docstrings to correct their descriptions: change the docstring for the exclude
field (symbol: exclude: FieldIterable | None) from "Globally included fields."
to something like "Globally excluded fields." and fix the typo in the
distinct_on field docstring (symbol: distinct_on: IncludeFields | None) from
"Enable/disable order by onelist resolvers." to "Enable/disable order by on list
resolvers."


inspector: SQLAlchemyGraphQLInspector = field(init=False)

Expand Down
26 changes: 18 additions & 8 deletions src/strawchemy/dto/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
DTOMissing,
DTOSkip,
DTOUnset,
ExcludeFields,
FieldIterable,
IncludeFields,
Purpose,
PurposeConfig,
Expand Down Expand Up @@ -392,17 +392,26 @@ def should_exclude_field(
explictly_excluded = node.is_root and field.model_field_name in dto_config.exclude
explicitly_included = node.is_root and field.model_field_name in dto_config.include

# Exclude fields not present in init if purpose is write
if dto_config.purpose is Purpose.WRITE and not explicitly_included:
explictly_excluded = explictly_excluded or not field.init
globally_excluded = field.model_field_name in dto_config.global_exclude
globally_included = field.model_field_name in dto_config.global_include

if dto_config.include == "all" and not explictly_excluded:
explicitly_included = True
explicitly_included = globally_included = True

if dto_config.global_include == "all" and not globally_excluded:
globally_included = True

excluded = dto_config.purpose not in field.allowed_purposes

# Exclude fields not present in init if purpose is write
if dto_config.purpose is Purpose.WRITE and not (explicitly_included or globally_included):
excluded = excluded or not field.init

if node.is_root:
excluded = excluded or (explictly_excluded or not explicitly_included)
else:
excluded = excluded or explictly_excluded
excluded = excluded or (globally_excluded or not globally_included)

Comment on lines +396 to +415
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Root DTOs still ignore global_include / global_exclude.

Lines 396-403 compute the global flags, but the root branch only checks explicitly_*. A root DTO with include="all" will still expose a field that was globally excluded, and a root field present only in global_include never becomes selectable.

Proposed fix
-        if dto_config.include == "all" and not explictly_excluded:
+        if dto_config.include == "all" and not (explictly_excluded or globally_excluded):
             explicitly_included = globally_included = True
@@
-        if node.is_root:
-            excluded = excluded or (explictly_excluded or not explicitly_included)
+        if node.is_root:
+            excluded = excluded or (
+                explictly_excluded
+                or globally_excluded
+                or not (explicitly_included or globally_included)
+            )
         else:
             excluded = excluded or (globally_excluded or not globally_included)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/strawchemy/dto/base.py` around lines 396 - 415, The root-branch exclusion
logic ignores global_include/global_exclude; update the node.is_root branch to
also respect globally_included/globally_excluded (same rules as the non-root
branch). Specifically, change the node.is_root clause that sets excluded to
combine explicit and global flags (e.g., excluded = excluded or
(explictly_excluded or globally_excluded or not (explicitly_included or
globally_included))). Also fix the inconsistent variable name for the explicit
exclusion/inclusion if needed (explictly_excluded vs explicitly_excluded) so the
correct flags are evaluated.

return not has_override and excluded

def _resolve_basic_type(self, field: DTOFieldDefinition[ModelT, ModelFieldT], dto_config: DTOConfig) -> Any:
Expand Down Expand Up @@ -626,6 +635,7 @@ def factory(
raise_if_no_fields: bool = False,
tags: set[str] | None = None,
backend_kwargs: dict[str, Any] | None = None,
no_cache: bool = False,
**kwargs: Any,
) -> type[DTOBaseT]:
"""Build a Data transfer object (DTO) from an SQAlchemy model."""
Expand All @@ -640,7 +650,7 @@ def factory(
if dto_config.scope == "global":
self._scoped_dto_names[self._scoped_cache_key(model, dto_config)] = name

if (dto := self._dto_cache.get(cache_key)) or (dto := self._dto_cache.get(scoped_cache_key)):
if not no_cache and ((dto := self._dto_cache.get(cache_key)) or (dto := self._dto_cache.get(scoped_cache_key))):
return self.backend.copy(dto, name) if node.is_root else dto

dto = self._factory(
Expand Down Expand Up @@ -685,7 +695,7 @@ def decorator(
model: type[ModelT],
purpose: Purpose,
include: IncludeFields | None = None,
exclude: ExcludeFields | None = None,
exclude: FieldIterable | None = None,
partial: bool | None = None,
type_map: Mapping[Any, Any] | None = None,
aliases: Mapping[str, str] | None = None,
Expand Down
4 changes: 3 additions & 1 deletion src/strawchemy/dto/strawberry.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ class _Key(Generic[T]):
string.
"""

separator: str = ":"
__slots__ = ("_key",)

separator: ClassVar[str] = ":"
Comment on lines +133 to +135
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the _Key class definition around lines 132-134
sed -n '120,160p' src/strawchemy/dto/strawberry.py

Repository: gazorby/strawchemy

Length of output: 1545


🏁 Script executed:

# Find all subclasses of _Key in the file
rg -n 'class\s+\w+.*\(_Key\)' src/strawchemy/dto/strawberry.py -A 5

Repository: gazorby/strawchemy

Length of output: 44


🏁 Script executed:

# Check the DTOKey class mentioned at line 179
sed -n '175,210p' src/strawchemy/dto/strawberry.py

Repository: gazorby/strawchemy

Length of output: 1081


🏁 Script executed:

# Search for all classes that inherit from _Key
rg -n '_Key' src/strawchemy/dto/strawberry.py | grep -E '(class|:)'

Repository: gazorby/strawchemy

Length of output: 124


🏁 Script executed:

# Check for dynamic attribute assignments or setattr patterns
rg -n 'setattr|__dict__|self\.\w+\s*=' src/strawchemy/dto/strawberry.py | head -20

Repository: gazorby/strawchemy

Length of output: 340


🏁 Script executed:

# Look at git history to verify if separator was changed from instance to ClassVar
git log -p --all -S 'separator' -- src/strawchemy/dto/strawberry.py | head -100

Repository: gazorby/strawchemy

Length of output: 44


🏁 Script executed:

# Check if DTOKey or its instances need to store additional attributes
rg -n 'DTOKey' src/strawchemy/dto/strawberry.py -B 2 -A 10 | head -50

Repository: gazorby/strawchemy

Length of output: 2047


🏁 Script executed:

# Check if there are any tests that might reveal the actual impact
find . -name '*test*.py' -type f | head -10

Repository: gazorby/strawchemy

Length of output: 474


🏁 Script executed:

# Look for all usages of DTOKey to understand its instantiation patterns
rg -n 'DTOKey' --type py | head -20

Repository: gazorby/strawchemy

Length of output: 1932


🏁 Script executed:

# Verify that DTOKey doesn't add any additional attributes by checking all its methods
sed -n '179,200p' src/strawchemy/dto/strawberry.py

Repository: gazorby/strawchemy

Length of output: 738


🏁 Script executed:

# Check if DTOKey instances are created in large quantities (memory-critical context)
rg -n 'DTOKey\(' src/strawchemy -c --type py | sort -rn

Repository: gazorby/strawchemy

Length of output: 140


🏁 Script executed:

# Look at the parent _Key's full implementation to understand inheritance model
sed -n '114,176p' src/strawchemy/dto/strawberry.py

Repository: gazorby/strawchemy

Length of output: 2205


🏁 Script executed:

# Verify if there are any other subclasses of _Key besides DTOKey
rg -n 'class\s+\w+.*\(_Key' src/ --type py

Repository: gazorby/strawchemy

Length of output: 129


🏁 Script executed:

# Check if separator usage would be different if it were an instance variable vs ClassVar
rg -n 'separator' src/strawchemy/dto/strawberry.py -B 1 -A 1

Repository: gazorby/strawchemy

Length of output: 479


🏁 Script executed:

# Verify Python behavior: check if empty __slots__ in subclass prevents __dict__
python3 << 'EOF'
# Test: Does subclass without __slots__ get a __dict__?
class Parent:
    __slots__ = ("x",)

class Child(Parent):
    pass

c = Child()
print("Child has __dict__:", hasattr(c, '__dict__'))
print("Parent __slots__:", Parent.__slots__)
print("Child __slots__:", getattr(Child, '__slots__', 'Not defined'))

# With __slots__ = () in child
class ChildWithSlots(Parent):
    __slots__ = ()

c2 = ChildWithSlots()
print("\nChildWithSlots has __dict__:", hasattr(c2, '__dict__'))
print("ChildWithSlots __slots__:", ChildWithSlots.__slots__)
EOF

Repository: gazorby/strawchemy

Length of output: 148


🏁 Script executed:

# Simpler test for Python __slots__ behavior
python3 << 'EOF'
class Parent:
    __slots__ = ("x",)

class Child(Parent):
    pass

try:
    c = Child()
    c.y = 5
    print("Child WITHOUT __slots__ definition can add attributes: True")
except AttributeError:
    print("Child WITHOUT __slots__ definition blocked attribute addition")
EOF

Repository: gazorby/strawchemy

Length of output: 121


🏁 Script executed:

# Check how many DTOKey instances are typically created in real usage
rg -n 'DTOKey\(' src/strawchemy/schema/factories/ -B 2 -A 2 | head -30

Repository: gazorby/strawchemy

Length of output: 2055


🏁 Script executed:

# Verify if DTOKey instances are stored in long-lived data structures
rg -n '__strawchemy_field_map__' src/strawchemy -A 1 | head -20

Repository: gazorby/strawchemy

Length of output: 1639


Fix DTOKey to define __slots__ to complete the memory optimization.

The addition of __slots__ in _Key prevents per-instance __dict__ creation, and using separator as a ClassVar properly shares it across instances. However, DTOKey (line 179) does not define __slots__, which causes each instance to gain a __dict__ despite inheriting from _Key, defeating the optimization. Since DTOKey instances are stored as keys in __strawchemy_field_map__ dictionaries and persist for the lifetime of the schema, adding __slots__ = () to DTOKey will complete the optimization.

🤖 Prompt for AI Agents
In @src/strawchemy/dto/strawberry.py around lines 132 - 134, DTOKey is missing a
__slots__ declaration so instances still get per-instance __dict__ despite
inheriting from _Key; add a class-level __slots__ = () to the DTOKey class (the
DTOKey class definition referenced in the diff) to prevent per-instance
dictionaries and complete the memory optimization already begun by _Key and the
separator ClassVar.


def __init__(self, components: Sequence[T | str] | str | None = None) -> None:
self._key: str = ""
Expand Down
Loading
Loading