about summary refs log tree commit diff stats
path: root/internal/programs/programs.go
blob: 5088444a6f9e00264ec610d4c7685f6ae760099f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
package programs

import (
	"context"
	"database/sql"
	"fmt"
	"os/exec"
	"strings"

	"github.com/pkg/errors"
	"go.alanpearce.eu/searchix/internal/config"
	"go.alanpearce.eu/x/log"
	_ "modernc.org/sqlite" //nolint:blank-imports // sqlite driver needed for database/sql
)

type DB struct {
	Path   string
	Source *config.Source

	logger *log.Logger
	db     *sql.DB
	stmt   *sql.Stmt
}

func Instantiate(
	ctx context.Context,
	source *config.Source,
	logger *log.Logger,
) (*DB, error) {
	// nix-instantiate --eval --json -I nixpkgs=channel:nixos-unstable --expr 'toString <nixpkgs/programs.sqlite>'
	args := []string{
		"--eval",
		"--json",
		"-I", fmt.Sprintf("%s=channel:%s", source.Key, source.Channel),
		"--expr", fmt.Sprintf("toString <%s/%s>", source.Key, source.Programs.Attribute),
	}

	logger.Debug("nix-instantiate command", "args", args)
	cmd := exec.CommandContext(ctx, "nix-instantiate", args...)
	out, err := cmd.Output()
	if err != nil {
		return nil, errors.WithMessage(err, "failed to run nix-instantiate")
	}

	outPath := strings.Trim(strings.TrimSpace(string(out)), "\"")
	logger.Debug("got output path", "outputPath", outPath)

	return &DB{
		Source: source,
		Path:   outPath,

		logger: logger,
	}, nil
}

func (p *DB) Open() error {
	var err error
	p.db, err = sql.Open("sqlite", p.Path)
	if err != nil {
		return errors.WithMessage(err, "failed to open sqlite database")
	}
	p.logger.Debug("opened sqlite database")

	_, err = p.db.Exec("ATTACH DATABASE ':memory:' AS mem")
	if err != nil {
		return errors.WithMessage(err, "failed to attach in-memory database")
	}

	_, err = p.db.Exec(`
CREATE TABLE mem.programs AS
SELECT name, package
FROM main.Programs
GROUP BY name, package
`)
	if err != nil {
		return errors.WithMessage(err, "failed to create programs table")
	}
	p.logger.Debug("created programs table")

	_, err = p.db.Exec(`CREATE INDEX mem.idx_package ON programs(package)`)
	if err != nil {
		return errors.WithMessage(err, "failed to create idx_package index")
	}
	p.logger.Debug("created idx_package index")

	p.stmt, err = p.db.Prepare(`
SELECT name
FROM mem.programs
WHERE package = ?
`)
	if err != nil {
		return errors.WithMessage(err, "failed to prepare statement")
	}
	p.logger.Debug("prepared statement")

	return nil
}

func (p *DB) Close() error {
	if err := p.db.Close(); err != nil {
		return errors.WithMessage(err, "failed to close sqlite database")
	}

	return nil
}

func (p *DB) GetPackagePrograms(ctx context.Context, pkg string) (programs []string, err error) {
	if p.db == nil {
		return nil, errors.New("database not open")
	}
	rows, err := p.stmt.QueryContext(ctx, pkg)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to execute query")
	}
	defer rows.Close()

	for rows.Next() {
		var name string
		if err := rows.Scan(&name); err != nil {
			return nil, errors.WithMessage(err, "failed to scan row")
		}

		programs = append(programs, name)
	}
	rerr := rows.Close()
	if rerr != nil {
		return nil, errors.WithMessage(rerr, "sql error")
	}

	return
}