Sounds real cool. Remember that PO doesn't support only sqlite, there's PostGreSQL and MySQL if that matters.
The Problem
Due to biggest PO servers already having hundreds of players, running scripts is taking more and more time. We need more efficient scripts.
A good start is to stop using {sys.saveVal, sys.removeVal, sys.getVal} functions to speed up execution of the server. However, manually reading and writing files in limited javascript library is kind of pain.
Using databases has been flashed on forums and servers couple of times already, and that is what I think we should start developing further.
In order to make people really use databases in their scripts instead of sys.*Val-functions, the interface needs to be clear. We brainstormed a bit already with Mystra on server chat about possible functions.
Solutions & Suggestions
One thing that appeals to me are active objects. You can fetch them from the database, read their properties and you can write them into database with ease.
This thread is made to discuss about database interface before actually rushing into implementing one.
Random suggestions:
sys.db.useDatabase(sqlitefile), which selects a database- sys.db.ensureTable(tablename, properties), which would create if missing a table with shown properties
- sys.db.select(tablename, properties), where properties would include where-clause, limits, order
- select would return a list of objects, which have common .prototype in ActiveObject. ActiveObject would have member-function save() to save data back to database.
- New! sys.db.insert(tablename, properties)
- New! sys.db.update(tablename, properties) Even though returned object would have .save
- New! sys.db.delete(tablename, properties)
- sys.db.save(objectlist) would allow saving many objects at the same time
In .ensureTable properties should contain Javascript hash, which has keys as field names and values as field types. Currently fieldnames are database specific.
.ensureTable should use "create table if not exists", for it to be able to be called everytime script is initialized.
In .select and .delete, properties could be like:
How to allow other operatorsCode :{
'where':
{
'somefield': 'has_value',
'somenumber': ['<', 'othernumber'],
},
'limit': 5,
'order by': [['somefield', 'asc'], ['otherfield', 'desc']]
}
Any input is appreciated!
Last edited by Lamperi; March 15th, 2011 at 08:11.
Master of Science (Tech), Mathematics
Dad with three daughters, full time Design Engineer in a software company
I wish days had 28 hours instead of 24
Maybe no useDatabase() just prefix those tables with something like "poscript_"? Thay way DB would be the same as PO itself.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Sounds sensible. If we have database already opened we can use it!
Master of Science (Tech), Mathematics
Dad with three daughters, full time Design Engineer in a software company
I wish days had 28 hours instead of 24
Hmm… clear table may be needed too.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Soooo… if no one is going to add anything someone should start doing it.
Maybe I'll even try to do it when I have free time although my SQL knowledge is pretty average.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
I guess the interface will grow when we have more needs ^^
Master of Science (Tech), Mathematics
Dad with three daughters, full time Design Engineer in a software company
I wish days had 28 hours instead of 24
Started. Tried doing ensureTable(). Does not quite work… "Parameter count mismatch" :\ Tried with SQLite. Any ideas?
Code cpp:#ifndef SCRIPTDB_H
#define SCRIPTDB_H
#include <QObject>
#include <QtScript>
#include "server.h"
class ScriptDB : public QObject
{
Q_OBJECT
public:
explicit ScriptDB(Server *s);
Q_INVOKABLE void ensureTable(const QString &tableName, const QScriptValue &properties);
signals:
public slots:
private:
Server *myserver;
QString makeFields(const QScriptValue &properties);
};
#endif // SCRIPTDB_H
Code cpp:#include <QSqlQuery>
#include "scriptdb.h"
#include "sql.h"
#include <QRegExp>
ScriptDB::ScriptDB(Server *s) : myserver(s)
{
}
// Properties should be a JS hash. This function will iterate over all properties
// and will use property name is field name and property valus as type.
// Type used as is so you must use SQL types, like varchar(255), int, etc.
void ScriptDB::ensureTable(const QString &tableName, const QScriptValue &properties)
{
QSqlQuery q;
QString fields = makeFields(properties);
if (fields.isEmpty()) {
myserver->print("ensureTable: cannot create table without any fields.");
return;
}
switch (SQLCreator::databaseType) {
case SQLCreator::SQLite:
q.prepare("create table :tablename (" + fields + "id integer primary key autoincrement)");
break;
case SQLCreator::MySQL:
q.prepare("create table :tablename (" + fields + "id integer auto_increment, primary key(id))");
break;
case SQLCreator::PostGreSQL:
q.prepare("create table :tablename (" + fields + "id serial, primary key(id))");
break;
default:
throw QString("Using a not supported database");
}
q.bindValue(":tablename", "poscript_" + tableName);
if (!q.exec()) {
myserver->print(QString("ensureTable: creation failed. %1").arg(q.lastError().text()));
}
}
QString ScriptDB::makeFields(const QScriptValue &properties)
{
if (!properties.isObject()) return QString();
QString result = "";
QScriptValueIterator it(properties);
QRegExp rx("[a-z_]+");
int pos = 0;
while (it.hasNext()) {
it.next();
QString name = it.name().toLower();
QString value = it.value().toString();
if ((name != "id") && (rx.exactMatch(name))) {
result += QString("%1 %2, ").arg(name, value);
}
}
return result;
}
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
I can't figure out anything else than a bug / oversight in Qt's SQLITE-driver.
Since boundValues are internally QVariant, I guess they can only be used in INSERT-statements or WHERE-clauses, where driver knows the type of what it is binding.
My suggestion is to use
Code :QRegExp rx("[a-z_]+");
if(!rx.exactMatch(tablename) return;
...
q.prepare("create table if not exists %1 (" ... ).arg(tablename);
Last edited by Lamperi; March 15th, 2011 at 09:00.
Master of Science (Tech), Mathematics
Dad with three daughters, full time Design Engineer in a software company
I wish days had 28 hours instead of 24
Ah… thanks. I've read about it once, but forgot since I haven't done it before.
Ok, I've pushed initial code to my branch. It has only ensureTable() for now so you can't do much besides this.
Testers, comments, suggestions, whateverelsers are welcomed to check or at least read it
This code will create poscript_test table with id (automatic), name, and rating fields.
Code javascript:({
serverStartUp: function() {
sys.db.ensureTable("test", { "name": "varchar(255)", "rating": "int" });
}
})
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Hmm… should I do startTransaction() and endTransaction()? It would be good to know how do I do that with current code we have.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Code :QSqlDatabase::database().transaction();
Code :QSqlDatabase::database().commit();
However due to the sensible nature of this you'd need to have transactions logged, meaning that if someone doesn't close a transaction with scripts the script needs to know it and close the transaction at the end of the event or stack of events (same for webCall and callLater)
Hmm… maybe I'll ditch it for now then. If it will be needed I'll think about it later.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Pushed sys.db.insert(table_name, properties) into my branch. Since I haven't found any escaping function I did
Will this be enough I wonder.Code cpp:valueData.replace("'", "''");
To test:
Code javascript:({
serverStartUp: function() {
sys.db.ensureTable("test", { "name": "varchar(255)", "rating": "int" });
sys.db.insert("test", { "name": "KIIIING COMBO!", "rating": 5 });
sys.db.insert("test", { "name": "AWESOME COMBO!", "rating": 4.5 });
sys.db.insert("test", { "name": "1\\n2", "rating": 3 });
}
})
Code :: sqlite3 pokemon
SQLite version 3.7.3
Enter ".help" for instructions
Enter SQL statements terminated with a ";"
sqlite> .dump poscript_test
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE poscript_test (id integer primary key autoincrement, name varchar(255), rating int);
INSERT INTO "poscript_test" VALUES(1,'KIIIING COMBO!',5);
INSERT INTO "poscript_test" VALUES(2,'AWESOME COMBO!',4.5);
INSERT INTO "poscript_test" VALUES(3,'1\n2',4.5);
COMMIT;
If anyone is willing to do any function drop message here… VOLUNTEEERSSS!
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
http://doc.qt.nokia.com/latest/qsqld...capeIdentifier
I guess this could be used? QSqlDriver is used internally by QSqlQuery usually but I think you could do escaping yourself.
Master of Science (Tech), Mathematics
Dad with three daughters, full time Design Engineer in a software company
I wish days had 28 hours instead of 24
Well, it says "identifier", while I'm escaping data, so I don't know :|
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Yeah, should have used it in ensureTable, since it has loads of identifiers :X
// Ok, .insert has now possible SQL injection. I guess we should use .bindValue() on data which can come from users.
Last edited by Lamperi; March 16th, 2011 at 07:42.
Master of Science (Tech), Mathematics
Dad with three daughters, full time Design Engineer in a software company
I wish days had 28 hours instead of 24
Any examples of this possibility?
Bind value is possible, it's just I'll need to iterate over values twice as number of items in this "hash" is unknown and inaccessible via Qt methods directly. At least I haven't found any. And there must be exact number of "?" for substitutions.
Well, I could go with double iteration if that's ok.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Ok, updated ensureTable() to use bound values.
Also, keep in mind that:
1) SQLite does not enforce type. It will store 3.5 as 3.5 even if the field is int type. Normal DBs will not do that and rounding will occur.
2) When using PostgreSQL if you get warning such as "nonstandard use of \\ in a string literal ... HINT: Use the escape string syntax for backslashes, e.g., E'\\'." you can either
a) do nothing and take it;
b) write the following in postgresql.conf
and restart your PostgreSQL server. This will escape all "\" characters in queries. This option may be default in future versions (or so they say).Code :standard_conforming_strings = on
HOWEVER, this will cause (AFAIK) inconsistencies with SQLite as it WILL interpret them as escape characters. So…
c) there is also escape_string_warning option in postgresql.conf. But behavior may change in future versions…
Conclusion: I wonder whether there is a bug in Qt's driver and whether it was fixed in 4.7…
3) When using PostgreSQL calling ensureTable() can print error message that a table already exists. It is expected behavior.
4) PostgreSQL will also warn this VACCUM cannot be done on some special tables that aren't own by current DB user. It is normal. Whether I can do anything about it is currently unknown. PostgreSQL also has auto-vacuum daemon, so doing this may not be needed at all. Maybe I should add an option to turn off auto-vacuum.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Any progress? I'm not feeling well enough to write code yet. Always sleepy at evening %)
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
SO? No one?
To make a prototype object to be set with setPrototype() on JS object in Qt you'll need to do something like this (AFAIU documentation).
Code cpp:class PORecordPrototype : public QObject, public QScriptable
{
Q_OBJECT
public:
PORecordPrototype(QObject *parent = 0) : QObject(parent) {};
Q_INVOKABLE void save();
private:
};
void PORecordPrototype::save()
{
// thisObject().blahblah
}
Last edited by Mystra; April 16th, 2011 at 18:15.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
I hate you, i ahd this idea from some weeks ago!![]()
I will, of course. I need to get the source again, but it is in the source yet or no? Oh, i'm also going to do again the things i had to do before. :P
I hope this will help me with my RPG. :P (Well, not now).
Base is there. Look into Server/scriptdb.h and .cpp. Although what needs to be done isn't exactly easiest ones to do.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
No clue of how MySQL works. But for every "Data" or however you want to call it (I suck at SQL),
Code CPP:QString queryString = QString("INSERT INTO poscript_") + tableName + QString("(") + fields
+ QString(") VALUES (") + values + QString(")");
will have a "poscript_" at start? Sorry, i suck on MySQL![]()
It is a table prefix.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Whats the problem exactly? Maybe named parameters are a solution for you?!
QSqlQuery query;
query.prepare("INSERT INTO person (id, forename, surname) "
"VALUES (:id, :forename, :surname)");
query.bindValue(":id", 1001);
query.bindValue(":forename", "Bart");
query.bindValue(":surname", "Simpson");
query.exec();
That was already done.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Pushed PORecordPrototype. Object of this type is created in ScriptDB and now can be used as a prototype for PORecord objects that will appear in future (query result).
PORecordPrototype currently has only save() that does nothing. When I'll do at least 1 query function (or someone else will do it), I'll try to implement it.
EDIT: ahem… I forgot to pass it QScriptEngine instance. Is it accessible through myserver (Script *) variable by any chance?
EDIT 2: Done with QScriptEngine passing so previous edit is now irrelevant.
Last edited by Mystra; April 16th, 2011 at 19:42.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Done sys.db.findBy(table, key, value) - limited version of select(). Mostly for testing, but whatever. Prototype is bound to it, but invoking save() on result of sys.db.findBy() currently does nothing. I'll need to disable modification of "id" data later too.
sys.db.findBy() returns an array of hashes. Each item of this array is 1 row of data. Hash is, well, hash. field => value.
If field is not found then it's value is null.
If data cannot be retrieved sys.db.findBy() returns null. Should I change it to be empty array instead?
Also, all data is currently returned as string. Should I leave it this way? Otherwise someone will need to change that somehow. Also considering that SQLite stores only strings... maybe we didn't need to bother with field types (other than strings) at all. Any ideas?
Sample code:
Code :({
serverStartUp: function() {
sys.db.ensureTable("test", { "name": "varchar(255)", "rating": "int" });
//sys.db.insert("test", { "name": "KIIIING COMBO!", "rating": 5 });
//sys.db.insert("test", { "name": "AWESOME COMBO!", "rating": 4.5 });
var x = sys.db.findBy("test", "name", "AWESOME COMBO!");
var d = x.data;
for (var i = 0; i < d.length; ++i) {
var hash = d[i];
for (var k in hash) {
sys.sendAll(k + " - " + hash[k]);
}
}
}
})
Check and try code before merging.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Pushed protections to properties and 'id'. Should work…
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
well, where you exactly set all the MySQL Data? The Database, Password, etc it's the same as SQL Options in the Menu?
Yes. It's the same database, as PO one. However only tables named with "poscript_" prefix are accessible (script itself is not aware of this).
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Drop it if no one need it. Simple key-value storage (with multiple values per key) could be enough anyway :\
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Works fine now? Anything new? This seems to be, dead. :O It's finished... what?
I'm a Pokémon Online Developer. You can feel free to contact me though Private Message or Visitor Message.
Nobody needs it, so it's stall.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Ok, dropped. New plan: either new DB file with direct SQL commands (easy, no need to code much) or something else that is non-SQL (preferable, but need to plug API to scripts, the easier the better).
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.
Alternatively, make another field in SQL DB and stuff JSON into it. Just make sure it's not too short.
EDIT: ok, also ip<->other data table too.
Last edited by Mystra; December 3rd, 2011 at 08:42.
Breaking news: everything that was ever done in PO is done by me. No exceptions. Everything bad, obviously.