# Safe SQL Composition

SQL query building with automatic escaping, identifier quoting, and parameter placeholder management to prevent SQL injection while maintaining readability and flexibility.

## Capabilities

### SQL String Composition

Build complex SQL queries safely using template strings with format placeholders and automatic escaping.

```python { .api }
class SQL:
    def __init__(self, template: str):
        """
        Create SQL template string.
        
        Args:
            template: SQL template with format placeholders like {table}, {field}
                     Must be a string literal for security
        """
        
    def format(self, *args, **kwargs) -> Composed:
        """
        Format SQL template with provided arguments.
        
        Args:
            *args: Positional format arguments
            **kwargs: Keyword format arguments
            
        Returns:
            Composed SQL object ready for execution
        """
        
    def join(self, seq: Iterable[Composable]) -> Composed:
        """
        Join sequence of composable objects with this SQL as separator.
        
        Args:
            seq: Sequence of Composable objects to join
            
        Returns:
            Composed SQL object with joined elements
        """
        
    def as_string(self, context: AdaptContext | None = None) -> str:
        """
        Convert to SQL string representation.
        
        Args:
            context: Adaptation context for custom formatting
            
        Returns:
            SQL string ready for database execution
        """
```

### Identifier Management

Safely handle database identifiers (table names, column names, schema names) with automatic quoting and validation.

```python { .api }
class Identifier:
    def __init__(self, *names: str):
        """
        Create database identifier with automatic quoting.
        
        Args:
            *names: Identifier parts (e.g., schema, table, column)
        """
        
    def as_string(self, context: AdaptContext | None = None) -> str:
        """
        Convert to properly quoted identifier string.
        
        Returns:
            Quoted identifier suitable for SQL queries
        """
```

#### Usage Examples

```python
from psycopg import sql

# Simple identifier
table_name = sql.Identifier("users")
# Produces: "users"

# Qualified identifier
qualified = sql.Identifier("public", "users", "name")  
# Produces: "public"."users"."name"

# With special characters
special_table = sql.Identifier("user-data")
# Produces: "user-data"
```

### Literal Value Handling

Convert Python objects to properly escaped SQL literals with automatic type detection and formatting.

```python { .api }
class Literal:
    def __init__(self, obj: Any):
        """
        Create SQL literal from Python object.
        
        Args:
            obj: Python object to convert to SQL literal
        """
        
    def as_string(self, context: AdaptContext | None = None) -> str:
        """
        Convert to properly escaped SQL literal string.
        
        Args:
            context: Adaptation context for custom type handling
            
        Returns:
            Escaped SQL literal string
        """
```

#### Usage Examples

```python
# String literal
name_literal = sql.Literal("John's Data")
# Produces: 'John''s Data'

# Number literal
age_literal = sql.Literal(25)
# Produces: 25

# Date literal
import datetime
date_literal = sql.Literal(datetime.date(2023, 12, 25))
# Produces: '2023-12-25'::date

# None/NULL literal
null_literal = sql.Literal(None)
# Produces: NULL
```

### Parameter Placeholders

Create named and positional parameter placeholders for prepared statements and dynamic queries.

```python { .api }
class Placeholder:
    def __init__(self, name: str | None = None):
        """
        Create parameter placeholder.
        
        Args:
            name: Named placeholder identifier (None for positional)
        """
        
    def as_string(self, context: AdaptContext | None = None) -> str:
        """
        Convert to parameter placeholder string.
        
        Returns:
            Parameter placeholder (%s, %(name)s, etc.)
        """
```

#### Usage Examples

```python
# Positional placeholder
pos_placeholder = sql.Placeholder()
# Produces: %s

# Named placeholder  
named_placeholder = sql.Placeholder("user_id")
# Produces: %(user_id)s
```

### Composed SQL Objects

Container for multiple SQL components that can be combined and manipulated as a single unit.

