Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

sea-orm-spanner

Google Cloud Spanner backend for SeaORM.

Sub-crates

CrateDescription
sea-query-spannerSQL query builder for Spanner (converts SeaQuery to Spanner SQL)
sea-orm-migration-spannerMigration support with CLI

Requirements

  • Rust 1.75+
  • Google Cloud Spanner (or emulator for local development)

Quick Start

1. Start Spanner Emulator

docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
export SPANNER_EMULATOR_HOST=localhost:9010

2. Add Dependencies

[dependencies]
sea-orm-spanner = "0.1"sea-orm = { git = "https://github.com/SeaQL/sea-orm.git", tag = "2.0.0-rc.32", features = ["runtime-tokio-native-tls", "macros"] }
tokio = { version = "1", features = ["full"] }
chrono = "0.4"uuid = { version = "1", features = ["v4"] }

3. Define Entity

use sea_orm::entity::prelude::*;#[derive(Clone,Debug,PartialEq,Eq,DeriveEntityModel)]#[sea_orm(table_name = "users")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubname:String,pubemail:String,pubcreated_at:DateTimeUtc,}#[derive(Copy,Clone,Debug,EnumIter,DeriveRelation)]pubenumRelation{}implActiveModelBehaviorforActiveModel{}

4. Connect and Query

use sea_orm::{EntityTrait,ActiveModelTrait,Set};use sea_orm_spanner::SpannerDatabase;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;// Insertlet user = user::ActiveModel{id:Set(uuid::Uuid::new_v4().to_string()),name:Set("Alice".to_string()),email:Set("alice@example.com".to_string()),created_at:Set(chrono::Utc::now()),};let inserted = user.insert(&db).await?;// Querylet users = user::Entity::find().all(&db).await?;// Updateletmut active: user::ActiveModel = inserted.into();
active.name = Set("Alice Smith".to_string());
active.update(&db).await?;// Delete
user::Entity::delete_by_id("some-id").exec(&db).await?;Ok(())}

Connection

Auto-Detect (Recommended)

SpannerDatabase::connect() automatically detects the environment:

// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010// GCP: uses ADC automatically (no code change needed)let db = SpannerDatabase::connect("projects/my-project/instances/my-instance/databases/my-db").await?;

ADC discovers credentials in the following order:

  1. GOOGLE_APPLICATION_CREDENTIALS env var (path to service account JSON file)
  2. gcloud auth application-default login (local development)
  3. 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 sea_orm_spanner::{SpannerDatabase,ClientConfig};// Example: explicit auth with custom endpointlet config = ClientConfig::default().with_auth().await.expect("Failed to authenticate");let db = SpannerDatabase::connect_with_config("projects/my-project/instances/my-instance/databases/my-db",
config,).await?;

Explicit Emulator Connection

If you prefer not to rely on environment variables:

// Default emulator (localhost:9010)let db = SpannerDatabase::connect_with_emulator("projects/test/instances/test/databases/test").await?;// Custom emulator hostlet db = SpannerDatabase::connect_with_emulator_host("projects/test/instances/test/databases/test","localhost:9020",).await?;// Auto-create instance and database on emulatorlet db = SpannerDatabase::connect_or_create_with_emulator("projects/test/instances/test/databases/test",CreateOptions::new().with_instance_creation(),).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

cargo run -p migration -- init --dir ./migration

This creates:

migration/
├── Cargo.toml
├── README.md
└── src/
├── lib.rs
├── main.rs
└── m20220101_000001_create_table.rs

Generate New Migration

cargo run -p migration -- generate create_users_table

Write Migration

use sea_orm_migration_spanner::prelude::*;pubstructMigration;implMigrationNameforMigration{fnname(&self) -> &str{"m20220101_000001_create_users"}}#[async_trait]implMigrationTraitforMigration{asyncfnup(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager
.create_table(SpannerTableBuilder::new().table("users").string("id",Some(36),true).string("name",Some(255),true).string("email",Some(255),true).timestamp("created_at",true).primary_key(["id"]),).await}asyncfndown(&self,manager:&SchemaManager) -> Result<(),DbErr>{
manager.drop_table("users").await}}

You can also use raw DDL if needed:

manager.create_table_raw("CREATE TABLE users ( id STRING(36) NOT NULL, name STRING(255) NOT NULL, ) PRIMARY KEY (id)").await

Run Migrations

The CLI auto-loads .env by default. Use --env-file to load a different file:

# Default: loads .env
cargo run -p migration -- up
# Load a specific env file
cargo run -p migration -- --env-file .env.stg up
# Or via ENV_FILE environment variable
ENV_FILE=.env.stg cargo run -p migration -- up

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
cargo run -p migration -- status
# Apply all pending migrations
cargo run -p migration -- up
# Apply N migrations
cargo run -p migration -- up -n 1
# Rollback last migration
cargo run -p migration -- down -n 1
# Rollback all migrations
cargo run -p migration -- reset
# Reset and reapply all
cargo run -p migration -- fresh

Testing

# Start emulator
docker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator
# Run tests
cargo test --features with-chrono,with-uuid

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 → Uses INSERT ... RETURNING *Fails on Spanner
  • DbBackend::MySql → Uses separate SELECT after INSERTWorks on Spanner

Features

  • with-chrono - DateTime support with chrono
  • with-uuid - UUID support
  • with-json - JSON support
  • with-rust_decimal - NUMERIC/Decimal support
  • with-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.

pubstructModel{pubcount:i64,pubuser_id:i64,}

Float Types

Spanner only has FLOAT64. Use f64 in your entities, not f32.

pubstructModel{pubprice:f64,// Correct: use f64// pub price: f32, // Avoid: will cause type mismatch}

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.

pubstructModel{pubcreated_at:DateTimeUtc,// Correct: DateTime<Utc>}
// Insert with UTC timestamplet user = user::ActiveModel{created_at:Set(chrono::Utc::now()),
..Default::default()};

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(json!(42))// Use:
json_val:Set(json!({"value":42}))

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:

#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "my_table")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,pubtags:Vec<String>,// ARRAY<STRING(MAX)>pubscores:Vec<i64>,// ARRAY<INT64>puboptional_flags:Option<Vec<bool>>,// ARRAY<BOOL> nullable}

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 rust_decimal::Decimal;#[derive(Clone,Debug,PartialEq,DeriveEntityModel)]#[sea_orm(table_name = "products")]pubstructModel{#[sea_orm(primary_key, auto_increment = false)]pubid:String,#[sea_orm(column_type = "Decimal(Some((38, 9)))")]pubprice:Decimal,#[sea_orm(column_type = "Decimal(Some((38, 9)))", nullable)]pubdiscount:Option<Decimal>,}

License

MIT OR Apache-2.0

About

Google Cloud Spanner backend for SeaORM

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages