This guide provides clear examples for using dbsamizdapper in your projects, both with and without Django.
- Quick Start
- Non-Django Usage
- Django Integration
- Library API
- Common Patterns
- Best Practices and Common Patterns
- Template Variables Reference
- Troubleshooting
pip install dbsamizdapperCreate a module with your database views:
# myapp/views.py
from dbsamizdat import SamizdatView, SamizdatMaterializedView
class UserStats(SamizdatView):
"""A simple view showing user statistics"""
sql_template = """
${preamble}
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE is_active) as active_users
FROM users
${postamble}
"""
class UserStatsCached(SamizdatMaterializedView):
"""Materialized view for faster queries"""
deps_on = {UserStats}
sql_template = """
${preamble}
SELECT * FROM "UserStats"
WHERE total_users > 100
${postamble}
"""Note: For a complete reference of template variables (
${preamble},${postamble},${samizdatname}) and what they're replaced with, see Template Variables Reference.
Sync to your database:
# Using CLI
python -m dbsamizdat.runner sync postgresql:///mydb myapp.views
# Or using library API
python -c "from dbsamizdat import sync; sync('postgresql:///mydb', samizdatmodules=['myapp.views'])"myproject/
├── myapp/
│ ├── __init__.py
│ ├── views.py # Your samizdat definitions
│ └── models.py # More samizdat definitions
├── requirements.txt
└── README.md
Create modules containing your samizdat classes:
# myapp/views.py
from dbsamizdat import SamizdatView, SamizdatMaterializedView, SamizdatTable
# Define a table
class CacheTable(SamizdatTable):
"""Unlogged table for caching"""
unlogged = True
sql_template = """
${preamble}
(
key TEXT PRIMARY KEY,
value JSONB,
expires_at TIMESTAMP
)
${postamble}
"""
# Define a view
class ActiveUsers(SamizdatView):
"""View of active users"""
sql_template = """
${preamble}
SELECT id, username, email
FROM users
WHERE is_active = true
${postamble}
"""
# Define a materialized view with dependencies
class UserActivity(SamizdatMaterializedView):
"""Materialized view showing user activity"""
deps_on = {ActiveUsers}
deps_on_unmanaged = {"orders"} # Reference to unmanaged table
sql_template = """
${preamble}
SELECT
u.id,
u.username,
COUNT(o.id) as order_count
FROM "ActiveUsers" u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.username
${postamble}
"""
# Optional: auto-refresh when base tables change
refresh_triggers = [("public", "orders")]The CLI requires module names as arguments. These modules will be imported automatically:
# Sync all samizdats from specified modules
python -m dbsamizdat.runner sync postgresql:///mydb myapp.views myapp.models
# Refresh materialized views
python -m dbsamizdat.runner refresh postgresql:///mydb myapp.views
# Show differences between code and database
python -m dbsamizdat.runner diff postgresql:///mydb myapp.views
# Drop all samizdat objects
python -m dbsamizdat.runner nuke postgresql:///mydb myapp.views
# Generate dependency graph
python -m dbsamizdat.runner printdot myapp.views | dot -Tpng > graph.pngSet DBURL environment variable to avoid passing connection string each time:
export DBURL="postgresql://user:password@localhost:5432/mydb"
python -m dbsamizdat.runner sync myapp.viewsOr use a .env file:
# .env
DBURL=postgresql://user:password@localhost:5432/mydbfrom dbsamizdat import sync, refresh, nuke
# Sync samizdats from specific modules
sync(
dburl="postgresql:///mydb",
samizdatmodules=["myapp.views", "myapp.models"]
)
# Refresh materialized views
refresh(
dburl="postgresql:///mydb",
samizdatmodules=["myapp.views"],
belownodes=["orders"] # Only refresh views depending on orders table
)
# Remove all samizdat objects
nuke(
dburl="postgresql:///mydb",
samizdatmodules=["myapp.views"]
)You can also pass samizdat classes directly:
from dbsamizdat.runner import cmd_sync, ArgType
from myapp.views import ActiveUsers, UserActivity
args = ArgType(
dburl="postgresql:///mydb",
txdiscipline="jumbo",
verbosity=1
)
# Pass explicit classes
cmd_sync(args, samizdatsIn=[ActiveUsers, UserActivity])- Add
dbsamizdattoINSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
# ... other apps
'dbsamizdat',
'myapp',
]- Create
dbsamizdat_defs.pyin your Django apps:
# myapp/dbsamizdat_defs.py
from dbsamizdat import SamizdatView, SamizdatMaterializedView
from myapp.models import User, Order
class UserStats(SamizdatView):
sql_template = """
${preamble}
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE is_active) as active_users
FROM myapp_user
${postamble}
"""# Sync all samizdats (auto-discovered from dbsamizdat_defs.py files)
./manage.py dbsamizdat sync
# Refresh materialized views
./manage.py dbsamizdat refresh
# Show differences
./manage.py dbsamizdat diff
# Drop all samizdat objects
./manage.py dbsamizdat nukeCreate views from Django QuerySets:
# myapp/dbsamizdat_defs.py
from dbsamizdat import SamizdatQuerySet, SamizdatMaterializedQuerySet
from myapp.models import User, Order
class ActiveUsersView(SamizdatQuerySet):
"""View from Django QuerySet"""
queryset = User.objects.filter(is_active=True).select_related('profile')
class UserOrderStats(SamizdatMaterializedQuerySet):
"""Materialized view from QuerySet"""
queryset = (
User.objects
.filter(is_active=True)
.annotate(order_count=Count('orders'))
.values('id', 'username', 'order_count')
)
# Auto-refresh when orders table changes
refresh_triggers = [("myapp", "order")]Create unmanaged Django models as views:
# myapp/dbsamizdat_defs.py
from dbsamizdat import SamizdatModel, SamizdatMaterializedModel
from django.db import models
class UserStatsModel(SamizdatModel):
"""Unmanaged Django model representing a view"""
total_users = models.IntegerField()
active_users = models.IntegerField()
class Meta:
managed = False # Don't create table, use view instead
db_table = 'user_stats_view'
sql_template = """
${preamble}
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE is_active) as active_users
FROM myapp_user
${postamble}
"""class BaseView(SamizdatView):
sql_template = """
${preamble}
SELECT 1 as value
${postamble}
"""
class DependentView(SamizdatView):
# Explicit dependency
deps_on = {BaseView}
sql_template = """
${preamble}
SELECT * FROM "BaseView"
${postamble}
"""
class AnotherView(SamizdatView):
# Reference unmanaged database objects
deps_on_unmanaged = {"public", "orders"}
sql_template = """
${preamble}
SELECT * FROM orders
${postamble}
"""class CustomSchemaView(SamizdatView):
schema = "analytics" # Use custom schema
sql_template = """
${preamble}
SELECT now() as timestamp
${postamble}
"""class MyView(SamizdatView):
object_name = "custom_view_name" # Override default class name
sql_template = """
${preamble}
SELECT 1
${postamble}
"""Important: PostgreSQL's $$ dollar-quoting syntax does not work in SQL templates because it clashes with Python's string.Template processing. Use a tag like $BODY$ instead.
When creating SamizdatFunction classes, you need to understand how function_arguments_signature and sql_template interact. There are two valid approaches:
Option A: Full CREATE FUNCTION in template (Less common)
Include the complete CREATE FUNCTION statement in your sql_template and set function_arguments_signature = "":
class MyFunction(SamizdatFunction):
function_arguments_signature = "" # Empty when using full CREATE FUNCTION
sql_template = f"""
CREATE FUNCTION {MyFunction.db_object_identity()}
RETURNS TEXT AS
$BODY$
BEGIN
RETURN 'Hello';
END;
$BODY$
LANGUAGE plpgsql;
"""Option B: Use ${preamble} (Recommended)
Omit CREATE FUNCTION from your template and use ${preamble}, which automatically includes CREATE FUNCTION {schema}.{name}({signature}). Set function_arguments_signature to your parameter signature:
See also: Template Variables Reference for complete details on
${preamble}and other template variables.
class MyFunction(SamizdatFunction):
function_arguments_signature = "name TEXT" # Parameter signature
sql_template = """
${preamble}
RETURNS TEXT AS
$BODY$
BEGIN
RETURN UPPER(name);
END;
$BODY$
LANGUAGE plpgsql;
"""Important Behavior:
creation_identity()always includes parentheses:"schema"."name"({args})- Even when
function_arguments_signature = "", it becomes"schema"."name"() - If you include
CREATE FUNCTIONin your template AND use${preamble}, you'll get signature duplication errors likeCREATE FUNCTION name(sig)(sig)- avoid this!
Function with no parameters:
class SimpleFunction(SamizdatFunction):
function_arguments_signature = "" # No parameters
sql_template = """
${preamble}
RETURNS TEXT AS
$BODY$
SELECT 'Hello, World!';
$BODY$
LANGUAGE SQL;
"""Function with parameters:
class GreetFunction(SamizdatFunction):
function_arguments_signature = "name TEXT, age INTEGER"
sql_template = """
${preamble}
RETURNS TEXT AS
$BODY$
BEGIN
RETURN format('Hello %s, you are %s years old', name, age);
END;
$BODY$
LANGUAGE plpgsql;
"""Function returning a table:
class UserStatsFunction(SamizdatFunction):
function_arguments_signature = "user_id INTEGER"
sql_template = """
${preamble}
RETURNS TABLE(
total_orders INTEGER,
total_spent NUMERIC,
last_order_date TIMESTAMP
) AS
$BODY$
BEGIN
RETURN QUERY
SELECT
COUNT(*)::INTEGER,
COALESCE(SUM(amount), 0),
MAX(order_date)
FROM orders
WHERE user_id = user_id;
END;
$BODY$
LANGUAGE plpgsql;
"""Function with function polymorphism (same name, different signatures):
class GetUser(SamizdatFunction):
function_name = "get_user" # Shared function name
function_arguments_signature = "user_id INTEGER"
sql_template = """
${preamble}
RETURNS TABLE(id INTEGER, name TEXT) AS
$BODY$
SELECT id, name FROM users WHERE id = user_id;
$BODY$
LANGUAGE SQL;
"""
class GetUserByName(SamizdatFunction):
function_name = "get_user" # Same name, different signature
function_arguments_signature = "username TEXT"
sql_template = """
${preamble}
RETURNS TABLE(id INTEGER, name TEXT) AS
$BODY$
SELECT id, name FROM users WHERE name = username;
$BODY$
LANGUAGE SQL;
"""from dbsamizdat import SamizdatTrigger
class MyTrigger(SamizdatTrigger):
deps_on = {MyFunction}
sql_template = f"""
${{preamble}}
FOR EACH ROW EXECUTE FUNCTION {MyFunction.creation_identity()};
"""Important: When referencing a function in a trigger template, you cannot use template variables. Use Python f-string interpolation with the function class's creation_identity() method instead. See Template Variables Reference for details.
This section provides actionable checklists and common patterns to help you implement functions and triggers correctly without trial-and-error.
See also:
- Function Signature Handling for detailed explanation of function creation approaches
- Template Variables Reference for complete details on available template variables
- Troubleshooting for solutions to common errors
When creating a SamizdatFunction, follow this checklist:
- Decide: Include
CREATE FUNCTIONin template OR usefunction_arguments_signature- Option A: Include full
CREATE FUNCTIONstatement → Setfunction_arguments_signature = "" - Option B (Recommended): Use
${preamble}→ Setfunction_arguments_signatureto your parameter signature
- Option A: Include full
- If including in template: Set
function_arguments_signature = "" - Use
${samizdatname}placeholder (not hardcoded name) when referencing the function within its own body - Use
$BODY$or$FUNC$for dollar-quoting (not$$) - PostgreSQL's$$conflicts with Python's template processing
When creating a SamizdatTrigger, follow this checklist:
- Start template with
${preamble}- This provides theCREATE TRIGGERstatement with proper table reference - Use
FunctionClass.creation_identity()for function references - Use Python f-string interpolation, not template variables - Include function class in
deps_onset - This ensures proper dependency ordering - Verify
on_tableformat - Can be a string, tuple(schema, table), or Django model class
Here are complete, working examples for common use cases:
from dbsamizdat import SamizdatFunction
class SimpleFunction(SamizdatFunction):
"""Function with no parameters"""
function_arguments_signature = "" # No parameters
sql_template = """
${preamble}
RETURNS TEXT AS
$BODY$
SELECT 'Hello, World!';
$BODY$
LANGUAGE SQL;
"""Key points:
function_arguments_signature = ""for no parameters- Uses
${preamble}which generatesCREATE FUNCTION "schema"."name"() - Uses
$BODY$instead of$$for dollar-quoting
from dbsamizdat import SamizdatFunction
class GreetFunction(SamizdatFunction):
"""Function with parameters"""
function_arguments_signature = "name TEXT, age INTEGER"
sql_template = """
${preamble}
RETURNS TEXT AS
$BODY$
BEGIN
RETURN format('Hello %s, you are %s years old', name, age);
END;
$BODY$
LANGUAGE plpgsql;
"""Key points:
function_arguments_signaturecontains parameter signature without parentheses${preamble}automatically includes the signature:CREATE FUNCTION "schema"."name"(name TEXT, age INTEGER)- Parameters are used directly in the function body
from dbsamizdat import SamizdatFunction, SamizdatTrigger, SamizdatTable
class MyTable(SamizdatTable):
"""Table for trigger example"""
sql_template = """
${preamble}
(
id SERIAL PRIMARY KEY,
value TEXT,
updated_at TIMESTAMP DEFAULT NOW()
)
${postamble}
"""
class UpdateTimestampFunction(SamizdatFunction):
"""Function that updates timestamp"""
function_arguments_signature = ""
sql_template = """
${preamble}
RETURNS TRIGGER AS
$BODY$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
"""
class UpdateTimestampTrigger(SamizdatTrigger):
"""Trigger that calls the update timestamp function"""
on_table = MyTable # Can be class, tuple, or string
condition = "BEFORE UPDATE"
deps_on = {UpdateTimestampFunction} # Include function in dependencies
sql_template = f"""
${{preamble}}
FOR EACH ROW EXECUTE FUNCTION {UpdateTimestampFunction.creation_identity()};
"""Key points:
- Trigger starts with
${preamble}(use double braces{{in f-strings) - Function reference uses
FunctionClass.creation_identity()with f-string interpolation - Function class included in
deps_onset on_tablecan be a class reference, tuple("schema", "table"), or string
from dbsamizdat import SamizdatFunction, SamizdatTrigger, SamizdatTable
class MyTable(SamizdatTable):
"""Table for multi-function example"""
sql_template = """
${preamble}
(
id SERIAL PRIMARY KEY,
name TEXT,
created_at TIMESTAMP DEFAULT NOW()
)
${postamble}
"""
class ValidateNameFunction(SamizdatFunction):
"""Function to validate name"""
function_arguments_signature = "name_value TEXT"
sql_template = """
${preamble}
RETURNS BOOLEAN AS
$BODY$
BEGIN
RETURN length(name_value) > 0 AND length(name_value) <= 100;
END;
$BODY$
LANGUAGE plpgsql;
"""
class LogChangeFunction(SamizdatFunction):
"""Function to log changes"""
function_arguments_signature = ""
sql_template = """
${preamble}
RETURNS TRIGGER AS
$BODY$
BEGIN
-- Log the change (simplified example)
RAISE NOTICE 'Change logged for record %', NEW.id;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql;
"""
class ValidateAndLogTrigger(SamizdatTrigger):
"""Trigger that uses multiple functions"""
on_table = MyTable
condition = "BEFORE INSERT OR UPDATE"
deps_on = {ValidateNameFunction, LogChangeFunction} # Multiple function dependencies
sql_template = f"""
${{preamble}}
FOR EACH ROW
WHEN ({ValidateNameFunction.creation_identity()}(NEW.name))
EXECUTE FUNCTION {LogChangeFunction.creation_identity()};
"""Key points:
- Multiple functions can be included in
deps_onset - Each function reference uses its own
creation_identity()method - Functions can be used in trigger conditions (
WHENclause) and execution - All dependent functions are automatically created before the trigger
❌ Don't use $$ for dollar-quoting - Use $BODY$ or $FUNC$ instead
# ❌ Wrong
sql_template = """
${preamble}
RETURNS TEXT AS $$
SELECT 'test';
$$ LANGUAGE SQL;
"""
# ✅ Correct
sql_template = """
${preamble}
RETURNS TEXT AS $BODY$
SELECT 'test';
$BODY$ LANGUAGE SQL;
"""❌ Don't hardcode function names - Use ${samizdatname} or creation_identity()
# ❌ Wrong
sql_template = """
${preamble}
RETURNS TEXT AS $BODY$
SELECT 'Function: MyFunction';
$BODY$ LANGUAGE SQL;
"""
# ✅ Correct
sql_template = """
${preamble}
RETURNS TEXT AS $BODY$
SELECT format('Function: %s', ${samizdatname});
$BODY$ LANGUAGE SQL;
"""❌ Don't use template variables for function references in triggers - Use f-strings with creation_identity()
# ❌ Wrong
class MyTrigger(SamizdatTrigger):
deps_on = {MyFunction}
sql_template = """
${preamble}
FOR EACH ROW EXECUTE FUNCTION ${function_ref}; # Template variable doesn't exist!
"""
# ✅ Correct
class MyTrigger(SamizdatTrigger):
deps_on = {MyFunction}
sql_template = f"""
${{preamble}}
FOR EACH ROW EXECUTE FUNCTION {MyFunction.creation_identity()};
"""Dbsamizdapper uses Python's string.Template to process SQL templates. Template variables are replaced with specific SQL strings based on the entity type. This section provides a complete reference of all available template variables and their exact replacements.
For SamizdatView, SamizdatTable, and SamizdatMaterializedView:
${preamble}
- Replaced with:
CREATE [UNLOGGED] VIEW/TABLE/MATERIALIZED VIEW "schema"."name" AS - Details:
- For views:
CREATE VIEW "schema"."name" AS - For tables:
CREATE TABLE "schema"."name"(noASkeyword) - For UNLOGGED tables:
CREATE UNLOGGED TABLE "schema"."name" - For materialized views:
CREATE MATERIALIZED VIEW "schema"."name" AS
- For views:
- Example:
class MyView(SamizdatView): sql_template = """ ${preamble} SELECT 1 as value ${postamble} """ # Generates: CREATE VIEW "public"."MyView" AS SELECT 1 as value
${postamble}
- Replaced with:
- For materialized views:
WITH NO DATA - For views and tables: empty string (removed from output)
- For materialized views:
- Details: Only materialized views need
WITH NO DATAto prevent immediate data population - Example:
class MyMatView(SamizdatMaterializedView): sql_template = """ ${preamble} AS SELECT 1 as value ${postamble} """ # Generates: CREATE MATERIALIZED VIEW "public"."MyMatView" AS SELECT 1 as value WITH NO DATA
${samizdatname}
- Replaced with:
"schema"."name"(thedb_object_identity()format) - Details: Fully qualified database object name, useful for self-references or cross-references
- Example:
class MyView(SamizdatView): sql_template = """ ${preamble} SELECT * FROM ${samizdatname} ${postamble} """ # Generates: CREATE VIEW "public"."MyView" AS SELECT * FROM "public"."MyView"
For SamizdatFunction:
${preamble}
- Replaced with:
CREATE FUNCTION "schema"."name"({signature}) - Details:
- Always includes the function signature in parentheses
- For functions with no parameters:
"schema"."name"() - For functions with parameters:
"schema"."name"(param1 TYPE1, param2 TYPE2) - Uses
creation_identity()format which includes the signature
- Example:
class MyFunction(SamizdatFunction): function_arguments_signature = "name TEXT" sql_template = """ ${preamble} RETURNS TEXT AS $BODY$ SELECT UPPER(name); $BODY$ LANGUAGE SQL; """ # Generates: CREATE FUNCTION "public"."MyFunction"(name TEXT) RETURNS TEXT AS ...
${samizdatname}
- Replaced with:
"schema"."name"({signature})(thedb_object_identity()format) - Details: Same as
${preamble}but without theCREATE FUNCTIONprefix. Includes signature in parentheses. - Example:
class MyFunction(SamizdatFunction): function_arguments_signature = "" sql_template = """ ${preamble} RETURNS TEXT AS $BODY$ SELECT 'Function: ${samizdatname}'; $BODY$ LANGUAGE SQL; """ # Inside the function body, ${samizdatname} becomes "public"."MyFunction"()
For SamizdatTrigger:
${preamble}
- Replaced with:
CREATE TRIGGER "trigger_name" {condition} ON "schema"."table_name" - Details:
- Trigger name is quoted
- Includes the trigger condition (e.g.,
AFTER INSERT,BEFORE UPDATE) - Includes the target table's fully qualified identity
- Does NOT include
FOR EACH ROWorEXECUTE FUNCTION- these must be in your template
- Example:
class MyTrigger(SamizdatTrigger): on_table = MyTable condition = "AFTER INSERT" sql_template = """ ${preamble} FOR EACH ROW EXECUTE FUNCTION my_function(); """ # Generates: CREATE TRIGGER "MyTrigger" AFTER INSERT ON "public"."MyTable" # FOR EACH ROW EXECUTE FUNCTION my_function();
${samizdatname}
- Replaced with:
trigger_name(just the trigger name, not quoted, not fully qualified) - Details:
- Unlike other entity types, this is just the plain trigger name
- Useful for self-referencing the trigger name in the template
- Cannot be used to reference functions - see "Referencing Functions in Triggers" below
- Example:
class MyTrigger(SamizdatTrigger): sql_template = """ ${preamble} FOR EACH ROW EXECUTE FUNCTION ${samizdatname}(); """ # ${samizdatname} becomes: MyTrigger # Full SQL: CREATE TRIGGER "MyTrigger" ... FOR EACH ROW EXECUTE FUNCTION MyTrigger();
Important: When you need to reference a function in a trigger template, you cannot use template variables. Template variables are only available for the current entity being created.
❌ Wrong Approach:
class MyTrigger(SamizdatTrigger):
deps_on = {MyFunction}
sql_template = """
${preamble}
FOR EACH ROW EXECUTE FUNCTION ${function_identity}(); # This doesn't exist!
"""✅ Correct Approach:
Use Python f-string interpolation with the function class's creation_identity() method:
class MyTrigger(SamizdatTrigger):
deps_on = {MyFunction}
sql_template = f"""
${{preamble}}
FOR EACH ROW EXECUTE FUNCTION {MyFunction.creation_identity()};
"""Why this works:
creation_identity()returns the full function identity with signature:"schema"."name"({args})- F-strings evaluate the Python expression at class definition time
- Use double braces
{{and}}to escape template variables in f-strings
Complete Example:
class RefreshFunction(SamizdatFunction):
function_arguments_signature = ""
sql_template = """
${preamble}
RETURNS TRIGGER AS
$BODY$
BEGIN
REFRESH MATERIALIZED VIEW my_matview;
RETURN NULL;
END;
$BODY$
LANGUAGE plpgsql;
"""
class RefreshTrigger(SamizdatTrigger):
on_table = MyTable
condition = "AFTER INSERT OR UPDATE"
deps_on = {RefreshFunction}
sql_template = f"""
${{preamble}}
FOR EACH STATEMENT EXECUTE FUNCTION {RefreshFunction.creation_identity()};
"""Undefined Variables: If you use a template variable that doesn't exist (e.g., ${undefined_var}), Python's safe_substitute() will leave it unchanged in the output. This can help catch typos but may also lead to confusing SQL errors.
Using Template Variables in F-strings: When using f-strings for your sql_template, escape template variables with double braces:
class MyView(SamizdatView):
sql_template = f"""
${{preamble}}
SELECT 1 as value
${{postamble}}
"""Template Variable Summary Table:
| Entity Type | ${preamble} |
${postamble} |
${samizdatname} |
|---|---|---|---|
| View | CREATE VIEW "schema"."name" AS |
(empty) | "schema"."name" |
| Table | CREATE TABLE "schema"."name" |
(empty) | "schema"."name" |
| UNLOGGED Table | CREATE UNLOGGED TABLE "schema"."name" |
(empty) | "schema"."name" |
| Materialized View | CREATE MATERIALIZED VIEW "schema"."name" AS |
WITH NO DATA |
"schema"."name" |
| Function | CREATE FUNCTION "schema"."name"({args}) |
(N/A) | "schema"."name"({args}) |
| Trigger | CREATE TRIGGER "name" {condition} ON "schema"."table" |
(N/A) | name (unquoted) |
Problem: Using $$ for dollar-quoted strings in PostgreSQL functions causes template errors.
Explanation: Dbsamizdapper uses Python's string.Template to process SQL templates. In Python templates, $$ is interpreted as an escaped $ character, which conflicts with PostgreSQL's $$ dollar-quoting syntax.
Solution: Use a tag instead of $$. Any tag works (e.g., $BODY$, $FUNC$, $CODE$):
See also: Template Variables Reference for information about how template variables are processed.
# ❌ This will NOT work
class BadFunction(SamizdatFunction):
sql_template = """
${preamble}
RETURNS TEXT AS $$
SELECT 'test';
$$ LANGUAGE SQL;
"""
# ✅ Use a tag instead
class GoodFunction(SamizdatFunction):
sql_template = """
${preamble}
RETURNS TEXT AS
$BODY$
SELECT 'test';
$BODY$
LANGUAGE SQL;
"""Problem: Getting syntax errors like CREATE FUNCTION name(sig)(sig) when creating functions.
Explanation: This happens when you include CREATE FUNCTION in your sql_template AND use ${preamble}. The ${preamble} already includes CREATE FUNCTION {schema}.{name}({signature}), so adding it again causes duplication.
Solution: Choose one approach:
- Option A: Include full
CREATE FUNCTIONin template, setfunction_arguments_signature = "" - Option B (Recommended): Use
${preamble}, omitCREATE FUNCTIONfrom template
# ❌ This will cause duplication
class BadFunction(SamizdatFunction):
function_arguments_signature = "name TEXT"
sql_template = """
CREATE FUNCTION "public"."BadFunction"(name TEXT) # Don't include this!
${preamble} # This already includes CREATE FUNCTION
RETURNS TEXT AS
$BODY$
SELECT name;
$BODY$
LANGUAGE SQL;
"""
# ✅ Correct: Use ${preamble} only
class GoodFunction(SamizdatFunction):
function_arguments_signature = "name TEXT"
sql_template = """
${preamble} # This includes CREATE FUNCTION automatically
RETURNS TEXT AS
$BODY$
SELECT name;
$BODY$
LANGUAGE SQL;
"""Problem: Even when function_arguments_signature = "", the generated SQL includes ().
Explanation: This is expected behavior. creation_identity() always includes parentheses: "schema"."name"({args}). When function_arguments_signature = "", it becomes "schema"."name"(), which is correct SQL for a function with no parameters.
Solution: This is not an error - it's the correct behavior. If you want a function with no parameters, use function_arguments_signature = "" and the () will be automatically added.
# ✅ This is correct - the () will be added automatically
class NoParamFunction(SamizdatFunction):
function_arguments_signature = "" # No parameters
sql_template = """
${preamble} # Generates: CREATE FUNCTION "public"."NoParamFunction"()
RETURNS TEXT AS
$BODY$
SELECT 'Hello';
$BODY$
LANGUAGE SQL;
"""Problem: Getting errors about missing CREATE FUNCTION statement.
Explanation: Your sql_template must either:
- Include a complete
CREATE FUNCTIONstatement (Option A), OR - Use
${preamble}which provides it automatically (Option B)
Solution: Ensure your template includes one of these:
# ✅ Option A: Full CREATE FUNCTION in template
class FunctionA(SamizdatFunction):
function_arguments_signature = ""
sql_template = f"""
CREATE FUNCTION {FunctionA.db_object_identity()}()
RETURNS TEXT AS
$BODY$
SELECT 'test';
$BODY$
LANGUAGE SQL;
"""
# ✅ Option B: Use ${preamble} (recommended)
class FunctionB(SamizdatFunction):
function_arguments_signature = ""
sql_template = """
${preamble} # Provides CREATE FUNCTION automatically
RETURNS TEXT AS
$BODY$
SELECT 'test';
$BODY$
LANGUAGE SQL;
"""
# ❌ This will fail - no CREATE FUNCTION
class BadFunction(SamizdatFunction):
function_arguments_signature = ""
sql_template = """
RETURNS TEXT AS # Missing CREATE FUNCTION!
$BODY$
SELECT 'test';
$BODY$
LANGUAGE SQL;
"""When SQL generation fails, dbsamizdapper now provides enhanced error messages to help debug template issues.
Enhanced error messages include:
- Original template: The SQL template before variable substitution
- Template variable substitutions: What values replaced
${preamble},${postamble},${samizdatname} - Function signature: The
function_arguments_signatureused (for functions) - Final SQL: The SQL that was attempted (always shown)
- Error hints: Automatic detection of common error patterns
1. Signature Duplication
Error: syntax error at or near "("
Problem: Function signature appears twice in the generated SQL, like:
CREATE FUNCTION "name"()(target_ts TIMESTAMP WITH TIME ZONE)Cause: Both function_arguments_signature attribute and the template include a signature.
Solution: Remove the signature from either:
- The
function_arguments_signatureattribute (if template has it), OR - The template (if
function_arguments_signatureis set)
Example:
# ❌ Problem: signature in both places
class BadFunction(SamizdatFunction):
function_arguments_signature = "target_ts TIMESTAMP WITH TIME ZONE"
sql_template = """
${preamble}
(target_ts TIMESTAMP WITH TIME ZONE) # ← Remove this
RETURNS TIMESTAMP AS $BODY$
SELECT target_ts;
$BODY$ LANGUAGE SQL;
"""
# ✅ Solution 1: Remove from template
class GoodFunction1(SamizdatFunction):
function_arguments_signature = "target_ts TIMESTAMP WITH TIME ZONE"
sql_template = """
${preamble}
RETURNS TIMESTAMP AS $BODY$
SELECT target_ts;
$BODY$ LANGUAGE SQL;
"""
# ✅ Solution 2: Remove from attribute
class GoodFunction2(SamizdatFunction):
function_arguments_signature = "" # ← Empty, template has it
sql_template = """
${preamble}
(target_ts TIMESTAMP WITH TIME ZONE)
RETURNS TIMESTAMP AS $BODY$
SELECT target_ts;
$BODY$ LANGUAGE SQL;
"""2. Missing CREATE FUNCTION
Error: syntax error at or near "RETURNS"
Problem: Template includes RETURNS but no CREATE FUNCTION statement.
Cause: Template doesn't start with ${preamble} or ${preamble} wasn't substituted properly.
Solution: Ensure your template starts with ${preamble}:
# ❌ Missing CREATE FUNCTION
class BadFunction(SamizdatFunction):
sql_template = """
RETURNS TEXT AS $BODY$
SELECT 'test';
$BODY$ LANGUAGE SQL;
"""
# ✅ Correct: Use ${preamble}
class GoodFunction(SamizdatFunction):
sql_template = """
${preamble}
RETURNS TEXT AS $BODY$
SELECT 'test';
$BODY$ LANGUAGE SQL;
"""3. Invalid Template Variable
Error: syntax error at or near "$"
Problem: Unsubstituted template variable found in SQL.
Cause: Template variable not recognized or typo in variable name.
Solution: Use only recognized variables:
${preamble}- CREATE statement prefix${postamble}- WITH NO DATA suffix (for materialized views)${samizdatname}- Fully qualified object name
# ❌ Invalid variable
class BadView(SamizdatView):
sql_template = """
${preamble} SELECT $invalid FROM users ${postamble}
"""
# ✅ Use valid variables
class GoodView(SamizdatView):
sql_template = """
${preamble} SELECT * FROM users ${postamble}
"""1. Enable Verbose Output
Use -v flag to see the SQL being executed:
python -m dbsamizdat.runner sync postgresql:///mydb myapp.views -v2. Inspect Generated SQL
When an error occurs, the error message shows:
- The original template
- What substitutions were made
- The final SQL that failed
3. Test Templates in Isolation
You can test template processing without executing:
from dbsamizdat import SamizdatView
class TestView(SamizdatView):
sql_template = "${preamble} SELECT 1 ${postamble}"
# See what SQL would be generated
print(TestView.create())
# Output: CREATE VIEW "public"."TestView" AS SELECT 14. Check Function Signatures
For functions, verify function_arguments_signature matches what's in the database:
# Check what signature PostgreSQL expects
# The error message will show candidate signatures if there's a mismatch5. Review Error Context
Enhanced error messages automatically detect common patterns and provide hints. Look for the 💡 Hint section in error output.
Problem: ModuleNotFoundError: No module named 'myapp.views'
Solution: Ensure your module is on Python path or use absolute imports:
# Add current directory to PYTHONPATH
PYTHONPATH=. python -m dbsamizdat.runner sync postgresql:///mydb myapp.views
# Or install your package in development mode
pip install -e .Problem: Connection string errors
Solution: Use proper PostgreSQL connection string format:
# Local database
"postgresql:///database_name"
# With user
"postgresql://user@localhost/database_name"
# Full connection string
"postgresql://user:password@host:port/database_name"Problem: DependencyCycleError
Solution: Review your dependency graph:
python -m dbsamizdat.runner printdot myapp.views | dot -Tpng > graph.pngProblem: Materialized views not refreshing
Solution:
- Use
refresh()command or API - Check
refresh_triggersconfiguration - Verify triggers were created:
\d+ view_namein psql
Problem: Samizdats not discovered in Django
Solution:
- Ensure
dbsamizdatis inINSTALLED_APPS - Create
dbsamizdat_defs.pyin your app directory - Check that app is in
INSTALLED_APPS - Use
DBSAMIZDAT_MODULESsetting for custom module locations:
# settings.py
DBSAMIZDAT_MODULES = [
"myapp.custom_views",
"shared.analytics",
]- See
README.mdfor installation and development setup - See
README.original.mdfor original rationale and advanced features - See
DEVELOPMENT.mdfor development setup and pre-commit usage - Check test files in
tests/for more examples - Pre-commit installation guide with uv - Recommended way to install pre-commit for development
This project uses pre-commit to ensure code quality. After installing pre-commit:
# Install pre-commit with uv (recommended)
uv tool install pre-commit --with pre-commit-uv
# Install Git hooks
pre-commit install
# Run manually on all files
pre-commit run --all-files
# Run on staged files (default on commit)
pre-commit runPre-commit will automatically:
- Format code with ruff
- Check linting with ruff
- Run type checking with mypy
- Check for common issues (trailing whitespace, large files, etc.)
See DEVELOPMENT.md for complete pre-commit documentation.