```python { .api }
class Composed:
    def __init__(self, seq: Iterable[Composable]):
        """
        Create composed SQL object from sequence of components.
        
        Args:
            seq: Sequence of Composable SQL components
        """
        
    def join(self, joiner: Composable) -> Composed:
        """
        Join components with specified joiner.
        
        Args:
            joiner: Composable object to use as separator
            
        Returns:
            New Composed object with joined components
        """
        
    def as_string(self, context: AdaptContext | None = None) -> str:
        """Convert all components to single SQL string"""
```

### Base Composable Interface

Abstract base class providing common functionality for all SQL composition objects.

```python { .api }
from abc import ABC, abstractmethod

class Composable(ABC):
    @abstractmethod
    def as_string(self, context: AdaptContext | None = None) -> str:
        """Convert to SQL string representation"""
        
    def __add__(self, other: Composable) -> Composed:
        """Concatenate with another composable object (+)"""
        
    def __mul__(self, n: int) -> Composed:
        """Repeat this object n times (*)"""
        
    def __rmul__(self, n: int) -> Composed:
        """Repeat this object n times (n * obj)"""
```

### Utility Functions

Standalone functions for common SQL composition tasks.

```python { .api }
def quote(obj: Any, context: AdaptContext | None = None) -> str:
    """
    Convert Python object to quoted SQL string.
    
    Args:
        obj: Python object to quote
        context: Adaptation context for custom handling
        
    Returns:
        Quoted SQL string
        
    Note:
        This function is less efficient than using Literal objects
        as it doesn't cache adaptation rules.
    """
```

## Advanced Usage Examples

### Dynamic Query Building

```python
from psycopg import sql

# Build SELECT query dynamically
def build_select(table, columns, where_conditions=None):
    query = sql.SQL("SELECT {fields} FROM {table}").format(
        fields=sql.SQL(", ").join(sql.Identifier(col) for col in columns),
        table=sql.Identifier(table)
    )
    
    if where_conditions:
        where_parts = []
        for column, value in where_conditions.items():
            where_parts.append(
                sql.SQL("{field} = {value}").format(
                    field=sql.Identifier(column),
                    value=sql.Placeholder(column)
                )
            )
        
        query += sql.SQL(" WHERE ") + sql.SQL(" AND ").join(where_parts)
    
    return query

# Usage
query = build_select(
    "users", 
    ["id", "name", "email"],
    {"active": True, "age": 25}
)
# Produces: SELECT "id", "name", "email" FROM "users" WHERE "active" = %(active)s AND "age" = %(age)s

cursor.execute(query, {"active": True, "age": 25})
```

### Complex INSERT Statements

```python
# Dynamic INSERT with multiple rows
def build_insert(table, columns, num_rows):
    placeholders = sql.SQL("({})").format(
        sql.SQL(", ").join(sql.Placeholder() for _ in columns)
    )
    
    values_list = sql.SQL(", ").join(placeholders for _ in range(num_rows))
    
    query = sql.SQL("INSERT INTO {table} ({fields}) VALUES {values}").format(
        table=sql.Identifier(table),
        fields=sql.SQL(", ").join(sql.Identifier(col) for col in columns),
        values=values_list
    )
    return query

# Insert multiple users
columns = ["name", "email", "age"]
query = build_insert("users", columns, 3)
cursor.execute(query, ("Alice", "alice@example.com", 30, 
                      "Bob", "bob@example.com", 25,
                      "Charlie", "charlie@example.com", 35))
```

### UPDATE with Dynamic SET Clause

