{"id":47179091,"url":"https://github.com/jubilee-works/sea-orm-spanner","last_synced_at":"2026-04-07T07:00:27.761Z","repository":{"id":342611635,"uuid":"1138908255","full_name":"jubilee-works/sea-orm-spanner","owner":"jubilee-works","description":"Google Cloud Spanner backend for SeaORM","archived":false,"fork":false,"pushed_at":"2026-03-20T17:53:47.000Z","size":230,"stargazers_count":7,"open_issues_count":2,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-03-21T06:52:22.458Z","etag":null,"topics":["rust","sea-orm","spanner"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jubilee-works.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-01-21T09:15:32.000Z","updated_at":"2026-03-20T17:53:50.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/jubilee-works/sea-orm-spanner","commit_stats":null,"previous_names":["jubilee-works/sea-orm-spanner"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/jubilee-works/sea-orm-spanner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jubilee-works%2Fsea-orm-spanner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jubilee-works%2Fsea-orm-spanner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jubilee-works%2Fsea-orm-spanner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jubilee-works%2Fsea-orm-spanner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jubilee-works","download_url":"https://codeload.github.com/jubilee-works/sea-orm-spanner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jubilee-works%2Fsea-orm-spanner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31503394,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T03:10:19.677Z","status":"ssl_error","status_checked_at":"2026-04-07T03:10:13.982Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rust","sea-orm","spanner"],"created_at":"2026-03-13T07:05:56.140Z","updated_at":"2026-04-07T07:00:27.695Z","avatar_url":"https://github.com/jubilee-works.png","language":"Rust","readme":"# sea-orm-spanner\n\nGoogle Cloud Spanner backend for [SeaORM](https://www.sea-ql.org/SeaORM/).\n\n## Sub-crates\n\n| Crate | Description |\n|-------|-------------|\n| `sea-query-spanner` | SQL query builder for Spanner (converts SeaQuery to Spanner SQL) |\n| `sea-orm-migration-spanner` | Migration support with CLI |\n\n## Requirements\n\n- Rust 1.75+\n- Google Cloud Spanner (or emulator for local development)\n\n## Quick Start\n\n### 1. Start Spanner Emulator\n\n```bash\ndocker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator\nexport SPANNER_EMULATOR_HOST=localhost:9010\n```\n\n### 2. Add Dependencies\n\n```toml\n[dependencies]\nsea-orm-spanner = \"0.1\"\nsea-orm = { git = \"https://github.com/SeaQL/sea-orm.git\", tag = \"2.0.0-rc.32\", features = [\"runtime-tokio-native-tls\", \"macros\"] }\ntokio = { version = \"1\", features = [\"full\"] }\nchrono = \"0.4\"\nuuid = { version = \"1\", features = [\"v4\"] }\n```\n\n### 3. Define Entity\n\n```rust\nuse sea_orm::entity::prelude::*;\n\n#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]\n#[sea_orm(table_name = \"users\")]\npub struct Model {\n    #[sea_orm(primary_key, auto_increment = false)]\n    pub id: String,\n    pub name: String,\n    pub email: String,\n    pub created_at: DateTimeUtc,\n}\n\n#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]\npub enum Relation {}\n\nimpl ActiveModelBehavior for ActiveModel {}\n```\n\n### 4. Connect and Query\n\n```rust\nuse sea_orm::{EntityTrait, ActiveModelTrait, Set};\nuse sea_orm_spanner::SpannerDatabase;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let db = SpannerDatabase::connect(\n        \"projects/my-project/instances/my-instance/databases/my-db\"\n    ).await?;\n\n    // Insert\n    let user = user::ActiveModel {\n        id: Set(uuid::Uuid::new_v4().to_string()),\n        name: Set(\"Alice\".to_string()),\n        email: Set(\"alice@example.com\".to_string()),\n        created_at: Set(chrono::Utc::now()),\n    };\n    let inserted = user.insert(\u0026db).await?;\n\n    // Query\n    let users = user::Entity::find().all(\u0026db).await?;\n\n    // Update\n    let mut active: user::ActiveModel = inserted.into();\n    active.name = Set(\"Alice Smith\".to_string());\n    active.update(\u0026db).await?;\n\n    // Delete\n    user::Entity::delete_by_id(\"some-id\").exec(\u0026db).await?;\n\n    Ok(())\n}\n```\n\n## Connection\n\n### Auto-Detect (Recommended)\n\n`SpannerDatabase::connect()` automatically detects the environment:\n\n- **`SPANNER_EMULATOR_HOST` is set** → connects to the emulator without authentication\n- **`SPANNER_EMULATOR_HOST` is not set** → connects to GCP using [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials)\n\n```rust\n// Emulator: just set SPANNER_EMULATOR_HOST=localhost:9010\n// GCP: uses ADC automatically (no code change needed)\nlet db = SpannerDatabase::connect(\n    \"projects/my-project/instances/my-instance/databases/my-db\"\n).await?;\n```\n\nADC discovers credentials in the following order:\n\n1. `GOOGLE_APPLICATION_CREDENTIALS` env var (path to service account JSON file)\n2. `gcloud auth application-default login` (local development)\n3. GCE/GKE metadata server (when running on Google Cloud)\n\n### Custom Configuration\n\nUse `connect_with_config()` with a `ClientConfig` for full control over the connection:\n\n```rust\nuse sea_orm_spanner::{SpannerDatabase, ClientConfig};\n\n// Example: explicit auth with custom endpoint\nlet config = ClientConfig::default()\n    .with_auth()\n    .await\n    .expect(\"Failed to authenticate\");\n\nlet db = SpannerDatabase::connect_with_config(\n    \"projects/my-project/instances/my-instance/databases/my-db\",\n    config,\n).await?;\n```\n\n### Explicit Emulator Connection\n\nIf you prefer not to rely on environment variables:\n\n```rust\n// Default emulator (localhost:9010)\nlet db = SpannerDatabase::connect_with_emulator(\n    \"projects/test/instances/test/databases/test\"\n).await?;\n\n// Custom emulator host\nlet db = SpannerDatabase::connect_with_emulator_host(\n    \"projects/test/instances/test/databases/test\",\n    \"localhost:9020\",\n).await?;\n\n// Auto-create instance and database on emulator\nlet db = SpannerDatabase::connect_or_create_with_emulator(\n    \"projects/test/instances/test/databases/test\",\n    CreateOptions::new().with_instance_creation(),\n).await?;\n```\n\n### TLS\n\nTLS is handled automatically. When connecting to GCP (non-emulator), `connect()` and `SchemaManager` install the `rustls` crypto provider internally. No manual setup needed.\n\n## Migrations\n\n### Initialize Migration Directory\n\n```bash\ncargo run -p migration -- init --dir ./migration\n```\n\nThis creates:\n```\nmigration/\n├── Cargo.toml\n├── README.md\n└── src/\n    ├── lib.rs\n    ├── main.rs\n    └── m20220101_000001_create_table.rs\n```\n\n### Generate New Migration\n\n```bash\ncargo run -p migration -- generate create_users_table\n```\n\n### Write Migration\n\n```rust\nuse sea_orm_migration_spanner::prelude::*;\n\npub struct Migration;\n\nimpl MigrationName for Migration {\n    fn name(\u0026self) -\u003e \u0026str {\n        \"m20220101_000001_create_users\"\n    }\n}\n\n#[async_trait]\nimpl MigrationTrait for Migration {\n    async fn up(\u0026self, manager: \u0026SchemaManager) -\u003e Result\u003c(), DbErr\u003e {\n        manager\n            .create_table(\n                SpannerTableBuilder::new()\n                    .table(\"users\")\n                    .string(\"id\", Some(36), true)\n                    .string(\"name\", Some(255), true)\n                    .string(\"email\", Some(255), true)\n                    .timestamp(\"created_at\", true)\n                    .primary_key([\"id\"]),\n            )\n            .await\n    }\n\n    async fn down(\u0026self, manager: \u0026SchemaManager) -\u003e Result\u003c(), DbErr\u003e {\n        manager.drop_table(\"users\").await\n    }\n}\n```\n\nYou can also use raw DDL if needed:\n\n```rust\nmanager.create_table_raw(\n    \"CREATE TABLE users (\n        id STRING(36) NOT NULL,\n        name STRING(255) NOT NULL,\n    ) PRIMARY KEY (id)\"\n).await\n```\n\n### Run Migrations\n\nThe CLI auto-loads `.env` by default. Use `--env-file` to load a different file:\n\n```bash\n# Default: loads .env\ncargo run -p migration -- up\n\n# Load a specific env file\ncargo run -p migration -- --env-file .env.stg up\n\n# Or via ENV_FILE environment variable\nENV_FILE=.env.stg cargo run -p migration -- up\n```\n\nExample `.env` files:\n\n```bash\n# .env (local development with emulator)\nSPANNER_EMULATOR_HOST=localhost:9010\nDATABASE_URL=projects/local-project/instances/test-instance/databases/test-db\n\n# .env.stg (staging — real GCP, no emulator)\nDATABASE_URL=projects/my-project/instances/stg-instance/databases/stg-db\n```\n\n```bash\n# Check status\ncargo run -p migration -- status\n\n# Apply all pending migrations\ncargo run -p migration -- up\n\n# Apply N migrations\ncargo run -p migration -- up -n 1\n\n# Rollback last migration\ncargo run -p migration -- down -n 1\n\n# Rollback all migrations\ncargo run -p migration -- reset\n\n# Reset and reapply all\ncargo run -p migration -- fresh\n```\n\n## Testing\n\n```bash\n# Start emulator\ndocker run -d -p 9010:9010 -p 9020:9020 gcr.io/cloud-spanner-emulator/emulator\n\n# Run tests\ncargo test --features with-chrono,with-uuid\n```\n\n## Architecture\n\n```\n┌─────────────────────┐\n│   Your Application  │\n└──────────┬──────────┘\n           │\n┌──────────▼──────────┐\n│      sea-orm        │  (ActiveRecord pattern)\n└──────────┬──────────┘\n           │\n┌──────────▼──────────┐\n│   sea-orm-spanner   │  (ProxyDatabaseTrait)\n│  ┌───────────────┐  │\n│  │ SQL Rewriting │  │  ? → @p1, @p2 ...\n│  │ Type Convert  │  │  MySQL compat\n│  └───────────────┘  │\n└──────────┬──────────┘\n           │\n┌──────────▼──────────┐\n│ google-cloud-spanner│  (gRPC client)\n└──────────┬──────────┘\n           │\n┌──────────▼──────────┐\n│   Cloud Spanner     │\n└─────────────────────┘\n```\n\n### Why MySQL Backend?\n\nSeaORM's `DbBackend` determines SQL generation behavior. Spanner doesn't support `RETURNING` clause, so:\n\n- `DbBackend::Postgres` → Uses `INSERT ... RETURNING *` → **Fails on Spanner**\n- `DbBackend::MySql` → Uses separate `SELECT` after `INSERT` → **Works on Spanner**\n\n## Features\n\n- `with-chrono` - DateTime support with chrono\n- `with-uuid` - UUID support\n- `with-json` - JSON support\n- `with-rust_decimal` - NUMERIC/Decimal support\n- `with-array` - ARRAY type support (INT64, FLOAT64, STRING, BOOL arrays)\n\n## Known Limitations\n\n### Type Mapping\n\nSpanner has a limited set of native types compared to other databases. This library maps SeaORM types to Spanner types with the following considerations:\n\n#### Integer Types\n\nSpanner only has `INT64`. All integer values are returned as `i64`.\n\n**Recommendation**: Use `i64` for all integer fields in your entities.\n\n```rust\npub struct Model {\n    pub count: i64,\n    pub user_id: i64,\n}\n```\n\n#### Float Types\n\nSpanner only has `FLOAT64`. Use `f64` in your entities, not `f32`.\n\n```rust\npub struct Model {\n    pub price: f64,            // Correct: use f64\n    // pub price: f32,         // Avoid: will cause type mismatch\n}\n```\n\n#### TIMESTAMP Type\n\nSpanner TIMESTAMP columns should use `DateTimeUtc` (`chrono::DateTime\u003cchrono::Utc\u003e`) in entity definitions. Spanner stores all timestamps in UTC, and the read path returns `DateTime\u003cUtc\u003e` directly.\n\n```rust\npub struct Model {\n    pub created_at: DateTimeUtc,        // Correct: DateTime\u003cUtc\u003e\n}\n```\n\n```rust\n// Insert with UTC timestamp\nlet user = user::ActiveModel {\n    created_at: Set(chrono::Utc::now()),\n    ..Default::default()\n};\n```\n\n#### BYTES vs STRING\n\nBoth BYTES and STRING columns are transmitted as strings (BYTES are base64-encoded). The library uses heuristics to distinguish them:\n- Strings containing base64 special characters (`+`, `/`, `=`) that decode to non-UTF8 or null bytes are treated as BYTES\n- Empty strings cannot be distinguished and are treated as STRING\n\n**Recommendation**: Avoid storing empty byte arrays. Use at least one byte (e.g., `vec![0]`) for BYTES columns that need to represent \"empty\".\n\n#### JSON Primitives\n\nJSON 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.\n\n**Recommendation**: Wrap JSON primitives in objects or arrays:\n\n```rust\n// Instead of:\njson_val: Set(json!(42))\n\n// Use:\njson_val: Set(json!({\"value\": 42}))\n```\n\n#### ARRAY Types\n\nSpanner ARRAY types are supported for the following element types:\n- `ARRAY\u003cINT64\u003e` → `Vec\u003ci64\u003e`\n- `ARRAY\u003cFLOAT64\u003e` → `Vec\u003cf64\u003e`\n- `ARRAY\u003cSTRING\u003e` → `Vec\u003cString\u003e`\n- `ARRAY\u003cBOOL\u003e` → `Vec\u003cbool\u003e`\n\n**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.\n\nExample entity:\n\n```rust\n#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]\n#[sea_orm(table_name = \"my_table\")]\npub struct Model {\n    #[sea_orm(primary_key, auto_increment = false)]\n    pub id: String,\n    pub tags: Vec\u003cString\u003e,              // ARRAY\u003cSTRING(MAX)\u003e\n    pub scores: Vec\u003ci64\u003e,               // ARRAY\u003cINT64\u003e\n    pub optional_flags: Option\u003cVec\u003cbool\u003e\u003e, // ARRAY\u003cBOOL\u003e nullable\n}\n```\n\n#### NUMERIC Type\n\nSpanner NUMERIC type is supported via `rust_decimal::Decimal`. NUMERIC provides 38 digits of precision with 9 decimal places.\n\n**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.\n\nExample entity:\n\n```rust\nuse rust_decimal::Decimal;\n\n#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]\n#[sea_orm(table_name = \"products\")]\npub struct Model {\n    #[sea_orm(primary_key, auto_increment = false)]\n    pub id: String,\n    #[sea_orm(column_type = \"Decimal(Some((38, 9)))\")]\n    pub price: Decimal,\n    #[sea_orm(column_type = \"Decimal(Some((38, 9)))\", nullable)]\n    pub discount: Option\u003cDecimal\u003e,\n}\n```\n\n## License\n\nMIT OR Apache-2.0\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjubilee-works%2Fsea-orm-spanner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjubilee-works%2Fsea-orm-spanner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjubilee-works%2Fsea-orm-spanner/lists"}