Pluggable database backends for Cot #594
Replies: 2 comments
|
Regarding the questions:
Otherwise, the plan sound fairly sound to me, and in general should be mostly fine to implement as proposed. One edge case I've bumped into recently: SQLite doesn't support ALTER FIELD, so we need to do "create-copy-drop-rename" table dance. We need to keep this in mind when implementing the new migration engine (specifically, the engine should probably keep the schema of each known model at each step, unlike now). |
|
One more use case that we currently don't support: there's no way to fetch a Model that physically doesn't exist in the database. This can be useful for executing complex queries that return columns that are not defined in any table. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Why open this
Right now Cot's database layer is monolithic: PostgreSQL, MySQL and SQLite are baked into core (
cot/src/db/impl_*.rs), all sharing the SQLx + SeaQuery stack. It works, and it gives us nice compile-time guarantees, but it has some structural downsides:We want to refactor this subsystem. This discussion is about the design - we're laying out some ideas and the open questions, and we want input before we lock the shape in.
What we want (goals)
One possible shape
A trait-based, Django-inspired layout. The pieces worth discussing:
Split the layer into separate crates. A
cot-db-corewith the traits/types, pluscot-db-postgres/cot-db-mysql/cot-db-sqliteas the reference backends that core depends on by default. Third parties publish e.g.cot-db-clickhouseon the same contract.Provider + registry, keyed by URL scheme. Each backend ships a "provider" that declares which URL schemes it handles (
postgres://,redis://, …). The app registers providers, andDatabase::from_url(...)resolves the right one. Registration could be explicit on an app builder, e.g.:Split a backend into focused concern-traits instead of one trait:
DatabaseFeatures- capability detection (supports_foreign_keys,supports_returning_clause,supports_json_fields, identifier limits, …), defaulting tofalse/conservative so backends opt in.DatabaseOperations- pure SQL/value formatting (quoting, datetime/bool formatting, limit-offset).SchemaEditor- DDL (create/drop/alter table, indexes, constraints).DatabaseIntrospection- read schema metadata back out.MigrationEngineandQueryBuilder- migrations and query construction.Async only where there's actual I/O. A hard line: connections, queries, transactions, schema changes and introspection are async; feature detection, SQL string generation, config parsing and type conversion are sync. Keeps the pure-compute paths ergonomic and avoids needless
asyncplumbing.A custom-type escape hatch. A
SqlValueenum with the standard types plus aCustom { type_name, data }variant and aSqlValueConvertibletrait, so backends can expose things like PostGISgeometry,jsonb, enums, or user-defined types without core knowing about them. Apps could queryfeatures().supports_custom_type_by_name("geometry")and degrade gracefully.The design questions we actually want input on
How wide should the abstraction be? The obvious temptation is to model everything - including Redis and ClickHouse - through a
query(sql, params)shaped trait. That leaks: Redis isn't SQL, ClickHouse isn't OLTP. Do we (a) define a SQL-relational contract and accept that non-SQL stores don't fit, (b) define a narrower core contract with optional capability traits layered on, or (c) stay relational-only and not pretend otherwise?Async-in-traits - how do we want to handle dyn? If we need
dyn DatabaseBackendwe hit the usual async-fn-in-traits friction. We now have AFIT on stable, plustrait-variant/dynosaurfor the object-safe dyn case. Do we needdynat all, or can backends be a generic type parameter / enum? That choice changes everything downstream, so it's worth settling early.Relationship to SeaQuery / SQLx. We already lean on both. Does a
QueryBuildertrait duplicate SeaQuery? Should the pluggable seam sit below SeaQuery (just connection + execution + dialect) and let SeaQuery keep doing query construction?Capability detection: runtime methods vs types. Booleans like
supports_upsert()are simple but push failures to runtime. Is there an appetite for encoding some capabilities in the type system, or is runtime detection the pragmatic sweet spot?Migrations across backends. Our migration engine is one of the stronger parts of the current system. How much of it generalizes, and how much is inherently dialect-specific?
What would help
We'd start by refactoring the existing three backends behind a clean internal trait boundary, then open that boundary up as the public plugin contract once it's proven. That sequencing derisks the abstraction before we commit to anything third parties depend on.
All reactions