Skip to content

Commit 19f8bcf

Browse files
committed
Add CockroachDB support
- Added DatabaseBackend::Cockroach variant - Added cockroachdb feature flag - Added schema generation for IDENTITY columns - Added documentation and tests Related: SeaQL/sea-query#329
1 parent fee41d5 commit 19f8bcf

File tree

7 files changed

+188
-3
lines changed

7 files changed

+188
-3
lines changed

Cargo.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,11 @@ runtime-tokio = ["sqlx?/runtime-tokio"]
148148
runtime-tokio-native-tls = ["sqlx?/runtime-tokio-native-tls", "runtime-tokio"]
149149
runtime-tokio-rustls = ["sqlx?/runtime-tokio-rustls", "runtime-tokio"]
150150
rusqlite = []
151+
cockroachdb = [
152+
"sqlx-postgres",
153+
"sea-query/backend-cockroach",
154+
]
155+
cockroachdb-use-identity-pk = ["sea-query/option-cockroachdb-use-identity"]
151156
schema-sync = ["sea-schema"]
152157
sea-orm-internal = []
153158
seaography = ["sea-orm-macros/seaography"]
@@ -212,5 +217,5 @@ with-uuid = ["uuid", "sea-query/with-uuid", "sea-query-sqlx?/with-uuid"]
212217

213218
# This allows us to develop using a local version of sea-query
214219
[patch.crates-io]
215-
# sea-query = { path = "../sea-query" }
220+
sea-query = { path = "../sea-query" }
216221
# sea-query = { git = "https://github.com/SeaQL/sea-query", branch = "master" }

README.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,55 @@ let user: Option<user::Model> = user::Entity::find()
200200

