From 0657883685487e29fcbefe4ec08d7d93be5e3c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 14 May 2026 13:59:53 +0200 Subject: [PATCH 1/8] Expose SyntaxInterface bridge APIs for jaspSyntax --- CMakeLists.txt | 2 + CommonData/CMakeLists.txt | 4 + CommonData/databaseinterface.cpp | 7 +- CommonData/databaseinterface.h | 1 + CommonData/databridge.cpp | 6 + CommonData/databridge.h | 5 + CommonData/rbridge.cpp | 7 + CommonData/rbridge.h | 1 + Engine/jaspBase | 2 +- QMLComponents/datasetprovider.cpp | 33 +- QMLComponents/datasetprovider.h | 2 + R-Interface/jasprcpp.cpp | 104 +++++-- SyntaxInterface/CMakeLists.txt | 4 +- SyntaxInterface/syntaxbridge.cpp | 378 +++++++++++++++++++---- SyntaxInterface/syntaxbridge_interface.h | 20 +- 15 files changed, 480 insertions(+), 96 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe611d9e08..d7c070877a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -207,6 +207,8 @@ else() USES_TERMINAL COMMENT "------ Configuring and Building the libR-Interface") + add_dependencies(SyntaxInterface R-Interface) + endif() if(NOT USE_QT_STATIC_LIBS) diff --git a/CommonData/CMakeLists.txt b/CommonData/CMakeLists.txt index 129da25bc6..647f6a0b11 100644 --- a/CommonData/CMakeLists.txt +++ b/CommonData/CMakeLists.txt @@ -60,6 +60,10 @@ target_link_libraries( ) +if(MSVC) + target_compile_options(CommonData PRIVATE /EHsc) +endif() + target_compile_definitions( CommonData PUBLIC $<$:PRINT_ENGINE_MESSAGES> diff --git a/CommonData/databaseinterface.cpp b/CommonData/databaseinterface.cpp index 0cf3ee324a..05a29750ae 100644 --- a/CommonData/databaseinterface.cpp +++ b/CommonData/databaseinterface.cpp @@ -2378,6 +2378,11 @@ void DatabaseInterface::preloadInterfaceForThread() _db(); } +void DatabaseInterface::loadExisting() +{ + load(); +} + void DatabaseInterface::load() { JASPTIMER_SCOPE(DatabaseInterface::load); @@ -2479,7 +2484,7 @@ void DatabaseInterface::close() _dbCheckMutex.unlock(); - while(sqlite3_close(_dbCreated) != SQLITE_OK) + while(_dbCreated && sqlite3_close(_dbCreated) != SQLITE_OK) { std::this_thread::sleep_for(std::chrono::nanoseconds(10000000)); } diff --git a/CommonData/databaseinterface.h b/CommonData/databaseinterface.h index 238ff01916..ca1b59c5c2 100644 --- a/CommonData/databaseinterface.h +++ b/CommonData/databaseinterface.h @@ -93,6 +93,7 @@ class DatabaseInterface static void closeInterfaces(); bool hasConnection() { return _db(); } + void loadExisting(); ///< Loads an already-written sqlite database from sessiondir, e.g. after extracting a .jasp archive. void upgradeDBFromVersion(Version originalVersion); ///< Ensures that the database has all the fields configured as required for the current JASP version, useful when loading older sqlite-containing jasp-files void runQuery( const std::string & query, std::function bindParameters, std::function processRow); ///< Runs a single query and then goes through the resultrows while calling processRow for each. diff --git a/CommonData/databridge.cpp b/CommonData/databridge.cpp index 9c00494c29..dfb51a11bb 100644 --- a/CommonData/databridge.cpp +++ b/CommonData/databridge.cpp @@ -36,6 +36,12 @@ DataBridge::DataBridge(unsigned long sessionID, bool useMemory) } } +DataBridge::~DataBridge() +{ + delete _dataSet; + _dataSet = nullptr; +} + void DataBridge::provideStateFileName(std::string & root, std::string & relativePath) { return TempFiles::createSpecific("state", _analysisId, root, relativePath); diff --git a/CommonData/databridge.h b/CommonData/databridge.h index 8d7a179355..0f0c2b6634 100644 --- a/CommonData/databridge.h +++ b/CommonData/databridge.h @@ -25,6 +25,11 @@ class DataBridge { public: DataBridge(unsigned long sessionID, bool useMemory = false); + ~DataBridge(); + DataBridge(const DataBridge &) = delete; + DataBridge & operator=(const DataBridge &) = delete; + DataBridge(DataBridge &&) = delete; + DataBridge & operator=(DataBridge &&) = delete; std::string createColumn( const std::string & columnName, bool computed=false); ///< Returns encoded columnname on success or "" on failure (cause it already exists) bool deleteColumn( const std::string & columnName); diff --git a/CommonData/rbridge.cpp b/CommonData/rbridge.cpp index f22a85840d..76608065ef 100644 --- a/CommonData/rbridge.cpp +++ b/CommonData/rbridge.cpp @@ -64,6 +64,13 @@ size_t _logWriteFunction(const void * buf, size_t len) void rbridge_setDataBridge(DataBridge * dataBridge) { data_bridge = dataBridge; + rbridge_dataSet = nullptr; +} + +void rbridge_clearDataBridge() +{ + data_bridge = nullptr; + rbridge_dataSet = nullptr; } const std::string jaspBaseDistributionSamplersR = diff --git a/CommonData/rbridge.h b/CommonData/rbridge.h index a1d4ac19e6..997e23fbd6 100644 --- a/CommonData/rbridge.h +++ b/CommonData/rbridge.h @@ -81,6 +81,7 @@ extern "C" { typedef std::function RCallback; void rbridge_setDataBridge(DataBridge * dataBridge); + void rbridge_clearDataBridge(); void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, pollMessagesFuncDef pollMessagesFunction, ColumnEncoder * encoder, const char * resultFont, bool insideJasp = true); void rbridge_memoryCleaning(); diff --git a/Engine/jaspBase b/Engine/jaspBase index 93f004588f..6d6834d608 160000 --- a/Engine/jaspBase +++ b/Engine/jaspBase @@ -1 +1 @@ -Subproject commit 93f004588f6d56b46d23210e23bff003ae7b1ff4 +Subproject commit 6d6834d608b55c8721f548277b767b4f2649973e diff --git a/QMLComponents/datasetprovider.cpp b/QMLComponents/datasetprovider.cpp index 3be6eaacf1..ac4615842c 100644 --- a/QMLComponents/datasetprovider.cpp +++ b/QMLComponents/datasetprovider.cpp @@ -26,6 +26,11 @@ DataSetProvider* DataSetProvider::getProvider(bool inMemory, bool reset, QObject { if (!_singleton) _singleton = new DataSetProvider(inMemory, parent); + else if (_singleton->_inMemory != inMemory) + { + delete _singleton; + _singleton = new DataSetProvider(inMemory, parent); + } else if (reset) _singleton->resetDataSet(); @@ -41,7 +46,7 @@ DataSetProvider::~DataSetProvider() _singleton = nullptr; } -DataSetProvider::DataSetProvider(bool inMemory, QObject *parent) : QAbstractTableModel(parent) +DataSetProvider::DataSetProvider(bool inMemory, QObject *parent) : QAbstractTableModel(parent), _inMemory(inMemory) { _db = new DatabaseInterface(true, inMemory); _dataSet = new DataSet(); @@ -52,6 +57,7 @@ DataSetProvider::DataSetProvider(bool inMemory, QObject *parent) : QAbstractTabl void DataSetProvider::resetDataSet() { + beginResetModel(); if (_dataSet) { _dataSet->dbDelete(); @@ -59,6 +65,31 @@ void DataSetProvider::resetDataSet() } _dataSet = new DataSet(); + endResetModel(); +} + +void DataSetProvider::reloadDataSetFromDatabase() +{ + beginResetModel(); + delete _dataSet; + _dataSet = nullptr; + + int dataSetId = _db ? _db->dataSetGetId() : -1; + if (dataSetId == 1 && _db->tableExists(_db->dataSetName(dataSetId))) + _dataSet = new DataSet(dataSetId); + else + _dataSet = new DataSet(); + + ColumnEncoder::columnEncoder()->setCurrentNames(_dataSet->getColumnTypesMap()); + endResetModel(); + + if (VariableInfo::info()) + { + emit VariableInfo::info()->dataSetChanged(); + emit VariableInfo::info()->rowCountChanged(); + emit VariableInfo::info()->variableCountChanged(); + emit VariableInfo::info()->dataAvailableChanged(); + } } int DataSetProvider::rowCount(const QModelIndex &) const diff --git a/QMLComponents/datasetprovider.h b/QMLComponents/datasetprovider.h index 9c3539338b..626b85ac3a 100644 --- a/QMLComponents/datasetprovider.h +++ b/QMLComponents/datasetprovider.h @@ -35,6 +35,7 @@ class DataSetProvider : public QAbstractTableModel, public VariableInfoProvider DataSet * dataSet() { return _dataSet; } void resetDataSet(); + void reloadDataSetFromDatabase(); int rowCount( const QModelIndex & parent = QModelIndex()) const override; int columnCount(const QModelIndex & parent = QModelIndex()) const override; @@ -59,6 +60,7 @@ class DataSetProvider : public QAbstractTableModel, public VariableInfoProvider DatabaseInterface * _db = nullptr; DataSet * _dataSet = nullptr; + bool _inMemory = true; }; diff --git a/R-Interface/jasprcpp.cpp b/R-Interface/jasprcpp.cpp index db5e889ce8..febc7d0c19 100644 --- a/R-Interface/jasprcpp.cpp +++ b/R-Interface/jasprcpp.cpp @@ -80,9 +80,10 @@ int __parseEval(const std::string & line, SEXP & ans) //#ifdef PRINT_ENGINE_MESSAGES //jaspRCPP_logString("parseEval: " + line + "\n"); //#endif + ans = R_NilValue; ParseStatus status; SEXP cmdSexp, cmdexpr = R_NilValue; - int i, errorOccurred; + int i, errorOccurred, rc = 0; PROTECT(cmdSexp = Rf_allocVector(STRSXP, 1)); SET_STRING_ELT(cmdSexp, 0, Rf_mkChar(line.c_str())); @@ -94,17 +95,21 @@ int __parseEval(const std::string & line, SEXP & ans) for(i = 0; i < Rf_length(cmdexpr); i++){ ans = R_tryEval(VECTOR_ELT(cmdexpr, i), Rcpp::Environment::global_env(), &errorOccurred); if (errorOccurred) { - UNPROTECT(2); - return 1; + rc = 1; + break; } } } - return 0; + else + rc = 1; + + UNPROTECT(2); + return rc; } SEXP _parseEval(const std::string &line) { - SEXP ans; + SEXP ans = R_NilValue; int rc = __parseEval(line, ans); if (rc != 0) { throw std::runtime_error(std::string("Error evaluating: ") + line); @@ -270,22 +275,22 @@ void STDCALL jaspRCPP_init_jaspBase() auto rEnvironment = Rcpp::Environment::global_env(); - rEnvironment[".logString"] = Rcpp::XPtr( & _logFuncDef); - rEnvironment[".createColumn"] = Rcpp::XPtr( & _createColumnFuncDef); - rEnvironment[".deleteColumn"] = Rcpp::XPtr( & _deleteColumnFuncDef); - rEnvironment[".getColumnType"] = Rcpp::XPtr( & _getColumnTypeFuncDef); - rEnvironment[".getColumnExists"] = Rcpp::XPtr( & _getColumnExistsFuncDef); - rEnvironment[".getColumnAnalysisId"] = Rcpp::XPtr( & _getColumnAnIdFuncDef); - rEnvironment[".getColumnOriginalIndex"] = Rcpp::XPtr( & _getColumnIndexFuncDef); - rEnvironment[".sendToDesktopFunction"] = Rcpp::XPtr( & _sendToDesktop); - rEnvironment[".pollMessagesFunction"] = Rcpp::XPtr( & _pollMessagesFunction); - rEnvironment[".setColumnDataAsScalePtr"] = Rcpp::XPtr( & _setColumnDataAsScale); - rEnvironment[".setColumnDataAsOrdinalPtr"] = Rcpp::XPtr( & _setColumnDataAsOrdinal); - rEnvironment[".setColumnDataAsNominalPtr"] = Rcpp::XPtr( & _setColumnDataAsOrdinal); - rEnvironment[".shouldEncodeColName"] = Rcpp::XPtr(& _shouldEncodeColumnName); - rEnvironment[".shouldDecodeColName"] = Rcpp::XPtr(& _shouldDecodeColumnName); - rEnvironment[".encodeColName"] = Rcpp::XPtr( & _encodeColumnName); - rEnvironment[".decodeColName"] = Rcpp::XPtr( & _decodeColumnName); + rEnvironment[".logString"] = Rcpp::XPtr( & _logFuncDef, false); + rEnvironment[".createColumn"] = Rcpp::XPtr( & _createColumnFuncDef, false); + rEnvironment[".deleteColumn"] = Rcpp::XPtr( & _deleteColumnFuncDef, false); + rEnvironment[".getColumnType"] = Rcpp::XPtr( & _getColumnTypeFuncDef, false); + rEnvironment[".getColumnExists"] = Rcpp::XPtr( & _getColumnExistsFuncDef, false); + rEnvironment[".getColumnAnalysisId"] = Rcpp::XPtr( & _getColumnAnIdFuncDef, false); + rEnvironment[".getColumnOriginalIndex"] = Rcpp::XPtr( & _getColumnIndexFuncDef, false); + rEnvironment[".sendToDesktopFunction"] = Rcpp::XPtr( & _sendToDesktop, false); + rEnvironment[".pollMessagesFunction"] = Rcpp::XPtr( & _pollMessagesFunction, false); + rEnvironment[".setColumnDataAsScalePtr"] = Rcpp::XPtr( & _setColumnDataAsScale, false); + rEnvironment[".setColumnDataAsOrdinalPtr"] = Rcpp::XPtr( & _setColumnDataAsOrdinal, false); + rEnvironment[".setColumnDataAsNominalPtr"] = Rcpp::XPtr( & _setColumnDataAsNominal, false); + rEnvironment[".shouldEncodeColName"] = Rcpp::XPtr(& _shouldEncodeColumnName, false); + rEnvironment[".shouldDecodeColName"] = Rcpp::XPtr(& _shouldDecodeColumnName, false); + rEnvironment[".encodeColName"] = Rcpp::XPtr( & _encodeColumnName, false); + rEnvironment[".decodeColName"] = Rcpp::XPtr( & _decodeColumnName, false); //Pass a whole bunch of pointers to jaspBase jaspRCPP_parseEvalQNT("jaspBase:::setColumnFuncs( .setColumnDataAsScalePtr, .setColumnDataAsOrdinalPtr, .setColumnDataAsNominalPtr, .getColumnType, .getColumnAnalysisId, .getColumnOriginalIndex, .createColumn, .deleteColumn, .getColumnExists, .encodeColName, .decodeColName, .shouldEncodeColName, .shouldDecodeColName)"); @@ -1240,12 +1245,53 @@ std::string __sinkMe(const std::string code) return "sink(.outputSink);\n" + code; //default type = c('message', 'output') anyway } +class SinkGuard +{ +public: + SinkGuard() + { + _parseEvalQNT(__sinkMe()); + } + + ~SinkGuard() + { + close(); + } + + void close() + { + if (!_active) + return; + + try + { + SEXP ignored = R_NilValue; + int rc = __parseEval("sink();", ignored); + if (rc != 0) + jaspRCPP_logString("SinkGuard failed to close the R output sink.\n"); + } + catch (const std::exception & exception) + { + jaspRCPP_logString(std::string("SinkGuard failed to close the R output sink: ") + exception.what() + "\n"); + } + catch (...) + { + jaspRCPP_logString("SinkGuard failed to close the R output sink with an unknown exception.\n"); + } + + _active = false; + } + +private: + bool _active = true; +}; + void jaspRCPP_setWorkingDirectory() { std::string root = requestTempRootNameCB(); std::string code = "setwd(\"" + root + "\");"; - _parseEvalQNT(__sinkMe(code)); - _parseEvalQNT("sink();"); //Back to normal! + SinkGuard sinkGuard; + _parseEvalQNT(code); } void jaspRCPP_parseEvalQNT(const std::string & code, bool setWd, bool preface) @@ -1256,10 +1302,9 @@ void jaspRCPP_parseEvalQNT(const std::string & code, bool setWd, bool preface) if(preface) jaspRCPP_parseEvalPreface(code); - _parseEvalQNT(__sinkMe()); + SinkGuard sinkGuard; _parseEvalQNT(code); jaspRCPP_logString("\n"); - _parseEvalQNT("sink();"); //Back to normal! } std::string jaspRCPP_parseEvalStringReturn(const std::string & code, bool setWd, bool preface) @@ -1278,12 +1323,12 @@ SEXP jaspRCPP_parseEval(const std::string & code, bool setWd, bool preface) if(preface) jaspRCPP_parseEvalPreface(code); - _parseEvalQNT(__sinkMe()); - SEXP returnthis = _parseEval(code); //Not throwing is nice actually! Well, unless you want to hear about missing modules etc... + SinkGuard sinkGuard; + SEXP returnthis = PROTECT(_parseEval(code)); // Keep the result alive while resetting the sink below. jaspRCPP_logString("\n"); + sinkGuard.close(); - _parseEvalQNT("sink();"); //back to normal! - + UNPROTECT(1); return returnthis; } @@ -1411,4 +1456,3 @@ SEXP jaspRCPP_CreateCaptureConnection() UNPROTECT(1); return rc; } - diff --git a/SyntaxInterface/CMakeLists.txt b/SyntaxInterface/CMakeLists.txt index fa13337521..756a95fd74 100644 --- a/SyntaxInterface/CMakeLists.txt +++ b/SyntaxInterface/CMakeLists.txt @@ -43,7 +43,7 @@ endif() target_compile_definitions( SyntaxInterface - PUBLIC + PRIVATE SYNTAX_INTERFACE_LIBRARY JASP_R_INTERFACE_LIBRARY ) @@ -51,7 +51,7 @@ target_compile_definitions( if(USE_QT_STATIC_LIBS) target_compile_definitions( SyntaxInterface - PUBLIC + PRIVATE USE_QT_STATIC_LIBS ) endif() diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index fddb50e662..5b07ba2f42 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -40,6 +42,9 @@ #include "utilities/appdirs.h" #include "modules/dynamicmodule.h" #include "archivereader.h" +#include "databaseinterface.h" + +#include #include #ifdef USE_QT_STATIC_LIBS @@ -51,6 +56,7 @@ Q_IMPORT_PLUGIN(JASP_ControlsPlugin) #define STRINGIZE(x) _STRINGIZE(x) static bool gl_initialized = false; +static bool gl_initializedDbInMemory = false; static QGuiApplication * gl_application = nullptr; static QQmlEngine * gl_qmlEngine = nullptr; static DataBridge * gl_dataBridge = nullptr; @@ -73,12 +79,184 @@ static std::string gl_param_resultFont = "freesans,sans-serif"; #endif +static bool readJaspJsonEntry(Json::Value & root, const char * filePath, const char * entry, std::string * error = nullptr) +{ + try + { + if (!filePath || std::string(filePath).empty()) + { + if (error) + *error = "Cannot read from an empty JASP archive path."; + return false; + } + + ArchiveReader reader(filePath, entry); + int errorCode = 0; + std::string json = reader.readAllData(sizeof(char), errorCode); + if (errorCode != 0) + { + if (error) + *error = std::string("Could not read entry ") + entry + " from JASP archive " + filePath + "."; + else + Log::log() << "Could not read JASP archive entry." << std::endl; + return false; + } + + Json::Reader parser; + if (!parser.parse(json, root)) + { + if (error) + *error = std::string("Could not parse entry ") + entry + " from JASP archive " + filePath + "."; + else + Log::log() << "Could not parse JASP archive entry." << std::endl; + return false; + } + return true; + } + catch (const std::exception & exception) + { + if (error) + *error = std::string("Could not read entry ") + entry + " from JASP archive " + (filePath ? filePath : "") + ": " + exception.what(); + else + Log::log() << "Could not read JASP archive entry." << std::endl; + return false; + } +} + +static const char* statusResult(Json::Value status) +{ + static std::string result; + result = status.toStyledString(); + return result.c_str(); +} + +static Json::Value statusBase(const char * operation) +{ + Json::Value status(Json::objectValue); + status["operation"] = operation; + status["ok"] = false; + return status; +} + +static const char* statusError(Json::Value status, const std::string & error) +{ + status["ok"] = false; + status["error"] = error; + Log::log() << error << std::endl; + return statusResult(status); +} + +static Json::Value analysisOptionsStatus(const char * filePath, int analysisNr) +{ + Json::Value status = statusBase("syntaxBridgeAnalysisOptionsFromJaspFile"); + status["analysisNr"] = analysisNr; + + Json::Value analysesJson; + std::string error; + if (!readJaspJsonEntry(analysesJson, filePath, "analyses.json", &error)) + { + status["failure"] = "read"; + status["error"] = error; + return status; + } + + const Json::Value & analyses = analysesJson["analyses"]; + if (!analyses.isArray()) + { + status["failure"] = "schema"; + status["error"] = std::string("JASP archive analyses.json does not contain an analyses array."); + return status; + } + + status["analysisCount"] = static_cast(analyses.size()); + if (analysisNr < 0 || analysisNr >= int(analyses.size())) + { + status["failure"] = "index"; + status["error"] = std::string("Could not find analysis ") + std::to_string(analysisNr) + " in JASP archive."; + return status; + } + + const Json::Value & options = analyses[analysisNr]["options"]; + if (options.isNull()) + { + status["failure"] = "schema"; + status["error"] = std::string("Analysis ") + std::to_string(analysisNr) + " does not contain options."; + return status; + } + + status["ok"] = true; + status["options"] = options; + return status; +} + +static void clearRequestedDataState() +{ + rbridge_setWantedCols(ColumnEncoder::colsPlusTypes()); + + ColumnEncoder::colTypeMap noColumns; + ColumnEncoder::columnEncoder()->setCurrentNames(noColumns); + + if (gl_extraEncodings) + gl_extraEncodings->setCurrentNames(noColumns); +} + +static void clearDataBridgeState() +{ + clearRequestedDataState(); + rbridge_clearDataBridge(); + + if (gl_dataBridge) + { + delete gl_dataBridge; + gl_dataBridge = nullptr; + } +} + +static void createDataBridge(bool dbInMemory) +{ + gl_dataBridge = new DataBridge(ProcessInfo::currentPID(), dbInMemory); + rbridge_setDataBridge(gl_dataBridge); + gl_initializedDbInMemory = dbInMemory; +} + +static DataSetProvider* resetDataProvider(bool dbInMemory, bool resetDataSet) +{ + DataSetProvider * provider = DataSetProvider::getProvider(dbInMemory, resetDataSet, gl_application); + gl_initializedDbInMemory = dbInMemory; + return provider; +} + +static bool recreateCleanDataBridgeState(bool dbInMemory) +{ + try + { + clearDataBridgeState(); + DataSetProvider::getProvider(!dbInMemory, true, gl_application); + resetDataProvider(dbInMemory, true); + createDataBridge(dbInMemory); + return gl_dataBridge != nullptr; + } + catch (const std::exception & exception) + { + Log::log() << "Could not restore SyntaxInterface native dataset state after failed JASP archive load: " << exception.what() << std::endl; + } + catch (...) + { + Log::log() << "Could not restore SyntaxInterface native dataset state after failed JASP archive load." << std::endl; + } + + return false; +} + extern "C" { -void STDCALL syntaxBridgeCleanup() +void STDCALL syntaxBridgeClearQmlState() { for (auto value : gl_qmlFormMap.values()) deleteQuickItem(value.second); + if (gl_application) + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + gl_qmlFormMap.clear(); if (gl_qmlEngine) @@ -88,6 +266,28 @@ void STDCALL syntaxBridgeCleanup() } } +void STDCALL syntaxBridgeClearDataSetState() +{ + clearDataBridgeState(); + + if (gl_initialized) + { + resetDataProvider(gl_initializedDbInMemory, true); + createDataBridge(gl_initializedDbInMemory); + } +} + +void STDCALL syntaxBridgeClearNativeState() +{ + syntaxBridgeClearQmlState(); + syntaxBridgeClearDataSetState(); +} + +void STDCALL syntaxBridgeCleanup() +{ + syntaxBridgeClearQmlState(); +} + void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeDataSet, bool dbInMemory, int threshold, bool orderLabelsByValue) { if (!init(dbInMemory)) @@ -96,7 +296,15 @@ void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeData return; } - DataSetProvider* provider = DataSetProvider::getProvider(dbInMemory); + DataSetProvider* provider = nullptr; + if (gl_initializedDbInMemory != dbInMemory) + { + clearDataBridgeState(); + provider = resetDataProvider(dbInMemory, true); + createDataBridge(dbInMemory); + } + else + provider = DataSetProvider::getProvider(dbInMemory); std::map dataSet; @@ -112,6 +320,85 @@ void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeData provider->loadDataSet(dataSet, threshold, orderLabelsByValue); } +void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory) +{ + syntaxBridgeLoadDataSetFromJaspFileStatus(filePath, dbInMemory); +} + +const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * filePath, bool dbInMemory) +{ + Json::Value status = statusBase("syntaxBridgeLoadDataSetFromJaspFile"); + status["dbInMemoryRequested"] = dbInMemory; + status["dbInMemoryUsed"] = false; + + if (!filePath || std::string(filePath).empty()) + return statusError(status, "Cannot load dataset from an empty JASP archive path."); + + if (dbInMemory) + status["warning"] = "dbInMemory=TRUE is ignored for .jasp archives; SyntaxInterface loads archive databases through file-backed internal.sqlite."; + + if (!init(false)) + { + return statusError(status, "Error during initialization."); + } + + bool nativeStateMutated = false; + + try + { + Json::Value manifest; + std::string manifestError; + if (!readJaspJsonEntry(manifest, filePath, "manifest.json", &manifestError)) + return statusError(status, manifestError); + + std::string jaspVersionStr = manifest.get("jaspVersion", "").asString(); + std::string archiveVersionStr = manifest.get("jaspArchiveVersion", "").asString(); + if (archiveVersionStr.empty()) + return statusError(status, "JASP archive manifest is missing jaspArchiveVersion."); + if (jaspVersionStr.empty()) + return statusError(status, "JASP archive manifest is missing jaspVersion."); + + status["jaspArchiveVersion"] = archiveVersionStr; + status["jaspVersion"] = jaspVersionStr; + + Version jaspVersion(jaspVersionStr); + + // Keep SyntaxInterface below Desktop's DataSetPackage/UI ownership while + // mirroring the archive import steps that matter for backend replay: + // extract internal.sqlite, upgrade it for the saved JASP version, then + // expose it through the bridge-owned DataBridge. + clearDataBridgeState(); + nativeStateMutated = true; + DataSetProvider * provider = resetDataProvider(false, false); + DatabaseInterface * database = DatabaseInterface::singleton(); + database->close(); + ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float) {}); + database->loadExisting(); + database->upgradeDBFromVersion(jaspVersion); + status["databaseUpgraded"] = true; + provider->reloadDataSetFromDatabase(); + createDataBridge(false); + + DataSet * dataSet = gl_dataBridge ? gl_dataBridge->provideAndUpdateDataSet() : nullptr; + if (!dataSet) + { + status["nativeStateRestored"] = recreateCleanDataBridgeState(false); + return statusError(status, std::string("Could not load dataset from JASP archive ") + filePath + ": no dataset was provided by the bridge."); + } + + status["ok"] = true; + status["columnCount"] = static_cast(dataSet->columnCount()); + status["rowCount"] = static_cast(dataSet->rowCount()); + return statusResult(status); + } + catch (const std::exception & exception) + { + if (nativeStateMutated) + status["nativeStateRestored"] = recreateCleanDataBridgeState(false); + return statusError(status, std::string("Could not load dataset from JASP archive ") + filePath + ": " + exception.what()); + } +} + const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData) { if (!init()) @@ -155,6 +442,31 @@ const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char* moduleName, c return result.c_str(); } +const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) +{ + static std::string result; + result = ""; + + Json::Value status = analysisOptionsStatus(filePath, analysisNr); + if (!status["ok"].asBool()) + { + if (status.isMember("error")) + Log::log() << status["error"].asString() << std::endl; + return result.c_str(); + } + + result = status["options"].toStyledString(); + return result.c_str(); +} + +const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr) +{ + Json::Value status = analysisOptionsStatus(filePath, analysisNr); + if (!status["ok"].asBool() && status.isMember("error")) + Log::log() << status["error"].asString() << std::endl; + return statusResult(status); +} + const char* STDCALL syntaxBridgeGenerateModuleWrappers(const char* modulePath) { @@ -263,71 +575,28 @@ const char* STDCALL syntaxBridgeParseDescription(const char* modulePath) return result.c_str(); } -void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory) +const char* STDCALL syntaxBridgeGetVariableNames() { - if (!init(dbInMemory)) - { - Log::log() << "Error during initialization" << std::endl; - return; - } - - ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float p){}); - ManifestInfo info = ArchiveReader::readManifest(filePath); - - DataSetProvider* provider = DataSetProvider::getProvider(dbInMemory, false); - - provider->loadDatabase(info.jaspVersion); -} + static std::string result; -const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr) -{ if (!init()) { Log::log() << "Error during initialization" << std::endl; - return ""; - } - - static std::string result; - - result = ""; - Json::Value analysesData; - - if (ArchiveReader::parseJsonEntry(analysesData, filePath, "analyses.json", false)) - { - Json::Value analysesDataList = analysesData.get("analyses", analysesData); - if (analysisNr < analysesDataList.size()) - result = analysesDataList[analysisNr]["options"].toStyledString(); - else - Log::log() << "Analyis number is higher than the number of analyses (" << analysesDataList.size() << ") in the JASP file" << std::endl; + result = ""; + return result.c_str(); } - else - Log::log() << "Fail to open or read the JASP file " << filePath << std::endl; - - - return result.c_str(); -} -const char* STDCALL syntaxBridgeGetVariableNames() -{ - DataSetProvider* provider = DataSetProvider::getProvider(false, false); - if (!provider) - return ""; - - static std::string result; + size_t numCols = 0; + const char ** columnNames = rbridge_allColumnNames(numCols, false); - QStringList names = provider->provideInfo(VariableInfo::VariableNames).toStringList(); Json::Value jsonNames(Json::arrayValue); - - for (const QString & name : names) - jsonNames.append(fq(name)); + for (size_t i = 0; i < numCols; ++i) + jsonNames.append(columnNames[i]); result = jsonNames.toStyledString(); - return result.c_str(); } - - } // extern "C" @@ -356,6 +625,7 @@ bool init(bool dbInMemory) { if (gl_initialized) return true; gl_initialized = true; + gl_initializedDbInMemory = dbInMemory; if (gl_verbose) { @@ -404,7 +674,7 @@ bool init(bool dbInMemory) QmlUtils::setupQMLEngine(gl_qmlEngine); QmlUtils::registerQmlModuleTypes(); - gl_dataBridge = new DataBridge(ProcessInfo::currentPID(), dbInMemory); + createDataBridge(dbInMemory); gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); diff --git a/SyntaxInterface/syntaxbridge_interface.h b/SyntaxInterface/syntaxbridge_interface.h index 0c0d5b54bc..be0e827416 100644 --- a/SyntaxInterface/syntaxbridge_interface.h +++ b/SyntaxInterface/syntaxbridge_interface.h @@ -39,30 +39,36 @@ extern "C" { +// Keep these exported structs plain C ABI data. Callers must initialize every +// field explicitly, e.g. with {} in C++ or calloc/memset in C. struct SyntaxBridgeColumn { - char * name = nullptr; - char ** values = nullptr; + char * name; + char ** values; } ; struct SyntaxBridgeDataSet { - char * name = nullptr; - int rowCount = 0; - int columnCount = 0; - SyntaxBridgeColumn * columns = nullptr; + char * name; + int rowCount; + int columnCount; + SyntaxBridgeColumn * columns; }; SYNTAX_INTERFACE void STDCALL syntaxBridgeCleanup(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeClearQmlState(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeClearDataSetState(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeClearNativeState(); SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* dataset, bool dbInMemory, int threshold, bool orderLabelsByValue); SYNTAX_INTERFACE void STDCALL syntaxBridgeLoadDataSetFromJaspFile(const char * filePath, bool dbInMemory); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * filePath, bool dbInMemory); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeLoadQmlAndParseOptions(const char * moduleName, const char* analysisName, const char* qmlFile, const char* options, const char* version, bool preloadData); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFile(const char * filePath, int analysisNr); +SYNTAX_INTERFACE const char* STDCALL syntaxBridgeAnalysisOptionsFromJaspFileStatus(const char * filePath, int analysisNr); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateModuleWrappers(const char* name); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGenerateAnalysisWrapper(const char* modulePath, const char* analysisName); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeParseDescription(const char* modulePath); SYNTAX_INTERFACE const char* STDCALL syntaxBridgeGetVariableNames(); - } // extern "C" #endif // SYNTAXBRIDGE_INTERFACE_H From 4eca8fe4e1486c08c252607b0b8702a37d4f5636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 14 May 2026 20:03:17 +0200 Subject: [PATCH 2/8] Stabilize SyntaxInterface replay loading --- SyntaxInterface/syntaxbridge.cpp | 34 +++++++++++--------------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 5b07ba2f42..43adf3e1cc 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -45,6 +45,7 @@ #include "databaseinterface.h" #include +#include #include #ifdef USE_QT_STATIC_LIBS @@ -62,6 +63,9 @@ static QQmlEngine * gl_qmlEngine = nullptr; static DataBridge * gl_dataBridge = nullptr; static ColumnEncoder * gl_extraEncodings = nullptr; static QMap > gl_qmlFormMap; +static int gl_applicationArgc = 0; +static std::vector gl_applicationArgvStorage; +static std::vector gl_applicationArgv; static bool gl_verbose = #ifdef JASP_DEBUG @@ -373,10 +377,8 @@ const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * fileP DatabaseInterface * database = DatabaseInterface::singleton(); database->close(); ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float) {}); - database->loadExisting(); - database->upgradeDBFromVersion(jaspVersion); + provider->loadDatabase(jaspVersion); status["databaseUpgraded"] = true; - provider->reloadDataSetFromDatabase(); createDataBridge(false); DataSet * dataSet = gl_dataBridge ? gl_dataBridge->provideAndUpdateDataSet() : nullptr; @@ -640,31 +642,19 @@ bool init(bool dbInMemory) Log::log() << "R_HOME: " << fq(rHome) << std::endl; } - int dummyArgc = 1; - char dummyArgv[2]; - dummyArgv[0] = '?'; - dummyArgv[1] = '\0'; - //const char* platformArg = "-platform"; //const char* platformOpt = "minimal"; //"cocoa"; - std::vector arguments = {"JASP"}; //{qmlR, platformArg, platformOpt}; - - - int argc = arguments.size(); - char** argvs = new char*[argc]; - - for (int i = 0; i < argc; i++) - { - argvs[i] = new char[strlen(arguments[i]) + 1]; - memset(argvs[i], '\0', strlen(arguments[i]) + 1); - memcpy(argvs[i], arguments[i], strlen(arguments[i])); - argvs[i][ strlen(arguments[i])] = '\0'; - } + gl_applicationArgvStorage = {"JASP"}; //{qmlR, platformArg, platformOpt}; + gl_applicationArgv.clear(); + for (std::string & argument : gl_applicationArgvStorage) + gl_applicationArgv.push_back(argument.data()); + gl_applicationArgv.push_back(nullptr); + gl_applicationArgc = static_cast(gl_applicationArgvStorage.size()); qputenv("QT_QPA_PLATFORM", "minimal"); - gl_application = new QGuiApplication(argc, argvs); + gl_application = new QGuiApplication(gl_applicationArgc, gl_applicationArgv.data()); gl_qmlEngine = new QQmlEngine(); Dirs::setLocalAppdataDir(AppDirs::appData(false).toStdString()); From 60d9c62e7f35d9ae9e80c5669a9692ab73a3c515 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 14 May 2026 21:41:54 +0200 Subject: [PATCH 3/8] Quiet SyntaxInterface warning paths --- CommonData/rbridge.cpp | 5 +- .../components/JASP/Controls/GridLayout.qml | 2 +- R-Interface/jasprcpp.cpp | 7 +- SyntaxInterface/syntaxbridge.cpp | 67 ++++++++++++++++++- SyntaxInterface/syntaxbridge_interface.h | 1 + 5 files changed, 76 insertions(+), 6 deletions(-) diff --git a/CommonData/rbridge.cpp b/CommonData/rbridge.cpp index 76608065ef..d3df6ed30e 100644 --- a/CommonData/rbridge.cpp +++ b/CommonData/rbridge.cpp @@ -156,6 +156,9 @@ void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, po jaspBaseTransformJohnsonR + "\n" + jaspBaseTransformYeoJohnsonR + "\n" + jaspBaseTransformPowerR; + // SyntaxInterface uses insideJasp=false for parse/dataset replay and only + // needs the native callbacks; the Engine still receives the full R helpers. + const char * initRCodeForMode = insideJasp ? initRCode.c_str() : ""; Log::log() << "Entering jaspRCPP_init." << std::endl; jaspRCPP_init( AppInfo::getBuildYear() .c_str(), @@ -169,7 +172,7 @@ void rbridge_init(DataBridge * dataBridge, sendFuncDef sendToDesktopFunction, po rbridge_moduleLibraryFixer, resultFont, tempDirStatic.c_str(), - initRCode.c_str(), + initRCodeForMode, insideJasp ); JASPTIMER_STOP(jaspRCPP_init); diff --git a/QMLComponents/components/JASP/Controls/GridLayout.qml b/QMLComponents/components/JASP/Controls/GridLayout.qml index 39fcc530ed..af462ff6c0 100644 --- a/QMLComponents/components/JASP/Controls/GridLayout.qml +++ b/QMLComponents/components/JASP/Controls/GridLayout.qml @@ -85,7 +85,7 @@ QT.GridLayout function _checkColumns() { - if (!_initialized || (width === 0)) return; + if (!_initialized || width <= 0 || implicitWidth <= 0 || !isFinite(width) || !isFinite(implicitWidth)) return; if (width < (implicitWidth - 1) && gridLayout.columns >= 2) { diff --git a/R-Interface/jasprcpp.cpp b/R-Interface/jasprcpp.cpp index febc7d0c19..37799b5a5a 100644 --- a/R-Interface/jasprcpp.cpp +++ b/R-Interface/jasprcpp.cpp @@ -244,8 +244,11 @@ void STDCALL jaspRCPP_init(const char* buildYear, const char* version, RBridgeCa jaspRCPP_parseEvalQNT("library(methods)"); - jaspRCPP_logString("Loading friendly R functions for computed columns and filters."); - jaspRCPP_parseEvalQNT(initFriendlyFunctionsRCode, false, false); + if (initFriendlyFunctionsRCode && initFriendlyFunctionsRCode[0] != '\0') + { + jaspRCPP_logString("Loading friendly R functions for computed columns and filters."); + jaspRCPP_parseEvalQNT(initFriendlyFunctionsRCode, false, false); + } _R_HOME = jaspRCPP_parseEvalStringReturn("R.home('')"); jaspRCPP_logString("jaspRCPP_init is done, R_HOME is: " + _R_HOME + "\n"); diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 43adf3e1cc..13f8d7cb24 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -58,6 +58,8 @@ Q_IMPORT_PLUGIN(JASP_ControlsPlugin) static bool gl_initialized = false; static bool gl_initializedDbInMemory = false; +static bool gl_rBridgeInitialized = false; +static bool gl_jaspBaseInitialized = false; static QGuiApplication * gl_application = nullptr; static QQmlEngine * gl_qmlEngine = nullptr; static DataBridge * gl_dataBridge = nullptr; @@ -292,6 +294,45 @@ void STDCALL syntaxBridgeCleanup() syntaxBridgeClearQmlState(); } +void STDCALL syntaxBridgeShutdown() +{ + syntaxBridgeClearQmlState(); + clearDataBridgeState(); + + if (gl_extraEncodings) + { + delete gl_extraEncodings; + gl_extraEncodings = nullptr; + } + + if (gl_initialized) + { + DataSetProvider * provider = DataSetProvider::getProvider(gl_initializedDbInMemory, false, gl_application); + delete provider; + } + + if (gl_qmlEngine) + { + delete gl_qmlEngine; + gl_qmlEngine = nullptr; + } + + if (gl_application) + { + gl_application->processEvents(); + delete gl_application; + gl_application = nullptr; + } + + gl_applicationArgc = 0; + gl_applicationArgv.clear(); + gl_applicationArgvStorage.clear(); + gl_initialized = false; + gl_initializedDbInMemory = false; + gl_rBridgeInitialized = false; + gl_jaspBaseInitialized = false; +} + void STDCALL syntaxBridgeLoadDataSet(const SyntaxBridgeDataSet* syntaxBridgeDataSet, bool dbInMemory, int threshold, bool orderLabelsByValue) { if (!init(dbInMemory)) @@ -668,17 +709,39 @@ bool init(bool dbInMemory) gl_extraEncodings = new ColumnEncoder("JaspExtraOptions_"); rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); - - jaspRCPP_init_jaspBase(); + gl_rBridgeInitialized = true; return true; } +void ensureRBridgeInitialized() +{ + if (gl_rBridgeInitialized) + return; + + rbridge_init(gl_dataBridge, sendMessage, [](){ return false; }, gl_extraEncodings, gl_param_resultFont.c_str(), false); + gl_rBridgeInitialized = true; +} + +void ensureJaspBaseInitialized() +{ + if (gl_jaspBaseInitialized) + return; + + // Option parsing and dataset replay do not need jaspBase. Load it only for + // the less common path where QML explicitly asks to evaluate R code. + ensureRBridgeInitialized(); + jaspRCPP_init_jaspBase(); + gl_jaspBaseInitialized = true; +} + void sendRScriptHandler(AnalysisForm* form, QString script, QString controlName, bool whiteListedVersion) { if (gl_verbose) Log::log() << "R Script " << fq(script) << " sent by " << controlName << std::endl; + ensureJaspBaseInitialized(); + bool hasError = false; std::string result = rbridge_evalRCodeWhiteListed(fq(script).c_str(), whiteListedVersion); diff --git a/SyntaxInterface/syntaxbridge_interface.h b/SyntaxInterface/syntaxbridge_interface.h index be0e827416..e228746288 100644 --- a/SyntaxInterface/syntaxbridge_interface.h +++ b/SyntaxInterface/syntaxbridge_interface.h @@ -55,6 +55,7 @@ struct SyntaxBridgeDataSet { SYNTAX_INTERFACE void STDCALL syntaxBridgeCleanup(); +SYNTAX_INTERFACE void STDCALL syntaxBridgeShutdown(); SYNTAX_INTERFACE void STDCALL syntaxBridgeClearQmlState(); SYNTAX_INTERFACE void STDCALL syntaxBridgeClearDataSetState(); SYNTAX_INTERFACE void STDCALL syntaxBridgeClearNativeState(); From e7d0c8de91a319ac28c14041f2813dbf99da459f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Thu, 14 May 2026 22:40:54 +0200 Subject: [PATCH 4/8] Refresh SyntaxInterface dataset context --- Engine/jaspBase | 2 +- QMLComponents/datasetprovider.cpp | 26 +----------------- QMLComponents/datasetprovider.h | 1 - SyntaxInterface/syntaxbridge.cpp | 44 ++++++++++++++++++++----------- 4 files changed, 31 insertions(+), 42 deletions(-) diff --git a/Engine/jaspBase b/Engine/jaspBase index 6d6834d608..f51c8d7aad 160000 --- a/Engine/jaspBase +++ b/Engine/jaspBase @@ -1 +1 @@ -Subproject commit 6d6834d608b55c8721f548277b767b4f2649973e +Subproject commit f51c8d7aadf45367c444545b79c360ef14dddc69 diff --git a/QMLComponents/datasetprovider.cpp b/QMLComponents/datasetprovider.cpp index ac4615842c..53c39ea42c 100644 --- a/QMLComponents/datasetprovider.cpp +++ b/QMLComponents/datasetprovider.cpp @@ -68,30 +68,6 @@ void DataSetProvider::resetDataSet() endResetModel(); } -void DataSetProvider::reloadDataSetFromDatabase() -{ - beginResetModel(); - delete _dataSet; - _dataSet = nullptr; - - int dataSetId = _db ? _db->dataSetGetId() : -1; - if (dataSetId == 1 && _db->tableExists(_db->dataSetName(dataSetId))) - _dataSet = new DataSet(dataSetId); - else - _dataSet = new DataSet(); - - ColumnEncoder::columnEncoder()->setCurrentNames(_dataSet->getColumnTypesMap()); - endResetModel(); - - if (VariableInfo::info()) - { - emit VariableInfo::info()->dataSetChanged(); - emit VariableInfo::info()->rowCountChanged(); - emit VariableInfo::info()->variableCountChanged(); - emit VariableInfo::info()->dataAvailableChanged(); - } -} - int DataSetProvider::rowCount(const QModelIndex &) const { return _dataSet->columnCount(); @@ -149,7 +125,7 @@ void DataSetProvider::loadDatabase(const Version & jaspVersion) delete _dataSet; _db->close(); - _db->load(); + _db->loadExisting(); _db->upgradeDBFromVersion(jaspVersion); _dataSet = new DataSet(0); // Setting 0 for "do nothing" because otherwise we can't pass on jaspVersion diff --git a/QMLComponents/datasetprovider.h b/QMLComponents/datasetprovider.h index 626b85ac3a..e3e6f77db1 100644 --- a/QMLComponents/datasetprovider.h +++ b/QMLComponents/datasetprovider.h @@ -35,7 +35,6 @@ class DataSetProvider : public QAbstractTableModel, public VariableInfoProvider DataSet * dataSet() { return _dataSet; } void resetDataSet(); - void reloadDataSetFromDatabase(); int rowCount( const QModelIndex & parent = QModelIndex()) const override; int columnCount(const QModelIndex & parent = QModelIndex()) const override; diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 13f8d7cb24..4c516f44e4 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -225,10 +225,38 @@ static void createDataBridge(bool dbInMemory) gl_initializedDbInMemory = dbInMemory; } +static void clearQmlFormCache() +{ + for (auto value : gl_qmlFormMap.values()) + deleteQuickItem(value.second); + + if (gl_application) + QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); + + gl_qmlFormMap.clear(); + + if (gl_qmlEngine) + { + gl_qmlEngine->clearSingletons(); + gl_qmlEngine->clearComponentCache(); + } +} + +static void refreshQmlDataSetInfoContext() +{ + if (gl_qmlEngine) + gl_qmlEngine->rootContext()->setContextProperty("dataSetInfo", VariableInfo::info()); +} + static DataSetProvider* resetDataProvider(bool dbInMemory, bool resetDataSet) { + bool providerWillBeRecreated = gl_initialized && gl_initializedDbInMemory != dbInMemory; + if (providerWillBeRecreated) + clearQmlFormCache(); + DataSetProvider * provider = DataSetProvider::getProvider(dbInMemory, resetDataSet, gl_application); gl_initializedDbInMemory = dbInMemory; + refreshQmlDataSetInfoContext(); return provider; } @@ -257,19 +285,7 @@ static bool recreateCleanDataBridgeState(bool dbInMemory) extern "C" { void STDCALL syntaxBridgeClearQmlState() { - for (auto value : gl_qmlFormMap.values()) - deleteQuickItem(value.second); - - if (gl_application) - QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); - - gl_qmlFormMap.clear(); - - if (gl_qmlEngine) - { - gl_qmlEngine->clearSingletons(); - gl_qmlEngine->clearComponentCache(); - } + clearQmlFormCache(); } void STDCALL syntaxBridgeClearDataSetState() @@ -415,8 +431,6 @@ const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * fileP clearDataBridgeState(); nativeStateMutated = true; DataSetProvider * provider = resetDataProvider(false, false); - DatabaseInterface * database = DatabaseInterface::singleton(); - database->close(); ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float) {}); provider->loadDatabase(jaspVersion); status["databaseUpgraded"] = true; From f256c7f40543379a98b6bd1dd83bb3e4449e91b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Fri, 15 May 2026 07:32:10 +0200 Subject: [PATCH 5/8] Harden archive dataset replay lifecycle --- CommonData/archivereader.cpp | 11 +++++++++++ QMLComponents/datasetprovider.cpp | 32 +++++++++++++++++++++++++------ QMLComponents/datasetprovider.h | 1 + SyntaxInterface/syntaxbridge.cpp | 3 ++- 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/CommonData/archivereader.cpp b/CommonData/archivereader.cpp index ab55da8427..83aa16b5d6 100644 --- a/CommonData/archivereader.cpp +++ b/CommonData/archivereader.cpp @@ -111,6 +111,8 @@ void ArchiveReader::writeEntryToTempFiles(std::function progressCal std::ofstream file(TempFiles::createSpecific("", _entryPath).c_str(), std::ios::out | std::ios::binary); + if (!file.is_open()) + throw runtime_error("Could not open temporary archive entry '" + _entryPath + "' for writing."); static char streamBuff[8192 * 32]; file.rdbuf()->pubsetbuf(streamBuff, sizeof(streamBuff)); //Set the buffer manually to make it much faster our issue https://github.com/jasp-stats/INTERNAL-jasp/issues/436 and solution from: https://stackoverflow.com/a/15177770 @@ -130,10 +132,19 @@ void ArchiveReader::writeEntryToTempFiles(std::function progressCal if(bytes > 0 && errorCode == 0) file.write(copyBuff, bytes); else break; + + if (!file.good()) + throw runtime_error("Could not write temporary archive entry '" + _entryPath + "'."); } while (true); + if (errorCode != 0) + throw runtime_error("Could not read archive entry '" + _entryPath + "'."); + file.flush(); + if (!file.good()) + throw runtime_error("Could not flush temporary archive entry '" + _entryPath + "'."); + file.close(); } diff --git a/QMLComponents/datasetprovider.cpp b/QMLComponents/datasetprovider.cpp index 53c39ea42c..4517c8be3c 100644 --- a/QMLComponents/datasetprovider.cpp +++ b/QMLComponents/datasetprovider.cpp @@ -20,6 +20,8 @@ #include "utilities/qutils.h" #include "columnencoder.h" +#include + DataSetProvider * DataSetProvider::_singleton = nullptr; DataSetProvider* DataSetProvider::getProvider(bool inMemory, bool reset, QObject* parent) @@ -120,18 +122,36 @@ void DataSetProvider::loadDataSet(const std::map & data } +void DataSetProvider::closeDatabase() +{ + _db->close(); +} + void DataSetProvider::loadDatabase(const Version & jaspVersion) { + beginResetModel(); delete _dataSet; + _dataSet = nullptr; - _db->close(); - _db->loadExisting(); - _db->upgradeDBFromVersion(jaspVersion); + try + { + _db->close(); + _db->loadExisting(); + _db->upgradeDBFromVersion(jaspVersion); - _dataSet = new DataSet(0); // Setting 0 for "do nothing" because otherwise we can't pass on jaspVersion - _dataSet->dbLoad(1, [](float p) {}, jaspVersion); + std::unique_ptr loadedDataSet(new DataSet(0)); // Setting 0 for "do nothing" because otherwise we can't pass on jaspVersion + loadedDataSet->dbLoad(1, [](float p) {}, jaspVersion); - ColumnEncoder::columnEncoder()->setCurrentNames(_dataSet->getColumnTypesMap()); + _dataSet = loadedDataSet.release(); + ColumnEncoder::columnEncoder()->setCurrentNames(_dataSet->getColumnTypesMap()); + endResetModel(); + } + catch (...) + { + _dataSet = new DataSet(); + endResetModel(); + throw; + } } QVariantList DataSetProvider::_getDoubleList(Column * column) const diff --git a/QMLComponents/datasetprovider.h b/QMLComponents/datasetprovider.h index e3e6f77db1..52df51fe71 100644 --- a/QMLComponents/datasetprovider.h +++ b/QMLComponents/datasetprovider.h @@ -41,6 +41,7 @@ class DataSetProvider : public QAbstractTableModel, public VariableInfoProvider QVariant data( const QModelIndex & index, int role = Qt::DisplayRole) const override; void loadDataSet(const std::map & dataSet, int threshold = 10, bool orderLabelsByValue = true); + void closeDatabase(); void loadDatabase(const Version & jaspVersion); QVariant provideInfo(VariableInfo::InfoType info, const QString& colName = "", int row = 0) const override; diff --git a/SyntaxInterface/syntaxbridge.cpp b/SyntaxInterface/syntaxbridge.cpp index 4c516f44e4..82aa02a711 100644 --- a/SyntaxInterface/syntaxbridge.cpp +++ b/SyntaxInterface/syntaxbridge.cpp @@ -166,7 +166,7 @@ static Json::Value analysisOptionsStatus(const char * filePath, int analysisNr) return status; } - const Json::Value & analyses = analysesJson["analyses"]; + const Json::Value & analyses = analysesJson.isArray() ? analysesJson : analysesJson["analyses"]; if (!analyses.isArray()) { status["failure"] = "schema"; @@ -431,6 +431,7 @@ const char* STDCALL syntaxBridgeLoadDataSetFromJaspFileStatus(const char * fileP clearDataBridgeState(); nativeStateMutated = true; DataSetProvider * provider = resetDataProvider(false, false); + provider->closeDatabase(); ArchiveReader(filePath, DatabaseInterface::singleton()->dbFile(true)).writeEntryToTempFiles([](float) {}); provider->loadDatabase(jaspVersion); status["databaseUpgraded"] = true; From 722ed7d94c178df74a9d54c351507216e912fb7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Fri, 15 May 2026 09:25:08 +0200 Subject: [PATCH 6/8] Update jaspBase bridge submodule --- Engine/jaspBase | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Engine/jaspBase b/Engine/jaspBase index f51c8d7aad..fc70f1a4f8 160000 --- a/Engine/jaspBase +++ b/Engine/jaspBase @@ -1 +1 @@ -Subproject commit f51c8d7aadf45367c444545b79c360ef14dddc69 +Subproject commit fc70f1a4f8687ad226886a57d73239475b124f65 From 780c387378e0cc9d8bfafef73910140560433ed9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Franti=C5=A1ek=20Barto=C5=A1?= Date: Fri, 15 May 2026 10:02:46 +0200 Subject: [PATCH 7/8] Pin desktop CI to locked R version --- .github/workflows/ubuntu.yml | 3 ++- .github/workflows/windows.yml | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index d7ad4bcdc2..4b446aae03 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -17,13 +17,14 @@ jobs: runs-on: ubuntu-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + JASP_R_VERSION: '4.5.3' steps: - uses: actions/checkout@v3 - uses: r-lib/actions/setup-r@v2 with: - r-version: 'release' + r-version: ${{ env.JASP_R_VERSION }} - name: Get Installed R Version run: echo "R_VERSION=$(R --version | head -n 1 | awk '{print $3}')" >> $GITHUB_ENV diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index fc5e117b4a..1c4785ed9a 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -12,6 +12,7 @@ jobs: runs-on: windows-latest env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} + JASP_R_VERSION: '4.5.3' name: build test on Windows @@ -19,7 +20,7 @@ jobs: - uses: r-lib/actions/setup-r@v2 with: - r-version: 'release' + r-version: ${{ env.JASP_R_VERSION }} windows-path-include-rtools: false windows-path-include-mingw: true From 6b05dd666db9da6f5b833b3dedf98e2230d4dfe9 Mon Sep 17 00:00:00 2001 From: boutinb Date: Fri, 15 May 2026 15:12:32 +0200 Subject: [PATCH 8/8] Remove unnecessary loadExisting function --- CommonData/databaseinterface.cpp | 5 ----- CommonData/databaseinterface.h | 1 - QMLComponents/datasetprovider.cpp | 2 +- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/CommonData/databaseinterface.cpp b/CommonData/databaseinterface.cpp index 05a29750ae..b2209282aa 100644 --- a/CommonData/databaseinterface.cpp +++ b/CommonData/databaseinterface.cpp @@ -2378,11 +2378,6 @@ void DatabaseInterface::preloadInterfaceForThread() _db(); } -void DatabaseInterface::loadExisting() -{ - load(); -} - void DatabaseInterface::load() { JASPTIMER_SCOPE(DatabaseInterface::load); diff --git a/CommonData/databaseinterface.h b/CommonData/databaseinterface.h index ca1b59c5c2..238ff01916 100644 --- a/CommonData/databaseinterface.h +++ b/CommonData/databaseinterface.h @@ -93,7 +93,6 @@ class DatabaseInterface static void closeInterfaces(); bool hasConnection() { return _db(); } - void loadExisting(); ///< Loads an already-written sqlite database from sessiondir, e.g. after extracting a .jasp archive. void upgradeDBFromVersion(Version originalVersion); ///< Ensures that the database has all the fields configured as required for the current JASP version, useful when loading older sqlite-containing jasp-files void runQuery( const std::string & query, std::function bindParameters, std::function processRow); ///< Runs a single query and then goes through the resultrows while calling processRow for each. diff --git a/QMLComponents/datasetprovider.cpp b/QMLComponents/datasetprovider.cpp index 4517c8be3c..448a635286 100644 --- a/QMLComponents/datasetprovider.cpp +++ b/QMLComponents/datasetprovider.cpp @@ -136,7 +136,7 @@ void DataSetProvider::loadDatabase(const Version & jaspVersion) try { _db->close(); - _db->loadExisting(); + _db->load(); _db->upgradeDBFromVersion(jaspVersion); std::unique_ptr loadedDataSet(new DataSet(0)); // Setting 0 for "do nothing" because otherwise we can't pass on jaspVersion