diff --git a/.clang-format b/.clang-format index 19ff63cb42294f1ce5aa49d78a74d82e6141927b..af6747eb4b062e0f6ec7d670cbb1aec40c4338bd 100644 --- a/.clang-format +++ b/.clang-format @@ -54,7 +54,7 @@ BreakConstructorInitializersBeforeComma: false BreakConstructorInitializers: BeforeColon BreakAfterJavaFieldAnnotations: false BreakStringLiterals: true -ColumnLimit: 80 +ColumnLimit: 100 CommentPragmas: '^ IWYU pragma:' CompactNamespaces: false ConstructorInitializerAllOnOneLineOrOnePerLine: false diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 27f2d5a0be75e4eb02ab6baac717f6295305a922..235f2f828b0a09c10e717b3ebdcfe9a6295247c4 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -24,7 +24,7 @@ RUN wget --output-document MOxUnit-master.zip \ RUN unzip MOxUnit-master.zip WORKDIR /MOxUnit-master/ RUN make install - +COPY .docker/caosdb_client.json /caosdb_client.json COPY . /caosdb-octavelib WORKDIR /caosdb-octavelib diff --git a/.docker/caosdb_client.json b/.docker/caosdb_client.json new file mode 100644 index 0000000000000000000000000000000000000000..50f3a797ec6bb8ef04a8a01ae2f008c92fdc3d1e --- /dev/null +++ b/.docker/caosdb_client.json @@ -0,0 +1,28 @@ +{ + "connections": { + "default": "local-caosdb", + "local-caosdb-admin": { + "host": "caosdb-server", + "port": 8443, + "server_certificate_path": "/cert/caosdb.cert.pem", + "authentication": { + "type": "plain", + "username": "admin", + "password": "caosdb" + } + }, + "local-caosdb": { + "host": "caosdb-server", + "port": 8443, + "server_certificate_path": "/cert/caosdb.cert.pem", + "authentication": { + "type": "plain", + "username": "admin", + "password": "caosdb" + } + } + }, + "extension": { + "this is my": "special option" + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 585a63db0bbb212bc533b222ade04db9359aadbb..b1a73c2d7c2ac60fe785ea3f853e218f789a7235 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security ### -## [0.0.1] - 2021-XX-XX ## +## [0.0.1] - 2021-08-12 ## ### Added ### -- Minimal working example with basic connection functionality. +- Added Entity mapping, error handling, Entity retrieval and query execution. diff --git a/CMakeLists.txt b/CMakeLists.txt index ac4b6f47b13f07cb0fe906c3501dddb9bbaee8f3..5780bb426e0504d6c00f49376b402db48d087039 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,7 @@ if(TEST) # append all the test cases here (file name without the ".cpp" suffix) set(test_cases test_utilities + test_caosdb_conversion ) # add special cmake functions for gtest, (although they are not used yet, see above) include(GoogleTest) diff --git a/DESCRIPTION b/DESCRIPTION index d34b56bb8f35b36c3632c5be96dfc5b1f76d1b55..508fa947b45c48d2675bf6772b5fe8aa24ccb2eb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ # See https://octave.org/doc/interpreter/The-DESCRIPTION-File.html Name: caosdb -Version: 0.1.0 +Version: 0.0.1 Date: 2021-07-09 Author: Daniel Hornung <d.hornung@indiscale.com> Maintainer: Daniel Hornung <d.hornung@indiscale.com> diff --git a/Makefile b/Makefile index 0ca33357d62d985e77167fd6ee39b6592faa2904..8f27df3a88e8abb42a55ce40751b94c9ac581b6a 100644 --- a/Makefile +++ b/Makefile @@ -41,16 +41,22 @@ style: style_octave style_cpp .PHONY: style style_octave: - mh_style --octave src doc test || echo "You may want to run `make style_fix`." + mh_style --octave src doc test || ( echo 'You may want to run `make style_fix`.'; exit 1 ) .PHONY: style_octave style_cpp: - clang-format-11 --dry-run --verbose --Werror $(shell find test/ src/ -type f -iname "*.cpp" -o -iname "*.hpp" -o -iname "*.h" -o -iname "*.h.in") || echo "You may want to run `make style_fix`." + clang-format-11 --dry-run --verbose --Werror \ + $(shell find test/ src/ -type f \ + -iname "*.cpp" -o -iname "*.hpp" -o -iname "*.h" -o -iname "*.h.in") \ + || ( echo 'You may want to run `make style_fix`.' ; exit 1 ) .PHONY: style_cpp style_fix: + echo "style_fix" mh_style --fix --octave src doc test - clang-format-11 -i --verbose --Werror $(shell find test/ src/ -type f -iname "*.cpp" -o -iname "*.hpp" -o -iname "*.h" -o -iname "*.h.in") + clang-format-11 -i --verbose --Werror \ + $(shell find test/ src/ -type f \ + -iname "*.cpp" -o -iname "*.hpp" -o -iname "*.h" -o -iname "*.h.in") .PHONY: style_fix ############################################################################### diff --git a/README_SETUP.md b/README_SETUP.md index 32c9d1a8b038085056248fca7d50d3d4874d58a9..1dd89cf800b321597353d741794169020941e157 100644 --- a/README_SETUP.md +++ b/README_SETUP.md @@ -5,7 +5,7 @@ This package requires the following software: - conan: `pip install conan` -- libcaosdb installed with Conan: See https://gitlab.indiscale.com/caosdb/src/caosdb-cpplib for +- Create a local conan package libcaosdb: See https://gitlab.indiscale.com/caosdb/src/caosdb-cpplib for further instructions. - For running this library, a valid libcaosdb configuration is needed, for example a `.caosdb_client.json` file in your home directory or current working directory. Please look at @@ -27,7 +27,13 @@ We use the standard test framework of Octave. See [this article](https://wiki.octave.org/Tests) for a primer and use [this](https://octave.org/doc/interpreter/Test-Functions.html) as a reference. -Additionally we use the [MOxUnit](https://github.com/MOxUnit/MOxUnit) framework. +Additionally we use the [MOxUnit](https://github.com/MOxUnit/MOxUnit) framework. Install MOxUnit somewhere: +```sh +git clone https://github.com/MOxUnit/MOxUnit.git +cd MOxUnit +make install +``` + ## Code Formatting diff --git a/conanfile.txt b/conanfile.txt index 37cf6d6b0f6a357663ea5c06f847e5c84b0207d0..d788d5da679da7ddf4172f814c0986f5b312f88e 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,5 +1,5 @@ [requires] -caosdb/[>=0.0.6] +caosdb/[>=0.0.9] [generators] cmake diff --git a/doc/conf.py b/doc/conf.py index 514d7de89df4d982d8f25d114f5d7787a33843be..b64527327430f1cbbf01d761c4f59a9698090bce 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -27,10 +27,10 @@ copyright = '2021, IndiScale GmbH' author = 'Daniel Hornung' # The short X.Y version -version = '0.1' +version = '0.0' # The full version, including alpha/beta/rc tags #release = '0.5.2-rc2' -release = '0.1.0' +release = '0.0.1' # -- General configuration --------------------------------------------------- diff --git a/src/Caosdb.m b/src/Caosdb.m index a94720905d6e188423457373bf895ffdbd5615ff..6b42098dbbed05569454bc26fdd94a8e5c4239ec 100644 --- a/src/Caosdb.m +++ b/src/Caosdb.m @@ -54,11 +54,11 @@ classdef Caosdb < handle obj.connection = connection; end - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % info % - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %% + %% info + %% function res = info(obj) - disp(["connection: >", obj.connection, "<"]); + % disp(["connection: >", obj.connection, "<"]); try info_result = maox_info(obj.connection); res = info_result; @@ -69,5 +69,91 @@ classdef Caosdb < handle end end + %% + % Retrieve entities by IDs + % + % entities = Caosdb.retrieve_by_id("my_id", {"more", "ids"}) + % + % Parameters + % ---------- + % + % ids: string or cell array of strings + % The ID(s) of the entity (entities) to be retrieved. + % + % Returns + % ------- + % entities : cell array + % The retrieved entities. + function entities = retrieve_by_id(obj, ids, varargin) + % Ensure that IDS is a string cell array + if ischar(ids) + ids = {ids}; + else + assert(iscellstr(ids), ... + "maox:InvalidArgument", ... + "IDS must be a string or string cell array, was:\n%s", ... + disp(ids)); + end + % Make one big cell array of potential strings + for argin = varargin + if ischar(argin) + ids(end + 1) = argin; + else + assert(iscellstr(argin), ... + "maox:InvalidArgument", ... + "arguments must be a string (cell array), but this argument was found:\n%s", ... + disp(argin)); + ids(end + 1:end + numel(argin)) = argin(:); + end + end + + % disp("ids:"); + % disp(ids); + try + collection = maox_retrieve(obj.connection, ids); + entities = maox_convert_collection(collection); + catch + % disp("error handling in Caosdb.m"); + % disp(lasterror()); + rethrow(lasterror()); + end + end + + %% + % Execute a query + % + % entities = Caosdb.query("FIND Record Foo WITH bar=baz") + % entities = Caosdb.query("COUNT Record Foo WITH bar=baz") + % + % Parameters + % ---------- + % + % query_str: string + % The query to be executed. + % + % Returns + % ------- + % entities : cell array + % The retrieved entities. If the query was a COUNT query, the result is an int64 instead. + function entities = query(obj, query_str) + % Ensure that QUERY is a string. + assert(ischar(query_str), "maox:InvalidArgument", "QUERY must be a string, was:\n%s", ... + disp(query_str)); + assert(nargin == 2, "maox:InvalidArgument", "This method accepts exactly 1 argument."); + % disp("query:"); + % disp(query_str); + try + [collection, count_results] = maox_query(obj.connection, query_str); + entities = maox_convert_collection(collection); + if (count_results >= 0) + entities = count_results; + end + catch + % disp("error handling in Caosdb.m"); + % disp(lasterror()); + rethrow(lasterror()); + end + end + end end diff --git a/src/Entity.m b/src/Entity.m new file mode 100644 index 0000000000000000000000000000000000000000..29a9dff9205156f05c55cd484c34a822341411c1 --- /dev/null +++ b/src/Entity.m @@ -0,0 +1,139 @@ +% This file is a part of the CaosDB Project. +% +% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> +% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU Affero General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU Affero General Public License for more details. +% +% You should have received a copy of the GNU Affero General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. + +classdef Entity < handle + + properties + role % One of "RecordType", "Record", "Property" + id + versionId + name + description + datatype + unit + value + end + properties (Access = private) + parents_ % Cell array of Parent objects.Struct array with the following fields: + % - id + % - name + % - description + properties_ % Cell array of Property objects.Struct array with the following fields: + % - id + % - name + % - description + % - importance + % - value + % - unit + % - datatype + errors_ % Cell array of Message objects. + warnings_ % Like messages. + infos_ % Like messages. + end + + methods + + % Constructor. + % The structure of the DATA parameter follows the convention outline in `maoxdb.hpp`. + function obj = Entity(data) + narginchk(1, 1); + obj.assign_scalars(data); + obj.assign_parents(data); + obj.assign_properties(data); + obj.errors_ = obj.create_messages(data, "errors"); + obj.warnings_ = obj.create_messages(data, "warnings"); + obj.infos_ = obj.create_messages(data, "infos"); + end + + % Getters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + function result = get_parents(obj) + result = obj.parents_; + end + + function result = get_properties(obj) + result = obj.properties_; + end + + function result = get_errors(obj) + result = obj.errors_; + end + + function result = get_warnings(obj) + result = obj.warnings_; + end + + function result = get_infos(obj) + result = obj.infos_; + end + + % Information %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + function result = has_errors(obj) + result = not(isempty(obj.errors_)); + end + + function result = has_warnings(obj) + result = not(isempty(obj.warnings_)); + end + + function result = has_infos(obj) + result = not(isempty(obj.infos_)); + end + + end + + methods (Access = private) + + % Only the simple metadata fields. + function assign_scalars(obj, data) + obj.role = data.role; + obj.id = data.id; + obj.versionId = data.versionId; + obj.name = data.name; + obj.description = data.description; + obj.datatype = data.datatype; + obj.unit = data.unit; + obj.value = data.value; + end + + % Assign the parents + function assign_parents(obj, data) + obj.parents_ = cell(); + for parent = data.parents + obj.parents_{end + 1} = Parent(parent); + end + end + + % Assign the properties + function assign_properties(obj, data) + obj.properties_ = cell(); + for property = getfield(data, "properties") + obj.properties_{end + 1} = Property(property); + end + end + + % Create the errors, warnings, infos + function result = create_messages(~, data, level) + result = cell(); + for message = getfield(data, level) + result{end + 1} = Message(message); + end + end + + end + +end diff --git a/src/FILES.in b/src/FILES.in deleted file mode 100644 index 92701f0929431293984c7c86f21cf8607f0829c5..0000000000000000000000000000000000000000 --- a/src/FILES.in +++ /dev/null @@ -1,2 +0,0 @@ -${BUILD_DIR}/inst/caosdb.mex -caosdb.m diff --git a/src/Message.m b/src/Message.m new file mode 100644 index 0000000000000000000000000000000000000000..e50782f9b95bc4aa8192c5bc935f1e9e84c4dd2b --- /dev/null +++ b/src/Message.m @@ -0,0 +1,37 @@ +% This file is a part of the CaosDB Project. +% +% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> +% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU Affero General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU Affero General Public License for more details. +% +% You should have received a copy of the GNU Affero General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. + +classdef Message < handle + + properties + code + description + end + + methods + + % Constructor. + % The structure of the DATA parameter follows the convention outline in `maoxdb.hpp`. + function obj = Message(data) + narginchk(1, 1); + obj.code = data.code; + obj.description = data.description; + end + + end +end diff --git a/src/Parent.m b/src/Parent.m new file mode 100644 index 0000000000000000000000000000000000000000..db221b5bc628ec7218ff1132eddeac01694358c5 --- /dev/null +++ b/src/Parent.m @@ -0,0 +1,39 @@ +% This file is a part of the CaosDB Project. +% +% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> +% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU Affero General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU Affero General Public License for more details. +% +% You should have received a copy of the GNU Affero General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. + +classdef Parent < handle + + properties + id + name + description + end + + methods + + % Constructor. + % The structure of the DATA parameter follows the convention outline in `maoxdb.hpp`. + function obj = Parent(data) + narginchk(1, 1); + obj.id = data.id; + obj.name = data.name; + obj.description = data.description; + end + + end +end diff --git a/src/Property.m b/src/Property.m new file mode 100644 index 0000000000000000000000000000000000000000..7c80ff88eb5cb2b70e49eacc5fa8ad1ddac2ec74 --- /dev/null +++ b/src/Property.m @@ -0,0 +1,47 @@ +% This file is a part of the CaosDB Project. +% +% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> +% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU Affero General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU Affero General Public License for more details. +% +% You should have received a copy of the GNU Affero General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. + +classdef Property < handle + + properties + id + name + description + importance + datatype + value + unit + end + + methods + + % Constructor. + % The structure of the DATA parameter follows the convention outline in `maoxdb.hpp`. + function obj = Property(data) + narginchk(1, 1); + obj.id = data.id; + obj.name = data.name; + obj.description = data.description; + obj.importance = data.importance; + obj.datatype = data.datatype; + obj.value = data.value; + obj.unit = data.unit; + end + + end +end diff --git a/src/configure b/src/configure index 5266b64232c56de3c21ca84025c79bf4cbab5646..a12ee14d1ddb336a930862a1b6a0885c03f83256 100755 --- a/src/configure +++ b/src/configure @@ -1,5 +1,7 @@ #!/bin/bash +set -e + # See https://octave.org/doc/interpreter/Creating-Packages.html echo "Octave CaosDB SRC_DIR: $PWD" @@ -10,6 +12,8 @@ echo "Octave CaosDB INST_DIR: $INST_DIR" rm -r "${INST_DIR}" || true mkdir -p "${INST_DIR}" mkdir -p "${INST_DIR}/private" +cp *.m "${INST_DIR}" +cp private/*.m "${INST_DIR}/private" BUILD_DIR="$(realpath ../build)" echo "Octave CaosDB BUILD_DIR: $BUILD_DIR" diff --git a/src/lib/maoxdb.cpp b/src/lib/maoxdb.cpp index 7c8ffce7d00402178dacd7e09ffcbce56b52d25a..176dd55a424d8bf6e49fa8f77d99b8ca5256a30b 100644 --- a/src/lib/maoxdb.cpp +++ b/src/lib/maoxdb.cpp @@ -28,19 +28,51 @@ #include "maoxdb.hpp" #include "mex.h" #include "caosdb/status_code.h" +#include "caosdb/transaction.h" +#include "caosdb/transaction_status.h" namespace maoxdb { using std::string; +auto mxFromCaosDBParents(const caosdb::entity::Parents &parents) -> mxArray *; +auto mxFromCaosDBProperties(const caosdb::entity::Properties &properties) -> mxArray *; +auto mxFromCaosDBMessages(const caosdb::entity::Messages &messages) -> mxArray *; + +// Exception and error handling /////////////////////////////////////////////// + +/** + * Generate a mex error message from a TransactionStatus. + */ +auto transactionStatusToMessage(const caosdb::transaction::Transaction *const trans) + -> std::pair<string, string> { + auto status = trans->GetStatus(); + auto code = status.GetCode(); + // Don't raise errors if it's only transaction errors (put into entity messages). + if (!status.IsError() || code == caosdb::StatusCode::GENERIC_TRANSACTION_ERROR) { + return std::pair<string, string>(); + } + string id = std::to_string(code); + string text = status.GetDescription(); + return std::pair<string, string>(id, text); +} + +/** + * Throw an Octave error if the transaction did not finish properly. + */ +void throwOctExceptionIfError(const caosdb::transaction::Transaction *const trans) { + auto msg = maoxdb::transactionStatusToMessage(trans); + if (msg.first != "") { + mexErrMsgIdAndTxt(msg.first.c_str(), msg.second.c_str()); + } +} + /** * Extract useful strings from an Exception: ID and description. */ -std::pair<string, string> -exceptionToMessage(const caosdb::exceptions::Exception &exc) { +auto exceptionToMessage(const caosdb::exceptions::Exception &exc) -> std::pair<string, string> { string id = std::to_string(exc.GetCode()); - string text = - caosdb::get_status_description(exc.GetCode()) + "\n" + exc.what(); + string text = caosdb::get_status_description(exc.GetCode()) + "\n" + exc.what(); return std::pair<string, string>(id, text); } @@ -53,4 +85,181 @@ void throwOctException(const caosdb::exceptions::Exception &exc) { mexErrMsgIdAndTxt(excContent.first.c_str(), excContent.second.c_str()); } +// Entity handling //////////////////////////////////////////////////////////// + +/** + * @brief Convert a ResultSet to a struct mexArray. + */ +auto mxFromResultSet(const caosdb::transaction::ResultSet &resultSet) -> mxArray * { + if (resultSet.Size() == 0) { + auto *result = mxEmptySTRUCT(); + return result; + } + std::vector<const mxArray *> entities; + // Obtain entities + for (auto entity : resultSet) { + entities.push_back(mxFromCaosDBEntity(entity)); + } + + auto *result = mxMergeScalarStructs(entities); + + return result; +} + +/** + * @brief Convert an Entity from libcaosdb to Octave struct mexArray. + */ +auto mxFromCaosDBEntity(const caosdb::entity::Entity &entity) -> mxArray * { + // clang-format off + auto fields = std::vector<const char*> + {"role", + "id", + "versionId", + "name", + "description", + "datatype", + "unit", + "value", + "parents", + "properties", + "errors", + "warnings", + "infos" + }; + // clang-format on + std::array<mwSize, 2> dims = {1, (mwSize)fields.size()}; + auto *result = mxCreateStructArray(2, dims.data(), fields.size(), fields.data()); + // Fill with scalar values + mxSetField(result, 0, "role", mxCreateString(entity.GetRole().c_str())); + mxSetField(result, 0, "id", mxCreateString(entity.GetId().c_str())); + mxSetField(result, 0, "versionId", mxCreateString(entity.GetVersionId().c_str())); + mxSetField(result, 0, "name", mxCreateString(entity.GetName().c_str())); + mxSetField(result, 0, "description", mxCreateString(entity.GetDescription().c_str())); + mxSetField(result, 0, "datatype", mxCreateString(entity.GetDatatype().c_str())); + mxSetField(result, 0, "unit", mxCreateString(entity.GetUnit().c_str())); + // Parse value to proper type. + mxSetField(result, 0, "value", mxScalarFromStringValue(entity)); + + // Parents and Properties + mxSetField(result, 0, "parents", mxFromCaosDBParents(entity.GetParents())); + mxSetField(result, 0, "properties", mxFromCaosDBProperties(entity.GetProperties())); + + // message type content + mxSetField(result, 0, "errors", mxFromCaosDBMessages(entity.GetErrors())); + mxSetField(result, 0, "warnings", mxFromCaosDBMessages(entity.GetWarnings())); + mxSetField(result, 0, "infos", mxFromCaosDBMessages(entity.GetInfos())); + + return result; +} + +// Parents to struct array +auto mxFromCaosDBParents(const caosdb::entity::Parents &parents) -> mxArray * { + // clang-format off + std::vector<const char*> fields = + {"id", + "name", + "description" + }; + // clang-format on + std::array<mwSize, 2> fieldDims = {1, (mwSize)fields.size()}; + std::array<mwSize, 2> dims = {1, (mwSize)parents.Size()}; + auto *result = mxCreateStructArray(2, dims.data(), fields.size(), fields.data()); + for (size_t i = 0; i < parents.Size(); ++i) { + auto parent = parents.At(i); + mxSetField(result, i, "id", mxCreateString(parent.GetId().c_str())); + mxSetField(result, i, "name", mxCreateString(parent.GetName().c_str())); + // FIXME Add again once upstream is ready. + // mxSetField(result, i, "description", + // mxCreateString(parent.GetDescription().c_str())); + } + return result; +} + +// Properties to struct array +auto mxFromCaosDBProperties(const caosdb::entity::Properties &properties) -> mxArray * { + // clang-format off + std::vector<const char*> fields = + {"id", + "name", + "description", + "importance", + "value", + "unit", + "datatype" + }; + // clang-format on + std::array<mwSize, 2> fieldDims = {1, (mwSize)fields.size()}; + std::array<mwSize, 2> dims = {1, (mwSize)properties.Size()}; + auto *result = mxCreateStructArray(2, dims.data(), fields.size(), fields.data()); + for (mwIndex i = 0; i < properties.Size(); ++i) { + auto property = properties.At(i); + mxSetField(result, i, "id", mxCreateString(property.GetId().c_str())); + mxSetField(result, i, "name", mxCreateString(property.GetName().c_str())); + mxSetField(result, i, "description", mxCreateString(property.GetDescription().c_str())); + mxSetField(result, i, "importance", mxCreateString(property.GetImportance().c_str())); + mxSetField(result, i, "unit", mxCreateString(property.GetUnit().c_str())); + mxSetField(result, i, "datatype", mxCreateString(property.GetDatatype().c_str())); + // Parse value to proper type. + mxSetField(result, i, "value", mxScalarFromStringValue(property)); + } + return result; +} + +// Messages to struct array +auto mxFromCaosDBMessages(const caosdb::entity::Messages &messages) -> mxArray * { + std::vector<const char *> fields = { + "code", // + "description" // + }; + std::array<mwSize, 2> fieldDims = {1, (mwSize)fields.size()}; + std::array<mwSize, 2> dims = {1, (mwSize)messages.Size()}; + auto *result = mxCreateStructArray(2, dims.data(), fields.size(), fields.data()); + for (mwIndex i = 0; i < messages.Size(); ++i) { + auto message = messages.At(i); + mxSetField(result, i, "code", mxScalarINT64(message.GetCode())); + mxSetField(result, i, "description", mxCreateString(message.GetDescription().c_str())); + } + return result; +} + +// Utility functions ////////////////////////////////////////////////////////// + +/** + * @brief Merges a number of scalar mex structs into a 1xN struct. + */ +auto mxMergeScalarStructs(const std::vector<const mxArray *> &structs) -> mxArray * { + + // We need the field names first to create a new struct + auto nFields = (size_t)mxGetNumberOfFields(structs[0]); + auto fields = std::vector<const char *>(nFields); + for (mwIndex i = 0; i < nFields; ++i) { + fields[i] = mxGetFieldNameByNumber(structs[0], i); + } + + auto dims = std::array<mwSize, 2>{1, (mwSize)structs.size()}; + auto result = mxCreateStructArray(2, dims.data(), fields.size(), fields.data()); + + auto i = mwIndex(0) - 1; + for (auto scalarStruct : structs) { + ++i; + for (auto field : fields) { + mxSetField(result, i, field, mxDuplicateArray(mxGetField(scalarStruct, 0, field))); + } + } + return result; +} + +/** + * @brief Extract a std::string from a char array + */ +auto mxGetStdString(const mxArray *array) -> std::string { + auto len = mxGetNumberOfElements(array) + 1; // NUL byte + auto buf = std::vector<char>(len); + auto ret = mxGetString(array, buf.data(), len); + if (ret) { + throw std::invalid_argument("Could not extract a string, probably wrong mxArray content."); + } + return std::string(buf.data()); +} + } // namespace maoxdb diff --git a/src/lib/maoxdb.hpp b/src/lib/maoxdb.hpp index e2f0154bb8a5b29c6353f7fe932a9a3bc60edcf9..d67eb0160085dc20be92cf20a0b82738c2aae511 100644 --- a/src/lib/maoxdb.hpp +++ b/src/lib/maoxdb.hpp @@ -1,13 +1,55 @@ -#ifndef MAOXDB_H // NOLINT +#ifndef MAOXDB_H // NOLINT #define MAOXDB_H #include "caosdb/exceptions.h" #include "caosdb/status_code.h" +#include "caosdb/transaction.h" +#include "caosdb/transaction_status.h" #include "mex.h" +#include <boost/algorithm/string.hpp> +#include <stdexcept> #include <string> -// Macros ///////////////////////////////////////////////////////////////////// +/** + * Mostly utility functions for MEX files interacting with libcaosdb. + * + * \b{Data exchange} + * + * Exchanging data between the Octave side wrapper and this C++ library is done + * via Octave struct arrays, each struct corresponds to one CaosDB Entity. + * + * The Entity structs have the following structure (not all keys may exist for + * all roles): + * + * - role: One of "RecordType", "Record", "Property" + * - id + * - versionId + * - name + * - description + * - datatype + * - unit + * - value: A string representation of the value. + * - parents: Struct array with the following fields: + * - id + * - name + * - description + * - properties: Struct array with the following fields: + * - id + * - name + * - description + * - importance + * - value + * - unit + * - datatype + * - errors: Struct array with the following fields: + * - code (as INT64) + * - description + * - warnings: Like messages. + * - infos: Like messages. + * + */ +// Macros ///////////////////////////////////////////////////////////////////// // Utility functions ////////////////////////////////////////////////////////// namespace maoxdb { @@ -35,6 +77,130 @@ inline auto mxScalarDOUBLE(double value) -> mxArray * { inline auto mxScalarLOGICAL(bool value) -> mxArray * { return mxScalar<UINT8_T>(static_cast<unsigned char>(value), mxLOGICAL_CLASS); } +inline auto mxEmptyUINT64() -> mxArray * { + mxArray *array = mxCreateNumericMatrix(0, 0, mxUINT64_CLASS, mxREAL); + return array; +} +inline auto mxEmptyINT64() -> mxArray * { + mxArray *array = mxCreateNumericMatrix(0, 0, mxINT64_CLASS, mxREAL); + return array; +} +inline auto mxEmptyDOUBLE() -> mxArray * { + mxArray *array = mxCreateNumericMatrix(0, 0, mxDOUBLE_CLASS, mxREAL); + return array; +} +inline auto mxEmptyLOGICAL() -> mxArray * { + mxArray *array = mxCreateLogicalMatrix(0, 0); + return array; +} +inline auto mxEmptySTRING() -> mxArray * { + mxArray *array = mxCreateString(""); + return array; +} +inline auto mxEmptySTRUCT() -> mxArray * { + mxArray *array = mxCreateStructArray(0, (mwSize const *)nullptr, 0, (char const **)nullptr); + return array; +} + +/** + * @brief Convert the string-typed value in an Entity-like object to a mxArray. + * + * @details For the existing datatypes, see + * https://docs.indiscale.com/caosdb-server/specification/Datatype.html + */ +template <class T> auto mxScalarFromStringValue(const T &entity) -> mxArray * { + mxArray *result; + auto dt = entity.GetDatatype(); + auto value = entity.GetValue(); + // std::cout << "Datatype/Value: " << dt << ": " << value << std::endl; + if (dt.empty()) { + result = mxEmptySTRING(); + } else if (dt == "TEXT") { + result = mxCreateString(value.c_str()); + } else if (dt == "BOOLEAN") { + if (value.empty()) { + result = mxEmptyLOGICAL(); + goto handled; + } + if (boost::to_upper_copy(value) == "TRUE") { + result = mxScalarLOGICAL(1); + } else { + if (boost::to_upper_copy(value) != "FALSE") { + mxAssert(boost::to_upper_copy(value) == "FALSE", + std::string("Value >") + value + "< is neither FALSE nor TRUE."); + throw std::invalid_argument(std::string("Value >") + value + + "< is neither FALSE nor TRUE."); + } + result = mxScalarLOGICAL(0); + } + } else if (dt == "INTEGER") { + if (value.empty()) { + result = mxEmptyINT64(); + goto handled; + } + result = mxScalarINT64(stol(value)); + } else if (dt == "DOUBLE") { + if (value.empty()) { + result = mxEmptyDOUBLE(); + goto handled; + } + result = mxScalarDOUBLE(stod(value)); + } else if (dt == "FILE") { + result = mxCreateString(value.c_str()); + } else if (dt == "REFERENCE") { + result = mxCreateString(value.c_str()); + } else if (dt == "DATETIME") { + result = mxCreateString(value.c_str()); + } else if (dt.substr(0, 4) == "LIST") { + mexErrMsgTxt("List values are not implemented yet."); + } else { + std::cout << "Unknown datatype, probably a reference: " << dt << ": " << value << std::endl; + result = mxCreateString(value.c_str()); + } + handled: + mxAssert(result, "Scalar value must have some result now."); + return result; +} + +/** + * @brief Extract a std::string from a char array + * + * @details Handles the length check + * + * @param array The char array from which the string shall be extracted. + * + * @return The std::string. + */ +auto mxGetStdString(const mxArray *array) -> std::string; + +// Exception and error handling /////////////////////////////////////////////// + +/** + * @brief Generate a mex error message from a TransactionStatus. + * + * @details Preprocess the transaction status of a transaction, to create the + * necessary information for raising an Octave error. + * + * @param trans + * The transaction of which to handle the status. + * + * @return std::pair<std::string, std::string> + * The message ID and description of any error. If the transaction status is + * SUCCESS, or some kind of "still running", both strings are empty. + */ +auto transactionStatusToMessage(const caosdb::transaction::Transaction *const trans) + -> std::pair<string, string>; + +/** + * @brief Throw an Octave error if the transaction did not finish properly. + * + * @details On error, this function will leave this C++ context and fall back to + * Octave. + * + * @param trans + * The transaction to check. + */ +void throwOctExceptionIfError(const caosdb::transaction::Transaction *const trans); /** * @brief Handle a CaosDB Exception and transform into error code and message. @@ -51,8 +217,7 @@ inline auto mxScalarLOGICAL(bool value) -> mxArray * { * @note Additional utility functions should be easy to implement when the * libcaosdb error handling changes. */ -auto -exceptionToMessage(const caosdb::exceptions::Exception &exc) -> std::pair<string, string>; +auto exceptionToMessage(const caosdb::exceptions::Exception &exc) -> std::pair<string, string>; /** * @brief Throw a CaosDB Exception inside Octave. @@ -67,6 +232,37 @@ exceptionToMessage(const caosdb::exceptions::Exception &exc) -> std::pair<string */ void throwOctException(const caosdb::exceptions::Exception &exc); +// Entity handling //////////////////////////////////////////////////////////// + +/** + * @brief Convert a ResultSet to a struct mexArray. + * + * @return A 1xN struct array, with Entity structs as values. + */ +auto mxFromResultSet(const caosdb::transaction::ResultSet &resultSet) -> mxArray *; + +/** + * @brief Convert an Entity from libcaosdb to Octave struct mexArray. + * + * @return A 1x1 struct array whose entry follows the maoxdb Entity convention + * outlined above. + */ +auto mxFromCaosDBEntity(const caosdb::entity::Entity &entity) -> mxArray *; + +// Utility functions ////////////////////////////////////////////////////////// + +/** + * @brief Merges a number of scalar mex structs into a 1xN struct. + * + * @details The content (the mxArrays behind the input structs values) is + * duplicated. + * + * @param structs: Must all have the same fields. + * + * @return A 1xN struct array. + */ +auto mxMergeScalarStructs(const std::vector<const mxArray *> &structs) -> mxArray *; + } // namespace maoxdb #endif /* MAOXDB_H */ diff --git a/src/private/maox_caosdb.cpp b/src/private/maox_caosdb.cpp index 88d408cc1d3b4576e49b0930dc1cdc253f8f3f53..7d1d5eb5e0a2bc44b248c2c53bca0105db2f2b40 100644 --- a/src/private/maox_caosdb.cpp +++ b/src/private/maox_caosdb.cpp @@ -17,14 +17,13 @@ void print_usage() { mexPrintf(" --help Print this help and return.\n"); mexPrintf(" --version Print the version of OctaveCaosDB and " "libcaosdb and return.\n"); - mexPrintf( - " --test-connection Test the default connection and return.\n"); + mexPrintf(" --test-connection Test the default connection and return.\n"); }; -const std::string FULL_VERSION = std::string( - "v0.1 (libcaosdb v" + std::to_string(caosdb::LIBCAOSDB_VERSION_MAJOR) + "." + - std::to_string(caosdb::LIBCAOSDB_VERSION_MINOR) + "." + - std::to_string(caosdb::LIBCAOSDB_VERSION_PATCH) + ")"); +const std::string FULL_VERSION = + std::string("v0.1 (libcaosdb v" + std::to_string(caosdb::LIBCAOSDB_VERSION_MAJOR) + "." + + std::to_string(caosdb::LIBCAOSDB_VERSION_MINOR) + "." + + std::to_string(caosdb::LIBCAOSDB_VERSION_PATCH) + ")"); auto print_version() -> const char * { mexPrintf("Octave caosdb client %s\n\n", FULL_VERSION.c_str()); @@ -33,17 +32,14 @@ auto print_version() -> const char * { }; void test_connection() { - const auto &connection = - caosdb::connection::ConnectionManager::GetDefaultConnection(); + const auto &connection = caosdb::connection::ConnectionManager::GetDefaultConnection(); const auto &version_info = connection->GetVersionInfo(); - mexPrintf("Server Version: v%d.%d.%d-%s-%s\n", version_info->GetMajor(), - version_info->GetMinor(), version_info->GetPatch(), - version_info->GetPreRelease().c_str(), + mexPrintf("Server Version: v%d.%d.%d-%s-%s\n", version_info->GetMajor(), version_info->GetMinor(), + version_info->GetPatch(), version_info->GetPreRelease().c_str(), version_info->GetBuild().c_str()); } -void mexFunction(int /*nlhs*/, mxArray *plhs[], int nrhs, - const mxArray *prhs[]) { +void mexFunction(int /*nlhs*/, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { if (nrhs == 1) { auto const *first_arg = mxArrayToString(prhs[0]); diff --git a/src/private/maox_convert_collection.m b/src/private/maox_convert_collection.m new file mode 100644 index 0000000000000000000000000000000000000000..eec0229d395af8e956b8bcee5bd18f2a743ea023 --- /dev/null +++ b/src/private/maox_convert_collection.m @@ -0,0 +1,25 @@ +% This file is a part of the CaosDB Project. +% +% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> +% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU Affero General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU Affero General Public License for more details. +% +% You should have received a copy of the GNU Affero General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. + +%% Convert a struct array as received from maoxdb to a cell array of Entity objects. +function entities = maox_convert_collection(collection) + entities = cell(); + for data = collection + entities{end + 1} = Entity(data); + end +end diff --git a/src/private/maox_info.cpp b/src/private/maox_info.cpp index 6712d9e310235570d2b3c21fe8115978a4b039e3..f1f1774493cd20ccbf3b80d67af2fe69fffbcb33 100644 --- a/src/private/maox_info.cpp +++ b/src/private/maox_info.cpp @@ -30,16 +30,11 @@ auto info(string const &connection_name) -> mxArray * { std::array<mwSize, 2> dims = {1, 1}; mxArray *info_struct = mxCreateStructArray(2, dims.data(), 5, keys); // NOLINT - mxSetField(info_struct, 0, "major", - maoxdb::mxScalarUINT64(version_info.GetMajor())); - mxSetField(info_struct, 0, "minor", - maoxdb::mxScalarUINT64(version_info.GetMinor())); - mxSetField(info_struct, 0, "patch", - maoxdb::mxScalarUINT64(version_info.GetPatch())); - mxSetField(info_struct, 0, "pre_release", - mxCreateString(version_info.GetPreRelease().c_str())); - mxSetField(info_struct, 0, "build", - mxCreateString(version_info.GetBuild().c_str())); + mxSetField(info_struct, 0, "major", maoxdb::mxScalarUINT64(version_info.GetMajor())); + mxSetField(info_struct, 0, "minor", maoxdb::mxScalarUINT64(version_info.GetMinor())); + mxSetField(info_struct, 0, "patch", maoxdb::mxScalarUINT64(version_info.GetPatch())); + mxSetField(info_struct, 0, "pre_release", mxCreateString(version_info.GetPreRelease().c_str())); + mxSetField(info_struct, 0, "build", mxCreateString(version_info.GetBuild().c_str())); return info_struct; } @@ -53,8 +48,7 @@ auto info(string const &connection_name) -> mxArray * { * * @return return type */ -void mexFunction(int /*nlhs*/, mxArray *plhs[], int nrhs, - const mxArray *prhs[]) { +void mexFunction(int /*nlhs*/, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { string conn_name; if (nrhs >= 1 && mxGetNumberOfElements(prhs[0]) > 0) { diff --git a/src/private/maox_query.cpp b/src/private/maox_query.cpp new file mode 100644 index 0000000000000000000000000000000000000000..958d201a4d4912fc1709b6a04808972b69a33d84 --- /dev/null +++ b/src/private/maox_query.cpp @@ -0,0 +1,85 @@ +#include "caosdb/connection.h" // for Connection, ConnectionManager +#include "caosdb/constants.h" // for LIBCAOSDB_VERSION_MAJOR, LIBCAOSDB_VE... +#include "caosdb/exceptions.h" // for all error handling +#include "caosdb/info.h" // for VersionInfo +#include "maoxdb.hpp" // caosDB utils for mex files +#include "mex.h" // for mxArray, mexFunction +#include <cstring> // for strcmp +#include <memory> // for unique_ptr, __shared_ptr_access, shar... +#include <string> // for allocator, char_traits, operator+ + +using caosdb::connection::Connection; +using caosdb::connection::ConnectionManager; +using caosdb::transaction::Transaction; +using caosdb::transaction::TransactionStatus; +using std::string; + +/** + * @brief Execute a query. + * + * @details This function returns the entities as a cell array. + * + * @param connection_name A string with the connection name. May be omitted or an empty string. + * + * @param query The query string. + * + * @return resultSet A struct with the entities. + * + * @return countResult The result of a COUNT query, is -1 if zero or more than one COUNT queries + * were given. + */ +void mexFunction(int nlhs, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { + + string conn_name; + string query; + std::vector<string> ids; + // Argument handling + try { + if (nrhs < 2) { + mexErrMsgIdAndTxt("maox:InsufficientArguments", + "Need 2 arguments: connection_name and a query string."); + } + if (nrhs > 2) { + mexErrMsgIdAndTxt("maox:TooManyArguments", + "Need 2 arguments: connection_name and a query string."); + } + if (nlhs < 2) { + mexErrMsgIdAndTxt("maox:InsufficientReturnArguments", + "Need 2 return arguments: resultSet and queryCount."); + } + if (nlhs > 2) { + mexErrMsgIdAndTxt("maox:TooManyReturnArguments", + "Need 2 return arguments: resultSet and queryCount."); + } + if (!mxIsChar(prhs[1])) { + mexErrMsgIdAndTxt("maox:InvalidArgument", "The second argument must be a string."); + } + if (mxGetNumberOfElements(prhs[0]) > 0) { + conn_name = mxArrayToString(prhs[0]); + } + query = mxArrayToString(prhs[1]); + } catch (...) { + mexErrMsgIdAndTxt("maox:ArgumentHandling", "Error while handling the arguments."); + } + + // Execute the query + std::shared_ptr<Connection> connection = nullptr; + if (conn_name.empty()) { + connection = ConnectionManager::GetDefaultConnection(); + } else { + connection = ConnectionManager::GetConnection(conn_name); + } + auto transaction = connection->CreateTransaction(); + transaction->Query(query); + transaction->ExecuteAsynchronously(); + auto t_stat = transaction->WaitForIt(); + maoxdb::throwOctExceptionIfError(transaction.get()); + // Status must be OK or GENERIC_TRANSACTION_ERROR now. + + const auto &results = transaction->GetResultSet(); + auto *mxResults = maoxdb::mxFromResultSet(results); + auto *mxCountResults = maoxdb::mxScalarINT64(transaction->GetCountResult()); + + plhs[0] = mxDuplicateArray(mxResults); + plhs[1] = mxCountResults; +} diff --git a/src/private/maox_retrieve.cpp b/src/private/maox_retrieve.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a15940249bfd5f400c796a7bb871fbe8766371b7 --- /dev/null +++ b/src/private/maox_retrieve.cpp @@ -0,0 +1,85 @@ +#include "caosdb/connection.h" // for Connection, ConnectionManager +#include "caosdb/constants.h" // for LIBCAOSDB_VERSION_MAJOR, LIBCAOSDB_VE... +#include "caosdb/exceptions.h" // for all error handling +#include "maoxdb.hpp" // caosDB utils for mex files +#include "mex.h" // for mxArray, mexFunction +#include <cstring> // for strcmp +#include <memory> // for unique_ptr, __shared_ptr_access, shar... +#include <string> // for allocator, char_traits, operator+ + +using caosdb::connection::Connection; +using caosdb::connection::ConnectionManager; +using caosdb::transaction::Transaction; +using caosdb::transaction::TransactionStatus; +using std::string; + +/** + * @brief Retrieve one or more entities. + * + * @details This function returns the entities as a cell array. + * + * @param connection_name A string with the connection name. May be omitted or + * an empty string. + * + * @param IDs One or more Entity IDs, encapsulated in a string cell array. + * + * @return collection A struct with the entities. + */ +void mexFunction(int /*nlhs*/, mxArray *plhs[], int nrhs, const mxArray *prhs[]) { + + string conn_name; + std::vector<string> ids; + try { + if (nrhs < 2) { + mexErrMsgIdAndTxt("maox:InsufficientArguments", + "Need 2 arguments: connection_name and a string cell array with IDs."); + } + if (nrhs > 2) { + mexErrMsgIdAndTxt("maox:TooManyArguments", + "Need 2 arguments: connection_name and a string cell array with IDs."); + } + if (!mxIsCell(prhs[1])) { + mexErrMsgIdAndTxt("maox:InvalidArgument", "The second argument must be a string cell array."); + } + if (mxGetNumberOfElements(prhs[0]) > 0) { + conn_name = mxArrayToString(prhs[0]); + } + + // Collect the IDs + for (auto i = 0; i < mxGetNumberOfElements(prhs[1]); ++i) { + try { + auto id = maoxdb::mxGetStdString(mxGetCell(prhs[1], i)); + ids.emplace_back(id); + } catch (const std::invalid_argument &exc) { + mexErrMsgIdAndTxt("maox:InvalidArgument", + "The second argument must be a string cell array."); + } + } + } catch (...) { + mexErrMsgIdAndTxt("maox:ArgumentHandling", "Error while handling the arguments."); + } + auto n_ids = ids.size(); + + // Retrieve the Entities + std::shared_ptr<Connection> connection = nullptr; + if (conn_name.empty()) { + connection = ConnectionManager::GetDefaultConnection(); + } else { + connection = ConnectionManager::GetConnection(conn_name); + } + auto transaction = connection->CreateTransaction(); + for (const auto &id : ids) { + // std::cout << "RetrieveById(" << id << ")" << std::endl; + transaction->RetrieveById(id); + } + transaction->ExecuteAsynchronously(); + auto t_stat = transaction->WaitForIt(); + maoxdb::throwOctExceptionIfError(transaction.get()); + // Status must be OK or GENERIC_TRANSACTION_ERROR now. + const auto &results = transaction->GetResultSet(); + // std::cout << "size: " << results.Size() << std::endl; + + auto *mxResults = maoxdb::mxFromResultSet(results); + + plhs[0] = mxDuplicateArray(mxResults); +} diff --git a/test/Run_Test.m b/test/Run_Test.m index e72e71a9cbc169647ccd1de253d9c7f178d89be9..37f818b9ef8fad605a2da08f48cb733d750a328b 100644 --- a/test/Run_Test.m +++ b/test/Run_Test.m @@ -17,7 +17,11 @@ % along with this program. If not, see <https://www.gnu.org/licenses/>. pkg load caosdb; -test_result = moxunit_runtests("-verbose", "test_unittest.m"); -if not(test_result) - exit(1) + +all_tests = true; +all_tests &= moxunit_runtests("-verbose", "test_unittest.m"); +all_tests &= moxunit_runtests("-verbose", "test_caosdb.m"); + +if not(all_tests) + exit(1); end diff --git a/test/test_caosdb.m b/test/test_caosdb.m new file mode 100644 index 0000000000000000000000000000000000000000..cd61886ab21cab1cd95307c6455c142970d004c0 --- /dev/null +++ b/test/test_caosdb.m @@ -0,0 +1,52 @@ +% This file is a part of the CaosDB Project. +% +% Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> +% Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> +% +% This program is free software: you can redistribute it and/or modify +% it under the terms of the GNU Affero General Public License as +% published by the Free Software Foundation, either version 3 of the +% License, or (at your option) any later version. +% +% This program is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU Affero General Public License for more details. +% +% You should have received a copy of the GNU Affero General Public License +% along with this program. If not, see <https://www.gnu.org/licenses/>. + +%% The main function which intitializes the tests. +function test_suite = test_caosdb() + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +%% Test retrieval with errors +function test_retrieve_failure() + % Default connection configuration is sufficient. + c = Caosdb(); + + % Call retrieve with wrong arguments. + % (Only m file level, no invalid arguments should make it to the mex function.) + assertExceptionThrown(@()c.retrieve_by_id(120), "maox:InvalidArgument"); + assertExceptionThrown(@()c.retrieve_by_id({120}), "maox:InvalidArgument"); + assertExceptionThrown(@()c.retrieve_by_id({"120"}, {}), "maox:InvalidArgument"); + assertExceptionThrown(@()c.retrieve_by_id({"120"}, {120}), "maox:InvalidArgument"); + assertExceptionThrown(@()c.retrieve_by_id({"120"}, {"120", 120}), "maox:InvalidArgument"); +end + +%% Test query with errors +function test_query_failure() + % Default connection configuration is sufficient. + c = Caosdb(); + + % Call retrieve with wrong arguments. + % (Only m file level, no invalid arguments should make it to the mex function.) + assertExceptionThrown(@()c.query(120), "maox:InvalidArgument"); + assertExceptionThrown(@()c.query({120}), "maox:InvalidArgument"); + assertExceptionThrown(@()c.query("FIND Record", "FIND Record"), "maox:InvalidArgument"); +end diff --git a/test/test_caosdb_conversion.cpp b/test/test_caosdb_conversion.cpp new file mode 100644 index 0000000000000000000000000000000000000000..fa773ea4b517c08f2fec69aec748d55a7f2fa631 --- /dev/null +++ b/test/test_caosdb_conversion.cpp @@ -0,0 +1,210 @@ +/* + * This file is a part of the CaosDB Project. + * + * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com> + * Copyright (C) 2021 Daniel Hornung <d.hornung@indiscale.com> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include "caosdb/exceptions.h" +#include "maoxdb.hpp" +#include "mex.h" +// #include "mexproto.h" +#include <boost/lexical_cast.hpp> +#include <gtest/gtest.h> +#include <charconv> +#include <limits> +#include <memory> +#include <string> +#include <type_traits> +#include <vector> + +using std::string; + +namespace maoxdb { + +/////////////////////////////////////////////////////////////////////////////// +// Helper functions // +/////////////////////////////////////////////////////////////////////////////// + +// Helper implementation ... +template <typename T> void test_value_impl(const mxArray *mxValue, const T &value) { + EXPECT_EQ(mxGetNumberOfElements(mxValue), 1); + if (value != value) { // equality test does not make sense for NaN + EXPECT_NE(*((T *)mxGetData(mxValue)), *((T *)mxGetData(mxValue))); + } else { + EXPECT_EQ(*((T *)mxGetData(mxValue)), value); + } +} +// ... and specializations. +template <> // string +void test_value_impl(const mxArray *mxValue, const string &value) { + EXPECT_EQ(mxGetNumberOfElements(mxValue), value.size()); + EXPECT_EQ(mxGetStdString(mxValue), value); +} + +/** + * Can be instantiated for e.g. Entity and Property. + * + * This expects T to be either "scalar" types, or std::string. + */ +template <typename EntType, typename T> +void test_type_value(const string &datatype, const std::vector<T> &values, + const std::vector<string> &validStrings, + const std::vector<string> &invalidStrings, + const std::vector<string> &expectedErrors) { + EXPECT_EQ(values.size(), validStrings.size()); + EXPECT_EQ(invalidStrings.size(), expectedErrors.size()); + auto valueEntity = EntType(); + valueEntity.SetDatatype(datatype); + + // Test an empty string + valueEntity.SetValue(""); + auto mxValue = mxScalarFromStringValue(valueEntity); + EXPECT_TRUE(mxIsEmpty(mxValue)); + + // Test the valid strings + for (size_t i = 0; i < values.size(); ++i) { + auto value = values[i]; + auto valueString = validStrings[i]; + // std::cout << "---- " << value << " ----" << std::endl; + valueEntity.SetValue(valueString); + auto mxValue = mxScalarFromStringValue(valueEntity); + test_value_impl<T>(mxValue, value); + } + // Test the invalid strings + for (size_t i = 0; i < invalidStrings.size(); ++i) { + auto invalidString = invalidStrings[i]; + auto expectedError = expectedErrors[i]; + try { + valueEntity.SetValue(invalidString); + auto mxValue = mxScalarFromStringValue(valueEntity); + ADD_FAILURE() << "Expected error for value '" << invalidString << "'."; + } catch (std::exception const &err) { + EXPECT_EQ(err.what(), expectedError); + } catch (...) { + ADD_FAILURE() << "Expected std::exception for value '" << invalidString << "'."; + } + } +} + +/** + * Generate a vector with useful values for testing. + */ +template <typename T> auto generateValues() -> std::vector<T> { + std::vector<T> values = { + 0, + 1, + std::numeric_limits<T>::max(), + std::numeric_limits<T>::min(), + std::numeric_limits<T>::lowest(), + std::numeric_limits<T>::epsilon() // 0 for integers, but who cares? + }; + return values; +} + +/** + * Convenience wrapper around std::to_chars + */ +template <typename T> auto toStr(T value) -> std::string { + char *chars = reinterpret_cast<char *>(std::malloc(50 * sizeof(char))); + std::to_chars(chars, chars + 50, value); + auto result = string(chars); + std::free(chars); + return result; +} + +// to_chars is not widely supported for floats yet. +template <> auto toStr(double value) -> std::string { + auto result = boost::lexical_cast<std::string>(value); + return result; +} + +/////////////////////////////////////////////////////////////////////////////// +// Actual tests // +/////////////////////////////////////////////////////////////////////////////// + +/** + * Test if value conversion works + */ +TEST(caosdb_conversion, value_TEXT) { + auto values = std::vector<string>{"", "23", "foo", "CaosDB", "\n\n\n\a\n</>"}; + auto validStrings = std::vector<string>(values); + auto invalidStrings = std::vector<string>(); + auto expectedErrors = std::vector<string>(); + for (auto datatype : std::vector<string>{"TEXT", "REFERENCE", "DATETIME", "custom name"}) { + test_type_value<caosdb::entity::Entity, string>(datatype, values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); + test_type_value<caosdb::entity::Property, string>(datatype, values, validStrings, + invalidStrings, expectedErrors); + } +} + +TEST(caosdb_conversion, value_BOOLEAN) { + auto values = std::vector<bool>{true, true, true, false, false, false}; + auto validStrings = std::vector<string>{"true", "True", "TRUE", "false", "fAlSe", "FALSE"}; + auto invalidStrings = std::vector<string>{"no", "yes", "MAYBE"}; + auto expectedErrors = std::vector<string>{"Value >no< is neither FALSE nor TRUE.", + "Value >yes< is neither FALSE nor TRUE.", + "Value >MAYBE< is neither FALSE nor TRUE."}; + test_type_value<caosdb::entity::Entity, bool>("BOOLEAN", values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); + test_type_value<caosdb::entity::Property, bool>("BOOLEAN", values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); +} + +TEST(caosdb_conversion, value_INTEGER) { + auto values = generateValues<long>(); + auto validStrings = std::vector<string>(values.size()); + std::transform(values.begin(), values.end(), validStrings.begin(), + [](long value) -> string { return std::to_string(value); }); + auto invalidStrings = std::vector<string>{"NaN", "zero", "many"}; + auto expectedErrors = std::vector<string>{"stol", "stol", "stol"}; + test_type_value<caosdb::entity::Entity, long>("INTEGER", values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); + test_type_value<caosdb::entity::Property, long>("INTEGER", values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); +} + +TEST(caosdb_conversion, value_DOUBLE) { + auto values = generateValues<double>(); + values.push_back(std::numeric_limits<double>::quiet_NaN()); + values.push_back(std::numeric_limits<double>::infinity()); + values.push_back(-std::numeric_limits<double>::infinity()); + auto validStrings = std::vector<string>(values.size()); + std::transform(values.begin(), values.end(), validStrings.begin(), + [](double value) -> string { return toStr<double>(value); }); + // for (size_t i = 0; i < values.size(); ++i) { + // std::cout << values[i] << " -> " << validStrings[i] << std::endl; + // } + auto invalidStrings = std::vector<string>{"dunno", "zero", "many"}; + auto expectedErrors = std::vector<string>{"stod", "stod", "stod"}; + test_type_value<caosdb::entity::Entity, double>("DOUBLE", values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); + test_type_value<caosdb::entity::Property, double>("DOUBLE", values, validStrings, // NOSTYLE + invalidStrings, expectedErrors); +} + +/* + * The following are treated just like TEXT, at least for the moment: + * - REFERENCE + * - DATETIME + * - References with other names + * + * No testing exists yet for: + * - FILE + */ + +} // namespace maoxdb diff --git a/test/test_unittest.m b/test/test_unittest.m index a4e38bd63e3fe3a6009c69419d83b7542dd04a94..45e1d35a066851a00492ac25421353b8dc19ffa2 100644 --- a/test/test_unittest.m +++ b/test/test_unittest.m @@ -16,9 +16,9 @@ % You should have received a copy of the GNU Affero General Public License % along with this program. If not, see <https://www.gnu.org/licenses/>. -function test_suite=test_unittest() +function test_suite = test_unittest() try % assignment of 'localfunctions' is necessary in Matlab >= 2016 - test_functions=localfunctions(); + test_functions = localfunctions(); catch % no problem; early Matlab versions can use initTestSuite fine end initTestSuite; @@ -28,15 +28,12 @@ end function test_local() % default connection c1 = Caosdb(); - assertEqual(c1.connection, "") + assertEqual(c1.connection, ""); % class with explicit connection c2 = Caosdb("local-caosdb-admin"); assertEqual(c2.connection, "local-caosdb-admin"); - % TODO(daniel) Re-write s.th. version >= 0.0.6 (or whatever minimal - % version will be valid then) is checked. - % Only get the local versions. - % version = caosdb_exec("--version"); - % assertEqual(version, "v0.1 (libcaosdb v0.0.7)"); + version = caosdb_exec("--version"); + assertEqual(version(1:21), "v0.1 (libcaosdb v0.0."); end diff --git a/test/test_utilities.cpp b/test/test_utilities.cpp index 1c42a2c375d7d669b0767e8fa319b1f874615396..72a453b8998c8cc358a6ed843e96f1e7c6eefb9c 100644 --- a/test/test_utilities.cpp +++ b/test/test_utilities.cpp @@ -40,42 +40,48 @@ void test_scalar_uint64(UINT64_T value) { mxArray *scalar = mxScalarUINT64(value); EXPECT_TRUE(mxIsUint64(scalar)); EXPECT_EQ(mxGetNumberOfElements(scalar), 1); - EXPECT_EQ(*((UINT64_T *) mxGetData(scalar)), value); + EXPECT_EQ(*((UINT64_T *)mxGetData(scalar)), value); } void test_scalar_int64(INT64_T value) { mxArray *scalar = mxScalarINT64(value); EXPECT_TRUE(mxIsInt64(scalar)); EXPECT_EQ(mxGetNumberOfElements(scalar), 1); - EXPECT_EQ(*((INT64_T *) mxGetData(scalar)), value); + EXPECT_EQ(*((INT64_T *)mxGetData(scalar)), value); } void test_scalar_double(double value) { mxArray *scalar = mxScalarDOUBLE(value); EXPECT_TRUE(mxIsDouble(scalar)); EXPECT_EQ(mxGetNumberOfElements(scalar), 1); - EXPECT_EQ(*((double *) mxGetData(scalar)), value); + EXPECT_EQ(*((double *)mxGetData(scalar)), value); } void test_scalar_logical(bool value) { mxArray *scalar = mxScalarLOGICAL(value); EXPECT_TRUE(mxIsLogical(scalar)); EXPECT_EQ(mxGetNumberOfElements(scalar), 1); - EXPECT_EQ(*((bool *) mxGetData(scalar)), value); + EXPECT_EQ(*((bool *)mxGetData(scalar)), value); +} + +void test_scalar_empty() { + mxArray *scalar = mxEmptyDOUBLE(); + EXPECT_TRUE(mxIsDouble(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); } /** * Generate a vector with useful values for testing. */ -template<typename T> -auto values() -> std::vector<T> { - std::vector<T> values = - {0, 1, - std::numeric_limits<T>::max(), - std::numeric_limits<T>::min(), - std::numeric_limits<T>::lowest(), - std::numeric_limits<T>::epsilon() // 0 for integers, but who cares? - }; +template <typename T> auto values() -> std::vector<T> { + std::vector<T> values = { + 0, + 1, + std::numeric_limits<T>::max(), + std::numeric_limits<T>::min(), + std::numeric_limits<T>::lowest(), + std::numeric_limits<T>::epsilon() // 0 for integers, but who cares? + }; return values; } @@ -86,7 +92,7 @@ auto values() -> std::vector<T> { /** * Test if construction of scalar mex arrays works */ -TEST(test_utilities, scalar_arrays) { +TEST(utilities, scalar_arrays) { auto values_uint64 = values<UINT64_T>(); for (auto value : values_uint64) { test_scalar_uint64(value); @@ -108,18 +114,19 @@ TEST(test_utilities, scalar_arrays) { } } +TEST(utilities, empty_array) { test_scalar_empty(); } + /** * Test exception handling */ -TEST(test_utilities, exception_handling) { +TEST(utilities, exception_handling) { caosdb::exceptions::AuthenticationError exc("Authentication failed."); auto strings = exceptionToMessage(exc); EXPECT_EQ(strings.first, std::string("16")); - EXPECT_EQ(strings.second, - std::string("The attempt to execute this transaction has not been " - "executed at all because the authentication did not " - "succeed.\nAuthentication failed.")); + EXPECT_EQ(strings.second, std::string("The attempt to execute this transaction has not been " + "executed at all because the authentication did not " + "succeed.\nAuthentication failed.")); } -} // maoxdb +} // namespace maoxdb