istate

package module
v1.0.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 10, 2020 License: Apache-2.0 Imports: 12 Imported by: 0

README

iState GoDoc Go Report Card

iState is a state management package for Hyperledger fabric chaincode. It can be used to perform high performance rich queries on leveldb.

Features

  • Rich Query in levelDB.

  • On an average case, query is ~7 times faster than CouchDB's Rich Query with Index enabled.

  • In-memory caching using ARC (Adaptive Replacement Cache) algorithm.

  • Cache consistency is maintained. Data returned by query is always consistent.

  • Easy to use.

Performance Chart

Follow the discussion on internal working mechanisms in Hyperledger Lists

Full Chart Link

Legends

Chart

Releases

Installation

Using govendor
  • Initialize vendor folder in the chaincode directory using govendor init

  • Get the dependent packages using the following commands:

    1. govendor fetch github.com/motoreq/istate
    2. govendor fetch github.com/bluele/gcache
    3. govendor fetch github.com/emirpasic/gods
    4. govendor fetch github.com/prasanths96/gostack
Mannual method (No tools needed)
  • Clone this repository in a preferred location using git clone https://github.com/motoreq/istate.git.

  • Copy the .go files in this repo and paste inside chaincode/vendor/github.com/motoreq/istate/ (Note: No need to copy files inside folders.)

  • Copy the vendor folder in this repo and merge it with chaincode/vendor

That's all, iState is ready to be imported in the chaincode.

Example

Adding tags to struct

The following tags must be added only to the struct types which is getting stored in state db.

  • primary tag is used to denote the primary key / id in the struct. This field must contain universal unique value and is handled externally.
  • istate tag is used to denote the fields that is allowed to be queried. It is recommended to add this tag to all fields.
  • Value of istate tag must be universally unique with other structs in the chaincode. Recommended format: <structname>_<fieldname>
type TestStruct struct {
	ID      string  `json:"id" istate:"TestStruct_id" primary:"true"`
	AString string  `json:"aString" istate:"TestStruct_aString"`
	AnInt   int64   `json:"anInt" istate:"TestStruct_anInt"`
}
Init
  • Init involves getting a new iState Interface for your struct type using istate.NewiState() function. It takes the empty struct value and istate.Options as input params. CacheSize in istate.Options indicate the maximum number of records that can be available in memory.
  • The returned istate.Interface can be stored as a field in main SmartContract struct.
  • This interface can then be used to interact with iState package for performing CRUD or Query operation with this struct type (TestStruct) further along.
package main 

import (
  "github.com/motoreq/istate"
  "github.com/hyperledger/fabric/core/chaincode/shim"
  pb "github.com/hyperledger/fabric/protos/peer"
)

type TestSmartContract struct {
	TestStructiState istate.Interface
}

// Init initializes chaincode.
func (sc *TestSmartContract) Init(stub shim.ChaincodeStubInterface) pb.Response {
	err := sc.init()
	if err != nil {
		return shim.Error(err.Error())
	}
	return shim.Success(nil)
}

func (sc *TestSmartContract) init() error {
	iStateOpt := istate.Options{
		CacheSize: 1000000,
	}
	
	TestStructiState, err := istate.NewiState(TestStruct{}, iStateOpt)
	if err != nil {
		return err
	}
	
	sc.TestStructiState = TestStructiState
	
	return nil
}
Create State
func (sc *TestSmartContract) CreateState(stub shim.ChaincodeStubInterface) pb.Response {
	var err error
	
	testStruct := TestStruct{
		ID:      "unique_id_1",
		AString: "John Doe",
		AnInt:   100,
	}	
	
	err = sc.TestStructiState.CreateState(stub, testStruct)
	if err != nil {
		return shim.Error(err.Error())
	}
	
	output := fmt.Sprintf("Successfully saved: %v", testStruct)
	return shim.Success([]byte(output))
}
Read State
func (sc *TestSmartContract) ReadState(stub shim.ChaincodeStubInterface) pb.Response {
	var err error

	stateInterface, err := sc.TestStructiState.ReadState(stub, "unique_id_1")
	if err != nil {
		return shim.Error(err.Error())
	}
	
	actualState := stateInterface.(TestStruct)
	fmt.Println("AString: ", actualState.AString)
	
	return shim.Success(nil)

}
Update State
func (sc *TestSmartContract) UpdateState(stub shim.ChaincodeStubInterface) pb.Response {
	var err error
	
	testStruct := TestStruct{
		ID:      "unique_id_1",
		AString: "John Doe Jr.",
		AnInt:   200,
	}	
	
	err = sc.TestStructiState.UpdateState(stub, testStruct)
	if err != nil {
		return shim.Error(err.Error())
	}
	
	output := fmt.Sprintf("Successfully updated: %v", testStruct)
	return shim.Success([]byte(output))
}
Delete State
func (sc *TestSmartContract) DeleteState(stub shim.ChaincodeStubInterface) pb.Response {
	var err error
	
	err = sc.TestStructiState.DeleteState(stub, "unique_id_1")
	if err != nil {
		return shim.Error(err.Error())
	}
	
	output := fmt.Sprintf("Successfully deleted: %v", "unique_id_1")
	return shim.Success([]byte(output))
}
Query
func (sc *TestSmartContract) Query(stub shim.ChaincodeStubInterface) pb.Response {
	var err error
	
	queryString := `[{"anInt": "eq 200"}]`
	
	isInvoke := false	
	
	// Important! isInvoke must be set to true, when Query is used for a invoke/write transaction.
	// It is not recommended to use Query during invoke transaction, when query results returned is 
	// greater than 100 in number. 
	// The more the query results, the slower it will be to fetch when using isInvoke=true.
	
	stateSliceInterface, err := sc.TestStructiState.Query(stub, queryString, isInvoke)
	if err != nil {
		return shim.Error(err.Error())
	}
	
	result := stateSliceInterface.([]TestStruct)
	for i := 0; i < len(result); i ++ {
		fmt.Println("ID: ", result[i].ID, "AString:", result[i].AString)
	}
	
	return shim.Success(nil)

}

