diff --git a/examples/usage.go b/examples/usage.go index dbf380b..86a4be0 100644 --- a/examples/usage.go +++ b/examples/usage.go @@ -134,7 +134,7 @@ func main() { defer connection.Close() opts := synchronizator.DefaultOptions - // opts.Log_level = synchronizator.DEBUG + opts.Log_level = synchronizator.DEBUG opts.DANGEROUSLY_DROP_TABLES = true sync, err := synchronizator.New(connection, opts) diff --git a/pkg/collection.go b/pkg/collection.go index e4dfa05..d3942fc 100644 --- a/pkg/collection.go +++ b/pkg/collection.go @@ -10,9 +10,13 @@ type default_collection struct { platform_name string } -func (collection *default_collection) ToNode() (string, []byte, error) { +func (collection *default_collection) GetClass() string { platform_name := strings.ToUpper(collection.platform_name) - return platform_name + "_DEFAULT", nil, nil + return platform_name + "_DEFAULT" +} + +func (collection *default_collection) GetMetadata() []byte { + return nil } func (collection *default_collection) FromNode(_class string, name string, metadata []byte) error { @@ -46,13 +50,19 @@ func NewCollection(name string, metadata []byte) *Collection { } // Internal RelationshipClass to handle the collection to node relationship -type collection_relation struct{} - -func (collection *collection_relation) ToRelationship() (string, []byte, error) { - return "COLLECTION_HAS", nil, nil +type collection_has_node struct { + Relationship } -func (collection *collection_relation) FromRelationship(_class string, metadata []byte) error { +func (col_has_node *collection_has_node) GetClass() string { + return "COLLECTION_HAS" +} + +func (col_has_node *collection_has_node) GetMetadata() []byte { + return nil +} + +func (col_has_node *collection_has_node) FromRelationship(_class string, metadata []byte) error { if _class != "COLLECTION_HAS" { return fmt.Errorf("invalid class %s", _class) } @@ -62,7 +72,7 @@ func (collection *collection_relation) FromRelationship(_class string, metadata // Adds a new child to this collection. Use the underlaying node's AddRelation // method. func (collection *Collection) AddChild(node *Node) error { - _, err := collection.AddRelation(&collection_relation{}, node.Id) + _, err := collection.StoreRelation(&collection_has_node{}, node.Id) if err != nil { return err } diff --git a/pkg/fetcher.go b/pkg/fetcher.go new file mode 100644 index 0000000..7c17c24 --- /dev/null +++ b/pkg/fetcher.go @@ -0,0 +1,17 @@ +package synchronizator + +type Fetcher = func(pagination Pagination) ([]*Collection, Pagination, error) + +type Pagination struct { + Total int + HasMore bool + Limit int + Offset int +} + +var StartPagination = Pagination{ + Total: 0, + HasMore: false, + Limit: 10, + Offset: 0, +} diff --git a/pkg/node.go b/pkg/node.go index 9ce83a0..93896f6 100644 --- a/pkg/node.go +++ b/pkg/node.go @@ -1,10 +1,18 @@ package synchronizator +import ( + "fmt" + "slices" +) + type StandardNode interface { GetClass() string GetName() string GetMetadata() []byte + AddRelationship(relationship StandardRelationship) error + AddRelationships(to int64, relationship []StandardRelationship) + SetId(int64) SetConnection(*DB) } @@ -12,18 +20,18 @@ type StandardNode interface { // A node in the database. // It adds some helper methods to easily manage the node. type Node struct { - _conn *DB // Underlaying connection to the database. - _class string // The class of the node, should not be modified to avoid inconsistencies. - _relationships []*Relationship // Relationships of the node - Id int64 // The id of the node - name string // The name of the node - metadata []byte // Arbitrary data. This is stored as a jsonb in the database + _conn *DB // Underlaying connection to the database. + _class string // The class of the node, should not be modified to avoid inconsistencies. + _relationships []StandardRelationship // Relationships of the node + Id int64 // The id of the node + name string // The name of the node + metadata []byte // Arbitrary data. This is stored as a jsonb in the database } func NewNode(name string, metadata []byte) *Node { return &Node{ name: name, - _class: "COLLECTION", + _class: "NODE", metadata: metadata, Id: -1, // Use -1 to indicate not persisted } @@ -41,6 +49,26 @@ func (node *Node) GetMetadata() []byte { return node.metadata } +func (node *Node) AddRelationship(relationship StandardRelationship) error { + node1, node2 := relationship.GetNodes() + + if node1 != node.Id && node2 != node.Id { + return fmt.Errorf( + "The current node (%v) is neither used as the source (%v), nor the destination (%v)", + node.Id, + node1, + node2, + ) + } + node._relationships = append(node._relationships, relationship) + + return nil +} + +func (node *Node) AddRelationships(to int64, relationships []StandardRelationship) { + node._relationships = slices.Concat(node._relationships, relationships) +} + func (node *Node) SetId(id int64) { node.Id = id } @@ -53,7 +81,7 @@ func (node *Node) SetConnection(conn *DB) { // provided id. An error is returned if the relation already exists. // // This method is a wrapper around the AddRelation method of the connection. -func (node *Node) AddRelation(relation StandardRelationship, to int64) (*Relationship, error) { +func (node *Node) StoreRelation(relation StandardRelationship, to int64) (*Relationship, error) { return node._conn.AddRelation(node.Id, relation, to) } @@ -74,7 +102,7 @@ func (node *Node) DeleteRelation(to int64) error { // Fetch all the outgoing relations for this node. This method will return a // slice of relationships pointers and also store them in the node for further // use. -func (node *Node) GetOutRelations() ([]*Relationship, error) { +func (node *Node) GetOutRelations() ([]StandardRelationship, error) { sql := ` WITH RECURSIVE NodeRelationships AS ( SELECT @@ -103,13 +131,13 @@ FROM defer rows.Close() - node._relationships = make([]*Relationship, 0) + node._relationships = make([]StandardRelationship, 0) for rows.Next() { relationship := &Relationship{ _conn: node._conn, } - if err := rows.Scan(&relationship.From, &relationship._class, &relationship.Metadata, &relationship.To); err != nil { + if err := rows.Scan(&relationship.From, &relationship._class, relationship.GetMetadata(), &relationship.To); err != nil { return nil, err } @@ -124,8 +152,3 @@ FROM func (node *Node) Delete() error { return node._conn.DeleteNode(node.Id) } - -// Returns the class of the node -func (relationship *Relationship) GetClass() string { - return relationship._class -} diff --git a/pkg/platform.go b/pkg/platform.go index ca0f5df..0b6a304 100644 --- a/pkg/platform.go +++ b/pkg/platform.go @@ -2,23 +2,6 @@ package synchronizator import "slices" -type PlatformClass interface { - // How to transform the struct into a node. It needs to return the class, - // name and a []byte representation of the metadata. - // - // - name: A user friendly name - // - metadata: Arbitrary data. This will be stored as a jsonb in the database - // - ToNode() (string, []byte, error) - - // How to transform a node into the struct. This method should modify the - // struct directly as it receives a pointer. - // - // - name: A user friendly name - // - metadata: Arbitrary data. This is stored as a jsonb in the database - FromNode(string, []byte) error -} - // Utility struct to represent a collection of nodes, it's a [Node] itself so all // the node's functionality is available. type Platform struct { @@ -26,22 +9,6 @@ type Platform struct { Collections []*Collection // Child nodes } -type Fetcher = func(pagination Pagination) ([]*Collection, Pagination, error) - -type Pagination struct { - Total int - HasMore bool - Limit int - Offset int -} - -var StartPagination = Pagination{ - Total: 0, - HasMore: false, - Limit: 10, - Offset: 0, -} - func (platform *Platform) FetchCollections(fetcher Fetcher, start_pagination Pagination) error { collections, pagination, err := fetcher(start_pagination) if err != nil { @@ -54,6 +21,26 @@ func (platform *Platform) FetchCollections(fetcher Fetcher, start_pagination Pag } err = BulkCreateNode(platform._conn, platform.Collections) + if err != nil { + return err + } + + for _, item := range platform.Collections { + err := platform.AddRelationship( + &Relationship{ + _class: "PLATFORM_HAS_COLLECTION", + From: platform.Id, + To: item.Id, + }) + if err != nil { + return err + } + } + + err = BulkCreateRelationships(platform._conn, platform._relationships) + if err != nil { + return err + } return nil } diff --git a/pkg/relationship.go b/pkg/relationship.go index d1adb09..8c7128d 100644 --- a/pkg/relationship.go +++ b/pkg/relationship.go @@ -19,20 +19,11 @@ package synchronizator // return nil // } type StandardRelationship interface { - // How to transform the struct into a collection. It needs to return the class, - // and a []byte representation of the metadata. - // - // - class: Is used for classification and query pourposes. It's recomended to provide a constante string to increase consistency. - // - metadata: Arbitrary data. This will be stored as a jsonb in the database - // - ToRelationship() (string, []byte, error) + GetClass() string + GetNodes() (int64, int64) + GetMetadata() []byte - // How to transform a relationship into the struct. This method should modify the - // struct directly as it receives a pointer. - // - // - class: Is used for classification and query pourposes. - // - metadata: Arbitrary data. This is stored as a jsonb in the database - FromRelationship(string, []byte) error + SetConnection(*DB) } // A relationship in the database. @@ -42,5 +33,21 @@ type Relationship struct { _class string // The class of the node, should not be modified to avoid inconsistencies. From int64 // From what node this relation comes from To int64 // To what node this relation goes to - Metadata []byte // Arbitrary data. This is stored as a jsonb in the database + metadata []byte // Arbitrary data. This is stored as a jsonb in the database +} + +func (relation *Relationship) GetClass() string { + return relation._class +} + +func (relation *Relationship) GetMetadata() []byte { + return relation.metadata +} + +func (relation *Relationship) GetNodes() (int64, int64) { + return relation.From, relation.To +} + +func (relation *Relationship) SetConnection(db *DB) { + relation._conn = db } diff --git a/pkg/synchronizator.go b/pkg/synchronizator.go index f1bfb93..a2f5e91 100644 --- a/pkg/synchronizator.go +++ b/pkg/synchronizator.go @@ -193,7 +193,7 @@ func (conn *DB) NewPlatform(name string, metadata []byte) (*Platform, error) { Collections: []*Collection{collection}, } - _, err = conn.addRelationwithTx(tx, platform.Id, &collection_relation{}, collection.Id) + _, err = conn.addRelationwithTx(tx, platform.Id, &collection_has_node{}, collection.Id) if err != nil { return err } @@ -381,15 +381,13 @@ func (conn *DB) addRelationwithTx( data StandardRelationship, to int64, ) (*Relationship, error) { - class, metadata, err := data.ToRelationship() - if err != nil { - return nil, err - } + class := data.GetClass() + metadata := data.GetMetadata() relationship := Relationship{ _conn: conn, _class: class, - Metadata: metadata, + metadata: metadata, From: from, To: to, } @@ -398,7 +396,7 @@ func (conn *DB) addRelationwithTx( sql := "INSERT INTO relationships (_class, node_from, node_to, metadata) VALUES ($1, $2, $3, $4) RETURNING node_from, node_to;" - _, err = tx.Exec(sql, relationship._class, relationship.From, relationship.To, metadata) + _, err := tx.Exec(sql, relationship._class, relationship.From, relationship.To, metadata) if err != nil { return nil, err } @@ -518,3 +516,66 @@ func BulkCreateNode[T StandardNode]( return rows.Err() } + +func BulkCreateRelationships[T StandardRelationship]( + conn *DB, + relationships []T, +) error { + if len(relationships) == 0 { + return nil + } + + tx, err := conn.Connection.Begin() + if err != nil { + return err + } + + defer tx.Rollback() + + // Build the query dynamically based on number of nodes + valueStrings := make([]string, 0, len(relationships)) + valueArgs := make([]interface{}, 0, len(relationships)*3) + + for i := range relationships { + n := i * 4 + valueStrings = append( + valueStrings, + fmt.Sprintf("($%d, $%d, $%d, $%d) ", n+1, n+2, n+3, n+4), + ) + + node1, node2 := relationships[i].GetNodes() + class := relationships[i].GetClass() + metadata := relationships[i].GetMetadata() + relationships[i].SetConnection(conn) + + if class == "" || node1 <= 0 || node2 <= 0 { + return fmt.Errorf("Invalid relationship: %v, %v, %v", class, node1, node2) + } + + valueArgs = append( + valueArgs, + class, + node1, + node2, + metadata, + ) + } + + sql := fmt.Sprintf(` + INSERT INTO relationships + (_class, node_from, node_to, metadata) + VALUES %s + `, strings.Join(valueStrings, ",")) + + conn.log(DEBUG, "Bulk creating relationships:", sql, valueArgs) + + // Execute and scan returned IDs + _, err = tx.Exec(sql, valueArgs...) + if err != nil { + return fmt.Errorf("bulk insert failed: %w", err) + } + + tx.Commit() + + return nil +}