Skip to content
doc

Quick start

Create a .doc file, insert documents, and run a query and an aggregation, first from the command line and then from Go.

This page goes from nothing to a working query twice: once with the doc command line and once with the Go library. Both end with real documents coming back.

From the command line

Insert a few documents. The shell speaks a mongosh-style surface, so the calls look like the ones you would type in a Mongo shell:

doc app.doc --eval 'db.users.insertMany([
  {name: "ada", age: 36, city: "london"},
  {name: "grace", age: 45, city: "new york"},
  {name: "linus", age: 21, city: "helsinki"}
])'

app.doc did not exist, so doc created it. Now query it:

doc app.doc --eval 'db.users.find({age: {$gte: 30}})'

That prints the two matching documents. Run an aggregation to count users by city:

doc app.doc --eval 'db.users.aggregate([
  {$group: {_id: "$city", n: {$sum: 1}}},
  {$sort: {n: -1}}
])'

Open the interactive shell to poke around without --eval each time:

doc app.doc

Inside, .help lists the dot-commands, .collections lists collections, and .pragma shows the engine settings. Type .quit to leave.

From Go

The same database, opened as a library, is a short program:

package main

import (
	"context"
	"fmt"
	"log"

	"github.com/tamnd/doc"
)

func main() {
	ctx := context.Background()

	db, err := doc.Open("app.doc")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	users := db.Database("default").Collection("users")

	// Insert.
	if _, err := users.InsertMany(ctx, []any{
		doc.M{"name": "ada", "age": 36, "city": "london"},
		doc.M{"name": "grace", "age": 45, "city": "new york"},
		doc.M{"name": "linus", "age": 21, "city": "helsinki"},
	}); err != nil {
		log.Fatal(err)
	}

	// Find one and decode it into a struct.
	var ada struct {
		Name string `bson:"name"`
		Age  int    `bson:"age"`
		City string `bson:"city"`
	}
	if err := users.FindOne(ctx, doc.M{"name": "ada"}).Decode(&ada); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%s is %d, in %s\n", ada.Name, ada.Age, ada.City)

	// Find many.
	cur, err := users.Find(ctx, doc.M{"age": doc.M{"$gte": 30}})
	if err != nil {
		log.Fatal(err)
	}
	var adults []doc.M
	if err := cur.All(ctx, &adults); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("%d adults\n", len(adults))

	// Aggregate: count by city.
	pipeline := doc.A{
		doc.M{"$group": doc.M{"_id": "$city", "n": doc.M{"$sum": 1}}},
		doc.M{"$sort": doc.M{"n": -1}},
	}
	agg, err := users.Aggregate(ctx, pipeline)
	if err != nil {
		log.Fatal(err)
	}
	var byCity []doc.M
	if err := agg.All(ctx, &byCity); err != nil {
		log.Fatal(err)
	}
	fmt.Println(byCity)
}

Run it:

go run .

doc.M is a map[string]any, the same role bson.M plays in the MongoDB driver. doc.A is the ordered slice you build a pipeline from. A FindOne that matches nothing returns doc.ErrNoDocuments, which you check with errors.Is.

Use an in-memory database

Pass :memory: as the path to get a database that never touches disk, which is handy for tests:

db, err := doc.Open(":memory:")

What is next