Commit 9d476a82 authored by Jyrki's avatar Jyrki :feet:
Browse files

Add DBManager class and schema migrations

parent 77c570ec
Showing with 349 additions and 6 deletions
+349 -6
# Database migrations
This directory contains the database schema migrations and a script to generate
the `DbManager::migrateFrom` method. It uses the excellent [Sequel][sequel]
library for defining the schema and performing migrations.
## Usage
Create a new migration in the `./migrations` directory:
```
cat > ./migrations/`date +%Y%m%d%H%M%S`_my_migration.rb <<EOF
Sequel.migration do
change do
# your schema changes go here
end
end
EOF
```
Run `./gen_migration_data.rb`. This will run the schema migrations from the
very beginning to the end using an in-memory SQLite database, collect all the
DDL statements it produces, and finally create a C++ header which defines the
`DbManager::migrateFrom` method. That method then runs those statements in the
main Qt application.
## But... why?
I am way too lazy to write my own SQL statements for schema definitions and
changes. Sorry not sorry ¯\\\_(ツ)\_
[sequel]: http://sequel.jeremyevans.net/
GEM
remote: https://rubygems.org/
specs:
erubis (2.7.0)
sequel (5.8.0)
sqlite3 (1.3.13)
PLATFORMS
ruby
DEPENDENCIES
erubis (~> 2.7)
sequel (~> 5.8)
sqlite3 (~> 1.3)
BUNDLED WITH
1.16.1
# frozen_string_literal: true
source 'https://rubygems.org'
gem 'erubis', '~> 2.7'
gem 'sequel', '~> 5.8'
gem 'sqlite3', '~> 1.3'
#!/usr/bin/env ruby
# generates db schema + migration code
require 'bundler'
Bundler.setup :default
require 'sequel'
require 'sequel/extensions/migration'
require 'sequel/adapters/sqlite'
require 'erubis'
# generic collector. collects things by adding them to a collection hash
class Collector
attr_reader :collection
attr_accessor :current
def initialize
@collection = {}
reset_current
end
def collect(thing)
collection[current] ||= []
collection[current] << thing
end
def reset_current
self.current = 'unknown'
end
end
# module to collect sql statements (only ddl for now)
class SQLCollector < Module
def initialize(collector)
define_method :collector do
collector
end
define_method :execute_ddl do |sql, *rest|
collector.collect(sql)
super(sql, *rest)
end
end
end
# custom migrator which sets the collector's +current+ to the file name of the
# migration
class CollectorMigrator < Sequel::TimestampMigrator
# https://github.com/jeremyevans/sequel/blob/2241ce3/lib/sequel/extensions/migration.rb#L678
def run # rubocop:disable Metrics/AbcSize
migration_tuples.each do |m, f, direction|
checked_transaction(m) do
db.collector.current = f
m.apply(db, direction)
fi = f.downcase
direction == :up ? ds.insert(column => fi) : ds.where(column => fi).delete # rubocop:disable Metrics/LineLength
db.collector.reset_current
end
end
nil
end
end
###############################################################################
collector = Collector.new
Sequel::SQLite::Database.prepend(SQLCollector.new(collector))
Sequel.extension :migration, :core_extensions
DB = Sequel.connect('sqlite:/')
CollectorMigrator.apply(DB, './migrations')
@migrations = collector
.collection
.reject { |(k, _v)| k == 'unknown' }
.map { |(k, v)| [k.split('_', 2).first, v] }
.to_h
Dir['./templates/*.erb'].each do |template_file_path|
target_file_name = File.basename(template_file_path, '.erb')
target_file_path = File.join(__dir__, 'inc', target_file_name)
puts "generating `#{target_file_name}'"
File.open(target_file_path, 'w') do |f|
f.puts Erubis::Eruby.new(File.read(template_file_path)).result(binding)
end
end
#ifndef DBMANAGER_MIGRATE_FROM
#define DBMANAGER_MIGRATE_FROM
// This file was generated by `/db/gen_migration_data.rb`.
// Last changed at: 2018-05-05 13:41:22 +0200
void DbManager::migrateFrom(qlonglong version)
{
QSqlQuery query;
if (version < 20180505002706)
{
// qDebug() << "migrating to version 20180505002706";
query.exec("CREATE TABLE `accounts` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `username` varchar(255) NOT NULL)");
query.exec("CREATE UNIQUE INDEX `accounts_unique_username` ON `accounts` (`username`)");
query.exec("CREATE TABLE `cookies` (`id` integer NOT NULL PRIMARY KEY AUTOINCREMENT, `account_id` integer NOT NULL REFERENCES `accounts`, `name` varchar(255) NOT NULL, `value` varchar(255) NOT NULL, `cookies_unique_account_name` add_index UNIQUE)");
query.exec("INSERT INTO `schema_migrations` (`version`) VALUES (20180505002706)");
version = 20180505002706;
// qDebug() << "====== done ========";
}
}
#endif // DBMANAGER_MIGRATE_FROM
Sequel.migration do
change do
create_table(:accounts) do
primary_key :id
String :username, null: false, index: { name: 'accounts_unique_username', unique: true }
end
create_table(:cookies) do
primary_key :id
foreign_key :account_id, :accounts, null: false
String :name, null: false
String :value, null: false
add_index %i[account_id name], name: 'cookies_unique_account_name', unique: true
end
end
end
#ifndef DBMANAGER_MIGRATE_FROM
#define DBMANAGER_MIGRATE_FROM
// This file was generated by `/db/gen_migration_data.rb`.
// Last changed at: <%= Time.now %>
void DbManager::migrateFrom(qlonglong version)
{
QSqlQuery query;
<% @migrations.each do |version, statements| %>
if (version < <%= version %>)
{
// qDebug() << "migrating to version <%= version %>";
<% statements.each do |statement| %>
query.exec(<%= statement.inspect %>);
<% end %>
query.exec("INSERT INTO `schema_migrations` (`version`) VALUES (<%= version %>)");
version = <%= version %>;
// qDebug() << "====== done ========";
}
<% end %>
}
#endif // DBMANAGER_MIGRATE_FROM
......@@ -4,7 +4,7 @@
#
#-------------------------------------------------
QT += core gui widgets
QT += core gui widgets sql
TARGET = nFurAffinity
TEMPLATE = app
......@@ -20,17 +20,22 @@ DEFINES += QT_DEPRECATED_WARNINGS
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0
INCLUDEPATH += src/
INCLUDEPATH += \
src/ \
db/inc/
SOURCES += \
src/main.cpp \
src/mainwindow.cpp \
src/furaffinityclient.cpp
src/furaffinityclient.cpp \
src/dbmanager.cpp
HEADERS += \
src/mainwindow.h \
src/furaffinityclient.h \
src/settings/nsettings.h
src/settings/nsettings.h \
src/dbmanager.h \
db/inc/dbmanager_migrate_from.h
FORMS += \
src/mainwindow.ui
#include "dbmanager.h"
#include <QDir>
#include <QVariant>
#include <QStandardPaths>
#include <QSqlQuery>
#include <QSqlError>
#include <QSqlRecord>
#include "dbmanager_migrate_from.h"
DbManager::DbManager(QObject *parent) : QObject(parent)
{
db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(dbPath());
db.open();
}
DbManager::~DbManager()
{
db.close();
}
void DbManager::init()
{
auto db = new DbManager;
db->runMigrations();
db->deleteLater();
}
//!
//! \brief DbManager::runMigrations runs the database migrations by first
//! fetching the last migration version from the database and then running
//! `migrateFrom` for that last version.
//!
void DbManager::runMigrations()
{
QSqlQuery query;
if (!query.exec("SELECT NULL AS 'nil' FROM `schema_migrations` LIMIT 1"))
{
// TODO: maybe check the error code. we will just assume this table
// does not exist yet.
query.exec("CREATE TABLE `schema_migrations` "
"(`version` INTEGER NOT NULL PRIMARY KEY)");
}
query.exec("SELECT `version` FROM `schema_migrations`"
"ORDER BY `version`");
qlonglong lastVersion = 0;
while (query.next())
{
lastVersion = query.value("version").toLongLong();
}
migrateFrom(lastVersion);
}
//!
//! \brief DbManager::dbPath returns the full path to the DB.
//! \return path to the DB
//!
QString DbManager::dbPath()
{
auto basePath =
QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)
.at(0);
auto dbPath = QDir(basePath).filePath("application.db");
return dbPath;
}
#ifndef DBMANAGER_H
#define DBMANAGER_H
#include <QObject>
#include <QSqlDatabase>
class DbManager : public QObject
{
Q_OBJECT
public:
explicit DbManager(QObject *parent = nullptr);
~DbManager();
static void init();
signals:
public slots:
private:
static QString dbPath();
void runMigrations();
//!
//! \brief migrateFrom migrates the database schema from `version` to the
//! most recent version of the schema. It is generated by a Ruby script in
//! the `/db` directory.
//! \param version version to migrate the schema from
//!
void migrateFrom(qlonglong version);
QSqlDatabase db;
};
#endif // DBMANAGER_H
#include "mainwindow.h"
#include <QApplication>
#include <QDir>
#include <QStandardPaths>
#include "dbmanager.h"
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
QCoreApplication::setApplicationName("nFurAffinity");
QCoreApplication::setOrganizationName("nilsding");
QApplication a(argc, argv);
// create app data dir if it does not exist yet
auto appDataPath =
QStandardPaths::standardLocations(QStandardPaths::AppDataLocation)
.at(0);
auto appDataDir = QDir(appDataPath);
if (!appDataDir.exists())
{
appDataDir.mkpath(appDataPath);
}
// initialise database and run migrations if needed
DbManager::init();
// finally, run the application
MainWindow w;
w.show();
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment