From 317b60156211b42e1c26b9ed4fa1da86170c8504 Mon Sep 17 00:00:00 2001 From: Edwin Candinegara Date: Sun, 21 Aug 2022 21:34:00 +0800 Subject: [PATCH 1/3] Add godoc for google package --- README.md | 2 +- google/auth/doc.go | 5 +++++ google/auth/models.go | 1 + google/auth/oauth2.go | 20 ++++++++++++++++++++ google/auth/service.go | 16 ++++++++++++++++ models.go | 6 ++++++ 6 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 google/auth/doc.go diff --git a/README.md b/README.md index 1bc3a36..d010331 100644 --- a/README.md +++ b/README.md @@ -513,7 +513,7 @@ auth, err := auth.NewServiceFromFile( **Explanations:** -1. The `service_account_json` can be obtained by following the steps in this [Google OAuth2 page](https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount). The JSON file of interest is **the downloaded file after creating a new service account key**. +1. The `service_account_json` can be obtained by following the steps in this [Google Service Account page](https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount). The JSON file of interest is **the downloaded file after creating a new service account key**. 2. The `scopes` tells Google what your application can do to your spreadsheets (`auth.GoogleSheetsReadOnly`, `auth.GoogleSheetsWriteOnly`, or `auth.GoogleSheetsReadWrite`). If you want to understand the details, you can start from this [Google Service Account page](https://developers.google.com/identity/protocols/oauth2/service-account). diff --git a/google/auth/doc.go b/google/auth/doc.go new file mode 100644 index 0000000..45518ad --- /dev/null +++ b/google/auth/doc.go @@ -0,0 +1,5 @@ +// Package auth provides general Google authentication implementation agnostic to what specific Google services or +// resources are used. Implementations in this package generate a https://pkg.go.dev/net/http#Client that can be used +// to access Google REST APIs seamlessly. Authentications will be handled automatically, including refreshing +//the access token when necessary. +package auth diff --git a/google/auth/models.go b/google/auth/models.go index e1d9022..949c087 100644 --- a/google/auth/models.go +++ b/google/auth/models.go @@ -1,5 +1,6 @@ package auth +// Scopes encapsulates a list of Google resources scopes to request during authentication step. type Scopes []string var ( diff --git a/google/auth/oauth2.go b/google/auth/oauth2.go index 8018caa..6494946 100644 --- a/google/auth/oauth2.go +++ b/google/auth/oauth2.go @@ -22,18 +22,38 @@ const ( stateLength = 32 ) +// OAuth2Config defines a list of configurations that can be used to customise how the Google OAuth2 flow works. type OAuth2Config struct { + // HTTPClient allows the client to customise the HTTP client used to perform the REST API calls. + // This will be useful if you want to have a more granular control over the HTTP client (e.g. using a connection pool). HTTPClient *http.Client } +// OAuth2 takes in OAuth2 relevant information and sets up *http.Client that can be used to access +// Google APIs seamlessly. Authentications will be handled automatically, including refreshing the access token +// when necessary. type OAuth2 struct { googleAuthClient *http.Client } +// HTTPClient returns a Google OAuth2 authenticated *http.Client that can be used to access Google APIs. func (o *OAuth2) HTTPClient() *http.Client { return o.googleAuthClient } +// NewOAuth2FromFile creates an OAuth2 instance by reading the OAuth2 related information from a secret file. +// +// The "secretFilePath" is referring to the OAuth2 credentials JSON file that can be obtained by +// creating a new OAuth2 credentials in https://console.cloud.google.com/apis/credentials. +// You can put any link for the redirection URL field. +// +// The "credsFilePath" is referring to a file where the generated access and refresh token will be cached. +// This file will be created automatically once the OAuth2 authentication is successful. +// +// The "scopes" tells Google what your application can do to your spreadsheets. +// +// Note that since this is an OAuth2 server flow, human interaction will be needed for the very first authentication. +// During the OAuth2 flow, you will be asked to click a generated URL in the terminal. func NewOAuth2FromFile(secretFilePath string, credsFilePath string, scopes Scopes, config OAuth2Config) (*OAuth2, error) { rawAuthConfig, err := ioutil.ReadFile(secretFilePath) if err != nil { diff --git a/google/auth/service.go b/google/auth/service.go index 42f310a..4af4375 100644 --- a/google/auth/service.go +++ b/google/auth/service.go @@ -7,18 +7,32 @@ import ( "golang.org/x/oauth2/google" ) +// ServiceConfig defines a list of configurations that can be used to customise how the Google +// service account authentication flow works. type ServiceConfig struct { + // HTTPClient allows the client to customise the HTTP client used to perform the REST API calls. + // This will be useful if you want to have a more granular control over the HTTP client (e.g. using a connection pool). HTTPClient *http.Client } +// Service takes in service account relevant information and sets up *http.Client that can be used to access +// Google APIs seamlessly. Authentications will be handled automatically, including refreshing the access token +// when necessary. type Service struct { googleAuthClient *http.Client } +// HTTPClient returns a Google OAuth2 authenticated *http.Client that can be used to access Google APIs. func (s *Service) HTTPClient() *http.Client { return s.googleAuthClient } +// NewServiceFromFile creates a Service instance by reading the Google service account related information from a file. +// +// The "filePath" is referring to the service account JSON file that can be obtained by +// creating a new service account credentials in https://developers.google.com/identity/protocols/oauth2/service-account#creatinganaccount. +// +// The "scopes" tells Google what your application can do to your spreadsheets. func NewServiceFromFile(filePath string, scopes Scopes, config ServiceConfig) (*Service, error) { authConfig, err := ioutil.ReadFile(filePath) if err != nil { @@ -27,6 +41,8 @@ func NewServiceFromFile(filePath string, scopes Scopes, config ServiceConfig) (* return NewServiceFromJSON(authConfig, scopes, config) } +// NewServiceFromJSON works exactly the same as NewServiceFromFile, but instead of reading from a file, the raw content +// of the Google service account JSON file is provided directly. func NewServiceFromJSON(raw []byte, scopes Scopes, config ServiceConfig) (*Service, error) { c, err := google.JWTConfigFromJSON(raw, scopes...) if err != nil { diff --git a/models.go b/models.go index 39bd81e..b8623bd 100644 --- a/models.go +++ b/models.go @@ -16,10 +16,14 @@ type OrderBy string const ( KVModeDefault KVMode = 0 KVModeAppendOnly KVMode = 1 +) +const ( OrderByAsc OrderBy = "ASC" OrderByDesc OrderBy = "DESC" +) +const ( // Currently limited to 26. // Otherwise, the sheet creation must extend the column as well to make the rowGetIndicesQueryTemplate formula works. // TODO(edocsss): add an option to extend the number of columns. @@ -49,7 +53,9 @@ const ( var ( ErrKeyNotFound = errors.New("error key not found") +) +var ( defaultRowHeaderRange = "A1:" + generateColumnName(maxColumn-1) + "1" defaultRowFullTableRange = "A2:" + generateColumnName(maxColumn-1) rowDeleteRangeTemplate = "A%d:" + generateColumnName(maxColumn-1) + "%d" From 07b6748bb75d0f346e52dcd48b2057bd7ce89e59 Mon Sep 17 00:00:00 2001 From: Edwin Candinegara Date: Mon, 22 Aug 2022 09:54:08 +0800 Subject: [PATCH 2/3] Add godoc for KV store --- codec.go | 14 ++++++-- codec_test.go | 4 +-- kv.go | 91 ++++++++++++++++++++++++--------------------------- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/codec.go b/codec.go index 5e1ee16..b59ced3 100644 --- a/codec.go +++ b/codec.go @@ -4,13 +4,21 @@ import "errors" const basicCodecPrefix string = "!" -type BasicCodec struct{} +// basicCodec encodes and decodes any bytes data using a very simple encoding rule. +// A prefix (an exclamation mark "!") will be attached to the raw bytes data. +// +// This allows the library to differentiate between empty raw bytes provided by the client from +// getting an empty data from Google Sheets API. +type basicCodec struct{} -func (c *BasicCodec) Encode(value []byte) (string, error) { +// Encode encodes the given raw bytes by using an exclamation mark "!" as a prefix. +func (c *basicCodec) Encode(value []byte) (string, error) { return basicCodecPrefix + string(value), nil } -func (c *BasicCodec) Decode(value string) ([]byte, error) { +// Decode converts the string data read from Google Sheet into raw bytes data after removing the +// exclamation mark "!" prefix. +func (c *basicCodec) Decode(value string) ([]byte, error) { if len(value) == 0 { return nil, errors.New("basic decode fail: empty string") } diff --git a/codec_test.go b/codec_test.go index 026281b..73321b4 100644 --- a/codec_test.go +++ b/codec_test.go @@ -33,7 +33,7 @@ func TestBasicCodecEncode(t *testing.T) { expected: "!" + naValue, }, } - codec := &BasicCodec{} + codec := &basicCodec{} for _, c := range tc { t.Run(c.name, func(t *testing.T) { @@ -82,7 +82,7 @@ func TestBasicCodecDecode(t *testing.T) { hasErr: false, }, } - codec := &BasicCodec{} + codec := &basicCodec{} for _, c := range tc { t.Run(c.name, func(t *testing.T) { diff --git a/kv.go b/kv.go index cfd9be5..b8ad2ff 100644 --- a/kv.go +++ b/kv.go @@ -8,59 +8,17 @@ import ( "github.com/FreeLeh/GoFreeLeh/internal/google/sheets" ) +// GoogleSheetKVStoreConfig defines a list of configurations that can be used to customise how the GoogleSheetKVStore works. type GoogleSheetKVStoreConfig struct { Mode KVMode codec Codec } -/* - -There are 2 formats of the same KV storage using Google Sheet: -- Default -> like a normal KV store, each key only appears once in the sheet. -- Append only -> each key update will be added as a new row, there maybe >1 rows for the same keys. The latest added row for a key is the latest value. - -## APPEND ONLY MODE - -The structure is as follows: - -key | value | timestamp -k1 | v1 | 1 -k2 | v2 | 2 -k3 | v3 | 3 -k2 | v22 | 4 --> Set(k2, v22) -k3 | v32 | 5 --> Set(k3, v32) -k2 | | 6 --> Delete(k2) -> value is set to an empty string - -The logic for Set() is as simple as appending a new row at the end of the current sheet with the latest value and the timestamp in milliseconds. -The logic for Delete() is basically Set(key, ""). -The logic for Get() is more complicated. - -=VLOOKUP(key, SORT(, 3, FALSE), 2, FALSE) - -The full table range can be assumed to be A1:C5000000. -The integer "3" is referring to the timestamp column (the third column of the table). -The FALSE inside the SORT() means sort in descending order. -The FALSE inside the VLOOKUP() means we consider the table as non-sorted (so that we can take the first row which has the latest timestamp as the final value). -The integer "2" is referring to which column we want to return from VLOOKUP(), which is referring to the value column. - -If the value returned is either "#N/A" or "", that means the key is not found or already deleted. - -## DEFAULT MODE - -The structure is as follows: - -key | value | timestamp -k1 | v1 | 1 -k2 | v2 | 2 -k3 | v3 | 3 - -The logic for Set() is Get() + (Append(OVERWRITE_MODE) if not exists OR Update if already exists). -The logic for Delete() is Get() + Clear(). -The logic for Get() is just a simple VLOOKUP without any sorting involved (unlike the APPEND ONLY mode). - -Here we assume there cannot be any race condition that leads to two rows with the same key. - -*/ +// GoogleSheetKVStore encapsulates key-value store functionality on top of a Google Sheet. +// +// There are 2 operation modes for the key-value store: default and append only mode. +// +// For more details on how they differ, please read the explanations for each method or the TODO(edocsss) protocol page. type GoogleSheetKVStore struct { wrapper sheetsWrapper spreadsheetID string @@ -70,6 +28,18 @@ type GoogleSheetKVStore struct { config GoogleSheetKVStoreConfig } +// Get retrieves the value associated with the given key. +// If the key exists in the store, the raw bytes value and no error will be returned. +// If the key does not exist in the store, a nil []byte and a wrapped ErrKeyNotFound will be returned. +// +// In default mode, +// - There will be only one row with the given key. It will return the value for that in that row. +// - There is only 1 API call behind the scene. +// +// In append only mode, +// - As there could be multiple rows with the same key, we need to only use the latest row as it contains the last updated value. +// - Note that deletion using append only mode results in a new row with a tombstone value. This method will also recognise and handle such cases. +// - There is only 1 API call behind the scene. func (s *GoogleSheetKVStore) Get(ctx context.Context, key string) ([]byte, error) { query := fmt.Sprintf(kvGetDefaultQueryTemplate, key, getA1Range(s.sheetName, defaultKVTableRange)) if s.config.Mode == KVModeAppendOnly { @@ -96,6 +66,16 @@ func (s *GoogleSheetKVStore) Get(ctx context.Context, key string) ([]byte, error return s.config.codec.Decode(value.(string)) } +// Set inserts the key-value pair into the key-value store. +// +// In default mode, +// - If the key is not in the store, `Set` will create a new row and store the key value pair there. +// - If the key is in the store, `Set` will update the previous row with the new value and timestamp. +// - There are exactly 2 API calls behind the scene: getting the row for the key and creating/updating with the given key value data. +// +// In append only mode, +// - It always creates a new row at the bottom of the sheet with the latest value and timestamp. +// - There is only 1 API call behind the scene. func (s *GoogleSheetKVStore) Set(ctx context.Context, key string, value []byte) error { encoded, err := s.config.codec.Encode(value) if err != nil { @@ -169,6 +149,16 @@ func (s *GoogleSheetKVStore) findKeyA1Range(ctx context.Context, key string) (sh return sheets.NewA1Range(a1Range), nil } +// Delete deletes the given key from the key-value store. +// +// In default mode, +// - If the key is not in the store, it will not do anything. +// - If the key is in the store, it will remove that row. +// - There are up to 2 API calls behind the scene: getting the row for the key and remove the row (if the key exists). +// +// In append only mode, +// - It creates a new row at the bottom of the sheet with a tombstone value and timestamp. +// - There is only 1 API call behind the scene. func (s *GoogleSheetKVStore) Delete(ctx context.Context, key string) error { if s.config.Mode == KVModeAppendOnly { return s.deleteAppendOnly(ctx, key) @@ -193,11 +183,14 @@ func (s *GoogleSheetKVStore) deleteDefault(ctx context.Context, key string) erro return err } +// Close cleans up all held resources like the scratchpad cell booked for this specific GoogleSheetKVStore instance. func (s *GoogleSheetKVStore) Close(ctx context.Context) error { _, err := s.wrapper.Clear(ctx, s.spreadsheetID, []string{s.scratchpadLocation.Original}) return err } +// NewGoogleSheetKVStore creates an instance of the key-value store with the given configuration. +// It will also try to create the sheet, in case it does not exist yet. func NewGoogleSheetKVStore( auth sheets.AuthClient, spreadsheetID string, @@ -233,6 +226,6 @@ func NewGoogleSheetKVStore( } func applyGoogleSheetKVStoreConfig(config GoogleSheetKVStoreConfig) GoogleSheetKVStoreConfig { - config.codec = &BasicCodec{} + config.codec = &basicCodec{} return config } From 9c6dfdce9c0072d695f3d361b3b688af86527555 Mon Sep 17 00:00:00 2001 From: Edwin Candinegara Date: Mon, 22 Aug 2022 20:39:29 +0800 Subject: [PATCH 3/3] Add godoc for row store --- models.go | 22 ++++++---- row.go | 75 +++++++++++++++++++++++++-------- stmt.go | 121 ++++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 164 insertions(+), 54 deletions(-) diff --git a/models.go b/models.go index b8623bd..b472b76 100644 --- a/models.go +++ b/models.go @@ -9,20 +9,15 @@ import ( "github.com/FreeLeh/GoFreeLeh/internal/google/sheets" ) +// KVMode defines the mode of the key value store. +// For more details, please read the README file. type KVMode int -type OrderBy string - const ( KVModeDefault KVMode = 0 KVModeAppendOnly KVMode = 1 ) -const ( - OrderByAsc OrderBy = "ASC" - OrderByDesc OrderBy = "DESC" -) - const ( // Currently limited to 26. // Otherwise, the sheet creation must extend the column as well to make the rowGetIndicesQueryTemplate formula works. @@ -51,6 +46,7 @@ const ( rowIdxFormula = "=ROW()" ) +// ErrKeyNotFound is returned only for the key-value store and when the key does not exist. var ( ErrKeyNotFound = errors.New("error key not found") ) @@ -65,6 +61,8 @@ var ( googleSheetSelectStmtStringKeyword = regexp.MustCompile("^(date|datetime|timeofday)") ) +// Codec is an interface for encoding and decoding the data provided by the client. +// At the moment, only key-value store requires data encoding. type Codec interface { Encode(value []byte) (string, error) Decode(value string) ([]byte, error) @@ -106,6 +104,16 @@ func (m colsMapping) ColIdxNameMap() map[string]string { return result } +// OrderBy defines the type of column ordering used for GoogleSheetRowStore.Select(). +type OrderBy string + +const ( + OrderByAsc OrderBy = "ASC" + OrderByDesc OrderBy = "DESC" +) + +// ColumnOrderBy defines what ordering is required for a particular column. +// This is used for GoogleSheetRowStore.Select(). type ColumnOrderBy struct { Column string OrderBy OrderBy diff --git a/row.go b/row.go index d3b1003..9f33cdf 100644 --- a/row.go +++ b/row.go @@ -9,7 +9,12 @@ import ( "github.com/FreeLeh/GoFreeLeh/internal/google/sheets" ) +// GoogleSheetRowStoreConfig defines a list of configurations that can be used to customise how the GoogleSheetRowStore works. type GoogleSheetRowStoreConfig struct { + // Columns defines the list of column names. + // Note that the column ordering matters. + // The column ordering will be used for arranging the real columns in Google Sheet. + // Changing the column ordering in this config but not in Google Sheet will result in unexpected behaviour. Columns []string } @@ -23,6 +28,7 @@ func (c GoogleSheetRowStoreConfig) validate() error { return nil } +// GoogleSheetRowStore encapsulates row store functionality on top of a Google Sheet. type GoogleSheetRowStore struct { wrapper sheetsWrapper spreadsheetID string @@ -33,37 +39,72 @@ type GoogleSheetRowStore struct { config GoogleSheetRowStoreConfig } -func (s *GoogleSheetRowStore) Select(output interface{}, columns ...string) *googleSheetSelectStmt { +// Select specifies which columns to return from the Google Sheet when querying and the output variable +// the data should be stored. +// You can think of this operation like a SQL SELECT statement (with limitations). +// +// If "columns" is an empty slice of string, then all columns will be returned. +// If a column is not found in the provided list of columns in `GoogleSheetRowStoreConfig.Columns`, that column will be ignored. +// +// "output" must be a pointer to a slice of a data type. +// The conversion from the Google Sheet data into the slice will be done using https://github.com/mitchellh/mapstructure. +// +// If you are providing a slice of structs into the "output" parameter and you want to define the mapping between the +// column name with the field name, you should add a "db" struct tag. +// +// // Without the `db` struct tag, the column name used will be "Name" and "Age". +// type Person struct { +// Name string `db:"name"` +// Age int `db:"age"` +// } +// +// Please note that calling Select() does not execute the query yet. +// Call GoogleSheetSelectStmt.Exec to actually execute the query. +func (s *GoogleSheetRowStore) Select(output interface{}, columns ...string) *GoogleSheetSelectStmt { return newGoogleSheetSelectStmt(s, output, columns) } -// Insert will try to infer what is the type of each row and perform certain logic based on the type. -// For example, a struct will be converted into a map[string]interface{} and then into []interface{} (following the -// column mapping ordering). +// Insert specifies the rows to be inserted into the Google Sheet. // -// A few things to take note: -// - Only `struct` base type (including a pointer to a struct) is supported. -// - Each field name corresponds to the column name (case-sensitive). -// - The mapping between field name and column name can be changed by adding the struct field tag `db:""`. -func (s *GoogleSheetRowStore) Insert(rows ...interface{}) *googleSheetInsertStmt { +// The underlying data type of each row must be a struct or a pointer to a struct. +// Providing other data types will result in an error. +// +// By default, the column name will be following the struct field name (case-sensitive). +// If you want to map the struct field name into another name, you can add a "db" struct tag +// (see GoogleSheetRowStore.Select docs for more details). +// +// Please note that calling Insert() does not execute the insertion yet. +// Call GoogleSheetInsertStmt.Exec() to actually execute the insertion. +func (s *GoogleSheetRowStore) Insert(rows ...interface{}) *GoogleSheetInsertStmt { return newGoogleSheetInsertStmt(s, rows) } -// Update applies the given value for each column into the applicable rows. -// Note that in current version, we are not doing any data type validation for the given column-value pair. -// Each value in the `interface{}` is going to be JSON marshalled. -func (s *GoogleSheetRowStore) Update(colToValue map[string]interface{}) *googleSheetUpdateStmt { +// Update specifies the new value for each of the targeted columns. +// +// The "colToValue" parameter specifies what value should be updated for which column. +// Each value in the map[string]interface{} is going to be JSON marshalled. +// If "colToValue" is empty, an error will be returned when GoogleSheetUpdateStmt.Exec() is called. +func (s *GoogleSheetRowStore) Update(colToValue map[string]interface{}) *GoogleSheetUpdateStmt { return newGoogleSheetUpdateStmt(s, colToValue) } -func (s *GoogleSheetRowStore) Delete() *googleSheetDeleteStmt { +// Delete prepares rows deletion operation. +// +// Please note that calling Delete() does not execute the deletion yet. +// Call GoogleSheetDeleteStmt.Exec() to actually execute the deletion. +func (s *GoogleSheetRowStore) Delete() *GoogleSheetDeleteStmt { return newGoogleSheetDeleteStmt(s) } -func (s *GoogleSheetRowStore) Count() *googleSheetCountStmt { +// Count prepares rows counting operation. +// +// Please note that calling Count() does not execute the query yet. +// Call GoogleSheetCountStmt.Exec() to actually execute the query. +func (s *GoogleSheetRowStore) Count() *GoogleSheetCountStmt { return newGoogleSheetCountStmt(s) } +// Close cleans up all held resources like the scratchpad cell booked for this specific GoogleSheetRowStore instance. func (s *GoogleSheetRowStore) Close(ctx context.Context) error { _, err := s.wrapper.Clear(ctx, s.spreadsheetID, []string{s.scratchpadLocation.Original}) return err @@ -97,6 +138,8 @@ func (s *GoogleSheetRowStore) ensureHeaders() error { return nil } +// NewGoogleSheetRowStore creates an instance of the row based store with the given configuration. +// It will also try to create the sheet, in case it does not exist yet. func NewGoogleSheetRowStore( auth sheets.AuthClient, spreadsheetID string, @@ -140,7 +183,7 @@ func NewGoogleSheetRowStore( return store } -// The additional _ts column is needed to differentiate which row is truly empty and which one is not. +// The additional rowIdxCol column is needed to differentiate which row is truly empty and which one is not. // Currently, we use this for detecting which rows are really empty for UPDATE without WHERE clause. // Otherwise, it will always update all rows (instead of the non-empty rows only). func injectTimestampCol(config GoogleSheetRowStoreConfig) GoogleSheetRowStoreConfig { diff --git a/stmt.go b/stmt.go index 89845c2..ad66545 100644 --- a/stmt.go +++ b/stmt.go @@ -197,34 +197,60 @@ func newQueryBuilder(colReplacements map[string]string, colSelected []string) *q return &queryBuilder{replacer: strings.NewReplacer(replacements...), columns: colSelected} } -type googleSheetSelectStmt struct { +// GoogleSheetSelectStmt encapsulates information required to query the row store. +type GoogleSheetSelectStmt struct { store *GoogleSheetRowStore columns []string queryBuilder *queryBuilder output interface{} } -func (s *googleSheetSelectStmt) Where(condition string, args ...interface{}) *googleSheetSelectStmt { +// Where specifies the condition to meet for a row to be included. +// +// "condition" specifies the WHERE clause. +// Values in the WHERE clause should be replaced by a placeholder "?". +// The actual values used for each placeholder (ordering matters) are provided via the "args" parameter. +// +// "args" specifies the real value to replace each placeholder in the WHERE clause. +// Note that the first "args" value will replace the first placeholder "?" in the WHERE clause. +// +// If you want to understand the reason behind this design, please read TODO(edocsss) the protocol page. +// +// All conditions supported by Google Sheet "QUERY" function are supported by this library. +// You can read the full information in https://developers.google.com/chart/interactive/docs/querylanguage#where. +func (s *GoogleSheetSelectStmt) Where(condition string, args ...interface{}) *GoogleSheetSelectStmt { s.queryBuilder.Where(condition, args...) return s } -func (s *googleSheetSelectStmt) OrderBy(ordering []ColumnOrderBy) *googleSheetSelectStmt { +// OrderBy specifies the column ordering. +// +// The default value is no ordering specified. +func (s *GoogleSheetSelectStmt) OrderBy(ordering []ColumnOrderBy) *GoogleSheetSelectStmt { s.queryBuilder.OrderBy(ordering) return s } -func (s *googleSheetSelectStmt) Limit(limit uint64) *googleSheetSelectStmt { +// Limit specifies the number of rows to retrieve. +// +// The default value is 0. +func (s *GoogleSheetSelectStmt) Limit(limit uint64) *GoogleSheetSelectStmt { s.queryBuilder.Limit(limit) return s } -func (s *googleSheetSelectStmt) Offset(offset uint64) *googleSheetSelectStmt { +// Offset specifies the number of rows to skip before starting to include the rows. +// +// The default value is 0. +func (s *GoogleSheetSelectStmt) Offset(offset uint64) *GoogleSheetSelectStmt { s.queryBuilder.Offset(offset) return s } -func (s *googleSheetSelectStmt) Exec(ctx context.Context) error { +// Exec retrieves rows matching with the given condition. +// +// There is only 1 API call behind the scene. +func (s *GoogleSheetSelectStmt) Exec(ctx context.Context) error { if err := s.ensureOutputSlice(); err != nil { return err } @@ -249,7 +275,7 @@ func (s *googleSheetSelectStmt) Exec(ctx context.Context) error { return mapstructureDecode(m, s.output) } -func (s *googleSheetSelectStmt) buildQueryResultMap(original sheets.QueryRowsResult) []map[string]interface{} { +func (s *GoogleSheetSelectStmt) buildQueryResultMap(original sheets.QueryRowsResult) []map[string]interface{} { result := make([]map[string]interface{}, len(original.Rows)) for rowIdx, row := range original.Rows { @@ -264,7 +290,7 @@ func (s *googleSheetSelectStmt) buildQueryResultMap(original sheets.QueryRowsRes return result } -func (s *googleSheetSelectStmt) ensureOutputSlice() error { +func (s *GoogleSheetSelectStmt) ensureOutputSlice() error { // Passing an uninitialised slice will not compare to nil due to this: https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/ // Only if passing an untyped `nil` will compare to the `nil` in the line below. // Observations as below: @@ -292,12 +318,12 @@ func (s *googleSheetSelectStmt) ensureOutputSlice() error { return nil } -func newGoogleSheetSelectStmt(store *GoogleSheetRowStore, output interface{}, columns []string) *googleSheetSelectStmt { +func newGoogleSheetSelectStmt(store *GoogleSheetRowStore, output interface{}, columns []string) *GoogleSheetSelectStmt { if len(columns) == 0 { columns = store.config.Columns } - return &googleSheetSelectStmt{ + return &GoogleSheetSelectStmt{ store: store, columns: columns, queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), columns), @@ -305,12 +331,13 @@ func newGoogleSheetSelectStmt(store *GoogleSheetRowStore, output interface{}, co } } -type googleSheetInsertStmt struct { +// GoogleSheetInsertStmt encapsulates information required to insert new rows into the Google Sheet. +type GoogleSheetInsertStmt struct { store *GoogleSheetRowStore rows []interface{} } -func (s *googleSheetInsertStmt) convertRowToSlice(row interface{}) ([]interface{}, error) { +func (s *GoogleSheetInsertStmt) convertRowToSlice(row interface{}) ([]interface{}, error) { if row == nil { return nil, errors.New("row type must not be nil") } @@ -340,7 +367,11 @@ func (s *googleSheetInsertStmt) convertRowToSlice(row interface{}) ([]interface{ return result, nil } -func (s *googleSheetInsertStmt) Exec(ctx context.Context) error { +// Exec inserts the provided new rows data into Google Sheet. +// This method calls the relevant Google Sheet APIs to actually insert the new rows. +// +// There is only 1 API call behind the scene. +func (s *GoogleSheetInsertStmt) Exec(ctx context.Context) error { if len(s.rows) == 0 { return nil } @@ -363,20 +394,25 @@ func (s *googleSheetInsertStmt) Exec(ctx context.Context) error { return err } -func newGoogleSheetInsertStmt(store *GoogleSheetRowStore, rows []interface{}) *googleSheetInsertStmt { - return &googleSheetInsertStmt{ +func newGoogleSheetInsertStmt(store *GoogleSheetRowStore, rows []interface{}) *GoogleSheetInsertStmt { + return &GoogleSheetInsertStmt{ store: store, rows: rows, } } -type googleSheetUpdateStmt struct { +// GoogleSheetUpdateStmt encapsulates information required to update rows. +type GoogleSheetUpdateStmt struct { store *GoogleSheetRowStore colToValue map[string]interface{} queryBuilder *queryBuilder } -func (s *googleSheetUpdateStmt) Where(condition string, args ...interface{}) *googleSheetUpdateStmt { +// Where specifies the condition to choose which rows are affected. +// +// It works just like the GoogleSheetSelectStmt.Where() method. +// Please read GoogleSheetSelectStmt.Where() for more details. +func (s *GoogleSheetUpdateStmt) Where(condition string, args ...interface{}) *GoogleSheetUpdateStmt { // The first condition `_ts IS NOT NULL` is necessary to ensure we are just updating rows that are non-empty. // This is required for UPDATE without WHERE clause (otherwise it will see every row as update target). if condition == "" { @@ -387,7 +423,14 @@ func (s *googleSheetUpdateStmt) Where(condition string, args ...interface{}) *go return s } -func (s *googleSheetUpdateStmt) Exec(ctx context.Context) error { +// Exec updates rows matching the condition with the new values for affected columns. +// +// There are 2 API calls behind the scene. +func (s *GoogleSheetUpdateStmt) Exec(ctx context.Context) error { + if len(s.colToValue) == 0 { + return errors.New("empty colToValue, at least one column must be updated") + } + selectStmt, err := s.queryBuilder.Generate() if err != nil { return err @@ -410,7 +453,7 @@ func (s *googleSheetUpdateStmt) Exec(ctx context.Context) error { return err } -func (s *googleSheetUpdateStmt) generateBatchUpdateRequests(rowIndices []int64) ([]sheets.BatchUpdateRowsRequest, error) { +func (s *GoogleSheetUpdateStmt) generateBatchUpdateRequests(rowIndices []int64) ([]sheets.BatchUpdateRowsRequest, error) { requests := make([]sheets.BatchUpdateRowsRequest, 0) for col, value := range s.colToValue { @@ -431,25 +474,33 @@ func (s *googleSheetUpdateStmt) generateBatchUpdateRequests(rowIndices []int64) return requests, nil } -func newGoogleSheetUpdateStmt(store *GoogleSheetRowStore, colToValue map[string]interface{}) *googleSheetUpdateStmt { - return &googleSheetUpdateStmt{ +func newGoogleSheetUpdateStmt(store *GoogleSheetRowStore, colToValue map[string]interface{}) *GoogleSheetUpdateStmt { + return &GoogleSheetUpdateStmt{ store: store, colToValue: colToValue, queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), []string{rowIdxCol}), } } -type googleSheetDeleteStmt struct { +// GoogleSheetDeleteStmt encapsulates information required to delete rows. +type GoogleSheetDeleteStmt struct { store *GoogleSheetRowStore queryBuilder *queryBuilder } -func (s *googleSheetDeleteStmt) Where(condition string, args ...interface{}) *googleSheetDeleteStmt { +// Where specifies the condition to choose which rows are affected. +// +// It works just like the GoogleSheetSelectStmt.Where() method. +// Please read GoogleSheetSelectStmt.Where() for more details. +func (s *GoogleSheetDeleteStmt) Where(condition string, args ...interface{}) *GoogleSheetDeleteStmt { s.queryBuilder.Where(condition, args...) return s } -func (s *googleSheetDeleteStmt) Exec(ctx context.Context) error { +// Exec deletes rows matching the condition. +// +// There are 2 API calls behind the scene. +func (s *GoogleSheetDeleteStmt) Exec(ctx context.Context) error { selectStmt, err := s.queryBuilder.Generate() if err != nil { return err @@ -467,24 +518,32 @@ func (s *googleSheetDeleteStmt) Exec(ctx context.Context) error { return err } -func newGoogleSheetDeleteStmt(store *GoogleSheetRowStore) *googleSheetDeleteStmt { - return &googleSheetDeleteStmt{ +func newGoogleSheetDeleteStmt(store *GoogleSheetRowStore) *GoogleSheetDeleteStmt { + return &GoogleSheetDeleteStmt{ store: store, queryBuilder: newQueryBuilder(store.colsMapping.ColIdxNameMap(), []string{lastColIdxName}), } } -type googleSheetCountStmt struct { +// GoogleSheetCountStmt encapsulates information required to count the number of rows matching some conditions. +type GoogleSheetCountStmt struct { store *GoogleSheetRowStore queryBuilder *queryBuilder } -func (s *googleSheetCountStmt) Where(condition string, args ...interface{}) *googleSheetCountStmt { +// Where specifies the condition to choose which rows are affected. +// +// It works just like the GoogleSheetSelectStmt.Where() method. +// Please read GoogleSheetSelectStmt.Where() for more details. +func (s *GoogleSheetCountStmt) Where(condition string, args ...interface{}) *GoogleSheetCountStmt { s.queryBuilder.Where(condition, args...) return s } -func (s *googleSheetCountStmt) Exec(ctx context.Context) (uint64, error) { +// Exec counts the number of rows matching the provided condition. +// +// There is only 1 API call behind the scene. +func (s *GoogleSheetCountStmt) Exec(ctx context.Context) (uint64, error) { selectStmt, err := s.queryBuilder.Generate() if err != nil { return 0, err @@ -516,8 +575,8 @@ func (s *googleSheetCountStmt) Exec(ctx context.Context) (uint64, error) { return strconv.ParseUint(raw, 10, 64) } -func newGoogleSheetCountStmt(store *GoogleSheetRowStore) *googleSheetCountStmt { - return &googleSheetCountStmt{ +func newGoogleSheetCountStmt(store *GoogleSheetRowStore) *GoogleSheetCountStmt { + return &GoogleSheetCountStmt{ store: store, queryBuilder: newQueryBuilder(store.colsMapping.NameMap(), []string{rowIdxCol}), }