diff --git a/docs/examples/usage/usage_data_flow_1.py b/docs/examples/usage/usage_data_flow_1.py new file mode 100644 index 00000000..272e7165 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_1.py @@ -0,0 +1,19 @@ +"""Example 1: Direct SQL Creation with positional and named parameters.""" + +__all__ = ("test_direct_sql_creation",) + + +def test_direct_sql_creation() -> None: + """Test creating SQL statements with different parameter styles.""" + # start-example + from sqlspec import SQL + + # Raw SQL string with positional parameters + sql = SQL("SELECT * FROM users WHERE id = ?", 1) + + # Named parameters + sql = SQL("SELECT * FROM users WHERE email = :email", email="user@example.com") + # end-example + + # Verify SQL objects were created + assert sql is not None diff --git a/docs/examples/usage/usage_data_flow_10.py b/docs/examples/usage/usage_data_flow_10.py new file mode 100644 index 00000000..89f0bf08 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_10.py @@ -0,0 +1,27 @@ +"""Example 10: SQLite Driver Execution implementation.""" + +__all__ = ("test_sqlite_driver_pattern",) + + +def test_sqlite_driver_pattern() -> None: + """Test SQLite driver execution pattern.""" + # start-example + from typing import Any + + from sqlspec.driver import ExecutionResult, SyncDriverAdapterBase + + class SqliteDriver(SyncDriverAdapterBase): + def _execute_statement(self, cursor: Any, statement: Any) -> ExecutionResult: + sql, params = self._get_compiled_sql(statement) + cursor.execute(sql, params or ()) + return self.create_execution_result(cursor) + + def _execute_many(self, cursor: Any, statement: Any) -> ExecutionResult: + sql, params = self._get_compiled_sql(statement) + cursor.executemany(sql, params) + return self.create_execution_result(cursor) + + # end-example + + # Verify class was defined + assert SqliteDriver is not None diff --git a/docs/examples/usage/usage_data_flow_11.py b/docs/examples/usage/usage_data_flow_11.py new file mode 100644 index 00000000..919741ce --- /dev/null +++ b/docs/examples/usage/usage_data_flow_11.py @@ -0,0 +1,27 @@ +"""Example 11: SQLResult Object.""" + +__all__ = ("test_sql_result_object",) + + +def test_sql_result_object() -> None: + """Test accessing SQLResult object properties.""" + from sqlspec import SQLSpec + from sqlspec.adapters.sqlite import SqliteConfig + + db_manager = SQLSpec() + db = db_manager.add_config(SqliteConfig(pool_config={"database": ":memory:"})) + + # start-example + with db_manager.provide_session(db) as session: + result = session.execute("SELECT 'test' as col1, 'value' as col2") + + # Access result data + result.data # List of dictionaries + result.rows_affected # Number of rows modified (INSERT/UPDATE/DELETE) + result.column_names # Column names for SELECT + result.operation_type # "SELECT", "INSERT", "UPDATE", "DELETE", "SCRIPT" + # end-example + + # Verify result properties + assert result.data is not None + assert result.column_names is not None diff --git a/docs/examples/usage/usage_data_flow_12.py b/docs/examples/usage/usage_data_flow_12.py new file mode 100644 index 00000000..a4be326f --- /dev/null +++ b/docs/examples/usage/usage_data_flow_12.py @@ -0,0 +1,35 @@ +"""Example 12: Convenience Methods.""" + +__all__ = ("test_convenience_methods",) + + +def test_convenience_methods() -> None: + """Test SQLResult convenience methods.""" + from sqlspec import SQLSpec + from sqlspec.adapters.sqlite import SqliteConfig + + db_manager = SQLSpec() + db = db_manager.add_config(SqliteConfig(pool_config={"database": ":memory:"})) + + with db_manager.provide_session(db) as session: + # Create a test table + session.execute("CREATE TABLE test (id INTEGER, name TEXT)") + session.execute("INSERT INTO test VALUES (1, 'Alice')") + + # start-example + result = session.execute("SELECT * FROM test WHERE id = 1") + + # Get exactly one row (raises if not exactly one) + user = result.one() + + # Get one or None + user = result.one_or_none() + + # Get scalar value (first column of first row) + result2 = session.execute("SELECT COUNT(*) FROM test") + count = result2.scalar() + # end-example + + # Verify results + assert user is not None + assert count == 1 diff --git a/docs/examples/usage/usage_data_flow_13.py b/docs/examples/usage/usage_data_flow_13.py new file mode 100644 index 00000000..1d48b63e --- /dev/null +++ b/docs/examples/usage/usage_data_flow_13.py @@ -0,0 +1,43 @@ +"""Example 13: Schema Mapping.""" + +__all__ = ("test_schema_mapping",) + + +def test_schema_mapping() -> None: + """Test mapping results to typed objects.""" + + # start-example + from pydantic import BaseModel + + from sqlspec import SQLSpec + from sqlspec.adapters.sqlite import SqliteConfig + + class User(BaseModel): + id: int + name: str + email: str + is_active: bool | None = True + + db_manager = SQLSpec() + db = db_manager.add_config(SqliteConfig(pool_config={"database": ":memory:"})) + + with db_manager.provide_session(db) as session: + # Create test table + session.execute("CREATE TABLE users (id INTEGER, name TEXT, email TEXT, is_active INTEGER)") + session.execute("INSERT INTO users VALUES (1, 'Alice', 'alice@example.com', 1)") + + # Execute query + result = session.execute("SELECT id, name, email, is_active FROM users") + + # Map results to typed User instances + users: list[User] = result.all(schema_type=User) + + # Or get single typed user + single_result = session.execute("SELECT id, name, email, is_active FROM users WHERE id = ?", 1) + user: User = single_result.one(schema_type=User) # Type-safe! + # end-example + + # Verify typed results + assert len(users) == 1 + assert isinstance(user, User) + assert user.id == 1 diff --git a/docs/examples/usage/usage_data_flow_14.py b/docs/examples/usage/usage_data_flow_14.py new file mode 100644 index 00000000..d66ed984 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_14.py @@ -0,0 +1,18 @@ +"""Example 14: Multi-Tier Caching.""" + +__all__ = ("test_multi_tier_caching",) + + +def test_multi_tier_caching() -> None: + """Test cache types in SQLSpec.""" + # start-example + from sqlglot import Expression + + # Cache types and their purposes: + sql_cache: dict[str, str] = {} # Compiled SQL strings + optimized_cache: dict[str, Expression] = {} # Post-optimization AST + # end-example + + # Verify cache dictionaries were created + assert isinstance(sql_cache, dict) + assert isinstance(optimized_cache, dict) diff --git a/docs/examples/usage/usage_data_flow_15.py b/docs/examples/usage/usage_data_flow_15.py new file mode 100644 index 00000000..222b6ca3 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_15.py @@ -0,0 +1,26 @@ +"""Example 15: Configuration-Driven Processing.""" + +__all__ = ("test_configuration_driven_processing",) + + +def test_configuration_driven_processing() -> None: + """Test StatementConfig for controlling pipeline behavior.""" + # start-example + from sqlspec import ParameterStyle, ParameterStyleConfig, StatementConfig + + config = StatementConfig( + dialect="postgres", + enable_parsing=True, # AST generation + enable_validation=True, # Security/performance checks + enable_transformations=True, # AST transformations + enable_caching=True, # Multi-tier caching + parameter_config=ParameterStyleConfig( + default_parameter_style=ParameterStyle.NUMERIC, has_native_list_expansion=False + ), + ) + # end-example + + # Verify config was created + assert config is not None + assert config.dialect == "postgres" + assert config.enable_parsing is True diff --git a/docs/examples/usage/usage_data_flow_2.py b/docs/examples/usage/usage_data_flow_2.py new file mode 100644 index 00000000..f8da779f --- /dev/null +++ b/docs/examples/usage/usage_data_flow_2.py @@ -0,0 +1,16 @@ +"""Example 2: Using the Query Builder.""" + +__all__ = ("test_query_builder",) + + +def test_query_builder() -> None: + """Test building SQL programmatically.""" + # start-example + from sqlspec import sql + + # Build SQL programmatically + query = sql.select("id", "name", "email").from_("users").where("status = ?", "active") + # end-example + + # Verify query object was created + assert query is not None diff --git a/docs/examples/usage/usage_data_flow_3.py b/docs/examples/usage/usage_data_flow_3.py new file mode 100644 index 00000000..6b830e97 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_3.py @@ -0,0 +1,20 @@ +"""Example 3: From SQL Files.""" + +from pathlib import Path + +__all__ = ("test_sql_file_loader",) + + +def test_sql_file_loader() -> None: + """Test loading SQL from files.""" + # start-example + from sqlspec.loader import SQLFileLoader + + loader = SQLFileLoader() + queries_path = Path(__file__).resolve().parents[1] / "queries" / "users.sql" + loader.load_sql(queries_path) + sql = loader.get_sql("get_user_by_id") + # end-example + + # Verify SQL object was created + assert sql is not None diff --git a/docs/examples/usage/usage_data_flow_4.py b/docs/examples/usage/usage_data_flow_4.py new file mode 100644 index 00000000..0859f919 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_4.py @@ -0,0 +1,19 @@ +"""Example 4: Parameter Extraction.""" + +__all__ = ("test_parameter_extraction",) + + +def test_parameter_extraction() -> None: + """Show how SQL captures positional and named parameters.""" + from sqlspec import SQL + + # start-example + positional = SQL("SELECT * FROM users WHERE id = ? AND status = ?", 1, "active") + positional_map = dict(enumerate(positional.positional_parameters)) + + named = SQL("SELECT * FROM users WHERE email = :email", email="user@example.com") + named_map = named.named_parameters + # end-example + + assert positional_map == {0: 1, 1: "active"} + assert named_map == {"email": "user@example.com"} diff --git a/docs/examples/usage/usage_data_flow_5.py b/docs/examples/usage/usage_data_flow_5.py new file mode 100644 index 00000000..11113197 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_5.py @@ -0,0 +1,16 @@ +"""Example 5: AST Generation with SQLGlot.""" + +__all__ = ("test_ast_generation",) + + +def test_ast_generation() -> None: + """Test parsing SQL into Abstract Syntax Tree.""" + # start-example + import sqlglot + + # Parse SQL into structured AST + expression = sqlglot.parse_one("SELECT * FROM users WHERE id = ?", dialect="sqlite") + # end-example + + # Verify expression was created + assert expression is not None diff --git a/docs/examples/usage/usage_data_flow_6.py b/docs/examples/usage/usage_data_flow_6.py new file mode 100644 index 00000000..1832a393 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_6.py @@ -0,0 +1,18 @@ +"""Example 6: Compilation to target dialect.""" + +__all__ = ("test_compilation",) + + +def test_compilation() -> None: + """Test compiling AST to target SQL dialect.""" + # start-example + import sqlglot + + # Compile AST to target dialect + compiled_sql = sqlglot.parse_one("SELECT * FROM users WHERE id = ?", dialect="sqlite").sql(dialect="postgres") + # Result: "SELECT * FROM users WHERE id = $1" + # end-example + + # Verify compilation produced a string + assert isinstance(compiled_sql, str) + assert "users" in compiled_sql diff --git a/docs/examples/usage/usage_data_flow_7.py b/docs/examples/usage/usage_data_flow_7.py new file mode 100644 index 00000000..c9886fba --- /dev/null +++ b/docs/examples/usage/usage_data_flow_7.py @@ -0,0 +1,25 @@ +"""Example 7: Parameter Processing.""" + +__all__ = ("test_parameter_processing",) + + +def test_parameter_processing() -> None: + """Convert SQLite-style placeholders to PostgreSQL numeric parameters.""" + from sqlspec import SQL, ParameterStyle, ParameterStyleConfig, StatementConfig + + # start-example + statement_config = StatementConfig( + dialect="sqlite", + parameter_config=ParameterStyleConfig( + default_parameter_style=ParameterStyle.NUMERIC, has_native_list_expansion=False + ), + ) + + sql = SQL("SELECT * FROM users WHERE id = ? AND status = ?", 1, "active", statement_config=statement_config) + + compiled_sql, execution_params = sql.compile() + # end-example + + assert "$1" in compiled_sql + assert "$2" in compiled_sql + assert execution_params == [1, "active"] diff --git a/docs/examples/usage/usage_data_flow_8.py b/docs/examples/usage/usage_data_flow_8.py new file mode 100644 index 00000000..fc635503 --- /dev/null +++ b/docs/examples/usage/usage_data_flow_8.py @@ -0,0 +1,22 @@ +"""Example 8: Statement Execution.""" + +__all__ = ("test_statement_execution",) + + +def test_statement_execution() -> None: + """Execute a compiled SQL object through SQLSpec.""" + from sqlspec import SQL, SQLSpec + from sqlspec.adapters.sqlite import SqliteConfig + + db_manager = SQLSpec() + db = db_manager.add_config(SqliteConfig(pool_config={"database": ":memory:"})) + + # start-example + sql_statement = SQL("SELECT ? AS message", "pipeline-complete") + + with db_manager.provide_session(db) as session: + result = session.execute(sql_statement) + message = result.scalar() + # end-example + + assert message == "pipeline-complete" diff --git a/docs/examples/usage/usage_data_flow_9.py b/docs/examples/usage/usage_data_flow_9.py new file mode 100644 index 00000000..ef6079af --- /dev/null +++ b/docs/examples/usage/usage_data_flow_9.py @@ -0,0 +1,19 @@ +"""Example 9: Driver Execution with session.""" + +__all__ = ("test_driver_execution",) + + +def test_driver_execution() -> None: + """Test driver execution pattern.""" + from sqlspec import SQLSpec + from sqlspec.adapters.sqlite import SqliteConfig + + # start-example + # Driver receives compiled SQL and parameters + db_manager = SQLSpec() + db = db_manager.add_config(SqliteConfig(pool_config={"database": ":memory:"})) + with db_manager.provide_session(db) as session: + result = session.execute("SELECT 'test' as message") + # end-example + + assert result is not None diff --git a/docs/usage/data_flow.rst b/docs/usage/data_flow.rst index 5f2d220e..e340750a 100644 --- a/docs/usage/data_flow.rst +++ b/docs/usage/data_flow.rst @@ -60,35 +60,28 @@ The execution flow begins when you create a SQL object. SQLSpec accepts multiple **Direct SQL Creation** -.. code-block:: python - - from sqlspec.core import SQL - - # Raw SQL string with positional parameters - sql = SQL("SELECT * FROM users WHERE id = ?", 1) - - # Named parameters - sql = SQL("SELECT * FROM users WHERE email = :email", email="user@example.com") +.. literalinclude:: /examples/usage/usage_data_flow_1.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **Using the Query Builder** -.. code-block:: python - - from sqlspec import sql - - # Build SQL programmatically - query = sql.select("id", "name", "email").from_("users").where("status = ?", "active") +.. literalinclude:: /examples/usage/usage_data_flow_2.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **From SQL Files** -.. code-block:: python - - from sqlspec.loader import SQLFileLoader - - loader = SQLFileLoader() - loader.load_sql("queries/users.sql") - sql = loader.get_sql("get_user_by_id", user_id=123) +.. literalinclude:: /examples/usage/usage_data_flow_3.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 During initialization, the SQL object: @@ -106,15 +99,11 @@ When the SQL object is compiled for execution, it passes through a sophisticated The first step extracts and preserves parameter information before any SQL modifications: -.. code-block:: python - - # SQLSpec identifies parameter placeholders - # Input: "SELECT * FROM users WHERE id = ? AND status = ?" - # Params: [1, 'active'] - # - # Result: Positional parameter mapping created - # Position 0 → value: 1 - # Position 1 → value: 'active' +.. literalinclude:: /examples/usage/usage_data_flow_4.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 This step uses ``ParameterValidator`` to ensure parameters are properly formatted and positions are tracked. @@ -122,15 +111,11 @@ This step uses ``ParameterValidator`` to ensure parameters are properly formatte The SQL string is parsed into an Abstract Syntax Tree (AST) using SQLGlot: -.. code-block:: python - - import sqlglot - - # Parse SQL into structured AST - expression = sqlglot.parse_one( - "SELECT * FROM users WHERE id = ?", - dialect="sqlite" - ) +.. literalinclude:: /examples/usage/usage_data_flow_5.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 The AST represents your query as a tree structure that can be analyzed and modified programmatically. This is crucial for the validation and transformation steps. @@ -147,13 +132,11 @@ Instead of treating SQL as plain text, SQLSpec uses the AST to: The AST is compiled into the target SQL dialect: -.. code-block:: python - - import sqlglot - - # Compile AST to target dialect - compiled_sql = expression.sql(dialect="postgres") - # Result: "SELECT * FROM users WHERE id = $1" +.. literalinclude:: /examples/usage/usage_data_flow_6.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 @@ -162,11 +145,11 @@ The AST is compiled into the target SQL dialect: Parameters are converted to the appropriate style for the target database: -.. code-block:: python - - # Input parameters: [1, 'active'] - # Target style: PostgreSQL numeric ($1, $2) - # Result: Parameters ready for execution +.. literalinclude:: /examples/usage/usage_data_flow_7.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 This ensures compatibility across different database drivers. @@ -174,11 +157,11 @@ This ensures compatibility across different database drivers. The compiled SQL and processed parameters are sent to the database: -.. code-block:: python - - # Driver executes compiled SQL with parameters - cursor.execute(compiled_sql, parameters) - results = cursor.fetchall() +.. literalinclude:: /examples/usage/usage_data_flow_8.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 The driver handles database-specific execution patterns and result retrieval. @@ -187,11 +170,11 @@ Stage 3: Driver Execution Once the SQL is compiled, it's sent to the database-specific driver for execution: -.. code-block:: python - - # Driver receives compiled SQL and parameters - with spec.provide_session(config) as session: - result = session.execute(compiled_sql, prepared_params) +.. literalinclude:: /examples/usage/usage_data_flow_9.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **Template Method Pattern** @@ -209,18 +192,11 @@ SQLSpec drivers use the Template Method pattern for consistent execution: **Example: SQLite Driver Execution** -.. code-block:: python - - class SqliteDriver(SyncDriverAdapterBase): - def _execute_statement(self, cursor, statement): - sql, params = self._get_compiled_sql(statement) - cursor.execute(sql, params or ()) - return self.create_execution_result(cursor) - - def _execute_many(self, cursor, statement): - sql, params = self._get_compiled_sql(statement) - cursor.executemany(sql, params) - return self.create_execution_result(cursor) +.. literalinclude:: /examples/usage/usage_data_flow_10.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 Stage 4: Result Handling ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -231,53 +207,29 @@ After database execution, raw results are transformed into typed Python objects. All query results are wrapped in a ``SQLResult`` object: -.. code-block:: python - - result = session.execute("SELECT * FROM users") - - # Access result data - result.data # List of dictionaries - result.rows_affected # Number of rows modified (INSERT/UPDATE/DELETE) - result.column_names # Column names for SELECT - result.operation_type # "SELECT", "INSERT", "UPDATE", "DELETE", "SCRIPT" +.. literalinclude:: /examples/usage/usage_data_flow_11.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **Convenience Methods** -.. code-block:: python - - # Get exactly one row (raises if not exactly one) - user = result.one() - - # Get one or None - user = result.one_or_none() - - # Get scalar value (first column of first row) - count = result.scalar() +.. literalinclude:: /examples/usage/usage_data_flow_12.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **Schema Mapping** SQLSpec can automatically map results to typed objects: -.. code-block:: python - - from pydantic import BaseModel - from typing import Optional - - class User(BaseModel): - id: int - name: str - email: str - is_active: Optional[bool] = True - - # Execute query - result = session.execute("SELECT id, name, email, is_active FROM users") - - # Map results to typed User instances - users: list[User] = result.all(schema_type=User) - - # Or get single typed user - single_result = session.execute("SELECT id, name, email, is_active FROM users WHERE id = ?", 1) - user: User = single_result.one(schema_type=User) # Type-safe! +.. literalinclude:: /examples/usage/usage_data_flow_13.py + :language: python + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **Supported Schema Types** @@ -297,14 +249,10 @@ Multi-Tier Caching SQLSpec implements caching at multiple levels: -.. code-block:: python - - # Cache types and their purposes: - sql_cache: dict[str, str] # Compiled SQL strings - optimized_cache: dict[str, Expression] # Post-optimization AST - builder_cache: dict[str, bytes] # QueryBuilder serialization - file_cache: dict[str, CachedSQLFile] # File loading with checksums - analysis_cache: dict[str, Any] # Pipeline step results +.. literalinclude:: ../examples/usage/usage_data_flow_14.py + :start-after: # start-example + :end-before: # end-example + :dedent: 2 **Cache Benefits** @@ -330,22 +278,10 @@ Configuration-Driven Processing ``StatementConfig`` controls pipeline behavior: -.. code-block:: python - - from sqlspec.core import StatementConfig - from sqlspec.core import ParameterStyle, ParameterStyleConfig - - config = StatementConfig( - dialect="postgres", - enable_parsing=True, # AST generation - enable_validation=True, # Security/performance checks - enable_transformations=True, # AST transformations - enable_caching=True, # Multi-tier caching - parameter_config=ParameterStyleConfig( - default_parameter_style=ParameterStyle.NUMERIC, - has_native_list_expansion=False, - ) - ) +.. literalinclude:: ../examples/usage/usage_data_flow_15.py + :start-after: # start-example + :end-before: # end-example + :dedent: 2 Disable features you don't need for maximum performance.