Skip to content

Commit c263a29

Browse files
authored
Merge pull request #1123 from go-kivik/dbexists
Add misc db method support
2 parents a7d6df7 + b674390 commit c263a29

File tree

12 files changed

+347
-35
lines changed

12 files changed

+347
-35
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ mockdb/other
66
**/*~
77
pouch__all_dbs__
88
*_test.[0-9]*.js
9-
*_test.[0-9]*.js.map
9+
*_test.[0-9]*.js.map
10+
go.work
11+
go.work.sum

x/options/options.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ func parseJSONKey(key string, in any) (string, error) {
7171
}
7272
}
7373

74-
// EndKey returns the endkey option.
74+
// EndKey returns the endkey option as an encoded JSON value.
7575
func (o Map) EndKey() (string, error) {
7676
key, value, ok := o.Get("endkey", "end_key")
7777
if !ok {

x/pg/alldbs.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
2+
// use this file except in compliance with the License. You may obtain a copy of
3+
// the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations under
11+
// the License.
12+
13+
package pg
14+
15+
import (
16+
"context"
17+
"fmt"
18+
"strings"
19+
20+
"github.com/jackc/pgx/v5"
21+
22+
"github.com/go-kivik/kivik/v4/driver"
23+
"github.com/go-kivik/kivik/v4/x/options"
24+
)
25+
26+
func (c *client) AllDBs(ctx context.Context, opts driver.Options) ([]string, error) {
27+
o, err := options.New(opts).PaginationOptions(true)
28+
if err != nil {
29+
return nil, err
30+
}
31+
32+
args := []any{tablePrefix}
33+
where := append([]string{"TRUE"}, o.BuildWhere(&args)...)
34+
35+
query := fmt.Sprintf(`
36+
WITH view AS (
37+
SELECT substr(tablename, length($1)+1) AS key
38+
FROM pg_tables
39+
WHERE tablename LIKE $1 || '%%'
40+
)
41+
SELECT key
42+
FROM view
43+
WHERE %s
44+
%s
45+
`, strings.Join(where, " AND "), o.BuildOrderBy())
46+
47+
rows, err := c.pool.Query(ctx, query, args...)
48+
if err != nil {
49+
return nil, err
50+
}
51+
return pgx.CollectRows(rows, pgx.RowTo[string])
52+
}

x/pg/alldbs_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
2+
// use this file except in compliance with the License. You may obtain a copy of
3+
// the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations under
11+
// the License.
12+
13+
package pg
14+
15+
import (
16+
"net/http"
17+
"testing"
18+
19+
"github.com/google/go-cmp/cmp"
20+
"github.com/google/go-cmp/cmp/cmpopts"
21+
"gitlab.com/flimzy/testy/v2"
22+
23+
"github.com/go-kivik/kivik/v4"
24+
"github.com/go-kivik/kivik/v4/driver"
25+
)
26+
27+
func TestAllDBs(t *testing.T) {
28+
t.Parallel()
29+
30+
type test struct {
31+
client *client
32+
options driver.Options
33+
want []string
34+
wantErr string
35+
wantStatus int
36+
}
37+
38+
tests := testy.NewTable[test]()
39+
tests.Add("no databases found", test{
40+
client: testClient(t),
41+
want: []string{},
42+
})
43+
tests.AddFunc("some dbs exist", func(t *testing.T) test {
44+
client := testClient(t)
45+
46+
const dbName1, dbName2 = "testdb1", "testdb2"
47+
if err := client.CreateDB(t.Context(), dbName1, nil); err != nil {
48+
t.Fatalf("Failed to create test db: %s", err)
49+
}
50+
if err := client.CreateDB(t.Context(), dbName2, nil); err != nil {
51+
t.Fatalf("Failed to create test db: %s", err)
52+
}
53+
54+
return test{
55+
client: client,
56+
want: []string{dbName1, dbName2},
57+
}
58+
})
59+
tests.AddFunc("exclude non-kivik tables", func(t *testing.T) test {
60+
client := testClient(t)
61+
62+
const dbName = "testdb"
63+
if err := client.CreateDB(t.Context(), dbName, nil); err != nil {
64+
t.Fatalf("Failed to create test db: %s", err)
65+
}
66+
if _, err := client.pool.Exec(t.Context(), "CREATE TABLE foo (id TEXT)"); err != nil {
67+
t.Fatalf("Failed to create non-kivik table: %s", err)
68+
}
69+
70+
return test{
71+
client: client,
72+
want: []string{dbName},
73+
}
74+
})
75+
tests.AddFunc("db connection error", func(t *testing.T) test {
76+
client := testClient(t)
77+
78+
client.pool.Close()
79+
80+
return test{
81+
client: client,
82+
wantErr: "closed pool",
83+
wantStatus: http.StatusInternalServerError,
84+
}
85+
})
86+
tests.AddFunc("option: descending=true", func(t *testing.T) test {
87+
client := testClient(t)
88+
89+
const dbName1, dbName2 = "testdb1", "testdb2"
90+
if err := client.CreateDB(t.Context(), dbName1, nil); err != nil {
91+
t.Fatalf("Failed to create test db: %s", err)
92+
}
93+
if err := client.CreateDB(t.Context(), dbName2, nil); err != nil {
94+
t.Fatalf("Failed to create test db: %s", err)
95+
}
96+
97+
return test{
98+
client: client,
99+
options: kivik.Param("descending", "true"),
100+
want: []string{dbName2, dbName1},
101+
}
102+
})
103+
tests.AddFunc("end key", func(t *testing.T) test {
104+
client := testClient(t)
105+
106+
const dbName1, dbName2 = "testdb1", "testdb2"
107+
if err := client.CreateDB(t.Context(), dbName1, nil); err != nil {
108+
t.Fatalf("Failed to create test db: %s", err)
109+
}
110+
if err := client.CreateDB(t.Context(), dbName2, nil); err != nil {
111+
t.Fatalf("Failed to create test db: %s", err)
112+
}
113+
114+
return test{
115+
client: client,
116+
options: kivik.Param("endkey", dbName1),
117+
want: []string{dbName1},
118+
}
119+
})
120+
121+
/*
122+
TODO:
123+
- options:
124+
- limit (number) – Limit the number of the returned databases to the specified number.
125+
- skip (number) – Skip this number of databases before starting to return the results. Default is 0.
126+
- startkey (json) – Return databases starting with the specified key.
127+
- start_key (json) – Alias for startkey.
128+
- inclusive_end (boolean) – If true, the specified endkey will be included in the result. Default is true.
129+
*/
130+
131+
tests.Run(t, func(t *testing.T, tt test) {
132+
t.Parallel()
133+
134+
got, err := tt.client.AllDBs(t.Context(), tt.options)
135+
if !testy.ErrorMatchesRE(tt.wantErr, err) {
136+
t.Errorf("Unexpected error: %s", err)
137+
}
138+
if status := kivik.HTTPStatus(err); status != tt.wantStatus {
139+
t.Errorf("Unexpected status: %d", status)
140+
}
141+
if d := cmp.Diff(tt.want, got, cmpopts.EquateEmpty()); d != "" {
142+
t.Errorf("Unexpected result:\n%s", d)
143+
}
144+
})
145+
}

