Giter Club home page Giter Club logo

fsharp.aws.dynamodb's Introduction

FSharp.AWS.DynamoDB

NuGet Badge

FSharp.AWS.DynamoDB is an F# wrapper over the standard AWSSDK.DynamoDBv2 library that represents Table Items as F# records, enabling one to perform updates, queries and scans using F# quotation expressions.

The API draws heavily on the corresponding FSharp.Azure.Storage wrapper for Azure table storage.

Introduction

Table items can be represented using F# records:

open FSharp.AWS.DynamoDB

type WorkItemInfo =
    {
        [<HashKey>]
        ProcessId : int64
        [<RangeKey>]
        WorkItemId : int64

        Name : string
        UUID : Guid
        Dependencies : Set<string>
        Started : DateTimeOffset option
    }

We can now perform table operations on DynamoDB like so:

open Amazon.DynamoDBv2
open FSharp.AWS.DynamoDB.Scripting // Expose non-Async methods, e.g. PutItem/GetItem

let client : IAmazonDynamoDB = ``your DynamoDB client instance``
let table = TableContext.Initialize<WorkItemInfo>(client, tableName = "workItems", Throughput.OnDemand)

let workItem = { ProcessId = 0L; WorkItemId = 1L; Name = "Test"; UUID = guid(); Dependencies = set [ "mscorlib" ]; Started = None; SubProcesses = [ "one"; "two" ] }

let key : TableKey = table.PutItem(workItem)
let workItem' = table.GetItem(key)

Queries and scans can be performed using quoted predicates:

let qResults = table.Query(keyCondition = <@ fun r -> r.ProcessId = 0 @>, 
                           filterCondition = <@ fun r -> r.Name = "test" @>)
                            
let sResults = table.Scan <@ fun r -> r.Started.Value >= DateTimeOffset.Now - TimeSpan.FromMinutes 1.  @>

Values can be updated using quoted update expressions:

let updated = table.UpdateItem(<@ fun r -> { r with Started = Some DateTimeOffset.Now } @>, 
                               preCondition = <@ fun r -> r.DateTimeOffset = None @>)

