[SQLite] Unrecognized token/UNIQUE constraint failed

Статус
В этой теме нельзя размещать новые ответы.
Сообщения
1,017
Реакции
819
Помог
10 раз(а)
Ошибка
"SQL QueryHandleInsertInto Error: unrecognized token: "'karatel'')""
"SQL QueryHandleInsertInto Error: UNIQUE constraint failed: stats.AuthID"
ОС
Linux
Amx Mod X
v1.9.0.5241
Билд
Exe version 1.1.2.7/Stdio (cstrike)
ReHLDS version: 3.4.0.668-dev
ReGamedll
ReGameDLL version: 5.7.0.322-dev
Версия Metamod
Metamod-r v1.3.0.128, API (5:13)
Список метамодулей
[ 1] SafeNameAndChat  RUN   -    safenameandchat.so          v1.1         ini  ANY   ANY  
[ 2] Reunion RUN - reunion_mm_i386.so v0.1.0.92 ini Start Never
[ 3] VoiceTranscoder RUN - voicetranscoder.so v2017RC3 ini ANY ANY
[ 4] AMX Mod X RUN - amxmodx_mm_i386.so v1.9.0.5241 ini Start ANY
[ 5] WHBlocker RUN - whblocker_mm_i386.so v1.5.696 ini Chlvl ANY
[ 6] ReSemiclip RUN - resemiclip_mm_i386.so v2.3.9 ini Chlvl ANY
[ 7] Rechecker RUN - rechecker_mm_i386.so v2.5 ini Chlvl ANY
[ 8] ReSRDetector RUN - resrdetector_mm_i386.so v0.1.0 ini Chlvl ANY
[ 9] ReAuthCheck RUN - reauthcheck_mm_i386.so v0.1.6 ini Start Never
[10] ReAimDetector RUN - reaimdetector_amxx_i386.so v0.2.2 pl4 ANY Never
[11] SQLite RUN - sqlite_amxx_i386.so v1.9.0.5241 pl4 ANY ANY
[12] Engine RUN - engine_amxx_i386.so v1.9.0.5241 pl4 ANY ANY
12 plugins, 12 running
Список плагинов
[  1] ReAimDetector API       0.2.2       ReHLDS Team       reaimdetector.a  debug    
[ 2] Server Commands 2.0 Javekson server_cmds.amx debug
[ 3] Stats 2.0 Javekson stats.amxx debug
[ 4] Map Spawns Editor 1.0.16 iG_os map_spawns_edit debug
Автор плагина
Javekson
Версия плагина
2.0
Исходный код
#include <amxmodx>
#include <sqlx>

#pragma loadlib sqlite

#pragma semicolon 1

const QUERY_STRLEN = 256;
const PATH_STRLEN = 64;
const MESSAGE_STRLEN = 512;
const TIME_STRLEN = 32;
const ELLIPSES_ARG = 4;
const AUTHID_STRLEN = 24;
const PLAYER_NAME_STRLEN = 32;
const ADDITIONAL_DATA = 1;

new Handle:g_SQLTuple;
new g_SQLQuery[QUERY_STRLEN];

new g_sDataDir[PATH_STRLEN];
new g_sLogsDir[PATH_STRLEN];
new g_sDataBase[PATH_STRLEN + 32];
new g_aData[ADDITIONAL_DATA];

public plugin_init() {
register_plugin("Stats", "2.0", "Javekson");
}

public plugin_cfg() {
get_localinfo("amxx_logs", g_sLogsDir, charsmax(g_sLogsDir));
get_localinfo("amxx_datadir", g_sDataDir, charsmax(g_sDataDir));
formatex(g_sDataBase, charsmax(g_sDataBase), "%s/stats.db", g_sDataDir);

if(!file_exists(g_sDataBase)) {
new iFileID = fopen(g_sDataBase, "a");
fclose(iFileID);
}

SQL_SetAffinity("sqlite");
g_SQLTuple = SQL_MakeDbTuple("", "", "", g_sDataBase);

formatex(g_SQLQuery, charsmax(g_SQLQuery),
"CREATE TABLE IF NOT EXISTS stats( \
AuthID TEXT PRIMARY KEY, \
Name TEXT NOT NULL, \
Frags INTEGER DEFAULT 0, \
Deaths INTEGER DEFAULT 0)");

SQL_ThreadQuery(g_SQLTuple, "QueryHandleCreate", g_SQLQuery);
}

