-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
43e3103
commit 072e40d
Showing
8 changed files
with
413 additions
and
98 deletions.
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
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 |
---|---|---|
@@ -0,0 +1,172 @@ | ||
package beeorm | ||
|
||
import ( | ||
"fmt" | ||
"hash/fnv" | ||
"reflect" | ||
"slices" | ||
"strconv" | ||
|
||
jsoniter "github.com/json-iterator/go" | ||
) | ||
|
||
type indexDefinition struct { | ||
Cached bool | ||
Columns []string | ||
Where string | ||
} | ||
|
||
func (d indexDefinition) CreteWhere(hasNil bool, attributes []any) Where { | ||
if !hasNil { | ||
return NewWhere(d.Where, attributes...) | ||
} | ||
query := "" | ||
newAttributes := make([]any, 0) | ||
for i, column := range d.Columns { | ||
if i > 0 { | ||
query += " AND " | ||
} | ||
if attributes[i] == nil { | ||
query += "`" + column + "` IS NULL" | ||
} else { | ||
query += "`" + column + "`=?" | ||
newAttributes = append(newAttributes, attributes[i]) | ||
} | ||
} | ||
return NewWhere(query, newAttributes) | ||
} | ||
|
||
func GetByIndex[E any](orm ORM, indexName string, attributes ...any) EntityIterator[E] { | ||
var e E | ||
schema := orm.(*ormImplementation).engine.registry.entitySchemas[reflect.TypeOf(e)] | ||
if schema == nil { | ||
panic(fmt.Errorf("entity '%T' is not registered", e)) | ||
} | ||
def, has := schema.indexes[indexName] | ||
if !has { | ||
panic(fmt.Errorf("unknow index name `%s`", indexName)) | ||
} | ||
if len(attributes) != len(def.Columns) { | ||
panic(fmt.Errorf("invalid attributes length, %d is required, %d provided", len(def.Columns), len(attributes))) | ||
} | ||
hasNil := false | ||
for i, attribute := range attributes { | ||
if attribute == nil { | ||
hasNil = true | ||
continue | ||
} | ||
setter := schema.fieldBindSetters[def.Columns[i]] | ||
bind, err := setter(attribute) | ||
if err != nil { | ||
panic(err) | ||
} | ||
attributes[i] = bind | ||
} | ||
if !def.Cached { | ||
return Search[E](orm, def.CreteWhere(hasNil, attributes), nil) | ||
} | ||
return getCachedByColumns[E](orm, indexName, def, schema, attributes, hasNil) | ||
} | ||
|
||
func getCachedByColumns[E any](orm ORM, indexName string, index indexDefinition, schema *entitySchema, attributes []any, hasNil bool) EntityIterator[E] { | ||
attributesHash, err := jsoniter.ConfigFastest.Marshal(attributes) | ||
hash := fnv.New64() | ||
_, err = hash.Write(attributesHash) | ||
checkError(err) | ||
bindID := hash.Sum64() | ||
if schema.hasLocalCache { | ||
fromCache, hasInCache := schema.localCache.getList(orm, indexName, bindID) | ||
if hasInCache { | ||
if fromCache == cacheNilValue { | ||
return &emptyResultsIterator[E]{} | ||
} | ||
|
||
if schema.hasLocalCache { | ||
results := &entityIterator[E]{index: -1} | ||
results.rows = fromCache.([]*E) | ||
return results | ||
} | ||
return GetByIDs[E](orm, fromCache.([]uint64)...) | ||
} | ||
} | ||
rc := orm.Engine().Redis(schema.getForcedRedisCode()) | ||
redisSetKey := schema.cacheKey + ":" + indexName + strconv.FormatUint(bindID, 10) | ||
fromRedis := rc.SMembers(orm, redisSetKey) | ||
if len(fromRedis) > 0 { | ||
ids := make([]uint64, len(fromRedis)) | ||
k := 0 | ||
hasValidValue := false | ||
for _, value := range fromRedis { | ||
if value == redisValidSetValue { | ||
hasValidValue = true | ||
continue | ||
} else if value == cacheNilValue { | ||
continue | ||
} | ||
ids[k], _ = strconv.ParseUint(value, 10, 64) | ||
k++ | ||
} | ||
if hasValidValue { | ||
if k == 0 { | ||
if schema.hasLocalCache { | ||
schema.localCache.setList(orm, indexName, bindID, cacheNilValue) | ||
} | ||
return &emptyResultsIterator[E]{} | ||
} | ||
ids = ids[0:k] | ||
slices.Sort(ids) | ||
values := GetByIDs[E](orm, ids...) | ||
if schema.hasLocalCache { | ||
if values.Len() == 0 { | ||
schema.localCache.setList(orm, indexName, bindID, cacheNilValue) | ||
} else { | ||
if schema.hasLocalCache { | ||
schema.localCache.setList(orm, indexName, bindID, values.All()) | ||
} else { | ||
schema.localCache.setList(orm, indexName, bindID, ids) | ||
} | ||
} | ||
} | ||
return values | ||
} | ||
} | ||
if schema.hasLocalCache { | ||
ids := SearchIDs[E](orm, index.CreteWhere(hasNil, attributes), nil) | ||
if len(ids) == 0 { | ||
schema.localCache.setList(orm, indexName, bindID, cacheNilValue) | ||
rc.SAdd(orm, redisSetKey, cacheNilValue) | ||
return &emptyResultsIterator[E]{} | ||
} | ||
idsForRedis := make([]any, len(ids)) | ||
for i, value := range ids { | ||
idsForRedis[i] = strconv.FormatUint(value, 10) | ||
} | ||
p := orm.RedisPipeLine(rc.GetCode()) | ||
p.Del(redisSetKey) | ||
p.SAdd(redisSetKey, redisValidSetValue) | ||
p.SAdd(redisSetKey, idsForRedis...) | ||
p.Exec(orm) | ||
values := GetByIDs[E](orm, ids...) | ||
if schema.hasLocalCache { | ||
schema.localCache.setList(orm, indexName, bindID, values.All()) | ||
} else { | ||
schema.localCache.setList(orm, indexName, bindID, ids) | ||
} | ||
return values | ||
} | ||
values := Search[E](orm, index.CreteWhere(hasNil, attributes), nil) | ||
if values.Len() == 0 { | ||
rc.SAdd(orm, redisSetKey, redisValidSetValue, cacheNilValue) | ||
} else { | ||
idsForRedis := make([]any, values.Len()+1) | ||
idsForRedis[0] = redisValidSetValue | ||
i := 0 | ||
for values.Next() { | ||
idsForRedis[i+1] = strconv.FormatUint(reflect.ValueOf(values.Entity()).Elem().Field(0).Uint(), 10) | ||
i++ | ||
} | ||
values.Reset() | ||
rc.SAdd(orm, redisSetKey, idsForRedis...) | ||
} | ||
return values | ||
} |
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 |
---|---|---|
@@ -0,0 +1,107 @@ | ||
package beeorm | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
type getByIndexEntity struct { | ||
ID uint64 `orm:"localCache;redisCache"` | ||
Name string `orm:"index=Name"` | ||
Age uint32 `orm:"index=Age;cached"` | ||
Born *time.Time `orm:"index=Age:2"` | ||
} | ||
|
||
func TestGetByIndexNoCache(t *testing.T) { | ||
testGetByIndex(t, false, false) | ||
} | ||
|
||
func TestGetByIndexLocalCache(t *testing.T) { | ||
testGetByIndex(t, true, false) | ||
} | ||
|
||
func TestGetByIndexRedisCache(t *testing.T) { | ||
testGetByIndex(t, false, true) | ||
} | ||
|
||
func TestGetByIndexLocalRedisCache(t *testing.T) { | ||
testGetByIndex(t, true, true) | ||
} | ||
|
||
func testGetByIndex(t *testing.T, local, redis bool) { | ||
var entity *getByIndexEntity | ||
orm := PrepareTables(t, NewRegistry(), entity) | ||
schema := GetEntitySchema[getByIndexEntity](orm) | ||
schema.DisableCache(!local, !redis) | ||
|
||
loggerDB := &MockLogHandler{} | ||
orm.RegisterQueryLogger(loggerDB, true, false, false) | ||
|
||
now := time.Now().UTC() | ||
nextWeek := now.Add(time.Hour * 24 * 7) | ||
// getting missing rows | ||
rows := GetByIndex[getByIndexEntity](orm, "Age", 23, now) | ||
assert.Equal(t, 0, rows.Len()) | ||
loggerDB.Clear() | ||
rows = GetByIndex[getByIndexEntity](orm, "Age", 23, now) | ||
assert.Equal(t, 0, rows.Len()) | ||
assert.Len(t, loggerDB.Logs, 0) | ||
|
||
var entities []*getByIndexEntity | ||
for i := 0; i < 10; i++ { | ||
entity = NewEntity[getByIndexEntity](orm) | ||
entity.Age = 10 | ||
if i >= 5 { | ||
entity.Name = "Test Name" | ||
entity.Age = 18 | ||
entity.Born = &now | ||
} | ||
if i >= 8 { | ||
entity.Name = "Test Name 2" | ||
entity.Age = 40 | ||
entity.Born = &nextWeek | ||
} | ||
entities = append(entities, entity) | ||
} | ||
assert.NoError(t, orm.Flush()) | ||
|
||
rows = GetByIndex[getByIndexEntity](orm, "Name", nil) | ||
assert.Equal(t, 5, rows.Len()) | ||
rows.Next() | ||
e := rows.Entity() | ||
assert.Equal(t, entities[0].ID, e.ID) | ||
|
||
rows = GetByIndex[getByIndexEntity](orm, "Name", "Test name") | ||
assert.Equal(t, 3, rows.Len()) | ||
rows.Next() | ||
e = rows.Entity() | ||
assert.Equal(t, entities[5].ID, e.ID) | ||
|
||
rows = GetByIndex[getByIndexEntity](orm, "Age", 10, nil) | ||
assert.Equal(t, 5, rows.Len()) | ||
rows.Next() | ||
e = rows.Entity() | ||
assert.Equal(t, entities[0].ID, e.ID) | ||
loggerDB.Clear() | ||
rows = GetByIndex[getByIndexEntity](orm, "Age", 10, nil) | ||
assert.Equal(t, 5, rows.Len()) | ||
if local || redis { | ||
assert.Len(t, loggerDB.Logs, 0) | ||
} | ||
loggerDB.Clear() | ||
|
||
rows = GetByIndex[getByIndexEntity](orm, "Age", 18, now) | ||
assert.Equal(t, 3, rows.Len()) | ||
rows.Next() | ||
e = rows.Entity() | ||
loggerDB.Clear() | ||
assert.Equal(t, entities[5].ID, e.ID) | ||
rows = GetByIndex[getByIndexEntity](orm, "Age", 18, now) | ||
assert.Equal(t, 3, rows.Len()) | ||
if local || redis { | ||
assert.Len(t, loggerDB.Logs, 0) | ||
} | ||
loggerDB.Clear() | ||
} |
Oops, something went wrong.