Query Syntax

Reference Struct:

struct TestStruct type {
	ID      	string   		`json:"id" istate:"TestStruct_id" primary:"true"`
	AString 	string   		`json:"aString" istate:"TestStruct_aString"`
	AnInt   	int64    		`json:"anInt" istate:"TestStruct_anInt"`
	ASlice  	[]string 		`json:"aSlice" istate:"TestStruct_aSlice"` 
	AMap    	map[string]string 	`json:"aMap" istate:"TestStruct_aMap"`
	ANestedStruct	SomeStruct		`json:"aNestedStruct" istate:"TestStruct_aNestedStruct"`
}

struct SomeStruct type {
	NestedString 	string `json:"nestedString"`
}

Note: json tag must be present in all fields (including nested structs), istate tag is not necessary to be included in nested structs. iState will index nested structs and make it available for query automatically, if the parent struct field has istate tag.

  • The base syntax of query is similar to that of a JSON array of objects [{"fieldname": "<operation> <value>"}]. Note: The fieldname in query should match the value of json tag in respective field and not the go struct's fieldname

  • For nested structs, dot notation can be used. Eg: [{"aNestedStruct.nestedString": "eq awesome string"}]

  • To search for elements in an array / slice, .* notation can be used. Eg: [{"aSlice.*": "eq one of awesome strings"}] Note: * can be used to fill a depth in a nested fieldname, if that depth represents a collection type such as array/slice/map

  • To search for a map's key, Eg: [{"aMap": "eq mapkey"}]

  • To search for map's value when key is not known, Eg: [{"aMap.*": "eq map value"}]

  • To search for a map's value when key is known, Eg: [{"aMap.mapkey": "eq map value"}]

  • To perform complex queries on a single field, cmplx syntax can be used. Eg: [{"anInt": "cmplx and(gt 100, lt 500)"}]. Refer Complex Queries section below for more info.

  • To perform or operation between two query sets, simply append the query array with another object. Eg: [{"anInt": "eq 100", "aSlice.*": "eq 1"}, {"anInt": "eq 100", "aMap.*": "eq 1"}]. The relationship between two or more query objects inside the query array is always or. To perform and, cmplx syntax can be used per field.

Queries on Collection type such as Array/Slice/Map
  • To match all elements of collection, ^ can be prefixed to the operator.

  • Eg: All elements equal to [{"aSlice.*": "^eq awesome string"}]

  • Eg: None of the elements equal to [{"aSlice.*": "^neq awesome string"}] Note:neq without ^ prefix will mean "atleast one element is not equal to."

  • Eg: Atleast one element equal to [{"aSlice.*": "eq awesome string"}]

Primitive Queries
Equal to (==)
  • [{"anInt": "eq 500"}]
Not Equal to (!=)
  • [{"anInt": "neq 500"}]
Greater than (>)
  • [{"anInt": "gt 500"}]
Less than (<)
  • [{"anInt": "lt 500"}]
Greater than or Equal (>=)
  • [{"anInt": "gte 500"}]
Less than or Equal (<=)
  • [{"anInt": "lte 500"}]
Match all Queries
Equal to (==)
  • [{"aSlice.*": "^eq 500"}]
Not Equal to (!=)
  • [{"aSlice.*": "^neq 500"}]
Greater than (>)
  • [{"aSlice.*": "^gt 500"}]
Less than (<)
  • [{"aSlice.*": "^lt 500"}]
