Clorinde

repository·main·Indexed 43 days ago

https://github.com/halcyonnouveau/clorinde

A tool that generates type-safe Rust interfaces from PostgreSQL queries. Clorinde provides a SQL-first approach to database interaction, validating queries against a real database during the generation process to ensure compile-time safety and high performance. It supports both synchronous and asynchronous Rust, including WebAssembly for environments like Cloudflare Workers, and allows for custom PostgreSQL-to-Rust type mappings via clorinde.toml.

Tokens
19.8K
Snippets
67
Records
117
Agent score
54%

What's inside Clorinde

  1. Overview of Clorinde's workflow

    main

    Clorinde is a tool powered by rust-postgres that generates type-checked Rust interfaces from PostgreSQL queries. It ensures compile-time safety and high performance by preparing queries against an actual database and running a validation suite.

    The standard workflow consists of three steps:

    1. Write your PostgreSQL queries.
    2. Generate a separate Rust crate containing type-safe interfaces to those queries using Clorinde.
    3. Import and use the generated crate within your main project.

    Unlike other database interfaces, Clorinde generates plain Rust structs rather than relying on heavy macros or complex generics, making the generated code ergonomic and easy to extend.

  2. Benchmark Scenarios Overview

    main

    The benchmarking suite evaluates several key database interaction patterns:

    • bench_trivial_query: Measures the performance of a simple SELECT * FROM users query.
    • bench_complex_query: Measures the performance of a LEFT JOIN between users and posts tables.
    • bench_loading_associations_sequentially: Measures the performance of loading users along with their associated posts and comments.
    • bench_insert: Measures the performance of inserting multiple rows (specifically testing scales of 1, 100, and 1000 rows).
  3. Use the Clorinde CLI to generate code

    main

    The Clorinde CLI provides three primary modes for code generation based on how you want to handle your database environment: schema for ephemeral containers, live for existing databases, and fresh for temporary databases on existing servers.

    To see the full list of available commands and options, run clorinde --help.

  4. Use the Clorinde API for programmatic access

    main
    Clorinde provides an API that mirrors its CLI functionality. Use the API instead of the CLI when you need to integrate Clorinde into a larger application, automate workflows programmatically, or leverage specific library abstractions like custom error types. For detailed technical documentation of every function, struct, and trait, refer to the official crate documentation on docs.rs.
  5. Use asynchronous Rust with Clorinde

    main

    Clorinde supports asynchronous programming patterns in Rust. The basic_async example provides a template for a minimal crate where:

    • Schema: Defined in schema.sql.
    • Queries: Defined in the queries/ directory.
    • Generated Code: Clorinde generates Rust modules based on your queries; in this example, they are located in src/clorinde.rs.
    • Implementation: You interact with these generated modules in your main.rs (or other application logic) to execute queries against a PostgreSQL database using an asynchronous connection pool.
  6. Understand parameter struct generation

    main

    Clorinde handles query parameters in two ways:

    1. Automatic Generation: If a query has more than one parameter column, Clorinde automatically generates a parameter struct. The name of this struct is derived from the query name.
    2. Manual Generation: You can manually define a parameter struct using the same type annotation (--:) or inline type (--! ... : Type()) syntax used for row structs.

    Note that parameter structs are optional; you can also work directly with the query function if preferred.

  7. How Clorinde works

    main

    Clorinde generates type-checked Rust interfaces from PostgreSQL queries. It follows a three-step workflow:

    1. Write SQL: Write your PostgreSQL queries using plain SQL with annotations and named parameters.
    2. Generate Crate: Run Clorinde to prepare queries against an actual database and validate them. This generates a separate Rust crate containing type-safe interfaces.
    3. Use Generated Code: Import the generated crate into your project and call the generated functions to execute your queries with compile-time safety.
  8. Using custom PostgreSQL types

    main

    Clorinde automatically generates support for custom PostgreSQL types such as enum, composite, and domain by inspecting your database.

    • Requirements: Custom types must be based on other supported types (either base types or other custom types).
    • Namespaces: Clorinde handles PostgreSQL schemas (namespaces) correctly, supporting types like my_schema.my_custom_type.
    • Domains: PostgreSQL domain types are unwrapped into their underlying inner types within your Rust queries.
  9. Configure borrowed type mappings

    main

    If your custom types have both owned and borrowed versions (e.g., String and &str), you can use the borrowed-type option in your mapping. This allows Clorinde to generate owned structs using the rust-type and borrowed structs using the borrowed-type.

    Requirements:

    • Both types must implement FromSql.
    • The owned type must implement ToSql.
    • The borrowed type must implement Into<OwnedType> for the generated From implementation.
    [types.mapping."pg_catalog.varchar"]
    rust-type = "db_types::string::CustomString"
    borrowed-type = "db_types::string::CustomStringRef<'a>"
    is-copy = false
  10. Map PostgreSQL types to custom Rust types

    main

    You can override Clorinde's default type mappings or add support for extension types using the [types.mapping] section.

    Simple Mapping: Map a PostgreSQL type string to a Rust type string. Detailed Mapping: Provides granular control over attributes and copy behavior.

    Detailed Mapping Options:

    • rust-type (required): The Rust type to use.
    • is-copy: Whether the type implements Copy (defaults to true).
    • attributes: Rust attributes for fields in owned structs.
    • attributes-borrowed: Rust attributes for fields in borrowed structs.
    • attributes-nullable: Attributes for fields in owned structs when the column is Option<T>. Falls back to attributes if unset.
    • attributes-borrowed-nullable: Attributes for fields in borrowed structs when the column is Option<T>. Falls back to attributes-borrowed if unset.
    [manifest.dependencies]
    ctypes = { path = "../ctypes" }
    postgres_range = { version = "0.11.1", features = ["with-chrono-0_4"] }
    
    [types.mapping]
    # Simple mapping
    "pg_catalog.date" = "ctypes::date::Date"
    
    # Detailed mapping
    [types.mapping."pg_catalog.timestamptz"]
    rust-type = "time::OffsetDateTime"
    attributes = ['serde(with = "time::serde::rfc3339")']
    attributes-nullable = ['serde(with = "time::serde::rfc3339::option")']
  11. Implement borrowed type mappings

    main

    To support both owned and borrowed versions of a type (e.g., String and &str), use the borrowed-type option in a detailed mapping.

    Requirements:

    • Both types must implement postgres_types::FromSql.
    • The owned type must implement postgres_types::ToSql.
    • The borrowed type must implement Into<OwnedType> to allow conversion from borrowed to owned structs.
    [types.mapping."pg_catalog.varchar"]
    rust-type = "my_crate::CustomString"
    borrowed-type = "my_crate::CustomStringRef<'a>"
    is-copy = false
  12. Use ergonomic parameters for bind parameters

    main

    Clorinde uses umbrella traits to allow passing multiple concrete types to the same query parameter. This means you can pass owned types, borrowed types, or smart pointers interchangeably when calling .bind() on a query.

    For example, if a query expects a string parameter, you can pass a &str, a String, or a Cow<'_, str> without needing to manually convert them to a specific Clorinde-internal type.

    // Both of these calls work for a query expecting a string parameter
    authors_by_first_name.bind(&client, &"John").all();
    authors_by_first_name.bind(&client, &String::from("John")).all();