A software project that starts at the foundations.

This is where I build software from the ground up. The parts people usually buy ready-made and never look at again.

The first piece that is ready to show is a database. It is further down, with code you can try.

What the approach means

I start below the framework. How does a program store its data? What is still there when the power goes off in the middle of a write?

And every claim about such a piece should be something you can check, with a test, a command or a number. If something does not work yet, the page says so, the same as for what already works.

The first piece

datumujo

An embedded database for Go

  • v0.1.0
  • EUPL-1.2
  • Go standard library only

No server, nothing to install. You get datumujo like any other Go library, and your data lives in one file next to your program. The name is Esperanto for roughly "data container".

What v0.1.0 does:

  • Transactions that survive a crash or a power loss. When a commit returns, it is on disk.
  • One writer at a time, and readers never wait for it.
  • Tables with a primary key, indexes (unique ones too), scans in both directions, and counters.
  • A checksum on every page of the file.
  • Backup while writes go on.

What it looks like

Create a table, write a row, read everything back. This program runs as shown against v0.1.0:

package main

import (
	"fmt"

	"github.com/dyadik-eu/datumujo"
)

func main() {
	db, err := datumujo.Open("notizen.db", datumujo.Options{})
	if err != nil {
		panic(err)
	}
	defer db.Close()

	err = db.Update(func(tx *datumujo.Tx) error {
		err := tx.CreateTable(datumujo.Def{
			Name: "notiz",
			Columns: []datumujo.Column{
				{Name: "id", Type: datumujo.Int64},
				{Name: "text", Type: datumujo.String},
			},
			Key: []string{"id"},
		})
		if err != nil {
			return err
		}
		return tx.Insert("notiz", datumujo.Row{int64(1), "Hallo, Leibniz"})
	})
	if err != nil {
		panic(err)
	}

	err = db.Read(func(v *datumujo.View) error {
		rows, err := v.Scan("notiz", datumujo.ScanOptions{})
		if err != nil {
			return err
		}
		for rows.Next() {
			fmt.Println(rows.Row())
		}
		return rows.Err()
	})
	if err != nil {
		panic(err)
	}
}

Output:

[1 Hallo, Leibniz]

And if a byte flips?

Then you get an error with the page number. You do not get wrong data. Tried on a small database with one table: a copy, with a single byte on page 2 overwritten.

$ datumujo check notizen.db
file: 12288 bytes, log: 28 bytes
pages: 3 of 4096 bytes, 0 free, 1 in the catalog
schema version: 1
table notiz: 1 rows, 1 pages
intact
$ cp notizen.db kaputt.db
$ printf '\xff' | dd of=kaputt.db bs=1 seek=8292 conv=notrunc status=none
$ datumujo check kaputt.db
fault: page 2: checksum does not match
file: 12288 bytes, log: 0 bytes
pages: 3 of 4096 bytes, 0 free, 0 in the catalog
schema version: 0
1 faults

datumujo check then exits with code 1. For an intact file the code is 0, and 2 if it could not check at all.

What is still missing

SQL. I am working on it now, as the second stage. Until v1, the API and the file format can still change. The changelog says the same.

Code, docs and roadmap on GitHub

The name

0  1  10  11  100  101  110  111  1000  1001

Dyadik is Leibniz's own German word for calculating with zero and one. He worked it out and wrote it down in Hanover.

For a project that starts at the foundations, it is hard to think of a better name.

Gottfried Wilhelm Leibniz, De progressione dyadica, Hanover, 15 March 1679.

Contact

Questions, ideas, or found a bug? Write to kontakt@dyadik.eu or open an issue on GitHub.