package synchronizator import ( sql "database/sql" "encoding/json" "fmt" "os" "time" ) type db struct { Connection *sql.DB logger *os.File log_level LogLevel drop_tables bool } type Options struct { Logger *os.File Log_level LogLevel DANGEROUSLY_DROP_TABLES bool } type LogLevel int // Lower levels take precedence //go:generate stringer -type=LogLevel const ( ERROR LogLevel = iota INFO DEBUG ) var DefaultOptions = &Options{ Logger: os.Stdout, Log_level: INFO, DANGEROUSLY_DROP_TABLES: false, } func New(connection *sql.DB, options *Options) (*db, error) { if options == nil { options = DefaultOptions } conn := db{ Connection: connection, logger: options.Logger, log_level: options.Log_level, drop_tables: options.DANGEROUSLY_DROP_TABLES, } err := conn.bootstrap() if err != nil { return nil, err } return &conn, nil } func (conn *db) log(level LogLevel, args ...any) { // Only log if the level is the same or lower than the configured if level > conn.log_level { return } fmt.Fprintf(conn.logger, "[ %s - %-5s ]: ", time.Now().Format(time.DateTime), level.String()) fmt.Fprintln(conn.logger, args...) } func (conn *db) bootstrap() error { conn.log(INFO, "Initializing database...") sql := "" if conn.drop_tables { sql += ` DROP TABLE IF EXISTS nodes; DROP TABLE IF EXISTS relationships; DROP INDEX IF EXISTS node_class; DROP INDEX IF EXISTS relationships_class; ` } sql += ` CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, _class text NOT NULL, name TEXT, metadata jsonb DEFAULT '{}' ); CREATE INDEX IF NOT EXISTS node_class on nodes (_class); CREATE TABLE IF NOT EXISTS relationships ( node_from INTEGER NOT NULL, node_to INTEGER NOT NULL, _class text NOT NULL, metadata jsonb DEFAULT '{}', PRIMARY KEY (node_from, node_to), CHECK (node_from != node_to), CONSTRAINT fk_node_from_relationships FOREIGN KEY (node_from) REFERENCES nodes(id), CONSTRAINT fk_node_to_relationships FOREIGN KEY (node_to) REFERENCES nodes(id) ); CREATE INDEX IF NOT EXISTS relationships_class on relationships (_class); ` conn.log(DEBUG, sql) _, err := conn.Connection.Exec(sql) if err != nil { return err } return nil } func (conn *db) NewCollection(data NodeClass) (*Collection, error) { node, err := conn.NewNode(data) if err != nil { return nil, err } collection := &Collection{ Node: *node, childs: make([]*Node, 0), } return collection, nil } func (conn *db) NewNode(data NodeClass) (*Node, error) { class, name, metadata, err := data.ToNode() if err != nil { return nil, err } node := Node{ _conn: conn, _class: class, name: name, metadata: metadata, Id: -1, } tx, err := conn.Connection.Begin() if err != nil { return nil, err } defer tx.Rollback() conn.log(DEBUG, "Creating node:", node) sql := "INSERT INTO nodes (_class, name, metadata) VALUES ($1, $2, $3) RETURNING id;" err = tx.QueryRow(sql, node._class, node.name, metadata).Scan(&node.Id) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return &node, nil } func (conn *db) UpdateNode(id int64, data NodeClass) (*Node, error) { class, name, metadata, err := data.ToNode() if err != nil { return nil, err } node := Node{ _conn: conn, _class: class, name: name, metadata: metadata, Id: id, } tx, err := conn.Connection.Begin() if err != nil { return nil, err } defer tx.Rollback() conn.log(DEBUG, "Updating node:", id, node) sql := "UPDATE nodes SET _class = $1, name = $2, metadata = $3 WHERE id = $4;" _, err = tx.Exec(sql, node._class, node.name, node.metadata, id) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return &node, nil } func (conn *db) GetNode(id int64) (*Node, error) { node := Node{Id: id} sql := "SELECT _class, metadata FROM nodes WHERE id = $1;" conn.log(DEBUG, sql) var metadata []byte err := conn.Connection.QueryRow(sql, id).Scan(&node._class, &metadata) if err != nil { conn.log(DEBUG, err) return nil, fmt.Errorf("No row matching id = %v", id) } if err := json.Unmarshal(metadata, &node.metadata); err != nil { conn.log(ERROR, err) return nil, fmt.Errorf("invalid metadata format: %w", err) } return &node, nil } func (conn *db) DeleteNode(id int64) error { tx, err := conn.Connection.Begin() if err != nil { return err } defer tx.Rollback() conn.log(DEBUG, "Deleting node:", id) sql := "DELETE FROM nodes WHERE id = $1;" _, err = tx.Exec(sql, id) if err != nil { return err } if err := tx.Commit(); err != nil { return err } return nil } func (conn *db) AddRelation( from int64, data RelationshipClass, to int64, ) (*Relationship, error) { class, metadata, err := data.ToRelationship() if err != nil { return nil, err } relationship := Relationship{ _conn: conn, _class: class, Metadata: metadata, From: from, To: to, } tx, err := conn.Connection.Begin() if err != nil { return nil, err } defer tx.Rollback() conn.log(DEBUG, "Creating relationship:", from, relationship, to) 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) if err != nil { return nil, err } if err := tx.Commit(); err != nil { return nil, err } return &relationship, nil } func (conn *db) UpdateRelation(from int64, metadata any, to int64) error { if metadata == nil { return fmt.Errorf("metadata cannot be nil") } json_metadata, err := json.Marshal(metadata) if err != nil { return fmt.Errorf("invalid metadata format: %w", err) } tx, err := conn.Connection.Begin() if err != nil { return err } defer tx.Rollback() conn.log(DEBUG, "Updating relationship:", from, metadata, to) sql := "UPDATE relationships SET metadata = $1 WHERE node_from = $2 AND node_to = $3;" _, err = tx.Exec(sql, json_metadata, from, to) if err != nil { return err } if err := tx.Commit(); err != nil { return err } return nil } func (conn *db) DeleteRelation(from int64, to int64) error { tx, err := conn.Connection.Begin() if err != nil { return err } defer tx.Rollback() sql := "DELETE FROM relationships WHERE node_from = $1 AND node_to = $2;" conn.log(DEBUG, "Deleting relationship:", from, to) _, err = tx.Exec(sql, from, to) if err != nil { return nil } if err := tx.Commit(); err != nil { return err } return nil }