Skip to content

Commit

Permalink
Update docs
Browse files Browse the repository at this point in the history
  • Loading branch information
tentone committed Aug 5, 2024
1 parent 9d3cc93 commit ebe7354
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 25 deletions.
65 changes: 59 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,24 +30,77 @@
Path HierarchyID `gorm:"unique;not null;"`
}
```
- In some scenarios it might be usefull to also keep a tradicional relationship to the parent.
- This can be done by adding a `ParentID` field to the model.
- It ensures that the tree is consistent and that actions (e.g. delete) are cascaded to the children.
- Some operations might also be easier to perform with the parent relationship.
```go
type Model struct {
gorm.Model
Path HierarchyId `gorm:"unique;not null;"`
ParentID uint `gorm:"index"`
Parent *TestParentsTable `foreignKey:"parent_id;references:id;constraint:OnUpdate:NO ACTION,OnDelete:CASCADE;"`
}
```

## Usage

### Create
- Elements can be added to the tree as regular entries
- Just make sure that the tree indexes are filled correctly, indexes dont need to be sequential.
```go
db.Create(&Table{Path: HierarchyID{1}})
db.Create(&Table{Path: HierarchyID{1, 1}})
db.Create(&Table{Path: HierarchyID{1, 1, 2}})
db.Create(&Table{Path: HierarchyID{Data: []int64{1}}})
db.Create(&Table{Path: HierarchyID{Data: []int64{1, 1}}})
db.Create(&Table{Path: HierarchyID{Data: []int64{1, 1, 2}}})
```

### Get Ancestors
- To get all parents of a node use the `GetAncestors` method.
- The method will return a slice with all the parents of the node. This can be used as param for a query.
```go
db.Model(&Table{}).Where("[path] IN (?)", child.Path.GetAncestors()).Find(&parents)
```
- Its also possible to get parents with the SQL version of the [`GetAncestor`](https://learn.microsoft.com/en-us/sql/t-sql/data-types/getancestor-database-engine?view=sql-server-ver16) method.
- Example on getting the parent of an element.
```go
db.Model(&Table{}).Where("[path] = ?.GetAncestor(1)", child.Path).Find(&parent)
```

### Get Parents
- To get all parents of a node use the `GetParents` method.
### Get Descendants
- To get all children of a node use the [`IsDescendantOf`](https://learn.microsoft.com/en-us/sql/t-sql/data-types/isdescendantof-database-engine?view=sql-server-ver16) method in SQL.
- Example on getting all children of a node (including the node itself).
```go
db.Model(&Table{}).Where("[path] IN (?)", child.Path.GetParents()).Find(&parents)
elements := []Table{}
db.Where("[path].IsDescendantOf(?)=1", HierarchyId{Data: []int64{1, 2}}).Find(&elements)
```
- It is also possible to filter the children based on sub-levels.
- Example on getting all nodes from root where at least one of the sub-level has a name that contains the text 'de'
```sql
SELECT *
FROM "table" as a
WHERE ([path].GetLevel()=1 AND [path].IsDescendantOf('/')=1) AND
(SELECT COUNT(*) FROM "table" AS b WHERE b.path.IsDescendantOf(a.path)=1 AND b.name LIKE '%de%')>0
```
- The `GetLevel` method can be used to filter nodes based on their level in the hierarchy. Also available in SQL with the same name [`GetLevel`](https://learn.microsoft.com/en-us/sql/t-sql/data-types/getlevel-database-engine?view=sql-server-ver16).
- A more generic version of the same code presented above writen in go.

```go
root := GetRoot()
subQuery := db.Table("table AS b").Select("COUNT(*)").Where("[b].[path].IsDescendantOf([a].[path])=1 AND [b].[name] LIKE '%de%'")
conn = db.Table("table AS a").
Where("[a].[path].GetLevel()=? AND [a].[path].IsDescendantOf(?)=1 AND (?)>0", root.GetLevel()+1, root, subQuery).
Find(&elements)
```


### Move nodes
- To move a node to a new parent there is the `GetReparentedValue` method that receives the old parent and new parent and calculates the new hierarchyid value.
- Example on moving a node to a new parent.
```go
db.Model(&Table{}).Where("[id] = ?", id).Update("[path]=?", node.Path.GetReparentedValue(oldParent.Path, newParent.Path))
```

## Resources
- [adamil.net - How the SQL Server hierarchyid data type works (kind of)](http://www.adammil.net/blog/v100_how_the_SQL_Server_hierarchyid_data_type_works_kind_of_.html)
Expand Down
46 changes: 36 additions & 10 deletions db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func TestParents(t *testing.T) {

type TestParentsTable struct {
gorm.Model
Path HierarchyId `gorm:"unique;not null;"`
Path HierarchyId `gorm:"unique;not null;type:hierarchyid;"`
}

_ = db.Migrator().DropTable(&TestParentsTable{})
Expand All @@ -110,14 +110,14 @@ func TestParents(t *testing.T) {
_ = db.Create(&TestParentsTable{Path: HierarchyId{Data: []int64{3}}})

var count int64 = 0
_ = db.Model(&TestParentsTable{}).Where("[path] IN (?)", child.Path.GetParents()).Count(&count)
_ = db.Model(&TestParentsTable{}).Where("[path] IN (?)", child.Path.GetAncestors()).Count(&count)

if count != 3 {
t.Fatal("Expected 3 parents, got", count)
}
}

func TestTreeBuild(t *testing.T) {
func TestGetTreeLevelSearch(t *testing.T) {
dsn := "sqlserver://sa:12345678@localhost:1433?database=test"
db, err := gorm.Open(sqlserver.Open(dsn), &gorm.Config{})
if err != nil {
Expand All @@ -129,9 +129,6 @@ func TestTreeBuild(t *testing.T) {

Path HierarchyId `gorm:"unique;not null;"`

ParentID uint `gorm:"index"`
Parent *TestParentsTable `foreignKey:"parent_id;references:id;constraint:OnUpdate:NO ACTION,OnDelete:CASCADE;"`

Name string `gorm:"not null;"`
}

Expand All @@ -142,12 +139,41 @@ func TestTreeBuild(t *testing.T) {
t.Fatal("Failed to migrate table", err)
}

_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{1, 2, 3, 4}}})
_ = db.Create(&TestParentsTable{Name: "ade", Path: HierarchyId{Data: []int64{1, 2, 3, 4}}})
_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{1, 2, 3}}})
_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{1, 2}}})
_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{1}}})
_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{2}}})
_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{2, 1}}})
_ = db.Create(&TestParentsTable{Name: "a", Path: HierarchyId{Data: []int64{3}}})
_ = db.Create(&TestParentsTable{Name: "b", Path: HierarchyId{Data: []int64{2}}})
_ = db.Create(&TestParentsTable{Name: "b", Path: HierarchyId{Data: []int64{2, 1}}})
_ = db.Create(&TestParentsTable{Name: "c", Path: HierarchyId{Data: []int64{3}}})
_ = db.Create(&TestParentsTable{Name: "c", Path: HierarchyId{Data: []int64{3, 1}}})
_ = db.Create(&TestParentsTable{Name: "cde", Path: HierarchyId{Data: []int64{3, 1, 1}}})

elements := []TestParentsTable{}

// Get all elements that are descendants of '/1/2/'
conn := db.Where("[path].IsDescendantOf(?)=1", HierarchyId{Data: []int64{1, 2}}).Find(&elements)
if conn.Error != nil {
t.Fatal("Failed to query database", conn.Error)
}

if len(elements) != 3 {
t.Fatal("Expected 3 elements, got", len(elements))
}

// Get all elements on the root that have a child with name that contains 'de'
root := GetRoot()

sub := db.Table("test_parents_tables AS b").Select("COUNT(*)").Where("[b].[path].IsDescendantOf([a].[path])=1 AND [b].[name] LIKE '%de%'")

conn = db.Table("test_parents_tables AS a").
Where("[a].[path].GetLevel()=? AND [a].[path].IsDescendantOf(?)=1 AND (?)>0", root.GetLevel()+1, root, sub).
Find(&elements)
if conn.Error != nil {
t.Fatal("Failed to query database", conn.Error)
}

if len(elements) != 2 {
t.Fatal("Expected 2 elements, got", len(elements))
}
}
47 changes: 42 additions & 5 deletions hierarchyid.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,47 @@ func (HierarchyId) GormDBDataType(db *gorm.DB, field *schema.Field) string {
return "hierarchyid"
}

// Get the tree level where this hierarchyid is located.
//
// '/1/2/3/4/' is at level 4, '/1/2/3/' is at level 3, etc.
func (j *HierarchyId) GetLevel() int {
return len(j.Data)
}

// Get the root of the tree '\'.
//
// The root is the hierarchyid with an empty path.
func GetRoot() HierarchyId {
return HierarchyId{Data: []int64{}}
}

// Check if a hierarchyid is a descendant of another hierarchyid
func (j *HierarchyId) IsDescendantOf(parent HierarchyId) bool {
return IsDescendantOf(j.Data, parent.Data)
}

// Get all parents of a hierarchyid.
func (j *HierarchyId) GetParents() []HierarchyId {
// Calculate a new hierarchyid when moving from a parent to another parent in the tree.
//
// The position will be calculated based on the old and new parents.
//
// E.g. if the element is on position '/1/2/57/8/' old parents is '/1/2/' and new parent is '/1/3/' the new position will be '/1/3/57/8/'
func (j *HierarchyId) GetReparentedValue(oldAncestor HierarchyId, newAncestor HierarchyId) HierarchyId {
if !j.IsDescendantOf(oldAncestor) {
return HierarchyId{}
}

path := j.Data
path = append(newAncestor.Data, path[len(oldAncestor.Data):]...)

return HierarchyId{Data: path}
}

// Get all ancestors of a hierarchyid.
//
// E.g. '/1/2/3/4/' will return ['/1/', '/1/2/', '/1/2/3/']
func (j *HierarchyId) GetAncestors() []HierarchyId {
p := []HierarchyId{}
pd := GetParents(j.Data)
pd := GetAncestors(j.Data)

for _, d := range pd {
p = append(p, HierarchyId{Data: d})
Expand All @@ -46,9 +78,14 @@ func (j *HierarchyId) GetParents() []HierarchyId {
return p
}

// Create a string representation of the hierarchyid data type
func (j *HierarchyId) ToString() string {
return ToString(j.Data)
}

// Get the direct parent of a hierarchyid.
func (j *HierarchyId) GetParent() HierarchyId {
return HierarchyId{Data: GetParent(j.Data)}
func (j *HierarchyId) GetAncestor() HierarchyId {
return HierarchyId{Data: GetAncestor(j.Data)}
}

// When marshaling to JSON, we want the field formatted as a string.
Expand Down
13 changes: 9 additions & 4 deletions type.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func ToString(data HierarchyIdData) string {
return r
}

// Get all parents of a hierarchyid.
func GetParents(data HierarchyIdData) []HierarchyIdData {
// Get all ancestors (parents) of a hierarchyid.
func GetAncestors(data HierarchyIdData) []HierarchyIdData {
var parents []HierarchyIdData = []HierarchyIdData{}

for i := 0; i < len(data)-1; i++ {
Expand All @@ -51,8 +51,8 @@ func GetParents(data HierarchyIdData) []HierarchyIdData {
return parents
}

// Get the direct parent of a hierarchyid.
func GetParent(data HierarchyIdData) HierarchyIdData {
// Get the direct ancestor of a hierarchyid.
func GetAncestor(data HierarchyIdData) HierarchyIdData {
if len(data) == 0 {
return []int64{}
}
Expand Down Expand Up @@ -141,6 +141,11 @@ func Decode(data []byte) (HierarchyIdData, error) {

// Encode a hierarchyid from hierarchyid.
func Encode(levels HierarchyIdData) ([]byte, error) {

if len(levels) == 0 {
return []byte{}, nil
}

var bin string = ""

for _, level := range levels {
Expand Down

0 comments on commit ebe7354

Please sign in to comment.