Or they can be updated using the SET, ADD, REMOVE and DELETE operations of the UpdateOp` DSL, which is closer to the underlying DynamoDB API:

let updated = table.UpdateItem <@ fun r -> SET r.Name "newName" &&& ADD r.Dependencies ["MBrace.Core.dll"] @>

Preconditions that are not upheld are signalled via an Exception by the underlying AWS SDK. These can be trapped using the supplied exception filter:

try let! updated = table.UpdateItemAsync(<@ fun r -> { r with Started = Some DateTimeOffset.Now } @>,
                                         preCondition = <@ fun r -> r.DateTimeOffset = None @>)
    return Some updated
with Precondition.CheckFailed ->
    return None 

Supported Field Types

FSharp.AWS.DynamoDB supports the following field types:

  • Numerical types, enumerations and strings.
  • Array, Nullable, Guid, DateTimeOffset and TimeSpan.
  • F# lists
  • F# sets with elements of type number, string or byte[].
  • F# maps with key of type string.
  • F# records and unions (recursive types not supported, nested ones are).

Supported operators in Query Expressions

Query expressions support the following F# operators in their predicates:

  • Array.length, List.length, Set.count and Map.Count.
  • String.StartsWith and String.Contains.
  • Set.contains and Map.containsKey NOTE: Only works for checking if a single value is contained in a set in the table. eg: Valid:table.Query(<@ fun r -> r.Dependencies |> Set.contains "mscorlib" @>) Invalid table.Query(<@ fun r -> set ["Test";"Other"] |> Set.contains r.Name @>)
  • Array.contains,List.contains
  • Array.isEmpty and List.isEmpty.
  • Option.isSome, Option.isNone, Option.Value and Option.get.
  • fst and snd for tuple records.

Supported operators in Update Expressions

Update expressions support the following F# value constructors:

  • (+) and (-) in numerical and set types.
  • Array.append and List.append (or @).
  • List consing (::).
  • defaultArg on optional fields.
  • Set.add and Set.remove.
  • Map.add and Map.remove.
  • Option.Value and Option.get.
  • fst and snd for tuple records.

Example: Representing an atomic counter as an Item in a DynamoDB Table

type private CounterEntry = { [<HashKey>] Id : Guid ; Value : int64 }

type Counter private (table : TableContext<CounterEntry>, key : TableKey) =
    
    member _.Value = async {
        let! current = table.GetItemAsync(key)
        return current.Value
    }
    
    member _.Incr() = async { 
        let! updated = table.UpdateItemAsync(key, <@ fun e -> { e with Value = e.Value + 1L } @>)
        return updated.Value
    }

    static member Create(client : IAmazonDynamoDB, tableName : string) = async {
        let table = TableContext<CounterEntry>(client, tableName)
        let throughput = ProvisionedThroughput(readCapacityUnits = 10L, writeCapacityUnits = 10L)        
        let! _desc = table.VerifyOrCreateTableAsync(Throughput.Provisioned throughput)
        let initialEntry = { Id = Guid.NewGuid() ; Value = 0L }
        let! key = table.PutItemAsync(initialEntry)
        return Counter(table, key)
    }

NOTE: It's advised to split single time initialization/verification of table creation from the application logic, see Script.fsx for further details.

Projection Expressions

Projection expressions can be used to fetch a subset of table attributes, which can be useful when performing large queries:

table.QueryProjected(<@ fun r -> r.HashKey = "Foo" @>, <@ fun r -> r.HashKey, r.Values.Nested.[0] @>)

the resulting value is a tuple of the specified attributes. Tuples can be of any arity but must contain non-conflicting document paths.

Secondary Indices

Global Secondary Indices can be defined using the GlobalSecondaryHashKey and GlobalSecondaryRangeKey attributes:

type Record =
    {
        [<HashKey>] HashKey : string
        ...
        [<GlobalSecondaryHashKey(indexName = "Index")>] GSIH : string
        [<GlobalSecondaryRangeKey(indexName = "Index")>] GSIR : string
    }

Queries can now be performed on the GSIH and GSIR fields as if they were regular HashKey and RangeKey Attributes.

NOTE: Global secondary indices are created using the same provisioned throughput as for the primary keys.

Local Secondary Indices can be defined using the LocalSecondaryIndex attribute:

type Record =
    {
        [<HashKey>] HashKey : string
        [<RangeKey>] RangeKey : Guid
        ...
        [<LocalSecondaryIndex>] LSI : double
    }

Queries can now be performed using LSI as a secondary RangeKey.

NB: Due to API restrictions, the secondary index support in FSharp.AWS.DynamoDB always projects ALL table attributes. NOTE: A key impact of this is that it induces larger write and storage costs (each write hits two copies of everything) although it does minimize read latency due to extra 'fetch' operations - see the LSI documentation for details.

Pagination

Pagination is supported on both scans & queries:

let firstPage = table.ScanPaginated(limit = 100)
printfn "First 100 results = %A" firstPage.Records
match firstPage.LastEvaluatedKey with
| Some key ->
    let nextPage = table.ScanPaginated(limit = 100, exclusiveStartKey = key)

Note that the exclusiveStartKey on paginated queries must include both the table key fields and the index fields (if querying an LSI or GSI). This is accomplished via the IndexKey type - if constructing manually (eg deserialising a start key from an API call):

let startKey = IndexKey.Combined(gsiHashValue, gsiRangeValue, TableKey.Hash(primaryKey))
let page = table.QueryPaginated(<@ fun t -> t.GsiHash = gsiHashValue @>, limit = 100, exclusiveStartKey = startKey)

Notes on value representation

Due to restrictions of DynamoDB, it may sometimes be the case that objects are not persisted faithfully. For example, consider the following record definition:

type Record = 
    {         
        [<HashKey>]
        HashKey : Guid

        Optional : int option option
        Lists : int list list
    }
    
let item = { HashKey = Guid.NewGuid() ; Optional = Some None ; Lists = [[1;2];[];[3;4]] }
let key = table.PutItem item

Subsequently recovering the given key will result in the following value:

> table.GetItem key
val it : Record = {HashKey = 8d4f0678-6def-4bc9-a0ff-577a53c1337c;
                   Optional = None;
                   Lists = [[1;2]; [3;4]];}

Precomputing DynamoDB Expressions

It is possible to precompute a DynamoDB expression as follows:

let precomputedConditional = table.Template.PrecomputeConditionalExpr <@ fun w -> w.Name <> "test" && w.Dependencies.Contains "mscorlib" @>

This precomputed conditional can now be used in place of the original expression in the FSharp.AWS.DynamoDB API:

let results = table.Scan precomputedConditional

FSharp.AWS.DynamoDB also supports precomputation of parametric expressions:

let startedBefore = table.Template.PrecomputeConditionalExpr <@ fun time w -> w.StartTime.Value <= time @>
table.Scan(startedBefore (DateTimeOffset.Now - TimeSpan.FromDays 1.))

(See Script.fsx for example timings showing the relative efficiency.)

TransactWriteItems

Using TransactWriteItems to compose multiple write operations into an aggregate request that will succeed or fail atomically is supported. See overview article by @alexdebrie

NOTE: while the underlying API supports combining operations on multiple tables, the exposed API does not.

The supported individual operations are:

  • Check: ConditionCheck - potentially veto the batch if the (precompiled) condition is not fulfilled by the item identified by key
  • Put: PutItem-equivalent operation that upserts a supplied item (with an optional precondition)
  • Update: UpdateItem-equivalent operation that applies a specified updater expression to an item with a specified key (with an optional precondition)
  • Delete: DeleteItem-equivalent operation that deletes the item with a specified key (with an optional precondition)
let compile = table.Template.PrecomputeConditionalExpr
let doesntExistCondition = compile <@ fun t -> NOT_EXISTS t.Value @>
let existsCondition = compile <@ fun t -> EXISTS t.Value @>

let key = TableKey.Combined(hashKey, rangeKey)
let requests = [
    TransactWrite.Check  (key, doesntExistCondition)
    TransactWrite.Put    (item2, None)
    TransactWrite.Put    (item3, Some existsCondition)
    TransactWrite.Delete (table.Template.ExtractKey item5, None) ]
do! table.TransactWriteItems requests

Failed preconditions (or TransactWrite.Checks) are signalled as per the underlying API: via a TransactionCanceledException. Use TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed to trap such conditions:

try do! table.TransactWriteItems writes
        return Some result
with TransactWriteItemsRequest.TransactionCanceledConditionalCheckFailed -> return None 

See TransactWriteItems tests for more details and examples.

It generally costs double or more the Write Capacity Units charges compared to using precondition expressions on individual operations.

Observability

Critical to any production deployment is to ensure that you have good insight into the costs your application is incurring at runtime.

A hook is provided so metrics can be published via your preferred Observability provider. For example, using Prometheus.NET:

let dbCounter = Prometheus.Metrics.CreateCounter("aws_dynamodb_requests_total", "Count of all DynamoDB requests", "table", "operation")
let processMetrics (m : RequestMetrics) =
    dbCounter.WithLabels(m.TableName, string m.Operation).Inc()
let table = TableContext<WorkItemInfo>(client, tableName = "workItems", metricsCollector = processMetrics)

If metricsCollector is supplied, the requests will set ReturnConsumedCapacity to ReturnConsumedCapacity.INDEX and the RequestMetrics parameter will contain a list of ConsumedCapacity objects returned from the DynamoDB operations.

Read consistency

DynamoDB follows an eventually consistent model by default. As a consequence, data returned from a read operation might not reflect the changes of the most recently performed write operation if they are made in quick succession. To circumvent this limitation and enforce strongly consistent reads, DynamoDB provides a ConsistentRead parameter for read operations. You can enable this by supplying the consistentRead parameter on the respective TableContext methods, e.g. for GetItem:

async {
    let! key : TableKey = table.PutItemAsync(workItem)
    let! workItem = table.GetItemAsync(key, consistentRead = true)
}

Note: strongly consistent reads are more likely to fail, have higher latency, and use more read capacity than eventually consistent reads.

Building & Running Tests

To build using the dotnet SDK:

dotnet tool restore dotnet build

Tests are run using dynamodb-local on port 8000. Using the docker image is recommended:

docker run -p 8000:8000 amazon/dynamodb-local

then

dotnet test -c Release

Maintainer(s)

The default maintainer account for projects under "fsprojects" is @fsprojectsgit - F# Community Project Incubation Space (repo management)

fsharp.aws.dynamodb's People

Contributors

aviavni avatar bartelink avatar chrsteinert avatar eiriktsarpalis avatar faldor20 avatar forki avatar kaashyapan avatar matti-avilabs avatar samritchie avatar sergey-tihon avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fsharp.aws.dynamodb's Issues

using Map.containsKey does not use expression attribute

Hello,

I have DynamoDB entries which hold a F# Map<string,string> called values, with keys following a specific schema (X+GUID, where X is a string with letters and digits AND special characters, namely dots, colons underscores and dashes (. : _ -), and GUID is a regular old guid also containing dashes.

Now when I use this library for a query like:
<@ fun p -> Map.containsKey s p.values @> |> template.PrecomputeConditionalExpr

I run into some problems, since the generated expression looks like this:
attribute_exists ( #ATTR6.xxx:xxx.xxx:xx.xxx:x.xx_xxxx-xx+xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx-xxxxxxxx)
that is, values is replaced with a expression attribute (I think that is the right DynamoDB term? Sorry, somewhat new to AWS), but my Map key is not.

Printing out the .Names of this ConditionExpression as well I can see that ATTR6 is mapped correctly [(#ATTR6, values)]

The issue here is that the expression contains illegal characters (running the query fails at the first : but I guess the dots would also do it) and I assume would need to use a expression attribute as well?

Is this something I can fix with correct usage of the library, a bug/oversight in the library, or something that cannot be supported?

Cheers!

Scan GSI

Doesn’t look like there’s an option to scan by GSI, add this as part of #27

Handling of ResourceNotFoundException

I notice that UpdateItemAsync doesn't handle 400: ResourceNotFoundExceptions and subsequently returns a 'TRecord, not a 'TRecord option. I have a scenario where I want to use an UpdateIfExists kind of function so I can achieve something in one request, not two. In my case, it wouldn't be particularly exceptional that it sometimes doesn't exist.

Is there a design reason why handling of ResourceNotFoundException isn't part of this library that I should be aware of? Why I might want to avoid that approach? (I'm new to DynamoDB...)

member __.UpdateItemAsync(key : TableKey, updater : UpdateExpression<'TRecord>,
?precondition : ConditionExpression<'TRecord>, ?returnLatest : bool) : Async<'TRecord> = async {
let kav = template.ToAttributeValues(key)
let request = new UpdateItemRequest(Key = kav, TableName = tableName)
request.ReturnValues <-
if defaultArg returnLatest true then ReturnValue.ALL_NEW
else ReturnValue.ALL_OLD
let writer = new AttributeWriter(request.ExpressionAttributeNames, request.ExpressionAttributeValues)
request.UpdateExpression <- updater.UpdateOps.Write(writer)
match precondition with
| Some pc -> request.ConditionExpression <- pc.Conditional.Write writer
| _ -> ()
let! ct = Async.CancellationToken
let! response = client.UpdateItemAsync(request, ct) |> Async.AwaitTaskCorrect
if response.HttpStatusCode <> HttpStatusCode.OK then
failwithf "PutItem request returned error %O" response.HttpStatusCode
return template.OfAttributeValues response.Attributes
}

Question on Combining Quotation Predicates

Hi there,

I'm relatively new to F#, so I assume I'm just thinking about this problem incorrectly but would love to get some feedback if possible.

I'm trying to take in optional query parameters from a request and use them to build up the Expr<T' -> bool> necessary to call the TableContext.ScanAsync method.

To build up that Expr<T' -> bool>, I'm using quotation slicing:

// Given the simplified record below
type Item = {
    Text : string
    Number : int
}

// Build the expressions based on incoming query parameters
// textQueryParam: string
// numQueryParam: int
let textQuery = <@ fun (item: Item) -> item.Text = textQueryParam @>
let numQuery = <@ fun (item: Item) -> item.Number = numQueryParam @>

let finalQuery = <@ fun (item: Item) -> (%textQuery) item && (%numQuery) item @>

However, this results in a Supplied expression is not a valid conditional. exception.

If I don't use quotation slicing, for instance, if I just do:

let finalQuery = <@ fun (item: Item) -> item.Text = textQueryParam && item.Number = numQueryParam @>

then it expectedly works just fine.

The only reason I'm trying to use slicing is that, in the real application, the query strings are optional - so I'd like to be able to dynamically construct the Expr<Item -> bool> based on which query strings are actually provided by the client and come in as Some.

My apologies if I'm just misunderstanding a language feature rather than anything with the FSharp.AWS.DynamoDB package, but any direction would be greatly appreciated. Thanks in advance.

Question: how to delete a record without the table key?

When I put data in a table, I provide a hash key and then I get a table key once the data is inserted.

But I am not able to keep that table key.

Is it possible to build it from the hashkey? or do I need to do a query to find it (and if so, how does it work?)

support for TTL

Any plans to implement the TimeToLive feature of DynamoDB?

Hash/sort keys and GSI hash/sort keys can not be set on same field

I'm trying to create a table that has a GSI with the Hash key and Sort key inverted.

The way I would do it is that I have the following record:

type TableItem =
    { [<HashKey>]
      [<GlobalSecondaryRangeKey(indexName = "Inverted")>]
      PrimaryKey: string
      [<RangeKey>]
      [<GlobalSecondaryHashKey(indexName = "Inverted")>]
      SortKey: string
      Model: Model }

This fails when doing a query by the GSI hash key and sort key because of this check: https://github.com/purkhusid/FSharp.AWS.DynamoDB/blob/710558033727f9cab1a7c1917146fc1cb5c5485b/src/FSharp.AWS.DynamoDB/TableContext.fs#L80-L80

I don't see any reason for not allowing this type of record since this is a very common thing to do with DynamoDB.

I could take a stab at fixing this if you would be OK with the change.

Question: How do you construct a TableKey for getItem?

The item "client.GetItem" takes a TableKey, but the TableKey constructor only takes unit. What is the proper way to construct a TableKey to pass to GetItem?

In the docs and unit tests I only see examples of a key being returned from PutItem

defaultArg/if_not_exists for the same attribute

I'm trying to figure out if there's any way to achieve the same end result as is given in the Preventing overwrites of an existing attrubute section from here:

aws dynamodb update-item \
    --table-name ProductCatalog \
    --key '{"Id":{"N":"789"}}' \
    --update-expression "SET Price = if_not_exists(Price, :p)" \
    --expression-attribute-values '{":p": {"N": "100"}}' \
    --return-values ALL_NEW

So far I haven't been successful in any of my attempts so wondering if there's something I'm missing? From digging around it seems if defaultArg would be the way to go but as far as I can tell the only way to construct a valid update expression using that is if the attribute being set and the one passed into defaultArg are not the same ones.

I've tried the following but the resulting expressions are invalid in both cases:

For optional attributes: SET p.Price (defaultArg p.Price price |> Some)
For non-optional attributes: SET p.Price (defaultArg (Some p.Price) price)

Updating with Map.remove raises Exception

Description

I tried using the Map.remove update expression and an exception was raised

I have tracked the issue down to

| SpecificCall2 <@ Map.remove @> (None, _, _, [keyE; AttributeGet attr]) when attr = parent ->

Repro steps

        table.Template.PrecomputeUpdateExpr
            <@ fun (sessionId : string) (entry: SessionModel) ->
                { entry with UserSessions = entry.UserSessions |> Map.remove sessionId  } @>

Specifically doesn't work for Precomputed Expressions

Expected behavior

Remove item from map and not raise exception

Actual behavior

Throws exception:
{Amazon.DynamoDBv2.AmazonDynamoDBException: Invalid UpdateExpression: Syntax error; token: "<", near: "#ATTR3<$" ---> Amazon.Runtime.Internal.HttpErrorResponseException: Exception of type 'Amazon.Runtime.Internal.HttpErrorResponseException' was thrown.\n at Amazon.Runtime.HttpWebRequestMessage.GetResponseAsync(CancellationToken cancellationToken) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\HttpHandler\_mobile\HttpRequestMessageFactory.cs:line 539\n at Amazon.Runtime.Internal.HttpHandler`1.InvokeAsync[T](IExecutionContext executionContext) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\HttpHandler\HttpHandler.cs:line 175\n at Amazon.Runtime.Internal.Unmarshaller.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext)\n --- End of inner exception stack trace ---\n at Amazon.Runtime.Internal.HttpErrorResponseExceptionHandler.HandleException(IExecutionContext executionContext, HttpErrorResponseException exception) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\ErrorHandler\HttpErrorResponseExceptionHandler.cs:line 60\n at Amazon.Runtime.Internal.ErrorHandler.ProcessException(IExecutionContext executionContext, Exception exception) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\ErrorHandler\ErrorHandler.cs:line 212\n at Amazon.Runtime.Internal.ErrorHandler.InvokeAsync[T](IExecutionContext executionContext) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\ErrorHandler\ErrorHandler.cs:line 104\n at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.EndpointDiscoveryHandler.InvokeAsync[T](IExecutionContext executionContext) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\Handlers\EndpointDiscoveryHandler.cs:line 79\n at Amazon.Runtime.Internal.CredentialsRetriever.InvokeAsync[T](IExecutionContext executionContext) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\Handlers\CredentialsRetriever.cs:line 98\n at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.RetryHandler.InvokeAsync[T](IExecutionContext executionContext) in D:\JenkinsWorkspaces\trebuchet-stage-release\AWSDotNetPublic\sdk\src\Core\Amazon.Runtime\Pipeline\RetryHandler\RetryHandler.cs:line 137\n at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.CallbackHandler.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.ErrorCallbackHandler.InvokeAsync[T](IExecutionContext executionContext)\n at Amazon.Runtime.Internal.MetricsHandler.InvokeAsync[T](IExecutionContext executionContext)\n at [email protected](Unit unitVar0) in C:\Users\humbo\source\repos\TaskBuilder.fs\TaskBuilder.fs:line 121\n at FSharp.Control.Tasks.TaskBuilder.tryWith[a](FSharpFunc`2 step, FSharpFunc`2 catch) in C:\Users\humbo\source\repos\TaskBuilder.fs\TaskBuilder.fs:line 165}

