StructuredQueries

repository·main·Indexed 43 days ago

https://github.com/pointfreeco/swift-structured-queries

A Swift library for building type-safe, composable SQL statements. It provides a query-building layer with features like schema mapping via the @Table macro, support for SELECT, INSERT, UPDATE, and DELETE, and Common Table Expressions (CTEs). Designed to be database agnostic and optimized for SQLite, it focuses on generating SQL checked at compile-time rather than database execution.

Tokens
37.1K
Snippets
138
Records
202
Agent score
51%

What's inside StructuredQueries

  1. Overview of StructuredQueriesSQLiteCore

    main
    StructuredQueriesSQLiteCore provides core SQLite extensions for the StructuredQueries framework. It extends the base functionality with SQLite-specific features such as support for custom database functions, specialized query representations, custom collations, and casting mechanisms tailored for SQLite's type system.
  2. Overview of StructuredQueries

    main

    StructuredQueries is a Swift library designed for building SQL statements in a safe, expressive, and composable manner. It is not an ORM, but rather a tool for generating SQL that reads closely to the language itself while providing type safety to prevent runtime errors caused by typos or type mismatches.

    Key features include:

    • Schema Mapping: Use the @Table macro to map Swift types to database tables.
    • Type-Safe Query Building: Access rich APIs for SELECT, INSERT, UPDATE, DELETE, joins, and common table expressions.
    • Safe SQL Escapes: Use the #sql macro to introduce custom SQL strings safely at any granularity.
    • Database Agnostic: Designed to support various SQL databases (SQLite, MySQL, Postgres), though currently optimized for SQLite.

    Note: This library focuses exclusively on building SQL statements. It does not include database drivers to execute them. You must integrate it with a library that handles actual database requests (e.g., GRDB via SQLiteData).

  3. Overview of StructuredQueries modules

    main

    The StructuredQueries library is designed for building SQL in a type-safe, expressive, and composable manner. It is organized into several modules depending on your needs:

    • StructuredQueriesCore: The foundational module containing the core functionality. StructuredQueries automatically exports this.
    • StructuredQueriesSQLiteCore: Provides SQLite-specific helpers, including full-text search and type-safe temporary triggers.
    • StructuredQueriesSQLite: Includes everything from StructuredQueriesSQLiteCore plus SQLite-specific macros like @DatabaseFunction.

    For general library usage, refer to the StructuredQueriesCore documentation.

  4. Seed a Common Table Expression (CTE) using `@Table` or `@Selection` values

    main

    You can use Values to seed a query with data that uses specific column names. By passing rows defined by @Table or @Selection (using their .Columns property) into a Values statement, the automatic SQLite names ("column1", etc.) are aliased back to your actual domain model column names. This is particularly useful for creating Common Table Expressions (CTEs).

    Select(
      Values {
        HighScore.Columns(score: 100, player: "Blob")
      }
    )
    // SELECT "column1" AS "score", "column2" AS "player"
    // FROM (VALUES (100, 'Blob')) AS "highScores"
  5. Map query results to types using QueryBindable and QueryBinding

    main

    To bridge the gap between raw database values and your Swift models, use the binding system:

    • QueryBindable: A protocol used to make a type capable of being bound to a query.
    • QueryBinding: Represents the mapping logic for a specific field or value.
    • QueryBindingError: An error type thrown when a binding operation fails (e.g., type mismatch or missing data).

    Use these to control how specific database columns are transformed into Swift properties during query execution.

  6. Understand Select supporting types: SelectOf and ValuesColumns

    main

    The Select API relies on specific supporting types to manage data shapes and column selection:

    • SelectOf: A type used to represent the projection or the specific subset of data selected by a query.
    • ValuesColumns: A type used to manage and represent the collection of columns being operated upon in a query.
  7. Use Drafts to insert new records

    main

    When a type conforms to PrimaryKeyedTable, the @Table macro generates a nested Draft type. This type contains all the same fields as the original type, but the primary key field is optional. This is useful for inserting rows where the database is responsible for generating the ID (e.g., auto-incrementing integers or UUIDs).

    You can use the insert(_:onConflict:) method to insert a row using a Draft object. To retrieve the generated ID or the full record after insertion, chain the returning(_:) method.

    // Inserting a draft and returning the new ID
    Reminder
      .insert { Reminder.Draft(title: "Get groceries") }
      .returning(\.id)
    
    // Inserting a draft and returning the full record
    Reminder
      .insert { Reminder.Draft(title: "Get groceries") }
      .returning(\.self)
  8. Decode query results using QueryDecodable and QueryDecoder

    main

    For complex data mapping, StructuredQueries provides a decoding layer similar to Decodable:

    • QueryRepresentable: A protocol for types that can be represented in a query format.
    • QueryDecodable: A protocol for types that can be decoded from query results.
    • QueryDecoder: The engine responsible for traversing query results and instantiating your Swift types.
    • QueryDecodingError: Errors encountered during the decoding process, such as missing keys or invalid data formats.

    This layer is ideal when you want to map database rows to nested or complex Swift structures.

  9. Handle SQL bindings and prevent injection

    main

    When you interpolate a Swift value into an #sql string, the macro automatically captures it as a separate statement binding (using ? in the underlying SQL) to protect against SQL injection.

    String Interpolation Rules

    To avoid ambiguity and deprecation warnings when working with strings, use explicit interpolation methods:

    1. Standard Binding (Recommended): Use \(bind: value) to explicitly indicate you want to bind a string as a value. This is the safest way to handle user input.
    2. Raw SQL Interpolation (Dangerous): Use \(raw: value) to inject a string directly into the SQL text. Warning: This makes your queries susceptible to SQL injection attacks. Use this only for trusted, static SQL fragments.
    MethodUsageResulting SQL
    Implicit\(searchText)... LIKE ? (with warning)
    Explicit Bind\(bind: searchText)... LIKE ? (safe)
    Raw\(raw: searchText)... LIKE 'value' (unsafe)
    let searchText = "%get%"
    
    // 1. Explicitly bind as a value (Safe)
    #sql("SELECT ... WHERE title LIKE \(bind: searchText)", as: Reminder.self)
    
    // 2. Interpolate as raw SQL (Dangerous - avoid!)
    #sql("SELECT ... WHERE title LIKE '\(raw: searchText)'", as: Reminder.self)