Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(examples): add p/jeronimoalbi/datastore package #3698

Open
wants to merge 35 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4710d08
chore: add `p/jeronimoalbi/datastore` package
jeronimoalbi Feb 5, 2025
18730d5
feat: add schema definition support
jeronimoalbi Feb 5, 2025
bcbb62a
chore: remove unused import
jeronimoalbi Feb 5, 2025
8aaf1d5
test: rename schema unit test
jeronimoalbi Feb 5, 2025
3a111cb
feat: add collection index wrapper
jeronimoalbi Feb 6, 2025
96211aa
feat: add storage and record support
jeronimoalbi Feb 6, 2025
57343aa
chore: remove unit test comments
jeronimoalbi Feb 6, 2025
305f7ad
feat: support storage size
jeronimoalbi Feb 6, 2025
dfdeef5
feat: add datastore support
jeronimoalbi Feb 6, 2025
365b125
chore: remove unused import
jeronimoalbi Feb 6, 2025
1c31379
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 6, 2025
cb6d763
chore: remove unused test import
jeronimoalbi Feb 6, 2025
95cd210
feat: add key field to record to support search by ID
jeronimoalbi Feb 6, 2025
e3c72f3
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 6, 2025
b12eb1c
doc: add documentation to all types
jeronimoalbi Feb 7, 2025
8a3544b
feat: add package documentation
jeronimoalbi Feb 7, 2025
69c3e8a
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 7, 2025
cd816de
feat: add iteration support to storage
jeronimoalbi Feb 7, 2025
f4afd08
test: add more storage unit tests
jeronimoalbi Feb 7, 2025
669a85e
test: add data store unit tests
jeronimoalbi Feb 7, 2025
20efd55
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 7, 2025
b217868
refactor: simplify storage API to get/query records
jeronimoalbi Feb 16, 2025
a16b21d
chore: remove unused import
jeronimoalbi Feb 16, 2025
ef2b877
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 17, 2025
cb89dce
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 21, 2025
72e1746
chore: rename `Storage.GetRecord` to `Storage.Get`
jeronimoalbi Feb 21, 2025
41709a1
test: add unit tests for `Recordset`
jeronimoalbi Feb 21, 2025
43cbdeb
chore: add warning paragraph to package documentation
jeronimoalbi Feb 22, 2025
6335242
feat: improve storage functionality and add query unit tests
jeronimoalbi Feb 22, 2025
0eab92a
docs: add query examples to package documentation
jeronimoalbi Feb 22, 2025
b3909fe
test: fix record iteration tests
jeronimoalbi Feb 22, 2025
6b3dc94
fix: correct code issues
jeronimoalbi Feb 22, 2025
430966e
chore: rename `indexes.gno` to `index.gno`
jeronimoalbi Feb 22, 2025
658a02f
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 25, 2025
6e41c7e
Merge branch 'master' into dev/jeronimoalbi/datastore
jeronimoalbi Feb 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/datastore.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package datastore

import (
"errors"

"gno.land/p/demo/avl"
)

// ErrStorageExists indicates that a storage exists with the same name.
var ErrStorageExists = errors.New("a storage with the same name exists")

// Datastore is a store that can contain multiple named storages.
// A storage is a collection of records.
//
// Example usage:
//
// // Create an empty storage to store user records
// var db Datastore
// storage := db.CreateStorage("users")
//
// // Get a storage that has been created before
// storage = db.GetStorage("profiles")
type Datastore struct {
storages avl.Tree // string(name) -> *Storage
}

// CreateStorage creates a new named storage within the data store.
func (ds *Datastore) CreateStorage(name string, options ...StorageOption) *Storage {
if ds.storages.Has(name) {
return nil
}

s := NewStorage(name, options...)
ds.storages.Set(name, &s)
return &s
}

// HasStorage checks if data store contains a storage with a specific name.
func (ds Datastore) HasStorage(name string) bool {
return ds.storages.Has(name)
}

// GetStorage returns a storage that has been created with a specific name.
// It returns nil when a storage with the specified name is not found.
func (ds Datastore) GetStorage(name string) *Storage {
if v, found := ds.storages.Get(name); found {
return v.(*Storage)
}
return nil
}
78 changes: 78 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/datastore_test.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package datastore

import (
"testing"

"gno.land/p/demo/uassert"
"gno.land/p/demo/urequire"
)

func TestDatastoreCreateStorage(t *testing.T) {
cases := []struct {
name string
storageName string
mustFail bool
setup func(*Datastore)
}{
{
name: "success",
storageName: "users",
},
{
name: "storage exists",
storageName: "users",
mustFail: true,
setup: func(db *Datastore) {
db.CreateStorage("users")
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
var db Datastore
if tc.setup != nil {
tc.setup(&db)
}

storage := db.CreateStorage(tc.storageName)

if tc.mustFail {
uassert.Equal(t, nil, storage)
return
}

urequire.NotEqual(t, nil, storage, "storage created")
uassert.Equal(t, tc.storageName, storage.Name())
uassert.True(t, db.HasStorage(tc.storageName))
})
}
}

func TestDatastoreHasStorage(t *testing.T) {
var (
db Datastore
name = "users"
)

uassert.False(t, db.HasStorage(name))

db.CreateStorage(name)
uassert.True(t, db.HasStorage(name))
}