Known workarounds

Not using Precomputed expressions arguments which weren't really tested for Map update expressions. Instead pass the arguments into a separate function.

    let RemoveSession(table: TableContext<SessionModel>) (sessionId : string) =
        table.Template.PrecomputeUpdateExpr
            <@ fun (entry: SessionModel) ->
                {entry with UserSessions = entry.UserSessions |> Map.remove sessionId } @>

Related information
*AWSSDK.DynamoDBv2 Version="3.3.101.58"
*FSharp.AWS.DynamoDB Version="0.8.0-beta"
*.Net CLI 2.2.401
*MacOs 10.14.4 (18E227)

Enhanced support for single-table design

This is somewhat related to the caveat mentioned in the TransactWriteItems section regarding multi-table operations not being supported.

For a single-table design this limitations also pops if there are multiple instances of TableContext using the same table name, and a need arises to write more than one record type in the same request:

let baseContext : TableContext<BaseSchema> = ...
let contextFoo : Context<Foo> =  baseContext.WithRecordType<Foo>()
let contextBar : Context<Bar> =  baseContext.WithRecordType<Bar>()

let fooItems : Foo list = ...
let barItems : Bar list = ...

async {
  // Separate DynamoDB requests are needed here
  let! _ = contextFoo.BatchPutItemsAsync fooItems
  let! _ = contextBar.BatchPutItemsAsync barItems
}

