diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66fd13c --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..03dae42 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module forge.redroom.link/yves/sqldb + +go 1.15 + +require github.com/lib/pq v1.9.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a4a764e --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= +github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= diff --git a/pg.go b/pg.go new file mode 100755 index 0000000..952559e --- /dev/null +++ b/pg.go @@ -0,0 +1,323 @@ +package sqldb + +import ( + "database/sql" + "fmt" + "log" + "strconv" + "strings" + + "github.com/lib/pq" +) + +type Db struct { + Driver string + Url string + conn *sql.DB +} + +// AssRow : associative row type +type AssRow map[string]interface{} + +// Select Result +type Rows []AssRow + +// Table is a table structure description +type TableInfo struct { + Name string `json:"name"` + Columns map[string]string `json:"columns"` + db *Db +} + +// Open the database +func Open(driver string, url string) *Db { + var database Db + var err error + database.Driver = driver + database.Url = url + database.conn, err = sql.Open(driver, url) + if err != nil { + log.Println(err) + } + return &database +} + +// Close the database connection +func (db *Db) Close() { + db.conn.Close() +} + +func (db *Db) Table(name string) *TableInfo { + var ti TableInfo + ti.Name = name + ti.db = db + return &ti +} + +// GetAssociativeArray : Provide results as an associative array +func (t *TableInfo) GetAssociativeArray(columns []string, restriction string, sortkeys []string, dir string) ([]AssRow, error) { + return t.db.QueryAssociativeArray(t.buildSelect("", columns, restriction, sortkeys, dir)) +} + +// QueryAssociativeArray : Provide results as an associative array +func (db *Db) QueryAssociativeArray(query string) (Rows, error) { + rows, err := db.conn.Query(query) + if err != nil { + log.Println(err) + log.Println(query) + return nil, err + } + defer rows.Close() + + var results Rows + cols, err := rows.Columns() + if err != nil { + log.Println(err) + log.Println(query) + return nil, err + } + for rows.Next() { + // Create a slice of interface{}'s to represent each column, + // and a second slice to contain pointers to each item in the columns slice. + columns := make([]interface{}, len(cols)) + columnPointers := make([]interface{}, len(cols)) + for i := range columns { + columnPointers[i] = &columns[i] + } + + // Scan the result into the column pointers... + if err := rows.Scan(columnPointers...); err != nil { + + } + // Create our map, and retrieve the value for each column from the pointers slice, + // storing it in the map with the name of the column as the key. + m := make(AssRow) + for i, colName := range cols { + val := columnPointers[i].(*interface{}) + m[colName] = fmt.Sprintf("%v", *val) + } + + results = append(results, m) + + } + return results, nil +} + +// GetSchema : Provide results as an associative array +func (t *TableInfo) GetSchema() (*TableInfo, error) { + var ti TableInfo + ti.Name = t.Name + ti.db = t.db + cols, err := t.db.QueryAssociativeArray("SELECT column_name :: varchar as name, REPLACE(REPLACE(data_type,'character varying','varchar'),'character','char') || COALESCE('(' || character_maximum_length || ')', '') as type from INFORMATION_SCHEMA.COLUMNS where table_name ='" + t.Name + "';") + if err != nil { + log.Println(err) + return nil, err + } + ti.Columns = make(map[string]string) + for _, row := range cols { + var name, rowtype string + for key, element := range row { + if key == "name" { + name = fmt.Sprintf("%v", element) + } + if key == "type" { + rowtype = fmt.Sprintf("%v", element) + } + } + ti.Columns[name] = rowtype + } + return &ti, nil +} + +func (db *Db) ListTables() (Rows, error) { + return db.QueryAssociativeArray("SELECT table_name :: varchar FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;") +} + +func (db *Db) CreateTable(t TableInfo) error { + t.db = db + query := "create table " + t.Name + " ( " + columns := "" + for name, rowtype := range t.Columns { + if fmt.Sprintf("%v", name) == "id" { + columns += fmt.Sprintf("%v", name) + " " + "SERIAL PRIMARY KEY," + } else { + + columns += fmt.Sprintf("%v", name) + " " + fmt.Sprintf("%v", rowtype) + columns += "," + } + } + query += columns + query = query[:len(query)-1] + " )" + _, err := t.db.conn.Query(query) + if err != nil { + log.Println(err.Error()) + return err + } + return nil +} + +func (t *TableInfo) DeleteTable() error { + query := "drop table " + t.Name + _, err := t.db.conn.Query(query) + if err != nil { + log.Println(err.Error()) + return err + } + query = "drop sequence if exists sq_" + t.Name + _, err = t.db.conn.Query(query) + if err != nil { + log.Println(err.Error()) + return err + } + return nil +} + +func (t *TableInfo) AddColumn(name string, sqltype string) error { + query := "alter table " + t.Name + " add " + name + " " + sqltype + rows, err := t.db.conn.Query(query) + if err != nil { + log.Println(err) + return err + } + defer rows.Close() + return nil +} + +func (t *TableInfo) DeleteColumn(name string) error { + query := "alter table " + t.Name + " drop " + name + rows, err := t.db.conn.Query(query) + if err != nil { + log.Println(err) + return err + } + defer rows.Close() + return nil +} + +func (db *Db) ListSequences() (Rows, error) { + return db.QueryAssociativeArray("SELECT sequence_name :: varchar FROM information_schema.sequences WHERE sequence_schema = 'public' ORDER BY sequence_name;") +} + +func (t *TableInfo) buildSelect(key string, columns []string, restriction string, sortkeys []string, dir ...string) string { + if key != "" { + columns = append(columns, key) + } + query := "select " + strings.Join(columns, ",") + " from " + t.Name + if restriction != "" { + query += " where " + restriction + } + if len(sortkeys) > 0 { + query += " order by " + strings.Join(sortkeys, ",") + } + if len(dir) > 0 { + query += " " + dir[0] + } + return query +} + +func (t *TableInfo) Insert(record AssRow) (int, error) { + columns := "" + values := "" + t, err := t.GetSchema() + if err != nil { + log.Println(err) + return -1, err + } + var id int + + for key, element := range record { + + if strings.Contains(t.Columns[key], "char") || strings.Contains(t.Columns[key], "date") { + columns += key + "," + values += fmt.Sprint(pq.QuoteLiteral(fmt.Sprintf("%v", element))) + "," + } else { + + columns += key + "," + values += fmt.Sprintf("%v", element) + "," + } + } + + t.db.conn.QueryRow("INSERT INTO " + t.Name + "(" + removeLastChar(columns) + ") VALUES (" + removeLastChar(values) + ") RETURNING id").Scan(&id) + return id, nil +} + +func (t *TableInfo) Update(record AssRow) error { + + t, err := t.GetSchema() + if err != nil { + log.Println(err) + return err + } + id := "" + stack := "" + + for key, element := range record { + + if strings.Contains(t.Columns[key], "char") || strings.Contains(t.Columns[key], "date") { + + stack = stack + " " + key + " = " + pq.QuoteLiteral(fmt.Sprintf("%v", element)) + "," + + } else { + + if key == "id" { + id = fmt.Sprintf("%v", element) + } else { + stack = stack + " " + key + " = " + fmt.Sprintf("%v", element) + "," + } + } + } + stack = removeLastChar(stack) + query := ("UPDATE " + t.Name + " SET " + stack + " WHERE id = " + id) + rows, err := t.db.conn.Query(query) + if err != nil { + log.Println(query) + log.Println(err) + return err + } + defer rows.Close() + return nil +} + +func (t *TableInfo) Delete(record AssRow) error { + id := "" + values := "" + + for key, element := range record { + if key == "id" { + values += fmt.Sprintf("%v", element) + "," + id = removeLastChar(values) + + } + } + query := ("DELETE FROM " + t.Name + " WHERE id = " + id) + rows, err := t.db.conn.Query(query) + if err != nil { + log.Println(query) + log.Println(err) + return err + } + defer rows.Close() + return nil +} + +func (t *TableInfo) UpdateOrInsert(record AssRow) (int, error) { + id := -1 + for key, element := range record { + if key == "id" { + sid := fmt.Sprintf("%v", element) + id, _ = strconv.Atoi(sid) + } + } + if id == -1 { + return t.Insert(record) + } else { + t.Update(record) + return id, nil + } + +} + +func removeLastChar(s string) string { + r := []rune(s) + return string(r[:len(r)-1]) +} diff --git a/pg_init.sql b/pg_init.sql new file mode 100644 index 0000000..96ec4d0 --- /dev/null +++ b/pg_init.sql @@ -0,0 +1,7 @@ +-- Init on Linux : +-- 1) login as postgres user : sudo su postgres +-- 2) start script : psql -f pg_init.sql + +CREATE ROLE test WITH PASSWORD 'test' NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT LOGIN; +CREATE DATABASE test OWNER test encoding 'UTF8'; +GRANT ALL PRIVILEGES ON DATABASE test TO test; diff --git a/pg_test.go b/pg_test.go new file mode 100755 index 0000000..32475c4 --- /dev/null +++ b/pg_test.go @@ -0,0 +1,184 @@ +package sqldb + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "testing" +) + +func TestCreateTable(t *testing.T) { + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + jsonFile, err := os.Open("test_table.json") + if err != nil { + fmt.Println(err) + } + defer jsonFile.Close() + + byteValue, _ := ioutil.ReadAll(jsonFile) + + var jsonSource TableInfo + json.Unmarshal([]byte(byteValue), &jsonSource) + + err = db.CreateTable(jsonSource) + if err != nil { + fmt.Println(err.Error()) + } + + sch, err := db.Table("test").GetSchema() + if err != nil { + fmt.Println(err.Error()) + } + if len(sch.Columns) == 0 { + t.Errorf("Create table failed") + } +} + +func TestAddColumn(t *testing.T) { + + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + old, err := db.Table("test").GetSchema() + if err != nil { + fmt.Println(err.Error()) + } + db.Table("test").AddColumn("addcolumn", "integer") + new, err := db.Table("test").GetSchema() + if err != nil { + fmt.Println(err.Error()) + } + + if len(old.Columns) == len(new.Columns) { + t.Errorf("Column already exist") + } +} + +func TestInsert(t *testing.T) { + + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + vl := make(AssRow) + vl["name"] = "toto" + vl["description"] = "tata" + + old, err := db.Table("test").GetAssociativeArray([]string{"*"}, "", []string{}, "") + if err != nil { + fmt.Println(err.Error()) + } + jsonStringOld, _ := json.Marshal(old) + fmt.Println(string(jsonStringOld)) + + db.Table("test").UpdateOrInsert(vl) + + new, err := db.Table("test").GetAssociativeArray([]string{"*"}, "", []string{}, "") + if err != nil { + fmt.Println(err.Error()) + } + jsonStringNew, _ := json.Marshal(new) + fmt.Println(string(jsonStringNew)) + + if len(jsonStringOld) == len(jsonStringNew) { + t.Errorf("Error row not created") + } +} + +func TestUpdate(t *testing.T) { + + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + vl := make(AssRow) + vl["id"] = 1 + vl["name"] = "titi" + vl["description"] = "toto" + + old, err := db.Table("test").GetAssociativeArray([]string{"*"}, "", []string{}, "") + if err != nil { + fmt.Println(err.Error()) + } + jsonStringOld, _ := json.Marshal(old) + fmt.Println(string(jsonStringOld)) + + db.Table("test").UpdateOrInsert(vl) + + new, err := db.Table("test").GetAssociativeArray([]string{"*"}, "", []string{}, "") + if err != nil { + fmt.Println(err.Error()) + } + jsonStringNew, _ := json.Marshal(new) + fmt.Println(string(jsonStringNew)) + + if string(jsonStringOld) == string(jsonStringNew) { + t.Errorf("Error row not updated") + } + +} + +func TestDelete(t *testing.T) { + + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + vl := make(AssRow) + vl["id"] = 1 + + old, err := db.Table("test").GetAssociativeArray([]string{"*"}, "", []string{}, "") + if err != nil { + fmt.Println(err.Error()) + } + jsonStringOld, _ := json.Marshal(old) + fmt.Println(string(jsonStringOld)) + + db.Table("test").Delete(vl) + + new, err := db.Table("test").GetAssociativeArray([]string{"*"}, "", []string{}, "") + if err != nil { + fmt.Println(err.Error()) + } + jsonStringNew, _ := json.Marshal(new) + fmt.Println(string(jsonStringNew)) + + if len(jsonStringOld) == len(jsonStringNew) { + t.Errorf("Error row not deleted") + } +} + +func TestDeleteColumn(t *testing.T) { + + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + old, err := db.Table("test").GetSchema() + if err != nil { + fmt.Println(err.Error()) + } + db.Table("test").DeleteColumn("addcolumn") + new, err := db.Table("test").GetSchema() + if err != nil { + fmt.Println(err.Error()) + } + + if len(old.Columns) == len(new.Columns) { + t.Errorf("Error column not deleted") + } +} + +func TestDeleteTable(t *testing.T) { + db := Open("postgres", "host=127.0.0.1 port=5432 user=test password=test dbname=test sslmode=disable") + defer db.Close() + + db.Table("test").DeleteTable() + + tbl, err := db.Table("test").GetSchema() + if err != nil { + fmt.Println(err.Error()) + } + if len(tbl.Columns) != 0 { + t.Errorf("Delete table failed") + } +} diff --git a/test_table.json b/test_table.json new file mode 100644 index 0000000..507166b --- /dev/null +++ b/test_table.json @@ -0,0 +1,17 @@ +{ + "name":"test", + "columns": + { + "id":"integer", + "name":"varchar(255)", + "description":"varchar(1000)", + "startdate":"timestamp", + "enddate":"timestamp", + "latitude":"float", + "longitude":"float", + "intvalue":"integer", + "floatvalue":"float", + "price":"money", + "testtype_id":"integer" + } +} \ No newline at end of file diff --git a/testtype_table.json b/testtype_table.json new file mode 100644 index 0000000..63a00db --- /dev/null +++ b/testtype_table.json @@ -0,0 +1,17 @@ +{ + "name":"testtype", + "columns":[ + { + "name":"id", + "type":"integer" + }, + { + "name":"name", + "type":"varchar(255)" + }, + { + "name":"detail", + "type":"varchar(255)" + } + ] +} \ No newline at end of file