x/pg/client.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,6 @@ func (c *client) Version(context.Context) (*driver.Version, error) {
3939
}, nil
4040
}
4141

42-
func (c *client) AllDBs(context.Context, driver.Options) ([]string, error) {
43-
return nil, errors.ErrUnsupported
44-
}
45-
46-
func (c *client) DBExists(context.Context, string, driver.Options) (bool, error) {
47-
return false, errors.ErrUnsupported
48-
}
49-
5042
func (c *client) DestroyDB(context.Context, string, driver.Options) error {
5143
return errors.ErrUnsupported
5244
}

x/pg/createdb.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import (
2525
internal "github.com/go-kivik/kivik/v4/int/errors"
2626
)
2727

28+
const tablePrefix = "kivik$"
29+
2830
var validDBNameRE = regexp.MustCompile("^[a-z_][a-z0-9_$()+/-]*$")
2931

3032
func (c *client) CreateDB(ctx context.Context, dbName string, _ driver.Options) error {
@@ -35,7 +37,7 @@ func (c *client) CreateDB(ctx context.Context, dbName string, _ driver.Options)
3537
}
3638
}
3739

38-
_, err := c.pool.Exec(ctx, "CREATE TABLE "+dbName+" (id SERIAL PRIMARY KEY, data JSONB)")
40+
_, err := c.pool.Exec(ctx, "CREATE TABLE "+tablePrefix+dbName+" (id SERIAL PRIMARY KEY, data JSONB)")
3941
if err != nil {
4042
if pgErr, ok := err.(*pgconn.PgError); ok {
4143
if pgErr.Code == pgerrcode.DuplicateTable {

x/pg/createdb_test.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616
"net/http"
1717
"testing"
1818

19-
"gitlab.com/flimzy/testy"
19+
"gitlab.com/flimzy/testy/v2"
2020

2121
"github.com/go-kivik/kivik/v4"
2222
"github.com/go-kivik/kivik/v4/driver"
@@ -33,7 +33,7 @@ func TestCreateDB(t *testing.T) {
3333
wantStatus int
3434
}
3535

36-
tests := testy.NewTable()
36+
tests := testy.NewTable[test]()
3737
tests.Add("invalid db name", test{
3838
// This test violates the documented database name requirements
3939
// found at https://docs.couchdb.org/en/stable/api/database/common.html#put--db
@@ -42,7 +42,7 @@ func TestCreateDB(t *testing.T) {
4242
wantErr: "invalid database name",
4343
wantStatus: http.StatusBadRequest,
4444
})
45-
tests.Add("db connection error", func(t *testing.T) any {
45+
tests.AddFunc("db connection error", func(t *testing.T) test {
4646
client := testClient(t)
4747

4848
client.pool.Close()
@@ -58,7 +58,7 @@ func TestCreateDB(t *testing.T) {
5858
client: testClient(t),
5959
dbName: "testdb",
6060
})
61-
tests.Add("db already exists", func(t *testing.T) any {
61+
tests.AddFunc("db already exists", func(t *testing.T) test {
6262
client := testClient(t)
6363

6464
// Create the database first
@@ -89,7 +89,7 @@ func TestCreateDB(t *testing.T) {
8989
}
9090
// Verify the database was created
9191
var found bool
92-
err = tt.client.pool.QueryRow(t.Context(), "SELECT true FROM pg_tables WHERE tablename = $1", tt.dbName).Scan(&found)
92+
err = tt.client.pool.QueryRow(t.Context(), "SELECT true FROM pg_tables WHERE tablename = $1", tablePrefix+tt.dbName).Scan(&found)
9393
if err != nil {
9494
t.Errorf("Failed to verify database creation: %s", err)
9595
return

x/pg/dbexists.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
2+
// use this file except in compliance with the License. You may obtain a copy of
3+
// the License at
4+
//
5+
// http://www.apache.org/licenses/LICENSE-2.0
6+
//
7+
// Unless required by applicable law or agreed to in writing, software
8+
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9+
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10+
// License for the specific language governing permissions and limitations under
11+
// the License.
12+
13+
package pg
14+
15+
import (
16+
"context"
17+
"errors"
18+
19+
"github.com/jackc/pgx/v5"
20+
21+
"github.com/go-kivik/kivik/v4/driver"
22+
)
23+
24+
func (c *client) DBExists(ctx context.Context, dbName string, _ driver.Options) (bool, error) {
25+
var exists bool
26+
err := c.pool.QueryRow(ctx, "SELECT true FROM pg_tables WHERE tablename = $1", tablePrefix+dbName).Scan(&exists)
27+
switch {
28+
case errors.Is(err, pgx.ErrNoRows):
29+
return false, nil
30+
case err != nil:
31+
return false, err
32+
}
33+
return exists, nil
34+
}

0 commit comments

Comments
 (0)