Skip to main content
This document compares stash-graphql-client to alternative approaches and libraries to help you understand when and why to use it.

vs Raw GraphQL with gql

Side-by-Side Example

Raw gql approach:
from gql import gql, Client
from gql.transport.httpx import HTTPXAsyncTransport

# Setup client
transport = HTTPXAsyncTransport(url="http://localhost:9999/graphql")
client = Client(transport=transport)

# Define query string (error-prone, no validation)
query = gql("""
    query FindScene($id: ID!) {
        findScene(id: $id) {
            id
            title
            rating100
            studio { id name }
            performers { id name }
        }
    }
""")

# Execute query
result = await client.execute(query, variable_values={"id": "123"})

# Navigate dict (no type safety)
scene_data = result["findScene"]
title = scene_data["title"]  # Typo in key? Runtime error!
rating = scene_data.get("rating100")  # Optional chaining

# Update requires manual mutation
mutation = gql("""
    mutation UpdateScene($input: SceneUpdateInput!) {
        sceneUpdate(input: $input) {
            id
            title
            rating100
        }
    }
""")

# Build input dict (must include all fields or risk overwriting)
update_result = await client.execute(mutation, variable_values={
    "input": {
        "id": "123",
        "title": "New Title",
        "rating100": 90,
        # Did we forget any fields? Hope not!
    }
})
stash-graphql-client approach:
from stash_graphql_client import StashContext
from stash_graphql_client.types import UNSET, is_set

# Setup with context manager
async with StashContext(conn={"Host": "localhost", "Port": 9999}) as client:
    # Find scene (type-safe, IDE autocomplete)
    scene = await client.find_scene("123")

    # Access properties (validated Pydantic models)
    title = scene.title  # Type-checked, autocomplete works
    rating = scene.rating100  # Optional fields handled automatically

    # Related objects
    if is_set(scene.studio):
        print(f"Studio: {scene.studio.name}")

    # Update (only changed fields sent)
    scene.title = "New Title"
    scene.rating100 = 90
    scene.details = UNSET  # Don't touch this field
    await scene.save(client)  # Mutation generated automatically

Feature Comparison

FeatureRaw gqlstash-graphql-client
Query constructionManual string buildingMethod calls (client.find_scene())
Type safetyNone (runtime dicts)Full Pydantic validation
IDE autocompleteNoYes (all fields, methods)
Response parsingDict navigationPydantic models
Mutation buildingManual GraphQL strings.save() / .delete() methods
Partial updatesInclude all fields or manual trackingUNSET pattern (automatic)
Object identityManual trackingAutomatic identity map
Relationship handlingManual dict navigationPydantic models with bidirectional sync
Error detectionRuntime (wrong keys, types)Development time (Pydantic validation)
Code volume~40-50 lines for basic CRUD~5-10 lines for same operations

When to Use Raw gql

✅ Use raw gql when:
  • Making a few simple queries
  • Need maximum control over GraphQL
  • Working with non-Stash GraphQL APIs
  • Building a custom abstraction layer
❌ Avoid raw gql when:
  • Building tools with complex entity relationships
  • Need type safety and validation
  • Making many queries for same entities
  • Want ORM-like convenience

vs Apollo Client (JavaScript)

Apollo Client is the most popular GraphQL client for JavaScript/TypeScript. Here’s how stash-graphql-client compares:

Architecture Comparison

ComponentApollo Clientstash-graphql-client
LanguageJavaScript/TypeScriptPython
Type systemTypeScript (compile-time)Pydantic (runtime validation)
CachingInMemoryCache (separate layer)Wrap validators (integrated)
Cache normalizationRefs to normalized objectsDirect object references
Cache writescache.writeQuery()Automatic in constructor
Cache readscache.readQuery()Automatic object reuse
Optimistic updatesManual configurationNot built-in (manual for now)
SubscriptionsVia graphql-wsVia websockets transport

Cache Implementation

Apollo Client:
// Separate cache layer
const cache = new InMemoryCache({
  typePolicies: {
    Scene: {
      keyFields: ["id"],
      fields: {
        studio: {
          merge(existing, incoming) {
            return incoming;
          },
        },
      },
    },
  },
});

// Manual cache writes
cache.writeQuery({
  query: SCENE_QUERY,
  data: { findScene: updatedScene },
});

// Manual cache reads
const scene = cache.readQuery({
  query: SCENE_QUERY,
  variables: { id: "123" },
});
stash-graphql-client:
# Caching built into model construction
scene1 = Scene.from_dict({"id": "123", "title": "Test"})
scene2 = Scene.from_dict({"id": "123", "title": "Test"})
assert scene1 is scene2  # Automatic caching!

# No manual cache API needed

Type Safety