public client_putinserver(id) {
if(is_user_bot(id) || is_user_hltv(id)) {
return PLUGIN_CONTINUE;
}
g_aData[0] = id;
new sAuthID[AUTHID_STRLEN], sPlayerName[PLAYER_NAME_STRLEN];
get_user_authid(id, sAuthID, charsmax(sAuthID));
get_user_name(id, sPlayerName, charsmax(sPlayerName));
logging(g_sLogsDir, "stats_inf_", "^"Игрок зашел на сервер: %s %s^"", sAuthID, sPlayerName);

formatex(g_SQLQuery, charsmax(g_SQLQuery), "SELECT * FROM stats WHERE AuthID = '%s'", sAuthID);
SQL_ThreadQuery(g_SQLTuple, "QueryHandleCheckPlayer", g_SQLQuery, g_aData, sizeof(g_aData));

return PLUGIN_CONTINUE;
}

public plugin_end() {
SQL_FreeHandle(g_SQLTuple);
}

public QueryHandleCreate(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) {
if(failstate != TQUERY_SUCCESS) {
logging(g_sLogsDir, "stats_error_", "^"SQL QueryHandleCreate Error: %s^"", error);
pause("a");
}
}

public QueryHandleCheckPlayer(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) {
if(failstate != TQUERY_SUCCESS) {
logging(g_sLogsDir, "stats_error_", "^"SQL QueryHandleCheckPlayer Error: %s^"", error);
return PLUGIN_CONTINUE;
}

if(SQL_MoreResults(query)) {
logging(g_sLogsDir, "stats_inf_", "^"Игрок найден в базе, строк в запросе: %d^"", SQL_MoreResults(query));
return PLUGIN_CONTINUE;
}
logging(g_sLogsDir, "stats_inf_", "^"Игрок не найден, добавляем^"");

new id = g_aData[0];
new sAuthID[AUTHID_STRLEN], sPlayerName[PLAYER_NAME_STRLEN];
get_user_authid(id, sAuthID, charsmax(sAuthID));
get_user_name(id, sPlayerName, charsmax(sPlayerName));

logging(g_sLogsDir, "stats_inf_", "^"Добавили игрока %s %s^"", sAuthID, sPlayerName);

formatex(g_SQLQuery, charsmax(g_SQLQuery), "INSERT INTO stats(AuthID, Name) VALUES ('%s', '%s')", sAuthID, sPlayerName);
SQL_ThreadQuery(g_SQLTuple, "QueryHandleInsertInto", g_SQLQuery);

return PLUGIN_CONTINUE;
}

public QueryHandleInsertInto(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) {
if(failstate != TQUERY_SUCCESS) {
logging(g_sLogsDir, "stats_error_", "^"SQL QueryHandleInsertInto Error: %s^"", error);
}
}

stock logging(const sLogsDir[], const sFileName[], const sMessage[], any:...) {
new sFmtMsg[MESSAGE_STRLEN], sTime[TIME_STRLEN], sLogFile[PATH_STRLEN + 32], iFileID;
vformat(sFmtMsg, charsmax(sFmtMsg), sMessage, ELLIPSES_ARG);
get_time("%m.%Y.logs", sTime, charsmax(sTime));
formatex(sLogFile, charsmax(sLogFile), "%s/%s%s", sLogsDir, sFileName, sTime);
iFileID = fopen(sLogFile, "at");
get_time("%d.%m.%Y - %H:%M:%S", sTime, charsmax(sTime));
fprintf(iFileID, "^"%s^" %s^n", sTime, sFmtMsg);
fclose(iFileID);
}
При подключении игрока с ником karatel' вылезла ошибка: unrecognized token: "'karatel'')"
Так же при подключении игрока "Игрок зашел на сервер: STEAM_1:0:143418091 gh" вылезла ошибка: UNIQUE constraint failed: stats.AuthID", что очень странно, ибо спустя минут 10 игрок перезашел и запись прошла успешно.
 
