From d4e45a63c9d269ddff72970df7c80d214aeddcf3 Mon Sep 17 00:00:00 2001 From: Francesco Cogno Date: Tue, 3 May 2016 18:47:15 +0200 Subject: [PATCH] storage: Azure Table implementation (#217) Initial implementation for Azure Table Storage --- storage/client.go | 108 ++++++++++++ storage/table.go | 129 ++++++++++++++ storage/table_entities.go | 351 ++++++++++++++++++++++++++++++++++++++ storage/table_test.go | 287 +++++++++++++++++++++++++++++++ 4 files changed, 875 insertions(+) create mode 100644 storage/table.go create mode 100644 storage/table_entities.go create mode 100644 storage/table_test.go diff --git a/storage/client.go b/storage/client.go index 4a0dc5b343d5..41cdb25fe772 100644 --- a/storage/client.go +++ b/storage/client.go @@ -4,6 +4,7 @@ package storage import ( "bytes" "encoding/base64" + "encoding/json" "encoding/xml" "errors" "fmt" @@ -54,6 +55,11 @@ type storageResponse struct { body io.ReadCloser } +type odataResponse struct { + storageResponse + odata odataErrorMessage +} + // AzureStorageServiceError contains fields of the error response from // Azure Storage Service REST API. See https://msdn.microsoft.com/en-us/library/azure/dd179382.aspx // Some fields might be specific to certain calls. @@ -68,6 +74,20 @@ type AzureStorageServiceError struct { RequestID string } +type odataErrorMessageMessage struct { + Lang string `json:"lang"` + Value string `json:"value"` +} + +type odataErrorMessageInternal struct { + Code string `json:"code"` + Message odataErrorMessageMessage `json:"message"` +} + +type odataErrorMessage struct { + Err odataErrorMessageInternal `json:"odata.error"` +} + // UnexpectedStatusCodeError is returned when a storage service responds with neither an error // nor with an HTTP status code indicating success. type UnexpectedStatusCodeError struct { @@ -166,6 +186,12 @@ func (c Client) GetQueueService() QueueServiceClient { return QueueServiceClient{c} } +// GetTableService returns a TableServiceClient which can operate on the table +// service of the storage account. +func (c Client) GetTableService() TableServiceClient { + return TableServiceClient{c} +} + // GetFileService returns a FileServiceClient which can operate on the file // service of the storage account. func (c Client) GetFileService() FileServiceClient { @@ -228,6 +254,22 @@ func (c Client) buildCanonicalizedHeader(headers map[string]string) string { return ch } +func (c Client) buildCanonicalizedResourceTable(uri string) (string, error) { + errMsg := "buildCanonicalizedResourceTable error: %s" + u, err := url.Parse(uri) + if err != nil { + return "", fmt.Errorf(errMsg, err.Error()) + } + + cr := "/" + c.accountName + + if len(u.Path) > 0 { + cr += u.Path + } + + return cr, nil +} + func (c Client) buildCanonicalizedResource(uri string) (string, error) { errMsg := "buildCanonicalizedResource error: %s" u, err := url.Parse(uri) @@ -236,6 +278,7 @@ func (c Client) buildCanonicalizedResource(uri string) (string, error) { } cr := "/" + c.accountName + if len(u.Path) > 0 { cr += u.Path } @@ -266,6 +309,7 @@ func (c Client) buildCanonicalizedResource(uri string) (string, error) { } } } + return cr, nil } @@ -364,6 +408,70 @@ func (c Client) exec(verb, url string, headers map[string]string, body io.Reader body: resp.Body}, nil } +func (c Client) execInternalJSON(verb, url string, headers map[string]string, body io.Reader) (*odataResponse, error) { + req, err := http.NewRequest(verb, url, body) + for k, v := range headers { + req.Header.Add(k, v) + } + + httpClient := c.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + resp, err := httpClient.Do(req) + if err != nil { + return nil, err + } + + respToRet := &odataResponse{} + respToRet.body = resp.Body + respToRet.statusCode = resp.StatusCode + respToRet.headers = resp.Header + + statusCode := resp.StatusCode + if statusCode >= 400 && statusCode <= 505 { + var respBody []byte + respBody, err = readResponseBody(resp) + if err != nil { + return nil, err + } + + if len(respBody) == 0 { + // no error in response body + err = fmt.Errorf("storage: service returned without a response body (%d)", resp.StatusCode) + return respToRet, err + } + // try unmarshal as odata.error json + err = json.Unmarshal(respBody, &respToRet.odata) + return respToRet, err + } + + return respToRet, nil +} + +func (c Client) createSharedKeyLite(url string, headers map[string]string) (string, error) { + can, err := c.buildCanonicalizedResourceTable(url) + + if err != nil { + return "", err + } + strToSign := headers["x-ms-date"] + "\n" + can + + hmac := c.computeHmac256(strToSign) + return fmt.Sprintf("SharedKeyLite %s:%s", c.accountName, hmac), nil +} + +func (c Client) execTable(verb, url string, headers map[string]string, body io.Reader) (*odataResponse, error) { + var err error + headers["Authorization"], err = c.createSharedKeyLite(url, headers) + if err != nil { + return nil, err + } + + return c.execInternalJSON(verb, url, headers, body) +} + func readResponseBody(resp *http.Response) ([]byte, error) { defer resp.Body.Close() out, err := ioutil.ReadAll(resp.Body) diff --git a/storage/table.go b/storage/table.go new file mode 100644 index 000000000000..39e997503552 --- /dev/null +++ b/storage/table.go @@ -0,0 +1,129 @@ +package storage + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "net/url" +) + +// TableServiceClient contains operations for Microsoft Azure Table Storage +// Service. +type TableServiceClient struct { + client Client +} + +// AzureTable is the typedef of the Azure Table name +type AzureTable string + +const ( + tablesURIPath = "/Tables" +) + +type createTableRequest struct { + TableName string `json:"TableName"` +} + +func pathForTable(table AzureTable) string { return fmt.Sprintf("%s", table) } + +func (c *TableServiceClient) getStandardHeaders() map[string]string { + return map[string]string{ + "x-ms-version": "2015-02-21", + "x-ms-date": currentTimeRfc1123Formatted(), + "Accept": "application/json;odata=nometadata", + "Accept-Charset": "UTF-8", + "Content-Type": "application/json", + } +} + +// QueryTables returns the tables created in the +// *TableServiceClient storage account. +func (c *TableServiceClient) QueryTables() ([]AzureTable, error) { + uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) + + headers := c.getStandardHeaders() + headers["Content-Length"] = "0" + + resp, err := c.client.execTable("GET", uri, headers, nil) + if err != nil { + return nil, err + } + defer resp.body.Close() + + if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + buf.ReadFrom(resp.body) + + var respArray queryTablesResponse + if err := json.Unmarshal(buf.Bytes(), &respArray); err != nil { + return nil, err + } + + s := make([]AzureTable, len(respArray.TableName)) + for i, elem := range respArray.TableName { + s[i] = AzureTable(elem.TableName) + } + + return s, nil +} + +// CreateTable creates the table given the specific +// name. This function fails if the name is not compliant +// with the specification or the tables already exists. +func (c *TableServiceClient) CreateTable(table AzureTable) error { + uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) + + headers := c.getStandardHeaders() + + req := createTableRequest{TableName: string(table)} + buf := new(bytes.Buffer) + + if err := json.NewEncoder(buf).Encode(req); err != nil { + return err + } + + headers["Content-Length"] = fmt.Sprintf("%d", buf.Len()) + + resp, err := c.client.execTable("POST", uri, headers, buf) + + if err != nil { + return err + } + defer resp.body.Close() + + if err := checkRespCode(resp.statusCode, []int{http.StatusCreated}); err != nil { + return err + } + + return nil +} + +// DeleteTable deletes the table given the specific +// name. This function fails if the table is not present. +// Be advised: DeleteTable deletes all the entries +// that may be present. +func (c *TableServiceClient) DeleteTable(table AzureTable) error { + uri := c.client.getEndpoint(tableServiceName, tablesURIPath, url.Values{}) + uri += fmt.Sprintf("('%s')", string(table)) + + headers := c.getStandardHeaders() + + headers["Content-Length"] = "0" + + resp, err := c.client.execTable("DELETE", uri, headers, nil) + + if err != nil { + return err + } + defer resp.body.Close() + + if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil { + return err + + } + return nil +} diff --git a/storage/table_entities.go b/storage/table_entities.go new file mode 100644 index 000000000000..c81393e5f506 --- /dev/null +++ b/storage/table_entities.go @@ -0,0 +1,351 @@ +package storage + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "reflect" +) + +const ( + partitionKeyNode = "PartitionKey" + rowKeyNode = "RowKey" + tag = "table" + tagIgnore = "-" + continuationTokenPartitionKeyHeader = "X-Ms-Continuation-Nextpartitionkey" + continuationTokenRowHeader = "X-Ms-Continuation-Nextrowkey" + maxTopParameter = 1000 +) + +type queryTablesResponse struct { + TableName []struct { + TableName string `json:"TableName"` + } `json:"value"` +} + +const ( + tableOperationTypeInsert = iota + tableOperationTypeUpdate = iota + tableOperationTypeMerge = iota + tableOperationTypeInsertOrReplace = iota + tableOperationTypeInsertOrMerge = iota +) + +type tableOperation int + +// TableEntity interface specifies +// the functions needed to support +// marshaling and unmarshaling into +// Azure Tables. The struct must only contain +// simple types because Azure Tables do not +// support hierarchy. +type TableEntity interface { + PartitionKey() string + RowKey() string + SetPartitionKey(string) error + SetRowKey(string) error +} + +// ContinuationToken is an opaque (ie not useful to inspect) +// struct that Get... methods can return if there are more +// entries to be returned than the ones already +// returned. Just pass it to the same function to continue +// receiving the remaining entries. +type ContinuationToken struct { + NextPartitionKey string + NextRowKey string +} + +type getTableEntriesResponse struct { + Elements []map[string]interface{} `json:"value"` +} + +// QueryTableEntities queries the specified table and returns the unmarshaled +// entities of type retType. +// top parameter limits the returned entries up to top. Maximum top +// allowed by Azure API is 1000. In case there are more than top entries to be +// returned the function will return a non nil *ContinuationToken. You can call the +// same function again passing the received ContinuationToken as previousContToken +// parameter in order to get the following entries. The query parameter +// is the odata query. To retrieve all the entries pass the empty string. +// The function returns a pointer to a TableEntity slice, the *ContinuationToken +// if there are more entries to be returned and an error in case something went +// wrong. +// +// Example: +// entities, cToken, err = tSvc.QueryTableEntities("table", cToken, reflect.TypeOf(entity), 20, "") +func (c *TableServiceClient) QueryTableEntities(tableName AzureTable, previousContToken *ContinuationToken, retType reflect.Type, top int, query string) ([]TableEntity, *ContinuationToken, error) { + if top > maxTopParameter { + return nil, nil, fmt.Errorf("top accepts at maximum %d elements. Requested %d instead", maxTopParameter, top) + } + + uri := c.client.getEndpoint(tableServiceName, pathForTable(tableName), url.Values{}) + uri += fmt.Sprintf("?$top=%d", top) + if query != "" { + uri += fmt.Sprintf("&$filter=%s", url.QueryEscape(query)) + } + + if previousContToken != nil { + uri += fmt.Sprintf("&NextPartitionKey=%s&NextRowKey=%s", previousContToken.NextPartitionKey, previousContToken.NextRowKey) + } + + headers := c.getStandardHeaders() + + headers["Content-Length"] = "0" + + resp, err := c.client.execTable("GET", uri, headers, nil) + + contToken := extractContinuationTokenFromHeaders(resp.headers) + + if err != nil { + return nil, contToken, err + } + defer resp.body.Close() + + if err := checkRespCode(resp.statusCode, []int{http.StatusOK}); err != nil { + return nil, contToken, err + } + + retEntries, err := deserializeEntity(retType, resp.body) + if err != nil { + return nil, contToken, err + } + + return retEntries, contToken, nil +} + +// InsertEntity inserts an entity in the specified table. +// The function fails if there is an entity with the same +// PartitionKey and RowKey in the table. +func (c *TableServiceClient) InsertEntity(table AzureTable, entity TableEntity) error { + var err error + + if sc, err := c.execTable(table, entity, false, "POST"); err != nil { + return checkRespCode(sc, []int{http.StatusCreated}) + } + + return err +} + +func (c *TableServiceClient) execTable(table AzureTable, entity TableEntity, specifyKeysInURL bool, method string) (int, error) { + uri := c.client.getEndpoint(tableServiceName, pathForTable(table), url.Values{}) + if specifyKeysInURL { + uri += fmt.Sprintf("(PartitionKey='%s',RowKey='%s')", url.QueryEscape(entity.PartitionKey()), url.QueryEscape(entity.RowKey())) + } + + headers := c.getStandardHeaders() + + var buf bytes.Buffer + + if err := injectPartitionAndRowKeys(entity, &buf); err != nil { + return 0, err + } + + headers["Content-Length"] = fmt.Sprintf("%d", buf.Len()) + + var err error + var resp *odataResponse + + resp, err = c.client.execTable(method, uri, headers, &buf) + + if err != nil { + return 0, err + } + + defer resp.body.Close() + + return resp.statusCode, nil +} + +// UpdateEntity updates the contents of an entity with the +// one passed as parameter. The function fails if there is no entity +// with the same PartitionKey and RowKey in the table. +func (c *TableServiceClient) UpdateEntity(table AzureTable, entity TableEntity) error { + var err error + + if sc, err := c.execTable(table, entity, true, "PUT"); err != nil { + return checkRespCode(sc, []int{http.StatusNoContent}) + } + return err +} + +// MergeEntity merges the contents of an entity with the +// one passed as parameter. +// The function fails if there is no entity +// with the same PartitionKey and RowKey in the table. +func (c *TableServiceClient) MergeEntity(table AzureTable, entity TableEntity) error { + var err error + + if sc, err := c.execTable(table, entity, true, "MERGE"); err != nil { + return checkRespCode(sc, []int{http.StatusNoContent}) + } + return err +} + +// DeleteEntityWithoutCheck deletes the entity matching by +// PartitionKey and RowKey. There is no check on IfMatch +// parameter so the entity is always deleted. +// The function fails if there is no entity +// with the same PartitionKey and RowKey in the table. +func (c *TableServiceClient) DeleteEntityWithoutCheck(table AzureTable, entity TableEntity) error { + return c.DeleteEntity(table, entity, "*") +} + +// DeleteEntity deletes the entity matching by +// PartitionKey, RowKey and ifMatch field. +// The function fails if there is no entity +// with the same PartitionKey and RowKey in the table or +// the ifMatch is different. +func (c *TableServiceClient) DeleteEntity(table AzureTable, entity TableEntity, ifMatch string) error { + uri := c.client.getEndpoint(tableServiceName, pathForTable(table), url.Values{}) + uri += fmt.Sprintf("(PartitionKey='%s',RowKey='%s')", url.QueryEscape(entity.PartitionKey()), url.QueryEscape(entity.RowKey())) + + headers := c.getStandardHeaders() + + headers["Content-Length"] = "0" + headers["If-Match"] = ifMatch + + resp, err := c.client.execTable("DELETE", uri, headers, nil) + + if err != nil { + return err + } + defer resp.body.Close() + + if err := checkRespCode(resp.statusCode, []int{http.StatusNoContent}); err != nil { + return err + } + + return nil +} + +// InsertOrReplaceEntity inserts an entity in the specified table +// or replaced the existing one. +func (c *TableServiceClient) InsertOrReplaceEntity(table AzureTable, entity TableEntity) error { + var err error + + if sc, err := c.execTable(table, entity, true, "PUT"); err != nil { + return checkRespCode(sc, []int{http.StatusNoContent}) + } + return err +} + +// InsertOrMergeEntity inserts an entity in the specified table +// or merges the existing one. +func (c *TableServiceClient) InsertOrMergeEntity(table AzureTable, entity TableEntity) error { + var err error + + if sc, err := c.execTable(table, entity, true, "MERGE"); err != nil { + return checkRespCode(sc, []int{http.StatusNoContent}) + } + return err +} + +func injectPartitionAndRowKeys(entity TableEntity, buf *bytes.Buffer) error { + if err := json.NewEncoder(buf).Encode(entity); err != nil { + return err + } + + dec := make(map[string]interface{}) + if err := json.NewDecoder(buf).Decode(&dec); err != nil { + return err + } + + // Inject PartitionKey and RowKey + dec[partitionKeyNode] = entity.PartitionKey() + dec[rowKeyNode] = entity.RowKey() + + // Remove tagged fields + // The tag is defined in the const section + // This is useful to avoid storing the PartitionKey and RowKey twice. + numFields := reflect.ValueOf(entity).Elem().NumField() + for i := 0; i < numFields; i++ { + f := reflect.ValueOf(entity).Elem().Type().Field(i) + + if f.Tag.Get(tag) == tagIgnore { + // we must look for its JSON name in the dictionary + // as the user can rename it using a tag + jsonName := f.Name + if f.Tag.Get("json") != "" { + jsonName = f.Tag.Get("json") + } + delete(dec, jsonName) + } + } + + buf.Reset() + + if err := json.NewEncoder(buf).Encode(&dec); err != nil { + return err + } + + return nil +} + +func deserializeEntity(retType reflect.Type, reader io.Reader) ([]TableEntity, error) { + buf := new(bytes.Buffer) + + var ret getTableEntriesResponse + if err := json.NewDecoder(reader).Decode(&ret); err != nil { + return nil, err + } + + tEntries := make([]TableEntity, len(ret.Elements)) + + for i, entry := range ret.Elements { + + buf.Reset() + if err := json.NewEncoder(buf).Encode(entry); err != nil { + return nil, err + } + + dec := make(map[string]interface{}) + if err := json.NewDecoder(buf).Decode(&dec); err != nil { + return nil, err + } + + var pKey, rKey string + // strip pk and rk + for key, val := range dec { + switch key { + case partitionKeyNode: + pKey = val.(string) + case rowKeyNode: + rKey = val.(string) + } + } + + delete(dec, partitionKeyNode) + delete(dec, rowKeyNode) + + buf.Reset() + if err := json.NewEncoder(buf).Encode(dec); err != nil { + return nil, err + } + + // Create a empty retType instance + tEntries[i] = reflect.New(retType.Elem()).Interface().(TableEntity) + // Popolate it with the values + if err := json.NewDecoder(buf).Decode(&tEntries[i]); err != nil { + return nil, err + } + + // Reset PartitionKey and RowKey + tEntries[i].SetPartitionKey(pKey) + tEntries[i].SetRowKey(rKey) + } + + return tEntries, nil +} + +func extractContinuationTokenFromHeaders(h http.Header) *ContinuationToken { + ct := ContinuationToken{h.Get(continuationTokenPartitionKeyHeader), h.Get(continuationTokenRowHeader)} + + if ct.NextPartitionKey != "" && ct.NextRowKey != "" { + return &ct + } + return nil +} diff --git a/storage/table_test.go b/storage/table_test.go new file mode 100644 index 000000000000..307e14a3924d --- /dev/null +++ b/storage/table_test.go @@ -0,0 +1,287 @@ +package storage + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "reflect" + + chk "gopkg.in/check.v1" +) + +type TableClient struct{} + +func getTableClient(c *chk.C) TableServiceClient { + return getBasicClient(c).GetTableService() +} + +type CustomEntity struct { + Name string `json:"name"` + Surname string `json:"surname"` + Number int + PKey string `json:"pk" table:"-"` + RKey string `json:"rk" table:"-"` +} + +type CustomEntityExtended struct { + *CustomEntity + ExtraField string +} + +func (c *CustomEntity) PartitionKey() string { + return c.PKey +} + +func (c *CustomEntity) RowKey() string { + return c.RKey +} + +func (c *CustomEntity) SetPartitionKey(s string) error { + c.PKey = s + return nil +} + +func (c *CustomEntity) SetRowKey(s string) error { + c.RKey = s + return nil +} + +func (s *StorageBlobSuite) Test_SharedKeyLite(c *chk.C) { + cli := getTableClient(c) + + // override the accountKey and accountName + // but make sure to reset when returning + oldAK := cli.client.accountKey + oldAN := cli.client.accountName + + defer func() { + cli.client.accountKey = oldAK + cli.client.accountName = oldAN + }() + + // don't worry, I've already changed mine :) + key, err := base64.StdEncoding.DecodeString("zHDHGs7C+Di9pZSDMuarxJJz3xRBzAHBYaobxpLEc7kwTptR/hPEa9j93hIfb2Tbe9IA50MViGmjQ6nUF/OVvA==") + if err != nil { + c.Fail() + } + + cli.client.accountKey = key + cli.client.accountName = "mindgotest" + + headers := map[string]string{ + "Accept-Charset": "UTF-8", + "Content-Type": "application/json", + "x-ms-date": "Wed, 23 Sep 2015 16:40:05 GMT", + "Content-Length": "0", + "x-ms-version": "2015-02-21", + "Accept": "application/json;odata=nometadata", + } + url := "https://mindgotest.table.core.windows.net/tquery()" + + ret, err := cli.client.createSharedKeyLite(url, headers) + if err != nil { + c.Fail() + } + + c.Assert(ret, chk.Equals, "SharedKeyLite mindgotest:+32DTgsPUgXPo/O7RYaTs0DllA6FTXMj3uK4Qst8y/E=") +} + +func (s *StorageBlobSuite) Test_CreateAndDeleteTable(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + + err = cli.DeleteTable(tn) + c.Assert(err, chk.IsNil) +} + +func (s *StorageBlobSuite) Test_InsertEntities(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + ce := &CustomEntity{Name: "Luke", Surname: "Skywalker", Number: 1543, PKey: "pkey"} + + for i := 0; i < 12; i++ { + ce.SetRowKey(fmt.Sprintf("%d", i)) + + err = cli.InsertEntity(tn, ce) + c.Assert(err, chk.IsNil) + } +} + +func (s *StorageBlobSuite) Test_InsertOrReplaceEntities(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + ce := &CustomEntity{Name: "Darth", Surname: "Skywalker", Number: 60, PKey: "pkey", RKey: "5"} + + err = cli.InsertOrReplaceEntity(tn, ce) + c.Assert(err, chk.IsNil) + + cextra := &CustomEntityExtended{&CustomEntity{PKey: "pkey", RKey: "5"}, "extra"} + err = cli.InsertOrReplaceEntity(tn, cextra) + c.Assert(err, chk.IsNil) +} + +func (s *StorageBlobSuite) Test_InsertOrMergeEntities(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + ce := &CustomEntity{Name: "Darth", Surname: "Skywalker", Number: 60, PKey: "pkey", RKey: "5"} + + err = cli.InsertOrMergeEntity(tn, ce) + c.Assert(err, chk.IsNil) + + cextra := &CustomEntityExtended{&CustomEntity{PKey: "pkey", RKey: "5"}, "extra"} + err = cli.InsertOrReplaceEntity(tn, cextra) + c.Assert(err, chk.IsNil) +} + +func (s *StorageBlobSuite) Test_InsertAndGetEntities(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + ce := &CustomEntity{Name: "Darth", Surname: "Skywalker", Number: 60, PKey: "pkey", RKey: "100"} + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + + ce.SetRowKey("200") + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + + entries, _, err := cli.QueryTableEntities(tn, nil, reflect.TypeOf(ce), 10, "") + c.Assert(err, chk.IsNil) + + c.Assert(len(entries), chk.Equals, 2) + + c.Assert(ce.RowKey(), chk.Equals, entries[1].RowKey()) + + c.Assert(entries[1].(*CustomEntity), chk.DeepEquals, ce) +} + +func (s *StorageBlobSuite) Test_InsertAndQueryEntities(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + ce := &CustomEntity{Name: "Darth", Surname: "Skywalker", Number: 60, PKey: "pkey", RKey: "100"} + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + + ce.SetRowKey("200") + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + + entries, _, err := cli.QueryTableEntities(tn, nil, reflect.TypeOf(ce), 10, "RowKey eq '200'") + c.Assert(err, chk.IsNil) + + c.Assert(len(entries), chk.Equals, 1) + + c.Assert(ce.RowKey(), chk.Equals, entries[0].RowKey()) +} + +func (s *StorageBlobSuite) Test_InsertAndDeleteEntities(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + ce := &CustomEntity{Name: "Test", Surname: "Test2", Number: 0, PKey: "pkey", RKey: "r01"} + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + + ce.Number = 1 + ce.SetRowKey("r02") + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + + entries, _, err := cli.QueryTableEntities(tn, nil, reflect.TypeOf(ce), 10, "Number eq 1") + c.Assert(err, chk.IsNil) + + c.Assert(len(entries), chk.Equals, 1) + + c.Assert(entries[0].(*CustomEntity), chk.DeepEquals, ce) + + c.Assert(cli.DeleteEntityWithoutCheck(tn, entries[0]), chk.IsNil) + + entries, _, err = cli.QueryTableEntities(tn, nil, reflect.TypeOf(ce), 10, "") + c.Assert(err, chk.IsNil) + + // only 1 entry must be present + c.Assert(len(entries), chk.Equals, 1) +} + +func (s *StorageBlobSuite) Test_ContinuationToken(c *chk.C) { + cli := getTableClient(c) + + tn := AzureTable(randTable()) + + err := cli.CreateTable(tn) + c.Assert(err, chk.IsNil) + defer cli.DeleteTable(tn) + + var ce *CustomEntity + var ceList [5]*CustomEntity + + for i := 0; i < 5; i++ { + ce = &CustomEntity{Name: "Test", Surname: "Test2", Number: i, PKey: "pkey", RKey: fmt.Sprintf("r%d", i)} + ceList[i] = ce + c.Assert(cli.InsertOrReplaceEntity(tn, ce), chk.IsNil) + } + + // retrieve using top = 2. Should return 2 entries, 2 entries and finally + // 1 entry + entries, contToken, err := cli.QueryTableEntities(tn, nil, reflect.TypeOf(ce), 2, "") + c.Assert(err, chk.IsNil) + c.Assert(len(entries), chk.Equals, 2) + c.Assert(entries[0].(*CustomEntity), chk.DeepEquals, ceList[0]) + c.Assert(entries[1].(*CustomEntity), chk.DeepEquals, ceList[1]) + c.Assert(contToken, chk.NotNil) + + entries, contToken, err = cli.QueryTableEntities(tn, contToken, reflect.TypeOf(ce), 2, "") + c.Assert(err, chk.IsNil) + c.Assert(len(entries), chk.Equals, 2) + c.Assert(entries[0].(*CustomEntity), chk.DeepEquals, ceList[2]) + c.Assert(entries[1].(*CustomEntity), chk.DeepEquals, ceList[3]) + c.Assert(contToken, chk.NotNil) + + entries, contToken, err = cli.QueryTableEntities(tn, contToken, reflect.TypeOf(ce), 2, "") + c.Assert(err, chk.IsNil) + c.Assert(len(entries), chk.Equals, 1) + c.Assert(entries[0].(*CustomEntity), chk.DeepEquals, ceList[4]) + c.Assert(contToken, chk.IsNil) +} + +func randTable() string { + const alphanum = "abcdefghijklmnopqrstuvwxyz" + var bytes = make([]byte, 32) + rand.Read(bytes) + for i, b := range bytes { + bytes[i] = alphanum[b%byte(len(alphanum))] + } + return string(bytes) +}