sea-orm-spanner
Google Cloud Spanner backend for SeaORM.
Sub-crates
| Crate | Description |
|---|---|
sea-query-spanner |
SQL query builder for Spanner (converts SeaQuery to Spanner SQL) |
sea-orm-migration-spanner |
Migration support with CLI |
Requirements
- Rust 1.75+
- Google Cloud Spanner (or emulator for local development)
Quick Start
1. Start Spanner Emulator
2. Add Dependencies
[]
= "0.1"
= { = "https://github.com/SeaQL/sea-orm.git", = "2.0.0-rc.32", = ["runtime-tokio-native-tls", "macros"] }
= { = "1", = ["full"] }
= "0.4"
= { = "1", = ["v4"] }
3. Define Entity
use *;
4. Connect and Query
use ;
use SpannerDatabase;
async
Connection
Auto-Detect (Recommended)
SpannerDatabase::connect() automatically detects the environment:
SPANNER_EMULATOR_HOSTis set → connects to the emulator without authenticationSPANNER_EMULATOR_HOSTis not set → connects to GCP using Application Default Credentials (ADC)
// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010
// GCP: uses ADC automatically (no code change needed)
let db = connect.await?;
ADC discovers credentials in the following order:
GOOGLE_APPLICATION_CREDENTIALSenv var (path to service account JSON file)gcloud auth application-default login(local development)- GCE/GKE metadata server (when running on Google Cloud)
Custom Configuration
Use connect_with_config() with a ClientConfig for full control over the connection:
use ;
// Example: explicit auth with custom endpoint
let config = default
.with_auth
.await
.expect;
let db = connect_with_config.await?;
Explicit Emulator Connection
If you prefer not to rely on environment variables:
// Default emulator (localhost:9010)
let db = connect_with_emulator.await?;
// Custom emulator host
let db = connect_with_emulator_host.await?;
// Auto-create instance and database on emulator
let db = connect_or_create_with_emulator.await?;
TLS
TLS is handled automatically. When connecting to GCP (non-emulator), connect() and SchemaManager install the rustls crypto provider internally. No manual setup needed.
Migrations
Initialize Migration Directory
This creates:
migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs
Generate New Migration
Write Migration
use *;
;
You can also use raw DDL if needed:
manager.create_table_raw.await
Run Migrations
The CLI auto-loads .env by default. Use --env-file to load a different file:
# Default: loads .env
# Load a specific env file
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg
Example .env files:
# .env (local development with emulator)
SPANNER_EMULATOR_HOST=localhost:9010
DATABASE_URL=projects/local-project/instances/test-instance/databases/test-db
# .env.stg (staging — real GCP, no emulator)
DATABASE_URL=projects/my-project/instances/stg-instance/databases/stg-db
# Check status
# Apply all pending migrations
# Apply N migrations
# Rollback last migration
# Rollback all migrations
# Reset and reapply all
Testing
# Start emulator
# Run tests
Architecture
┌─────────────────────┐
│ Your Application │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ sea-orm │ (ActiveRecord pattern)
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ sea-orm-spanner │ (ProxyDatabaseTrait)
│ ┌───────────────┐ │
│ │ SQL Rewriting │ │ ? → @p1, @p2 ...
│ │ Type Convert │ │ MySQL compat
│ └───────────────┘ │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ google-cloud-spanner│ (gRPC client)
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Cloud Spanner │
└─────────────────────┘
Why MySQL Backend?
SeaORM's DbBackend determines SQL generation behavior. Spanner doesn't support RETURNING clause, so:
DbBackend::Postgres→ UsesINSERT ... RETURNING *→ Fails on SpannerDbBackend::MySql→ Uses separateSELECTafterINSERT→ Works on Spanner
Features
with-chrono- DateTime support with chronowith-uuid- UUID supportwith-json- JSON supportwith-rust_decimal- NUMERIC/Decimal supportwith-array- ARRAY type support (INT64, FLOAT64, STRING, BOOL arrays)
Known Limitations
Type Mapping
Spanner has a limited set of native types compared to other databases. This library maps SeaORM types to Spanner types with the following considerations:
Integer Types
Spanner only has INT64. All integer values are returned as i64.
Recommendation: Use i64 for all integer fields in your entities.
Float Types
Spanner only has FLOAT64. Use f64 in your entities, not f32.
TIMESTAMP Type
Spanner TIMESTAMP columns should use DateTimeUtc (chrono::DateTime<chrono::Utc>) in entity definitions. Spanner stores all timestamps in UTC, and the read path returns DateTime<Utc> directly.
// Insert with UTC timestamp
let user = ActiveModel ;
BYTES vs STRING
Both BYTES and STRING columns are transmitted as strings (BYTES are base64-encoded). The library uses heuristics to distinguish them:
- Strings containing base64 special characters (
+,/,=) that decode to non-UTF8 or null bytes are treated as BYTES - Empty strings cannot be distinguished and are treated as STRING
Recommendation: Avoid storing empty byte arrays. Use at least one byte (e.g., vec![0]) for BYTES columns that need to represent "empty".
JSON Primitives
JSON columns containing simple numeric values (e.g., 42, 3.14) cannot be distinguished from INT64/FLOAT64 columns at read time. This limitation affects JSON columns storing primitive numbers.
Recommendation: Wrap JSON primitives in objects or arrays:
// Instead of:
json_val: Set
// Use:
json_val: Set
ARRAY Types
Spanner ARRAY types are supported for the following element types:
ARRAY<INT64>→Vec<i64>ARRAY<FLOAT64>→Vec<f64>ARRAY<STRING>→Vec<String>ARRAY<BOOL>→Vec<bool>
Limitation: Empty arrays cannot be reliably read back from Spanner due to SDK limitations. The Spanner SDK returns empty arrays without type information, making it impossible to determine the correct element type. Always store at least one element in arrays, or use nullable arrays with NULL instead of empty arrays.
Example entity:
NUMERIC Type
Spanner NUMERIC type is supported via rust_decimal::Decimal. NUMERIC provides 38 digits of precision with 9 decimal places.
Limitation: Due to Spanner SDK limitations with type detection, avoid using NUMERIC with special values like zero in the same table as STRING columns. The SDK may misinterpret types when reading null or zero values.
Example entity:
use Decimal;
License
MIT OR Apache-2.0