diff options
Diffstat (limited to 'src/gnunet/service/store/database.go')
-rw-r--r-- | src/gnunet/service/store/database.go | 186 |
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 | |||
19 | package store | ||
20 | |||
21 | import ( | ||
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 | ||
34 | var ( | ||
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. | ||
45 | type 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. | ||
52 | func (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 | ||
61 | func (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 | ||
66 | func (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 | ||
71 | func (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) | ||
83 | var ( | ||
84 | DBPool *dbPool | ||
85 | ) | ||
86 | |||
87 | // DBPoolEntry holds information about a database instance. | ||
88 | type 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 | ||
95 | func 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 | ||
103 | type 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. | ||
110 | func (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&...]"). | ||
140 | func (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 | } | ||