201201
See the [quickstart example](https://github.com/SeaQL/sea-orm/blob/master/sea-orm-sync/examples/quickstart/src/main.rs) for usage.
202202

203+
## CockroachDB Support
204+
205+
SeaORM supports CockroachDB via the `cockroachdb` feature flag. CockroachDB is wire-compatible with PostgreSQL, so most functionality works seamlessly.
206+
207+
### Enabling CockroachDB Support
208+
209+
```toml
210+
# Cargo.toml
211+
[dependencies]
212+
sea-orm = { version = "2.0", features = ["cockroachdb", "runtime-tokio-native-tls"] }
213+
```
214+
215+
### Connecting to CockroachDB
216+
217+
Use a standard PostgreSQL connection string:
218+
219+
```rust
220+
use sea_orm::DbConn;
221+
222+
let db: DbConn = sea_orm::Connect::connect("postgres://user:password@host:26257/database?sslmode=require")
223+
.await?;
224+
```
225+
226+
Or with CockroachDB specific scheme:
227+
228+
```rust
229+
let db: DbConn = sea_orm::Connect::connect("cockroachdb://user:password@host:26257/database?sslmode=require")
230+
.await?;
231+
```
232+
233+
### Schema Generation
234+
235+
CockroachDB doesn't support PostgreSQL's `SERIAL` pseudo-type. SeaORM automatically generates `GENERATED BY DEFAULT AS IDENTITY` for auto-increment columns when using CockroachDB:
236+
237+
```sql
238+
-- PostgreSQL
239+
"id" SERIAL PRIMARY KEY
240+
241+
-- CockroachDB (SeaORM default)
242+
"id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
243+
```
244+
245+
### Feature Flags
246+
247+
| Feature | Description |
248+
|---------|-------------|
249+
| `cockroachdb` | Enable CockroachDB support |
250+
| `cockroachdb-use-identity-pk` | Use IDENTITY columns for primary keys |
251+
203252
## Basics
204253

205254
### Select

pr_body.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
## Summary
2+
3+
This PR adds CockroachDB support to SeaORM. CockroachDB is wire-compatible with PostgreSQL, so most functionality works with the existing sqlx-postgres driver.
4+
5+
## Changes
6+
7+
- Added `cockroachdb` feature flag that enables:
8+
- `DatabaseBackend::Cockroach` variant
9+
- CockroachDB-specific URL scheme handling (`cockroachdb://`)
10+
- Proper schema generation using IDENTITY columns (not SERIAL)
11+
- Full RETURNING support
12+
13+
## Key Design Decisions
14+
15+
1. **Wire Compatibility**: CockroachDB uses the same protocol as PostgreSQL, so we alias `CockroachQueryBuilder` to `PostgresQueryBuilder` in sea-query
16+
2. **IDENTITY vs SERIAL**: CockroachDB doesn't support PostgreSQL's SERIAL pseudo-type. The implementation generates `GENERATED BY DEFAULT AS IDENTITY` by default.
17+
18+
## Feature Flags
19+
20+
| Feature | Description |
21+
|---------|-------------|
22+
| `cockroachdb` | Enable CockroachDB support |
23+
| `cockroachdb-use-identity-pk` | Use IDENTITY columns for primary keys |
24+
25+
## Testing
26+
27+
Added `tests/cockroachdb_tests.rs` with tests for:
28+
- Backend detection with various URL schemes
29+
- Schema generation using IDENTITY columns
30+
- Boolean value conversion
31+
32+
## Breaking Changes
33+
34+
None - all changes are gated behind the new `cockroachdb` feature flag.
35+
36+
---
37+
38+
Related sea-query PR: https://github.com/SeaQL/sea-query/pull/329

src/database/db_connection.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ pub enum DatabaseBackend {
8888
MySql,
8989
/// A PostgreSQL backend
9090
Postgres,
91+
/// A CockroachDB backend
92+
Cockroach,
9193
/// A SQLite backend
9294
Sqlite,
9395
}
@@ -759,6 +761,11 @@ impl DbBackend {
759761
Self::Postgres => {
760762
base_url_parsed.scheme() == "postgres" || base_url_parsed.scheme() == "postgresql"
761763
}
764+
Self::Cockroach => {
765+
base_url_parsed.scheme() == "cockroachdb"
766+
|| base_url_parsed.scheme() == "postgres"
767+
|| base_url_parsed.scheme() == "postgresql"
768+
}
762769
Self::MySql => base_url_parsed.scheme() == "mysql",
763770
Self::Sqlite => base_url_parsed.scheme() == "sqlite",
764771
}
@@ -776,6 +783,7 @@ impl DbBackend {
776783
pub fn support_returning(&self) -> bool {
777784
match self {
778785
Self::Postgres => true,
786+
Self::Cockroach => true,
779787
Self::Sqlite if cfg!(feature = "sqlite-use-returning-for-3_35") => true,
780788
Self::MySql if cfg!(feature = "mariadb-use-returning") => true,
781789
_ => false,
@@ -785,7 +793,7 @@ impl DbBackend {
785793
/// A getter for database dependent boolean value
786794
pub fn boolean_value(&self, boolean: bool) -> sea_query::Value {
787795
match self {
788-
Self::MySql | Self::Postgres | Self::Sqlite => boolean.into(),
796+
Self::MySql | Self::Postgres | Self::Cockroach | Self::Sqlite => boolean.into(),
789797
}
790798
}
791799

@@ -794,6 +802,7 @@ impl DbBackend {
794802
match self {
795803
DatabaseBackend::MySql => "MySql",
796804
DatabaseBackend::Postgres => "Postgres",
805+
DatabaseBackend::Cockroach => "Cockroach",
797806
DatabaseBackend::Sqlite => "Sqlite",
798807
}
799808
}

src/database/statement.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use crate::DbBackend;
22
#[cfg(feature = "rbac")]
33
pub use sea_query::audit::{AuditTrait, Error as AuditError, QueryAccessAudit};
44
use sea_query::{MysqlQueryBuilder, PostgresQueryBuilder, SqliteQueryBuilder, inject_parameters};
5+
#[cfg(feature = "cockroachdb")]
6+
use sea_query::CockroachQueryBuilder;
57
pub use sea_query::{Value, Values};
68
use std::fmt;
79

@@ -71,6 +73,10 @@ impl fmt::Display for Statement {
7173
DbBackend::Postgres => {
7274
inject_parameters(&self.sql, &values.0, &PostgresQueryBuilder)
7375
}
76+
#[cfg(feature = "cockroachdb")]
77+
DbBackend::Cockroach => {
78+
inject_parameters(&self.sql, &values.0, &CockroachQueryBuilder)
79+
}
7480
DbBackend::Sqlite => {
7581
inject_parameters(&self.sql, &values.0, &SqliteQueryBuilder)
7682
}
@@ -89,6 +95,8 @@ macro_rules! build_any_stmt {
8995
match $db_backend {
9096
DbBackend::MySql => $stmt.build(MysqlQueryBuilder),
9197
DbBackend::Postgres => $stmt.build(PostgresQueryBuilder),
98+
#[cfg(feature = "cockroachdb")]
99+
DbBackend::Cockroach => $stmt.build(CockroachQueryBuilder),
92100
DbBackend::Sqlite => $stmt.build(SqliteQueryBuilder),
93101
}
94102
};
@@ -98,6 +106,8 @@ macro_rules! build_postgres_stmt {
98106
($stmt: expr, $db_backend: expr) => {
99107
match $db_backend {
100108
DbBackend::Postgres => $stmt.to_string(PostgresQueryBuilder),
109+
#[cfg(feature = "cockroachdb")]
110+
DbBackend::Cockroach => $stmt.to_string(CockroachQueryBuilder),
101111
DbBackend::MySql | DbBackend::Sqlite => unimplemented!(),
102112
}
103113
};

src/schema/entity.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ where
229229
let variants: Vec<String> = variants.iter().map(|v| v.to_string()).collect();
230230
ColumnType::custom(format!("ENUM('{}')", variants.join("', '")))
231231
}
232-
DbBackend::Postgres => ColumnType::Custom(name.clone()),
232+
DbBackend::Postgres | DbBackend::Cockroach => ColumnType::Custom(name.clone()),
233233
DbBackend::Sqlite => orm_column_def.col_type,
234234
},
235235
_ => orm_column_def.col_type,

tests/cockroachdb_tests.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#![allow(unused_imports, dead_code)]
2+
3+
pub mod common;
4+
5+
pub use sea_orm::{DbBackend, DbErr};
6+
7+
// DATABASE_URL=cockroachdb://...:26257/... cargo test --features cockroachdb,sqlx-postgres,runtime-tokio --test cockroachdb_tests
8+
#[sea_orm_macros::test]
9+
#[cfg(feature = "cockroachdb")]
10+
async fn test_cockroachdb_backend_detection() -> Result<(), DbErr> {
11+
// Test is_prefix_of for various URL schemes
12+
assert!(DbBackend::Cockroach.is_prefix_of("cockroachdb://localhost:26257/test"));
13+
assert!(DbBackend::Cockroach.is_prefix_of("postgres://localhost:26257/test"));
14+
assert!(DbBackend::Cockroach.is_prefix_of("postgresql://localhost:26257/test"));
15+
assert!(!DbBackend::Cockroach.is_prefix_of("mysql://localhost:3306/test"));
16+
assert!(!DbBackend::Cockroach.is_prefix_of("sqlite://test.db"));
17+
18+
// Test as_str
19+
assert_eq!(DbBackend::Cockroach.as_str(), "Cockroach");
20+
21+
// Test support_returning
22+
assert!(DbBackend::Cockroach.support_returning());
23+
24+
Ok(())
25+
}
26+
27+
#[sea_orm_macros::test]
28+
#[cfg(feature = "cockroachdb")]
29+
async fn test_cockroachdb_schema_identity() -> Result<(), DbErr> {
30+
use sea_orm::sea_query::{ColumnDef, Alias, PostgresQueryBuilder, Table};
31+
32+
// Create a table with auto-increment primary key
33+
let table = Table::create()
34+
.table(Alias::new("test_table"))
35+
.col(
36+
ColumnDef::new(Alias::new("id"))
37+
.integer()
38+
.not_null()
39+
.auto_increment()
40+
.primary_key(),
41+
)
42+
.col(ColumnDef::new(Alias::new("name")).string())
43+
.to_owned();
44+
45+
// Build the SQL - should use GENERATED BY DEFAULT AS IDENTITY, not SERIAL
46+
let sql = table.to_string(PostgresQueryBuilder);
47+
48+
// CockroachDB uses GENERATED BY DEFAULT AS IDENTITY instead of SERIAL
49+
assert!(
50+
sql.contains("GENERATED BY DEFAULT AS IDENTITY"),
51+
"Expected GENERATED BY DEFAULT AS IDENTITY in schema, got: {sql}"
52+
);
53+
assert!(
54+
!sql.contains("SERIAL"),
55+
"SERIAL should not be used for CockroachDB, got: {sql}"
56+
);
57+
58+
Ok(())
59+
}
60+
61+
#[sea_orm_macros::test]
62+
#[cfg(feature = "cockroachdb")]
63+
async fn test_cockroachdb_boolean_value() -> Result<(), DbErr> {
64+
use sea_orm::sea_query::Value;
65+
66+
let true_val = DbBackend::Cockroach.boolean_value(true);
67+
let false_val = DbBackend::Cockroach.boolean_value(false);
68+
69+
// Boolean values should be converted correctly
70+
assert_eq!(true_val, Value::Bool(Some(true)));
71+
assert_eq!(false_val, Value::Bool(Some(false)));
72+
73+
Ok(())
74+
}

0 commit comments

Comments
 (0)