This repository has been archived by the owner on Sep 9, 2024. It is now read-only.
forked from gocassa/gocassa
-
Notifications
You must be signed in to change notification settings - Fork 13
Add ability to have sentinel values for Clustering Columns #71
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a34c02f
Add the ability to propagate a Clustering Sentinel value when generat…
suhailpatel 9b6f070
Add the ability to check and get the non-sentinel value
suhailpatel 7a68f6d
Tweak our scanner to deal with sentinel values by ignoring them
suhailpatel 12e1192
Rename allowClusteringSentinel to clusteringSentinelsEnabled
suhailpatel File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,32 @@ | ||
package gocassa | ||
|
||
import ( | ||
"bytes" | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
"time" | ||
) | ||
|
||
var ( | ||
// ClusteringSentinel represents a placeholder value to be used for cases | ||
// where a value needs to be present (ie: a stand-in representing a | ||
// clustering key that is empty) | ||
ClusteringSentinel = "<gocassa.ClusteringSentinel>" | ||
) | ||
|
||
// SelectStatement represents a read (SELECT) query for some data in C* | ||
// It satisfies the Statement interface | ||
type SelectStatement struct { | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
fields []string // list of fields we want to select | ||
where []Relation // where filter clauses | ||
order []ClusteringOrderColumn // order by clauses | ||
limit int // limit count, 0 means no limit | ||
allowFiltering bool // whether we should allow filtering | ||
keys Keys // partition / clustering keys for table | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
fields []string // list of fields we want to select | ||
where []Relation // where filter clauses | ||
order []ClusteringOrderColumn // order by clauses | ||
limit int // limit count, 0 means no limit | ||
allowFiltering bool // whether we should allow filtering | ||
keys Keys // partition / clustering keys for table | ||
clusteringSentinelsEnabled bool // whether we should enable our clustering sentinel | ||
} | ||
|
||
// NewSelectStatement adds the ability to craft a new SelectStatement | ||
|
@@ -64,7 +74,7 @@ func (s SelectStatement) QueryAndValues() (string, []interface{}) { | |
fmt.Sprintf("FROM %s.%s", s.Keyspace(), s.Table()), | ||
} | ||
|
||
whereCQL, whereValues := generateWhereCQL(s.Relations()) | ||
whereCQL, whereValues := generateWhereCQL(s.Relations(), s.Keys(), s.clusteringSentinelsEnabled) | ||
if whereCQL != "" { | ||
query = append(query, "WHERE", whereCQL) | ||
values = append(values, whereValues...) | ||
|
@@ -161,14 +171,22 @@ func (s SelectStatement) Keys() Keys { | |
return s.keys | ||
} | ||
|
||
// WithClusteringSentinel allows you to specify whether the use of the | ||
// clustering sentinel value is enabled | ||
func (s SelectStatement) WithClusteringSentinel(enabled bool) SelectStatement { | ||
s.clusteringSentinelsEnabled = enabled | ||
return s | ||
} | ||
|
||
// InsertStatement represents an INSERT query to write some data in C* | ||
// It satisfies the Statement interface | ||
type InsertStatement struct { | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
fieldMap map[string]interface{} // fields to be inserted | ||
ttl time.Duration // ttl of the row | ||
keys Keys // partition / clustering keys for table | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
fieldMap map[string]interface{} // fields to be inserted | ||
ttl time.Duration // ttl of the row | ||
keys Keys // partition / clustering keys for table | ||
allowClusterSentinel bool // whether we should enable our clustering sentinel | ||
} | ||
|
||
// NewInsertStatement adds the ability to craft a new InsertStatement | ||
|
@@ -217,7 +235,11 @@ func (s InsertStatement) QueryAndValues() (string, []interface{}) { | |
for _, field := range sortedKeys(fieldMap) { | ||
fieldNames = append(fieldNames, strings.ToLower(field)) | ||
placeholders = append(placeholders, "?") | ||
values = append(values, fieldMap[field]) | ||
if isClusteringKeyField(field, s.keys) && s.allowClusterSentinel { | ||
values = append(values, clusteringFieldOrSentinel(fieldMap[field])) | ||
} else { | ||
values = append(values, fieldMap[field]) | ||
} | ||
} | ||
|
||
query = append(query, "("+strings.Join(fieldNames, ", ")+")") | ||
|
@@ -272,15 +294,23 @@ func (s InsertStatement) Keys() Keys { | |
return s.keys | ||
} | ||
|
||
// WithClusteringSentinel allows you to specify whether the use of the | ||
// clustering sentinel value is enabled | ||
func (s InsertStatement) WithClusteringSentinel(enabled bool) InsertStatement { | ||
s.allowClusterSentinel = enabled | ||
return s | ||
} | ||
|
||
// UpdateStatement represents an UPDATE query to update some data in C* | ||
// It satisfies the Statement interface | ||
type UpdateStatement struct { | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
fieldMap map[string]interface{} // fields to be updated | ||
where []Relation // where filter clauses | ||
ttl time.Duration // ttl of the row | ||
keys Keys // partition / clustering keys for table | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
fieldMap map[string]interface{} // fields to be updated | ||
where []Relation // where filter clauses | ||
ttl time.Duration // ttl of the row | ||
keys Keys // partition / clustering keys for table | ||
allowClusterSentinel bool // whether we should enable our clustering sentinel | ||
} | ||
|
||
// NewUpdateStatement adds the ability to craft a new UpdateStatement | ||
|
@@ -338,7 +368,7 @@ func (s UpdateStatement) QueryAndValues() (string, []interface{}) { | |
query = append(query, "SET", setCQL) | ||
values = append(values, setValues...) | ||
|
||
whereCQL, whereValues := generateWhereCQL(s.Relations()) | ||
whereCQL, whereValues := generateWhereCQL(s.Relations(), s.Keys(), s.allowClusterSentinel) | ||
if whereCQL != "" { | ||
query = append(query, "WHERE", whereCQL) | ||
values = append(values, whereValues...) | ||
|
@@ -392,13 +422,21 @@ func (s UpdateStatement) Keys() Keys { | |
return s.keys | ||
} | ||
|
||
// WithClusteringSentinel allows you to specify whether the use of the | ||
// clustering sentinel value is enabled | ||
func (s UpdateStatement) WithClusteringSentinel(enabled bool) UpdateStatement { | ||
s.allowClusterSentinel = enabled | ||
return s | ||
} | ||
|
||
// DeleteStatement represents a DELETE query to delete some data in C* | ||
// It satisfies the Statement interface | ||
type DeleteStatement struct { | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
where []Relation // where filter clauses | ||
keys Keys // partition / clustering keys for table | ||
keyspace string // name of the keyspace | ||
table string // name of the table | ||
where []Relation // where filter clauses | ||
keys Keys // partition / clustering keys for table | ||
allowClusterSentinel bool // whether we should enable our clustering sentinel | ||
} | ||
|
||
// NewDeleteStatement adds the ability to craft a new DeleteStatement | ||
|
@@ -439,7 +477,7 @@ func (s DeleteStatement) Values() []interface{} { | |
// QueryAndValues returns the CQL query and any bind values | ||
func (s DeleteStatement) QueryAndValues() (string, []interface{}) { | ||
query := fmt.Sprintf("DELETE FROM %s.%s", s.Keyspace(), s.Table()) | ||
whereCQL, whereValues := generateWhereCQL(s.Relations()) | ||
whereCQL, whereValues := generateWhereCQL(s.Relations(), s.Keys(), s.allowClusterSentinel) | ||
if whereCQL != "" { | ||
query += " WHERE " + whereCQL | ||
} | ||
|
@@ -467,6 +505,13 @@ func (s DeleteStatement) Keys() Keys { | |
return s.keys | ||
} | ||
|
||
// WithClusteringSentinel allows you to specify whether the use of the | ||
// clustering sentinel value is enabled | ||
suhailpatel marked this conversation as resolved.
Show resolved
Hide resolved
|
||
func (s DeleteStatement) WithClusteringSentinel(enabled bool) DeleteStatement { | ||
s.allowClusterSentinel = enabled | ||
return s | ||
} | ||
|
||
// cqlStatement represents a statement that executes raw CQL | ||
type cqlStatement struct { | ||
query string | ||
|
@@ -509,20 +554,23 @@ func generateUpdateSetCQL(fm map[string]interface{}) (string, []interface{}) { | |
// a WHERE clause. An expected output may be something like: | ||
// - "foo = ?", {1} | ||
// - "foo = ? AND bar IN ?", {1, {"a", "b", "c"}} | ||
func generateWhereCQL(rs []Relation) (string, []interface{}) { | ||
func generateWhereCQL(rs []Relation, keys Keys, clusteringSentinelsEnabled bool) (string, []interface{}) { | ||
clauses, values := make([]string, 0, len(rs)), make([]interface{}, 0, len(rs)) | ||
for _, relation := range rs { | ||
clause, bindValue := generateRelationCQL(relation) | ||
clause, bindValue := generateRelationCQL(relation, keys, clusteringSentinelsEnabled) | ||
clauses = append(clauses, clause) | ||
values = append(values, bindValue) | ||
} | ||
return strings.Join(clauses, " AND "), values | ||
} | ||
|
||
func generateRelationCQL(rel Relation) (string, interface{}) { | ||
func generateRelationCQL(rel Relation, keys Keys, clusteringSentinelsEnabled bool) (string, interface{}) { | ||
field := strings.ToLower(rel.Field()) | ||
switch rel.Comparator() { | ||
case CmpEquality: | ||
if isClusteringKeyField(rel.Field(), keys) && clusteringSentinelsEnabled { | ||
return field + " = ?", clusteringFieldOrSentinel(rel.Terms()[0]) | ||
} | ||
return field + " = ?", rel.Terms()[0] | ||
case CmpIn: | ||
return field + " IN ?", rel.Terms() | ||
|
@@ -552,3 +600,53 @@ func generateOrderByCQL(order []ClusteringOrderColumn) string { | |
} | ||
return strings.Join(out, ", ") | ||
} | ||
|
||
// isClusteringKeyField determines whether the relation makes up the | ||
// clustering key of the statement | ||
func isClusteringKeyField(field string, keys Keys) bool { | ||
for _, key := range keys.ClusteringColumns { | ||
if strings.ToLower(key) == strings.ToLower(field) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// clusteringFieldOrSentinel will check if we should substitute in our | ||
// sentinel value for empty clustering fields | ||
func clusteringFieldOrSentinel(term interface{}) interface{} { | ||
switch v := term.(type) { | ||
case string: | ||
if len(v) == 0 { | ||
return ClusteringSentinel | ||
} | ||
return v | ||
case []byte: | ||
if len(v) == 0 { | ||
return []byte(ClusteringSentinel) | ||
} | ||
return v | ||
default: | ||
return term | ||
} | ||
} | ||
|
||
// isClusteringSentinelValue returns a boolean on whether the value passed in | ||
// is the clustering sentinel value and what the non-sentinel value is | ||
func isClusteringSentinelValue(term interface{}) (bool, interface{}) { | ||
val := reflect.ValueOf(term) | ||
switch { | ||
case val.Kind() == reflect.String: | ||
if val.String() == ClusteringSentinel { | ||
return true, reflect.New(val.Type()).Elem().Interface() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there a scenario where There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially had that but we need to consider if you have a type alias for which that doesn't work |
||
} | ||
return false, term | ||
case val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8: | ||
if bytes.Equal(val.Bytes(), []byte(ClusteringSentinel)) { | ||
return true, reflect.MakeSlice(val.Type(), 0, 0).Interface() | ||
} | ||
return false, term | ||
default: | ||
return false, term | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to what degree (if any) should we be thinking about potential performance regressions by adding an extra step of reflection/value comparison for every field pointer now, do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can benchmark this using the existing benchmarks
Before:
After:
I think we can probably optimise this codepath a bit more (the before is quite optimised since #68)