The most straightforward way of resolving the immediate problem would be to make the AttributeValue helper methods on RecordTemplate here public, and from there just use the SDK to make the necessary request.

If there is another approach that offers a solution that doesn't require working with the SDK directly, and maybe solves the problem more generally for transactions as well I'd be happy to take a look, just let me know.

Support DynamoDB streams

From #59 (comment)

Currently if we write to a table using the table context it will get automatically serialised with the internal picklers.

Once this same data hits the DynamoDB Stream, we can't deserialise it again because the TableContext serialisation code is all private in the library.

Look into supporting Stream -> Record deserialisation so that F# code can consume Dynamo changes.

Questions to resolve:

  • should this just cover the DynamoDB Streams Client API directly?
  • should Kinesis streams also be supported?
  • should Lambda trigger events also be supported?

Round trip changes the offset of DateTimeOffset

Description

The offset information of DateTimeOffset is lost when it is put to DynamoDb.

Repro steps

open System
open Amazon
open Amazon.DynamoDBv2
open FSharp.AWS.DynamoDB

let dynamoDbClient = new AmazonDynamoDBClient("AccessKey", "SecretKey", RegionEndpoint.EUWest1)

type TestTable =
    { [<HashKey>] Id : int
      Timestamp : DateTimeOffset }

let table = TableContext.Create<TestTable>(dynamoDbClient, tableName = "TestTable", createIfNotExists = true)

