diff --git a/source/Logger.cpp b/source/Logger.cpp index 5263d0a0..03514c86 100644 --- a/source/Logger.cpp +++ b/source/Logger.cpp @@ -67,11 +67,11 @@ void FPSciLogger::initResultsFile(const String& filename, // Add the session info to the sessions table m_openTimeStr = genUniqueTimestamp(); RowEntry sessValues = { - "'" + sessConfig->id + "'", - "'" + m_openTimeStr + "'", - "'" + m_openTimeStr + "'", - "'" + subjectID + "'", - "'" + description + "'", + sessConfig->id, + m_openTimeStr, + m_openTimeStr, + subjectID, + description, "false", "0" }; @@ -79,7 +79,7 @@ void FPSciLogger::initResultsFile(const String& filename, // Create any table to do lookup here Any a = sessConfig->toAny(true); // Add the looked up values - for (String name : sessConfig->logger.sessParamsToLog) { sessValues.append("'" + a[name].unparse() + "'"); } + for (String name : sessConfig->logger.sessParamsToLog) { sessValues.append(a[name].unparse()); } // add header row insertRowIntoDB(m_db, "Sessions", sessValues); @@ -102,10 +102,10 @@ void FPSciLogger::createExperimentsTable(const String& expConfigFilename) { // Update row RowEntry expRow = { - "'" + expConfig.description + "'", - "'" + genUniqueTimestamp() + "'", - "'" + format("0x%x", hash) + "'", - "'" + readWholeFile(expConfigFilename) + "'" + expConfig.description, + genUniqueTimestamp(), + format("0x%x", hash), + readWholeFile(expConfigFilename) }; insertRowIntoDB(m_db, "Experiments", expRow); } @@ -123,7 +123,7 @@ void FPSciLogger::createSessionsTable(const shared_ptr& sessConfi { "trials_complete", "integer" } }; // add any user-specified parameters as headers - for (String name : sessConfig->logger.sessParamsToLog) { sessColumns.append({ "'" + name + "'", "text", "NOT NULL" }); } + for (String name : sessConfig->logger.sessParamsToLog) { sessColumns.append({ name, "text", "NOT NULL" }); } createTableInDB(m_db, "Sessions", sessColumns); // no need of Primary Key for this table. } @@ -168,9 +168,9 @@ void FPSciLogger::logTargetTypes(const Array>& targets) const String type = (config->destinations.size() > 0) ? "waypoint" : "parametrized"; const String modelName = config->modelSpec["filename"]; const RowEntry targetTypeRow = { - "'" + config->id + "'", - "'" + type + "'", - "'" + config->destSpace + "'", + config->id, + type, + config->destSpace, String(std::to_string(config->size[0])), String(std::to_string(config->size[1])), config->symmetricEccH ? "true" : "false", @@ -184,7 +184,7 @@ void FPSciLogger::logTargetTypes(const Array>& targets) String(std::to_string(config->motionChangePeriod[0])), String(std::to_string(config->motionChangePeriod[1])), config->jumpEnabled ? "true" : "false", - "'" + modelName + "'" + modelName }; rows.append(targetTypeRow); } @@ -206,9 +206,9 @@ void FPSciLogger::createTargetsTable() { void FPSciLogger::addTarget(const String& name, const shared_ptr& config, const String& spawnTime, const float& size, const Point2& spawnEcc) { const RowEntry targetValues = { - "'" + name + "'", - "'" + config->id + "'", - "'" + spawnTime + "'", + name, + config->id, + spawnTime, String(std::to_string(size)), String(std::to_string(spawnEcc.x)), String(std::to_string(spawnEcc.y)), @@ -251,9 +251,9 @@ void FPSciLogger::recordTargetLocations(const Array& locations) for (const auto& loc : locations) { String stateStr = presentationStateToString(loc.state); Array targetTrajectoryValues = { - "'" + FPSciLogger::formatFileTime(loc.time) + "'", - "'" + loc.name + "'", - "'" + stateStr + "'", + FPSciLogger::formatFileTime(loc.time), + loc.name, + stateStr, String(std::to_string(loc.position.x)), String(std::to_string(loc.position.y)), String(std::to_string(loc.position.z)), @@ -294,15 +294,15 @@ void FPSciLogger::recordPlayerActions(const Array& actions) { } Array playerActionValues = { - "'" + FPSciLogger::formatFileTime(action.time) + "'", + FPSciLogger::formatFileTime(action.time), String(std::to_string(action.viewDirection.x)), String(std::to_string(action.viewDirection.y)), String(std::to_string(action.position.x)), String(std::to_string(action.position.y)), String(std::to_string(action.position.z)), - "'" + stateStr + "'", - "'" + actionStr + "'", - "'" + action.targetName + "'", + stateStr, + actionStr, + action.targetName, }; rows.append(playerActionValues); } @@ -323,7 +323,7 @@ void FPSciLogger::recordFrameInfo(const Array& frameInfo) { Array rows; for (FrameInfo info : frameInfo) { Array frameValues = { - "'" + FPSciLogger::formatFileTime(info.time) + "'", + FPSciLogger::formatFileTime(info.time), //String(std::to_string(info.idt)), String(std::to_string(info.sdt)) }; @@ -355,13 +355,13 @@ void FPSciLogger::addQuestion(Question q, String session, const shared_ptr(dialog)->options()).unparse(); } RowEntry rowContents = { - "'" + time + "'", - "'" + session + "'", - "'" + q.prompt + "'", - "'" + optStr + "'", - "'" + keyStr + "'", - "'" + orderStr + "'", - "'" + q.result + "'" + time, + session, + q.prompt, + optStr, + keyStr, + orderStr, + q.result }; logQuestionResult(rowContents); } @@ -400,17 +400,17 @@ void FPSciLogger::logUserConfig(const UserConfig& user, const String& sessId, co const String time = genUniqueTimestamp(); RowEntry row = { - "'" + user.id + "'", - "'" + sessId + "'", - "'" + time + "'", + user.id, + sessId, + time, String(std::to_string(cmp360)), String(std::to_string(user.mouseDegPerMm)), String(std::to_string(user.mouseDPI)), String(std::to_string(user.reticle.index)), String(std::to_string(user.reticle.scale[0])), String(std::to_string(user.reticle.scale[1])), - "'" + user.reticle.color[0].toString() + "'", - "'" + user.reticle.color[1].toString() + "'", + user.reticle.color[0].toString(), + user.reticle.color[1].toString(), String(std::to_string(user.reticle.changeTimeS)), String(std::to_string(user.turnScale.x)), String(std::to_string(userYTurnScale)), diff --git a/source/Session.cpp b/source/Session.cpp index 570b78f2..ab2b222f 100644 --- a/source/Session.cpp +++ b/source/Session.cpp @@ -372,7 +372,7 @@ void Session::processResponse() if (notNull(logger)) { int totalTrials = 0; for (int tCount : m_completedTrials) { totalTrials += tCount; } - logger->updateSessionEntry((m_remainingTrials[m_currTrialIdx] == 0), totalTrials); // Update session entry in database + logger->updateSessionEntry(false, totalTrials); // Update session entry in database } // Check for whether all targets have been destroyed @@ -480,7 +480,7 @@ void Session::updatePresentationState() if (notNull(logger) && m_config->logger.enable) { int totalTrials = 0; for (int tCount : m_completedTrials) { totalTrials += tCount; } - logger->updateSessionEntry((m_remainingTrials[m_currTrialIdx] == 0), totalTrials); // Update session entry in database + logger->updateSessionEntry(true, totalTrials); // Update session entry in database } if (m_config->logger.enable) { endLogging(); @@ -588,12 +588,12 @@ void Session::recordTrialResponse(int destroyedTargets, int totalTargets) if (m_config->logger.logTrialResponse) { // Trials table. Record trial start time, end time, and task completion time. FPSciLogger::TrialValues trialValues = { - "'" + m_config->id + "'", + m_config->id, String(std::to_string(m_currTrialIdx)), String(std::to_string(m_completedTrials[m_currTrialIdx])), - format("'Block %d'", m_currBlock), - "'" + m_taskStartTime + "'", - "'" + m_taskEndTime + "'", + format("Block %d", m_currBlock), + m_taskStartTime, + m_taskEndTime, String(std::to_string(m_pretrialDuration)), String(std::to_string(m_taskExecutionTime)), String(std::to_string(destroyedTargets)), diff --git a/source/sqlHelpers.cpp b/source/sqlHelpers.cpp index d2e5740d..c6479880 100644 --- a/source/sqlHelpers.cpp +++ b/source/sqlHelpers.cpp @@ -27,56 +27,136 @@ bool createTableInDB(sqlite3* db, const String tableName, const Array& values, const String colNames) { +bool insertRowIntoDB(sqlite3* db, const String tableName, const Array& values) { if (values.length() == 0) { logPrintf("Warning insert row with empty values ignored!\n"); - return false; // Don't attempt to insert for empty values + return false; } - // Quotes must be added around text-type values (eg. "addQuotes(expVersion)") - // Note that ID does not need to be provided unless PRIMARY KEY is set. - String insertC = "INSERT INTO " + tableName + colNames + " VALUES("; + + // ? is a variable that will be bound later with a `sqlite3_bind_XXXX` function + // see https://www2.sqlite.org/draft/c3ref/bind_blob.html for details + String insertC = "INSERT INTO " + tableName + " VALUES("; for (int i = 0; i < values.size(); i++) { - insertC += values[i]; + insertC += "?"; if(i < values.size() - 1) insertC += ","; } insertC += ");"; + + // prepare + sqlite3_stmt* res; + int ret = sqlite3_prepare_v2(db, insertC.c_str(), -1, &res, 0); + if (ret != SQLITE_OK) + { + logPrintf("Error preparing INSERT INTO statement (%s): %s\n", insertC, sqlite3_errmsg(db)); + return ret == SQLITE_OK; + } + // bind values + for (int i = 0; i < values.size(); i++) { + // All values sent to this function are explicitly Strings and get stored as text in the database + sqlite3_bind_text(res, i + 1, values[i].c_str(), -1, SQLITE_TRANSIENT); + } + ret = sqlite3_step(res); + if (ret != SQLITE_DONE) + { + logPrintf("Error in INSERT (%s) with VALUE including %s!\n", insertC, values[0]); + } + // clean up the sqlite3_stmt + ret = sqlite3_finalize(res); //logPrintf("Inserting row into %s table w/ SQL query:%s\n\n", tableName.c_str(), insertC.c_str()); - char* errmsg; - int ret = sqlite3_exec(db, insertC.c_str(), 0, 0, &errmsg); if (ret != SQLITE_OK) { - logPrintf("Error in INSERT INTO statement (%s): %s\n", insertC, errmsg); + logPrintf("Error in INSERT INTO statement (%s): %s\n", insertC, sqlite3_errmsg(db)); } return ret == SQLITE_OK; } -bool insertRowsIntoDB(sqlite3* db, const String tableName, const Array>& value_vector, const String colNames) { - if (value_vector.length() == 0) { - logPrintf("Warning insert rows with empty row value array ignored!\n"); - return false; // Don't insert for empty value vector (creates an error) - } - // Quotes must be added around text-type values - // Note that ID does not need to be provided unless PRIMARY KEY is set - String insertC = "INSERT INTO " + tableName + colNames + " VALUES"; - for (int i = 0; i < value_vector.size(); ++i) { +/** Helper function that takes in a start and end row and only inserts that given range. + Assumes the range is valid and below the SQLITE_LIMIT_VARIABLE_NUMBER */ +bool groupInsertRows(sqlite3* db, const String tableName, const Array>& value_vector, const int start_row, const int end_row) { + + // ? is a variable that will be bound later with a `sqlite3_bind_XXXX` function + // see https://www2.sqlite.org/draft/c3ref/bind_blob.html for details + String insertC = "INSERT INTO " + tableName + " VALUES"; + for (int i = start_row; i < end_row; ++i) { insertC += "("; for (int j = 0; j < value_vector[i].size(); j++) { - insertC += value_vector[i][j]; + insertC += "?"; if (j < value_vector[i].size() - 1) insertC += ","; } insertC += ")"; - if (i < value_vector.size() - 1) { // We have more rows coming after this row. - insertC += ","; + if (i < end_row - 1) { + // We have more rows coming after this row. + insertC += ","; } - else { // The last row of this insert operation. Terminate it with a semi-colon. + else { + // The last row of this insert operation. Terminate it with a semi-colon (which is optional). insertC += ";"; } } - //logPrintf("Inserting rows into %s table with SQL query:%s\n\n", tableName.c_str(), insertC.c_str()); - char* errmsg; - int ret = sqlite3_exec(db, insertC.c_str(), 0, 0, &errmsg); + logPrintf("insertRowsIntoDB: %s\n\n", insertC); + + + // prepare + sqlite3_stmt* res; + int ret = sqlite3_prepare_v2(db, insertC.c_str(), -1, &res, 0); + if (ret != SQLITE_OK) + { + logPrintf("Error preparing INSERT INTO statement (%s): %s\n", insertC, sqlite3_errmsg(db)); + return ret == SQLITE_OK; + } + // bind values + for (int i = start_row; i < end_row; ++i) { + for (int j = 0; j < value_vector[i].size(); j++) { + sqlite3_bind_text(res, i * value_vector[i].size() + j + 1, value_vector[i][j].c_str(), -1, SQLITE_TRANSIENT); + } + } + ret = sqlite3_step(res); + if (ret != SQLITE_DONE) + { + logPrintf("Error in INSERT (%s) with VALUE including %s!\n", insertC, value_vector[0][0]); + } + // clean up the sqlite3_stmt + ret = sqlite3_finalize(res); + //logPrintf("Inserting row into %s table w/ SQL query:%s\n\n", tableName.c_str(), insertC.c_str()); if (ret != SQLITE_OK) { - logPrintf("Error in INSERT INTO statement (%s): %s\n", insertC, errmsg); + logPrintf("Error in INSERT INTO statement (%s): %s\n", insertC, sqlite3_errmsg(db)); } return ret == SQLITE_OK; } +bool insertRowsIntoDB(sqlite3* db, const String tableName, const Array>& value_vector) { + if (value_vector.length() == 0) { + logPrintf("Warning insert rows with empty row value array ignored!\n"); + return false; + } + + // Figure out how many insert groups we need + // SQLITE_LIMIT_VARIABLE_NUMBER indicates the max number of variables in the compiled sqlite version + int max_variables = sqlite3_limit(db, SQLITE_LIMIT_VARIABLE_NUMBER, -1); + int num_rows_per_group = max_variables / value_vector[0].size(); + //logPrintf("insertRows %d total in %d rows with %d groups of %d per INSERT\n", + // value_vector.size() * value_vector[0].size(), + // value_vector.size(), + // num_rows_per_group, + // value_vector[0].size() + // ); + + int ret = 0; // sqlite return value + int start_row = 0; + bool ret_val = true; + while (start_row < value_vector.size()) { + int end_row = min(start_row + num_rows_per_group, value_vector.size()); + //logPrintf("inserting rows %d to %d\n", start_row, end_row); + + bool success = groupInsertRows(db, tableName, value_vector, start_row, end_row); + + if (!success) { + logPrintf("Failed group insert!\n"); + ret_val = false; + } + + start_row += num_rows_per_group; + } + + return ret_val; +} + diff --git a/source/sqlHelpers.h b/source/sqlHelpers.h index bad6baf7..64f87063 100644 --- a/source/sqlHelpers.h +++ b/source/sqlHelpers.h @@ -5,5 +5,6 @@ bool createTableInDB(sqlite3* db, const String tableName, const Array>& columns); -bool insertRowIntoDB(sqlite3* db, const String tableName, const Array& values, const String colNames = ""); -bool insertRowsIntoDB(sqlite3* db, const String tableName, const Array>& valueVector, const String colNames = ""); +/**All values sent to this function are stored in the database as Strings.*/ +bool insertRowIntoDB(sqlite3* db, const String tableName, const Array& values); +bool insertRowsIntoDB(sqlite3* db, const String tableName, const Array>& valueVector);