/*
This file is part of libdjinterop.
libdjinterop is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
libdjinterop is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with libdjinterop. If not, see .
*/
#include
#include
#include
#include
#include
#include
#include
namespace djinterop
{
namespace enginelibrary
{
using djinterop::crate;
using djinterop::track;
// Note that crates in the Engine Library format may exist either at top/root
// level, or be sub-crates underneath another crate. This information is
// encoded redundantly in multiple places in the EL database schema:
//
// * Crate (id, title, path)
// The `path` field is a semicolon-delimited string of crate titles,
// representing the path from the root to the current crate. Note that
// there is always an additional trailing semicolon in this field. As such,
// semicolon is a prohibited character in crate names.
//
// * CrateParentList (crateOriginId, crateParentId)
// Every crate is specified as having precisely one immediate parent. A
// top-level crate is said to have itself as parent. The crate id is
// written to the `crateOriginId` field, and the parent (or itself) is
// written to the `crateParentId` field.
//
// * CrateHierarchy (crateId, crateIdChild)
// The denormalised/flattened inheritance hierarchy is written to this
// table, whereby the id of every descendant (not child) of a crate is
// written to the `crateIdChild` field. Note that the reflexive
// relationship is not written to this table.
namespace
{
void update_path(
sqlite::database& music_db, crate cr, const std::string& parent_path)
{
// update path
std::string path = parent_path + cr.name() + ';';
music_db << "UPDATE Crate SET path = ? WHERE id = ?" << path << cr.id();
// recursive call in order to update the path of indirect descendants
for (crate cr2 : cr.children())
{
update_path(music_db, cr2, path);
}
}
void ensure_valid_name(const std::string& name)
{
if (name == "")
{
throw djinterop::crate_invalid_name{
"Crate names must be non-empty", name};
}
else if (name.find_first_of(';') != std::string::npos)
{
throw djinterop::crate_invalid_name{
"Crate names must not contain semicolons", name};
}
}
} // namespace
el_crate_impl::el_crate_impl(std::shared_ptr storage, int64_t id) :
crate_impl{id}, storage_{std::move(storage)}
{
}
void el_crate_impl::add_track(int64_t track_id)
{
el_transaction_guard_impl trans{storage_};
storage_->db
<< "DELETE FROM CrateTrackList WHERE crateId = ? AND trackId = ?"
<< id() << track_id;
storage_->db
<< "INSERT INTO CrateTrackList (crateId, trackId) VALUES (?, ?)" << id()
<< track_id;
trans.commit();
}
void el_crate_impl::add_track(track tr)
{
add_track(tr.id());
}
std::vector el_crate_impl::children()
{
std::vector results;
storage_->db << "SELECT crateIdChild FROM CrateHierarchy WHERE crateId = ?"
<< id() >>
[&](int64_t crate_id_child) {
results.emplace_back(
std::make_shared(storage_, crate_id_child));
};
return results;
}
void el_crate_impl::clear_tracks()
{
storage_->db << "DELETE FROM CrateTrackList WHERE crateId = ?" << id();
}
crate el_crate_impl::create_sub_crate(std::string name)
{
ensure_valid_name(name);
el_transaction_guard_impl trans{storage_};
std::string path;
storage_->db << "SELECT path FROM Crate WHERE id = ?" << id() >>
[&](std::string path_val) {
if (path.empty())
{
path = std::move(path_val);
}
else
{
throw crate_database_inconsistency{
"More than one crate for the same id", id()};
}
};
int64_t sub_id;
if (storage_->version >= version_1_9_1)
{
// Newer schemas consider crates to be a kind of 'list', and so the
// `Crate` table has been replaced with a VIEW onto `List`. The main
// difference is that `List` does not have an integer primary key, so
// the new id will need to be determined in advance.
storage_->db << "SELECT IFNULL(MAX(id), 0) + 1 FROM Crate" >> sub_id;
storage_->db << "INSERT INTO Crate (id, title, path) VALUES (?, ?, ?)"
<< sub_id << name.data() << (path + name + ";");
}
else
{
// Older schema versions have a dedicated table for crates that has
// an integer primary key, which will be filled automatically.
storage_->db << "INSERT INTO Crate (title, path) VALUES (?, ?)"
<< name.data() << (path + name + ";");
sub_id = storage_->db.last_insert_rowid();
}
storage_->db << "INSERT INTO CrateParentList (crateOriginId, "
"crateParentId) VALUES (?, ?)"
<< sub_id << id();
storage_->db << "INSERT INTO CrateHierarchy (crateId, crateIdChild) "
"SELECT crateId, ? FROM CrateHierarchy "
"WHERE crateIdChild = ? "
"UNION "
"SELECT ? AS crateId, ? AS crateIdChild"
<< sub_id << id() << id() << sub_id;
crate cr{std::make_shared(storage_, sub_id)};
trans.commit();
return cr;
}
database el_crate_impl::db()
{
return database{std::make_shared(storage_)};
}
std::vector el_crate_impl::descendants()
{
std::vector results;
storage_->db
<< "SELECT crateOriginId FROM CrateParentList WHERE crateParentId "
"= ? AND crateOriginId <> crateParentId"
<< id() >>
[&](int64_t descendant_id) {
results.push_back(crate{
std::make_shared(storage_, descendant_id)});
};
return results;
}
bool el_crate_impl::is_valid()
{
bool valid = false;
storage_->db << "SELECT COUNT(*) FROM Crate WHERE id = ?" << id() >>
[&](int count) {
if (count == 1)
{
valid = true;
}
else if (count > 1)
{
throw crate_database_inconsistency{
"More than one crate with the same ID", id()};
}
};
return valid;
}
std::string el_crate_impl::name()
{
stdx::optional name;
storage_->db << "SELECT title FROM Crate WHERE id = ?" << id() >>
[&](std::string title) {
if (!name)
{
name = std::move(title);
}
else
{
throw crate_database_inconsistency{
"More than one crate with the same ID", id()};
}
};
if (!name)
{
throw crate_deleted{id()};
}
return *name;
}
stdx::optional el_crate_impl::parent()
{
stdx::optional parent;
storage_->db
<< "SELECT crateParentId FROM CrateParentList WHERE crateOriginId "
"= ? AND crateParentId <> crateOriginId"
<< id() >>
[&](int64_t parent_id) {
if (!parent)
{
parent =
crate{std::make_shared(storage_, parent_id)};
}
else
{
throw crate_database_inconsistency{
"More than one parent crate for the same crate", id()};
}
};
return parent;
}
void el_crate_impl::remove_track(track tr)
{
storage_->db
<< "DELETE FROM CrateTrackList WHERE crateId = ? AND trackId = ?"
<< id() << tr.id();
}
void el_crate_impl::set_name(std::string name)
{
ensure_valid_name(name);
el_transaction_guard_impl trans{storage_};
// obtain parent's `path`
std::string parent_path;
storage_->db
<< "SELECT path FROM Crate c JOIN CrateParentList cpl ON c.id = "
"cpl.crateParentId WHERE cpl.crateOriginId = ? AND "
"cpl.crateOriginId <> cpl.crateParentId"
<< id() >>
[&](std::string path) {
if (parent_path.empty())
{
parent_path = std::move(path);
}
else
{
throw crate_database_inconsistency{
"More than one parent crate for the same crate", id()};
}
};
// update name and path
std::string path = std::move(parent_path) + name.data() + ';';
storage_->db << "UPDATE Crate SET title = ?, path = ? WHERE id = ?"
<< name.data() << path << id();
// call the lambda in order to update the path of direct children
for (crate cr : children())
{
update_path(storage_->db, cr, path);
}
trans.commit();
}
void el_crate_impl::set_parent(stdx::optional parent)
{
el_transaction_guard_impl trans{storage_};
storage_->db << "DELETE FROM CrateParentList WHERE crateOriginId = ?"
<< id();
storage_->db << "INSERT INTO CrateParentList (crateOriginId, "
"crateParentId) VALUES (?, ?)"
<< id() << (parent ? parent->id() : id());
storage_->db << "DELETE FROM CrateHierarchy WHERE crateIdChild = ?" << id();
if (parent)
{
storage_->db
<< "INSERT INTO CrateHierarchy (crateId, crateIdChild) SELECT "
"crateId, ? FROM CrateHierarchy WHERE crateIdChild = ? UNION "
"SELECT ? AS crateId, ? AS crateIdChild"
<< id() << parent->id() << parent->id() << id();
}
trans.commit();
}
stdx::optional el_crate_impl::sub_crate_by_name(const std::string& name)
{
stdx::optional cr;
storage_->db << "SELECT cr.id FROM Crate cr "
"JOIN CrateParentList cpl ON (cpl.crateOriginId = cr.id) "
"WHERE cr.title = ? "
"AND cpl.crateParentId = ? "
"ORDER BY cr.id"
<< name.data() << id() >>
[&](int64_t id) {
cr = crate{std::make_shared(storage_, id)};
};
return cr;
}
std::vector