From 45b02fdbff31bc3b77d65eb5858623792e994807 Mon Sep 17 00:00:00 2001 From: Eirik Tsarpalis Date: Mon, 21 Mar 2016 20:26:52 +0200 Subject: [PATCH] implement projection expressions; minor bugfixes and api improvements --- .../Expression/ExpressionContainers.fs | 23 +- .../Expression/ProjectionExpr.fs | 103 ++++ src/FSharp.AWS.DynamoDB/Extensions.fs | 31 + .../FSharp.AWS.DynamoDB.fsproj | 4 +- .../Picklers/RecordPickler.fs | 13 +- src/FSharp.AWS.DynamoDB/RecordTemplate.fs | 10 + src/FSharp.AWS.DynamoDB/TableContext.fs | 530 +++++++++++++----- .../FSharp.AWS.DynamoDB.Tests.fsproj | 3 +- .../ProjectionExpressionTests.fs | 190 +++++++ .../RecordGenerationTests.fs | 12 +- .../SimpleTableOperationTests.fs | 11 +- 11 files changed, 778 insertions(+), 152 deletions(-) create mode 100644 src/FSharp.AWS.DynamoDB/Expression/ProjectionExpr.fs create mode 100644 src/FSharp.AWS.DynamoDB/Extensions.fs create mode 100644 tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs diff --git a/src/FSharp.AWS.DynamoDB/Expression/ExpressionContainers.fs b/src/FSharp.AWS.DynamoDB/Expression/ExpressionContainers.fs index 0c7fa38..532974f 100644 --- a/src/FSharp.AWS.DynamoDB/Expression/ExpressionContainers.fs +++ b/src/FSharp.AWS.DynamoDB/Expression/ExpressionContainers.fs @@ -8,6 +8,7 @@ open Microsoft.FSharp.Quotations open FSharp.AWS.DynamoDB.ExprCommon open FSharp.AWS.DynamoDB.ConditionalExpr open FSharp.AWS.DynamoDB.UpdateExpr +open FSharp.AWS.DynamoDB.ProjectionExpr // // Public converted condition expression wrapper implementations @@ -114,4 +115,24 @@ and UpdateExpression = let msg = sprintf "found conflicting paths '%s' and '%s' being accessed in update expression." p1 p2 invalidArg "expr" msg - new UpdateExpression<'TRecord>({ UpdateOps = uops ; NParams = 0 }) \ No newline at end of file + new UpdateExpression<'TRecord>({ UpdateOps = uops ; NParams = 0 }) + +/// Represents a projection expression for a given record type +[] +type ProjectionExpression<'TRecord, 'TProjection> internal (expr : ProjectionExpr) = + let data = lazy(expr.GetDebugData()) + /// Internal projection expression object + member internal __.ProjectionExpr = expr + /// DynamoDB projection expression string + member __.Expression = let expr,_ = data.Value in expr + /// DynamoDB attribute names + member __.Names = let _,names = data.Value in names + + member internal __.UnPickle(ro : RestObject) = expr.Ctor ro :?> 'TProjection + + override __.Equals(other : obj) = + match other with + | :? ProjectionExpression<'TRecord, 'TProjection> as other -> expr.Attributes = other.ProjectionExpr.Attributes + | _ -> false + + override __.GetHashCode() = hash expr.Attributes \ No newline at end of file diff --git a/src/FSharp.AWS.DynamoDB/Expression/ProjectionExpr.fs b/src/FSharp.AWS.DynamoDB/Expression/ProjectionExpr.fs new file mode 100644 index 0000000..8004206 --- /dev/null +++ b/src/FSharp.AWS.DynamoDB/Expression/ProjectionExpr.fs @@ -0,0 +1,103 @@ +module internal FSharp.AWS.DynamoDB.ProjectionExpr + +open System +open System.Collections.Generic +open System.Reflection + +open Microsoft.FSharp.Reflection +open Microsoft.FSharp.Quotations +open Microsoft.FSharp.Quotations.Patterns +open Microsoft.FSharp.Quotations.DerivedPatterns +open Microsoft.FSharp.Quotations.ExprShape + +open Amazon.DynamoDBv2 +open Amazon.DynamoDBv2.Model + +open FSharp.AWS.DynamoDB.ExprCommon + +/////////////////////////////// +// +// Extracts projection expressions from an F# quotation of the form +// <@ fun record -> record.A, record.B, record.B.[0].C @> +// +// c.f. http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.AccessingItemAttributes.html +// +/////////////////////////////// + +type ProjectionExpr = + { + Attributes : AttributeId [] + Ctor : Dictionary -> obj + } + +with + + static member Extract (recordInfo : RecordInfo) (expr : Expr<'TRecord -> 'Tuple>) = + let invalidExpr () = invalidArg "expr" "supplied expression is not a valid projection." + match expr with + | Lambda(r, body) when r.Type = recordInfo.Type -> + let (|AttributeGet|_|) expr = QuotedAttribute.TryExtract (fun _ -> None) r recordInfo expr + + match body with + | AttributeGet qa -> + let pickler = qa.Pickler + let attrId = qa.RootProperty.Name + let attr = qa.Id + let ctor (ro : RestObject) = + let ok, av = ro.TryGetValue attrId + if ok then pickler.UnPickleUntyped av + else pickler.DefaultValueUntyped + + { Attributes = [|attr|] ; Ctor = ctor } + + | NewTuple values -> + let qas = + values + |> Seq.map (function AttributeGet qa -> qa | _ -> invalidExpr ()) + |> Seq.toArray + + // check for conflicting projection attributes + qas + |> Seq.groupBy (fun qa -> qa.RootProperty.AttrId) + |> Seq.filter (fun (_,rps) -> Seq.length rps > 1) + |> Seq.iter (fun (attr,_) -> + sprintf "Projection expression accessing conflicting property '%s'." attr + |> invalidArg "expr") + + let attrs = qas |> Array.map (fun qa -> qa.Id) + let picklers = qas |> Array.map (fun attr -> attr.Pickler) + let attrIds = qas |> Array.map (fun attr -> attr.RootProperty.Name) + let tupleCtor = FSharpValue.PreComputeTupleConstructor typeof<'Tuple> + + let ctor (ro : RestObject) = + let values = Array.zeroCreate picklers.Length + for i = 0 to picklers.Length - 1 do + let id = attrIds.[i] + let ok, av = ro.TryGetValue (attrIds.[i]) + if ok then values.[i] <- picklers.[i].UnPickleUntyped av + else values.[i] <- picklers.[i].DefaultValueUntyped + + tupleCtor values + + { Attributes = attrs ; Ctor = ctor } + + | _ -> invalidArg "expr" "projection type must either be a single property, or tuple of properties." + | _ -> invalidExpr () + + member __.Write (writer : AttributeWriter) = + let sb = new System.Text.StringBuilder() + let inline (!) (x:string) = sb.Append x |> ignore + let mutable isFirst = true + for attr in __.Attributes do + if isFirst then isFirst <- false + else ! ", " + + ! (writer.WriteAttibute attr) + + sb.ToString() + + member __.GetDebugData() = + let aw = new AttributeWriter() + let expr = __.Write (aw) + let names = aw.Names |> Seq.map (fun kv -> kv.Key, kv.Value) |> Seq.toList + expr, names \ No newline at end of file diff --git a/src/FSharp.AWS.DynamoDB/Extensions.fs b/src/FSharp.AWS.DynamoDB/Extensions.fs new file mode 100644 index 0000000..4bf457b --- /dev/null +++ b/src/FSharp.AWS.DynamoDB/Extensions.fs @@ -0,0 +1,31 @@ +namespace FSharp.AWS.DynamoDB + +open Microsoft.FSharp.Quotations + +/// Collection of extensions for the public API +[] +module Extensions = + + /// Precomputes a template expression + let inline template<'TRecord> = RecordTemplate.Define<'TRecord>() + + /// A conditional which verifies that given item exists + let inline itemExists<'TRecord> = template<'TRecord>.ItemExists + /// A conditional which verifies that given item does not exist + let inline itemDoesNotExist<'TRecord> = template<'TRecord>.ItemDoesNotExist + + /// Precomputes a conditional expression + let inline cond (expr : Expr<'TRecord -> bool>) : ConditionExpression<'TRecord> = + template<'TRecord>.PrecomputeConditionalExpr expr + + /// Precomputes an update expression + let inline update (expr : Expr<'TRecord -> 'TRecord>) : UpdateExpression<'TRecord> = + template<'TRecord>.PrecomputeUpdateExpr expr + + /// Precomputes an update operation expression + let inline updateOp (expr : Expr<'TRecord -> UpdateOp>) : UpdateExpression<'TRecord> = + template<'TRecord>.PrecomputeUpdateExpr expr + + /// Precomputes a projection expression + let inline proj (expr : Expr<'TRecord -> 'TProjection>) : ProjectionExpression<'TRecord, 'TProjection> = + template<'TRecord>.PrecomputeProjectionExpr<'TProjection> expr \ No newline at end of file diff --git a/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj b/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj index e5ca239..867747a 100644 --- a/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj +++ b/src/FSharp.AWS.DynamoDB/FSharp.AWS.DynamoDB.fsproj @@ -59,10 +59,12 @@ + + @@ -751,4 +753,4 @@ - + \ No newline at end of file diff --git a/src/FSharp.AWS.DynamoDB/Picklers/RecordPickler.fs b/src/FSharp.AWS.DynamoDB/Picklers/RecordPickler.fs index 522010e..bc8769e 100644 --- a/src/FSharp.AWS.DynamoDB/Picklers/RecordPickler.fs +++ b/src/FSharp.AWS.DynamoDB/Picklers/RecordPickler.fs @@ -27,7 +27,7 @@ type AttributeType = type RecordInfo = { Type : Type - ConstructorInfo : ConstructorInfo + Constructor : obj[] -> obj Properties : RecordPropertyInfo [] } with @@ -96,10 +96,10 @@ with and IRecordPickler = abstract RecordInfo : RecordInfo -type RecordPickler<'T>(ctorInfo : ConstructorInfo, properties : RecordPropertyInfo []) = +type RecordPickler<'T>(ctor : obj[] -> obj, properties : RecordPropertyInfo []) = inherit Pickler<'T> () - let recordInfo = { Type = typeof<'T> ; Properties = properties ; ConstructorInfo = ctorInfo } + let recordInfo = { Type = typeof<'T> ; Properties = properties ; Constructor = ctor } member __.RecordInfo = recordInfo member __.OfRecord (value : 'T) : RestObject = @@ -122,7 +122,7 @@ type RecordPickler<'T>(ctorInfo : ConstructorInfo, properties : RecordPropertyIn elif prop.NoDefaultValue then notFound() else values.[i] <- prop.Pickler.DefaultValueUntyped - ctorInfo.Invoke values :?> 'T + ctor values :?> 'T interface IRecordPickler with member __.RecordInfo = recordInfo @@ -143,12 +143,11 @@ type RecordPickler<'T>(ctorInfo : ConstructorInfo, properties : RecordPropertyIn let mkTuplePickler<'T> (resolver : IPicklerResolver) = - let ctor, rest = FSharpValue.PreComputeTupleConstructorInfo(typeof<'T>) - if Option.isSome rest then invalidArg (string typeof<'T>) "Tuples of arity > 7 not supported" + let ctor = FSharpValue.PreComputeTupleConstructor typeof<'T> let properties = typeof<'T>.GetProperties() |> Array.mapi (RecordPropertyInfo.FromPropertyInfo resolver) new RecordPickler<'T>(ctor, properties) let mkFSharpRecordPickler<'T> (resolver : IPicklerResolver) = - let ctor = FSharpValue.PreComputeRecordConstructorInfo(typeof<'T>, true) + let ctor = FSharpValue.PreComputeRecordConstructor(typeof<'T>, true) let properties = FSharpType.GetRecordFields(typeof<'T>, true) |> Array.mapi (RecordPropertyInfo.FromPropertyInfo resolver) new RecordPickler<'T>(ctor, properties) \ No newline at end of file diff --git a/src/FSharp.AWS.DynamoDB/RecordTemplate.fs b/src/FSharp.AWS.DynamoDB/RecordTemplate.fs index 3d6ab90..69e75ec 100644 --- a/src/FSharp.AWS.DynamoDB/RecordTemplate.fs +++ b/src/FSharp.AWS.DynamoDB/RecordTemplate.fs @@ -12,6 +12,7 @@ open Amazon.DynamoDBv2.Model open FSharp.AWS.DynamoDB.KeySchema open FSharp.AWS.DynamoDB.ConditionalExpr open FSharp.AWS.DynamoDB.UpdateExpr +open FSharp.AWS.DynamoDB.ProjectionExpr /// DynamoDB table template defined by provided F# record type [] @@ -222,6 +223,15 @@ type RecordTemplate<'TRecord> internal () = let uops = UpdateOperations.ExtractOpExpr pickler.RecordInfo expr fun i1 i2 i3 i4 i5 -> new UpdateExpression<'TRecord>(uops.Apply(i1, i2, i3, i4, i5)) + /// + /// Precomputes a DynamoDB projection expression using + /// supplied quoted projection expression. + /// + /// Quoted record projection expression. + member __.PrecomputeProjectionExpr(expr : Expr<'TRecord -> 'TProjection>) : ProjectionExpression<'TRecord, 'TProjection> = + let pexpr = ProjectionExpr.Extract pickler.RecordInfo expr + new ProjectionExpression<'TRecord, 'TProjection>(pexpr) + /// Convert table key to attribute values member internal __.ToAttributeValues(key : TableKey) = KeyStructure.ExtractKey(keyStructure, key) diff --git a/src/FSharp.AWS.DynamoDB/TableContext.fs b/src/FSharp.AWS.DynamoDB/TableContext.fs index 16774ae..e1d715d 100644 --- a/src/FSharp.AWS.DynamoDB/TableContext.fs +++ b/src/FSharp.AWS.DynamoDB/TableContext.fs @@ -1,5 +1,6 @@ namespace FSharp.AWS.DynamoDB +open System open System.Collections.Generic open System.Net @@ -25,6 +26,141 @@ type ProvisionedThroughput = Amazon.DynamoDBv2.Model.ProvisionedThroughput [] type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : string, template : RecordTemplate<'TRecord>) = + let getItemAsync (key : TableKey) (proj : ProjectionExpr.ProjectionExpr option) = async { + let kav = template.ToAttributeValues(key) + let request = new GetItemRequest(tableName, kav) + match proj with + | None -> () + | Some proj -> + let aw = new AttributeWriter(request.ExpressionAttributeNames, null) + request.ProjectionExpression <- proj.Write aw + + let! ct = Async.CancellationToken + let! response = client.GetItemAsync(request, ct) |> Async.AwaitTaskCorrect + if response.HttpStatusCode <> HttpStatusCode.OK then + failwithf "GetItem request returned error %O" response.HttpStatusCode + + if not response.IsItemSet then + let msg = sprintf "could not find item %O" key + raise <| new ResourceNotFoundException(msg) + + return response + } + + let batchGetItemsAsync (keys : seq) (consistentRead : bool option) + (projExpr : ProjectionExpr.ProjectionExpr option) = async { + + let consistentRead = defaultArg consistentRead false + let kna = new KeysAndAttributes() + kna.AttributesToGet.AddRange(template.Info.Properties |> Seq.map (fun p -> p.Name)) + kna.Keys.AddRange(keys |> Seq.map template.ToAttributeValues) + kna.ConsistentRead <- consistentRead + match projExpr with + | None -> () + | Some projExpr -> + let aw = new AttributeWriter(kna.ExpressionAttributeNames, null) + kna.ProjectionExpression <- projExpr.Write aw + + let request = new BatchGetItemRequest() + request.RequestItems.[tableName] <- kna + + let! ct = Async.CancellationToken + let! response = client.BatchGetItemAsync(request, ct) |> Async.AwaitTaskCorrect + if response.HttpStatusCode <> HttpStatusCode.OK then + failwithf "GetItem request returned error %O" response.HttpStatusCode + + return response.Responses.[tableName] + } + + let queryAsync (keyCondition : ConditionalExpr.ConditionalExpression) + (filterCondition : ConditionalExpr.ConditionalExpression option) + (projectionExpr : ProjectionExpr.ProjectionExpr option) + (limit: int option) (consistentRead : bool option) (scanIndexForward : bool option) = async { + + if not keyCondition.IsKeyConditionCompatible then + invalidArg "keyCondition" + """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. +""" + + let downloaded = new ResizeArray<_>() + let rec aux last = async { + let request = new QueryRequest(tableName) + let writer = new AttributeWriter(request.ExpressionAttributeNames, request.ExpressionAttributeValues) + request.KeyConditionExpression <- keyCondition.Write writer + + match filterCondition with + | None -> () + | Some fc -> request.FilterExpression <- fc.Write writer + + match projectionExpr with + | None -> () + | Some pe -> request.ProjectionExpression <- pe.Write writer + + limit |> Option.iter (fun l -> request.Limit <- l - downloaded.Count) + consistentRead |> Option.iter (fun cr -> request.ConsistentRead <- cr) + scanIndexForward |> Option.iter (fun sif -> request.ScanIndexForward <- sif) + last |> Option.iter (fun l -> request.ExclusiveStartKey <- l) + + let! ct = Async.CancellationToken + let! response = client.QueryAsync(request, ct) |> Async.AwaitTaskCorrect + if response.HttpStatusCode <> HttpStatusCode.OK then + failwithf "Query request returned error %O" response.HttpStatusCode + + downloaded.AddRange response.Items + if response.LastEvaluatedKey.Count > 0 && + limit |> Option.forall (fun l -> downloaded.Count < l) + then + do! aux (Some response.LastEvaluatedKey) + } + + do! aux None + + return downloaded + } + + let scanAsync (filterCondition : ConditionalExpr.ConditionalExpression option) + (projectionExpr : ProjectionExpr.ProjectionExpr option) + (limit : int option) (consistentRead : bool option) = async { + + let downloaded = new ResizeArray<_>() + let rec aux last = async { + let request = new ScanRequest(tableName) + let writer = new AttributeWriter(request.ExpressionAttributeNames, request.ExpressionAttributeValues) + match filterCondition with + | None -> () + | Some fc -> request.FilterExpression <- fc.Write writer + + match projectionExpr with + | None -> () + | Some pe -> request.ProjectionExpression <- pe.Write writer + + limit |> Option.iter (fun l -> request.Limit <- l - downloaded.Count) + consistentRead |> Option.iter (fun cr -> request.ConsistentRead <- cr) + last |> Option.iter (fun l -> request.ExclusiveStartKey <- l) + + let! ct = Async.CancellationToken + let! response = client.ScanAsync(request, ct) |> Async.AwaitTaskCorrect + if response.HttpStatusCode <> HttpStatusCode.OK then + failwithf "Query request returned error %O" response.HttpStatusCode + + downloaded.AddRange response.Items + if response.LastEvaluatedKey.Count > 0 && + limit |> Option.forall (fun l -> downloaded.Count < l) + then + do! aux (Some response.LastEvaluatedKey) + } + + do! aux None + + return downloaded + } + /// DynamoDB client instance used for the table operations member __.Client = client /// DynamoDB table name targeted by the context @@ -227,6 +363,8 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri member __.ContainsKeyAsync(key : TableKey) : Async = async { let kav = template.ToAttributeValues(key) let request = new GetItemRequest(tableName, kav) + request.ExpressionAttributeNames.Add("#HKEY", template.KeySchema.HashKey.AttributeName) + request.ProjectionExpression <- "#HKEY" let! ct = Async.CancellationToken let! response = client.GetItemAsync(request, ct) |> Async.AwaitTaskCorrect return response.IsItemSet @@ -244,17 +382,7 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri /// /// Key of item to be fetched. member __.GetItemAsync(key : TableKey) : Async<'TRecord> = async { - let kav = template.ToAttributeValues(key) - let request = new GetItemRequest(tableName, kav) - let! ct = Async.CancellationToken - let! response = client.GetItemAsync(request, ct) |> Async.AwaitTaskCorrect - if response.HttpStatusCode <> HttpStatusCode.OK then - failwithf "GetItem request returned error %O" response.HttpStatusCode - - if not response.IsItemSet then - let msg = sprintf "could not find item %O" key - raise <| new ResourceNotFoundException(msg) - + let! response = getItemAsync key None return template.OfAttributeValues response.Item } @@ -264,26 +392,58 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri /// Key of item to be fetched. member __.GetItem(key : TableKey) = __.GetItemAsync(key) |> Async.RunSynchronously + /// + /// Asynchronously fetches item of given key from table. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key of item to be fetched. + /// Projection expression to be applied to item. + member __.GetItemProjectedAsync(key : TableKey, projection : ProjectionExpression<'TRecord, 'TProjection>) : Async<'TProjection> = async { + let! response = getItemAsync key (Some projection.ProjectionExpr) + return projection.UnPickle response.Item + } + + /// + /// Asynchronously fetches item of given key from table. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key of item to be fetched. + /// Projection expression to be applied to item. + member __.GetItemProjectedAsync(key : TableKey, projection : Expr<'TRecord -> 'TProjection>) : Async<'TProjection> = async { + return! __.GetItemProjectedAsync(key, template.PrecomputeProjectionExpr projection) + } + + /// + /// Fetches item of given key from table. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key of item to be fetched. + /// Projection expression to be applied to item. + member __.GetItemProjected(key : TableKey, projection : ProjectionExpression<'TRecord, 'TProjection>) : 'TProjection = + __.GetItemProjectedAsync(key, projection) |> Async.RunSynchronously + + + /// + /// Fetches item of given key from table. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key of item to be fetched. + /// Projection expression to be applied to item. + member __.GetItemProjected(key : TableKey, projection : Expr<'TRecord -> 'TProjection>) : 'TProjection = + __.GetItemProjected(key, template.PrecomputeProjectionExpr projection) + /// /// Asynchronously performs a batch fetch of items with supplied keys. /// /// Keys of items to be fetched. /// Perform consistent read. Defaults to false. member __.BatchGetItemsAsync(keys : seq, ?consistentRead : bool) : Async<'TRecord[]> = async { - let consistentRead = defaultArg consistentRead false - let kna = new KeysAndAttributes() - kna.AttributesToGet.AddRange(template.Info.Properties |> Seq.map (fun p -> p.Name)) - kna.Keys.AddRange(keys |> Seq.map template.ToAttributeValues) - kna.ConsistentRead <- consistentRead - let request = new BatchGetItemRequest() - request.RequestItems.[tableName] <- kna - - let! ct = Async.CancellationToken - let! response = client.BatchGetItemAsync(request, ct) |> Async.AwaitTaskCorrect - if response.HttpStatusCode <> HttpStatusCode.OK then - failwithf "GetItem request returned error %O" response.HttpStatusCode - - return response.Responses.[tableName] |> Seq.map template.OfAttributeValues |> Seq.toArray + let! response = batchGetItemsAsync keys consistentRead None + return response |> Seq.map template.OfAttributeValues |> Seq.toArray } /// @@ -295,6 +455,48 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri __.BatchGetItemsAsync(keys, ?consistentRead = consistentRead) |> Async.RunSynchronously + /// + /// Asynchronously performs a batch fetch of items with supplied keys. + /// + /// Keys of items to be fetched. + /// Perform consistent read. Defaults to false. + member __.BatchGetItemsProjectedAsync<'TProjection>(keys : seq, projection : ProjectionExpression<'TRecord, 'TProjection>, + ?consistentRead : bool) : Async<'TProjection[]> = async { + + let! response = batchGetItemsAsync keys consistentRead (Some projection.ProjectionExpr) + return response |> Seq.map projection.UnPickle |> Seq.toArray + } + + /// + /// Asynchronously performs a batch fetch of items with supplied keys. + /// + /// Keys of items to be fetched. + /// Perform consistent read. Defaults to false. + member __.BatchGetItemsProjectedAsync<'TProjection>(keys : seq, projection : Expr<'TRecord -> 'TProjection>, + ?consistentRead : bool) : Async<'TProjection[]> = async { + return! __.BatchGetItemsProjectedAsync(keys, template.PrecomputeProjectionExpr projection, ?consistentRead = consistentRead) + } + + /// + /// Asynchronously performs a batch fetch of items with supplied keys. + /// + /// Keys of items to be fetched. + /// Perform consistent read. Defaults to false. + member __.BatchGetItemsProjected<'TProjection>(keys : seq, projection : ProjectionExpression<'TRecord, 'TProjection>, + ?consistentRead : bool) : 'TProjection [] = + __.BatchGetItemsProjectedAsync(keys, projection, ?consistentRead = consistentRead) |> Async.RunSynchronously + + + /// + /// Asynchronously performs a batch fetch of items with supplied keys. + /// + /// Keys of items to be fetched. + /// Perform consistent read. Defaults to false. + member __.BatchGetItemsProjected<'TProjection>(keys : seq, projection : Expr<'TRecord -> 'TProjection>, + ?consistentRead : bool) : 'TProjection [] = + __.BatchGetItemsProjectedAsync(keys, projection, ?consistentRead = consistentRead) |> Async.RunSynchronously + + /// /// Asynchronously deletes item of given key from table. /// @@ -383,50 +585,8 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri member __.QueryAsync(keyCondition : ConditionExpression<'TRecord>, ?filterCondition : ConditionExpression<'TRecord>, ?limit: int, ?consistentRead : bool, ?scanIndexForward : bool) : Async<'TRecord []> = async { - if not keyCondition.IsKeyConditionCompatible then - invalidArg "keyCondition" - """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. -""" - - let downloaded = new ResizeArray<_>() - let rec aux last = async { - let request = new QueryRequest(tableName) - let cond = keyCondition.Conditional - let writer = new AttributeWriter(request.ExpressionAttributeNames, request.ExpressionAttributeValues) - request.KeyConditionExpression <- cond.Write writer - - match filterCondition with - | Some fc -> - let cond = fc.Conditional - request.FilterExpression <- cond.Write writer - - | None -> () - - limit |> Option.iter (fun l -> request.Limit <- l - downloaded.Count) - consistentRead |> Option.iter (fun cr -> request.ConsistentRead <- cr) - scanIndexForward |> Option.iter (fun sif -> request.ScanIndexForward <- sif) - last |> Option.iter (fun l -> request.ExclusiveStartKey <- l) - - let! ct = Async.CancellationToken - let! response = client.QueryAsync(request, ct) |> Async.AwaitTaskCorrect - if response.HttpStatusCode <> HttpStatusCode.OK then - failwithf "Query request returned error %O" response.HttpStatusCode - - downloaded.AddRange response.Items - if response.LastEvaluatedKey.Count > 0 && - limit |> Option.forall (fun l -> downloaded.Count < l) - then - do! aux (Some response.LastEvaluatedKey) - } - - do! aux None - + let filterCondition = filterCondition |> Option.map (fun fc -> fc.Conditional) + let! downloaded = queryAsync keyCondition.Conditional filterCondition None limit consistentRead scanIndexForward return downloaded |> Seq.map template.OfAttributeValues |> Seq.toArray } @@ -474,41 +634,95 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri ?consistentRead = consistentRead, ?scanIndexForward = scanIndexForward) |> Async.RunSynchronously + /// - /// Asynchronously scans table with given condition expressions. + /// Asynchronously queries table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. /// + /// Key condition expression. + /// Projection expression. /// Filter condition expression. /// Maximum number of items to evaluate. /// Specify whether to perform consistent read operation. - member __.ScanAsync(?filterCondition : ConditionExpression<'TRecord>, ?limit : int, ?consistentRead : bool) : Async<'TRecord []> = async { + /// Specifies the order in which to evaluate results. Either ascending (true) or descending (false). + member __.QueryProjectedAsync<'TProjection>(keyCondition : ConditionExpression<'TRecord>, projection : ProjectionExpression<'TRecord, 'TProjection>, + ?filterCondition : ConditionExpression<'TRecord>, + ?limit: int, ?consistentRead : bool, ?scanIndexForward : bool) : Async<'TProjection []> = async { - let downloaded = new ResizeArray<_>() - let rec aux last = async { - let request = new ScanRequest(tableName) - match filterCondition with - | None -> () - | Some fc -> - let writer = new AttributeWriter(request.ExpressionAttributeNames, request.ExpressionAttributeValues) - request.FilterExpression <- fc.Conditional.Write writer + let filterCondition = filterCondition |> Option.map (fun fc -> fc.Conditional) + let! downloaded = queryAsync keyCondition.Conditional filterCondition None limit consistentRead scanIndexForward + return downloaded |> Seq.map projection.UnPickle |> Seq.toArray + } - limit |> Option.iter (fun l -> request.Limit <- l - downloaded.Count) - consistentRead |> Option.iter (fun cr -> request.ConsistentRead <- cr) - last |> Option.iter (fun l -> request.ExclusiveStartKey <- l) + /// + /// Asynchronously queries table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key condition expression. + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + /// Specifies the order in which to evaluate results. Either ascending (true) or descending (false). + member __.QueryProjectedAsync<'TProjection>(keyCondition : Expr<'TRecord -> bool>, projection : Expr<'TRecord -> 'TProjection>, + ?filterCondition : Expr<'TRecord -> bool>, + ?limit: int, ?consistentRead : bool, ?scanIndexForward : bool) : Async<'TProjection []> = async { + + let filterCondition = filterCondition |> Option.map (fun fc -> template.PrecomputeConditionalExpr fc) + return! __.QueryProjectedAsync(template.PrecomputeConditionalExpr keyCondition, template.PrecomputeProjectionExpr projection, + ?filterCondition = filterCondition, ?limit = limit, ?consistentRead = consistentRead, + ?scanIndexForward = scanIndexForward) + } - let! ct = Async.CancellationToken - let! response = client.ScanAsync(request, ct) |> Async.AwaitTaskCorrect - if response.HttpStatusCode <> HttpStatusCode.OK then - failwithf "Query request returned error %O" response.HttpStatusCode - - downloaded.AddRange response.Items - if response.LastEvaluatedKey.Count > 0 && - limit |> Option.forall (fun l -> downloaded.Count < l) - then - do! aux (Some response.LastEvaluatedKey) - } + /// + /// Queries table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key condition expression. + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + /// Specifies the order in which to evaluate results. Either ascending (true) or descending (false). + member __.QueryProjected<'TProjection>(keyCondition : ConditionExpression<'TRecord>, projection : ProjectionExpression<'TRecord, 'TProjection>, + ?filterCondition : ConditionExpression<'TRecord>, + ?limit: int, ?consistentRead : bool, ?scanIndexForward : bool) : 'TProjection [] = - do! aux None - + __.QueryProjectedAsync(keyCondition, projection, ?filterCondition = filterCondition, ?limit = limit, + ?consistentRead = consistentRead, ?scanIndexForward = scanIndexForward) + |> Async.RunSynchronously + + /// + /// Queries table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Key condition expression. + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + /// Specifies the order in which to evaluate results. Either ascending (true) or descending (false). + member __.QueryProjected<'TProjection>(keyCondition : Expr<'TRecord -> bool>, projection : Expr<'TRecord -> 'TProjection>, + ?filterCondition : Expr<'TRecord -> bool>, + ?limit: int, ?consistentRead : bool, ?scanIndexForward : bool) : 'TProjection [] = + + __.QueryProjectedAsync(keyCondition, projection, ?filterCondition = filterCondition, ?limit = limit, + ?consistentRead = consistentRead, ?scanIndexForward = scanIndexForward) + |> Async.RunSynchronously + + /// + /// Asynchronously scans table with given condition expressions. + /// + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + member __.ScanAsync(?filterCondition : ConditionExpression<'TRecord>, ?limit : int, ?consistentRead : bool) : Async<'TRecord []> = async { + let filterCondition = filterCondition |> Option.map (fun fc -> fc.Conditional) + let! downloaded = scanAsync filterCondition None limit consistentRead return downloaded |> Seq.map template.OfAttributeValues |> Seq.toArray } @@ -544,6 +758,72 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri |> Async.RunSynchronously + /// + /// Asynchronously scans table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + member __.ScanProjectedAsync<'TProjection>(projection : ProjectionExpression<'TRecord, 'TProjection>, + ?filterCondition : ConditionExpression<'TRecord>, + ?limit : int, ?consistentRead : bool) : Async<'TProjection []> = async { + let filterCondition = filterCondition |> Option.map (fun fc -> fc.Conditional) + let! downloaded = scanAsync filterCondition (Some projection.ProjectionExpr) limit consistentRead + return downloaded |> Seq.map projection.UnPickle |> Seq.toArray + } + + /// + /// Asynchronously scans table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + member __.ScanProjectedAsync<'TProjection>(projection : Expr<'TRecord -> 'TProjection>, + ?filterCondition : Expr<'TRecord -> bool>, + ?limit : int, ?consistentRead : bool) : Async<'TProjection []> = async { + let filterCondition = filterCondition |> Option.map (fun fc -> template.PrecomputeConditionalExpr fc) + return! __.ScanProjectedAsync(template.PrecomputeProjectionExpr projection, ?filterCondition = filterCondition, + ?limit = limit, ?consistentRead = consistentRead) + } + + /// + /// Scans table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + member __.ScanProjected<'TProjection>(projection : ProjectionExpression<'TRecord, 'TProjection>, + ?filterCondition : ConditionExpression<'TRecord>, + ?limit : int, ?consistentRead : bool) : 'TProjection [] = + __.ScanProjectedAsync(projection, ?filterCondition = filterCondition, + ?limit = limit, ?consistentRead = consistentRead) + |> Async.RunSynchronously + + /// + /// Scans table with given condition expressions. + /// Uses supplied projection expression to narrow downloaded attributes. + /// Projection type must either be a single property or a tuple of properties. + /// + /// Projection expression. + /// Filter condition expression. + /// Maximum number of items to evaluate. + /// Specify whether to perform consistent read operation. + member __.ScanProjected<'TProjection>(projection : Expr<'TRecord -> 'TProjection>, + ?filterCondition : Expr<'TRecord -> bool>, + ?limit : int, ?consistentRead : bool) : 'TProjection [] = + __.ScanProjectedAsync(projection, ?filterCondition = filterCondition, + ?limit = limit, ?consistentRead = consistentRead) + |> Async.RunSynchronously + /// /// Asynchronously updates the underlying table with supplied provisioned throughput. /// @@ -569,25 +849,29 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri /// Provisioned throughput for the table if newly created. Defaults to (10,10). member __.VerifyTableAsync(?createIfNotExists : bool, ?provisionedThroughput : ProvisionedThroughput) : Async = async { let createIfNotExists = defaultArg createIfNotExists false - let rec verify retries = async { - if retries = 0 then failwithf "failed to create table '%s'" tableName + let (|Conflict|_|) (e : exn) = + match e with + | :? AmazonDynamoDBException as e when e.StatusCode = HttpStatusCode.Conflict -> Some() + | :? ResourceInUseException -> Some () + | _ -> None + + let rec verify lastExn retries = async { + match lastExn with + | Some e when retries = 0 -> do! Async.Raise e + | _ -> () + let! ct = Async.CancellationToken let! response = client.DescribeTableAsync(tableName, ct) |> Async.AwaitTaskCorrect |> Async.Catch - let (|Conflict|_|) (e : exn) = - match e with - | :? AmazonDynamoDBException as e when e.StatusCode = HttpStatusCode.Conflict -> Some() - | :? ResourceInUseException -> Some () - | _ -> None - match response with | Choice1Of2 td -> if td.Table.TableStatus <> TableStatus.ACTIVE then - do! Async.Sleep 1000 - return! verify (retries - 1) + do! Async.Sleep 2000 + // wait indefinitely if table is in transition state + return! verify None retries else let existingSchema = TableKeySchema.OfTableDescription td.Table @@ -610,21 +894,21 @@ type TableContext<'TRecord> internal (client : IAmazonDynamoDB, tableName : stri |> Async.Catch match response with - | Choice1Of2 _ -> return! verify (retries - 1) - | Choice2Of2 Conflict -> - do! Async.Sleep 1000 - return! verify (retries - 1) + | Choice1Of2 _ -> return! verify None retries + | Choice2Of2 (Conflict as e) -> + do! Async.Sleep 2000 + return! verify (Some e) (retries - 1) | Choice2Of2 e -> do! Async.Raise e - | Choice2Of2 Conflict -> - do! Async.Sleep 1000 - return! verify (retries - 1) + | Choice2Of2 (Conflict as e) -> + do! Async.Sleep 2000 + return! verify (Some e) (retries - 1) | Choice2Of2 e -> do! Async.Raise e } - do! verify 30 + do! verify None 10 } /// @@ -672,22 +956,4 @@ type TableContext = ?provisionedThroughput : ProvisionedThroughput) = TableContext.CreateAsync<'TRecord>(client, tableName, ?verifyTable = verifyTable, ?createIfNotExists = createIfNotExists, ?provisionedThroughput = provisionedThroughput) - |> Async.RunSynchronously - - -[] -module TableContextUtils = - - let template<'TRecord> = RecordTemplate.Define<'TRecord>() - - /// A conditional which verifies that given item exists - let itemExists<'TRecord> = template<'TRecord>.ItemExists - /// A conditional which verifies that given item does not exist - let itemDoesNotExist<'TRecord> = template<'TRecord>.ItemDoesNotExist - - /// Precomputes a conditional expression - let inline cond (tmp : RecordTemplate<'TRecord>) expr = tmp.PrecomputeConditionalExpr expr - /// Precomputes an update expression - let inline updateOp (tmp : RecordTemplate<'TRecord>) (expr : Expr<'TRecord -> UpdateOp>) = tmp.PrecomputeUpdateExpr expr - /// Precomputes an update expression - let inline updateRec (tmp : RecordTemplate<'TRecord>) (expr : Expr<'TRecord -> 'TRecord>) = tmp.PrecomputeUpdateExpr expr \ No newline at end of file + |> Async.RunSynchronously \ No newline at end of file diff --git a/tests/FSharp.AWS.DynamoDB.Tests/FSharp.AWS.DynamoDB.Tests.fsproj b/tests/FSharp.AWS.DynamoDB.Tests/FSharp.AWS.DynamoDB.Tests.fsproj index c6144e2..4b912df 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/FSharp.AWS.DynamoDB.Tests.fsproj +++ b/tests/FSharp.AWS.DynamoDB.Tests/FSharp.AWS.DynamoDB.Tests.fsproj @@ -63,6 +63,7 @@ + @@ -933,4 +934,4 @@ - + \ No newline at end of file diff --git a/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs new file mode 100644 index 0000000..6a1173f --- /dev/null +++ b/tests/FSharp.AWS.DynamoDB.Tests/ProjectionExpressionTests.fs @@ -0,0 +1,190 @@ +namespace FSharp.AWS.DynamoDB.Tests + +open System +open System.Threading + +open Microsoft.FSharp.Quotations + +open Xunit +open FsUnit.Xunit + +open FSharp.AWS.DynamoDB + +[] +module ProjectionExprTypes = + + [] + type Enum = A = 1 | B = 2 | C = 4 + + type Nested = { NV : string ; NE : Enum } + + type Union = UA of int64 | UB of string + + type ProjectionExprRecord = + { + [] + HashKey : string + [] + RangeKey : string + + Value : int64 + + String : string + + Tuple : int64 * int64 + + Nested : Nested + + NestedList : Nested list + + TimeSpan : TimeSpan + + DateTimeOffset : DateTimeOffset + + Guid : Guid + + Bool : bool + + Bytes : byte[] + + Ref : string ref + + Union : Union + + Unions : Union list + + Optional : string option + + List : int64 list + + Map : Map + + IntSet : Set + + StringSet : Set + + ByteSet : Set + + [] + Serialized : int64 * string + + [] + Serialized2 : Nested + } + + type R = ProjectionExprRecord + +type ``Projection Expression Tests`` () = + + let client = getDynamoDBAccount() + let tableName = getRandomTableName() + + let rand = let r = Random() in fun () -> int64 <| r.Next() + let bytes() = Guid.NewGuid().ToByteArray() + let mkItem() = + { + HashKey = guid() ; RangeKey = guid() ; String = guid() + Value = rand() ; Tuple = rand(), rand() ; + TimeSpan = TimeSpan.FromTicks(rand()) ; DateTimeOffset = DateTimeOffset.Now ; Guid = Guid.NewGuid() + Bool = false ; Optional = Some (guid()) ; Ref = ref (guid()) ; Bytes = Guid.NewGuid().ToByteArray() + Nested = { NV = guid() ; NE = enum (int (rand()) % 3) } ; + NestedList = [{ NV = guid() ; NE = enum (int (rand()) % 3) } ] + Map = seq { for i in 0L .. rand() % 5L -> "K" + guid(), rand() } |> Map.ofSeq + IntSet = seq { for i in 0L .. rand() % 5L -> rand() } |> Set.ofSeq + StringSet = seq { for i in 0L .. rand() % 5L -> guid() } |> Set.ofSeq + ByteSet = seq { for i in 0L .. rand() % 5L -> bytes() } |> Set.ofSeq + List = [for i in 0L .. rand() % 5L -> rand() ] + Union = if rand() % 2L = 0L then UA (rand()) else UB(guid()) + Unions = [for i in 0L .. rand() % 5L -> if rand() % 2L = 0L then UA (rand()) else UB(guid()) ] + Serialized = rand(), guid() ; Serialized2 = { NV = guid() ; NE = enum (int (rand()) % 3) } ; + } + + let table = TableContext.Create(client, tableName, createIfNotExists = true) + + [] + let ``Should fail on invalid projections`` () = + let testProj (p : Expr 'T>) = + fun () -> proj p + |> shouldFailwith<_, ArgumentException> + + testProj <@ fun r -> 1 @> + testProj <@ fun r -> Guid.Empty @> + testProj <@ fun r -> not r.Bool @> + testProj <@ fun r -> r.List.[0] + 1L @> + + [] + let ``Should fail on conflicting projections`` () = + let testProj (p : Expr 'T>) = + fun () -> proj p + |> shouldFailwith<_, ArgumentException> + + testProj <@ fun r -> r.Bool, r.Bool @> + testProj <@ fun r -> r.NestedList.[0].NE, r.NestedList.[1].NV @> + + [] + let ``Single value projection`` () = + let item = mkItem() + let key = table.PutItem(item) + let guid = table.GetItemProjected(key, <@ fun r -> r.Guid @>) + guid |> should equal item.Guid + + [] + let ``Map projection`` () = + let item = mkItem() + let key = table.PutItem(item) + let map = table.GetItemProjected(key, <@ fun r -> r.Map @>) + map |> should equal item.Map + + [] + let ``Option-None projection`` () = + let item = { mkItem() with Optional = None } + let key = table.PutItem(item) + let opt = table.GetItemProjected(key, <@ fun r -> r.Optional @>) + opt |> should equal None + + [] + let ``Option-Some projection`` () = + let item = { mkItem() with Optional = Some "test" } + let key = table.PutItem(item) + let opt = table.GetItemProjected(key, <@ fun r -> r.Optional @>) + opt |> should equal item.Optional + + [] + let ``Multi-value projection`` () = + let item = mkItem() + let key = table.PutItem(item) + let result = table.GetItemProjected(key, <@ fun r -> r.Bool, r.ByteSet, r.Bytes, r.Serialized2 @>) + result |> should equal (item.Bool, item.ByteSet, item.Bytes, item.Serialized2) + + [] + let ``Projected query`` () = + let hKey = guid() + + seq { for i in 1 .. 200 -> { mkItem() with HashKey = hKey ; RangeKey = string i }} + |> Seq.splitInto 25 + |> Seq.map table.BatchPutItemsAsync + |> Async.Parallel + |> Async.Ignore + |> Async.RunSynchronously + + let results = table.QueryProjected(<@ fun r -> r.HashKey = hKey @>, <@ fun r -> r.RangeKey @>) + results |> Seq.map int |> set |> should equal (set [1 .. 200]) + + [] + let ``Projected scan`` () = + let hKey = guid() + + seq { for i in 1 .. 200 -> { mkItem() with HashKey = hKey ; RangeKey = string i }} + |> Seq.splitInto 25 + |> Seq.map table.BatchPutItemsAsync + |> Async.Parallel + |> Async.Ignore + |> Async.RunSynchronously + + let results = table.ScanProjected(<@ fun r -> r.RangeKey @>, filterCondition = <@ fun r -> r.HashKey = hKey @>) + results |> Seq.map int |> set |> should equal (set [1 .. 200]) + + + interface IDisposable with + member __.Dispose() = + ignore <| client.DeleteTable(tableName) \ No newline at end of file diff --git a/tests/FSharp.AWS.DynamoDB.Tests/RecordGenerationTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/RecordGenerationTests.fs index 1d69673..21c895c 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/RecordGenerationTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/RecordGenerationTests.fs @@ -297,4 +297,14 @@ module ``Record Generation Tests`` = [] let ``Record containing costant HashKey attribute with HashKey attribute should fail`` () = fun () -> RecordTemplate.Define<``Record containing costant HashKey attribute with HashKey attribute``>() - |> shouldFailwith<_, ArgumentException> \ No newline at end of file + |> shouldFailwith<_, ArgumentException> + + + type FooRecord = { A : int ; B : string ; C : DateTimeOffset * string } + + [] + let ``Generated picklers should be singletons`` () = + Array.Parallel.init 100 (fun _ -> Pickler.resolve()) + |> Seq.distinct + |> Seq.length + |> should equal 1 \ No newline at end of file diff --git a/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs b/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs index 8644a9a..783f84e 100644 --- a/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs +++ b/tests/FSharp.AWS.DynamoDB.Tests/SimpleTableOperationTests.fs @@ -45,8 +45,6 @@ module SimpleTableTypes = Values : Set } - type FooRecord = { A : int ; B : string ; C : DateTimeOffset * string } - type ``Simple Table Operation Tests`` () = let client = getDynamoDBAccount() @@ -85,6 +83,8 @@ type ``Simple Table Operation Tests`` () = let value = mkItem() let key = table.PutItem value table.ContainsKey key |> should equal true + let _ = table.DeleteItem key + table.ContainsKey key |> should equal false [] let ``Batch Put Operation`` () = @@ -102,13 +102,6 @@ type ``Simple Table Operation Tests`` () = item' |> should equal item table.ContainsKey key |> should equal false - [] - let ``Generated picklers should be singletons`` () = - Array.Parallel.init 100 (fun _ -> Pickler.resolve()) - |> Seq.distinct - |> Seq.length - |> should equal 1 - interface IDisposable with member __.Dispose() =