table.PutItem({ Id = 1; Timestamp = DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan(0, 0, 0))})
table.PutItem({ Id = 2; Timestamp = DateTimeOffset(2016, 1, 1, 0, 0, 0, TimeSpan(5, 0, 0))})

let r1 = table.Query(<@ fun r -> r.Id = 1 @>)
// val r1 : TestTable [] = [|{Id = 1;
//                            Timestamp = 1.1.2016 2:00:00 +02:00;}|]
let r2 = table.Query(<@ fun r -> r.Id = 2 @>)
// val r2 : TestTable [] = [|{Id = 2;
//                            Timestamp = 31.12.2015 21:00:00 +02:00;}|]

Expected behavior

Offset should be the same as it was when the item was inserted.

Actual behavior

Returned DateTimeOffset represents the same instant, but with the offset of local time zone.

Known workarounds

Use string instead of DateTimeOffset.

Related information

  • FSharp.AWS.DynamoDB version: 0.5.0-beta

Feature Request: recursive record types

This is a great library, very easy to use!

What are some of the difficulties in adding recursive record type support? I wouldn't mind taking a shot at the PR, I'm just not really sure if it's already been attempted.

StartsWith query condition not working as expected

Description

I'm trying to execute a query using StartsWith in the keyCondition, but it throws. Is this supported?