В этой теме было размещено решение! Перейти к решению.
Последнее редактирование:
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
Javekson, для этого есть ескейп. К сожалению в амхх нету готового для потоковых запросов. Придется юзать сток костыль.
 
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
Javekson, строк. Вот сток-костыль
Код:
/*********    mysql escape functions     ************/
mysql_escape_string(dest[],len)
{
    //copy(dest, len, source);
    replace_all(dest,len,"\\","\\\\");
    replace_all(dest,len,"\0","\\0");
    replace_all(dest,len,"\n","\\n");
    replace_all(dest,len,"\r","\\r");
    replace_all(dest,len,"\x1a","\Z");
    replace_all(dest,len,"'","\'");
    replace_all(dest,len,"^"","\^"");
}
 
Сообщения
1,017
Реакции
819
Помог
10 раз(а)
fantom, понял, а по поводу второй ошибки?
 
Сообщения
1,182
Реакции
2,146
Помог
57 раз(а)
Javekson, google
The issue is the same for the item table. You get a UNIQUE constraint failed error when the data that you are inserting has an entry which is already in the corresponding column of the table that you are inserting into.
Засовываешь в столбец AuthID значение, которое уже там есть, а столбец требует уникальности (PRIMARY KEY же).
 
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
Javekson, походу попитка создать запись с значением которое уже существует и является уникальным индексом
 
Сообщения
496
Реакции
618
Помог
16 раз(а)
AuthID не является ли PRIMARY KEY?
Ругается на дубли в таблице по значению AuthID, которое должно быть уникальным.
 
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
voed, и правда есть. Я думал она только для соединений. Но можно указать Empty_Handle как оказалось.

Касательно mysql драйвера то я все равно не рекомендую использовать данный натив.
SQL_QuoteStringFmt -> QuoteString
как видно используется mysql_escape_string. А в офф документации указано
Do not use this function. Use mysql_real_escape_string_quote() instead.
А также можно нагуглить експлойты использования.
А в целом лучше всего использовать подготовленные запросы. Тогда вообще не нужно ничего экранировать.
 
Сообщения
1,017
Реакции
819
Помог
10 раз(а)
А в целом лучше всего использовать подготовленные запросы. Тогда вообще не нужно ничего экранировать.
Да, пожалуй соглашусь, лучше буду юзать сток в плагине, заранее подготовлю запрос правильный.
28 Фев 2019
BlackSignature, fantom, Gudaus, AuthID является PRIMARY KEY, но прикол в том, что игрока еще не было в базе.
Он зашел, вылезла ошибка, в базе его не было и он не добавился соответственно из-за ошибки, затем игрок перезашел просто и он уже добавился в базу, то есть запрос выполнился успешно. Да и к тому же, если бы игрок уже был в базе мне выдало бы сообщение в лог, что игрок в базе есть, потому выполнять запрос не будем
 
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
Javekson, это не то. Суть подготовленых запросов в отправке самого запроса и данных отдельно. Таким образом нас не волнует экранирование
 
Сообщения
1,017
Реакции
819
Помог
10 раз(а)
fantom, тогда я не понял ничего, на примере будет возможность показать?
 
