Using SQLAlchemy with Spatial Databases
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Comprehensive Alembic integration for managing spatial database schema migrations, including specialized operations for spatial columns, tables, and indexes with support for all major spatial database backends.
Migration support requires Alembic to be installed:
pip install alembicConfigure Alembic environment for spatial migrations:
# alembic/env.py
from geoalchemy2.alembic_helpers import writer
from geoalchemy2 import Geometry, Geography, Raster
# Register GeoAlchemy2 types for autogeneration
target_metadata = Base.metadata
# Configure rewriter for spatial operations
context.configure(
# ... other configuration
render_item=writer.render_item,
compare_type=True,
compare_server_default=True
)Specialized operations for adding and removing spatial columns with proper metadata and index management.
class AddGeospatialColumnOp:
"""
Operation for adding spatial columns to existing tables.
Handles:
- Spatial column creation with proper constraints
- Automatic spatial index generation
- Spatial metadata registration
- Database-specific implementation details
"""
class DropGeospatialColumnOp:
"""
Operation for removing spatial columns from tables.
Handles:
- Spatial index removal
- Spatial metadata cleanup
- Constraint removal
- Cross-database compatibility
"""Usage examples:
# In Alembic migration file
from alembic import op
from geoalchemy2 import Geometry
from geoalchemy2.alembic_helpers import AddGeospatialColumnOp, DropGeospatialColumnOp
def upgrade():
# Add spatial column
op.invoke(AddGeospatialColumnOp(
table_name='buildings',
column=Column('geom', Geometry('POLYGON', srid=4326))
))
# Standard Alembic syntax also works
op.add_column('buildings',
Column('location', Geometry('POINT', srid=4326))
)
def downgrade():
# Remove spatial column
op.invoke(DropGeospatialColumnOp(
table_name='buildings',
column_name='geom'
))
op.drop_column('buildings', 'location')Operations for creating and dropping entire spatial tables with proper setup.
class CreateGeospatialTableOp:
"""
Operation for creating tables with spatial columns.
Handles:
- Table creation with spatial column definitions
- Automatic spatial index creation
- Spatial metadata initialization
- Database-specific optimizations
"""
class DropGeospatialTableOp:
"""
Operation for dropping tables containing spatial columns.
Handles:
- Spatial metadata cleanup
- Spatial index removal
- Proper table dropping order
- Cross-database cleanup
"""Usage examples:
def upgrade():
# Create spatial table
op.invoke(CreateGeospatialTableOp(
table_name='parcels',
columns=[
Column('id', Integer, primary_key=True),
Column('parcel_id', String(50)),
Column('geom', Geometry('POLYGON', srid=4326)),
Column('centroid', Geometry('POINT', srid=4326))
]
))
def downgrade():
# Drop spatial table
op.invoke(DropGeospatialTableOp(table_name='parcels'))Dedicated operations for managing spatial indexes independently of columns and tables.
class CreateGeospatialIndexOp:
"""
Operation for creating spatial indexes.
Handles:
- Database-specific spatial index creation
- Index naming conventions
- Advanced index options (N-D, partial indexes)
- Performance optimization settings
"""
class DropGeospatialIndexOp:
"""
Operation for dropping spatial indexes.
Handles:
- Safe index removal
- Database-specific cleanup
- Index dependency management
"""Usage examples:
def upgrade():
# Create spatial index
op.invoke(CreateGeospatialIndexOp(
index_name='idx_buildings_geom_spatial',
table_name='buildings',
column_name='geom',
index_type='gist', # PostgreSQL
postgresql_ops={'geom': 'gist_geometry_ops_nd'} # N-D index
))
def downgrade():
# Drop spatial index
op.invoke(DropGeospatialIndexOp(
index_name='idx_buildings_geom_spatial'
))Specialized implementations for different database backends with appropriate spatial handling.
class GeoPackageImpl:
"""
Alembic implementation for GeoPackage databases.
Extends SQLiteImpl with GeoPackage-specific handling:
- OGC GeoPackage standard compliance
- Spatial reference system management
- Built-in R-tree spatial indexing
"""Automatic detection and generation of spatial schema changes:
# alembic/env.py configuration for spatial type detection
def compare_type(context, inspected_column, metadata_column,
inspected_type, metadata_type):
# Handle spatial type changes
if isinstance(metadata_type, (Geometry, Geography, Raster)):
return not spatial_types_equal(inspected_type, metadata_type)
return None
def spatial_types_equal(inspected, metadata):
"""Compare spatial types for schema changes."""
if type(inspected) != type(metadata):
return False
# Compare geometry_type, srid, dimension
return (
getattr(inspected, 'geometry_type', None) ==
getattr(metadata, 'geometry_type', None) and
getattr(inspected, 'srid', -1) ==
getattr(metadata, 'srid', -1)
)# Generate migration with spatial changes
$ alembic revision --autogenerate -m "Add spatial columns"
# Generated migration includes spatial operations
def upgrade():
# ### commands auto generated by Alembic ###
op.add_column('locations',
sa.Column('geom', geoalchemy2.types.Geometry(
geometry_type='POINT', srid=4326), nullable=True))
op.create_geospatial_index('idx_locations_geom_spatial',
'locations', ['geom'])
# ### end Alembic commands ###Migrate non-spatial database to support spatial operations:
# Migration: Add spatial extension
def upgrade():
# PostgreSQL: Enable PostGIS
op.execute('CREATE EXTENSION IF NOT EXISTS postgis')
# Add spatial columns to existing tables
op.add_column('addresses',
Column('location', Geometry('POINT', srid=4326))
)
# Create spatial indexes
op.create_index('idx_addresses_location_spatial',
'addresses', ['location'], postgresql_using='gist')
def downgrade():
op.drop_index('idx_addresses_location_spatial')
op.drop_column('addresses', 'location')
# Note: PostGIS extension cleanup may require manual interventionMigrate spatial data between coordinate systems:
def upgrade():
# Add new column with different SRID
op.add_column('parcels',
Column('geom_utm', Geometry('POLYGON', srid=3857))
)
# Transform existing data
op.execute('''
UPDATE parcels
SET geom_utm = ST_Transform(geom, 3857)
WHERE geom IS NOT NULL
''')
# Drop old column (in separate migration for safety)
def downgrade():
op.drop_column('parcels', 'geom_utm')Migrate spatial data between different database backends:
# From PostgreSQL to SQLite with SpatiaLite
def upgrade():
# Initialize SpatiaLite (handled automatically)
pass
def downgrade():
pass
# Data migration script (separate from schema migration)
def migrate_spatial_data():
# Source: PostgreSQL
source_engine = create_engine('postgresql://...')
# Target: SQLite with SpatiaLite
target_engine = create_engine('sqlite:///spatial.db')
# Read from source
source_data = source_engine.execute(
'SELECT id, name, ST_AsEWKT(geom) as geom_wkt FROM spatial_table'
).fetchall()
# Write to target
for row in source_data:
target_engine.execute(
'INSERT INTO spatial_table (id, name, geom) VALUES (?, ?, GeomFromEWKT(?))',
(row.id, row.name, row.geom_wkt)
)Common patterns for evolving spatial schemas:
# Pattern 1: Add spatial constraint
def upgrade():
# Add constraint to existing geometry column
op.execute('''
ALTER TABLE buildings
ADD CONSTRAINT enforce_geom_type
CHECK (geometrytype(geom) = 'POLYGON' OR geom IS NULL)
''')
# Pattern 2: Partition spatial data
def upgrade():
# Create partitioned table for large spatial datasets
op.execute('''
CREATE TABLE spatial_data_2024 (
LIKE spatial_data INCLUDING ALL
) INHERITS (spatial_data)
''')
# Add partition constraint
op.execute('''
ALTER TABLE spatial_data_2024
ADD CONSTRAINT year_2024
CHECK (extract(year from created_at) = 2024)
''')
# Pattern 3: Upgrade geometry dimension
def upgrade():
# Convert 2D geometries to 3D
op.execute('''
UPDATE elevation_points
SET geom = ST_Force3D(geom)
WHERE ST_CoordDim(geom) = 2
''')Robust error handling for spatial migrations:
def upgrade():
try:
# Spatial operations in transaction
with op.get_context().autocommit_block():
op.add_column('features',
Column('geom', Geometry('POLYGON', srid=4326))
)
# Validate spatial data
result = op.get_bind().execute('''
SELECT COUNT(*) FROM features
WHERE geom IS NOT NULL AND NOT ST_IsValid(geom)
''').scalar()
if result > 0:
raise ValueError(f"Found {result} invalid geometries")
except Exception as e:
# Rollback spatial changes
op.drop_column('features', 'geom')
raise
def downgrade():
# Safe downgrade with data preservation
invalid_geoms = op.get_bind().execute('''
SELECT id FROM features WHERE NOT ST_IsValid(geom)
''').fetchall()
if invalid_geoms:
print(f"Warning: {len(invalid_geoms)} invalid geometries will be lost")
op.drop_column('features', 'geom')Optimize spatial migrations for large datasets:
def upgrade():
# Process large spatial updates in batches
batch_size = 10000
offset = 0
while True:
result = op.get_bind().execute(f'''
UPDATE large_spatial_table
SET geom_transformed = ST_Transform(geom, 3857)
WHERE id BETWEEN {offset} AND {offset + batch_size - 1}
AND geom_transformed IS NULL
''')
if result.rowcount == 0:
break
offset += batch_size
print(f"Processed {offset} rows")def upgrade():
# Drop spatial indexes before bulk operations
op.drop_index('idx_features_geom_spatial')
# Perform bulk spatial updates
op.execute('UPDATE features SET geom = ST_Transform(geom, 3857)')
# Recreate spatial indexes
op.create_index('idx_features_geom_spatial', 'features', ['geom'],
postgresql_using='gist')Install with Tessl CLI
npx tessl i tessl/pypi-geoalchemy2