Nanyx

SQL utilities

Database access through a connection context the caller supplies with use.

module sqlUtils

-- Database access through contexts: a function that needs a connection says
-- so in its type, and the caller supplies one with `use`.

type SqlPrimitive = #sqlInt(int) | #sqlString(string)

type SqlError = #duplicateKey | #sqlError(string)

type PreparedStatement = (
  setInt: (int, int) -> ()
  setString: (int, string) -> ()
  executeUpdate: () -> #some(int) | #error(SqlError)
)

context Connection = (
  prepare: string -> PreparedStatement
)

context DataSource = (
  getConnection: () -> Connection
)

def PreparedStatement.setArgument: (PreparedStatement, int, SqlPrimitive) -> () = {
  | statement, position, #sqlInt(value) -> statement.setInt(position, value)
  | statement, position, #sqlString(value) -> statement.setString(position, value)
}

export def executeUpdate: [Connection] (string, list(SqlPrimitive)) -> #some(int) | #error(SqlError)
  = { query, args ->
    def statement = prepare(query)
    for args, 1.. | arg, position -> statement \setArgument(position, arg)
    statement.executeUpdate()
  }

-- Callers that treat a duplicate key as "nothing to do" narrow the error with `except`
export def insertIgnoringDuplicates
  : [Connection] (string, list(SqlPrimitive)) -> #some(int) | #error(#sqlError(string))
  = { query, args ->
    executeUpdate(query, args) except #error(#duplicateKey) -> #some(0)
  }

export def withConnection: [DataSource] ([Connection] () -> a) -> a = { action ->
  def connection = getConnection()
  use connection in action()
}

export def addUser: [DataSource] (id: int, name: string) -> #some(int) | #error(#sqlError(string))
  = { id, name ->
    withConnection {
      insertIgnoringDuplicates("insert into users (id, name) values (?, ?)", [#sqlInt(id), #sqlString(name)])
    }
  }

This program lives in the repository as examples/sql-utils.nyx.