Skip to content

Commit

Permalink
Merge pull request #942 from go-kivik/ddocs
Browse files Browse the repository at this point in the history
Start support for design docs
  • Loading branch information
flimzy committed Apr 16, 2024
2 parents 18d2243 + 34d4f07 commit ec42afe
Show file tree
Hide file tree
Showing 8 changed files with 426 additions and 28 deletions.
61 changes: 61 additions & 0 deletions x/sqlite/designdocs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http:https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

package sqlite

import (
"context"
"database/sql"
)

func (d *db) updateDesignDoc(ctx context.Context, tx *sql.Tx, rev revision, data *docData) error {
stmt, err := tx.PrepareContext(ctx, d.query(`
INSERT INTO {{ .Design }} (id, rev, rev_id, language, func_type, func_name, func_body, auto_update)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
`))
if err != nil {
return err
}
defer stmt.Close()

for name, view := range data.DesignFields.Views {
if view.Map != "" {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "map", name, view.Map, data.DesignFields.AutoUpdate); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, d.ddocQuery(data.ID, name, viewMapTable)); err != nil {
return err
}
}
if view.Reduce != "" {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "reduce", name, view.Reduce, data.DesignFields.AutoUpdate); err != nil {
return err
}
}
}
for name, update := range data.DesignFields.Updates {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "update", name, update, data.DesignFields.AutoUpdate); err != nil {
return err
}
}
for name, filter := range data.DesignFields.Filters {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "filter", name, filter, data.DesignFields.AutoUpdate); err != nil {
return err
}
}
if data.DesignFields.ValidateDocUpdates != "" {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "validate_doc_update", "validate", data.DesignFields.ValidateDocUpdates, data.DesignFields.AutoUpdate); err != nil {
return err
}
}
return nil
}
194 changes: 194 additions & 0 deletions x/sqlite/designdocs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http:https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

//go:build !js
// +build !js

package sqlite

import (
"context"
"database/sql"
"net/http"
"regexp"
"testing"

"github.com/google/go-cmp/cmp"
"gitlab.com/flimzy/testy"

"github.com/go-kivik/kivik/v4"
"github.com/go-kivik/kivik/v4/driver"
"github.com/go-kivik/kivik/v4/internal/mock"
)

type ddoc struct {
ID string
Rev int
RevID string
Lang string
FuncType string
FuncName string
FuncBody string
AutoUpdate bool
}

func TestDBPut_designDocs(t *testing.T) {
t.Parallel()
type test struct {
db *testDB
docID string
doc interface{}
options driver.Options
check func(*testing.T)
wantRev string
wantRevs []leaf
wantStatus int
wantErr string
wantAttachments []attachmentRow
wantDDocs []ddoc
}
tests := testy.NewTable()
tests.Add("design doc with non-string language returns 400", test{
docID: "_design/foo",
doc: map[string]interface{}{
"language": 1234,
},
wantStatus: http.StatusBadRequest,
wantErr: "json: cannot unmarshal number into Go struct field designDocData.language of type string",
})
tests.Add("non-design doc with non-string language value is ok", test{
docID: "foo",
doc: map[string]interface{}{
"language": 1234,
},
wantRev: "1-.*",
wantRevs: []leaf{
{ID: "foo", Rev: 1},
},
})
tests.Add("design doc with view function creates .Design entries and map table", func(t *testing.T) interface{} {
d := newDB(t)
return test{
db: d,
docID: "_design/foo",
doc: map[string]interface{}{
"language": "javascript",
"views": map[string]interface{}{
"bar": map[string]interface{}{
"map": "function(doc) { emit(doc._id, null); }",
},
},
},
wantRev: "1-.*",
wantRevs: []leaf{
{ID: "_design/foo", Rev: 1},
},
wantDDocs: []ddoc{
{
ID: "_design/foo",
Rev: 1,
Lang: "javascript",
FuncType: "map",
FuncName: "bar",
FuncBody: "function(doc) { emit(doc._id, null); }",
AutoUpdate: true,
},
},
check: func(t *testing.T) {
var viewCount int
err := d.underlying().QueryRow(`
SELECT COUNT(*)
FROM sqlite_master
WHERE type = 'table'
AND name LIKE 'foo_map_bar_%'
`).Scan(&viewCount)
if err != nil {
t.Fatal(err)
}
if viewCount != 1 {
t.Errorf("Found %d view tables, expected 1", viewCount)
}
},
}
})
/*
TODO:
- unsupported language? -- ignored?
*/

tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
dbc := tt.db
if dbc == nil {
dbc = newDB(t)
}
opts := tt.options
if opts == nil {
opts = mock.NilOption
}
rev, err := dbc.Put(context.Background(), tt.docID, tt.doc, opts)
if !testy.ErrorMatches(tt.wantErr, err) {
t.Errorf("Unexpected error: %s", err)
}
if status := kivik.HTTPStatus(err); status != tt.wantStatus {
t.Errorf("Unexpected status: %d", status)
}
if tt.check != nil {
tt.check(t)
}
if err != nil {
return
}
if !regexp.MustCompile(tt.wantRev).MatchString(rev) {
t.Errorf("Unexpected rev: %s, want %s", rev, tt.wantRev)
}
checkLeaves(t, dbc.underlying(), tt.wantRevs)
checkAttachments(t, dbc.underlying(), tt.wantAttachments)
checkDDocs(t, dbc.underlying(), tt.wantDDocs)
})
}

func checkDDocs(t *testing.T, d *sql.DB, want []ddoc) {
t.Helper()
rows, err := d.Query(`
SELECT id, rev, rev_id, language, func_type, func_name, func_body, auto_update
FROM test_design
`)
if err != nil {
t.Fatal(err)
}
defer rows.Close()
var got []ddoc
for rows.Next() {
var d ddoc
if err := rows.Scan(&d.ID, &d.Rev, &d.RevID, &d.Lang, &d.FuncType, &d.FuncName, &d.FuncBody, &d.AutoUpdate); err != nil {
t.Fatal(err)
}
got = append(got, d)
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
for i, w := range want {
if i > len(got)-1 {
t.Errorf("Missing expected design doc: %+v", w)
break
}
// allow tests to omit RevID
if w.RevID == "" {
got[i].RevID = ""
}
}
if d := cmp.Diff(want, got); d != "" {
t.Errorf("Unexpected design docs:\n%s", d)
}
}
41 changes: 39 additions & 2 deletions x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,30 @@ type docData struct {
Doc []byte
// MD5sum is the MD5sum of the document data. It, along with a hash of
// attachment metadata, is used to calculate the document revision.
MD5sum md5sum `json:"-"`
MD5sum md5sum `json:"-"`
DesignFields designDocData `json:"-"`
}

func (d *docData) IsDesignDoc() bool {
return strings.HasPrefix(d.ID, "_design/")
}

type views struct {
Map string `json:"map"`
Reduce string `json:"reduce,omitempty"`
}

// designDocData represents a design document. See
// https://docs.couchdb.org/en/stable/ddocs/ddocs.html#creation-and-structure
type designDocData struct {
Language string `json:"language,omitempty"`
Views map[string]views `json:"views,omitempty"`
Updates map[string]string `json:"updates,omitempty"`
Filters map[string]string `json:"filters,omitempty"`
ValidateDocUpdates string `json:"validate_doc_update,omitempty"`
// AutoUpdate indicates whether to automatically build indexes defined in
// this design document. Default is true.
AutoUpdate *bool `json:"autoupdate,omitempty"`
}

// RevID returns calculated revision ID, possibly setting the MD5sum if it is
Expand Down Expand Up @@ -263,11 +286,25 @@ func prepareDoc(docID string, doc interface{}) (*docData, error) {
if err != nil {
return nil, err
}
var ddocData designDocData
if strings.HasPrefix(docID, "_design/") {
if err := json.Unmarshal(tmpJSON, &ddocData); err != nil {
return nil, &internal.Error{Status: http.StatusBadRequest, Err: err}
}
if ddocData.Language == "" {
ddocData.Language = "javascript"
}
if ddocData.AutoUpdate == nil {
ddocData.AutoUpdate = &[]bool{true}[0]
}
}
var tmp map[string]interface{}
if err := json.Unmarshal(tmpJSON, &tmp); err != nil {
return nil, err
}
data := &docData{}
data := &docData{
DesignFields: ddocData,
}
if err := json.Unmarshal(tmpJSON, &data); err != nil {
return nil, &internal.Error{Status: http.StatusBadRequest, Err: err}
}
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
return "", err
}

if err := createDocAttachments(ctx, data, tx, d, rev, ancestorRev); err != nil {
if err := d.createDocAttachments(ctx, data, tx, rev, ancestorRev); err != nil {
return "", err
}

Expand Down
38 changes: 22 additions & 16 deletions x/sqlite/put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1132,26 +1132,32 @@ func TestDBPut(t *testing.T) {
if !regexp.MustCompile(tt.wantRev).MatchString(rev) {
t.Errorf("Unexpected rev: %s, want %s", rev, tt.wantRev)
}
if len(tt.wantRevs) == 0 {
t.Errorf("No leaves to check")
}
leaves := readRevisions(t, dbc.underlying())
for i, r := range tt.wantRevs {
// allow tests to omit RevID
if r.RevID == "" {
leaves[i].RevID = ""
}
if r.ParentRevID == nil {
leaves[i].ParentRevID = nil
}
}
if d := cmp.Diff(tt.wantRevs, leaves); d != "" {
t.Errorf("Unexpected leaves: %s", d)
}

checkLeaves(t, dbc.underlying(), tt.wantRevs)
checkAttachments(t, dbc.underlying(), tt.wantAttachments)
})
}

func checkLeaves(t *testing.T, d *sql.DB, want []leaf) {
t.Helper()
if len(want) == 0 {
t.Errorf("No leaves to check")
}
leaves := readRevisions(t, d)
for i, r := range want {
// allow tests to omit RevID or ParentRevID
if r.RevID == "" {
leaves[i].RevID = ""
}
if r.ParentRevID == nil {
leaves[i].ParentRevID = nil
}
}
if d := cmp.Diff(want, leaves); d != "" {
t.Errorf("Unexpected leaves: %s", d)
}
}

func checkAttachments(t *testing.T, d *sql.DB, want []attachmentRow) {
t.Helper()
rows, err := d.Query(`
Expand Down
Loading

0 comments on commit ec42afe

Please sign in to comment.