```python
def build_update(table, updates, where_conditions):
    set_parts = []
    for column, value in updates.items():
        set_parts.append(
            sql.SQL("{field} = {placeholder}").format(
                field=sql.Identifier(column),
                placeholder=sql.Placeholder(column)
            )
        )
    
    query = sql.SQL("UPDATE {table} SET {sets}").format(
        table=sql.Identifier(table),
        sets=sql.SQL(", ").join(set_parts)
    )
    
    if where_conditions:
        where_parts = []
        for column, value in where_conditions.items():
            where_key = f"where_{column}"
            where_parts.append(
                sql.SQL("{field} = {placeholder}").format(
                    field=sql.Identifier(column),
                    placeholder=sql.Placeholder(where_key)
                )
            )
        query += sql.SQL(" WHERE ") + sql.SQL(" AND ").join(where_parts)
    
    return query

# Update user data
updates = {"name": "John Smith", "email": "john.smith@example.com"}
where = {"id": 123}
query = build_update("users", updates, where)

params = {**updates, "where_id": 123}
cursor.execute(query, params)
```

### Schema and Table Management

```python
# Create table with dynamic schema
def create_table_query(schema, table, columns):
    """
    columns: dict of {column_name: column_definition}
    """
    table_ident = sql.Identifier(schema, table) if schema else sql.Identifier(table)
    
    column_defs = []
    for name, definition in columns.items():
        column_defs.append(
            sql.SQL("{name} {definition}").format(
                name=sql.Identifier(name),
                definition=sql.SQL(definition)
            )
        )
    
    query = sql.SQL("CREATE TABLE {table} ({columns})").format(
        table=table_ident,
        columns=sql.SQL(", ").join(column_defs)
    )
    return query

# Usage
columns = {
    "id": "SERIAL PRIMARY KEY",
    "name": "VARCHAR(100) NOT NULL",
    "email": "VARCHAR(255) UNIQUE",
    "created_at": "TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
}

query = create_table_query("public", "users", columns)
cursor.execute(query)
```

### Handling Complex WHERE Clauses

```python
def build_complex_where(conditions):
    """
    conditions: dict with operators
    {
        "name": ("LIKE", "%john%"),
        "age": (">=", 18),
        "status": ("IN", ["active", "pending"])
    }
    """
    where_parts = []
    params = {}
    
    for i, (column, (operator, value)) in enumerate(conditions.items()):
        param_name = f"param_{i}"
        
        if operator.upper() == "IN":
            # Handle IN clause with list
            placeholders = sql.SQL("({})").format(
                sql.SQL(", ").join(sql.Placeholder(f"{param_name}_{j}") 
                                 for j in range(len(value)))
            )
            where_parts.append(
                sql.SQL("{field} IN {placeholders}").format(
                    field=sql.Identifier(column),
                    placeholders=placeholders
                )
            )
            for j, item in enumerate(value):
                params[f"{param_name}_{j}"] = item
        else:
            where_parts.append(
                sql.SQL("{field} {op} {placeholder}").format(
                    field=sql.Identifier(column),
                    op=sql.SQL(operator),
                    placeholder=sql.Placeholder(param_name)
                )
            )
            params[param_name] = value
    
    where_clause = sql.SQL(" AND ").join(where_parts)
    return where_clause, params

# Usage
conditions = {
    "name": ("LIKE", "%john%"),
    "age": (">=", 18),
    "status": ("IN", ["active", "pending"])
}

where_clause, params = build_complex_where(conditions)
query = sql.SQL("SELECT * FROM {table} WHERE {where}").format(
    table=sql.Identifier("users"),
    where=where_clause
)

cursor.execute(query, params)
```

## Security Benefits

The SQL composition system provides several security advantages:

1. **SQL Injection Prevention**: All user input is properly escaped and parameterized
2. **Identifier Safety**: Table and column names are automatically quoted to handle special characters
3. **Type Safety**: Automatic type detection and proper formatting for different data types
4. **Validation**: Invalid SQL constructs are caught at composition time rather than execution time

## Performance Considerations

- SQL composition objects are lightweight and can be reused
- Complex queries should be built once and reused with different parameters
- Use `sql.Placeholder()` for repeated parameter patterns
- Consider caching composed queries for frequently used patterns