Apollo with TypeScript:
// Generated types from schema
interface Scene {
  id: string;
  title: string;
  rating100?: number;
}

// Compile-time checking only
const scene: Scene = await client.query({...});
scene.title = "New";  // ✅ TypeScript happy
// Runtime: No validation that server actually sent the right shape
stash-graphql-client:
# Pydantic models with runtime validation
scene = Scene(title="New", rating100=150)  # ❌ Validation error at runtime!
# rating100 must be 0-100

# Runtime type coercion
scene = Scene(title="New", rating100="85")  # ✅ Converts str -> int
assert scene.rating100 == 85

When to Use Apollo vs stash-graphql-client

Use Apollo when:
  • Building web applications in JavaScript/TypeScript
  • Need React integration (React Query, hooks)
  • Working with any GraphQL API
  • Need optimistic UI updates
  • Want community plugins/extensions
Use stash-graphql-client when:
  • Building Python tools/scripts
  • Working specifically with Stash
  • Need runtime type validation
  • Want ORM-like entity management
  • Prefer identity map over normalized cache

vs SQLAlchemy

SQLAlchemy is Python’s most popular ORM for SQL databases. stash-graphql-client borrows patterns from SQLAlchemy but applies them to GraphQL.

Conceptual Mapping

SQLAlchemy Conceptstash-graphql-client Equivalent
SessionStashEntityStore
Session.identity_mapWrap validator cache
Model.query.filter()store.find(Type, field__modifier=value)
relationship()RelationshipMetadata + auto-sync
back_populatesinverse_query_field
Lazy loadingField-aware population
session.add()Automatic in constructor
session.commit().save(client)
session.flush()No direct equivalent (saves immediately)

Side-by-Side Example

SQLAlchemy:
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import Session, relationship, declarative_base

Base = declarative_base()

class Scene(Base):
    __tablename__ = 'scenes'
    id = Column(Integer, primary_key=True)
    title = Column(String)
    rating100 = Column(Integer)
    studio_id = Column(Integer, ForeignKey('studios.id'))

    # Relationship with backref
    studio = relationship("Studio", back_populates="scenes")

# Usage
engine = create_engine("sqlite:///stash.db")
session = Session(engine)

# Query
scene = session.query(Scene).filter(Scene.id == 123).first()
top_rated = session.query(Scene).filter(Scene.rating100 >= 80).all()

# Update
scene.title = "Updated"
session.commit()

# Identity map
scene1 = session.query(Scene).filter(Scene.id == 123).first()
scene2 = session.query(Scene).filter(Scene.id == 123).first()
assert scene1 is scene2  # True - same object!
stash-graphql-client:
from stash_graphql_client import StashContext, StashEntityStore
from stash_graphql_client.types import Scene

async with StashContext(conn={...}) as client:
    store = StashEntityStore(client)

    # Query
    scene = await store.get(Scene, "123")
    top_rated = await store.find(Scene, rating100__gte=80)

    # Update
    scene.title = "Updated"
    await scene.save(client)

    # Identity map
    scene1 = await store.get(Scene, "123")
    scene2 = await store.get(Scene, "123")
    assert scene1 is scene2  # True - same object!

Key Differences

AspectSQLAlchemystash-graphql-client
BackendSQL databases (Postgres, MySQL, SQLite)GraphQL API (Stash)
Query languageSQLAlchemy expressions / SQLGraphQL / Django-style kwargs
Identity mapSession.identity_map dictPydantic wrap validators
Asyncasync_scoped_session (extension)Native async throughout
Type validationOptional (via type hints)Required (Pydantic runtime)
RelationshipsForeign keys + relationship()RelationshipMetadata + query strategies
Lazy loadingDatabase query when accessedField-aware populate
Transactionssession.begin() / commit() / rollback()No transactions (GraphQL mutations)
Schema changesAlembic migrationsN/A (server handles schema)

Advantages of stash-graphql-client

Runtime type validation - Pydantic catches errors immediately ✅ UNSET pattern - Distinguish unqueried from null (SQL can’t do this) ✅ Async-first - Not retrofitted like SQLAlchemy’s async support ✅ No ORM impedance mismatch - GraphQL already returns objects

Advantages of SQLAlchemy

Transactions - ACID guarantees, rollback support ✅ Complex queries - Joins, subqueries, window functions ✅ Database portability - Works with any SQL database ✅ Mature ecosystem - 15+ years of development

vs Django ORM

Django ORM is built into the Django web framework but can be used standalone.

Filter Syntax Comparison

Django ORM:
from myapp.models import Scene

# Django double-underscore syntax
top_rated = Scene.objects.filter(rating100__gte=80)
unrated = Scene.objects.filter(rating100__isnull=True)
search = Scene.objects.filter(title__icontains="test")
date_range = Scene.objects.filter(
    date__range=("2024-01-01", "2024-12-31")
)

