ormin ORM

repository·master·Indexed 41 days ago

https://github.com/araq/ormin

A lightweight ORM and prepared SQL statement generator for Nim that emphasizes compile-time safety and performance. It features a Nim-based DSL for queries to prevent SQL injections, compile-time validation of types and schema, automatic join generation, and first-class JSON support. Supports SQLite and PostgreSQL backends.

Tokens
2.8K
Snippets
12
Records
15
Agent score
2%

What's inside ormin

  1. Overview of ormin

    master

    ormin is a lightweight, prepared SQL statement generator for Nim. It provides a Nim-based DSL for queries to prevent runtime syntax errors and SQL injections.

    Key features include:

    • Compile-time query checking: Validates types, table names, and column names at compile time.
    • Automatic join generation: Automatically computes natural joins based on table relations.
    • High performance: Generates prepared statements that match the speed of low-level hand-written API calls.
    • First-class JSON support: Allows direct use of JSON without explicit row-to-JSON conversions.
  2. Overview of additional ormin facilities

    master

    Ormin provides several high-level abstractions for building networked applications:

    • Protocol DSL: Use the protocol macro to define paired server/client handlers that communicate via JSON. It uses keywords like recv, broadcast, and send. Every server block must have a corresponding client block.
    • JSON Dispatcher: Use createDispatcher to map textual commands to Nim procedures.
    • WebSocket Server: Use serverws to run a small WebSocket server that supports broadcasting messages to specific receivers via the serve procedure.
  3. Handle Query Return Types

    master

    The return type of a query: block depends on the selection and clauses used:

    • Multiple columns: A sequence of tuples containing the inferred Nim types.
    • Single column: A sequence of that specific Nim type (e.g., seq[string]).
    • JSON output: Use produce json to return JsonNode objects instead of Nim tuples. Use produce nim to force standard Nim results if produce json was previously set.
    • Single row: Using returning or limit 1 (where 1 is a constant) returns a single value or tuple instead of a sequence.

    Warning: If you want to return a sequence, use an integer variable for the limit instead of a constant 1.

    ```nim
    # Sequence of tuples
    let threads = query:
      select thread(id, name)
    
    # Sequence of simple Nim types
    let ids = query:
      select thread(id)
    
    # JSON result
    let threadJson = query:
      select thread(id, name)
      produce json
    
    # Single value returning (via returning)
    let newId = query:
      insert thread(name = ?
  4. Use Static Schema for compile-time SQL embedding

    master

    You can embed SQL files at compile time to ensure schema integrity.

    Option 1: Using staticLoad Import ormin/db_utils and use staticLoad("filename.sql"). This returns a DbSql type after performing sanity checks. You can then pass this constant to createTable or dropTable.

    Option 2: Using importModel with includeStatic = true When calling importModel, set includeStatic = true. This skips generating a .nim file and instead builds model metadata from the SQL file at compile time, automatically exposing a sqlSchema constant.

    import ormin/db_utils
    
    const schema = staticLoad("model.sql")
    
    db.createTable(schema)
    db.createTable(schema, "quoted table")
    db.dropTable(schema)
    db.dropTable(schema, "quoted table")
    import ../ormin
    importModel(DbBackend.sqlite, "model_sqlite", includeStatic = true)
    
    db.createTable(sqlSchema)
    db.dropTable(sqlSchema, "tb_timestamp")
  5. Use the Ormin Query DSL

    master

    Ormin uses query: blocks to generate prepared statements at compile time.

    Placeholder Syntax

    • ? is used for Nim values.
    • % is used for JSON values (splicing JsonNode trees directly).
    • !! is used to splice raw, vendor-specific SQL fragments.

    Supported Clauses

    Common SQL clauses are supported, including with (CTEs), where, joins, orderby, groupby, limit, offset, exists, distinct, window expressions (over), set operations (union, intersect, except), returning, and upserts (onconflict with donothing or doupdate).

    Automatic Joins

    If you refer to columns from related tables, Ormin can automatically generate joins based on existing foreign keys, reducing boilerplate.

    # Select recent rows with a Nim parameter
    let recentMessages = query:
      select Messages(content, creation, author)
      orderby desc(creation)
      limit ?maxMessages
    
    # Automatic join generated from foreign keys
    let postsWithAuthors = query:
      select Post(title)
      join Author(name)
      where author.name == ?userName
  6. Use inline query blocks with local database connections

    master

    Inline query blocks resolve the db identifier from the current lexical scope. This allows you to pass a specific DbConn into a procedure to override a global connection, enabling complex database logic within scoped procedures.

    proc loadUser(db: DbConn; userId: int): User =
      let row = query:
        select user(id, name, email)
        where id == ?userId
        limit 1
    
      User(id: row.id, name: row.name, email: row.email)
  7. Set up schema and database connection

    master

    To use ormin, follow these two steps:

    1. Generate a model from SQL: Place your schema in an .sql file and use importModel. This runs ormin_importer to include the generated Nim code. Alternatively, use includeStatic = true to build the model metadata directly from the SQL file at compile time.
    2. Create a database connection: Ormin requires a global connection named db to issue queries. You must use the driver matching your backend (SQLite or PostgreSQL) and open the connection using Nim's database modules.
  8. Perform Upserts (SQLite/PostgreSQL)

    master

    You can perform upsert operations using onconflict combined with either donothing() (to ignore duplicates) or doupdate(...) (to update existing rows).

    Note: A plain INSERT ... VALUES does not support a where clause. To use a conditional update during an upsert, you must use the onconflict(...)+doupdate(...)+where ... pattern.

    # Upsert on conflict (SQLite/PostgreSQL)
    query:
      insert tb_nullable(id = ?id, note = ?note)
      onconflict(id)
      doupdate(note = ?note)
    
    # Conditional upsert update
    query:
      insert tb_nullable(id = ?id, note = ?note)
      onconflict(id)
      doupdate(note = ?note)
      where note != ?note
    
    # Ignore duplicates
    query:
      insert tb_nullable(id = ?id, note = ?note)
      onconflict(id)
      donothing()
  9. Configure SQLite backend

    master

    To use SQLite, import the ormin module, call importModel with DbBackend.sqlite, and initialize a global db connection.

    Note: Ormin handles quoted table names in dropTable. If you need the legacy behavior (using normalized lookup names instead of preserved SQL identifiers), use the compile flag -d:orminLegacySqliteDropNames.

    import ../ormin
    importModel(DbBackend.sqlite, "model_sqlite")
    let db {.global.} = open(":memory:", "", "", "")
  10. Configure PostgreSQL backend

    master

    To use PostgreSQL, import the postgres module along with ormin, call importModel with DbBackend.postgre, and initialize a global db connection using your database credentials.

    import postgres
    import ../ormin
    importModel(DbBackend.postgre, "model_postgre")
    let db {.global.} = open("localhost", "user", "password", "dbname")
  11. Create lazy iterators with createIter

    master

    Use createIter to transform a query into a Nim iterator. This allows you to yield rows lazily, which is more memory-efficient for large result sets. The iterator accepts parameters matching the ? or % placeholders in the query.

    createIter postsIter:
      select post(id, title)
      where author == ?userId
    
    for row in db.postsIter(userId):
      echo row.title