xref: /netbsd-src/external/bsd/kyua-cli/dist/store/backend.cpp (revision 6b3a42af15b5e090c339512c790dd68f3d11a9d8)
1*6b3a42afSjmmv // Copyright 2011 Google Inc.
2*6b3a42afSjmmv // All rights reserved.
3*6b3a42afSjmmv //
4*6b3a42afSjmmv // Redistribution and use in source and binary forms, with or without
5*6b3a42afSjmmv // modification, are permitted provided that the following conditions are
6*6b3a42afSjmmv // met:
7*6b3a42afSjmmv //
8*6b3a42afSjmmv // * Redistributions of source code must retain the above copyright
9*6b3a42afSjmmv //   notice, this list of conditions and the following disclaimer.
10*6b3a42afSjmmv // * Redistributions in binary form must reproduce the above copyright
11*6b3a42afSjmmv //   notice, this list of conditions and the following disclaimer in the
12*6b3a42afSjmmv //   documentation and/or other materials provided with the distribution.
13*6b3a42afSjmmv // * Neither the name of Google Inc. nor the names of its contributors
14*6b3a42afSjmmv //   may be used to endorse or promote products derived from this software
15*6b3a42afSjmmv //   without specific prior written permission.
16*6b3a42afSjmmv //
17*6b3a42afSjmmv // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18*6b3a42afSjmmv // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19*6b3a42afSjmmv // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20*6b3a42afSjmmv // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
21*6b3a42afSjmmv // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22*6b3a42afSjmmv // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23*6b3a42afSjmmv // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24*6b3a42afSjmmv // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25*6b3a42afSjmmv // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26*6b3a42afSjmmv // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27*6b3a42afSjmmv // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28*6b3a42afSjmmv 
29*6b3a42afSjmmv #include "store/backend.hpp"
30*6b3a42afSjmmv 
31*6b3a42afSjmmv #include <fstream>
32*6b3a42afSjmmv 
33*6b3a42afSjmmv #include "store/exceptions.hpp"
34*6b3a42afSjmmv #include "store/metadata.hpp"
35*6b3a42afSjmmv #include "store/transaction.hpp"
36*6b3a42afSjmmv #include "utils/env.hpp"
37*6b3a42afSjmmv #include "utils/format/macros.hpp"
38*6b3a42afSjmmv #include "utils/logging/macros.hpp"
39*6b3a42afSjmmv #include "utils/sanity.hpp"
40*6b3a42afSjmmv #include "utils/stream.hpp"
41*6b3a42afSjmmv #include "utils/sqlite/database.hpp"
42*6b3a42afSjmmv #include "utils/sqlite/exceptions.hpp"
43*6b3a42afSjmmv #include "utils/sqlite/statement.ipp"
44*6b3a42afSjmmv 
45*6b3a42afSjmmv namespace fs = utils::fs;
46*6b3a42afSjmmv namespace sqlite = utils::sqlite;
47*6b3a42afSjmmv 
48*6b3a42afSjmmv 
49*6b3a42afSjmmv /// The current schema version.
50*6b3a42afSjmmv ///
51*6b3a42afSjmmv /// Any new database gets this schema version.  Existing databases with an older
52*6b3a42afSjmmv /// schema version must be first migrated to the current schema with
53*6b3a42afSjmmv /// migrate_schema() before they can be used.
54*6b3a42afSjmmv ///
55*6b3a42afSjmmv /// This must be kept in sync with the value in the corresponding schema_vX.sql
56*6b3a42afSjmmv /// file, where X matches this version number.
57*6b3a42afSjmmv ///
58*6b3a42afSjmmv /// This variable is not const to allow tests to modify it.  No other code
59*6b3a42afSjmmv /// should change its value.
60*6b3a42afSjmmv int store::detail::current_schema_version = 2;
61*6b3a42afSjmmv 
62*6b3a42afSjmmv 
63*6b3a42afSjmmv namespace {
64*6b3a42afSjmmv 
65*6b3a42afSjmmv 
66*6b3a42afSjmmv /// Opens a database and defines session pragmas.
67*6b3a42afSjmmv ///
68*6b3a42afSjmmv /// This auxiliary function ensures that, every time we open a SQLite database,
69*6b3a42afSjmmv /// we define the same set of pragmas for it.
70*6b3a42afSjmmv ///
71*6b3a42afSjmmv /// \param file The database file to be opened.
72*6b3a42afSjmmv /// \param flags The flags for the open; see sqlite::database::open.
73*6b3a42afSjmmv ///
74*6b3a42afSjmmv /// \return The opened database.
75*6b3a42afSjmmv ///
76*6b3a42afSjmmv /// \throw store::error If there is a problem opening or creating the database.
77*6b3a42afSjmmv static sqlite::database
do_open(const fs::path & file,const int flags)78*6b3a42afSjmmv do_open(const fs::path& file, const int flags)
79*6b3a42afSjmmv {
80*6b3a42afSjmmv     try {
81*6b3a42afSjmmv         sqlite::database database = sqlite::database::open(file, flags);
82*6b3a42afSjmmv         database.exec("PRAGMA foreign_keys = ON");
83*6b3a42afSjmmv         return database;
84*6b3a42afSjmmv     } catch (const sqlite::error& e) {
85*6b3a42afSjmmv         throw store::error(F("Cannot open '%s': %s") % file % e.what());
86*6b3a42afSjmmv     }
87*6b3a42afSjmmv }
88*6b3a42afSjmmv 
89*6b3a42afSjmmv 
90*6b3a42afSjmmv /// Checks if a database is empty (i.e. if it is new).
91*6b3a42afSjmmv ///
92*6b3a42afSjmmv /// \param db The database to check.
93*6b3a42afSjmmv ///
94*6b3a42afSjmmv /// \return True if the database is empty.
95*6b3a42afSjmmv static bool
empty_database(sqlite::database & db)96*6b3a42afSjmmv empty_database(sqlite::database& db)
97*6b3a42afSjmmv {
98*6b3a42afSjmmv     sqlite::statement stmt = db.create_statement("SELECT * FROM sqlite_master");
99*6b3a42afSjmmv     return !stmt.step();
100*6b3a42afSjmmv }
101*6b3a42afSjmmv 
102*6b3a42afSjmmv 
103*6b3a42afSjmmv /// Performs a single migration step.
104*6b3a42afSjmmv ///
105*6b3a42afSjmmv /// \param db Open database to which to apply the migration step.
106*6b3a42afSjmmv /// \param version_from Current schema version in the database.
107*6b3a42afSjmmv /// \param version_to Schema version to migrate to.
108*6b3a42afSjmmv ///
109*6b3a42afSjmmv /// \throw error If there is a problem applying the migration.
110*6b3a42afSjmmv static void
migrate_schema_step(sqlite::database & db,const int version_from,const int version_to)111*6b3a42afSjmmv migrate_schema_step(sqlite::database& db, const int version_from,
112*6b3a42afSjmmv                     const int version_to)
113*6b3a42afSjmmv {
114*6b3a42afSjmmv     PRE(version_to == version_from + 1);
115*6b3a42afSjmmv 
116*6b3a42afSjmmv     const fs::path migration = store::detail::migration_file(version_from,
117*6b3a42afSjmmv                                                              version_to);
118*6b3a42afSjmmv 
119*6b3a42afSjmmv     std::ifstream input(migration.c_str());
120*6b3a42afSjmmv     if (!input)
121*6b3a42afSjmmv         throw store::error(F("Cannot open migration file '%s'") % migration);
122*6b3a42afSjmmv 
123*6b3a42afSjmmv     const std::string migration_string = utils::read_stream(input);
124*6b3a42afSjmmv     try {
125*6b3a42afSjmmv         db.exec(migration_string);
126*6b3a42afSjmmv     } catch (const sqlite::error& e) {
127*6b3a42afSjmmv         throw store::error(F("Schema migration failed: %s") % e.what());
128*6b3a42afSjmmv     }
129*6b3a42afSjmmv }
130*6b3a42afSjmmv 
131*6b3a42afSjmmv 
132*6b3a42afSjmmv }  // anonymous namespace
133*6b3a42afSjmmv 
134*6b3a42afSjmmv 
135*6b3a42afSjmmv /// Calculates the path to a schema migration file.
136*6b3a42afSjmmv ///
137*6b3a42afSjmmv /// \param version_from The version from which the database is being upgraded.
138*6b3a42afSjmmv /// \param version_to The version to which the database is being upgraded.
139*6b3a42afSjmmv ///
140*6b3a42afSjmmv /// \return The path to the installed migrate_vX_vY.sql file.
141*6b3a42afSjmmv fs::path
migration_file(const int version_from,const int version_to)142*6b3a42afSjmmv store::detail::migration_file(const int version_from, const int version_to)
143*6b3a42afSjmmv {
144*6b3a42afSjmmv     return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR))
145*6b3a42afSjmmv         / (F("migrate_v%s_v%s.sql") % version_from % version_to);
146*6b3a42afSjmmv }
147*6b3a42afSjmmv 
148*6b3a42afSjmmv 
149*6b3a42afSjmmv /// Calculates the path to the schema file for the database.
150*6b3a42afSjmmv ///
151*6b3a42afSjmmv /// \return The path to the installed schema_vX.sql file that matches the
152*6b3a42afSjmmv /// current_schema_version.
153*6b3a42afSjmmv fs::path
schema_file(void)154*6b3a42afSjmmv store::detail::schema_file(void)
155*6b3a42afSjmmv {
156*6b3a42afSjmmv     return fs::path(utils::getenv_with_default("KYUA_STOREDIR", KYUA_STOREDIR))
157*6b3a42afSjmmv         / (F("schema_v%s.sql") % current_schema_version);
158*6b3a42afSjmmv }
159*6b3a42afSjmmv 
160*6b3a42afSjmmv 
161*6b3a42afSjmmv /// Initializes an empty database.
162*6b3a42afSjmmv ///
163*6b3a42afSjmmv /// \param db The database to initialize.
164*6b3a42afSjmmv ///
165*6b3a42afSjmmv /// \return The metadata record written into the new database.
166*6b3a42afSjmmv ///
167*6b3a42afSjmmv /// \throw store::error If there is a problem initializing the database.
168*6b3a42afSjmmv store::metadata
initialize(sqlite::database & db)169*6b3a42afSjmmv store::detail::initialize(sqlite::database& db)
170*6b3a42afSjmmv {
171*6b3a42afSjmmv     PRE(empty_database(db));
172*6b3a42afSjmmv 
173*6b3a42afSjmmv     const fs::path schema = schema_file();
174*6b3a42afSjmmv 
175*6b3a42afSjmmv     std::ifstream input(schema.c_str());
176*6b3a42afSjmmv     if (!input)
177*6b3a42afSjmmv         throw error(F("Cannot open database schema '%s'") % schema);
178*6b3a42afSjmmv 
179*6b3a42afSjmmv     LI(F("Populating new database with schema from %s") % schema);
180*6b3a42afSjmmv     const std::string schema_string = utils::read_stream(input);
181*6b3a42afSjmmv     try {
182*6b3a42afSjmmv         db.exec(schema_string);
183*6b3a42afSjmmv 
184*6b3a42afSjmmv         const metadata metadata = metadata::fetch_latest(db);
185*6b3a42afSjmmv         LI(F("New metadata entry %s") % metadata.timestamp());
186*6b3a42afSjmmv         if (metadata.schema_version() != detail::current_schema_version) {
187*6b3a42afSjmmv             UNREACHABLE_MSG(F("current_schema_version is out of sync with "
188*6b3a42afSjmmv                               "%s") % schema);
189*6b3a42afSjmmv         }
190*6b3a42afSjmmv         return metadata;
191*6b3a42afSjmmv     } catch (const store::integrity_error& e) {
192*6b3a42afSjmmv         // Could be raised by metadata::fetch_latest.
193*6b3a42afSjmmv         UNREACHABLE_MSG("Inconsistent code while creating a database");
194*6b3a42afSjmmv     } catch (const sqlite::error& e) {
195*6b3a42afSjmmv         throw error(F("Failed to initialize database: %s") % e.what());
196*6b3a42afSjmmv     }
197*6b3a42afSjmmv }
198*6b3a42afSjmmv 
199*6b3a42afSjmmv 
200*6b3a42afSjmmv /// Backs up a database for schema migration purposes.
201*6b3a42afSjmmv ///
202*6b3a42afSjmmv /// \todo We should probably use the SQLite backup API instead of doing a raw
203*6b3a42afSjmmv /// file copy.  We issue our backup call with the database already open, but
204*6b3a42afSjmmv /// because it is quiescent, it's OK to do so.
205*6b3a42afSjmmv ///
206*6b3a42afSjmmv /// \param source Location of the database to be backed up.
207*6b3a42afSjmmv /// \param old_version Version of the database's CURRENT schema, used to
208*6b3a42afSjmmv ///     determine the name of the backup file.
209*6b3a42afSjmmv ///
210*6b3a42afSjmmv /// \throw error If there is a problem during the backup.
211*6b3a42afSjmmv void
backup_database(const fs::path & source,const int old_version)212*6b3a42afSjmmv store::detail::backup_database(const fs::path& source, const int old_version)
213*6b3a42afSjmmv {
214*6b3a42afSjmmv     const fs::path target(F("%s.v%s.backup") % source.str() % old_version);
215*6b3a42afSjmmv 
216*6b3a42afSjmmv     LI(F("Backing up database %s to %s") % source % target);
217*6b3a42afSjmmv 
218*6b3a42afSjmmv     std::ifstream input(source.c_str());
219*6b3a42afSjmmv     if (!input)
220*6b3a42afSjmmv         throw error(F("Cannot open database file %s") % source);
221*6b3a42afSjmmv 
222*6b3a42afSjmmv     std::ofstream output(target.c_str());
223*6b3a42afSjmmv     if (!output)
224*6b3a42afSjmmv         throw error(F("Cannot create database backup file %s") % target);
225*6b3a42afSjmmv 
226*6b3a42afSjmmv     char buffer[1024];
227*6b3a42afSjmmv     while (input.good()) {
228*6b3a42afSjmmv         input.read(buffer, sizeof(buffer));
229*6b3a42afSjmmv         if (input.good() || input.eof())
230*6b3a42afSjmmv             output.write(buffer, input.gcount());
231*6b3a42afSjmmv     }
232*6b3a42afSjmmv     if (!input.good() && !input.eof())
233*6b3a42afSjmmv         throw error(F("Error while reading input file %s") % source);
234*6b3a42afSjmmv }
235*6b3a42afSjmmv 
236*6b3a42afSjmmv 
237*6b3a42afSjmmv /// Internal implementation for the backend.
238*6b3a42afSjmmv struct store::backend::impl {
239*6b3a42afSjmmv     /// The SQLite database this backend talks to.
240*6b3a42afSjmmv     sqlite::database database;
241*6b3a42afSjmmv 
242*6b3a42afSjmmv     /// Constructor.
243*6b3a42afSjmmv     ///
244*6b3a42afSjmmv     /// \param database_ The SQLite database instance.
245*6b3a42afSjmmv     /// \param metadata_ The metadata for the loaded database.  This must match
246*6b3a42afSjmmv     ///     the schema version we implement in this module; otherwise, a
247*6b3a42afSjmmv     ///     migration is necessary.
248*6b3a42afSjmmv     ///
249*6b3a42afSjmmv     /// \throw integrity_error If the schema in the database is too modern,
250*6b3a42afSjmmv     ///     which might indicate some form of corruption or an old binary.
251*6b3a42afSjmmv     /// \throw old_schema_error If the schema in the database is older than our
252*6b3a42afSjmmv     ///     currently-implemented version and needs an upgrade.  The caller can
253*6b3a42afSjmmv     ///     use migrate_schema() to fix this problem.
implstore::backend::impl254*6b3a42afSjmmv     impl(sqlite::database& database_, const metadata& metadata_) :
255*6b3a42afSjmmv         database(database_)
256*6b3a42afSjmmv     {
257*6b3a42afSjmmv         const int database_version = metadata_.schema_version();
258*6b3a42afSjmmv 
259*6b3a42afSjmmv         if (database_version == detail::current_schema_version) {
260*6b3a42afSjmmv             // OK.
261*6b3a42afSjmmv         } else if (database_version < detail::current_schema_version) {
262*6b3a42afSjmmv             throw old_schema_error(database_version);
263*6b3a42afSjmmv         } else if (database_version > detail::current_schema_version) {
264*6b3a42afSjmmv             throw integrity_error(
265*6b3a42afSjmmv                 F("Database at schema version %s, which is newer than the "
266*6b3a42afSjmmv                   "supported version %s")
267*6b3a42afSjmmv                 % database_version % detail::current_schema_version);
268*6b3a42afSjmmv         }
269*6b3a42afSjmmv     }
270*6b3a42afSjmmv };
271*6b3a42afSjmmv 
272*6b3a42afSjmmv 
273*6b3a42afSjmmv /// Constructs a new backend.
274*6b3a42afSjmmv ///
275*6b3a42afSjmmv /// \param pimpl_ The internal data.
backend(impl * pimpl_)276*6b3a42afSjmmv store::backend::backend(impl* pimpl_) :
277*6b3a42afSjmmv     _pimpl(pimpl_)
278*6b3a42afSjmmv {
279*6b3a42afSjmmv }
280*6b3a42afSjmmv 
281*6b3a42afSjmmv 
282*6b3a42afSjmmv /// Destructor.
~backend(void)283*6b3a42afSjmmv store::backend::~backend(void)
284*6b3a42afSjmmv {
285*6b3a42afSjmmv }
286*6b3a42afSjmmv 
287*6b3a42afSjmmv 
288*6b3a42afSjmmv /// Opens a database in read-only mode.
289*6b3a42afSjmmv ///
290*6b3a42afSjmmv /// \param file The database file to be opened.
291*6b3a42afSjmmv ///
292*6b3a42afSjmmv /// \return The backend representation.
293*6b3a42afSjmmv ///
294*6b3a42afSjmmv /// \throw store::error If there is any problem opening the database.
295*6b3a42afSjmmv store::backend
open_ro(const fs::path & file)296*6b3a42afSjmmv store::backend::open_ro(const fs::path& file)
297*6b3a42afSjmmv {
298*6b3a42afSjmmv     sqlite::database db = do_open(file, sqlite::open_readonly);
299*6b3a42afSjmmv     return backend(new impl(db, metadata::fetch_latest(db)));
300*6b3a42afSjmmv }
301*6b3a42afSjmmv 
302*6b3a42afSjmmv 
303*6b3a42afSjmmv /// Opens a database in read-write mode and creates it if necessary.
304*6b3a42afSjmmv ///
305*6b3a42afSjmmv /// \param file The database file to be opened.
306*6b3a42afSjmmv ///
307*6b3a42afSjmmv /// \return The backend representation.
308*6b3a42afSjmmv ///
309*6b3a42afSjmmv /// \throw store::error If there is any problem opening or creating
310*6b3a42afSjmmv ///     the database.
311*6b3a42afSjmmv store::backend
open_rw(const fs::path & file)312*6b3a42afSjmmv store::backend::open_rw(const fs::path& file)
313*6b3a42afSjmmv {
314*6b3a42afSjmmv     sqlite::database db = do_open(file, sqlite::open_readwrite |
315*6b3a42afSjmmv                                   sqlite::open_create);
316*6b3a42afSjmmv     if (empty_database(db))
317*6b3a42afSjmmv         return backend(new impl(db, detail::initialize(db)));
318*6b3a42afSjmmv     else
319*6b3a42afSjmmv         return backend(new impl(db, metadata::fetch_latest(db)));
320*6b3a42afSjmmv }
321*6b3a42afSjmmv 
322*6b3a42afSjmmv 
323*6b3a42afSjmmv /// Gets the connection to the SQLite database.
324*6b3a42afSjmmv ///
325*6b3a42afSjmmv /// \return A database connection.
326*6b3a42afSjmmv sqlite::database&
database(void)327*6b3a42afSjmmv store::backend::database(void)
328*6b3a42afSjmmv {
329*6b3a42afSjmmv     return _pimpl->database;
330*6b3a42afSjmmv }
331*6b3a42afSjmmv 
332*6b3a42afSjmmv 
333*6b3a42afSjmmv /// Opens a transaction.
334*6b3a42afSjmmv ///
335*6b3a42afSjmmv /// \return A new transaction.
336*6b3a42afSjmmv store::transaction
start(void)337*6b3a42afSjmmv store::backend::start(void)
338*6b3a42afSjmmv {
339*6b3a42afSjmmv     return transaction(*this);
340*6b3a42afSjmmv }
341*6b3a42afSjmmv 
342*6b3a42afSjmmv 
343*6b3a42afSjmmv /// Migrates the schema of a database to the current version.
344*6b3a42afSjmmv ///
345*6b3a42afSjmmv /// The algorithm implemented here performs a migration step for every
346*6b3a42afSjmmv /// intermediate version between the schema version in the database to the
347*6b3a42afSjmmv /// version implemented in this file.  This should permit upgrades from
348*6b3a42afSjmmv /// arbitrary old databases.
349*6b3a42afSjmmv ///
350*6b3a42afSjmmv /// \param file The database whose schema to upgrade.
351*6b3a42afSjmmv ///
352*6b3a42afSjmmv /// \throw error If there is a problem with the migration.
353*6b3a42afSjmmv void
migrate_schema(const utils::fs::path & file)354*6b3a42afSjmmv store::migrate_schema(const utils::fs::path& file)
355*6b3a42afSjmmv {
356*6b3a42afSjmmv     sqlite::database db = do_open(file, sqlite::open_readwrite);
357*6b3a42afSjmmv 
358*6b3a42afSjmmv     const int version_from = metadata::fetch_latest(db).schema_version();
359*6b3a42afSjmmv     const int version_to = detail::current_schema_version;
360*6b3a42afSjmmv     if (version_from == version_to) {
361*6b3a42afSjmmv         throw error(F("Database already at schema version %s; migration not "
362*6b3a42afSjmmv                       "needed") % version_from);
363*6b3a42afSjmmv     } else if (version_from > version_to) {
364*6b3a42afSjmmv         throw error(F("Database at schema version %s, which is newer than the "
365*6b3a42afSjmmv                       "supported version %s") % version_from % version_to);
366*6b3a42afSjmmv     }
367*6b3a42afSjmmv 
368*6b3a42afSjmmv     detail::backup_database(file, version_from);
369*6b3a42afSjmmv 
370*6b3a42afSjmmv     for (int i = version_from; i < version_to; ++i) {
371*6b3a42afSjmmv         LI(F("Migrating schema from version %s to %s") % i % (i + 1));
372*6b3a42afSjmmv         migrate_schema_step(db, i, i + 1);
373*6b3a42afSjmmv     }
374*6b3a42afSjmmv }
375