# Chaining filters
results = Scene.objects.filter(
    rating100__gte=80,
    organized=True
).exclude(title__startswith="OLD")
stash-graphql-client:
from stash_graphql_client import StashEntityStore
from stash_graphql_client.types import Scene

store = StashEntityStore(client)

# Same double-underscore syntax!
top_rated = await store.find(Scene, rating100__gte=80)
unrated = await store.find(Scene, rating100__null=True)
search = await store.find(Scene, title__contains="test")
date_range = await store.find(Scene, date__between=("2024-01-01", "2024-12-31"))

# Multiple filters (no exclude yet)
results = await store.find(
    Scene,
    rating100__gte=80,
    organized=True
)

Key Differences

FeatureDjango ORMstash-graphql-client
Filter syntaxfield__modifierSame! field__modifier
Query chaining.filter().filter().exclude()Single find() call
Identity mapImplicit (QuerySet caching)Explicit (StashEntityStore)
Async supportLimited (sync_to_async)Native async
Type validationModel field typesPydantic runtime validation
Partial updatessave(update_fields=[...])UNSET pattern
RelationshipsForeignKey / ManyToManyRelationshipMetadata
DatabaseSQL (Postgres, MySQL, SQLite)GraphQL (Stash)

When to Use Django ORM vs stash-graphql-client

Use Django ORM when:
  • Building Django web applications
  • Working with SQL databases
  • Need transactions and complex joins
  • Want admin interface for free
Use stash-graphql-client when:
  • Working specifically with Stash
  • Building Python tools/scripts (not web apps)
  • Want GraphQL flexibility
  • Need UNSET pattern for sparse updates

When to Use This Library

✅ Use stash-graphql-client when:

  1. Building tools that interact with Stash
    • Media organization scripts
    • Batch processing tools
    • Data migration utilities
    • Custom integrations
  2. You need type safety and validation
    • Catch errors at development time
    • IDE autocomplete for all fields
    • Runtime validation of server responses
  3. You want ORM-like convenience
    • .save() / .delete() methods
    • Relationship helpers
    • Change tracking for partial updates
  4. Working with complex entity relationships
    • Scenes with performers, studios, tags
    • Need bidirectional relationship sync
    • Want object identity across queries
  5. Making many queries for same entities
    • Identity map prevents duplicate objects
    • Read-through caching reduces network requests
    • Field-aware population loads only what’s needed

❌ Don’t use stash-graphql-client when:

  1. Just need a few simple queries
    • Use raw gql library directly
    • Simpler for one-off operations
  2. Not using Python
    • Use GraphQL client for your language
    • Apollo (JS), graphql-ruby (Ruby), etc.
  3. Need to work with multiple GraphQL APIs
    • This is specialized for Stash’s schema
    • Use general-purpose GraphQL client
  4. Memory constrained environment
    • Identity map keeps objects in memory
    • May not be suitable for very large datasets
  5. Need optimistic updates / offline support
    • Not built-in (would need manual implementation)
    • Apollo Client better for this use case

Migration Guide

From Raw gql

Before:
query = gql("query { findScene(id: $id) { ... } }")
result = await client.execute(query, {"id": "123"})
scene_data = result["findScene"]
After:
scene = await client.find_scene("123")
Effort: Low - mostly replacing string queries with method calls

From Apollo Client (JS → Python)

Before (JavaScript):
const { data } = await client.query({
  query: FIND_SCENE,
  variables: { id: "123" },
});
const scene = data.findScene;
After (Python):
scene = await client.find_scene("123")
Effort: Medium - language switch + learning Pydantic patterns

From SQLAlchemy

Before:
session = Session(engine)
scene = session.query(Scene).filter(Scene.id == 123).first()
After:
store = StashEntityStore(client)
scene = await store.get(Scene, "123")
Effort: Low - very similar patterns, main change is async

From Django ORM

Before:
from myapp.models import Scene
scene = Scene.objects.get(id=123)
After:
from stash_graphql_client.types import Scene
scene = await client.find_scene("123")
Effort: Low - filter syntax nearly identical

Summary

stash-graphql-client combines the best patterns from:
  • Apollo Client - GraphQL caching and query management
  • SQLAlchemy - Identity map and session pattern
  • Django ORM - Filter syntax and query building
  • Pydantic - Runtime type validation and models
It’s specifically designed for:
  • Python developers
  • Building tools that interact with Stash
  • Need type safety + ORM convenience + GraphQL flexibility
It’s NOT designed for:
  • General-purpose GraphQL APIs
  • Web applications (use Apollo + React)
  • Maximum control over every GraphQL query

Next Steps