552 lines
12 KiB
Go
552 lines
12 KiB
Go
package nigiri
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"reflect"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type Validatable interface {
|
|
Validate() error
|
|
}
|
|
|
|
type IndexBuilder[T any] func(allItems map[int]*T) any
|
|
|
|
type BaseStore[T any] struct {
|
|
items map[int]*T
|
|
maxID int
|
|
mu sync.RWMutex
|
|
itemType reflect.Type
|
|
indices map[string]any
|
|
indexBuilders map[string]IndexBuilder[T]
|
|
schema *SchemaInfo
|
|
uniqueIndices map[string]map[any]int
|
|
validator any
|
|
}
|
|
|
|
func NewBaseStore[T any]() *BaseStore[T] {
|
|
var zero T
|
|
schema := ParseSchema[T]()
|
|
|
|
store := &BaseStore[T]{
|
|
items: make(map[int]*T),
|
|
maxID: 0,
|
|
itemType: reflect.TypeOf(zero),
|
|
indices: make(map[string]any),
|
|
indexBuilders: make(map[string]IndexBuilder[T]),
|
|
schema: schema,
|
|
uniqueIndices: make(map[string]map[any]int),
|
|
}
|
|
|
|
for fieldName, constraints := range schema.Constraints {
|
|
for _, constraint := range constraints {
|
|
if constraint.Type == ConstraintUnique {
|
|
store.uniqueIndices[fieldName] = make(map[any]int)
|
|
}
|
|
}
|
|
}
|
|
|
|
store.registerSchemaIndices()
|
|
return store
|
|
}
|
|
|
|
func (bs *BaseStore[T]) registerSchemaIndices() {
|
|
for fieldName, indexName := range bs.schema.Indices {
|
|
bs.RegisterIndex(indexName, BuildFieldLookupIndex[T](fieldName))
|
|
}
|
|
}
|
|
|
|
func (bs *BaseStore[T]) SetValidator(validator any) {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
bs.validator = validator
|
|
}
|
|
|
|
func (bs *BaseStore[T]) EntityExists(id int) bool {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
_, exists := bs.items[id]
|
|
return exists
|
|
}
|
|
|
|
func (bs *BaseStore[T]) ValidateConstraints(id int, item *T) error {
|
|
itemValue := reflect.ValueOf(item).Elem()
|
|
|
|
for fieldName, constraints := range bs.schema.Constraints {
|
|
fieldValue := itemValue.FieldByName(fieldName)
|
|
if !fieldValue.IsValid() {
|
|
continue
|
|
}
|
|
|
|
for _, constraint := range constraints {
|
|
switch constraint.Type {
|
|
case ConstraintRequired:
|
|
if isZeroValue(fieldValue) {
|
|
return fmt.Errorf("field %s is required", fieldName)
|
|
}
|
|
case ConstraintUnique:
|
|
value := fieldValue.Interface()
|
|
if existingID, exists := bs.uniqueIndices[fieldName][value]; exists && existingID != id {
|
|
return fmt.Errorf("field %s value %v already exists", fieldName, value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (bs *BaseStore[T]) validateRelationships(item *T) error {
|
|
if bs.validator == nil {
|
|
return nil
|
|
}
|
|
|
|
validator, ok := bs.validator.(interface {
|
|
EntityExists(entityName string, id int) bool
|
|
})
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
itemValue := reflect.ValueOf(item).Elem()
|
|
for fieldName, constraint := range bs.schema.Relationships {
|
|
fieldValue := itemValue.FieldByName(fieldName)
|
|
if !fieldValue.IsValid() {
|
|
continue
|
|
}
|
|
|
|
switch constraint.Relationship {
|
|
case RelationshipManyToOne:
|
|
if !fieldValue.IsNil() {
|
|
targetItem := fieldValue.Elem()
|
|
idField := targetItem.FieldByName("ID")
|
|
if idField.IsValid() {
|
|
targetID := int(idField.Int())
|
|
if !validator.EntityExists(constraint.Target, targetID) {
|
|
return fmt.Errorf("foreign key violation: %s references non-existent %s.%d",
|
|
fieldName, constraint.Target, targetID)
|
|
}
|
|
}
|
|
}
|
|
|
|
case RelationshipOneToMany:
|
|
if !fieldValue.IsNil() {
|
|
for i := 0; i < fieldValue.Len(); i++ {
|
|
elem := fieldValue.Index(i)
|
|
if !elem.IsNil() {
|
|
targetItem := elem.Elem()
|
|
idField := targetItem.FieldByName("ID")
|
|
if idField.IsValid() {
|
|
targetID := int(idField.Int())
|
|
if !validator.EntityExists(constraint.Target, targetID) {
|
|
return fmt.Errorf("foreign key violation: %s[%d] references non-existent %s.%d",
|
|
fieldName, i, constraint.Target, targetID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func isZeroValue(v reflect.Value) bool {
|
|
return v.Interface() == reflect.Zero(v.Type()).Interface()
|
|
}
|
|
|
|
func (bs *BaseStore[T]) updateUniqueIndices(id int, item *T, add bool) {
|
|
itemValue := reflect.ValueOf(item).Elem()
|
|
|
|
for fieldName := range bs.uniqueIndices {
|
|
fieldValue := itemValue.FieldByName(fieldName)
|
|
if !fieldValue.IsValid() {
|
|
continue
|
|
}
|
|
|
|
value := fieldValue.Interface()
|
|
if add {
|
|
bs.uniqueIndices[fieldName][value] = id
|
|
} else {
|
|
delete(bs.uniqueIndices[fieldName], value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
bs.indexBuilders[name] = builder
|
|
}
|
|
|
|
func (bs *BaseStore[T]) GetIndex(name string) (any, bool) {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
index, exists := bs.indices[name]
|
|
return index, exists
|
|
}
|
|
|
|
func (bs *BaseStore[T]) RebuildIndices() {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
bs.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
func (bs *BaseStore[T]) rebuildIndicesUnsafe() {
|
|
allItems := make(map[int]*T, len(bs.items))
|
|
maps.Copy(allItems, bs.items)
|
|
|
|
for name, builder := range bs.indexBuilders {
|
|
bs.indices[name] = builder(allItems)
|
|
}
|
|
}
|
|
|
|
// Main CRUD operations with validation
|
|
func (bs *BaseStore[T]) Add(id int, item *T) error {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
|
|
if err := bs.ValidateConstraints(id, item); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := bs.validateRelationships(item); err != nil {
|
|
return err
|
|
}
|
|
|
|
if validatable, ok := any(item).(Validatable); ok {
|
|
if err := validatable.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
bs.updateUniqueIndices(id, item, true)
|
|
bs.items[id] = item
|
|
if id > bs.maxID {
|
|
bs.maxID = id
|
|
}
|
|
bs.rebuildIndicesUnsafe()
|
|
return nil
|
|
}
|
|
|
|
func (bs *BaseStore[T]) Update(id int, item *T) error {
|
|
return bs.Add(id, item)
|
|
}
|
|
|
|
func (bs *BaseStore[T]) Remove(id int) {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
|
|
if item, exists := bs.items[id]; exists {
|
|
bs.updateUniqueIndices(id, item, false)
|
|
}
|
|
delete(bs.items, id)
|
|
bs.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
func (bs *BaseStore[T]) Create(item *T) (int, error) {
|
|
id := bs.GetNextID()
|
|
err := bs.Add(id, item)
|
|
return id, err
|
|
}
|
|
|
|
// Unsafe operations for performance
|
|
func (bs *BaseStore[T]) AddUnsafe(id int, item *T) {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
bs.items[id] = item
|
|
if id > bs.maxID {
|
|
bs.maxID = id
|
|
}
|
|
}
|
|
|
|
func (bs *BaseStore[T]) RemoveUnsafe(id int) {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
delete(bs.items, id)
|
|
}
|
|
|
|
func (bs *BaseStore[T]) Find(id int) (*T, bool) {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
item, exists := bs.items[id]
|
|
return item, exists
|
|
}
|
|
|
|
func (bs *BaseStore[T]) GetByID(id int) (*T, bool) {
|
|
return bs.Find(id)
|
|
}
|
|
|
|
func (bs *BaseStore[T]) GetAll() map[int]*T {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
result := make(map[int]*T, len(bs.items))
|
|
maps.Copy(result, bs.items)
|
|
return result
|
|
}
|
|
|
|
func (bs *BaseStore[T]) AllSorted(indexName string) []*T {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
|
|
if index, exists := bs.indices[indexName]; exists {
|
|
if sortedIDs, ok := index.([]int); ok {
|
|
result := make([]*T, 0, len(sortedIDs))
|
|
for _, id := range sortedIDs {
|
|
if item, exists := bs.items[id]; exists {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
|
|
ids := make([]int, 0, len(bs.items))
|
|
for id := range bs.items {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Ints(ids)
|
|
|
|
result := make([]*T, 0, len(ids))
|
|
for _, id := range ids {
|
|
result = append(result, bs.items[id])
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (bs *BaseStore[T]) LookupByIndex(indexName, key string) (*T, bool) {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
|
|
if index, exists := bs.indices[indexName]; exists {
|
|
if lookupMap, ok := index.(map[string]int); ok {
|
|
if id, found := lookupMap[key]; found {
|
|
if item, exists := bs.items[id]; exists {
|
|
return item, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil, false
|
|
}
|
|
|
|
func (bs *BaseStore[T]) GroupByIndex(indexName string, key any) []*T {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
|
|
if index, exists := bs.indices[indexName]; exists {
|
|
switch groupMap := index.(type) {
|
|
case map[int][]int:
|
|
if intKey, ok := key.(int); ok {
|
|
if ids, found := groupMap[intKey]; found {
|
|
result := make([]*T, 0, len(ids))
|
|
for _, id := range ids {
|
|
if item, exists := bs.items[id]; exists {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
case map[string][]int:
|
|
if strKey, ok := key.(string); ok {
|
|
if ids, found := groupMap[strKey]; found {
|
|
result := make([]*T, 0, len(ids))
|
|
for _, id := range ids {
|
|
if item, exists := bs.items[id]; exists {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return []*T{}
|
|
}
|
|
|
|
func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool) []*T {
|
|
bs.mu.RLock()
|
|
defer bs.mu.RUnlock()
|
|
|
|
var sourceIDs []int
|
|
|
|
if index, exists := bs.indices[indexName]; exists {
|
|
if sortedIDs, ok := index.([]int); ok {
|
|
sourceIDs = sortedIDs
|
|
}
|
|
}
|
|
|
|
if sourceIDs == nil {
|
|
for id := range bs.items {
|
|
sourceIDs = append(sourceIDs, id)
|
|
}
|
|
sort.Ints(sourceIDs)
|
|
}
|
|
|
|
var result []*T
|
|
for _, id := range sourceIDs {
|
|
if item, exists := bs.items[id]; exists && filterFunc(item) {
|
|
result = append(result, item)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func (bs *BaseStore[T]) GetNextID() int {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
bs.maxID++
|
|
return bs.maxID
|
|
}
|
|
|
|
func (bs *BaseStore[T]) Clear() {
|
|
bs.mu.Lock()
|
|
defer bs.mu.Unlock()
|
|
bs.items = make(map[int]*T)
|
|
bs.maxID = 0
|
|
|
|
for fieldName := range bs.uniqueIndices {
|
|
bs.uniqueIndices[fieldName] = make(map[any]int)
|
|
}
|
|
|
|
bs.rebuildIndicesUnsafe()
|
|
}
|
|
|
|
// Index Builder Functions
|
|
func BuildFieldLookupIndex[T any](fieldName string) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[string]int)
|
|
for id, item := range allItems {
|
|
itemValue := reflect.ValueOf(item).Elem()
|
|
fieldValue := itemValue.FieldByName(fieldName)
|
|
if !fieldValue.IsValid() {
|
|
continue
|
|
}
|
|
|
|
key := fmt.Sprintf("%v", fieldValue.Interface())
|
|
index[key] = id
|
|
}
|
|
return index
|
|
}
|
|
}
|
|
|
|
func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[string]int)
|
|
for id, item := range allItems {
|
|
key := keyFunc(item)
|
|
index[key] = id
|
|
}
|
|
return index
|
|
}
|
|
}
|
|
|
|
func BuildCaseInsensitiveLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[string]int)
|
|
for id, item := range allItems {
|
|
key := strings.ToLower(keyFunc(item))
|
|
index[key] = id
|
|
}
|
|
return index
|
|
}
|
|
}
|
|
|
|
func BuildIntGroupIndex[T any](keyFunc func(*T) int) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[int][]int)
|
|
for id, item := range allItems {
|
|
key := keyFunc(item)
|
|
index[key] = append(index[key], id)
|
|
}
|
|
|
|
for key := range index {
|
|
sort.Ints(index[key])
|
|
}
|
|
|
|
return index
|
|
}
|
|
}
|
|
|
|
func BuildStringGroupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[string][]int)
|
|
for id, item := range allItems {
|
|
key := keyFunc(item)
|
|
index[key] = append(index[key], id)
|
|
}
|
|
|
|
for key := range index {
|
|
sort.Ints(index[key])
|
|
}
|
|
|
|
return index
|
|
}
|
|
}
|
|
|
|
func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
ids := make([]int, 0, len(allItems))
|
|
for id := range allItems {
|
|
ids = append(ids, id)
|
|
}
|
|
|
|
sort.Slice(ids, func(i, j int) bool {
|
|
return sortFunc(allItems[ids[i]], allItems[ids[j]])
|
|
})
|
|
|
|
return ids
|
|
}
|
|
}
|
|
|
|
func BuildFilteredIntGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) int) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[int][]int)
|
|
for id, item := range allItems {
|
|
if filterFunc(item) {
|
|
key := keyFunc(item)
|
|
index[key] = append(index[key], id)
|
|
}
|
|
}
|
|
|
|
for key := range index {
|
|
sort.Ints(index[key])
|
|
}
|
|
|
|
return index
|
|
}
|
|
}
|
|
|
|
func BuildFilteredStringGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) string) IndexBuilder[T] {
|
|
return func(allItems map[int]*T) any {
|
|
index := make(map[string][]int)
|
|
for id, item := range allItems {
|
|
if filterFunc(item) {
|
|
key := keyFunc(item)
|
|
index[key] = append(index[key], id)
|
|
}
|
|
}
|
|
|
|
for key := range index {
|
|
sort.Ints(index[key])
|
|
}
|
|
|
|
return index
|
|
}
|
|
}
|
|
|
|
func NewSingleton[S any](initFunc func() *S) func() *S {
|
|
var store *S
|
|
var once sync.Once
|
|
|
|
return func() *S {
|
|
once.Do(func() {
|
|
store = initFunc()
|
|
})
|
|
return store
|
|
}
|
|
}
|