Repro steps

I have a type Lock:

type Lock = {
    [<HashKey>]
    key:string
    user_id:int
    created: int64
    ttl:int64 option
}

Being used as follows:

let getIneligibleLocks (userId:int) (keyPrefix:string) =
    let locks = semaphoreTable.Query(keyCondition = <@ fun r -> r.key.StartsWith keyPrefix @>)
    let ineligibleLocks = locks |> Array.filter(fun l -> l.user_id <> userId)
    ineligibleLocks

Expected behavior

I expect this to not throw.

Actual behavior

I get the following error:

key conditions must satisfy the following constraints:
* Must only reference HashKey & RangeKey attributes.
* Must reference HashKey attribute exactly once.
* Must reference RangeKey attribute at most once.
* HashKey comparison must be equality comparison only.
* Must not contain OR and NOT clauses.
* Must not contain nested operands.              

Parameter name: keyCondition

Also if I change the code to:

let getIneligibleLocks (userId:int) (keyPrefix:string) =
    let locks = semaphoreTable.Query(keyCondition = <@ fun r -> r.key.StartsWith keyPrefix  = true @>)
    let ineligibleLocks = locks |> Array.filter(fun l -> l.user_id <> userId)
    ineligibleLocks

I get this error:

Supplied expression is not a valid conditional.
Parameter name: expr

