all repos — searchix @ 3dfbd8dd7212f74622ba4892efb34bf3487da09b

Search engine for NixOS, nix-darwin, home-manager and NUR users

internal/programs/programs.go (view raw)

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
132
133
package programs

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

	"alin.ovh/searchix/internal/config"
	"alin.ovh/x/log"
	"github.com/Southclaws/fault"
	"github.com/Southclaws/fault/fmsg"
	_ "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, fault.Wrap(err, fmsg.With("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 fault.Wrap(err, fmsg.With("failed to open sqlite database"))
	}
	p.logger.Debug("opened sqlite database")

	_, err = p.db.Exec("ATTACH DATABASE ':memory:' AS mem")
	if err != nil {
		return fault.Wrap(err, fmsg.With("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 fault.Wrap(err, fmsg.With("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 fault.Wrap(err, fmsg.With("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 fault.Wrap(err, fmsg.With("failed to prepare statement"))
	}
	p.logger.Debug("prepared statement")

	return nil
}

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

	return nil
}

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

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

		programs = append(programs, name)
	}
	rerr := rows.Close()
	if rerr != nil {
		return nil, fault.Wrap(rerr, fmsg.With("sql error"))
	}

	return programs, nil
}