Greater than or Equal (>=)
  • [{"aSlice.*": "^gte 500"}]
Less than or Equal (<=)
  • [{"aSlice.*": "^lte 500"}]
Complex Queries
  • The syntax for complex field queries is as follows: cmplx <operator>(<query 1>, <query 2>, ... <query n>)

  • The queries enclosed by the operator can also be another complex query. Eg: cmplx or(and(gt 7, lt 10), lt 5)

  • cmplx syntax supports two logical operators.

    • and
    • or
Example
  • [{"aSlice.*":"cmplx or(and(or(^eq 200, eq -102), ^gt -105), eq 0)", "id":"cmplx or(and(lt test100, gte test1), neq test11)"}]

Restrictions:

  • Cannot use type "interface{}" as field type.
  • Cannot use type "[]byte" as field type. Instead, string can be used.
  • Cannot use the following ascii characters in the struct names or field values:
    • "\000"
    • "\001"
    • "\002"
    • "~" (or) "\176"
    • "\177"
  • Cannot use these in struct field names:
    • "."
    • "*"
    • ".docType"
    • ".keyref"
    • ".value"
    • ".fieldName"
    • (For future) It is good to avoid having field names starting with "." in the structs

Useful Peer container ENV

  • CORE_LEDGER_STATE_TOTALQUERYLIMIT=1000000
  • CORE_VM_DOCKER_HOSTCONFIG_MEMORY=5368709120

Reference

godoc or https://godoc.org/github.com/motoreq/istate

License

iState Project source code files are made available under the Apache License, Version 2.0 (Apache-2.0), located in the LICENSE file.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func NewiState

func NewiState(object interface{}, opt Options) (iStateInterface Interface, iStateErr Error)

NewiState function is used to create an iState interface for a struct type. Further CRUD operation on the struct type can be performed using the interface returned. It takes an empty struct value, Options as input and returns istate interface.

Types

type Error

type Error interface {
	Error() string
	GetCode() int
}

Error is the interface of this package.

type Interface

type Interface interface {
	// CreateState function is used to create a new state in the state db
	// It must be called via the Interface returned by NewiState function
	// It takes chaincode stub and the actual structure value as input params
	// Note: This function does not do state validations such as, checking whether state exists or not
	// before performing the operation
	CreateState(stub shim.ChaincodeStubInterface, object interface{}) Error
	// ReadState function is used to read a state from state db
	// It must be called via the Interface returned by NewiState function
	// It takes chaincode stub and value of primary key as input params
	// It returns the actual structure value as an interface{}. The returned value can
	// be type asserted to the actual struct type before using
	// Note: This function does not do state validations such as, checking whether state exists or not
	// before performing the operation
	ReadState(stub shim.ChaincodeStubInterface, primaryKey interface{}) (interface{}, Error)
	// UpdateState function is used to update a state in statedb
	// It must be called via the Interface returned by NewiState function
	// It takes chaincode stub and the actual structure value as input params
	// Note: This function does not do state validations such as, checking whether state exists or not
	// before performing the operation
	UpdateState(stub shim.ChaincodeStubInterface, object interface{}) Error
	// PartialUpdateState function is used to update a state partially in statedb
	// It must be called via the Interface returned by NewiState function
	// It takes chaincode stub, state id and the partial structure value converted to the type
	// map[string]interface{} as input params
	// Note: This function does state validation, and returns error if state does not exist.
	PartialUpdateState(stub shim.ChaincodeStubInterface, primaryKey interface{}, partialObject map[string]interface{}) Error
	// DeleteState function is used to delete a state from state db
	// It must be called via the Interface returned by NewiState function
	// It takes chaincode stub and value of primary key as input params
	// Note: This function does not do state validations such as, checking whether state exists or not
	// before performing the operation
	DeleteState(stub shim.ChaincodeStubInterface, primaryKey interface{}) Error
	// Query function is used to perform Rich Queries over a state type in state db
	// It must be called via the Interface returned by NewiState function
	// It takes stub, query string and isInvoke (bool) flag as input params
	// isInvoke is set true, if this func is called in an invoke transaction
	// It returns slice of actual structure values as an interface{}. The returned value can
	// be type asserted to the actual slice of struct type before using
	// Learn more about the Rich query language formats in README.md
	Query(stub shim.ChaincodeStubInterface, queryString string, isInvoke bool) (interface{}, Error)
}

type Options

type Options struct {
	CacheSize             int
	DefaultCompactionSize int
}

Options is an input parameter for NewiState function. CacheSize determines the max no. of records allowed in memory. DefaultCompactionSize determines how many index records to compact at one go. Since Compaction process takes lot of time, it is useful to set a batchsize and Compaction can be run multiple times, where each time, n records gets compacted.

Directories

Path Synopsis
test

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL