Skip to content

Commit

Permalink
Merge pull request #983 from go-kivik/includeDDocs
Browse files Browse the repository at this point in the history
Add support for view options: include_design & local_seq
  • Loading branch information
flimzy committed May 23, 2024
2 parents d5fc1bf + 0264c97 commit 90559ef
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 17 deletions.
17 changes: 10 additions & 7 deletions x/sqlite/designdocs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ func (d *db) updateDesignDoc(ctx context.Context, tx *sql.Tx, rev revision, data
return nil
}
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)
INSERT INTO {{ .Design }} (id, rev, rev_id, language, func_type, func_name, func_body, auto_update, include_design, local_seq)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
`))
if err != nil {
return err
Expand All @@ -32,31 +32,34 @@ func (d *db) updateDesignDoc(ctx context.Context, tx *sql.Tx, rev revision, data

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 {
if _, err := stmt.ExecContext(ctx,
data.ID, rev.rev, rev.id, data.DesignFields.Language, "map", name, view.Map,
data.DesignFields.AutoUpdate, data.DesignFields.Options.IncludeDesign, data.DesignFields.Options.LocalSeq,
); err != nil {
return err
}
if err := d.createViewMap(ctx, tx, data.ID, name, rev.String()); 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 {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "reduce", name, view.Reduce, data.DesignFields.AutoUpdate, nil, nil); 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 {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "update", name, update, data.DesignFields.AutoUpdate, nil, nil); 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 {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "filter", name, filter, data.DesignFields.AutoUpdate, nil, nil); 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 {
if _, err := stmt.ExecContext(ctx, data.ID, rev.rev, rev.id, data.DesignFields.Language, "validate_doc_update", "validate", data.DesignFields.ValidateDocUpdates, data.DesignFields.AutoUpdate, nil, nil); err != nil {
return err
}
}
Expand Down
13 changes: 10 additions & 3 deletions x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ type views struct {
Reduce string `json:"reduce,omitempty"`
}

type designDocViewOptions struct {
LocalSeq bool `json:"local_seq,omitempty"`
IncludeDesign bool `json:"include_design,omitempty"`
}

// designDocData represents a design document. See
// https://docs.couchdb.org/en/stable/ddocs/ddocs.html#creation-and-structure
type designDocData struct {
Expand All @@ -95,8 +100,8 @@ type designDocData struct {
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"`
Options map[string]interface{} `json:"options,omitempty"`
AutoUpdate *bool `json:"autoupdate,omitempty"`
Options designDocViewOptions `json:"options,omitempty"`
}

// RevID returns calculated revision ID, possibly setting the MD5sum if it is
Expand Down Expand Up @@ -448,12 +453,14 @@ func (d *fullDoc) toMap() map[string]interface{} {
}
result["_attachments"] = attachments
}
if d.LocalSeq > 0 {
result["_local_seq"] = d.LocalSeq
}
/*
Conflicts []string `json:"_conflicts,omitempty"`
DeletedConflicts []string `json:"_deleted_conflicts,omitempty"`
RevsInfo []map[string]string `json:"_revs_info,omitempty"`
Revisions *revsInfo `json:"_revisions,omitempty"`
LocalSeq int `json:"_local_seq,omitempty"`
*/
return result
}
97 changes: 96 additions & 1 deletion x/sqlite/put_designdocs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,11 +120,106 @@ func TestDBPut_designDocs(t *testing.T) {
},
}
})
tests.Add("options.include_design=true", 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); }",
},
},
"options": map[string]interface{}{
"include_design": true,
},
},
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 includeDesign bool
err := d.underlying().QueryRow(`
SELECT include_design
FROM test_design
WHERE func_type = 'map'
LIMIT 1
`).Scan(&includeDesign)
if err != nil {
t.Fatal(err)
}
if !includeDesign {
t.Errorf("include_design was false, expected true")
}
},
}
})
tests.Add("options.local_seq=true", 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); }",
},
},
"options": map[string]interface{}{
"local_seq": true,
},
},
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 includeDesign bool
err := d.underlying().QueryRow(`
SELECT local_seq
FROM test_design
WHERE func_type = 'map'
LIMIT 1
`).Scan(&includeDesign)
if err != nil {
t.Fatal(err)
}
if !includeDesign {
t.Errorf("include_design was false, expected true")
}
},
}
})
/*
TODO:
- unsupported language? -- ignored?
- Drop old indexes when a ddoc changes
*/

