aboutsummaryrefslogtreecommitdiff
path: root/src/gnunet/service/store/database.go
diff options
context:
space:
mode:
Diffstat (limited to 'src/gnunet/service/store/database.go')
-rw-r--r--src/gnunet/service/store/database.go186
1 files changed, 186 insertions, 0 deletions
diff --git a/src/gnunet/service/store/database.go b/src/gnunet/service/store/database.go
new file mode 100644
index 0000000..2b94122
--- /dev/null
+++ b/src/gnunet/service/store/database.go
@@ -0,0 +1,186 @@
1// This file is part of gnunet-go, a GNUnet-implementation in Golang.
2// Copyright (C) 2019-2022 Bernd Fix >Y<
3//
4// gnunet-go is free software: you can redistribute it and/or modify it
5// under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License,
7// or (at your option) any later version.
8//
9// gnunet-go is distributed in the hope that it will be useful, but
10// WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12// Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program. If not, see <http://www.gnu.org/licenses/>.
16//
17// SPDX-License-Identifier: AGPL3.0-or-later
18
19package store
20
21import (
22 "context"
23 "database/sql"
24 "fmt"
25 "gnunet/util"
26 "os"
27 "strings"
28
29 _ "github.com/go-sql-driver/mysql" // init MySQL driver
30 _ "github.com/mattn/go-sqlite3" // init SQLite3 driver
31)
32
33// Error messages related to databases
34var (
35 ErrSQLInvalidDatabaseSpec = fmt.Errorf("invalid database specification")
36 ErrSQLNoDatabase = fmt.Errorf("database not found")
37)
38
39//----------------------------------------------------------------------
40// Connection to a database instance. There can be multiple connections
41// on the same instance, managed by the database pool.
42//----------------------------------------------------------------------
43
44// DBConn is a database connection suitable for executing SQL commands.
45type DBConn struct {
46 conn *sql.Conn // connection to database instance
47 key string // database connect string (identifier for pool)
48 engine string // database engine
49}
50
51// Close database connection.
52func (db *DBConn) Close() (err error) {
53 if err = db.conn.Close(); err != nil {
54 return
55 }
56 err = DBPool.remove(db.key)
57 return
58}
59
60// QueryRow returns a single record for a query
61func (db *DBConn) QueryRow(query string, args ...any) *sql.Row {
62 return db.conn.QueryRowContext(DBPool.ctx, query, args...)
63}
64
65// Query returns all matching records for a query
66func (db *DBConn) Query(query string, args ...any) (*sql.Rows, error) {
67 return db.conn.QueryContext(DBPool.ctx, query, args...)
68}
69
70// Exec a SQL statement
71func (db *DBConn) Exec(query string, args ...any) (sql.Result, error) {
72 return db.conn.ExecContext(DBPool.ctx, query, args...)
73}
74
75// TODO: add more SQL methods
76
77//----------------------------------------------------------------------
78// DbPool holds all database instances used: Connecting with the same
79// connect string returns the same instance.
80//----------------------------------------------------------------------
81
82// global instance for the database pool (singleton)
83var (
84 DBPool *dbPool
85)
86
87// DBPoolEntry holds information about a database instance.
88type DBPoolEntry struct {
89 db *sql.DB // reference to the database engine
90 refs int // number of open connections (reference count)
91 connect string // SQL connect string
92}
93
94// package initialization
95func init() {
96 // construct database pool
97 DBPool = new(dbPool)
98 DBPool.insts = util.NewMap[string, *DBPoolEntry]()
99 DBPool.ctx, DBPool.cancel = context.WithCancel(context.Background())
100}
101
102// dbPool keeps a mapping between connect string and database instance
103type dbPool struct {
104 ctx context.Context // connection context
105 cancel context.CancelFunc // cancel function
106 insts *util.Map[string, *DBPoolEntry] // map of database instances
107}
108
109// remove a database instance from the pool based on its connect string.
110func (p *dbPool) remove(key string) error {
111 return p.insts.Process(func() (err error) {
112 // get pool entry
113 pe, ok := p.insts.Get(key)
114 if !ok {
115 return nil
116 }
117 // decrement ref count
118 pe.refs--
119 if pe.refs == 0 {
120 // no more refs: close database
121 err = pe.db.Close()
122 p.insts.Delete(key)
123 }
124 return
125 }, false)
126}
127
128// Connect to a SQL database (various types and flavors):
129// The 'spec' option defines the arguments required to connect to a database;
130// the meaning and format of the arguments depends on the specific SQL database.
131// The arguments are separated by the '+' character; the first (and mandatory)
132// argument defines the SQL database type. Other arguments depend on the value
133// of this first argument.
134// The following SQL types are implemented:
135// * 'sqlite3': SQLite3-compatible database; the second argument specifies the
136// file that holds the data (e.g. "sqlite3+/home/user/store.db")
137// * 'mysql': A MySQL-compatible database; the second argument specifies the
138// information required to log into the database (e.g.
139// "[user[:passwd]@][proto[(addr)]]/dbname[?param1=value1&...]").
140func (p *dbPool) Connect(spec string) (db *DBConn, err error) {
141 err = p.insts.Process(func() error {
142 // check if we have a connection to this database.
143 db = new(DBConn)
144 inst, ok := p.insts.Get(spec)
145 if !ok {
146 inst = new(DBPoolEntry)
147 inst.refs = 0
148 inst.connect = spec
149
150 // No: create new database instance.
151 // split spec string into segments
152 specs := strings.Split(spec, ":")
153 if len(specs) < 2 {
154 return ErrSQLInvalidDatabaseSpec
155 }
156 // create database object
157 db.engine = specs[0]
158 switch db.engine {
159 case "sqlite3":
160 // check if the database file exists
161 var fi os.FileInfo
162 if fi, err = os.Stat(specs[1]); err != nil {
163 return ErrSQLNoDatabase
164 }
165 if fi.IsDir() {
166 return ErrSQLNoDatabase
167 }
168 // open the database file
169 inst.db, err = sql.Open("sqlite3", specs[1])
170 case "mysql":
171 // just connect to the database
172 inst.db, err = sql.Open("mysql", specs[1])
173 default:
174 return ErrSQLInvalidDatabaseSpec
175 }
176 // save database in pool
177 p.insts.Put(spec, inst)
178 }
179 // increment reference count
180 inst.refs++
181 // return a new connection to the database.
182 db.conn, err = inst.db.Conn(p.ctx)
183 return err
184 }, false)
185 return
186}