3#include <Poco/Data/RecordSet.h>
4#include <Poco/Data/SQLite/Connector.h>
5#include <Poco/Data/Session.h>
6#include <Poco/Dynamic/Var.h>
7#include <Poco/JSON/Array.h>
8#include <Poco/JSON/Object.h>
9#include <Poco/JSON/Parser.h>
10#include <Poco/JSON/Stringifier.h>
11#include <simplesquirrel/simplesquirrel.hpp>
20std::string quoteIdentifier(
const std::string&
name) {
21 if (
name.empty())
throw std::invalid_argument(
"database identifier must not be empty");
22 std::string quoted =
"\"";
24 if (
c ==
'\0')
throw std::invalid_argument(
"database identifier contains NUL");
25 quoted +=
c ==
'"' ?
"\"\"" : std::string(1,
c);
31 if (std::holds_alternative<std::monostate>(
value))
return "NULL";
32 if (
auto p = std::get_if<std::int64_t>(&
value))
return std::to_string(*
p);
33 if (
auto p = std::get_if<double>(&
value))
return std::to_string(*
p);
34 if (
auto p = std::get_if<bool>(&
value))
return *
p ?
"1" :
"0";
36 for (
char c : std::get<std::string>(
value)) escaped +=
c ==
'\'' ?
"''" : std::string(1,
c);
37 return "'" + escaped +
"'";
40Value fromDynamic(
const Poco::Dynamic::Var&
value) {
41 if (
value.isEmpty())
return std::monostate{};
42 if (
value.isBoolean())
return value.convert<
bool>();
43 if (
value.isInteger())
return value.convert<Poco::Int64>();
44 if (
value.isNumeric())
return value.convert<
double>();
45 return value.convert<std::string>();
48Row parseObject(
const std::string& json) {
49 auto object = Poco::JSON::Parser().parse(json).extract<Poco::JSON::Object::Ptr>();
51 for (
const auto&
name :
object->getNames()) row[
name] = fromDynamic(
object->get(
name));
55Poco::Dynamic::Var toDynamic(
const Value&
value);
56Value parseScalar(
const std::string& json) {
57 auto wrapper = Poco::JSON::Parser().parse(
"[" + json +
"]").extract<Poco::JSON::Array::Ptr>();
58 if (wrapper->size() != 1)
throw std::invalid_argument(
"ORM id must be one JSON scalar");
59 auto value = wrapper->get(0);
61 throw std::invalid_argument(
"ORM id must be a JSON scalar");
62 return fromDynamic(
value);
65std::string rowsToJson(
const std::vector<Row>&
source) {
66 Poco::JSON::Array
rows;
67 for (
const auto& row :
source) {
73 Poco::JSON::Stringifier::stringify(
rows,
output);
77Poco::Dynamic::Var toDynamic(
const Value&
value) {
78 if (std::holds_alternative<std::monostate>(
value))
return {};
79 if (
auto p = std::get_if<std::int64_t>(&
value))
return Poco::Dynamic::Var(Poco::Int64(*
p));
80 if (
auto p = std::get_if<double>(&
value))
return Poco::Dynamic::Var(*
p);
81 if (
auto p = std::get_if<bool>(&
value))
return Poco::Dynamic::Var(*
p);
82 return Poco::Dynamic::Var(std::get<std::string>(
value));
88 Poco::Data::SQLite::Connector::registerConnector();
89 session_ = std::make_unique<Poco::Data::Session>(connector, connectionString);
95 if (session_ && session_->isConnected()) session_->close();
99 if (!
isConnected())
throw std::runtime_error(
"database connection is closed");
100 return static_cast<int>((*session_ << sql).
execute());
104 if (!
isConnected())
throw std::runtime_error(
"database connection is closed");
105 Poco::Data::Statement statement(*session_);
108 Poco::Data::RecordSet records(statement);
109 std::vector<Row> result;
110 for (std::size_t rowIndex = 0; rowIndex < records.rowCount(); ++rowIndex) {
113 row[records.columnName(
column)] = fromDynamic(records.value(
column, rowIndex));
114 result.push_back(std::move(row));
122 if (
values.empty())
throw std::invalid_argument(
"insert values must not be empty");
132 return execute(
"INSERT INTO " + quoteIdentifier(table) +
" (" +
columns +
") VALUES (" + data +
")");
136 if (
values.empty())
throw std::invalid_argument(
"update values must not be empty");
137 std::string assignments;
139 if (!assignments.empty()) assignments +=
',';
142 return execute(
"UPDATE " + quoteIdentifier(table) +
" SET " + assignments +
143 (whereClause.empty() ?
"" :
" WHERE " + whereClause));
147 return execute(
"DELETE FROM " + quoteIdentifier(table) + (whereClause.empty() ?
"" :
" WHERE " + whereClause));
151 return insert(table, parseObject(json));
154 return update(table, parseObject(json), whereClause);
166 return query(
"SELECT * FROM " + quoteIdentifier(table) +
" WHERE " + quoteIdentifier(
key) +
"=" +
literal(
id) +
171 return new Model(
this, std::move(table), std::move(primaryKey));
176 if (!connection_)
throw std::invalid_argument(
"query requires a connection");
180 static const std::vector<std::string> allowed{
"=",
"!=",
"<",
"<=",
">",
">=",
"LIKE"};
181 if (std::find(allowed.begin(), allowed.end(), op) == allowed.end())
182 throw std::invalid_argument(
"unsupported query operator: " + op);
192 ordering_.push_back(quoteIdentifier(
column) + (ascending ?
" ASC" :
" DESC"));
197 if (
count < 0)
throw std::invalid_argument(
"query limit must not be negative");
203 if (
count < 0)
throw std::invalid_argument(
"query offset must not be negative");
208std::string Query::selectSql(
bool countOnly,
int forcedLimit)
const {
209 std::string sql = countOnly ?
"SELECT COUNT(*) AS count FROM " :
"SELECT * FROM ";
210 sql += quoteIdentifier(table_);
211 for (std::size_t i = 0; i < predicates_.size(); ++i) sql += (i == 0 ?
" WHERE " :
" AND ") + predicates_[i];
212 if (!countOnly && !ordering_.empty()) {
214 for (std::size_t i = 0; i < ordering_.size(); ++i) sql += (i ?
"," :
"") + ordering_[i];
216 int effectiveLimit = forcedLimit >= 0 ? forcedLimit : limit_;
217 if (!countOnly && effectiveLimit >= 0)
218 sql +=
" LIMIT " + std::to_string(effectiveLimit);
219 else if (!countOnly && offset_ > 0)
221 if (!countOnly && offset_ > 0) sql +=
" OFFSET " + std::to_string(offset_);
229 auto rows = connection_->
query(selectSql(
true));
230 if (
rows.empty())
return 0;
232 if (
auto integer = std::get_if<std::int64_t>(&
value))
return static_cast<int>(*integer);
233 if (
auto number = std::get_if<double>(&
value))
return static_cast<int>(*number);
234 if (
auto text = std::get_if<std::string>(&
value))
return std::stoi(*
text);
235 throw std::runtime_error(
"database COUNT returned a non-numeric value");
239 : connection_(connection), table_(
std::move(table)), primaryKey_(
std::move(primaryKey)) {
240 if (!connection_)
throw std::invalid_argument(
"ORM model requires a connection");
246 return rowsToJson(connection_->
queryByKey(table_, primaryKey_, parseScalar(idJson)));
249 return connection_->
updateByKey(table_, parseObject(json), primaryKey_, parseScalar(idJson));
252 return connection_->
removeByKey(table_, primaryKey_, parseScalar(idJson));
258 return new Connection(std::move(connector), std::move(connectionString));
262void Database::expose(ssq::Table& table) {
263 auto module = table.addClass(name, Database::create, false);
266 "DatabaseConnection", std::function<Connection*()>([] {
return new Connection(
"SQLite",
":memory:"); }),
true);
278 table.addClass<Query>(
"DatabaseQuery", std::function<Query*()>([]() -> Query* {
return nullptr; }),
true);
288 table.addClass<Model>(
"DatabaseModel", std::function<Model*()>([]() -> Model* {
return nullptr; }),
true);
297void Database::expose(ssq::Class&
cls) {
std::map< std::string, Var > values
#define Module_IMPL(ModuleName, newExpr)
const UnitySourceAsset & source
virtual std::string getName() const =0
Returns the name.
SQLite connection with a small ORM layer on top of Poco::Data. Tables/columns are quoted identifiers;...
std::string queryJson(const std::string &sql)
Runs query() and serializes the rows to a JSON array.
std::vector< Row > query(const std::string &sql)
Runs a SELECT and returns every row as name/value maps.
void close()
Closes the session (idempotent).
Query * from(std::string table)
Builds a fluent Query builder for a table.
std::vector< Row > queryByKey(const std::string &table, const std::string &key, const Value &id)
Selects rows where key == id.
int update(const std::string &table, const Row &values, const std::string &whereClause)
Updates rows matching whereClause; returns the affected row count.
int insert(const std::string &table, const Row &values)
Inserts a row map; returns the affected row count.
int remove(const std::string &table, const std::string &whereClause)
Deletes rows matching whereClause; returns the affected row count.
int removeByKey(const std::string &table, const std::string &key, const Value &id)
Deletes a single row by primary key value.
bool isConnected() const
True while the underlying session is open.
Connection(std::string connector, std::string connectionString)
Opens a Poco::Data session; connector is e.g. "SQLite".
Model * model(std::string table, std::string primaryKey="id")
Builds a script-facing Model repository for a table.
int execute(const std::string &sql)
Executes a statement; returns the affected row count.
int updateJson(const std::string &table, const std::string &json, const std::string &whereClause)
Script-friendly update; whereClause is raw SQL.
int insertJson(const std::string &table, const std::string &json)
Script-friendly insert: JSON must be an object of column/value pairs.
int updateByKey(const std::string &table, const Row &values, const std::string &key, const Value &id)
Updates a single row by primary key value.
Database module (eve.Database): SQLite connection factory. Script: db <- eve.Database(); conn <- db....
Connection * connectSQLite(std::string path)
Opens a SQLite database file (":memory:" is supported).
Connection * connect(std::string connector, std::string connectionString)
Opens a connection with an explicit Poco connector name.
Runtime ORM model exposed to Squirrel.
Query * query()
Queries query.
std::string allJson()
All json.
int updateJson(const std::string &idJson, const std::string &json)
Updates json.
int insertJson(const std::string &json)
Inserts json.
int remove(const std::string &idJson)
Removes remove.
Model(Connection *connection, std::string table, std::string primaryKey)
Model.
std::string findJson(const std::string &idJson)
Finds json.
Composable filtered/sorted/paged query shared by C++ and scripts.
Query(Connection *connection, std::string table)
Queries Query.
std::string firstJson()
First json.
std::vector< Row > all()
All.
Query * limit(int count)
Limit.
Query * offset(int count)
Offset.
Query * orderBy(const std::string &column, bool ascending=true)
Order by.
Query * where(const std::string &column, const std::string &op, Value value)
Where.
int count()
Returns the number of count.
Query * whereJson(const std::string &column, const std::string &op, const std::string &valueJson)
Where json.
std::string allJson()
All json.
std::unordered_map< std::string, Value > Row
std::variant< std::monostate, std::int64_t, double, std::string, bool > Value