diff --git a/x/README.md b/x/README.md new file mode 100644 index 0000000..434a2b1 --- /dev/null +++ b/x/README.md @@ -0,0 +1,3 @@ +## x + +Experimental packages. diff --git a/x/postgres/README.md b/x/postgres/README.md new file mode 100644 index 0000000..28878ec --- /dev/null +++ b/x/postgres/README.md @@ -0,0 +1,5 @@ +# postgres + +Experimenting with predicates that use Go database/sql to query relational databases. + +Currently just supports Postgres but should be applicable to other DBs as well. diff --git a/x/postgres/conn.go b/x/postgres/conn.go new file mode 100644 index 0000000..99e90a4 --- /dev/null +++ b/x/postgres/conn.go @@ -0,0 +1,32 @@ +package postgres + +import ( + "database/sql" + "sync" + "sync/atomic" +) + +var ( + currentID = new(int64) + connections sync.Map // connection id → *sql.DB +) + +func nextID() int64 { + return atomic.AddInt64(currentID, 1) +} + +func getConn(id int64) *sql.DB { + db, ok := connections.Load(id) + if !ok { + return nil + } + return db.(*sql.DB) +} + +func setConn(id int64, db *sql.DB) { + connections.Store(id, db) +} + +func deleteConn(id int64) { + connections.Delete(id) +} diff --git a/x/postgres/go.mod b/x/postgres/go.mod new file mode 100644 index 0000000..ad5f975 --- /dev/null +++ b/x/postgres/go.mod @@ -0,0 +1,13 @@ +module github.com/trealla-prolog/go/x/postgres + +go 1.20 + +require ( + github.com/lib/pq v1.10.9 + github.com/trealla-prolog/go v0.13.9 +) + +require ( + github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect +) diff --git a/x/postgres/go.sum b/x/postgres/go.sum new file mode 100644 index 0000000..b4977e7 --- /dev/null +++ b/x/postgres/go.sum @@ -0,0 +1,12 @@ +github.com/bytecodealliance/wasmtime-go/v8 v8.0.0 h1:jP4sqm2PHgm3+eQ50zCoCdIyQFkIL/Rtkw6TT8OYPFI= +github.com/bytecodealliance/wasmtime-go/v8 v8.0.0/go.mod h1:tgazNLU7xSC2gfRAM8L4WyE+dgs5yp9FF5/tGebEQyM= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/trealla-prolog/go v0.13.9 h1:ZZesdRT+RjEXWEaDI4extKFIAbmExUUOVvw5NvXxZAI= +github.com/trealla-prolog/go v0.13.9/go.mod h1:PnMv/imG6iuoon0QUP/gSAqtHWeA55iV05W2dtOLn9U= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/x/postgres/library.go b/x/postgres/library.go new file mode 100644 index 0000000..04c1229 --- /dev/null +++ b/x/postgres/library.go @@ -0,0 +1,26 @@ +package postgres + +import ( + "context" + "fmt" + + "github.com/trealla-prolog/go/trealla" +) + +var predicates = []struct { + name string + arity int + proc trealla.Predicate +}{ + {"postgres_open_url", 2, open_url_2}, + {"postgres_execute", 4, execute_4}, +} + +func Register(ctx context.Context, pl trealla.Prolog) error { + for _, pred := range predicates { + if err := pl.Register(ctx, pred.name, pred.arity, pred.proc); err != nil { + return fmt.Errorf("failed to register predicate %s/%d: %w", pred.name, pred.arity, err) + } + } + return nil +} diff --git a/x/postgres/postgres.go b/x/postgres/postgres.go new file mode 100644 index 0000000..bedc6b0 --- /dev/null +++ b/x/postgres/postgres.go @@ -0,0 +1,74 @@ +package postgres + +import ( + "database/sql" + + _ "github.com/lib/pq" + + "github.com/trealla-prolog/go/trealla" + "github.com/trealla-prolog/go/trealla/terms" +) + +func open_url_2(pl trealla.Prolog, _ trealla.Subquery, goal trealla.Term) trealla.Term { + pi := terms.PI(goal) + g, ok := goal.(trealla.Compound) + if !ok { + return terms.Throw(terms.TypeError("compound", goal, pi)) + } + + connStr, ok := g.Args[0].(string) + if !ok { + return terms.Throw(terms.TypeError("chars", g.Args[0], pi)) + } + + _, ok = g.Args[1].(trealla.Variable) + if !ok { + return terms.Throw(trealla.Atom("error").Of(trealla.Atom("uninstantiation_error").Of(g.Args[1]), pi)) + } + + db, err := sql.Open("postgres", connStr) + if err != nil { + return terms.Throw(dbError(err, pi)) + } + + id := nextID() + connections.Store(id, db) + + g.Args[1] = trealla.Atom("pg").Of(id) + return g +} + +func execute_4(pl trealla.Prolog, _ trealla.Subquery, goal trealla.Term) trealla.Term { + pi := terms.PI(goal) + g, ok := goal.(trealla.Compound) + if !ok { + return terms.Throw(terms.TypeError("compound", goal, pi)) + } + + handle, _ := g.Args[0].(trealla.Compound) + if handle.Functor != "pg" { + return terms.Throw(terms.TypeError("db_connection", g.Args[0], pi)) + } + + rawDB, ok := connections.Load(handle.Args[0].(int64)) + if !ok { + return terms.Throw(terms.DomainError("db_connection", handle.Args[0], pi)) + } + db := rawDB.(*sql.DB) + + // TODO: apply query arguments + + result, err := db.Exec(g.Args[1].(string)) + _ = result + if err != nil { + return terms.Throw(dbError(err, pi)) + } + + // TODO: build result + + return g +} + +func dbError(err error, pi trealla.Term) trealla.Term { + return trealla.Atom("error").Of(trealla.Atom("db_error").Of(err.Error()), pi) +} diff --git a/x/postgres/postgres_test.go b/x/postgres/postgres_test.go new file mode 100644 index 0000000..d353683 --- /dev/null +++ b/x/postgres/postgres_test.go @@ -0,0 +1,43 @@ +package postgres + +import ( + "context" + "reflect" + "testing" + + "github.com/trealla-prolog/go/trealla" +) + +func TestPG(t *testing.T) { + pl, err := trealla.New() + if err != nil { + t.Fatal(err) + } + + ctx := context.Background() + if err := Register(ctx, pl); err != nil { + t.Fatal(err) + } + + t.Run("open connection", func(t *testing.T) { + ctx := context.Background() + ans, err := pl.QueryOnce(ctx, `postgres_open_url("user=postgres password=password dbname=postgres sslmode=disable", Handle)`) + if err != nil { + t.Fatal(err) + } + want := trealla.Atom("pg").Of(int64(1)) + if !reflect.DeepEqual(ans.Solution["Handle"], want) { + t.Error("bad handle. want:", want, "got:", ans.Solution["Handle"]) + } + }) + + t.Run("execute query", func(t *testing.T) { + ctx := context.Background() + ans, err := pl.QueryOnce(ctx, `postgres_open_url("user=postgres password=password dbname=postgres sslmode=disable", Handle), postgres_execute(Handle, "CREATE TABLE IF NOT EXISTS guestbook (id serial primary key, time timestamp default CURRENT_TIMESTAMP, author text, msg text);", [], Results).`) + if err != nil { + t.Fatal(err) + } + _ = ans + // TODO + }) +}