Cornucopia

repository·main·Indexed 22 days ago

https://github.com/cornucopia-rs/cornucopia

A tool that generates type-checked Rust interfaces from PostgreSQL queries. Cornucopia validates SQL against a real database to generate a dedicated Rust crate, ensuring compile-time safety and high performance. It supports asynchronous and synchronous Rust, WebAssembly targets for Cloudflare Workers, and custom type mappings via a cornucopia.toml configuration file.

Tokens
19.4K
Snippets
60
Records
94
Agent score
77%

What's inside cornucopia

  1. What is Cornucopia?

    main

    Cornucopia is a code generation tool that creates type-checked Rust interfaces from PostgreSQL queries. It is powered by rust-postgres and focuses on compile-time safety and high performance.

    Unlike libraries that rely heavily on complex macros or generics at runtime, Cornucopia prepares your queries against an actual database to validate them and then generates plain Rust structs into a separate crate. This makes the generated code ergonomic, easy to understand, and easy to extend.

  2. Understand Cornucopia's compile-time error reporting

    main

    Cornucopia provides detailed error reporting during the build process (before runtime) to catch malformed query annotations. If a query annotation (e.g., using --! author: (age?) to declare a nullable field) refers to a field that does not exist in the database schema, Cornucopia will emit a diagnostic error.

    These errors include:

    • The specific file path and line/column location (e.g., queries/test.sql:1:1).
    • A visual pointer to the problematic code.
    • A descriptive error message (e.g., no field with this name was found).
    • Helpful suggestions (e.g., help: use one of those names: id, name).

    If your development environment supports it, you can click the file path in the error output to navigate directly to the error site in your SQL code.

    × unknown field
       ╭─[queries/test.sql:1:1]
     1 │ --! author: (age?)
       ·              ─┬─
       ·               ╰── no field with this name was found
     2 │ SELECT * FROM author;
       ╰────
      help: use one of those names: id, name
  3. Understand parameter structs

    main

    Cornucopia handles query parameters in two ways:

    1. Automatic Generation: If a query has more than one parameter column, Cornucopia automatically generates a parameter struct named after the query.
    2. Manual Generation: You can manually define a parameter struct using the same type annotation (--:) or inline type (: Name()) syntax used for row structs.

    Note: You are not strictly required to use a parameter struct; you can always pass parameters directly to the query function.

  4. Control nullity for parameters and columns

    main

    By default, Cornucopia infers all parameters and returned columns as non-null. To mark them as nullable (which results in Option<T> in Rust), append a question mark (?) to the name within the annotation.

    Use a colon (:) to separate the bind parameters section from the returned row columns section. Both sections are optional if you only want to provide a query name.

    Syntax for composites and arrays: You can granularly specify nullity for fields within composites or elements within arrays using the following patterns:

    • compos?.some_field?: Makes the composite and a specific field nullable.
    • arr?[?]: Makes the array and its elements nullable.
    --! authors_from_country (country?) : (age?)
    SELECT id, name, age
    FROM authors
    WHERE authors.nationality = :country;
    
    -- Example for composites and arrays:
    --! example_query : (compos?.some_field?, arr?[?])
    SELECT compos, arr
    FROM example
  5. Understand Cornucopia's ergonomic parameter umbrella traits

    main

    Cornucopia uses 'umbrella traits' for bind parameters, allowing you to pass various concrete types to the same query without manual conversion. Instead of requiring a specific type, the generated bind methods accept any type that implements a specific umbrella trait (e.g., StringSql, BytesSql).

    // Because of StringSql, both of these are valid for a string parameter
    authors_by_first_name.bind(&client, &"John").all();
    authors_by_first_name.bind(&client, &String::from("John")).all();
  6. How Cornucopia works

    main

    Cornucopia follows a SQL-first workflow to provide type-safe Rust interfaces for PostgreSQL queries:

    1. Write SQL: Write plain PostgreSQL queries in .sql files using annotations for function names and named parameters (e.g., :param_name).
    2. Generate Crate: Run cornucopia to prepare queries against an actual database. This validates the SQL and 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, which handle parameter binding and result mapping.
  7. Use the Cornucopia API for programmatic workflows

    main
    While the Cornucopia CLI is useful for manual tasks, the API is designed for programmatic use cases. Using the API allows you to integrate Cornucopia directly into your Rust code and provides access to useful abstractions, such as specialized error types, that are not available via the CLI.
  8. Handle Rust keyword collisions in SQL identifiers

    main

    When generating Rust code, Cornucopia automatically escapes identifiers that collide with non-strict Rust keywords. For example, a SQL column named async will be generated as r#async in the resulting Rust code.

    Note:

    • Non-strict keywords: Automatically escaped using the r# prefix.
    • Strict keywords: These will cause a code generation error.

    To keep your generated code clean and easy to use, it is recommended to avoid using Rust keywords as identifiers in your SQL queries.

  9. Identify supported database connection types

    main

    The types of connections your generated queries accept depend on your chosen driver (Sync or Async) and whether you are using a connection pooler like Deadpool.

    Note: The generated Cornucopia crate re-exports all necessary connection modules. You do not need to add these specific driver crates to your Cargo.toml manually to use them with the generated code.

    ### Supported Connections
    
    #### Sync
    * `postgres::Client`
    * `postgres::Transaction`
    
    #### Async
    * `tokio_postgres::Client`
    * `tokio_postgres::Transaction`
    
    #### Async + Deadpool
    * `tokio_postgres::Client`
    * `tokio_postgres::Transaction`
    * `deadpool_postgres::Client`
    * `deadpool_postgres::Transaction`
  10. The Cornucopia workflow

    main

    To use Cornucopia in your project, follow these three steps:

    1. Write your PostgreSQL queries: Define the SQL you want to execute.
    2. Generate the crate: Use Cornucopia to run your queries against a database and generate a dedicated Rust crate containing type-safe interfaces for those queries.
    3. Import and use: Add the generated crate as a dependency to your main project and call the generated functions.