Loading…

RethinkDb.Driver.FSharp
Functions

Getting Oriented

Open an Extra Namespace

In order to access the functions, you will need to open RethinkDb.Driver.FSharp.Functions in addition to its parent namespace. This was done to allow RethinkDb.Driver.FSharp to be opened without bringing in all of the function names. As ReQL commands are common words (insert, update, get, etc.), they have a much higher likelihood of name collisions with user code that things like rethink and UpperBound.

There's No Overloading in Functions!

Unlike the Domain Specific Language (DSL), plain functions do not support overloading. However, by following patterns, this API is still pretty easy to discover.

Functions also can support a combination of the above, as well as Async<'T> and synchronous 'T versions; asyncWriteResultWithOptArgsAndCancel is probably close to the longest function name. It's more verbose than the DSL, but for those who view overloading as cognitive overhead, they will likely prefer this version.

(re)Trying Times

Version beta-03 fixed an issue with the withRetry[x] functions; if you are seeing errors with code that should be working, ensure you are on that version.

Using the Functions

If you read through the concepts page, you have seen some examples; let's look at several scenarios in depth.

Retrieving an Object by Its ID

ReQL will let you start a query by specifying a database (with r.Db()) or by specifying a table (with r.Table()); in the latter case, the query will use the default database for the connection. Within the functions, both db and fromTable may be starting places. Assuming the default database is “example”, the following two queries are equivalent:

// Type: string -> IConnection -> Task<User option>
let findUserById (userId : string) conn = backgroundTask {
    let! namedDb =
        db "example"
        |> table "User"
        |> get userId
        |> runResult<User>
        |> asOption
        |> withRetryDefault
        |> withConn conn
    let! defaultDb =
        fromTable "User"
        |> get userId
        |> runResult<User>
        |> asOption
        |> withRetryDefault
        |> withConn conn
    // Both namedDb and defaultDb should be pointing to the same object
    return namedDb
}

Notice that runResult is generic; giving it a type means that the results will have that type. Also, assuming User is a [<CLIMutable>]-decorated record, piping it through asOption will transform a no-find (null) to an option.

Note also that unlike the rethink computation expression, which usually defines an action that can be executed when it is passed a connection, this style performs the action. To accomplish the other behavior, you would have to move the conn parameter:

// Type: string -> (IConnection -> Task<User option>)
let findByUserId (userId : string) =
    fun conn -> backgroundTask {
        // do stuff
    {

Either form is acceptable; this is a matter of style preference.

Retrieving a List of Objects

Here is a query to get a list of active users:

// Type: IConnection -> Task<User list>
let findActiveUsers conn = backgroundTask {
    return!
        fromTable "User"
        |> getAllWithIndex [ true ] "isActive"
        |> orderByFunc (fun row -> r.Array (row["lastName"], row["firstName"]))
        |> runResult<User list>
        |> withRetryDefault
        |> withConn conn
}

Here, we use the version of getAll that specifies an index name, and the version of orderBy that uses a ReQL function.

Inserting Data

Objects can be inserted one at a time (insert) or in multiples (insertMany). Inserts also have an “on conflict” optional argument that specifies what to do if a document with the given ID already exists in the table. The below example attempts to insert a user, specifying to update the existing record if it already exists; it also will throw an exception if there is an error.

// Type: User -> IConnection -> Task<unit>
let addUser (user : User) conn = backgroundTask {
    let! _ =
        fromTable "User"
        |> insertWithOptArgs user [ OnConflict Update ]
        |> runWrite
        |> withRetryDefault
        |> withConn conn
}

Updating Data

In ReQL, update can be called on a single record or a collection of records, including an entire table. The concepts page showed a complex update-with-function query; we'll go much more basic here, updating our user's first and last name. We will also return true if we updated anything, or false if not.

// Type: User -> IConnection -> Task<bool>
let updateUser (user : User) conn = backgroundTask {
    let! result =
        fromTable "User"
        |> get user.id
        |> update {| lastName = user.lastName; firstName = user.firstName |}
        // or
        // update (r.HashMap("lastName", user.lastName).With ("firstName, user.firstName))
        |> runWrite
        |> withRetryDefault
        |> withConn conn
    }
    return result.Replaced > 0UL
}

Deleting Data

Like update, delete can empty a table; be sure to get, getAll, filter, or however you need to identify the documents to be deleted before calling delete. For our example, we'll delete all inactive users, and return how many users we deleted.

// Type: IConnection -> Task<int>
let deleteInactiveUsers conn = backgroundTask {
    let! result =
        fromTable "User"
        |> getAllWithIndex [ false ] "isActive"
        |> delete
        |> runWrite
        |> withRetryDefault
        |> withConn conn
    }
    return int result.Deleted
}

Other Operations

This same pattern works for manipulating databases, tables, and indexes. dbList, tableList, and indexList all return string list, and the [x]create and [x]drop commands work with runWrite, Results, and the like. For operations that start with a table, the functions that do not require db to be called end with [from/in]Default (ex. tableCreate's signature is string -> Db -> TableCreate, while tableCreateInDefault's signature is string -> TableCreate).

You can view the entire list of functions to see what is available; and, if you see something missing, open an issue!