Known workarounds

None that I know of.

Related information

Using latest version, 0.8 beta on .NET Core 2.2, macOS.

Why is a scan all tautological?

I'm trying to run

myTable.Scan <@ fun _ -> true @>

but am getting

supplied query is tautological

Is there a reason for this?

Other client libraries tend to be alright with this, for example in the official document sdk client for node you can do

dynamodb.scan({
  TableName: myTableName
}, (error, data) => {
  if (error) {
     //...
  } else {
     //...
  }
});

Request for maintainers

I'm no longer in a capacity to maintain this library, currently working exclusively with Azure at a direct competitor of Amazon's. This is a public request for new maintainers, who could potentially also sponsor AWS resources for driving tests.

Rename to FSharp.Data.DynamoDB?

Great to see! Now following this. Just say if you want to transfer to fsprojects.

I suppose this will eventually need a two-level namespace name to follow the guidelines, e.g.

FSharp.Data.DynamoDB

Cheers!
Don

Support ReturnValuesOnConditionCheckFailure

For ConditionCheckFailedException, its now possible to have the exception include the content of the conflicting item in the same roundtrip, by setting ReturnValuesOnConditionCheckFailure flag in the request)

The library should either turn that on unconditionally (there is no RU charge impact, though obviously the response will be larger)

(NOTE the AWS SDK's exception type has long been plumbed for holding the content in question - the new bit is the fact that a DDB server will now include it in the response for single Item APIs as TransactWriteItems has done for some time).

It's unclear to me whether a SDK update will be necessary (my money is on no though)

'default values not supported for unions' on item delete

I’m seeing InvalidOperationException - default values not supported for unions. on TableContext.DeleteItemAsync() calls. It looks like this is the result of specifying ReturnValue.ALL_OLD and attempting to unpickle the record after deleting a nonexistent row. There are probably a couple of options here:

  1. Throw a better error message that describes what’s happening - it took me ages to work out what was going on, not least because the record was gone when I was trying to reconstruct what happened. However, this approach is inconsistent with the Dynamo DeleteItem API which is designed to be idempotent.
  2. Specify ReturnValue.NONE and return unit instead of the old row. This would be a breaking API change and would probably annoy people out there using the deleted row.
  3. Implement both 1 & 2 by adding new DeleteItem methods (DeleteItemNoReturn or DeleteItemIgnoreOld or something similarly badly-named). Not a breaking change but it would involve coming up with a sensible name.
  4. Change the return type to 'TRecord option and unpickling the response only if it exists - also a breaking API change but probably closest to the underlying Dynamo API.

Either way, it would be great to be able to delete items without either fetching it first, and/or catching & swallowing pickler exceptions.

Feature Request: Migration

Probably the biggest want for me is a nice way to handle table 'schema' changes; 9 times out of 10 this is just some way to come up with a default value for a new non-optional field.

A minimal approach would be a TableContext hook that’s invoked when a missing (required) field is encountered, which is passed the untyped row data and should return the new value.

A more substantial migration feature would be storing schema versions in the table and running a schema upgrade function on load.

Both options would benefit from having a 'scanUpgrade' function that pre-emptively loads, upgrades & saves all the rows in the table, so you’ve got the choice between that or lazily upgrading on demand.

Would appreciate any thoughts or suggestions here.

Pagination support

Placeholder issue/design discussion for Scan & Query pagination support

  1. Paginated queries/scans will take an ?exclusiveStartKey : TableKey parameter and return a PaginationResult<'TRecord> defined as { Records : 'TRecord[]; LastEvaluatedKey : TableKey option }
  2. (Tentatively) these will be added as new methods QueryPaginated etc, although I’m tempted to convert the existing methods to return paginated results as the default and add QueryAll to perform unbounded queries, even though this is a breaking change.
  3. New convenience accessors on the PaginationResult - eg member x.GetLastEvaluatedHashKey<'HashKey>() : 'HashKey option to enable serialisation of key values for client-side pagination. For a query this will typically only be the range key as the hash key should already be known. For a scan two key values may be required.