tests.Run(t, func(t *testing.T, tt test) {
Expand Down
18 changes: 13 additions & 5 deletions x/sqlite/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,22 +319,25 @@ const batchSize = 100
// ddoc revid and last_seq. If mode is "true", it will also update the index.
func (d *db) updateIndex(ctx context.Context, ddoc, view, mode string) (revision, error) {
var (
ddocRev revision
mapFuncJS *string
lastSeq int
ddocRev revision
mapFuncJS *string
lastSeq int
includeDesign, localSeq sql.NullBool
)
err := d.db.QueryRowContext(ctx, d.query(`
SELECT
docs.rev,
docs.rev_id,
design.func_body,
design.include_design,
design.local_seq,
COALESCE(design.last_seq, 0) AS last_seq
FROM {{ .Docs }} AS docs
LEFT JOIN {{ .Design }} AS design ON docs.id = design.id AND docs.rev = design.rev AND docs.rev_id = design.rev_id AND design.func_type = 'map'
WHERE docs.id = $1
ORDER BY docs.rev DESC, docs.rev_id DESC
LIMIT 1
`), "_design/"+ddoc).Scan(&ddocRev.rev, &ddocRev.id, &mapFuncJS, &lastSeq)
`), "_design/"+ddoc).Scan(&ddocRev.rev, &ddocRev.id, &mapFuncJS, &includeDesign, &localSeq, &lastSeq)
switch {
case errors.Is(err, sql.ErrNoRows):
return revision{}, &internal.Error{Status: http.StatusNotFound, Message: "missing"}
Expand Down Expand Up @@ -480,7 +483,8 @@ func (d *db) updateIndex(ctx context.Context, ddoc, view, mode string) (revision
}

// TODO move this to the query
if strings.HasPrefix(full.ID, "_design/") || strings.HasPrefix(full.ID, "_local/") {
if strings.HasPrefix(full.ID, "_local/") ||
(!includeDesign.Bool && strings.HasPrefix(full.ID, "_design/")) {
continue
}

Expand All @@ -489,6 +493,10 @@ func (d *db) updateIndex(ctx context.Context, ddoc, view, mode string) (revision
continue
}

if localSeq.Bool {
full.LocalSeq = seq
}

if err := vm.Set("emit", emit(full.ID, rev)); err != nil {
return revision{}, err
}
Expand Down
58 changes: 57 additions & 1 deletion x/sqlite/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1650,6 +1650,63 @@ func TestDBQuery(t *testing.T) {
},
}
})
tests.Add("include design docs in view output", func(t *testing.T) interface{} {
d := newDB(t)
_ = d.tPut("_design/foo", map[string]interface{}{
"key": "design",
"views": map[string]interface{}{
"bar": map[string]string{
"map": `function(doc) {
if (doc.key) {
emit(doc.key, doc.value);
}
}`,
},
},
"options": map[string]interface{}{
"include_design": true,
},
})
_ = d.tPut("a", map[string]interface{}{"key": "a"})

return test{
db: d,
ddoc: "_design/foo",
view: "_view/bar",
want: []rowResult{
{ID: "a", Key: `"a"`, Value: "null"},
{ID: "_design/foo", Key: `"design"`, Value: "null"},
},
}
})
tests.Add("options.local_seq=true", func(t *testing.T) interface{} {
d := newDB(t)
_ = d.tPut("_design/foo", map[string]interface{}{
"key": "design",
"views": map[string]interface{}{
"bar": map[string]string{
"map": `function(doc) {
if (doc.key) {
emit(doc.key, doc._local_seq);
}
}`,
},
},
"options": map[string]interface{}{
"local_seq": true,
},
})
_ = d.tPut("a", map[string]interface{}{"key": "a"})

return test{
db: d,
ddoc: "_design/foo",
view: "_view/bar",
want: []rowResult{
{ID: "a", Key: `"a"`, Value: "2"},
},
}
})

/*
TODO:
Expand Down Expand Up @@ -1687,7 +1744,6 @@ func TestDBQuery(t *testing.T) {
- treat map non-exception errors as exceptions
- make sure local docs are properly skipped
- make sure deleted docs are properly skipped
*/

tests.Run(t, func(t *testing.T, tt test) {
Expand Down
3 changes: 3 additions & 0 deletions x/sqlite/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ var schema = []string{
func_name TEXT NOT NULL,
func_body TEXT NOT NULL,
auto_update BOOLEAN NOT NULL DEFAULT TRUE,
-- Options include_design and local_seq are only stored for 'map' type
include_design BOOLEAN,
local_seq BOOLEAN,
last_seq INTEGER, -- the last map-indexed sequence id, NULL for others
FOREIGN KEY (id, rev, rev_id) REFERENCES {{ .Docs }} (id, rev, rev_id) ON DELETE CASCADE,
UNIQUE (id, rev, rev_id, func_type, func_name)
Expand Down

0 comments on commit 90559ef

Please sign in to comment.