Сообщения
1,182
Реакции
2,146
Помог
57 раз(а)
Javekson,
Код:
public QueryHandleCheckPlayer(failstate, Handle:query, error[], errnum, data[], size, Float:queuetime) {
	if(failstate != TQUERY_SUCCESS) {
		logging(g_sLogsDir, "stats_error_", "^"SQL QueryHandleCheckPlayer Error: %s^"", error);
		return PLUGIN_CONTINUE;
	}
	
	if(SQL_MoreResults(query)) {
		logging(g_sLogsDir, "stats_inf_", "^"Игрок найден в базе, строк в запросе: %d^"", SQL_MoreResults(query));
		return PLUGIN_CONTINUE;
	}
	logging(g_sLogsDir, "stats_inf_", "^"Игрок не найден, добавляем^"");
	
	new id = g_aData[0];
	new sAuthID[AUTHID_STRLEN], sPlayerName[PLAYER_NAME_STRLEN];
	get_user_authid(id, sAuthID, charsmax(sAuthID));
	get_user_name(id, sPlayerName, charsmax(sPlayerName));
	
	logging(g_sLogsDir, "stats_inf_", "^"Добавили игрока %s %s^"", sAuthID, sPlayerName);
	
	formatex(g_SQLQuery, charsmax(g_SQLQuery), "INSERT INTO stats(AuthID, Name) VALUES ('%s', '%s')", sAuthID, sPlayerName);
	SQL_ThreadQuery(g_SQLTuple, "QueryHandleInsertInto", g_SQLQuery);
	
	return PLUGIN_CONTINUE;
}
Почему?
Код:
new id = g_aData[0];
Индекс игрока берётся неоттуда, из-за этого и дубли. Брать нужно
Код:
new id = data[0]
И ещё нужно учесть, что игрок уже может быть отключён от сервера
 
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
Javekson, https://www.sqlite.org/c3ref/stmt.html. API не выведено в AMXX. Но суть такова. Сначала отправляется запрос. Но вместо
SQL:
INSERT INTO stats(AuthID, Name) VALUES ('STEAM_ID_LAN', 'Player')
Отправляется только сам запрос без данных
Код:
NSERT INTO stats(AuthID, Name) VALUES (:AuthID, :Name)
Сами данные биндятся и отправляются отдельно от самого запроса. Это придумано для того, чтобы ускорить массовые запросы. Но дополнительно защищает от иньекций, так как вся суть иньекций заключается в том, чтобы подтасовать данные так, чтобы изменить запрос. Популярный пример
SQL:
SELECT * FROM stats WHERE AuthID='%s'
Если AuthID сменить на что то вроде 1' or 1 = 1 -- то получим
SQL:
SELECT * FROM stats WHERE AuthID='1' or 1 = 1 --'
Получили всегда успешный запрос. Подготовленные запросы такого не позволят просто физически. Как я говорил данные идут отдельно.

P.S. а в batch запросах оно выгодно, потому что движку базы нужно распарсить и оптимизировать запрос. Если у нас например вставка чата при каждом сообщении игроков, то каждый такой запрос будет начинатся с парсинга и оптимизаций. А это время. С подготовленними запросами мы заранее говорим движку что такой запрос нужно зарание распарсить и оптимизировать. После чего просто отправляем данные. Движок лишний раз не будет тратить время, а сразу исполнять уже готовый запрос.

P.S.S. В амхх (а именно в потоковых запросах) такая штука не работает ввиду того, что каждый такой запрос порождает новое соединение. Сами же подготовленные запросы существуют в пределах сессии (читай конекта).
 
Сообщения
1,017
Реакции
819
Помог
10 раз(а)
BlackSignature, посмотрел так сказать в живую )) Когда запрос еще не успел завершиться, как уже пошел новый.

2019-02-28_145200.png

Как видим, игрок зашел, отправился запрос на его добавление, запрос еще не успел выполниться, как уже другой игрок зашел на сервер и его тоже стал чекать параллельный запрос.

Теперь дублей нету и ошибок тоже ))

И да, я не знал, что PRIMARY KEY будет работать как AUTO_INCREMENT, ибо оно само добавляет идентификатор, и к тому же прибавляет его автоматически с каждой новой строкой.
2019-02-28_145217.png
28 Фев 2019
fantom, очень сложно для моего мозга понять все это, но спасибо, за старание ) Извини, что все еще ничего не понял =DD
 
Сообщения
2,491
Реакции
2,791
Помог
61 раз(а)
Javekson, Используй натив https://dev-cs.ru/threads/5490/#post-53408. Я уже конкретно заофтопил. Можна продолжать вечно. Будь мои знания побогаче, то я б вообше подключил NoSQL (например RocksDB или MDBX) базу вместо sqlite. В даном случае это намного практичней (не всегда). Но это никак не касается темы вопроса. Ответ был дан и думаю тему можна закрывать
 
Статус
В этой теме нельзя размещать новые ответы.

Пользователи, просматривающие эту тему

Сейчас на форуме нет ни одного пользователя.
Сверху Снизу