Unfortunately I’ve just discovered GSI queries also require the table key attributes in addition to the index. I’ll be able to get this to work if the returned LastEvaluatedKey is used as an opaque parameter for the next query, but manually constructing an ExclusiveStartKey from client requests is going to be painful.

Empty string support

Been running into the empty strings not supported by DynamoDB error from here quite a bit. While it might be an indication of an underlying "issue" with the data, I'm wondering if this might be outdated after coming across this article? Is it worth updating this - and if so, should the aim be to have different types of picklers for key (enforce non-empty strings) and non-key (allow non-empty strings) attributes?

Sparse GSI support

Description

If I annotate an option attribute with [<GlobalSecondaryHashKey>], at runtime I get an exception DynamoDB Key attributes do not support serialization attributes, which means I’m not able to use a sparse Global Secondary Index.

Repro steps

Define a record type and table like

type Test = 
    {
            [<HashKey>]
            Id : int

            [<GlobalSecondaryHashKey(indexName = "Name-index")>]
            Name : string option
    }

 let table = TableContext.Create<Test>(dynamoDb, tableName = "Test", createIfNotExists = true)

Expected behavior

Should create the table and allow me to query by Name, and also allow me to set Name to None to remove it from the index

Actual behavior

Throws an exception System.ArgumentException: DynamoDB Key attributes do not support serialization attributes.

Known workarounds

None. Making the string not optional is a no go as "empty strings not supported by DynamoDB". Best option is to drop the GSI and manually maintain separate tables.

Related information

library version 0.5.0-beta
mono 4.4.2
F# 4.1

PutItem precondition not working as expected

Description

Precondition on PutItem isn't working as expected.

Repro steps

  1. Add item with same Guid
table.PutItemAsync (newEntry, precondition = (<@ fun entry -> entry.Guid <> newEntry.Guid @>))

Expected behavior

Throw an exception because entry with that Guid has already been added

Actual behavior

Exception is not thrown and item is added

Known workarounds

No work arounds yet

Related information

*AWSSDK.DynamoDBv2 Version="3.3.101.58"
*FSharp.AWS.DynamoDB Version="0.8.0-beta"
*.Net CLI 2.2.401
*MacOs 10.14.4 (18E227)

Support for Task-based layer

Logging this as a placeholder - it's more of a nice to have than something that I'd be personally investing the time to execute on at present.

In Equinox and Propulsion, I've recently transitioned to task from async. This means Equinox.DynamoStore has a layer which is all task and/or TaskSeq stuff, which then needs to call out to FSharp.AWS.DynamoDB. Aside from the perf cost, the layers of redundant mapping also make it harder to diagnose/reason about exception mappings and the like.

If one was building this lib today, arguably one would build it primarily with task in the first instance.

For Equinox's purposes, the ideal would be to introduce a layer that exposes a Task-based API, and then layer the existing one over that. I suspect that over time there will be a growing number of usage scenarios where the caller is task-based.

One concern would be that realistically one would need to expose a ?ct (or perhaps a non-optional CancellationToken arg) absolutely everywhere ... unless one waits for a long term cancellableTask impl to gain currency).

Support `TransactWriteItems` across multiple tables/record types

See discussion in #59 - there would be value in supporting transactional writes across multiple tables (or contexts in the case of multiple single-table contexts).

Probably the main question here is around the 'base' context - eg the below suggested design approach from @bartelink:

let dynamo : DynamoContext = DynamoContext(client : IAmazonDynamoDB, ?metricsCollector : RequestMetrics -> unit)
let! residual = dynamo.Batch.Write( [contextFoo.Batch.Put fooItems; contextBar.Batch.Put barItems])

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.