├── .gitignore ├── .github └── workflows │ └── build.yml ├── go.mod ├── LICENSE ├── tree.go ├── README.md ├── mock_test.go ├── nested_set.go ├── go.sum └── nested_set_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | log 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 6 | runs-on: ubuntu-latest 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | include: 11 | - go: 1.18 12 | postgres: 10 13 | env: 14 | DATABASE_URL: ${{ matrix.gemfile }} 15 | USE_OFFICIAL_GEM_SOURCE: 1 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions/setup-go@v2 19 | with: 20 | go-version: ${{ matrix.go }} 21 | - uses: ankane/setup-postgres@v1 22 | with: 23 | postgres-version: ${{ matrix.postgres }} 24 | - run: createdb nested-set-test 25 | - run: go test ./... 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/longbridgeapp/nested-set 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/bluele/factory-go v0.0.0-20200430111232-df9c4ffc2e3e 7 | github.com/stretchr/testify v1.8.0 8 | gorm.io/driver/postgres v1.3.10 9 | gorm.io/gorm v1.23.10 10 | ) 11 | 12 | require ( 13 | github.com/davecgh/go-spew v1.1.1 // indirect 14 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 15 | github.com/jackc/pgconn v1.13.0 // indirect 16 | github.com/jackc/pgio v1.0.0 // indirect 17 | github.com/jackc/pgpassfile v1.0.0 // indirect 18 | github.com/jackc/pgproto3/v2 v2.3.1 // indirect 19 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect 20 | github.com/jackc/pgtype v1.12.0 // indirect 21 | github.com/jackc/pgx/v4 v4.17.2 // indirect 22 | github.com/jinzhu/inflection v1.0.0 // indirect 23 | github.com/jinzhu/now v1.1.4 // indirect 24 | github.com/pmezard/go-difflib v1.0.0 // indirect 25 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be // indirect 26 | golang.org/x/text v0.3.7 // indirect 27 | gopkg.in/yaml.v3 v3.0.1 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Longbridge 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tree.go: -------------------------------------------------------------------------------- 1 | package nestedset 2 | 3 | type Tree struct { 4 | Children []*TreeNode 5 | data map[int64]*TreeNode 6 | } 7 | 8 | type TreeNode struct { 9 | *nestedItem 10 | Children []*TreeNode 11 | } 12 | 13 | func initTree(items []*nestedItem) *Tree { 14 | tree := &Tree{ 15 | data: make(map[int64]*TreeNode), 16 | Children: make([]*TreeNode, 0), 17 | } 18 | 19 | for _, item := range items { 20 | node := &TreeNode{ 21 | nestedItem: item, 22 | Children: make([]*TreeNode, 0), 23 | } 24 | tree.data[node.ID] = node 25 | } 26 | 27 | for _, item := range items { 28 | node, _ := tree.getNode(item.ID) 29 | parent, found := tree.getNode(item.ParentID.Int64) 30 | if !found { 31 | tree.Children = append(tree.Children, node) 32 | } else { 33 | parent.Children = append(parent.Children, node) 34 | } 35 | } 36 | 37 | return tree 38 | } 39 | 40 | func (tree *Tree) getNode(id int64) (node *TreeNode, found bool) { 41 | if id == 0 { 42 | return nil, false 43 | } 44 | node, found = tree.data[id] 45 | return 46 | } 47 | 48 | func (tree *Tree) addNestedItem(item *nestedItem) { 49 | node := &TreeNode{ 50 | nestedItem: item, 51 | Children: make([]*TreeNode, 0), 52 | } 53 | parent, found := tree.getNode(item.ParentID.Int64) 54 | if !found { 55 | tree.Children = append(tree.Children, node) 56 | } else { 57 | parent.Children = append(parent.Children, node) 58 | } 59 | } 60 | 61 | func (tree *Tree) rebuild() *Tree { 62 | count, depth := 0, 0 63 | for _, node := range tree.Children { 64 | count = travelNode(node, count, depth) 65 | } 66 | return tree 67 | } 68 | 69 | func travelNode(node *TreeNode, lft, depth int) int { 70 | original := &nestedItem{ 71 | ID: node.ID, 72 | ParentID: node.ParentID, 73 | Depth: node.Depth, 74 | Lft: node.Lft, 75 | Rgt: node.Rgt, 76 | ChildrenCount: node.ChildrenCount, 77 | } 78 | lft += 1 79 | node.Lft = lft 80 | node.ChildrenCount = len(node.Children) 81 | node.Depth = depth 82 | for _, childNode := range node.Children { 83 | lft = travelNode(childNode, lft, depth+1) 84 | } 85 | lft += 1 86 | node.Rgt = lft 87 | node.IsChanged = node.nestedItem.IsPositionSame(original) 88 | return lft 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nested Set for Go 2 | 3 | [![build](https://github.com/longbridgeapp/nested-set/workflows/build/badge.svg)](https://github.com/longbridgeapp/nested-set/actions?query=workflow%3Abuild) 4 | 5 | Nested Set is an implementation of the [Nested set model](https://en.wikipedia.org/wiki/Nested_set_model) for [Gorm](https://gorm.io/index.html). 6 | 7 | This project is the Go version of [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set), which uses the same data structure design, so it uses the same data together with [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set). 8 | 9 | > Actually the original design is for this, the content managed by [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set) in our Rails application, the front-end Go API also needs to be maintained. 10 | 11 | This is a Go version of the [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set), and it built for compatible with [awesome_nested_set](https://github.com/collectiveidea/awesome_nested_set). 12 | 13 | ## What Go Nested Set can do? 14 | 15 | For manage a nested tree node like this: 16 | 17 | ![Showcase](https://user-images.githubusercontent.com/5518/103117256-8717d900-46a4-11eb-9079-743051b59104.gif) 18 | 19 | > Video taken from [BlueDoc](https://github.com/huacnlee/bluedoc), used by awesome_nested_set + [react-dnd](https://react-dnd.github.io/react-dnd/examples/sortable/simple). 20 | 21 | ## Installation 22 | 23 | ``` 24 | go get github.com/longbridgeapp/nested-set 25 | ``` 26 | 27 | ## Usage 28 | 29 | ### Define the model 30 | 31 | You must use `nestedset` Stuct tag to define your Gorm model like this: 32 | 33 | Support struct tags: 34 | 35 | - `id` - int64 - Primary key of the node 36 | - `parent_id` - sql.NullInt64 - ParentID column, null is root 37 | - `lft` - int 38 | - `rgt` - int 39 | - `depth` - int - Depth of the node 40 | - `children_count` - Number of children 41 | 42 | Optional: 43 | 44 | - `scope` - restricts what is to be considered a list. You can also setup scope by multiple attributes. 45 | 46 | Example: 47 | 48 | ```go 49 | import ( 50 | "database/sql" 51 | "github.com/longbridgeapp/nested-set" 52 | ) 53 | 54 | // Category 55 | type Category struct { 56 | ID int64 `gorm:"PRIMARY_KEY;AUTO_INCREMENT" nestedset:"id"` 57 | ParentID sql.NullInt64 `nestedset:"parent_id"` 58 | UserType string `nestedset:"scope"` 59 | UserID int64 `nestedset:"scope"` 60 | Rgt int `nestedset:"rgt"` 61 | Lft int `nestedset:"lft"` 62 | Depth int `nestedset:"depth"` 63 | ChildrenCount int `nestedset:"children_count"` 64 | Title string 65 | } 66 | ``` 67 | 68 | ### Move Node 69 | 70 | ```go 71 | import nestedset "github.com/longbridgeapp/nested-set" 72 | 73 | // create a new node root level last child 74 | nestedset.Create(tx, &node, nil) 75 | 76 | // create a new node as parent first child 77 | nestedset.Create(tx, &node, &parent) 78 | 79 | // nestedset.MoveDirectionLeft 80 | // nestedset.MoveDirectionRight 81 | // nestedset.MoveDirectionInner 82 | nestedset.MoveTo(tx, node, to, nestedset.MoveDirectionLeft) 83 | ``` 84 | 85 | ### Get Nodes with tree order 86 | 87 | ```go 88 | // With scope, limit tree in a scope 89 | tx := db.Model(&Category{}).Where("user_type = ? AND user_id = ?", "User", 100) 90 | 91 | // Get all nodes 92 | categories, _ := tx.Order("lft asc").Error 93 | 94 | // Get root nodes 95 | categories, _ := tx.Where("parent_id IS NULL").Order("lft asc").Error 96 | 97 | // Get childrens 98 | categories, _ := tx.Where("parent_id = ?", parentCategory.ID).Order("lft asc").Error 99 | ``` 100 | 101 | ## Testing 102 | 103 | ```bash 104 | $ createdb nested-set-test 105 | $ go test ./... 106 | ``` 107 | 108 | ```SQL 109 | -- some useful sql to check status 110 | SELECT n.id, 111 | CONCAT(REPEAT('. . ', (COUNT(p.id) - 1)::int), n.title) AS t, 112 | n.title, n.lft, n.rgt, n.depth, n.children_count 113 | FROM categories AS n, categories AS p 114 | WHERE (n.lft BETWEEN p.lft AND p.rgt) 115 | GROUP BY n.id ORDER BY n.lft; 116 | ``` 117 | 118 | ## License 119 | 120 | MIT 121 | -------------------------------------------------------------------------------- /mock_test.go: -------------------------------------------------------------------------------- 1 | package nestedset 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "time" 10 | 11 | "github.com/bluele/factory-go/factory" 12 | "gorm.io/driver/postgres" 13 | "gorm.io/gorm" 14 | "gorm.io/gorm/logger" 15 | ) 16 | 17 | func databaseURL() string { 18 | databaseURL := os.Getenv("DATABASE_URL") 19 | if len(databaseURL) == 0 { 20 | databaseURL = "postgres://localhost:5432/nested-set-test?sslmode=disable" 21 | } 22 | return databaseURL 23 | } 24 | 25 | var ( 26 | memoryDB, _ = sql.Open("postgres", databaseURL()) 27 | db = newMock(memoryDB) 28 | 29 | ctx = context.TODO() 30 | ) 31 | var clothing, mens, suits, slacks, jackets, womens, dresses, skirts, blouses, eveningGowns, sunDresses Category 32 | 33 | type Category struct { 34 | ID int64 `gorm:"PRIMARY_KEY;AUTO_INCREMENT" nestedset:"id"` 35 | Title string 36 | UserID int `nestedset:"scope"` 37 | UserType string `nestedset:"scope"` 38 | ParentID sql.NullInt64 `nestedset:"parent_id"` 39 | Rgt int `nestedset:"rgt" gorm:"type:int4"` 40 | Lft int `nestedset:"lft" gorm:"type:int4"` 41 | Depth int `nestedset:"depth" gorm:"type:int4"` 42 | ChildrenCount int `nestedset:"children_count" gorm:"type:int4"` 43 | CreatedAt time.Time 44 | UpdatedAt time.Time 45 | } 46 | 47 | type SpecialItem struct { 48 | ItemID int64 `gorm:"PRIMARY_KEY;AUTO_INCREMENT" nestedset:"id"` 49 | Title string 50 | Pid sql.NullInt64 `nestedset:"parent_id"` 51 | Right int `nestedset:"rgt"` 52 | Left int `nestedset:"lft"` 53 | Depth1 int `nestedset:"depth"` 54 | NodesCount int `nestedset:"children_count"` 55 | CreatedAt time.Time 56 | UpdatedAt time.Time 57 | } 58 | 59 | func findNode(query *gorm.DB, id int64) (category Category, err error) { 60 | err = query.Where("id=?", id).Find(&category).Error 61 | return 62 | } 63 | 64 | var CategoryFactory = factory.NewFactory(&Category{ 65 | Title: "Clothing", 66 | ParentID: sql.NullInt64{Valid: false}, 67 | UserType: "User", 68 | UserID: 999, 69 | Rgt: 1, 70 | Lft: 2, 71 | Depth: 0, 72 | ChildrenCount: 0, 73 | }). 74 | OnCreate(func(args factory.Args) error { 75 | return db.Create(args.Instance()).Error 76 | }) 77 | 78 | func newMock(_db *sql.DB) *gorm.DB { 79 | dir, _ := os.Getwd() 80 | os.MkdirAll(filepath.Join(dir, "./log"), 0777) 81 | logFile, err := os.OpenFile(filepath.Join(dir, "./log/test.log"), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) 82 | if err != nil { 83 | panic(err) 84 | } 85 | gormDB, _ := gorm.Open(postgres.New(postgres.Config{ 86 | DSN: databaseURL(), 87 | PreferSimpleProtocol: true, 88 | }), &gorm.Config{ 89 | Logger: logger.New(log.New(logFile, "\n", 1), logger.Config{ 90 | LogLevel: logger.Info, 91 | }), 92 | DisableForeignKeyConstraintWhenMigrating: true, 93 | }) 94 | 95 | return gormDB 96 | } 97 | 98 | func initData() { 99 | db.Exec("DROP TABLE IF EXISTS categories") 100 | db.Exec("DROP TABLE IF EXISTS special_items") 101 | err := db.AutoMigrate( 102 | &Category{}, 103 | &SpecialItem{}, 104 | ) 105 | if err != nil { 106 | panic(err) 107 | } 108 | buildTestData() 109 | } 110 | 111 | func buildTestData() { 112 | clothing = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 113 | "Title": "Clothing", 114 | "Lft": 1, 115 | "Rgt": 22, 116 | "Depth": 0, 117 | "ChildrenCount": 2, 118 | }).(*Category) 119 | 120 | // Create a category in other group 121 | _ = CategoryFactory.MustCreateWithOption(map[string]interface{}{ 122 | "Title": "Clothing", 123 | "Lft": 1, 124 | "UserID": 98, 125 | "Rgt": 22, 126 | "Depth": 0, 127 | "ChildrenCount": 2, 128 | }).(*Category) 129 | 130 | mens = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 131 | "Title": "Men's", 132 | "ParentID": sql.NullInt64{Valid: true, Int64: clothing.ID}, 133 | "Lft": 2, 134 | "Rgt": 9, 135 | "Depth": 1, 136 | "ChildrenCount": 1, 137 | }).(*Category) 138 | suits = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 139 | "Title": "Suits", 140 | "ParentID": sql.NullInt64{Valid: true, Int64: mens.ID}, 141 | "Lft": 3, 142 | "Rgt": 8, 143 | "Depth": 2, 144 | "ChildrenCount": 2, 145 | }).(*Category) 146 | slacks = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 147 | "Title": "Slacks", 148 | "ParentID": sql.NullInt64{Valid: true, Int64: suits.ID}, 149 | "Lft": 4, 150 | "Rgt": 5, 151 | "Depth": 3, 152 | }).(*Category) 153 | jackets = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 154 | "Title": "Jackets", 155 | "ParentID": sql.NullInt64{Valid: true, Int64: suits.ID}, 156 | "Lft": 6, 157 | "Rgt": 7, 158 | "Depth": 3, 159 | }).(*Category) 160 | womens = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 161 | "Title": "Women's", 162 | "ParentID": sql.NullInt64{Valid: true, Int64: clothing.ID}, 163 | "Lft": 10, 164 | "Rgt": 21, 165 | "Depth": 1, 166 | "ChildrenCount": 3, 167 | }).(*Category) 168 | dresses = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 169 | "Title": "Dresses", 170 | "ParentID": sql.NullInt64{Valid: true, Int64: womens.ID}, 171 | "Lft": 11, 172 | "Rgt": 16, 173 | "Depth": 2, 174 | "ChildrenCount": 2, 175 | }).(*Category) 176 | eveningGowns = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 177 | "Title": "Evening Gowns", 178 | "ParentID": sql.NullInt64{Valid: true, Int64: dresses.ID}, 179 | "Lft": 12, 180 | "Rgt": 13, 181 | "Depth": 3, 182 | }).(*Category) 183 | sunDresses = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 184 | "Title": "Sun Dresses", 185 | "ParentID": sql.NullInt64{Valid: true, Int64: dresses.ID}, 186 | "Lft": 14, 187 | "Rgt": 15, 188 | "Depth": 3, 189 | }).(*Category) 190 | skirts = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 191 | "Title": "Skirts", 192 | "ParentID": sql.NullInt64{Valid: true, Int64: womens.ID}, 193 | "Lft": 17, 194 | "Rgt": 18, 195 | "Depth": 2, 196 | }).(*Category) 197 | blouses = *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 198 | "Title": "Blouses", 199 | "ParentID": sql.NullInt64{Valid: true, Int64: womens.ID}, 200 | "Lft": 19, 201 | "Rgt": 20, 202 | "Depth": 2, 203 | }).(*Category) 204 | } 205 | 206 | func reloadCategories() { 207 | clothing, _ = findNode(db, clothing.ID) 208 | mens, _ = findNode(db, mens.ID) 209 | suits, _ = findNode(db, suits.ID) 210 | slacks, _ = findNode(db, slacks.ID) 211 | jackets, _ = findNode(db, jackets.ID) 212 | womens, _ = findNode(db, womens.ID) 213 | dresses, _ = findNode(db, dresses.ID) 214 | skirts, _ = findNode(db, skirts.ID) 215 | blouses, _ = findNode(db, blouses.ID) 216 | eveningGowns, _ = findNode(db, eveningGowns.ID) 217 | sunDresses, _ = findNode(db, sunDresses.ID) 218 | } 219 | -------------------------------------------------------------------------------- /nested_set.go: -------------------------------------------------------------------------------- 1 | package nestedset 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "strings" 10 | "sync" 11 | 12 | "gorm.io/gorm" 13 | "gorm.io/gorm/clause" 14 | "gorm.io/gorm/schema" 15 | ) 16 | 17 | // MoveDirection means where the node is going to be located 18 | type MoveDirection int 19 | 20 | // MoveDirections ... 21 | const ( 22 | // MoveDirectionLeft : MoveTo(db, a, n, MoveDirectionLeft) => a|n|... 23 | MoveDirectionLeft MoveDirection = -1 24 | 25 | // MoveDirectionRight : MoveTo(db, a, n, MoveDirectionRight) => ...|n|a| 26 | MoveDirectionRight MoveDirection = 1 27 | 28 | // MoveDirectionInner : MoveTo(db, a, n, MoveDirectionInner) => [n [...|a]] 29 | MoveDirectionInner MoveDirection = 0 30 | ) 31 | 32 | type nestedItem struct { 33 | ID int64 34 | ParentID sql.NullInt64 35 | Depth int 36 | Lft int 37 | Rgt int 38 | ChildrenCount int 39 | TableName string `gorm:"-"` 40 | DbNames map[string]string `gorm:"-"` 41 | IsChanged bool `gorm:"-"` 42 | } 43 | 44 | func (item *nestedItem) IsPositionSame(original *nestedItem) bool { 45 | return item.ID != original.ID || 46 | item.ParentID != original.ParentID || 47 | item.Depth != original.Depth || 48 | item.Lft != original.Lft || 49 | item.Rgt != original.Rgt || 50 | item.ChildrenCount != original.ChildrenCount 51 | } 52 | 53 | // parseNode parse a gorm struct into an internal nested item struct 54 | // bring in all required data attribute like scope, left, righ etc. 55 | func parseNode(db *gorm.DB, source interface{}) (tx *gorm.DB, item nestedItem, err error) { 56 | scm, err := schema.Parse(source, &sync.Map{}, schema.NamingStrategy{}) 57 | if err != nil { 58 | err = fmt.Errorf("Invalid source, must be a valid Gorm Model instance, %v", source) 59 | return 60 | } 61 | 62 | tx = db.Table(scm.Table) 63 | 64 | item = nestedItem{TableName: scm.Table, DbNames: map[string]string{}} 65 | sourceValue := reflect.Indirect(reflect.ValueOf(source)) 66 | sourceType := sourceValue.Type() 67 | for i := 0; i < sourceType.NumField(); i++ { 68 | t := sourceType.Field(i) 69 | v := sourceValue.Field(i) 70 | 71 | schemaField := scm.LookUpField(t.Name) 72 | if schemaField == nil { 73 | continue; 74 | } 75 | dbName := schemaField.DBName 76 | 77 | switch t.Tag.Get("nestedset") { 78 | case "id": 79 | item.ID = v.Int() 80 | item.DbNames["id"] = dbName 81 | break 82 | case "parent_id": 83 | item.ParentID = v.Interface().(sql.NullInt64) 84 | item.DbNames["parent_id"] = dbName 85 | break 86 | case "depth": 87 | item.Depth = int(v.Int()) 88 | item.DbNames["depth"] = dbName 89 | break 90 | case "rgt": 91 | item.Rgt = int(v.Int()) 92 | item.DbNames["rgt"] = dbName 93 | break 94 | case "lft": 95 | item.Lft = int(v.Int()) 96 | item.DbNames["lft"] = dbName 97 | break 98 | case "children_count": 99 | item.ChildrenCount = int(v.Int()) 100 | item.DbNames["children_count"] = dbName 101 | break 102 | case "scope": 103 | rawVal, _ := schemaField.ValueOf(context.TODO(), sourceValue) 104 | tx = tx.Where(dbName+" = ?", rawVal) 105 | break 106 | } 107 | } 108 | 109 | return 110 | } 111 | 112 | // Create a new node within its parent by Gorm original Create() method 113 | // ```nestedset.Create(db, &Category{...}, nil)``` will create a new category in root level 114 | // ```nestedset.Create(db, &Category{...}, &parent)``` will create a new category under parent node as its last child 115 | func Create(db *gorm.DB, source, parent interface{}) error { 116 | tx, target, err := parseNode(db, source) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | // for totally blank table / scope default init root would be [1 - 2] 122 | setToDepth, setToLft, setToRgt := 0, 1, 2 123 | dbNames := target.DbNames 124 | 125 | return tx.Transaction(func(tx *gorm.DB) (err error) { 126 | err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Pluck("id", &[]int64{}).Error 127 | if err != nil { 128 | return 129 | } 130 | 131 | // create node in root level when parent is nil 132 | if parent == nil || (reflect.ValueOf(parent).Kind() == reflect.Ptr && reflect.ValueOf(parent).IsNil()) { 133 | lastNode := make(map[string]interface{}) 134 | rst := tx.Select(dbNames["rgt"]).Order(formatSQL(":rgt DESC", target)).Take(&lastNode) 135 | if rst.Error == nil { 136 | lastNodeRgt, _ := strconv.Atoi(fmt.Sprintf("%d", lastNode[dbNames["rgt"]])) 137 | setToLft = lastNodeRgt + 1 138 | setToRgt = setToLft + 1 139 | } 140 | } else { 141 | _, targetParent, err := parseNode(db, parent) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | setToLft = targetParent.Rgt 147 | setToRgt = targetParent.Rgt + 1 148 | setToDepth = targetParent.Depth + 1 149 | 150 | // UPDATE tree SET rgt = rgt + 2 WHERE rgt >= new_lft; 151 | err = tx.Where(formatSQL(":rgt >= ?", target), setToLft). 152 | UpdateColumn(dbNames["rgt"], gorm.Expr(formatSQL(":rgt + 2", target))).Error 153 | if err != nil { 154 | return err 155 | } 156 | 157 | // UPDATE tree SET lft = lft + 2 WHERE lft > new_lft; 158 | err = tx.Where(formatSQL(":lft > ?", target), setToLft). 159 | UpdateColumn(dbNames["lft"], gorm.Expr(formatSQL(":lft + 2", target))).Error 160 | if err != nil { 161 | return err 162 | } 163 | 164 | // UPDATE tree SET children_count = children_count + 1 WHERE id = parent.id; 165 | err = tx.Model(parent).Update( 166 | dbNames["children_count"], gorm.Expr(formatSQL(":children_count + 1", target))).Error 167 | if err != nil { 168 | return err 169 | } 170 | } 171 | 172 | // Set Lft, Rgt, Depth dynamically 173 | v := reflect.Indirect(reflect.ValueOf(source)) 174 | t := v.Type() 175 | for i := 0; i < t.NumField(); i++ { 176 | f := t.Field(i) 177 | switch f.Tag.Get("nestedset") { 178 | case "lft": 179 | f := v.FieldByName(f.Name) 180 | f.SetInt(int64(setToLft)) 181 | break 182 | case "rgt": 183 | f := v.FieldByName(f.Name) 184 | f.SetInt(int64(setToRgt)) 185 | break 186 | case "depth": 187 | f := v.FieldByName(f.Name) 188 | f.SetInt(int64(setToDepth)) 189 | break 190 | } 191 | } 192 | 193 | return tx.Create(source).Error 194 | }) 195 | } 196 | 197 | // Delete a node from scoped list and its all descendent 198 | // ```nestedset.Delete(db, &Category{...})``` 199 | func Delete(db *gorm.DB, source interface{}) error { 200 | tx, target, err := parseNode(db, source) 201 | if err != nil { 202 | return err 203 | } 204 | 205 | // Batch Delete Method in GORM requires an instance of current source type without ID 206 | // to avoid GORM style Delete interface, we hacked here by set source ID to 0 207 | dbNames := target.DbNames 208 | v := reflect.Indirect(reflect.ValueOf(source)) 209 | t := v.Type() 210 | for i := 0; i < t.NumField(); i++ { 211 | f := t.Field(i) 212 | if f.Tag.Get("nestedset") == "id" { 213 | f := v.FieldByName(f.Name) 214 | f.SetInt(0) 215 | break 216 | } 217 | } 218 | 219 | return tx.Transaction(func(tx *gorm.DB) (err error) { 220 | err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Pluck("id", &[]int64{}).Error 221 | if err != nil { 222 | return 223 | } 224 | 225 | err = tx.Where(formatSQL(":lft >= ? AND :rgt <= ?", target), target.Lft, target.Rgt). 226 | Delete(source).Error 227 | if err != nil { 228 | return err 229 | } 230 | 231 | // UPDATE tree SET rgt = rgt - width WHERE rgt > target_rgt; 232 | // UPDATE tree SET lft = lft - width WHERE lft > target_rgt; 233 | width := target.Rgt - target.Lft + 1 234 | for _, d := range []string{"rgt", "lft"} { 235 | err = tx.Where(formatSQL(":"+d+" > ?", target), target.Rgt). 236 | Update(dbNames[d], gorm.Expr(formatSQL(":"+d+" - ?", target), width)). 237 | Error 238 | if err != nil { 239 | return err 240 | } 241 | } 242 | 243 | return syncChildrenCount(tx, target, target.ParentID, sql.NullInt64{}) 244 | }) 245 | } 246 | 247 | // MoveTo move node to a position which is related a target node 248 | // ```nestedset.MoveTo(db, &node, &to, nestedset.MoveDirectionInner)``` will move [&node] to [&to] node's child_list as its first child 249 | func MoveTo(db *gorm.DB, node, to interface{}, direction MoveDirection) error { 250 | tx, targetNode, err := parseNode(db, node) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | _, toNode, err := parseNode(db, to) 256 | if err != nil { 257 | return err 258 | } 259 | 260 | err = moveIsValid(targetNode, toNode) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | var right, depthChange int 266 | var newParentID sql.NullInt64 267 | if direction == MoveDirectionLeft || direction == MoveDirectionRight { 268 | newParentID = toNode.ParentID 269 | depthChange = toNode.Depth - targetNode.Depth 270 | if direction == MoveDirectionLeft { 271 | right = toNode.Lft - 1 272 | } else { 273 | right = toNode.Rgt 274 | } 275 | } else { 276 | newParentID = sql.NullInt64{Int64: toNode.ID, Valid: true} 277 | depthChange = toNode.Depth + 1 - targetNode.Depth 278 | right = toNode.Lft 279 | } 280 | 281 | return moveToRightOfPosition(tx, targetNode, right, depthChange, newParentID) 282 | } 283 | 284 | // Rebuild rebuild nodes as any nestedset which in the scope 285 | // ```nestedset.Rebuild(db, &node, true)``` will rebuild [&node] as nestedset 286 | func Rebuild(db *gorm.DB, source interface{}, doUpdate bool) (affectedCount int, err error) { 287 | tx, target, err := parseNode(db, source) 288 | if err != nil { 289 | return 290 | } 291 | err = tx.Transaction(func(tx *gorm.DB) (err error) { 292 | allItems := []*nestedItem{} 293 | err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). 294 | Where(formatSQL("", target)). 295 | Order(formatSQL(":parent_id ASC NULLS FIRST, :lft ASC", target)). 296 | Find(&allItems). 297 | Error 298 | 299 | if err != nil { 300 | return 301 | } 302 | initTree(allItems).rebuild() 303 | for _, item := range allItems { 304 | if item.IsChanged { 305 | affectedCount += 1 306 | if doUpdate { 307 | err = tx.Table(target.TableName). 308 | Where(formatSQL(":id=?", target), item.ID). 309 | Updates(map[string]interface{}{ 310 | target.DbNames["lft"]: item.Lft, 311 | target.DbNames["rgt"]: item.Rgt, 312 | target.DbNames["depth"]: item.Depth, 313 | target.DbNames["children_count"]: item.ChildrenCount, 314 | }).Error 315 | if err != nil { 316 | return 317 | } 318 | } 319 | } 320 | } 321 | return nil 322 | }) 323 | return 324 | } 325 | 326 | 327 | // RebuildBatched rebuild nodes as any nestedset which in the scope 328 | // ```nestedset.RebuildBatched(db, &node, true, 1000)``` will rebuild [&node] as nestedset 329 | func RebuildBatched(db *gorm.DB, source interface{}, doUpdate bool, batchSize int) (affectedCount int, err error) { 330 | tx, target, err := parseNode(db, source) 331 | if err != nil { 332 | return 333 | } 334 | err = tx.Transaction(func(tx *gorm.DB) (err error) { 335 | allItems := []*nestedItem{} 336 | err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). 337 | Where(formatSQL("", target)). 338 | Order(formatSQL(":parent_id ASC NULLS FIRST, :lft ASC", target)). 339 | Find(&allItems). 340 | Error 341 | 342 | if err != nil { 343 | return 344 | } 345 | initTree(allItems).rebuild() 346 | 347 | var itemsToUpdate []*nestedItem 348 | for _, item := range allItems { 349 | if item.IsChanged { 350 | affectedCount += 1 351 | if doUpdate { 352 | itemsToUpdate = append(itemsToUpdate, item) 353 | } 354 | } 355 | } 356 | if doUpdate && len(itemsToUpdate) > 0 { 357 | err = batchUpdate(tx, []string{"lft", "rgt", "depth", "children_count"}, target.DbNames, itemsToUpdate, batchSize) 358 | if err != nil { 359 | return 360 | } 361 | } 362 | return nil 363 | }) 364 | return 365 | } 366 | 367 | // batchUpdate performs a batched upsert (update on conflict) for the given columns and items. 368 | func batchUpdate(db *gorm.DB, columns []string, dbNames map[string]string, items []*nestedItem, batchSize int) error { 369 | if len(items) == 0 { 370 | return nil 371 | } 372 | 373 | assignmentMap := map[string]interface{}{} 374 | for _, column := range columns { 375 | column = dbNames[column] 376 | assignmentMap[column] = gorm.Expr("EXCLUDED." + column) 377 | } 378 | 379 | return db.Clauses(clause.OnConflict{ 380 | Columns: []clause.Column{{Name: dbNames["id"]}}, 381 | DoUpdates: clause.Assignments(assignmentMap), 382 | }).CreateInBatches(items, batchSize).Error 383 | } 384 | 385 | func moveIsValid(node, to nestedItem) error { 386 | validLft, validRgt := node.Lft, node.Rgt 387 | if (to.Lft >= validLft && to.Lft <= validRgt) || (to.Rgt >= validLft && to.Rgt <= validRgt) { 388 | return fmt.Errorf("in valid move target: %v => %v", node, to) 389 | } 390 | 391 | return nil 392 | } 393 | 394 | func moveToRightOfPosition(tx *gorm.DB, targetNode nestedItem, position, depthChange int, newParentID sql.NullInt64) error { 395 | return tx.Transaction(func(tx *gorm.DB) (err error) { 396 | err = tx.Clauses(clause.Locking{Strength: "UPDATE"}).Pluck("id", &[]int64{}).Error 397 | if err != nil { 398 | return 399 | } 400 | 401 | oldParentID := targetNode.ParentID 402 | targetRight := targetNode.Rgt 403 | targetLeft := targetNode.Lft 404 | targetWidth := targetRight - targetLeft + 1 405 | 406 | targetIds := []int64{} 407 | err = tx.Where(formatSQL(":lft >= ? AND :rgt <= ?", targetNode), targetLeft, targetRight).Pluck("id", &targetIds).Error 408 | if err != nil { 409 | return 410 | } 411 | 412 | var moveStep, affectedStep, affectedGte, affectedLte int 413 | moveStep = position - targetLeft + 1 414 | if moveStep < 0 { 415 | affectedGte = position + 1 416 | affectedLte = targetLeft - 1 417 | affectedStep = targetWidth 418 | } else if moveStep > 0 { 419 | affectedGte = targetRight + 1 420 | affectedLte = position 421 | affectedStep = targetWidth * -1 422 | // move backwards should minus target covered length/width 423 | moveStep = moveStep - targetWidth 424 | } else { 425 | return nil 426 | } 427 | 428 | err = moveAffected(tx, targetNode, affectedGte, affectedLte, affectedStep) 429 | if err != nil { 430 | return 431 | } 432 | 433 | err = moveTarget(tx, targetNode, targetNode.ID, targetIds, moveStep, depthChange, newParentID) 434 | if err != nil { 435 | return 436 | } 437 | 438 | return syncChildrenCount(tx, targetNode, oldParentID, newParentID) 439 | }) 440 | } 441 | 442 | func syncChildrenCount(tx *gorm.DB, targetNode nestedItem, oldParentID, newParentID sql.NullInt64) (err error) { 443 | var oldParentCount, newParentCount int64 444 | 445 | if oldParentID.Valid { 446 | err = tx.Where(formatSQL(":parent_id = ?", targetNode), oldParentID).Count(&oldParentCount).Error 447 | if err != nil { 448 | return 449 | } 450 | err = tx.Where(formatSQL(":id = ?", targetNode), oldParentID).Update(targetNode.DbNames["children_count"], oldParentCount).Error 451 | if err != nil { 452 | return 453 | } 454 | } 455 | 456 | if newParentID.Valid { 457 | err = tx.Where(formatSQL(":parent_id = ?", targetNode), newParentID).Count(&newParentCount).Error 458 | if err != nil { 459 | return 460 | } 461 | err = tx.Where(formatSQL(":id = ?", targetNode), newParentID).Update(targetNode.DbNames["children_count"], newParentCount).Error 462 | if err != nil { 463 | return 464 | } 465 | } 466 | 467 | return nil 468 | } 469 | 470 | func moveTarget(tx *gorm.DB, targetNode nestedItem, targetID int64, targetIds []int64, step, depthChange int, newParentID sql.NullInt64) (err error) { 471 | dbNames := targetNode.DbNames 472 | 473 | if len(targetIds) > 0 { 474 | err = tx.Where(formatSQL(":id IN (?)", targetNode), targetIds). 475 | Updates(map[string]interface{}{ 476 | dbNames["lft"]: gorm.Expr(formatSQL(":lft + ?", targetNode), step), 477 | dbNames["rgt"]: gorm.Expr(formatSQL(":rgt + ?", targetNode), step), 478 | dbNames["depth"]: gorm.Expr(formatSQL(":depth + ?", targetNode), depthChange), 479 | }).Error 480 | if err != nil { 481 | return 482 | } 483 | } 484 | 485 | return tx.Where(formatSQL(":id = ?", targetNode), targetID).Update(dbNames["parent_id"], newParentID).Error 486 | } 487 | 488 | func moveAffected(tx *gorm.DB, targetNode nestedItem, gte, lte, step int) (err error) { 489 | dbNames := targetNode.DbNames 490 | 491 | return tx.Where(formatSQL("(:lft BETWEEN ? AND ?) OR (:rgt BETWEEN ? AND ?)", targetNode), gte, lte, gte, lte). 492 | Updates(map[string]interface{}{ 493 | dbNames["lft"]: gorm.Expr(formatSQL("(CASE WHEN :lft >= ? THEN :lft + ? ELSE :lft END)", targetNode), gte, step), 494 | dbNames["rgt"]: gorm.Expr(formatSQL("(CASE WHEN :rgt <= ? THEN :rgt + ? ELSE :rgt END)", targetNode), lte, step), 495 | }).Error 496 | } 497 | 498 | func formatSQL(placeHolderSQL string, node nestedItem) (out string) { 499 | out = placeHolderSQL 500 | 501 | out = strings.ReplaceAll(out, ":table_name", node.TableName) 502 | for k, v := range node.DbNames { 503 | out = strings.Replace(out, ":"+k, v, -1) 504 | } 505 | 506 | return 507 | } 508 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= 3 | github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= 4 | github.com/bluele/factory-go v0.0.0-20200430111232-df9c4ffc2e3e h1:jEw5WGmc8WiBfPb+XxavfbxPAgtPdIycxSjMN8svHzw= 5 | github.com/bluele/factory-go v0.0.0-20200430111232-df9c4ffc2e3e/go.mod h1:C+/xfXxCR66wsm6I3Mzbf72W/Lz2NPsGQhSWDVBa5YU= 6 | github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= 7 | github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= 8 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 9 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 10 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 11 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 12 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 13 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 14 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 15 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 16 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 17 | github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= 18 | github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 19 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 20 | github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= 21 | github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 22 | github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= 23 | github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= 24 | github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= 25 | github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= 26 | github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= 27 | github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= 28 | github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= 29 | github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= 30 | github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys= 31 | github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI= 32 | github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= 33 | github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= 34 | github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= 35 | github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= 36 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= 37 | github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= 38 | github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 39 | github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 40 | github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= 41 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= 42 | github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= 43 | github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 44 | github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= 45 | github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 46 | github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 47 | github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y= 48 | github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= 49 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg= 50 | github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= 51 | github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= 52 | github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= 53 | github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= 54 | github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= 55 | github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w= 56 | github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= 57 | github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= 58 | github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= 59 | github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= 60 | github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= 61 | github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E= 62 | github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw= 63 | github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 64 | github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 65 | github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 66 | github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= 67 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 68 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 69 | github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= 70 | github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 71 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 72 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 73 | github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 74 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 75 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 76 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 77 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 78 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 79 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 80 | github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 81 | github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 82 | github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 83 | github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= 84 | github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 85 | github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= 86 | github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 87 | github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 88 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 89 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 90 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 91 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 92 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 93 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 94 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 95 | github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= 96 | github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= 97 | github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= 98 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 99 | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= 100 | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= 101 | github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= 102 | github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= 103 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 104 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 105 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 106 | github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= 107 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 108 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 109 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 110 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 111 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 112 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 113 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 114 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 115 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 116 | github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= 117 | go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 118 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 119 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 120 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 121 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 122 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 123 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 124 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 125 | go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 126 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 127 | go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 128 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 129 | golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= 130 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 131 | golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 132 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 133 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 134 | golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= 135 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 136 | golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 137 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 138 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A= 139 | golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 140 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 141 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 142 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 143 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 144 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 145 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 146 | golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 147 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 148 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 149 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 150 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 151 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 152 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 153 | golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 154 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 155 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 156 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 157 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 158 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 159 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 161 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 162 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 | golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= 164 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 165 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 166 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 167 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 168 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 169 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 170 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 171 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 172 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 173 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 174 | golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 175 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 176 | golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 177 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 178 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 179 | golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 180 | golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 181 | golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 182 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 183 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 185 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 186 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 187 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 188 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 189 | gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= 190 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 191 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 192 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 193 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 194 | gorm.io/driver/postgres v1.3.10 h1:Fsd+pQpFMGlGxxVMUPJhNo8gG8B1lKtk8QQ4/VZZAJw= 195 | gorm.io/driver/postgres v1.3.10/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw= 196 | gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= 197 | gorm.io/gorm v1.23.10 h1:4Ne9ZbzID9GUxRkllxN4WjJKpsHx8YbKvekVdgyWh24= 198 | gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= 199 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 200 | -------------------------------------------------------------------------------- /nested_set_test.go: -------------------------------------------------------------------------------- 1 | package nestedset 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "gorm.io/gorm/clause" 9 | ) 10 | 11 | func TestReloadData(t *testing.T) { 12 | reloadCategories() 13 | } 14 | 15 | func TestNewNodeItem(t *testing.T) { 16 | source := Category{ 17 | ID: 123, 18 | ParentID: sql.NullInt64{Valid: true, Int64: 100}, 19 | Depth: 2, 20 | Rgt: 12, 21 | Lft: 32, 22 | UserType: "User", 23 | UserID: 1000, 24 | ChildrenCount: 10, 25 | } 26 | tx, node, err := parseNode(db, source) 27 | assert.NoError(t, err) 28 | assert.Equal(t, source.ID, node.ID) 29 | assert.Equal(t, source.ParentID, node.ParentID) 30 | assert.Equal(t, source.Depth, node.Depth) 31 | assert.Equal(t, source.Lft, node.Lft) 32 | assert.Equal(t, source.Rgt, node.Rgt) 33 | assert.Equal(t, source.ChildrenCount, node.ChildrenCount) 34 | assert.Equal(t, "categories", node.TableName) 35 | stmt := tx.Statement 36 | stmt.Build(clause.Where{}.Name()) 37 | assert.Equal(t, "WHERE user_id = $1 AND user_type = $2", stmt.SQL.String()) 38 | 39 | tx, node, err = parseNode(db, &source) 40 | assert.NoError(t, err) 41 | assert.Equal(t, source.ID, node.ID) 42 | assert.Equal(t, source.ParentID, node.ParentID) 43 | assert.Equal(t, source.Depth, node.Depth) 44 | assert.Equal(t, source.Lft, node.Lft) 45 | assert.Equal(t, source.Rgt, node.Rgt) 46 | assert.Equal(t, source.ChildrenCount, node.ChildrenCount) 47 | assert.Equal(t, "categories", node.TableName) 48 | stmt = tx.Statement 49 | stmt.Build(clause.Where{}.Name()) 50 | assert.Equal(t, "WHERE user_id = $1 AND user_type = $2", stmt.SQL.String()) 51 | 52 | dbNames := node.DbNames 53 | assert.Equal(t, "id", dbNames["id"]) 54 | assert.Equal(t, "parent_id", dbNames["parent_id"]) 55 | assert.Equal(t, "depth", dbNames["depth"]) 56 | assert.Equal(t, "rgt", dbNames["rgt"]) 57 | assert.Equal(t, "lft", dbNames["lft"]) 58 | assert.Equal(t, "children_count", dbNames["children_count"]) 59 | 60 | // Test for difference column names 61 | specialItem := SpecialItem{ 62 | ItemID: 100, 63 | Pid: sql.NullInt64{Valid: true, Int64: 101}, 64 | Depth1: 2, 65 | Right: 10, 66 | Left: 1, 67 | NodesCount: 8, 68 | } 69 | tx, node, err = parseNode(db, specialItem) 70 | assert.NoError(t, err) 71 | assert.Equal(t, specialItem.ItemID, node.ID) 72 | assert.Equal(t, specialItem.Pid, node.ParentID) 73 | assert.Equal(t, specialItem.Depth1, node.Depth) 74 | assert.Equal(t, specialItem.Right, node.Rgt) 75 | assert.Equal(t, specialItem.Left, node.Lft) 76 | assert.Equal(t, specialItem.NodesCount, node.ChildrenCount) 77 | assert.Equal(t, "special_items", node.TableName) 78 | 79 | stmt = tx.Statement 80 | stmt.Build(clause.Where{}.Name()) 81 | assert.Equal(t, "", stmt.SQL.String()) 82 | 83 | dbNames = node.DbNames 84 | assert.Equal(t, "item_id", dbNames["id"]) 85 | assert.Equal(t, "pid", dbNames["parent_id"]) 86 | assert.Equal(t, "depth1", dbNames["depth"]) 87 | assert.Equal(t, "right", dbNames["rgt"]) 88 | assert.Equal(t, "left", dbNames["lft"]) 89 | assert.Equal(t, "nodes_count", dbNames["children_count"]) 90 | 91 | // formatSQL test 92 | assert.Equal(t, "item_id = ? AND left > right AND pid = ?, nodes_count = 1, depth1 = 0", formatSQL(":id = ? AND :lft > :rgt AND :parent_id = ?, :children_count = 1, :depth = 0", node)) 93 | } 94 | 95 | func TestCreateSource(t *testing.T) { 96 | initData() 97 | 98 | c1 := Category{Title: "c1s"} 99 | var cNil *Category 100 | Create(db, &c1, cNil) 101 | assert.Equal(t, c1.Lft, 1) 102 | assert.Equal(t, c1.Rgt, 2) 103 | assert.Equal(t, c1.Depth, 0) 104 | 105 | cp := Category{Title: "cps"} 106 | Create(db, &cp, nil) 107 | assert.Equal(t, cp.Lft, 3) 108 | assert.Equal(t, cp.Rgt, 4) 109 | 110 | c2 := Category{Title: "c2s", UserType: "ux"} 111 | Create(db, &c2, nil) 112 | assert.Equal(t, c2.Lft, 1) 113 | assert.Equal(t, c2.Rgt, 2) 114 | 115 | c3 := Category{Title: "c3s", UserType: "ux"} 116 | Create(db, &c3, nil) 117 | assert.Equal(t, c3.Lft, 3) 118 | assert.Equal(t, c3.Rgt, 4) 119 | 120 | c4 := Category{Title: "c4s", UserType: "ux"} 121 | Create(db, &c4, &c2) 122 | assert.Equal(t, c4.Lft, 2) 123 | assert.Equal(t, c4.Rgt, 3) 124 | assert.Equal(t, c4.Depth, 1) 125 | 126 | // after insert a new node into c2 127 | db.Find(&c3) 128 | db.Find(&c2) 129 | assert.Equal(t, c3.Lft, 5) 130 | assert.Equal(t, c3.Rgt, 6) 131 | assert.Equal(t, c2.ChildrenCount, 1) 132 | } 133 | 134 | func TestDeleteSource(t *testing.T) { 135 | initData() 136 | 137 | c1 := Category{Title: "c1s"} 138 | Create(db, &c1, nil) 139 | 140 | cp := Category{Title: "cp"} 141 | Create(db, &cp, c1) 142 | 143 | c2 := Category{Title: "c2s"} 144 | Create(db, &c2, nil) 145 | 146 | db.First(&c1) 147 | Delete(db, &c1) 148 | 149 | db.Model(&c2).First(&c2) 150 | assert.Equal(t, c2.Lft, 1) 151 | assert.Equal(t, c2.Rgt, 2) 152 | } 153 | 154 | func TestMoveToRight(t *testing.T) { 155 | // case 1 156 | initData() 157 | MoveTo(db, dresses, jackets, MoveDirectionRight) 158 | reloadCategories() 159 | 160 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 161 | assertNodeEqual(t, mens, 2, 15, 1, 1, clothing.ID) 162 | assertNodeEqual(t, suits, 3, 14, 2, 3, mens.ID) 163 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 164 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 165 | assertNodeEqual(t, dresses, 8, 13, 3, 2, suits.ID) 166 | assertNodeEqual(t, eveningGowns, 9, 10, 4, 0, dresses.ID) 167 | assertNodeEqual(t, sunDresses, 11, 12, 4, 0, dresses.ID) 168 | assertNodeEqual(t, womens, 16, 21, 1, 2, clothing.ID) 169 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 170 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 171 | 172 | // case 2 173 | initData() 174 | MoveTo(db, suits, blouses, MoveDirectionRight) 175 | reloadCategories() 176 | 177 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 178 | assertNodeEqual(t, mens, 2, 3, 1, 0, clothing.ID) 179 | assertNodeEqual(t, womens, 4, 21, 1, 4, clothing.ID) 180 | assertNodeEqual(t, dresses, 5, 10, 2, 2, womens.ID) 181 | assertNodeEqual(t, eveningGowns, 6, 7, 3, 0, dresses.ID) 182 | assertNodeEqual(t, sunDresses, 8, 9, 3, 0, dresses.ID) 183 | assertNodeEqual(t, skirts, 11, 12, 2, 0, womens.ID) 184 | assertNodeEqual(t, blouses, 13, 14, 2, 0, womens.ID) 185 | assertNodeEqual(t, suits, 15, 20, 2, 2, womens.ID) 186 | assertNodeEqual(t, slacks, 16, 17, 3, 0, suits.ID) 187 | assertNodeEqual(t, jackets, 18, 19, 3, 0, suits.ID) 188 | } 189 | 190 | func TestRebuild(t *testing.T) { 191 | initData() 192 | affectedCount, err := Rebuild(db, clothing, true) 193 | assert.NoError(t, err) 194 | assert.Equal(t, 0, affectedCount) 195 | reloadCategories() 196 | 197 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 198 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 199 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 200 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 201 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 202 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 203 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 204 | assertNodeEqual(t, eveningGowns, 12, 13, 3, 0, dresses.ID) 205 | assertNodeEqual(t, sunDresses, 14, 15, 3, 0, dresses.ID) 206 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 207 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 208 | 209 | sunDresses.Rgt = 123 210 | sunDresses.Lft = 12 211 | sunDresses.Depth = 1 212 | sunDresses.ChildrenCount = 100 213 | err = db.Updates(&sunDresses).Error 214 | assert.NoError(t, err) 215 | reloadCategories() 216 | assertNodeEqual(t, sunDresses, 12, 123, 1, 100, dresses.ID) 217 | 218 | affectedCount, err = Rebuild(db, clothing, true) 219 | assert.NoError(t, err) 220 | assert.Equal(t, 2, affectedCount) 221 | reloadCategories() 222 | 223 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 224 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 225 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 226 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 227 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 228 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 229 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 230 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 231 | assertNodeEqual(t, sunDresses, 12, 13, 3, 0, dresses.ID) 232 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 233 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 234 | 235 | affectedCount, err = Rebuild(db, clothing, true) 236 | assert.NoError(t, err) 237 | assert.Equal(t, 0, affectedCount) 238 | reloadCategories() 239 | 240 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 241 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 242 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 243 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 244 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 245 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 246 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 247 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 248 | assertNodeEqual(t, sunDresses, 12, 13, 3, 0, dresses.ID) 249 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 250 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 251 | 252 | hat := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 253 | "Title": "Hat", 254 | "ParentID": sql.NullInt64{Valid: false}, 255 | }).(*Category) 256 | 257 | affectedCount, err = Rebuild(db, clothing, false) 258 | assert.NoError(t, err) 259 | assert.Equal(t, 1, affectedCount) 260 | 261 | affectedCount, err = Rebuild(db, clothing, true) 262 | assert.NoError(t, err) 263 | assert.Equal(t, 1, affectedCount) 264 | reloadCategories() 265 | hat, _ = findNode(db, hat.ID) 266 | 267 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 268 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 269 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 270 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 271 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 272 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 273 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 274 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 275 | assertNodeEqual(t, sunDresses, 12, 13, 3, 0, dresses.ID) 276 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 277 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 278 | assertNodeEqual(t, hat, 23, 24, 0, 0, 0) 279 | 280 | jacksClothing := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 281 | "Title": "Jack's Clothing", 282 | "ParentID": sql.NullInt64{Valid: false}, 283 | "UserType": "User", 284 | "UserID": 8686, 285 | }).(*Category) 286 | jacksSuits := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 287 | "Title": "Jack's Suits", 288 | "ParentID": sql.NullInt64{Valid: true, Int64: jacksClothing.ID}, 289 | "UserType": "User", 290 | "UserID": 8686, 291 | }).(*Category) 292 | jacksHat := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 293 | "Title": "Jack's Hat", 294 | "UserType": "User", 295 | "UserID": 8686, 296 | "ParentID": sql.NullInt64{Valid: false}, 297 | }).(*Category) 298 | jacksSlacks := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 299 | "Title": "Jack's Slacks", 300 | "ParentID": sql.NullInt64{Valid: true, Int64: jacksClothing.ID}, 301 | "UserType": "User", 302 | "UserID": 8686, 303 | }).(*Category) 304 | 305 | lilysHat := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 306 | "Title": "Lily's Hat", 307 | "UserType": "User", 308 | "UserID": 6666, 309 | "ParentID": sql.NullInt64{Valid: false}, 310 | }).(*Category) 311 | lilysClothing := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 312 | "Title": "Lily's Clothing", 313 | "ParentID": sql.NullInt64{Valid: false}, 314 | "UserType": "User", 315 | "UserID": 6666, 316 | }).(*Category) 317 | lilysDresses := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 318 | "Title": "Lily's Dresses", 319 | "ParentID": sql.NullInt64{Valid: true, Int64: lilysClothing.ID}, 320 | "UserType": "User", 321 | "UserID": 6666, 322 | }).(*Category) 323 | 324 | affectedCount, err = Rebuild(db, jacksSuits, true) 325 | assert.NoError(t, err) 326 | assert.Equal(t, 4, affectedCount) 327 | affectedCount, err = Rebuild(db, lilysHat, true) 328 | assert.NoError(t, err) 329 | assert.Equal(t, 3, affectedCount) 330 | jacksClothing, _ = findNode(db, jacksClothing.ID) 331 | jacksSuits, _ = findNode(db, jacksSuits.ID) 332 | jacksSlacks, _ = findNode(db, jacksSlacks.ID) 333 | jacksHat, _ = findNode(db, jacksHat.ID) 334 | lilysHat, _ = findNode(db, lilysHat.ID) 335 | lilysClothing, _ = findNode(db, lilysClothing.ID) 336 | lilysDresses, _ = findNode(db, lilysDresses.ID) 337 | 338 | assertNodeEqual(t, jacksClothing, 1, 6, 0, 2, 0) 339 | assertNodeEqual(t, jacksSuits, 2, 3, 1, 0, jacksClothing.ID) 340 | assertNodeEqual(t, jacksSlacks, 4, 5, 1, 0, jacksClothing.ID) 341 | assertNodeEqual(t, jacksHat, 7, 8, 0, 0, 0) 342 | assertNodeEqual(t, lilysHat, 1, 2, 0, 0, 0) 343 | assertNodeEqual(t, lilysClothing, 3, 6, 0, 1, 0) 344 | assertNodeEqual(t, lilysDresses, 4, 5, 1, 0, lilysClothing.ID) 345 | } 346 | 347 | func TestRebuildBatched(t *testing.T) { 348 | const batchSize = 5 349 | initData() 350 | affectedCount, err := RebuildBatched(db, clothing, true, batchSize) 351 | assert.NoError(t, err) 352 | assert.Equal(t, 0, affectedCount) 353 | reloadCategories() 354 | 355 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 356 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 357 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 358 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 359 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 360 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 361 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 362 | assertNodeEqual(t, eveningGowns, 12, 13, 3, 0, dresses.ID) 363 | assertNodeEqual(t, sunDresses, 14, 15, 3, 0, dresses.ID) 364 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 365 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 366 | 367 | sunDresses.Rgt = 123 368 | sunDresses.Lft = 12 369 | sunDresses.Depth = 1 370 | sunDresses.ChildrenCount = 100 371 | err = db.Updates(&sunDresses).Error 372 | assert.NoError(t, err) 373 | reloadCategories() 374 | assertNodeEqual(t, sunDresses, 12, 123, 1, 100, dresses.ID) 375 | 376 | affectedCount, err = RebuildBatched(db, clothing, true, batchSize) 377 | assert.NoError(t, err) 378 | assert.Equal(t, 2, affectedCount) 379 | reloadCategories() 380 | 381 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 382 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 383 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 384 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 385 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 386 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 387 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 388 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 389 | assertNodeEqual(t, sunDresses, 12, 13, 3, 0, dresses.ID) 390 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 391 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 392 | 393 | affectedCount, err = RebuildBatched(db, clothing, true, batchSize) 394 | assert.NoError(t, err) 395 | assert.Equal(t, 0, affectedCount) 396 | reloadCategories() 397 | 398 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 399 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 400 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 401 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 402 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 403 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 404 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 405 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 406 | assertNodeEqual(t, sunDresses, 12, 13, 3, 0, dresses.ID) 407 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 408 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 409 | 410 | hat := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 411 | "Title": "Hat", 412 | "ParentID": sql.NullInt64{Valid: false}, 413 | }).(*Category) 414 | 415 | affectedCount, err = RebuildBatched(db, clothing, false, batchSize) 416 | assert.NoError(t, err) 417 | assert.Equal(t, 1, affectedCount) 418 | 419 | affectedCount, err = RebuildBatched(db, clothing, true, batchSize) 420 | assert.NoError(t, err) 421 | assert.Equal(t, 1, affectedCount) 422 | reloadCategories() 423 | hat, _ = findNode(db, hat.ID) 424 | 425 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 426 | assertNodeEqual(t, mens, 2, 9, 1, 1, clothing.ID) 427 | assertNodeEqual(t, suits, 3, 8, 2, 2, mens.ID) 428 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 429 | assertNodeEqual(t, jackets, 6, 7, 3, 0, suits.ID) 430 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 431 | assertNodeEqual(t, dresses, 11, 16, 2, 2, womens.ID) 432 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 433 | assertNodeEqual(t, sunDresses, 12, 13, 3, 0, dresses.ID) 434 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 435 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 436 | assertNodeEqual(t, hat, 23, 24, 0, 0, 0) 437 | 438 | jacksClothing := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 439 | "Title": "Jack's Clothing", 440 | "ParentID": sql.NullInt64{Valid: false}, 441 | "UserType": "User", 442 | "UserID": 8686, 443 | }).(*Category) 444 | jacksSuits := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 445 | "Title": "Jack's Suits", 446 | "ParentID": sql.NullInt64{Valid: true, Int64: jacksClothing.ID}, 447 | "UserType": "User", 448 | "UserID": 8686, 449 | }).(*Category) 450 | jacksHat := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 451 | "Title": "Jack's Hat", 452 | "UserType": "User", 453 | "UserID": 8686, 454 | "ParentID": sql.NullInt64{Valid: false}, 455 | }).(*Category) 456 | jacksSlacks := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 457 | "Title": "Jack's Slacks", 458 | "ParentID": sql.NullInt64{Valid: true, Int64: jacksClothing.ID}, 459 | "UserType": "User", 460 | "UserID": 8686, 461 | }).(*Category) 462 | 463 | lilysHat := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 464 | "Title": "Lily's Hat", 465 | "UserType": "User", 466 | "UserID": 6666, 467 | "ParentID": sql.NullInt64{Valid: false}, 468 | }).(*Category) 469 | lilysClothing := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 470 | "Title": "Lily's Clothing", 471 | "ParentID": sql.NullInt64{Valid: false}, 472 | "UserType": "User", 473 | "UserID": 6666, 474 | }).(*Category) 475 | lilysDresses := *CategoryFactory.MustCreateWithOption(map[string]interface{}{ 476 | "Title": "Lily's Dresses", 477 | "ParentID": sql.NullInt64{Valid: true, Int64: lilysClothing.ID}, 478 | "UserType": "User", 479 | "UserID": 6666, 480 | }).(*Category) 481 | 482 | affectedCount, err = RebuildBatched(db, jacksSuits, true, batchSize) 483 | assert.NoError(t, err) 484 | assert.Equal(t, 4, affectedCount) 485 | affectedCount, err = RebuildBatched(db, lilysHat, true, batchSize) 486 | assert.NoError(t, err) 487 | assert.Equal(t, 3, affectedCount) 488 | jacksClothing, _ = findNode(db, jacksClothing.ID) 489 | jacksSuits, _ = findNode(db, jacksSuits.ID) 490 | jacksSlacks, _ = findNode(db, jacksSlacks.ID) 491 | jacksHat, _ = findNode(db, jacksHat.ID) 492 | lilysHat, _ = findNode(db, lilysHat.ID) 493 | lilysClothing, _ = findNode(db, lilysClothing.ID) 494 | lilysDresses, _ = findNode(db, lilysDresses.ID) 495 | 496 | assertNodeEqual(t, jacksClothing, 1, 6, 0, 2, 0) 497 | assertNodeEqual(t, jacksSuits, 2, 3, 1, 0, jacksClothing.ID) 498 | assertNodeEqual(t, jacksSlacks, 4, 5, 1, 0, jacksClothing.ID) 499 | assertNodeEqual(t, jacksHat, 7, 8, 0, 0, 0) 500 | assertNodeEqual(t, lilysHat, 1, 2, 0, 0, 0) 501 | assertNodeEqual(t, lilysClothing, 3, 6, 0, 1, 0) 502 | assertNodeEqual(t, lilysDresses, 4, 5, 1, 0, lilysClothing.ID) 503 | } 504 | 505 | func TestMoveToLeft(t *testing.T) { 506 | // case 1 507 | initData() 508 | MoveTo(db, dresses, jackets, MoveDirectionLeft) 509 | reloadCategories() 510 | 511 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 512 | assertNodeEqual(t, mens, 2, 15, 1, 1, clothing.ID) 513 | assertNodeEqual(t, suits, 3, 14, 2, 3, mens.ID) 514 | assertNodeEqual(t, slacks, 4, 5, 3, 0, suits.ID) 515 | assertNodeEqual(t, dresses, 6, 11, 3, 2, suits.ID) 516 | assertNodeEqual(t, eveningGowns, 7, 8, 4, 0, dresses.ID) 517 | assertNodeEqual(t, sunDresses, 9, 10, 4, 0, dresses.ID) 518 | assertNodeEqual(t, jackets, 12, 13, 3, 0, suits.ID) 519 | assertNodeEqual(t, womens, 16, 21, 1, 2, clothing.ID) 520 | assertNodeEqual(t, skirts, 17, 18, 2, 0, womens.ID) 521 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 522 | 523 | // case 2 524 | initData() 525 | MoveTo(db, suits, blouses, MoveDirectionLeft) 526 | reloadCategories() 527 | 528 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 529 | assertNodeEqual(t, mens, 2, 3, 1, 0, clothing.ID) 530 | assertNodeEqual(t, womens, 4, 21, 1, 4, clothing.ID) 531 | assertNodeEqual(t, dresses, 5, 10, 2, 2, womens.ID) 532 | assertNodeEqual(t, eveningGowns, 6, 7, 3, 0, dresses.ID) 533 | assertNodeEqual(t, sunDresses, 8, 9, 3, 0, dresses.ID) 534 | assertNodeEqual(t, skirts, 11, 12, 2, 0, womens.ID) 535 | assertNodeEqual(t, suits, 13, 18, 2, 2, womens.ID) 536 | assertNodeEqual(t, slacks, 14, 15, 3, 0, suits.ID) 537 | assertNodeEqual(t, jackets, 16, 17, 3, 0, suits.ID) 538 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 539 | } 540 | 541 | func TestMoveToInner(t *testing.T) { 542 | // case 1 543 | initData() 544 | MoveTo(db, mens, blouses, MoveDirectionInner) 545 | reloadCategories() 546 | 547 | assertNodeEqual(t, clothing, 1, 22, 0, 1, 0) 548 | assertNodeEqual(t, womens, 2, 21, 1, 3, clothing.ID) 549 | assertNodeEqual(t, dresses, 3, 8, 2, 2, womens.ID) 550 | assertNodeEqual(t, eveningGowns, 4, 5, 3, 0, dresses.ID) 551 | assertNodeEqual(t, sunDresses, 6, 7, 3, 0, dresses.ID) 552 | assertNodeEqual(t, skirts, 9, 10, 2, 0, womens.ID) 553 | assertNodeEqual(t, blouses, 11, 20, 2, 1, womens.ID) 554 | assertNodeEqual(t, mens, 12, 19, 3, 1, blouses.ID) 555 | assertNodeEqual(t, suits, 13, 18, 4, 2, mens.ID) 556 | assertNodeEqual(t, slacks, 14, 15, 5, 0, suits.ID) 557 | assertNodeEqual(t, jackets, 16, 17, 5, 0, suits.ID) 558 | 559 | // case 2 560 | initData() 561 | MoveTo(db, skirts, slacks, MoveDirectionInner) 562 | reloadCategories() 563 | 564 | assertNodeEqual(t, clothing, 1, 22, 0, 2, 0) 565 | assertNodeEqual(t, mens, 2, 11, 1, 1, clothing.ID) 566 | assertNodeEqual(t, suits, 3, 10, 2, 2, mens.ID) 567 | assertNodeEqual(t, slacks, 4, 7, 3, 1, suits.ID) 568 | assertNodeEqual(t, skirts, 5, 6, 4, 0, slacks.ID) 569 | assertNodeEqual(t, jackets, 8, 9, 3, 0, suits.ID) 570 | assertNodeEqual(t, womens, 12, 21, 1, 2, clothing.ID) 571 | assertNodeEqual(t, dresses, 13, 18, 2, 2, womens.ID) 572 | assertNodeEqual(t, eveningGowns, 14, 15, 3, 0, dresses.ID) 573 | assertNodeEqual(t, sunDresses, 16, 17, 3, 0, dresses.ID) 574 | assertNodeEqual(t, blouses, 19, 20, 2, 0, womens.ID) 575 | } 576 | 577 | func TestMoveIsInvalid(t *testing.T) { 578 | initData() 579 | err := MoveTo(db, womens, dresses, MoveDirectionInner) 580 | assert.NotEmpty(t, err) 581 | reloadCategories() 582 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 583 | 584 | err = MoveTo(db, womens, dresses, MoveDirectionLeft) 585 | assert.NotEmpty(t, err) 586 | reloadCategories() 587 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 588 | 589 | err = MoveTo(db, womens, dresses, MoveDirectionRight) 590 | assert.NotEmpty(t, err) 591 | reloadCategories() 592 | assertNodeEqual(t, womens, 10, 21, 1, 3, clothing.ID) 593 | } 594 | 595 | func assertNodeEqual(t *testing.T, target Category, left, right, depth, childrenCount int, parentID int64) { 596 | nullInt64ParentID := sql.NullInt64{Valid: false} 597 | if parentID > 0 { 598 | nullInt64ParentID = sql.NullInt64{Valid: true, Int64: parentID} 599 | } 600 | assert.Equal(t, left, target.Lft) 601 | assert.Equal(t, right, target.Rgt) 602 | assert.Equal(t, depth, target.Depth) 603 | assert.Equal(t, childrenCount, target.ChildrenCount) 604 | assert.Equal(t, nullInt64ParentID, target.ParentID) 605 | } 606 | --------------------------------------------------------------------------------