func TestDatastoreGetStorage(t *testing.T) {
var (
db Datastore
name = "users"
)

storage := db.GetStorage(name)
uassert.Equal(t, nil, storage)

db.CreateStorage(name)

storage = db.GetStorage(name)
urequire.NotEqual(t, nil, storage, "storage found")
uassert.Equal(t, name, storage.Name())
}
98 changes: 98 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/doc.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Package datastore provides support to store multiple collections of records.
//
// It supports the definition of multiple storages, where each one is a collection
// of records. Records can have any number of user defined fields which are added
// dynamically when values are set on a record. These fields can also be renamed
// or removed.
//
// Storages have support for simple schemas that allows users to pre-define fields
// which can optionally have a default value also defined. Default values are
// assigned to new records on creation.
//
// User defined schemas can optionally be strict, which means that records from a
// storage using the schema can only assign values to the pre-defined set of fields.
// In which case, assigning a value to an unknown field would result on an error.
//
// Package also support the definition of custom record indexes. Indexes are used
// by storages to search and iterate records.
// The default index is the ID index but custom single and multi value indexes can
// be defined.
//
// WARNING: Using this package to store your realm data must be carefully considered.
// The fact that record fields are not strictly typed and can be renamed or removed
// could lead to issues if not careful when coding your realm(s). So it's recommended
// that you consider other alternatives first, like alternative patterns or solutions
// provided by the blockchain to deal with data, types and data migration for example.
//
// Example usage:
//
// var db datastore.Datastore
//
// // Define a unique case insensitive index for user emails
// emailIdx := datastore.NewIndex("email", func(r datastore.Record) string {
// return r.MustGet("email").(string)
// }).Unique().CaseInsensitive()
//
// // Create a new storage for user records
// storage := db.CreateStorage("users", datastore.WithIndex(emailIdx))
//
// // Add a user with a single "email" field
// user := storage.NewRecord()
// user.Set("email", "[email protected]")
//
// // Save to assing user ID and update indexes
// user.Save()
//
// // Find user by email using the custom index
// user, _ = storage.Get(emailIdx.Name(), "[email protected]")
//
// // Find user by ID
// user, _ = storage.GetByID(user.ID())
//
// // Search user's profile by email in another existing storage
// storage = db.GetStorage("profiles")
// email := user.MustGet("email").(string)
// profile, found := storage.Get("user", email)
// if !found {
// panic("profile not found")
// }
//
// // Delete the profile from the storage and update indexes
// storage.Delete(profile.ID())
//
// Example query usage:
//
// var db datastore.Datastore
//
// // Create a query with a custom offset and size
// storage := db.GetStorage("users")
// recordset, err := storage.Query(datastore.WithOffset(100), datastore.WithSize(50))
// if err != nil {
// panic(err)
// }
//
// // Get all query results
// var records []Record
// recordset.Iterate(func(r datastore.Record) bool {
// records = append(records, r)
// return false
// })
//
// Example query using a custom index usage:
//
// var db datastore.Datastore
//
// // Create a query to get records using a custom pre-defined index
// storage := db.GetStorage("posts")
// recordset, err := storage.Query(datastore.UseIndex("tags", "tagname"))
// if err != nil {
// panic(err)
// }
//
// // Get all query results
// var records []Record
// recordset.Iterate(func(r datastore.Record) bool {
// records = append(records, r)
// return false
// })
package datastore
1 change: 1 addition & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/jeronimoalbi/datastore
100 changes: 100 additions & 0 deletions examples/gno.land/p/jeronimoalbi/datastore/index.gno
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package datastore

import (
"gno.land/p/moul/collection"
)

// DefaultIndexOptions defines the default options for new indexes.
const DefaultIndexOptions = collection.DefaultIndex | collection.SparseIndex

type (
// IndexFn defines a type for single value indexing functions.
// This type of function extracts a single string value from
// a record that is then used to index it.
IndexFn func(Record) string

// IndexMultiValueFn defines a type for multi value indexing functions.
// This type of function extracts multiple string values from a
// record that are then used to index it.
IndexMultiValueFn func(Record) []string

// Index defines a type for custom user defined storage indexes.
// Storages are by default indexed by the auto geneated record ID
// but can additionally be indexed by other custom record fields.
Index struct {
name string
options collection.IndexOption
fn interface{}
}
)

// NewIndex creates a new single value index.
//
// Usage example:
//
// // Index a User record by email
// idx := NewIndex("email", func(r Record) string {
// return r.MustGet("email").(string)
// })
func NewIndex(name string, fn IndexFn) Index {
return Index{
name: name,
options: DefaultIndexOptions,
fn: func(v interface{}) string {
return fn(v.(Record))
},
}
}

// NewMultiValueIndex creates a new multi value index.
//
// Usage example:
//
// // Index a Post record by tag
// idx := NewMultiValueIndex("tag", func(r Record) []string {
// return r.MustGet("tags").([]string)
// })
func NewMultiValueIndex(name string, fn IndexMultiValueFn) Index {
return Index{
name: name,
options: DefaultIndexOptions,
fn: func(v interface{}) []string {
return fn(v.(Record))
},
}
}

// Name returns index's name.
func (idx Index) Name() string {
return idx.name
}

// Options returns current index options.
// These options define the index behavior regarding case sensitivity and uniquenes.
func (idx Index) Options() collection.IndexOption {
return idx.options
}

// Func returns the function that storage collections apply
// to each record to get the value to use for indexing it.
func (idx Index) Func() interface{} {
return idx.fn
}

// Unique returns a copy of the index that indexes record values as unique values.
// Returned index contains previous options plus the unique one.
func (idx Index) Unique() Index {
if idx.options&collection.UniqueIndex == 0 {
idx.options |= collection.UniqueIndex
}
return idx
}

// CaseInsensitive returns a copy of the index that indexes record values ignoring casing.
// Returned index contains previous options plus the case insensitivity one.
func (idx Index) CaseInsensitive() Index {
if idx.options&collection.CaseInsensitiveIndex == 0 {
idx.options |= collection.CaseInsensitiveIndex
}
return idx
}
Loading
Loading