Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
11 changes: 0 additions & 11 deletions .github/dependabot.yml

This file was deleted.

44 changes: 39 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ env:

on:
push:
branches: [main, ci/*, dependabot/*, renovate/*]
branches: [ main, ci/*, dependabot/*, renovate/* ]
tags:
- "v*.*.*"
pull_request:
branches: [main]
branches: [ main ]

concurrency:
group: ${{ github.head_ref || github.run_id }}
Expand Down Expand Up @@ -57,7 +57,7 @@ jobs:

tests:
name: 🔬 ${{ matrix.session.job_name }}
needs: [pre, generate-jobs-tests]
needs: [ pre, generate-jobs-tests ]
if: github.ref_type == 'tag' || needs.pre.outputs.should_skip != 'true'
runs-on: ubuntu-latest
strategy:
Expand Down Expand Up @@ -119,9 +119,42 @@ jobs:
name: test-results-${{ matrix.session.session }}
path: junit-${{ matrix.session.session }}.xml

benchmarks:
name: 📈 Run benchmarks
needs: [ tests, generate-jobs-tests ]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5

- name: Pip and nox cache
id: cache
uses: actions/cache@v4
with:
path: |
~/.cache
key:
${{ runner.os }}-benchmarks-${{ env.pythonLocation }}-${{ hashFiles('**/uv.lock') }}
restore-keys: |
${{ runner.os }}-benchmarks-${{ env.pythonLocation }}

- name: Setup mise
uses: jdx/mise-action@v3
with:
cache: true
log_level: debug

- name: Run the benchmarks
uses: CodSpeedHQ/action@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security (yaml.github-actions.security.third-party-action-not-pinned-to-commit-sha): An action sourced from a third-party repository on GitHub is not pinned to a full length commit SHA. Pinning an action to a full length commit SHA is currently the only way to use an action as an immutable release. Pinning to a particular SHA helps mitigate the risk of a bad actor adding a backdoor to the action's repository, as they would need to generate a SHA-1 collision for a valid Git object payload.

Source: opengrep

with:
mode: instrumentation
run: mise run test:benchmarks
token: ${{ secrets.CODSPEED_TOKEN }} # optional for public repos


upload-coverage:
name: 🆙 Upload Coverage
needs: [tests, generate-jobs-tests]
needs: [ tests, generate-jobs-tests ]
runs-on: ubuntu-latest

steps:
Expand Down Expand Up @@ -155,7 +188,7 @@ jobs:

upload-test-results:
name: 📊 Upload test results
needs: [tests, generate-jobs-tests]
needs: [ tests, generate-jobs-tests ]
runs-on: ubuntu-latest
strategy:
matrix:
Expand Down Expand Up @@ -208,6 +241,7 @@ jobs:
- pre
- tests
- lint
- benchmarks
steps:
- name: Mark workflow as failed if lint or test did not pass or were cancelled
if: contains(fromJson('["failure", "cancelled"]'), needs.tests.result) || contains(fromJson('["failure", "cancelled"]'), needs.lint.result)
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,5 @@ mise.local.toml
.windsurf

CLAUDE.md

.codspeed
11 changes: 11 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,11 @@ description = "Install dependencies"
hide = true
run = "uv sync --all-extras --dev"

[tasks."_install:test"]
description = "Install test dependencies"
hide = true
run = "uv sync --all-extras --only-group test"

[tasks."_install:geo"]
description = "Install dependencies with geo extras"
hide = true
Expand Down Expand Up @@ -149,6 +154,12 @@ description = "Run snapshot-based tests and update snapshots"
depends = "_install"
run = "uv run pytest {{vars.local_pytest_options}} -m snapshot --snapshot-update"

[tasks."test:benchmarks"]
description = "Run benchmarks"
depends = "_install:test"
run = "uv run pytest --codspeed tests/benchmarks"


# ###############
# CI
# ###############
Expand Down
14 changes: 8 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ test = [
"pytest-xdist",
"pytest-databases[postgres,mysql]",
"pytest-lazy-fixtures",
"pytest-codspeed",
"syrupy",
"sqlparse",
"covdefaults",
Expand Down Expand Up @@ -296,12 +297,13 @@ markers = [
"snapshot: snapshot-based tests using syrupy",
"integration: SQLAlchemy integration tests",
"geo: Geoalchemy2 integration tests",
"aiosqlite: SQLite (aiosqlite) Tests",
"sqlite: SQLite Tests",
"asyncmy: MySQL (asyncmy) Tests",
"psycopg_async: SQLAlchemy Postgres (psycopg async) Tests",
"psycopg_sync: SQLAlchemy Postgres (psycopg sync) Tests",
"asyncpg: SQLAlchemy Postgres (asyncpg) Tests",
"aiosqlite: SQLite (aiosqlite) tests",
"sqlite: SQLite tests",
"asyncmy: MySQL (asyncmy) tests",
"psycopg_async: SQLAlchemy Postgres (psycopg async) tests",
"psycopg_sync: SQLAlchemy Postgres (psycopg sync) tests",
"asyncpg: SQLAlchemy Postgres (asyncpg) tests",
"benchmark: Benchmark tests"
]
testpaths = ["tests"]
xfail_strict = true
Expand Down
8 changes: 8 additions & 0 deletions tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ Run tests

#### `[test]`

## `test:benchmarks`

- Depends: _install:test

- **Usage**: `test:benchmarks`

Run benchmarks

## `test:coverage`

- Depends: _install
Expand Down
Empty file added tests/benchmarks/__init__.py
Empty file.
94 changes: 94 additions & 0 deletions tests/benchmarks/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
from __future__ import annotations

from tests.integration.fixtures import (
aiosqlite_engine,
any_query,
any_session,
async_engine,
async_mutation,
async_query,
async_session,
asyncmy_engine,
asyncpg_engine,
before_create_all_statements,
config,
db_features,
dialect,
engine,
fx_metadata,
mapper,
no_session_query,
postgis_service,
postgres_database_service,
psycopg_async_engine,
psycopg_engine,
query_tracker,
raw_arrays,
raw_colors,
raw_date_times,
raw_farms,
raw_fruits,
raw_geo,
raw_geo_flipped,
raw_groups,
raw_intervals,
raw_json,
raw_topics,
raw_users,
seed_db_async,
seed_db_sync,
seed_insert_statements,
session,
sqlite_engine,
sync_mutation,
sync_query,
)

from .fixtures import fx_strawchemy_mysql, fx_strawchemy_postgresql, fx_strawchemy_sqlite

__all__ = (
"aiosqlite_engine",
"any_query",
"any_session",
"async_engine",
"async_mutation",
"async_query",
"async_session",
"asyncmy_engine",
"asyncpg_engine",
"before_create_all_statements",
"config",
"db_features",
"dialect",
"engine",
"fx_metadata",
"fx_strawchemy_mysql",
"fx_strawchemy_postgresql",
"fx_strawchemy_sqlite",
"mapper",
"no_session_query",
"postgis_service",
"postgres_database_service",
"psycopg_async_engine",
"psycopg_engine",
"query_tracker",
"raw_arrays",
"raw_colors",
"raw_date_times",
"raw_farms",
"raw_fruits",
"raw_geo",
"raw_geo_flipped",
"raw_groups",
"raw_intervals",
"raw_json",
"raw_topics",
"raw_users",
"seed_db_async",
"seed_db_sync",
"seed_insert_statements",
"session",
"sqlite_engine",
"sync_mutation",
"sync_query",
)
19 changes: 19 additions & 0 deletions tests/benchmarks/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from __future__ import annotations

import pytest
from strawchemy import Strawchemy


@pytest.fixture(name="strawchemy_postgresql")
def fx_strawchemy_postgresql() -> Strawchemy:
return Strawchemy("postgresql")


@pytest.fixture(name="strawchemy_sqlite")
def fx_strawchemy_sqlite() -> Strawchemy:
return Strawchemy("sqlite")


@pytest.fixture(name="strawchemy_mysql")
def fx_strawchemy_mysql() -> Strawchemy:
return Strawchemy("mysql")
59 changes: 59 additions & 0 deletions tests/benchmarks/test_build_schema.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from pytest_lazy_fixtures import lf
from strawchemy import StrawchemySyncRepository

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Verify public export for StrawchemySyncRepository

Confirm StrawchemySyncRepository is re-exported from the package root; otherwise import from its submodule.

Run:


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail
rg -nP 'class\s+StrawchemySyncRepository\b' -S
rg -nP 'from\s+strawchemy\s+import\s+.*StrawchemySyncRepository|__all__.*StrawchemySyncRepository' src -S

Length of output: 224


Re-export StrawchemySyncRepository from package root or import it from its submodule

StrawchemySyncRepository is defined at src/strawchemy/strawberry/repository/_sync.py and exported in src/strawchemy/strawberry/repository/init.py, but there’s no evidence it’s re-exported from the package root — update tests/benchmarks/test_build_schema.py (line 7) to:
from strawchemy.strawberry.repository import StrawchemySyncRepository
or add to src/strawchemy/init.py:
from .strawberry.repository import StrawchemySyncRepository

🤖 Prompt for AI Agents
In tests/benchmarks/test_build_schema.py around line 7, the test imports
StrawchemySyncRepository from the package root but that symbol is not
re-exported there; either change the import in this test to import
StrawchemySyncRepository from strawchemy.strawberry.repository, or add a
re-export in src/strawchemy/__init__.py that imports StrawchemySyncRepository
from .strawberry.repository so the symbol is available at the package root;
implement one of these two fixes so the import resolves.


from tests.integration.models import User

if TYPE_CHECKING:
from pytest_codspeed import BenchmarkFixture
from strawchemy import Strawchemy


@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_type(strawchemy: Strawchemy) -> None:
strawchemy.type(User, include="all", override=True)(type("UserType", (), {}))

Comment on lines +16 to +20

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bench not recorded: wrap in benchmark fixture

CodSpeed records only benchmark(...) calls. This test currently won’t produce metrics.

Apply:

 @pytest.mark.benchmark(group="schema")
 @pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
-def test_build_user_type(strawchemy: Strawchemy) -> None:
-    strawchemy.type(User, include="all", override=True)(type("UserType", (), {}))
+def test_build_user_type(strawchemy: Strawchemy, benchmark: BenchmarkFixture) -> None:
+    benchmark(lambda: strawchemy.type(User, include="all", override=True)(type("UserType", (), {})))
📝 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
@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_type(strawchemy: Strawchemy) -> None:
strawchemy.type(User, include="all", override=True)(type("UserType", (), {}))
@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_type(strawchemy: Strawchemy, benchmark: BenchmarkFixture) -> None:
benchmark(lambda: strawchemy.type(User, include="all", override=True)(type("UserType", (), {})))
🤖 Prompt for AI Agents
In tests/benchmarks/test_build_schema.py around lines 16 to 20, the test
currently calls strawchemy.type(...) directly so CodSpeed won’t record anything;
change the test signature to accept the benchmark fixture and wrap the operation
in a benchmark call (e.g., def test_build_user_type(strawchemy: Strawchemy,
benchmark): and then call benchmark(lambda: strawchemy.type(User, include="all",
override=True)(type("UserType", (), {})))) so the benchmark framework actually
records the metric.


@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_filter(strawchemy: Strawchemy) -> None:
strawchemy.filter(User, include="all", override=True)(type("UserFilter", (), {}))

Comment on lines +22 to +26

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bench not recorded: wrap filter build in benchmark

Same issue as above.

Apply:

 @pytest.mark.benchmark(group="schema")
 @pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
-def test_build_user_filter(strawchemy: Strawchemy) -> None:
-    strawchemy.filter(User, include="all", override=True)(type("UserFilter", (), {}))
+def test_build_user_filter(strawchemy: Strawchemy, benchmark: BenchmarkFixture) -> None:
+    benchmark(lambda: strawchemy.filter(User, include="all", override=True)(type("UserFilter", (), {})))
📝 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
@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_filter(strawchemy: Strawchemy) -> None:
strawchemy.filter(User, include="all", override=True)(type("UserFilter", (), {}))
@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_filter(strawchemy: Strawchemy, benchmark: BenchmarkFixture) -> None:
benchmark(lambda: strawchemy.filter(User, include="all", override=True)(type("UserFilter", (), {})))
🤖 Prompt for AI Agents
In tests/benchmarks/test_build_schema.py around lines 22-26, the filter build
call is executed directly so the benchmark framework doesn't record it; change
the test to accept the pytest-benchmark fixture (add a benchmark parameter) and
wrap the construction in a benchmark call, e.g. call benchmark(lambda:
strawchemy.filter(User, include="all", override=True)(type("UserFilter", (),
{}))), so the benchmark measures the filter build rather than running it outside
the benchmark harness.


@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_create_input(strawchemy: Strawchemy) -> None:
strawchemy.create_input(User, include="all", override=True)(type("UserFilter", (), {}))


@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_pk_update_input(strawchemy: Strawchemy) -> None:
strawchemy.create_input(User, include="all", override=True)(type("UserFilter", (), {}))


@pytest.mark.benchmark(group="schema")
@pytest.mark.parametrize("strawchemy", [lf("strawchemy_postgresql"), lf("strawchemy_sqlite"), lf("strawchemy_mysql")])
def test_build_user_query_field(strawchemy: Strawchemy, benchmark: BenchmarkFixture) -> None:
@strawchemy.filter(User, include="all", override=True)
class UserFilter: ...

@strawchemy.order(User, include="all", override=True)
class UserOrderBy: ...

@strawchemy.distinct_on(User, include="all", override=True)
class UserDistinctOn: ...

benchmark(
lambda: strawchemy.field(
filter_input=UserFilter,
order_by=UserOrderBy,
distinct_on=UserDistinctOn,
repository_type=StrawchemySyncRepository,
)
)
23 changes: 23 additions & 0 deletions tests/benchmarks/test_queries.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from __future__ import annotations

import pytest

from tests.integration.typing import RawRecordData
from tests.typing import AnyQueryExecutor
from tests.utils import maybe_async


@pytest.mark.benchmark(group="queries")
async def test_single(any_query: AnyQueryExecutor, raw_users: RawRecordData) -> None:
await maybe_async(
any_query(
"""
query GetUser($id: Int!) {
user(id: $id) {
name
}
}
""",
{"id": raw_users[0]["id"]},
)
)
Loading
Loading