diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f02b6972e4c0baf8b761ec97e6aac2c65350f0e6..1282a72ae253e440025384fdf035cecfb5c77c61 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -24,7 +24,7 @@ variables: # caosdb-cpplib) CPPLIB_REGISTRY_IMAGE: $CI_REGISTRY/caosdb/src/caosdb-cpplib/testenv OCTAVE_REGISTRY_IMAGE: $CI_REGISTRY_IMAGE/testenv:$CI_COMMIT_REF_NAME - CPPLIB_BRANCH: dev + CPPLIB_BRANCH: f-cpp-to-string OCTAVEINTTEST_PIPELINE: https://gitlab.indiscale.com/api/v4/projects/121/trigger/pipeline diff --git a/.miss_hit b/.miss_hit index f15ccfe29ff4a2e193fb4ae51ca5a7bd1cc3a88d..60282cf32ddc863c0406aba8712e56ade006a5d1 100644 --- a/.miss_hit +++ b/.miss_hit @@ -3,5 +3,6 @@ line_length: 100 regex_function_name: "[a-z]+(_[a-z]+)*" +regex_parameter_name: "~|([a-z]+(_[a-z]+)*)" suppress_rule: "whitespace_comments" tab_width: 2 diff --git a/CHANGELOG.md b/CHANGELOG.md index b1a73c2d7c2ac60fe785ea3f853e218f789a7235..f8400a33edf45615116dbb7ed6c2230c1e0afea5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security ### +## [0.0.2] - 2021-XX-XX ## + +### Added ### + +- Full functionality of libcaosdb mapped to Octave / Matlab. + ## [0.0.1] - 2021-08-12 ## ### Added ### diff --git a/CMakeLists.txt b/CMakeLists.txt index 5780bb426e0504d6c00f49376b402db48d087039..c887f7c53e0417d038fb3f435a0948cbe7708218 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,13 +100,12 @@ file(MAKE_DIRECTORY ${PKG_INST_DIR}) # Options for mex compilation string(REGEX REPLACE ";" ";-I" _MKOCTFILE_INCLUDES "-I${CONAN_INCLUDE_DIRS};${MAOXDB_DIR}") string(REGEX REPLACE ";" ";-L" _MKOCTFILE_LIB_DIRS "-L${CONAN_LIB_DIRS};${_MAOX_LIB_DIR}") -string(REGEX REPLACE ";" ";-l" _MKOCTFILE_LIBS "-l${CONAN_LIBS};maoxdb") +string(REGEX REPLACE ";" ";-l" _MKOCTFILE_LIBS "-lmaoxdb;${CONAN_LIBS}") string(REGEX REPLACE ";" ":" _MKOCTFILE_RPATH "${CONAN_LIB_DIRS}") set(_MKOCTFILE_OPTIONS "-Wl,-rpath,${_MKOCTFILE_RPATH}" "--mex" "-std=gnu++17" "-L/usr/local/lib" ${_MKOCTFILE_INCLUDES} ${_MKOCTFILE_LIB_DIRS} ${_MKOCTFILE_LIBS}) -add_custom_target(mex ALL - DEPENDS maoxdb) +add_custom_target(mex ALL) file(GLOB_RECURSE _CPP_SOURCES RELATIVE "${PROJECT_SOURCE_DIR}/src" src/private/*.cpp) foreach(sourcefile ${_CPP_SOURCES}) string(REGEX REPLACE ".cpp$" ".mex" _mex_ext_file ${sourcefile}) @@ -119,7 +118,7 @@ foreach(sourcefile ${_CPP_SOURCES}) ARGS ${_MKOCTFILE_OPTIONS} ${_mkoct_output} ${_abs_source} MAIN_DEPENDENCY ${_abs_source} ) - add_custom_target(${_target_name} DEPENDS ${_mex_ext_file}) + add_custom_target(${_target_name} DEPENDS ${_mex_ext_file} maoxdb) add_dependencies(mex ${_target_name}) endforeach(sourcefile) @@ -179,6 +178,7 @@ endif() option(TEST "Unit test with gtest" OFF) if(TEST) + set(CMAKE_BUILD_TYPE Debug) # cmake tests suffers from not being able to define dependencies, see # https://stackoverflow.com/questions/733475, so we disable it for now. # enable_testing() diff --git a/DESCRIPTION b/DESCRIPTION index 508fa947b45c48d2675bf6772b5fe8aa24ccb2eb..5d2d70059af7c7994ba2e3ce7176fd3c47f006c7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ # See https://octave.org/doc/interpreter/The-DESCRIPTION-File.html Name: caosdb -Version: 0.0.1 +Version: 0.0.2-alpha 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 8f27df3a88e8abb42a55ce40751b94c9ac581b6a..8866139b76072ab087db79db293118998fb47366 100644 --- a/Makefile +++ b/Makefile @@ -41,11 +41,12 @@ style: style_octave style_cpp .PHONY: style style_octave: - mh_style --octave src doc test || ( echo 'You may want to run `make style_fix`.'; exit 1 ) + @mh_style --octave src doc test || ( echo 'You may want to run `make style_fix`.'; exit 1 ) .PHONY: style_octave +CLANG_FORMAT ?= clang-format-11 style_cpp: - clang-format-11 --dry-run --verbose --Werror \ + @$(CLANG_FORMAT) --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 ) @@ -54,7 +55,7 @@ style_cpp: style_fix: echo "style_fix" mh_style --fix --octave src doc test - clang-format-11 -i --verbose --Werror \ + $(CLANG_FORMAT) -i --verbose --Werror \ $(shell find test/ src/ -type f \ -iname "*.cpp" -o -iname "*.hpp" -o -iname "*.h" -o -iname "*.h.in") .PHONY: style_fix @@ -86,6 +87,14 @@ test: install $(MAKE) -C test test .PHONY: test +test_octave: install + $(MAKE) -C test test_octave +.PHONY: test_octave + +test_cpp: install + $(MAKE) -C test test_cpp +.PHONY: test_cpp + ############################################################################### # Packaging and Installation # ############################################################################### diff --git a/conanfile.txt b/conanfile.txt index d788d5da679da7ddf4172f814c0986f5b312f88e..8dafba4de8a42d67af019707f0d9dc9f9a9ad8fc 100644 --- a/conanfile.txt +++ b/conanfile.txt @@ -1,5 +1,5 @@ [requires] -caosdb/[>=0.0.9] +caosdb/[>=0.0.17] [generators] cmake diff --git a/dev-requirements.txt b/dev-requirements.txt index fdb3579450258709d22d7df462dee7cfe698d7b7..06f516ae5b721607563332bac4bf1833ae2cfabf 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -14,8 +14,8 @@ idna==3.2 imagesize==1.2.0 Jinja2==2.11.3 MarkupSafe==2.0.1 -miss-hit==0.9.23 -miss-hit-core==0.9.23 +miss-hit==0.9.24 +miss-hit-core==0.9.24 node-semver==0.6.1 packaging==21.0 patch-ng==1.17.4 diff --git a/doc/Development.rst b/doc/Development.rst index fca47d741ad4cf19b66d31719f101e3d4225d875..177393b86e356969ea988c6cc7935327fbe7d989 100644 --- a/doc/Development.rst +++ b/doc/Development.rst @@ -1,19 +1,21 @@ +=========== Development =========== Structure ---------- +========= The sources for functions and classes are in ``src/``. Private functions (mostly C++ source files -which are to be compiled into ``*.mex``) are implemented in ``private/_some_function.*``. - - +which are to be compiled into ``*.mex``) are implemented in +``private/maox_some_function.*``. (``maox`` stands for "mex file for CaosDB". Writing Documentation ---------------------- +===================== -- Example for texinfo documentation: - https://github.com/gnu-octave/octave/blob/default/scripts/geometry/inpolygon.m +- The first string after the copyright block is used for documentation. This implies that the + copyright block must be recognized as such: A copyright line should be the first line there. +- Documentation should be written as TexInfo (see below for details) in order to be available in the + online documentation. - Extract documentation from file: ``[txt, form] = get_help_text_from_file(make_absolute_filename('pkg/inst/some_function.m'))`` - Generate HTML documentation from single file: @@ -29,3 +31,15 @@ Writing Documentation .. code-block:: octave generate_package_html('caosdb', 'htdocs', 'octave-forge') + +TexInfo details +--------------- + +Here is an Example for texinfo documentation:.. + https://github.com/gnu-octave/octave/blob/default/scripts/geometry/inpolygon.m + +Useful TexInfo commands: + +- ``@deftypefn`` :: + Define a function that can be called. Example code:.. + ``@deftypefn{Function File} {@var{out} =} caosdb_exec (@var{arg})`` diff --git a/doc/conf.py b/doc/conf.py index b64527327430f1cbbf01d761c4f59a9698090bce..499d4f6e3f75b45869d4997d8a14b46497fe6238 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -30,7 +30,7 @@ author = 'Daniel Hornung' version = '0.0' # The full version, including alpha/beta/rc tags #release = '0.5.2-rc2' -release = '0.0.1' +release = '0.0.2-alpha' # -- General configuration --------------------------------------------------- diff --git a/src/Caosdb.m b/src/Caosdb.m index 6b42098dbbed05569454bc26fdd94a8e5c4239ec..3337f669ecd42fb717d47f41537890d265453d4e 100644 --- a/src/Caosdb.m +++ b/src/Caosdb.m @@ -58,13 +58,10 @@ classdef Caosdb < handle %% info %% function res = info(obj) - % disp(["connection: >", obj.connection, "<"]); try info_result = maox_info(obj.connection); res = info_result; catch - % disp("some error!"); - % disp(lasterror()); rethrow(lasterror()); end end @@ -107,14 +104,9 @@ classdef Caosdb < handle end end - % disp("ids:"); - % disp(ids); try - collection = maox_retrieve(obj.connection, ids); - entities = maox_convert_collection(collection); + [entities, count_results] = maox_run_transaction(obj.connection, ids); catch - % disp("error handling in Caosdb.m"); - % disp(lasterror()); rethrow(lasterror()); end end @@ -143,17 +135,191 @@ classdef Caosdb < handle % 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] = maox_run_transaction(obj.connection, {}, {query_str}); + if count_results >= 0 entities = count_results; end catch - % disp("error handling in Caosdb.m"); - % disp(lasterror()); rethrow(lasterror()); end end + %% + % Insert an entity + % + % inserted = Caosdb.insert(entities) + % + % Parameters + % ---------- + % + % entities: cell array + % A cell array with the entities to be inserted. + % + % Returns + % ------- + % inserted : cell array + % The resulting inserted entities. + function inserted = insert(obj, entities) + % Ensure that entities is a cell array. + assert(nargin == 2, "maox:InvalidArgument", "This method accepts exactly 1 argument."); + assert(iscell(entities), "maox:InvalidArgument", ... + "ENTITIES must be a cell array, was:\n%s", disp(entities)); + assert(numel(entities) <= 1, "maox:NotImplementedError", ... + "Multi-insert has not been implemented yet."); + % Convert the entities + mx_entities = struct("", {}); + for entity = entities + mx_entities(end + 1) = entity{1}.to_struct(); + end + assert(numel(mx_entities) == numel(entities)); + + % Create an run transaction + try + [inserted, count_results] = maox_run_transaction(obj.connection, {}, {}, mx_entities); + assert(count_results == -1, "maox:RuntimeError", "There should be no count results."); + catch + rethrow(lasterror()); + end + end + + %% + % Update an entity + % + % updated = Caosdb.update(entities) + % + % Parameters + % ---------- + % + % entities: cell array + % A cell array with the entities to be updated. + % + % Returns + % ------- + % updated : cell array + % The resulting updated entities. + function updated = update(obj, entities) + % Ensure that entities is a cell array. + assert(nargin == 2, "maox:InvalidArgument", "This method accepts exactly 1 argument."); + assert(iscell(entities), "maox:InvalidArgument", ... + "ENTITIES must be a cell array, was:\n%s", disp(entities)); + assert(numel(entities) <= 1, "maox:NotImplementedError", ... + "Multi-update has not been implemented yet."); + % Convert the entities + mx_entities = struct("", {}); + % disp("UPDATE") + for entity = entities + mx_entities(end + 1) = entity{1}.to_struct(); + % disp(mx_entities(end)) + end + assert(numel(mx_entities) == numel(entities)); + + % Create an run transaction + try + [updated, count_results] = maox_run_transaction(obj.connection, {}, {}, {}, mx_entities); + % disp(updated); + assert(count_results == -1, "maox:RuntimeError", "There should be no count results."); + catch + rethrow(lasterror()); + end + end + + %% + % Delete entities by IDs. + % + % entities = Caosdb.delete_by_id("my_id", {"more", "ids"}) + % + % The usage is equivalent to retrieve_by_id(...). + % + % Note: Caosdb.delete will call the object's destructor + % + % Parameters + % ---------- + % + % ids: string or cell array of strings + % The ID(s) of the entity (entities) to be retrieved. + % + % Returns + % ------- + % entities : cell array + % The deleted entities (only IDs, possibly with messages). + function entities = delete_by_id(obj, ids, varargin) + % Ensure that IDS is a string cell array + assert(nargin >= 2, "maox:InvalidArgument", "delete(...) needs at least one argument."); + 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 + if nargin == 2 + varargin = {}; + end + for argin = varargin + argin = argin{1}; + 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 + + try + % disp("IDs to delete:") + % disp(ids); + [entities, count_results] = maox_run_transaction(obj.connection, {}, {}, ... + struct("", {}), struct("", {}), ... + ids); + assert(count_results == -1, "maox:RuntimeError", "There should be no count results."); + catch + rethrow(lasterror()); + end + end + + %% + % Download a file by ID + % + % entity = Caosdb.download_file_by_single_id("my_id", "/save/file/here.dat") + % + % Inserting files is done by creating a "FILE" Entity and inserting it. + % + % Parameters + % ---------- + % + % id: string + % The ID of the file to be downloaded. + % + % local_path: string + % The location where the file shall be saved. + % + % Returns + % ------- + % entity : cell array + % 1x1 cell array with the retrieved file entity. + function entity = download_file_by_single_id(obj, id, local_path) + narginchk(3, 3); + assert(ischar(id), "maox:InvalidArgument", "Need a single ID as first argument."); + assert(ischar(local_path), "maox:InvalidArgument", ... + "Need a local path for file download as second argument."); + try + ids = {id}; + local_paths = {local_path}; + [entities, count_results] = ... + maox_run_transaction(obj.connection, {}, {}, struct("", {}), struct("", {}), {}, ... + ids, local_paths); + assert(count_results == -1, "maox:RuntimeError", "There should be no count results."); + entity = entities{1}; + catch + rethrow(lasterror()); + end + + end + end end diff --git a/src/Entity.m b/src/Entity.m index 29a9dff9205156f05c55cd484c34a822341411c1..70fba8d8366bbc9437960c142ba7e0e344896699 100644 --- a/src/Entity.m +++ b/src/Entity.m @@ -19,21 +19,28 @@ classdef Entity < handle properties - role % One of "RecordType", "Record", "Property" + role % One of "RECORD_TYPE", "RECORD", "PROPERTY", "FILE" id versionId name description - datatype unit value + filepath % Only for file entities: the file path on the server + localpath % Only for file entities: the local path to the file (for upload) end properties (Access = private) - parents_ % Cell array of Parent objects.Struct array with the following fields: + datatype_ % 1x1 struct with the following fields: + % - isList logical + % - isReference logical + % - dtypeName string. Either the atomic datatype or the reference name. + parents_ % Cell array of Parent objects. May be created from a struct array with the + % following fields: % - id % - name % - description - properties_ % Cell array of Property objects.Struct array with the following fields: + properties_ % Cell array of Property objects. May be created from a struct array with the + % following fields: % - id % - name % - description @@ -51,16 +58,30 @@ classdef Entity < handle % 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"); + narginchk(0, 1); + if nargin == 1 + assert(isstruct(data), "caosdb:InvalidArgument", "Need a struct as argument"); + 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"); + else + obj.parents_ = {}; + obj.properties_ = {}; + obj.errors_ = {}; + obj.warnings_ = {}; + obj.infos_ = {}; + end end % Getters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + function result = get_datatype(obj) + result = obj.datatype_; + end + function result = get_parents(obj) result = obj.parents_; end @@ -81,6 +102,38 @@ classdef Entity < handle result = obj.infos_; end + % Setters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + function set_datatype(obj, dtype_name, is_reference, is_list) + if nargin < 4 + is_list = false; + end + if nargin < 3 + is_reference = false; + end + assert(nargin >= 2, "maox:InvalidArgument", "Need at least an atomic datatype."); + + dtype.isList = is_list; + dtype.isReference = is_reference; + if ~is_reference + assert(any(strcmp({ "UNSPECIFIED", "TEXT", "DOUBLE", "DATETIME", "INTEGER", "BOOLEAN"}, ... + dtype_name)), ... + "maox:InvalidArgument", ["Unrecognized atomic datatype: " dtype_name]); + end + dtype.dtypeName = dtype_name; + obj.datatype_ = dtype; + end + + function set_parents(obj, parents) + obj.parents_ = parents; + parents = obj.parents_; + end + + function set_properties(obj, props) + obj.properties_ = props; + props = obj.properties_; + end + % Information %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% function result = has_errors(obj) result = not(isempty(obj.errors_)); @@ -94,20 +147,72 @@ classdef Entity < handle result = not(isempty(obj.infos_)); end + % Convert to a struct which has all the fields that may be needed for interaction with the + % maoxdb library. + % + % If the datatype indicates a list value, the value is interpreted as such. It is an error if + % the value is list-like (cell string or more than one numeric element) while the datatype + % indicates a scalar value. + function struct_array = to_struct(obj, warn) + if nargin < 2 + warn = true; + end + res = struct(); + % Scalars first + res.role = obj.role; + res.id = obj.id; + res.versionId = obj.versionId; + res.name = obj.name; + res.description = obj.description; + res.datatype = obj.datatype_; + res.unit = obj.unit; + if isempty(obj.datatype_) + if ~isempty(obj.value) + warning(["Trying to transmit an Entity with VALUE but without DATATYPE. List-iness", ... + " is unknown, so the VALUE will be omitted."]); + end + % res.datatype = struct("dtypeName", "UNSPECIFIED", "isList", false, "isReference", false); + res.value = sparse([]); + else + res.value = maox_pack_value(obj.value, obj.datatype_.isList); + end + + % file handling + res.filepath = obj.filepath; + res.localpath = obj.localpath; + if warn && ~isequal(obj.role, "FILE") && (obj.filepath || obj.localpath) + warning("caosdb:file-properties-ignored", ... + "Setting FILEPATH or LOCALPATH makes no sense when the entity is not a FILE."); + end + % parents and properties + res.parents = cellfun(@(x)(x.to_struct()), obj.get_parents()); + res.properties = cellfun(@(x)(x.to_struct()), obj.get_properties()); + + struct_array = res; + end + + function disp(obj) + disp(obj.to_struct()); + end + end methods (Access = private) - % Only the simple metadata fields. + % Only the simple metadata fields (and datatype_). 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; + obj.role = getfielddefault(data, "role", ""); + obj.id = getfielddefault(data, "id", ""); + obj.versionId = getfielddefault(data, "versionId", ""); + obj.name = getfielddefault(data, "name", ""); + obj.description = getfielddefault(data, "description", ""); + obj.unit = getfielddefault(data, "unit", ""); + obj.value = getfielddefault(data, "value", ""); + if isfield(data, "datatype") + obj.datatype_ = data.datatype; + else + obj.set_datatype("UNSPECIFIED"); + end end % Assign the parents @@ -129,8 +234,10 @@ classdef Entity < handle % Create the errors, warnings, infos function result = create_messages(~, data, level) result = cell(); - for message = getfield(data, level) - result{end + 1} = Message(message); + if isfield(data, level) + for message = getfield(data, level) + result{end + 1} = Message(message); + end end end diff --git a/src/Message.m b/src/Message.m index e50782f9b95bc4aa8192c5bc935f1e9e84c4dd2b..96c13886b2cc371c2a1b44521e5a4cc7b04ff7a3 100644 --- a/src/Message.m +++ b/src/Message.m @@ -33,5 +33,13 @@ classdef Message < handle obj.description = data.description; end + function ans = str(obj) + ans = [num2str(obj.code), " - ", obj.description]; + end + + function disp(obj) + disp(obj.str()); + end + end end diff --git a/src/Parent.m b/src/Parent.m index db221b5bc628ec7218ff1132eddeac01694358c5..cbe99150fe5c6f967db434683162fbfdf3e9d674 100644 --- a/src/Parent.m +++ b/src/Parent.m @@ -16,6 +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/>. +%% +% +% Note: Setting the description has no effect for inserting or updating parents. classdef Parent < handle properties @@ -29,10 +32,27 @@ classdef Parent < handle % 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; + narginchk(0, 1); + if nargin == 1 + obj.id = data.id; + obj.name = data.name; + obj.description = data.description; + end + end + + % Convert to a struct which has all the fields that may be needed for interaction with the + % maoxdb library. + function struct_array = to_struct(obj) + res = struct(); + res.id = obj.id; + res.name = obj.name; + res.description = obj.description; + + struct_array = res; + end + + function disp(obj) + disp(obj.to_struct()); end end diff --git a/src/Property.m b/src/Property.m index 7c80ff88eb5cb2b70e49eacc5fa8ad1ddac2ec74..c0024cdab94073032476ee5a0c34fa102d20ec8b 100644 --- a/src/Property.m +++ b/src/Property.m @@ -23,9 +23,14 @@ classdef Property < handle name description importance - datatype - value unit + value + end + properties (Access = private) + datatype_ % 1x1 struct with the following fields: + % - isList logical + % - isReference logical + % - dtypeName string. Either the atomic datatype or the reference name. end methods @@ -33,14 +38,76 @@ classdef Property < handle % 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; + narginchk(0, 1); + if nargin == 1 + obj.id = data.id; + obj.name = data.name; + obj.description = data.description; + obj.importance = data.importance; + obj.datatype_ = data.datatype; + obj.unit = data.unit; + obj.value = data.value; + end + end + + % Convert to a struct which has all the fields that may be needed for interaction with the + % maoxdb library. + function struct_array = to_struct(obj) + res = struct(); + % Scalars first + res.id = obj.id; + res.name = obj.name; + res.description = obj.description; + res.importance = obj.importance; + res.datatype = obj.datatype_; + res.unit = obj.unit; + if isempty(obj.datatype_) + if ~isempty(obj.value) + warning(["Trying to transmit an Entity with value but without Datatype. List-iness", ... + " is unknown, so the value will be omitted."]); + end + % res.datatype = struct("dtypeName", "UNSPECIFIED", "isList", false, "isReference", false); + res.value = sparse([]); + else + res.value = maox_pack_value(obj.value, obj.datatype_.isList); + end + if isempty(res.value) && ~ischar(res.value) + res.value = sparse([]); + end + + struct_array = res; + end + + function disp(obj) + disp(obj.to_struct()); + end + + % Getters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + function result = get_datatype(obj) + result = obj.datatype_; + end + + % Setters %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + function set_datatype(obj, dtype_name, is_reference, is_list) + if nargin < 4 + is_list = false; + end + if nargin < 3 + is_reference = false; + end + assert(nargin >= 2, "maox:InvalidArgument", "Need at least an atomic datatype."); + + dtype.isList = is_list; + dtype.isReference = is_reference; + if ~is_reference + assert(any(strcmp({ "UNSPECIFIED", "TEXT", "DOUBLE", "DATETIME", "INTEGER", "BOOLEAN"}, ... + dtype_name)), ... + "maox:InvalidArgument", ["Unrecognized atomic datatype: " dtype_name]); + end + dtype.dtypeName = dtype_name; + obj.datatype_ = dtype; end end diff --git a/src/caosdb_exec.m b/src/caosdb_exec.m index 3bcf273b0ff908ff1197ec0e1b40223e726dfd5e..0827f31d3a6e81d9abf2cb0989fe8a4384acc924 100644 --- a/src/caosdb_exec.m +++ b/src/caosdb_exec.m @@ -1,8 +1,8 @@ -% 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 file is a part of the CaosDB Project. +% % 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 @@ -19,7 +19,7 @@ % -*- texinfo -*- % @deftypefn {Function File} {@var{out} =} caosdb_exec (arg) % @cindex index term -% Calls the equivalkent of the @code{caosdb} command line interface executable. Mostly, this can +% Calls the equivalent of the @code{caosdb} command line interface executable. Mostly, this can % output the version of libcaosdb. % % Typical arguments are @code{"--version"} or @code{"--test-connection"}. diff --git a/src/caosdb_get_ids.m b/src/caosdb_get_ids.m new file mode 100644 index 0000000000000000000000000000000000000000..3fa0870a2f6e632a282f25013737330e4c8610a2 --- /dev/null +++ b/src/caosdb_get_ids.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/>. + +%% Get the IDs of the given entities. +% +% This is a convenience function, because often cell arrays with Entity objects are returned, but +% only the IDs are needed. +% +% Parameters +% ---------- +% +% entities : cell array +% The Entity objects. +% +% Returns +% ------- +% +% out : string cell array +% The IDs, as strings in a cell array of the same shape as ENTITIES. +function out = caosdb_get_ids(entities) + out = cellfun(@(x)(x.id), entities, "UniformOutput", false); +end diff --git a/src/configure b/src/configure index a12ee14d1ddb336a930862a1b6a0885c03f83256..d4de430d18f0cc18950660def38b88f7cdb64f50 100755 --- a/src/configure +++ b/src/configure @@ -22,6 +22,6 @@ mkdir -p $BUILD_DIR pushd $BUILD_DIR conan install .. -s "compiler.libcxx=libstdc++11" -cmake .. +cmake -G "Unix Makefiles" -D CMAKE_BUILD_TYPE=Release -D AUTOFORMATTING=OFF .. popd diff --git a/src/lib/maoxdb.cpp b/src/lib/maoxdb.cpp index 176dd55a424d8bf6e49fa8f77d99b8ca5256a30b..70212aa0f8d232d802f159cb6bb963ce255fa8df 100644 --- a/src/lib/maoxdb.cpp +++ b/src/lib/maoxdb.cpp @@ -27,16 +27,40 @@ #include "maoxdb.hpp" #include "mex.h" +#include "caosdb/connection.h" +#include "caosdb/logging.h" // for CAOSDB_LOG_TRACE #include "caosdb/status_code.h" #include "caosdb/transaction.h" #include "caosdb/transaction_status.h" +#include <boost/lexical_cast.hpp> +#include <map> +#include <type_traits> 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 *; +namespace ce = caosdb::entity; +namespace cu = caosdb::utility; + +// Workaround for issue caosdb-cpplib#17 +#undef CAOSDB_LOG_TRACE +#undef CAOSDB_LOG_DEBUG +#undef CAOSDB_LOG_INFO +// #define CAOSDB_LOG_TRACE(name) std::clog << std::endl << "[" << name << "] " +// #define CAOSDB_LOG_DEBUG(name) std::clog << std::endl << "[" << name << "] " +// #define CAOSDB_LOG_INFO(name) std::clog << std::endl << "[" << name << "] " + +std::ostream nullout(nullptr); // NOLINT + +#define CAOSDB_LOG_TRACE(name) nullout // NOLINT +#define CAOSDB_LOG_DEBUG(name) nullout // NOLINT +#define CAOSDB_LOG_INFO(name) nullout // NOLINT + +const auto logger_name = string("maoxdb"); + +auto mxFromCaosDBParents(const ce::Parents &parents) -> mxArray *; +auto mxFromCaosDBProperties(const ce::Properties &properties) -> mxArray *; +auto mxFromCaosDBMessages(const ce::Messages &messages) -> mxArray *; // Exception and error handling /////////////////////////////////////////////// @@ -45,14 +69,19 @@ auto mxFromCaosDBMessages(const caosdb::entity::Messages &messages) -> mxArray * */ 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). + const auto &status = trans->GetStatus(); + const 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) { + if (code == caosdb::StatusCode::GENERIC_TRANSACTION_ERROR) { + CAOSDB_LOG_INFO(logger_name) << std::to_string(code) << " / " << status.GetDescription(); + } return std::pair<string, string>(); } - string id = std::to_string(code); + string id = string("maoxdb:E") + std::to_string(code); string text = status.GetDescription(); + CAOSDB_LOG_INFO(logger_name) << id << " / " << text; return std::pair<string, string>(id, text); } @@ -62,6 +91,7 @@ auto transactionStatusToMessage(const caosdb::transaction::Transaction *const tr void throwOctExceptionIfError(const caosdb::transaction::Transaction *const trans) { auto msg = maoxdb::transactionStatusToMessage(trans); if (msg.first != "") { + CAOSDB_LOG_TRACE(logger_name) << msg.first << " // " << msg.second; mexErrMsgIdAndTxt(msg.first.c_str(), msg.second.c_str()); } } @@ -84,32 +114,33 @@ void throwOctException(const caosdb::exceptions::Exception &exc) { std::pair<string, string> excContent = exceptionToMessage(exc); mexErrMsgIdAndTxt(excContent.first.c_str(), excContent.second.c_str()); } - +/////////////////////////////////////////////////////////////////////////////// // Entity handling //////////////////////////////////////////////////////////// +////////// libcaosdb to mxArray /////////////////////////////////////////////// + /** * @brief Convert a ResultSet to a struct mexArray. */ auto mxFromResultSet(const caosdb::transaction::ResultSet &resultSet) -> mxArray * { - if (resultSet.Size() == 0) { - auto *result = mxEmptySTRUCT(); + if (resultSet.size() == 0) { + auto *result = mxEmptyStruct(); return result; } std::vector<const mxArray *> entities; // Obtain entities - for (auto entity : resultSet) { + for (const 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 * { +auto mxFromCaosDBEntity(const ce::Entity &entity) -> mxArray * { // clang-format off auto fields = std::vector<const char*> {"role", @@ -127,18 +158,18 @@ auto mxFromCaosDBEntity(const caosdb::entity::Entity &entity) -> mxArray * { "infos" }; // clang-format on - std::array<mwSize, 2> dims = {1, (mwSize)fields.size()}; - auto *result = mxCreateStructArray(2, dims.data(), fields.size(), fields.data()); + + CAOSDB_LOG_DEBUG(logger_name + "::mxFromCaosDBEntity") << entity.ToString(); + auto *result = mxCreateStructFromStrings(fields); // Fill with scalar values - mxSetField(result, 0, "role", mxCreateString(entity.GetRole().c_str())); + mxSetField(result, 0, "role", mxEnumToString<ce::Role>(entity.GetRole())); 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, "datatype", mxFromDataType(entity.GetDataType())); mxSetField(result, 0, "unit", mxCreateString(entity.GetUnit().c_str())); - // Parse value to proper type. - mxSetField(result, 0, "value", mxScalarFromStringValue(entity)); + mxSetField(result, 0, "value", mxFromValue(entity.GetValue())); // Parents and Properties mxSetField(result, 0, "parents", mxFromCaosDBParents(entity.GetParents())); @@ -153,7 +184,7 @@ auto mxFromCaosDBEntity(const caosdb::entity::Entity &entity) -> mxArray * { } // Parents to struct array -auto mxFromCaosDBParents(const caosdb::entity::Parents &parents) -> mxArray * { +auto mxFromCaosDBParents(const ce::Parents &parents) -> mxArray * { // clang-format off std::vector<const char*> fields = {"id", @@ -161,22 +192,19 @@ auto mxFromCaosDBParents(const caosdb::entity::Parents &parents) -> mxArray * { "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); + std::array<mwSize, 2> dims = {1, (mwSize)parents.size()}; + auto *result = mxCreateStructFromStrings(fields, dims); + 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())); + mxSetField(result, i, "description", mxCreateString(parent.GetDescription().c_str())); } return result; } // Properties to struct array -auto mxFromCaosDBProperties(const caosdb::entity::Properties &properties) -> mxArray * { +auto mxFromCaosDBProperties(const ce::Properties &properties) -> mxArray * { // clang-format off std::vector<const char*> fields = {"id", @@ -188,56 +216,384 @@ auto mxFromCaosDBProperties(const caosdb::entity::Properties &properties) -> mxA "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); + std::array<mwSize, 2> dims = {1, (mwSize)properties.size()}; + auto *result = mxCreateStructFromStrings(fields, dims); + 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, "importance", mxEnumToString<ce::Importance>(property.GetImportance())); 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)); + mxSetField(result, i, "datatype", mxFromDataType(property.GetDataType())); + mxSetField(result, i, "value", mxFromValue(property.GetValue())); } return result; } // Messages to struct array -auto mxFromCaosDBMessages(const caosdb::entity::Messages &messages) -> mxArray * { +auto mxFromCaosDBMessages(const ce::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); + std::array<mwSize, 2> dims = {1, (mwSize)messages.size()}; + auto *result = mxCreateStructFromStrings(fields, dims); + for (mwIndex i = 0; i < messages.size(); ++i) { + const 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 ////////////////////////////////////////////////////////// +/** + * Convert a DataType descriptor object to a struct array. + */ +auto mxFromDataType(const ce::DataType &data_type) -> mxArray * { + namespace ce = ce; + + mxArray *dtype_name; + auto is_list = mxScalarLOGICAL(data_type.IsList()); + auto is_reference = mxScalarLOGICAL(data_type.IsReference()); + if (data_type.IsList()) { + const auto &list_datatype = data_type.GetAsList(); + is_reference = mxScalarLOGICAL(list_datatype.IsListOfReference()); + if (list_datatype.IsListOfReference()) { + dtype_name = mxCreateString(list_datatype.GetReferenceDataType().GetName().c_str()); + } else if (list_datatype.IsListOfAtomic()) { + dtype_name = mxEnumToString<ce::AtomicDataType>(list_datatype.GetAtomicDataType()); + } else { + throw std::logic_error(string("Unexpected data type: " + data_type.ToString())); + } + } else if (data_type.IsReference()) { + dtype_name = mxCreateString(data_type.GetAsReference().GetName().c_str()); + } else if (data_type.IsAtomic()) { + dtype_name = mxEnumToString<ce::AtomicDataType>(data_type.GetAsAtomic()); + } else if (data_type.ToString() == "{}") { + return mxEmptySparse(); // Workaround to denote a Null value: sparse array + } else { + throw std::logic_error(string("Unexpected data type: " + data_type.ToString())); + } + + // clang-format off + auto fields = std::vector<const char*> + {"dtypeName", + "isReference", + "isList" + }; + // clang-format on + auto *result = mxCreateStructFromStrings(fields); + mxSetField(result, 0, "dtypeName", dtype_name); + mxSetField(result, 0, "isReference", is_reference); + mxSetField(result, 0, "isList", is_list); + + return result; +} + +/** + * Convert the libcaosdb value to a mxArray. + */ +auto mxFromValue(const ce::Value &value) -> mxArray * { + if (value.IsNull()) { + return mxEmptySparse(); // Workaround to denote a Null value: sparse array + } else if (value.IsString()) { // Scalars are converted to normal arrays. + return mxCreateString(value.GetAsString().c_str()); + } else if (value.IsDouble()) { + return mxScalarDOUBLE(value.GetAsDouble()); + } else if (value.IsBool()) { + return mxScalarLOGICAL(value.GetAsBool()); + } else if (value.IsInt64()) { + return mxScalarINT64(value.GetAsInt64()); + } else if (!value.IsVector()) { + throw std::logic_error(string("Unexpected value type: " + value.ToString())); + } + // It's a list now -> convert to cell array always. + auto list = value.GetAsVector(); + auto *result = mxCreateCellArray(0, (mwSize *)nullptr); + if (list.empty()) { // Empty list: empty cell array + return result; + } + const std::array<mwSize, 2> dims = {1, (mwSize)list.size()}; + auto list_value = list[0]; + if (list_value.IsString()) { // String + result = mxCreateCellArray(2, dims.data()); + for (size_t i = 0; i < list.size(); ++i) { + mxSetCell(result, i, mxCreateString(list[i].GetAsString().c_str())); + } + } else if (list_value.IsBool()) { // Bool + result = mxCreateLogicalArray(2, dims.data()); + auto *data = static_cast<mxLogical *>(mxGetData(result)); + for (size_t i = 0; i < list.size(); ++i) { + data[i] = list[i].GetAsBool(); + } + } else if (list_value.IsInt64()) { // Int + result = mxCreateNumericArray(2, dims.data(), mxINT64_CLASS, mxREAL); + auto *data = static_cast<INT64_T *>(mxGetData(result)); + for (size_t i = 0; i < list.size(); ++i) { + data[i] = list[i].GetAsInt64(); + } + } else if (list_value.IsDouble()) { // Double + result = mxCreateNumericArray(2, dims.data(), mxDOUBLE_CLASS, mxREAL); + auto *data = static_cast<double *>(mxGetData(result)); + for (size_t i = 0; i < list.size(); ++i) { + data[i] = list[i].GetAsDouble(); + } + } else { + throw std::logic_error("Unexpected list value type"); + } + // Non-empty list: 1x1 cell array with the payload at first position. + const std::array<mwSize, 2> wrapper_dims = {1, 1}; + auto *result_wrapper = mxCreateCellArray(0, wrapper_dims.data()); + mxSetCell(result_wrapper, 0, result); + return result_wrapper; +} + +////////// mxArray to libcaosdb /////////////////////////////////////////////// + +/* + * Set an Entity object's data (parents, properties, name etc.) from the data in ARRAY. + */ +void assignEntityDataFromMx(ce::Entity &entity, const mxArray *array, const mwSize index) { + CAOSDB_LOG_TRACE(logger_name) << "assignEntityDataFromMx"; + CAOSDB_LOG_TRACE(logger_name) << "type: " << mxGetClassID(array); + entity.SetRole(mxStringToEnum<ce::Role>(mxGetField(array, index, "role"))); + CAOSDB_LOG_TRACE(logger_name) << "Role set"; + entity.SetName(mxGetStdString(mxGetField(array, index, "name"))); + entity.SetDescription(mxGetStdString(mxGetField(array, index, "description"))); + entity.SetDataType(dataTypeFromMx(mxGetField(array, index, "datatype"))); + entity.SetUnit(mxGetStdString(mxGetField(array, index, "unit"))); + entity.SetValue(valueFromMx(mxGetField(array, index, "value"))); + if (entity.GetRole() == ce::Role::FILE) { // This is necessary until caosdb-server#174 is fixed. + entity.SetFilePath(mxGetStdString(mxGetField(array, index, "filepath"))); + entity.SetLocalPath(mxGetStdString(mxGetField(array, index, "localpath"))); + } + CAOSDB_LOG_TRACE(logger_name) << "parents & properties"; + for (size_t i = entity.GetParents().size(); i > 0; i--) { + entity.RemoveParent(i - 1); + } + for (auto parent : parentsFromMx(mxGetField(array, index, "parents"))) { + entity.AppendParent(parent); + } + for (size_t i = entity.GetProperties().size(); i > 0; i--) { + entity.RemoveProperty(i - 1); + } + for (auto &property : propertiesFromMx(mxGetField(array, index, "properties"))) { + entity.AppendProperty(property); + } +} + +/* + * Create a vector of Entity objects from the data in ARRAY. + */ +auto entitiesFromMx(const mxArray *array, bool for_update, string conn_name) + -> std::vector<ce::Entity> { + auto numel = mxGetNumberOfElements(array); + CAOSDB_LOG_TRACE(logger_name) << "entitiesFromMx : numel : " << numel; + std::vector<ce::Entity> entities(numel); // Vector with empty entities + if (numel == 0) { + return entities; + } + // for_update: Retrieve entities and assign everything but ID /////////////// + if (for_update) { + CAOSDB_LOG_TRACE(logger_name) << "This is for updating."; + std::shared_ptr<caosdb::connection::Connection> connection; + if (conn_name.empty()) { + connection = caosdb::connection::ConnectionManager::GetDefaultConnection(); + } else { + connection = caosdb::connection::ConnectionManager::GetConnection(conn_name); + } + std::map<string, mwSize> idMap; // To get the Entity data for each ID + auto transaction = connection->CreateTransaction(); + for (mwSize i = 0; i < numel; ++i) { + auto id = mxGetStdString(mxGetField(array, i, "id")); + idMap[id] = i; // map ID -> index in mxArray [0..numel-1] + transaction->RetrieveById(id); + } + CAOSDB_LOG_TRACE(logger_name) << "exec async"; + transaction->ExecuteAsynchronously(); + CAOSDB_LOG_TRACE(logger_name) << "waitforit"; + auto t_stat = transaction->WaitForIt(); + CAOSDB_LOG_TRACE(logger_name) << "waited: " << t_stat.GetCode() << " / " + << t_stat.GetDescription(); + maoxdb::throwOctExceptionIfError(transaction.get()); + CAOSDB_LOG_TRACE(logger_name) << "status: " << transaction->GetStatus().GetDescription(); + const auto &results = transaction->GetResultSet(); + // Update the Entity contents + CAOSDB_LOG_TRACE(logger_name) << "&results " << &results; + CAOSDB_LOG_TRACE(logger_name) << "numel results " << results.size(); + entities = std::vector<ce::Entity>(); + for (ce::Entity entity : results) { + auto index = idMap[entity.GetId()]; + auto remote_version = entity.GetVersionId(); + auto local_version = mxGetStdString(mxGetField(array, index, "versionId")); + if (remote_version != local_version) { + throw std::invalid_argument( + "Version is not at HEAD of remote, but this is required for an update action."); + } + assignEntityDataFromMx(entity, array, index); + entities.push_back(entity); + // TODO (dh) Wtf, why can't I do entities[foo] = entity? + } + } + // insert only: No ID, only assign other values /////////////////////////// + else { + CAOSDB_LOG_TRACE(logger_name) << "This is for inserting."; + for (mwSize i = 0; i < numel; ++i) { + assignEntityDataFromMx(entities[i], array, i); + } + } + return entities; +} + +/* + * Extract the parents from the data in ARRAY. + */ +auto parentsFromMx(const mxArray *parentsArray) -> std::vector<ce::Parent> { + std::vector<ce::Parent> parents(mxGetNumberOfElements(parentsArray)); + for (mwSize i = 0; i < mxGetNumberOfElements(parentsArray); ++i) { + parents[i].SetId(mxGetStdString(mxGetField(parentsArray, i, "id"))); + parents[i].SetName(mxGetStdString(mxGetField(parentsArray, i, "name"))); + } + return parents; +} + +/* + * Extract the properties from the data in ARRAY. + */ +auto propertiesFromMx(const mxArray *propertiesArray) -> std::vector<ce::Property> { + std::vector<ce::Property> properties(mxGetNumberOfElements(propertiesArray)); + for (mwSize i = 0; i < mxGetNumberOfElements(propertiesArray); ++i) { + properties[i].SetId(mxGetStdString(mxGetField(propertiesArray, i, "id"))); + properties[i].SetName(mxGetStdString(mxGetField(propertiesArray, i, "name"))); + properties[i].SetDescription(mxGetStdString(mxGetField(propertiesArray, i, "description"))); + properties[i].SetImportance( + mxStringToEnum<ce::Importance>(mxGetField(propertiesArray, i, "importance"))); + properties[i].SetDataType(dataTypeFromMx(mxGetField(propertiesArray, i, "datatype"))); + properties[i].SetUnit(mxGetStdString(mxGetField(propertiesArray, i, "unit"))); + properties[i].SetValue(valueFromMx(mxGetField(propertiesArray, i, "value"))); + } + return properties; +} + +/** + * Convert a struct array to a DataType descriptor object. + */ +auto dataTypeFromMx(const mxArray *datatypeArray) -> ce::DataType { + if (mxIsEmpty(datatypeArray)) { + return ce::DataType(); + } + CAOSDB_LOG_TRACE(logger_name) << "dataTypeFromMx"; + if (!mxIsStruct(datatypeArray)) { + throw std::logic_error(string("Unexpected type for datatype (Class ID ") + + boost::lexical_cast<std::string>(mxGetClassID(datatypeArray)) + + ", expected " + + boost::lexical_cast<std::string>(mxClassID::mxSTRUCT_CLASS) + ")"); + } + auto is_list = mxGetScalarValue<bool>(mxGetField(datatypeArray, 0, "isList")); + auto is_reference = mxGetScalarValue<bool>(mxGetField(datatypeArray, 0, "isReference")); + auto dtype_name = mxGetStdString(mxGetField(datatypeArray, 0, "dtypeName")); + + ce::DataType dtype; + if (is_reference && is_list) { + dtype = ce::DataType::ListOf(dtype_name); + } else if (is_reference) { + dtype = ce::DataType(dtype_name); + } else if (is_list) { + dtype = ce::DataType::ListOf(cu::getEnumValueFromName<ce::AtomicDataType>(dtype_name)); + } else { + dtype = ce::DataType(cu::getEnumValueFromName<ce::AtomicDataType>(dtype_name)); + } + + return dtype; +} /** + * Convert the mxArray to a libcaosdb value. + */ +auto valueFromMx(const mxArray *array) -> caosdb::entity::Value { + using ce::Value; + auto result = Value(); + size_t numel; + if (mxIsSparse(array)) { // Null value + return result; + } else if (mxIsCell(array)) { // list value (in cell array) + if (mxIsEmpty(array)) { + // Empty list + return Value(std::vector<int>()); + } + array = mxGetCell(array, 0); + numel = mxGetNumberOfElements(array); + if (mxIsCell(array)) { // string cell array + auto content = std::vector<string>(numel); + for (size_t i = 0; i < numel; ++i) { + auto value = mxGetStdString(mxGetCell(array, i)); + content[i] = value; + } + result = Value(content); + } else if (mxIsLogical(array)) { // bool + auto content = std::vector<bool>(numel); + auto data = static_cast<bool *>(mxGetData(array)); + for (size_t i = 0; i < numel; ++i) { + content[i] = data[i]; + } + result = Value(content); + } else if (mxIsInt64(array)) { // int + auto content = std::vector<INT64_T>(numel); + auto data = static_cast<INT64_T *>(mxGetData(array)); + for (size_t i = 0; i < numel; ++i) { + content[i] = data[i]; + } + result = Value(content); + } else if (mxIsDouble(array)) { // double + auto content = std::vector<double>(numel); + auto data = static_cast<double *>(mxGetData(array)); + for (size_t i = 0; i < numel; ++i) { + content[i] = data[i]; + } + result = Value(content); + } else { + throw std::logic_error(string("Unexpected array type for list value (Class ID ") + + boost::lexical_cast<std::string>((mxGetClassID(array))) + ")"); + } + } else { // scalar value (no cell array) + if (mxIsChar(array)) { // string + result = Value(mxGetStdString(array)); + } else if (mxIsLogical(array)) { // bool + auto data = *static_cast<bool *>(mxGetData(array)); + result = Value(data); + } else if (mxIsInt64(array)) { // int + auto data = *static_cast<INT64_T *>(mxGetData(array)); + result = Value(data); + } else if (mxIsDouble(array)) { // double + auto data = *static_cast<double *>(mxGetData(array)); + result = Value(data); + } else { + throw std::logic_error(string("Unexpected type for scalar value (Class ID ") + + boost::lexical_cast<std::string>((mxGetClassID(array))) + ")"); + } + } + + 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 nFields = (mwSize)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 result = mxCreateStructFromStrings(fields, dims); auto i = mwIndex(0) - 1; for (auto scalarStruct : structs) { @@ -249,17 +605,37 @@ auto mxMergeScalarStructs(const std::vector<const mxArray *> &structs) -> mxArra return result; } -/** - * @brief Extract a std::string from a char array +/* + * @brief Extract a string from a char array */ -auto mxGetStdString(const mxArray *array) -> std::string { +auto mxGetStdString(const mxArray *array) -> string { + if (array == nullptr) { + std::cerr << "Trying to convert a null pointer to std::string." << std::endl; + } auto len = mxGetNumberOfElements(array) + 1; // NUL byte auto buf = std::vector<char>(len); auto ret = mxGetString(array, buf.data(), len); if (ret) { + if (len == 1) { + return string(); + } throw std::invalid_argument("Could not extract a string, probably wrong mxArray content."); } - return std::string(buf.data()); + return string(buf.data()); +} + +/* + * @brief Convert the mx cell string array to a string vector. + */ +auto mxCellToStrings(const mxArray *array) -> std::vector<std::string> { + if (!mxIsCell(array)) { + throw std::invalid_argument("Must be a cell array of strings."); + } + std::vector<string> result(mxGetNumberOfElements(array)); + for (mwIndex i = 0; i < mxGetNumberOfElements(array); ++i) { + result[i] = mxGetStdString(mxGetCell(array, i)); + } + return result; } } // namespace maoxdb diff --git a/src/lib/maoxdb.hpp b/src/lib/maoxdb.hpp index d67eb0160085dc20be92cf20a0b82738c2aae511..818041f1323b771ed0a7624a3b3bca2517cfd0dc 100644 --- a/src/lib/maoxdb.hpp +++ b/src/lib/maoxdb.hpp @@ -5,8 +5,10 @@ #include "caosdb/status_code.h" #include "caosdb/transaction.h" #include "caosdb/transaction_status.h" +#include "caosdb/utility.h" #include "mex.h" #include <boost/algorithm/string.hpp> +#include <boost/iostreams/stream.hpp> #include <stdexcept> #include <string> @@ -18,21 +20,30 @@ * 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): + * The Entity structs have the following structure. Not all keys need to exist for all directions, + * when a key is only required for one direction, this is denoted by an angled bracket ("<" from + * libcaosdb to Octave, ">" from Octave to libcaosdb). * - * - role: One of "RecordType", "Record", "Property" - * - id - * - versionId + * - role Enum string from entity.h + * - id Ignored when inserting an Entity. + * - versionId Ignored when inserting an Entity. * - name * - description - * - datatype + * - datatype Struct with the following fields: + * - dtypeName: string, either AtomicDataType from data_type.h or reference name + * - isReference: logical, true if reference datatype + * - isList: logical, true if list datatype * - unit - * - value: A string representation of the value. + * - filepath Only relevant for FILE entities. + * - localpath > Only relevant for FILE insertion/update. + * - value: Array representing the value. Normal arrays represent scalar values (1xN char + * arrays for strings, scalar arrays for other atomic value), and arrays inside a 1x1 + * cell array represent list values. Unset scalars are represented by a sparse + * array, unset list values by an empty cell array. * - parents: Struct array with the following fields: * - id * - name - * - description + * - description < * - properties: Struct array with the following fields: * - id * - name @@ -41,137 +52,121 @@ * - value * - unit * - datatype - * - errors: Struct array with the following fields: + * - errors < Struct array with the following fields: * - code (as INT64) * - description - * - warnings: Like messages. - * - infos: Like messages. + * - warnings < Like messages. + * - infos < Like messages. * */ // Macros ///////////////////////////////////////////////////////////////////// -// Utility functions ////////////////////////////////////////////////////////// namespace maoxdb { using std::string; -template <typename T> auto mxScalar(T value, mxClassID class_id) -> mxArray * { - mxArray *array = mxCreateNumericMatrix(1, 1, class_id, mxREAL); - auto *data = static_cast<T *>(mxGetData(array)); - data[0] = value; - return array; -} +// Entity handling //////////////////////////////////////////////////////////// -// Convenience shortcut functions +/** + * @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 *; -inline auto mxScalarUINT64(UINT64_T value) -> mxArray * { - return mxScalar<UINT64_T>(value, mxUINT64_CLASS); -} -inline auto mxScalarINT64(INT64_T value) -> mxArray * { - return mxScalar<INT64_T>(value, mxINT64_CLASS); -} -inline auto mxScalarDOUBLE(double value) -> mxArray * { - return mxScalar<double>(value, mxDOUBLE_CLASS); -} -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 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 *; /** - * @brief Convert the string-typed value in an Entity-like object to a mxArray. + * @brief Convert a DataType descriptor object to a struct array. * - * @details For the existing datatypes, see - * https://docs.indiscale.com/caosdb-server/specification/Datatype.html + * @return A 1x1 struct array with the fields (atomic_datatype, reference_name, is_list) as + * described in this file. */ -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; -} +auto mxFromDataType(const caosdb::entity::DataType &data_type) -> mxArray *; /** - * @brief Extract a std::string from a char array + * @brief Convert the libcaosdb value to a mxArray. * - * @details Handles the length check + * @details Null values are represented as an empty sparse array, lists are cell arrays, scalars + * are normal 1x1 arrays. + */ +auto mxFromValue(const caosdb::entity::Value &value) -> mxArray *; + +/** + * @brief Set an Entity object's data (parents, properties, name etc.) from the data in ARRAY. * - * @param array The char array from which the string shall be extracted. + * @details The id and versionId are not modified. Either they exist already or they remain + * unset. * - * @return The std::string. + * @param entity The entity to be updated. + * + * @param array The struct array with the data. + * + * @param index The index of the array from which to take the data. Default is 0. */ -auto mxGetStdString(const mxArray *array) -> std::string; +auto assignEntityDataFromMx(caosdb::entity::Entity &entity, const mxArray *array, + const mwSize index = 0) -> void; + +/** + * @brief Create a vector of Entity objects from the data in ARRAY. + * + * @details By default, the id and versionId are not set, this is the required state for + * inserting the entity into a CaosDB instance. If `for_update` is `true`, the id and + * versionId are matched against the remote HEAD version and set if they are the same. + * If they are not the same, an exception is thrown. + * + * This function is typically used for insert and update actions. + * + * @param array The struct array with the data (may have more than one element). + * + * @param for_update If true, assert that id and versionId match the remote HEAD and the + * returned Entity. + * + * @param conn_name The connection name. Same conventions hold as for other functions. + * + * @return A vector with Entity objects which have their values set according to ARRAY. + */ +auto entitiesFromMx(const mxArray *array, bool for_update = false, string conn_name = "") + -> std::vector<caosdb::entity::Entity>; + +/** + * @brief Extract the parents from the data in ARRAY. + * + * @param array The 1x1 struct array with the data, for example the "parents" field of an + * entity struct. + * + * @return A vector with Parent objects, created from the values in ARRAY. + */ +auto parentsFromMx(const mxArray *parentsArray) -> std::vector<caosdb::entity::Parent>; + +/** + * @brief Extract the properties from the data in ARRAY. + * + * @param array The 1x1 struct array with the data, for example the "properties" field of an + * entity struct. + * + * @return A vector with Property objects, created from the values in ARRAY. + */ +auto propertiesFromMx(const mxArray *propertiesArray) -> std::vector<caosdb::entity::Property>; + +/** + * @brief Convert a struct array to a DataType descriptor object. + * + * @return A libcaosdb DataType object. + */ +auto dataTypeFromMx(const mxArray *datatypeArray) -> caosdb::entity::DataType; + +/** + * @brief Convert the mxArray to a libcaosdb value. + */ +auto valueFromMx(const mxArray *array) -> caosdb::entity::Value; // Exception and error handling /////////////////////////////////////////////// @@ -232,36 +227,165 @@ auto exceptionToMessage(const caosdb::exceptions::Exception &exc) -> std::pair<s */ void throwOctException(const caosdb::exceptions::Exception &exc); -// Entity handling //////////////////////////////////////////////////////////// +// Utility functions ////////////////////////////////////////////////////////// /** - * @brief Convert a ResultSet to a struct mexArray. + * @brief Merges a number of scalar (1x1) mex structs into a 1xN struct. * - * @return A 1xN struct array, with Entity structs as values. + * @details The content (the mxArrays behind the input structs values) is + * duplicated. + * + * @param structs: Must all have the same fields, must all be of size 1x1. + * + * @return A 1xN struct array. */ -auto mxFromResultSet(const caosdb::transaction::ResultSet &resultSet) -> mxArray *; +auto mxMergeScalarStructs(const std::vector<const mxArray *> &structs) -> mxArray *; + +template <typename T> auto mxScalar(T value, mxClassID class_id) -> mxArray * { + mxArray *array = mxCreateNumericMatrix(1, 1, class_id, mxREAL); + auto *data = static_cast<T *>(mxGetData(array)); + data[0] = value; + return array; +} + +// Convenience shortcut functions + +inline auto mxScalarUINT64(UINT64_T value) -> mxArray * { + return mxScalar<UINT64_T>(value, mxUINT64_CLASS); +} +inline auto mxScalarINT64(INT64_T value) -> mxArray * { + return mxScalar<INT64_T>(value, mxINT64_CLASS); +} +inline auto mxScalarDOUBLE(double value) -> mxArray * { return mxCreateDoubleScalar(value); } +inline auto mxScalarLOGICAL(bool value) -> mxArray * { + return mxCreateLogicalScalar(static_cast<mxLogical>(value)); +} +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 mxEmptyCHAR() -> mxArray * { + mxArray *array = mxCreateCharArray(0, (mwSize const *)nullptr); + return array; +} +inline auto mxEmptySTRING() -> mxArray * { + mxArray *array = mxCreateString(""); + return array; +} +inline auto mxEmptySparse() -> mxArray * { + mxArray *array = mxCreateSparse(0, 0, 0, mxREAL); + return array; +} +inline auto mxEmptyStruct() -> mxArray * { + mxArray *array = mxCreateStructArray(0, (mwSize const *)nullptr, 0, (char const **)nullptr); + return array; +} +inline auto mxEmptyCell() -> mxArray * { + mxArray *array = mxCreateCellMatrix(0, 0); + return array; +} /** - * @brief Convert an Entity from libcaosdb to Octave struct mexArray. + * @brief Get the value of a scalar array. + */ +template <typename T> inline auto mxGetScalarValue(mxArray *scalar) -> T { + return *(static_cast<T *>(mxGetData(scalar))); +} + +/** + * @brief Create a struct array from a string container. * - * @return A 1x1 struct array whose entry follows the maoxdb Entity convention - * outlined above. + * @details Just a convenience wrapper for C++ data structures. + * + * @param fields A container of string-convertibles. Must support data() and size(). + * + * @param fields A container of ints. Must support data() and size(). + * + * @return The struct array */ -auto mxFromCaosDBEntity(const caosdb::entity::Entity &entity) -> mxArray *; +template <typename FieldContainer, typename DimsContainer> +auto mxCreateStructFromStrings(const FieldContainer &fields, const DimsContainer &dims) + -> mxArray * { + auto char_fields = std::vector<const char *>(); + auto alloc = std::allocator<char>(); + for (auto field : fields) { + std::string field_str(field); + auto *data = alloc.allocate(field_str.size() + 1); + std::strcpy(data, field_str.c_str()); + char_fields.push_back(data); + } + auto result = + mxCreateStructArray(dims.size(), dims.data(), char_fields.size(), char_fields.data()); + // TODO (dh) Memory is not freed afterwards. + return result; +} -// Utility functions ////////////////////////////////////////////////////////// +/** + * @brief Create 1x1 a struct array from a string container. + * + * @details Just a convenience wrapper for C++ data structures. + * + * @param fields A container of string-convertibles. Must support data() and size(). + * + * @return The struct array + */ +template <typename FieldContainer> +auto mxCreateStructFromStrings(const FieldContainer &fields) -> mxArray * { + const std::array<mwSize, 2> dims = {1, 1}; + return mxCreateStructFromStrings(fields, dims); +} /** - * @brief Merges a number of scalar mex structs into a 1xN struct. + * @brief Extract a std::string from a char array * - * @details The content (the mxArrays behind the input structs values) is - * duplicated. + * @details Handles the length check * - * @param structs: Must all have the same fields. + * @param array The char array from which the string shall be extracted. * - * @return A 1xN struct array. + * @return The std::string. */ -auto mxMergeScalarStructs(const std::vector<const mxArray *> &structs) -> mxArray *; +auto mxGetStdString(const mxArray *array) -> std::string; + +/** + * @brief Convert the mx cell string array to a string vector. + */ +auto mxCellToStrings(const mxArray *array) -> std::vector<std::string>; + +/** + * @brief Convert the enum to an appropriate string array. + * + * @details There must be a specialization upstream for each enum type. + */ +template <typename Enum> auto mxEnumToString(Enum enum_value) -> mxArray * { + auto const name = caosdb::utility::getEnumNameFromValue(enum_value); + return mxCreateString(name.c_str()); +} + +/** + * @brief Convert the array to an enum. + * + * @details There must be a specialization upstream for each enum type. + */ +template <typename Enum> auto mxStringToEnum(const mxArray *const array) -> Enum { + if (mxIsEmpty(array)) { + auto value = static_cast<Enum>(0); // attempt to use the default value + return value; + } + Enum value = caosdb::utility::getEnumValueFromName<Enum>(mxGetStdString(array)); + return value; +} } // namespace maoxdb diff --git a/src/private/getfielddefault.m b/src/private/getfielddefault.m new file mode 100644 index 0000000000000000000000000000000000000000..5e3359ed7e9d8ab90294c6c9ed307bcb75d79651 --- /dev/null +++ b/src/private/getfielddefault.m @@ -0,0 +1,28 @@ +% 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/>. + +%% getfield(...) if the field exists, else default. +% +% Additional index arguments are used in both cases. +function result = getfielddefault(str, field, default, varargin) + if isfield(str, field) + result = getfield(str, field, varargin); + else + result = default(varargin{:}); + end +end diff --git a/src/private/maox_caosdb.cpp b/src/private/maox_caosdb.cpp index 7d1d5eb5e0a2bc44b248c2c53bca0105db2f2b40..38444ca4cbeb53fab036174462d9f70ac914706a 100644 --- a/src/private/maox_caosdb.cpp +++ b/src/private/maox_caosdb.cpp @@ -1,3 +1,23 @@ +/* + * 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/connection.h" // for Connection, ConnectionManager #include "caosdb/constants.h" // for LIBCAOSDB_VERSION_MAJOR, LIBCAOSDB_VE... #include "caosdb/info.h" // for VersionInfo diff --git a/src/private/maox_convert_collection.m b/src/private/maox_convert_collection.m index eec0229d395af8e956b8bcee5bd18f2a743ea023..ee54ea42926188f3a8bcc20db2c1266035d4046e 100644 --- a/src/private/maox_convert_collection.m +++ b/src/private/maox_convert_collection.m @@ -19,7 +19,31 @@ %% Convert a struct array as received from maoxdb to a cell array of Entity objects. function entities = maox_convert_collection(collection) entities = cell(); + if isempty(collection) % We don't care about the type in this case. + return + end + assert(isstruct(collection), "caosdb:InvalidArgument", ... + ["1xN struct array required.\n " disp(collection)]); for data = collection - entities{end + 1} = Entity(data); + ent = Entity(data); + entities{end + 1} = ent; + if ent.has_errors() + warning("caosdb:transactionError", ["Entity `" ent.id "` has error(s):"]); + for msg = ent.get_errors() + disp(msg{1}); + end + end + if ent.has_warnings() + warning("caosdb:transactionWarning", ["Entity `" ent.id "` has warning(s):"]); + for msg = ent.get_warnings() + disp(msg{1}); + end + end + % if ent.has_infos() + % warning("caosdb:transactionInfo", ["Entity `" ent.id "` has info(s):"]); + % for msg = ent.get_infos() + % disp(msg{1}) + % end + % end end end diff --git a/src/private/maox_info.cpp b/src/private/maox_info.cpp index f1f1774493cd20ccbf3b80d67af2fe69fffbcb33..e8d3c5a7053ac4005870aeae23bf02a2f66df891 100644 --- a/src/private/maox_info.cpp +++ b/src/private/maox_info.cpp @@ -1,3 +1,23 @@ +/* + * 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/connection.h" // for Connection, ConnectionManager #include "caosdb/constants.h" // for LIBCAOSDB_VERSION_MAJOR, LIBCAOSDB_VE... #include "caosdb/exceptions.h" // for all error handling @@ -12,6 +32,8 @@ using caosdb::connection::Connection; using caosdb::connection::ConnectionManager; using std::string; +const auto logger_name = "maox_info"; + auto info(const string &connection_name) -> mxArray *; /** diff --git a/src/private/maox_pack_value.m b/src/private/maox_pack_value.m new file mode 100644 index 0000000000000000000000000000000000000000..b220ec15ead47337959a872da8e2095d6cfcc9ce --- /dev/null +++ b/src/private/maox_pack_value.m @@ -0,0 +1,41 @@ +% 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/>. + +%% Pack the value for handling by maoxdb +% +% This means that the value will be packed into a cell array if it is considered a list. +function result = maox_pack_value(value, is_list) + % Pack list values into a 1x1 cell array + if is_list + if isempty(value) && ~ischar(value) % empty list + result = {}; + else + result = {value}; + end + else % test if value looks scalar + if iscellstr(value) || (isnumeric(value) && numel(value) > 1) + error("caosdb:valueError", "Value looks list-like, but it must be scalar."); + end + % handle empty scalar value + if isempty(value) && ~ischar(value) + result = sparse([]); + else + result = value; + end + end +end diff --git a/src/private/maox_query.cpp b/src/private/maox_query.cpp deleted file mode 100644 index 958d201a4d4912fc1709b6a04808972b69a33d84..0000000000000000000000000000000000000000 --- a/src/private/maox_query.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#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 deleted file mode 100644 index a15940249bfd5f400c796a7bb871fbe8766371b7..0000000000000000000000000000000000000000 --- a/src/private/maox_retrieve.cpp +++ /dev/null @@ -1,85 +0,0 @@ -#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/src/private/maox_run_transaction.m b/src/private/maox_run_transaction.m new file mode 100644 index 0000000000000000000000000000000000000000..45b977840bb9516d3bfbd0f9d900268575c8a6c9 --- /dev/null +++ b/src/private/maox_run_transaction.m @@ -0,0 +1,119 @@ +% 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/>. + +%% +% Create and execute a transaction with all possible parameters (which may be empty). +% +% This is the transaction implementation which shall be called by the simple wrappers for insert, +% update, delete. +% +% Parameters +% ---------- +% +% connection_name : string +% The connection name +% +% retrieve_ids : string cell array +% The IDs to retrieve by ID. +% +% queries : string cell array +% The queries to execute. +% +% inserts : struct array +% The entities to insert, as a struct array (not Entity objects). +% +% updates : struct array +% The entities to update, as a struct array (not Entity objects). +% +% deletes : string cell array +% The IDs to delete. +% +% Returns +% ------- +% single_result : cell array +% The retrieved entities. If no entities are returned and the query was a COUNT query, the result +% is an int64 instead with the COUNT result. If there are returned entities AND there is a count +% result, a warning is printed and the result is a cell array with two elements: the entities and +% the count result. NOTE: The arguments should be so that this does not happen, and the behaviour +% may change in the future. +% +% entities, count_results : cell array, int64 +% With two output arguments, the first is the cell array with the entities, and the second is the +% COUNT result. The count result is -1 if there was not exactly one COUNT query. +function [entities, count_results] = maox_run_transaction(connection_name, ... + retrieve_ids, queries, inserts, ... + updates, deletes, ... + file_ids, file_paths) + % Boilerplate for default values + if nargin == 0 + connection_name = ""; + end + if nargin < 2 + retrieve_ids = {}; + end + if nargin < 3 + queries = {}; + end + if nargin < 4 + inserts = struct("", {}); + end + if nargin < 5 + updates = struct("", {}); + end + if nargin < 6 + deletes = {}; + end + if nargin == 7 + error("maox:InvalidArgument", ... + "If file IDs are given for download, file locations must be given as well."); + end + if nargin < 8 + file_ids = {}; + file_paths = {}; + end + assert(numel(file_ids) == numel(file_paths), "maox:InvalidArgument", ... + "Number of file IDs and file paths must be the same."); + + try + % Retrieve and convert + % disp(["----\nmaox_run_transaction(...) --> ", num2str(nargout()), "\n----"]); + % disp({connection_name, retrieve_ids, queries, ... + % inserts, updates, deletes}); + [collection, count_results] = maox_transaction(connection_name, retrieve_ids, queries, ... + inserts, updates, deletes, file_ids, file_paths); + % disp("--- converting to Objects ---"); + entities = maox_convert_collection(collection); + % disp("--- converted to Objects ---"); + % And some output argument handling + if nargout == 1 + if count_results >= 0 + entities = count_results; + + if not(isempty(entities)) + warning("caosdb:LostValues", ... + ["Only the count result was returned although there were entities. " + "Consider using two output arguments."]); + end + end + end + catch + % disp("error handling in Caosdb.m"); + % disp(lasterror()); + rethrow(lasterror()); + end +end diff --git a/src/private/maox_transaction.cpp b/src/private/maox_transaction.cpp new file mode 100644 index 0000000000000000000000000000000000000000..f6ec709be2a910fa2a1919fc513c5984d807e69b --- /dev/null +++ b/src/private/maox_transaction.cpp @@ -0,0 +1,236 @@ +/* + * 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/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 "caosdb/logging.h" // for CAOSDB_LOG_TRACE +#include "caosdb/transaction_status.h" // for TransactionStatus +#include "maoxdb.hpp" // caosDB utils for mex files +#include "mex.h" // for mxArray, mexFunction +#include <algorithm> // for strcmp +#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::entity::Entity; +using std::string; + +// Workaround for issue caosdb-cpplib#17 +#undef CAOSDB_LOG_TRACE +#undef CAOSDB_LOG_DEBUG +#undef CAOSDB_LOG_INFO +// #define CAOSDB_LOG_TRACE(name) std::clog << std::endl << "[" << name << "] " +// #define CAOSDB_LOG_DEBUG(name) std::clog << std::endl << "[" << name << "] " +// #define CAOSDB_LOG_INFO(name) std::clog << std::endl << "[" << name << "] " + +std::ostream nullout(nullptr); // NOLINT + +#define CAOSDB_LOG_TRACE(name) nullout // NOLINT +#define CAOSDB_LOG_DEBUG(name) nullout // NOLINT +#define CAOSDB_LOG_INFO(name) nullout // NOLINT + +const auto logger_name = "maox_transaction"; +/** + * @brief Execute a rich transaction. + * + * @details This function enables users to stage multiple actions and execute all of them in a + * single transaction. + * + * @note The code currently is a mix of all arguments required, and only some required (it may + * segfault with debugging logging if there are arguments missing). It is + * definitely safer to have all arguments, but it should be pretty simple to remove this + * requirement if need be. + * + * @param connection_name A string with the connection name. May be omitted or + * an empty string. + * + * @param retrieve_ids A cell string array of IDs to retrieve. + * + * @param queries A cell array of query strings (for retrieval). More than one query is not + * supported at the moment though. + * + * @param inserts A struct array of Entities to insert. + * + * @param updates A struct array of Entities to update. The behaviour is undefined if there are + * several Entities with the same ID. The versionId of all Entities must be the + * same as the remote HEAD. + * + * @param deletes A cell array of IDs to delete. + * + * @param file_ids A cell array of IDs of files to download. + * + * @param file_download_paths A cell array of paths where the files shall be downloaded. Must be at + * least as large as the file_ids celll array. + * + * @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; + std::vector<string> retrieves; + std::vector<string> queries; + std::vector<Entity> inserts; + std::vector<Entity> updates; + std::vector<string> deletes; + std::vector<string> file_ids; + std::vector<string> file_download_paths; + // Argument handling + auto helpText = string("maox_transaction(connection_name, retrieve_ids, queries, inserts, ") + + "updates, deletes, file_ids, file_paths)"; + try { + CAOSDB_LOG_DEBUG(logger_name) << "maox_transaction(): " << nlhs << " <- " << nrhs << "(" + << (nrhs == 8) << ")" << std::endl; // NOLINT + CAOSDB_LOG_DEBUG(logger_name) << " ( " << mxGetNumberOfElements(prhs[0]) << ", " + << mxGetNumberOfElements(prhs[1]) << ", " + << mxGetNumberOfElements(prhs[2]) << ", " + << mxGetNumberOfElements(prhs[3]) << ", " + << mxGetNumberOfElements(prhs[4]) << ", " + << mxGetNumberOfElements(prhs[5]) << ", " // NOLINT + << mxGetNumberOfElements(prhs[6]) << ", " // NOLINT + << mxGetNumberOfElements(prhs[7]) << ")" << std::endl; // NOLINT + if (nrhs < 2) { + mexErrMsgIdAndTxt("maox:InsufficientArguments", + (string("Need at least 2 arguments: ") + helpText).c_str()); + } + if (nrhs > 8) { // NOLINT + mexErrMsgIdAndTxt("maox:TooManyArguments", + (string("Can handle at most 8 arguments: ") + helpText).c_str()); + } + 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[0])) { + mexErrMsgIdAndTxt("maox:InvalidArgument", "The connection name must be a string."); + } + CAOSDB_LOG_TRACE(logger_name) << "0" << std::endl; + conn_name = maoxdb::mxGetStdString(prhs[0]); + CAOSDB_LOG_TRACE(logger_name) << "1" << std::endl; + retrieves = maoxdb::mxCellToStrings(prhs[1]); + if (nrhs > 2) { + CAOSDB_LOG_TRACE(logger_name) << "2" << std::endl; + queries = maoxdb::mxCellToStrings(prhs[2]); + } + if (nrhs > 3) { + CAOSDB_LOG_DEBUG(logger_name) << "3: insert" << std::endl; + inserts = maoxdb::entitiesFromMx(prhs[3], false, conn_name); + } + if (nrhs > 4) { + CAOSDB_LOG_TRACE(logger_name) << "4" << std::endl; + updates = maoxdb::entitiesFromMx(prhs[4], true, conn_name); + } + if (nrhs > 5) { // NOLINT + CAOSDB_LOG_TRACE(logger_name) << "5" << std::endl; + deletes = maoxdb::mxCellToStrings(prhs[5]); // NOLINT + } + if (nrhs == 7) { // NOLINT + mexErrMsgIdAndTxt("maox:InvalidArgument", "File IDs need file download path arguments"); + } + if (nrhs > 7) { // NOLINT + CAOSDB_LOG_TRACE(logger_name) << "6 & 7" << std::endl; + file_ids = maoxdb::mxCellToStrings(prhs[6]); // NOLINT + file_download_paths = maoxdb::mxCellToStrings(prhs[7]); // NOLINT + if (file_ids.size() != file_download_paths.size()) { + mexErrMsgIdAndTxt("maox:InvalidArgument", + "File IDs and file download paths must have the same number of elements"); + } + } + } catch (const std::exception &exc) { + mexErrMsgIdAndTxt("maox:ArgumentHandling", + (string("Error while handling the arguments: ") + exc.what()).c_str()); + } + + // Execute the query + std::shared_ptr<Connection> connection = nullptr; + if (conn_name.empty()) { + connection = ConnectionManager::GetDefaultConnection(); + } else { + connection = ConnectionManager::GetConnection(conn_name); + } + // CAOSDB_LOG_TRACE(logger_name) << "connection.use_count() " << connection.use_count(); + auto transaction = connection->CreateTransaction(); + // Fill transaction with content + std::for_each(retrieves.begin(), retrieves.end(), + [&](const string &id) { transaction->RetrieveById(id); }); + // CAOSDB_LOG_DEBUG(logger_name) << "# queries: " << queries.size(); + std::for_each(queries.begin(), queries.end(), [&](const string &query) { + CAOSDB_LOG_TRACE(logger_name) << "Adding query: " << query; + transaction->Query(query); + }); + std::for_each(inserts.begin(), inserts.end(), + [&](Entity &ent) { transaction->InsertEntity(&ent); }); + std::for_each(updates.begin(), updates.end(), + [&](Entity &ent) { transaction->UpdateEntity(&ent); }); + std::for_each(deletes.begin(), deletes.end(), + [&](const string &id) { transaction->DeleteById(id); }); + for (size_t i = 0; i < file_ids.size(); ++i) { + transaction->RetrieveAndDownloadFileById(file_ids[i], file_download_paths[i]); + } + // Execute transaction, if there is anything queued. + if (transaction->IsStatus(caosdb::transaction::TransactionStatus::READY()) || + transaction->IsStatus(caosdb::transaction::TransactionStatus::GO_ON())) { + const auto exec_stat = transaction->ExecuteAsynchronously(); // NOLINT + // CAOSDB_LOG_TRACE(logger_name) << "Execute initiated, exec_stat: " + // << static_cast<int>(exec_stat) + // << " / " + // << caosdb::get_status_description(exec_stat); + if (exec_stat != caosdb::StatusCode::EXECUTING) { + mexErrMsgIdAndTxt("maox:Execution", (string("Executing the transaction failed (") + + std::to_string(static_cast<int>(exec_stat)) + + "): " + caosdb::get_status_description(exec_stat)) + .c_str()); + } + const auto t_stat = transaction->WaitForIt(); + // CAOSDB_LOG_TRACE(logger_name) << "Waited"; + // CAOSDB_LOG_DEBUG(logger_name) << "status: " << t_stat.GetCode() << " // " + // << t_stat.GetDescription(); + maoxdb::throwOctExceptionIfError(transaction.get()); + // Status must be OK, INITIAL or GENERIC_TRANSACTION_ERROR now. + // CAOSDB_LOG_TRACE(logger_name) << "No Octave exception thrown."; + // std::cout << "status: " << transaction->GetStatus().GetCode() << " // " + // << transaction->GetStatus().GetDescription() << std::endl; + } + const auto &results = transaction->GetResultSet(); + CAOSDB_LOG_TRACE(logger_name) << "ResultSet contents: "; + CAOSDB_LOG_TRACE(logger_name) << "size: " << results.size(); + for (const auto &result : results) { + CAOSDB_LOG_TRACE(logger_name) << "errors = " << result.HasErrors(); + CAOSDB_LOG_TRACE(logger_name) << result.ToString(); + } + auto *mxResults = maoxdb::mxFromResultSet(results); + CAOSDB_LOG_TRACE(logger_name) << "results converted."; + auto *mxCountResults = maoxdb::mxScalarINT64(transaction->GetCountResult()); + CAOSDB_LOG_TRACE(logger_name) << "counts converted."; + + plhs[0] = mxDuplicateArray(mxResults); + plhs[1] = mxCountResults; + CAOSDB_LOG_TRACE(logger_name) << "\n---" << std::endl; +} diff --git a/test/Makefile b/test/Makefile index 2993b098fd36c233b1404cc9137faf5453694404..3d03396132385ac1bc077c96ba2e42bb4dea9710 100644 --- a/test/Makefile +++ b/test/Makefile @@ -36,7 +36,7 @@ help: # Tests # ############################################################################### -test: test_octave test_cpp +test: test_cpp test_octave .PHONY: test test_octave: @@ -46,7 +46,7 @@ test_octave: # && make test || echo "Test(s) failed."; exit 1 test_cpp: - $(eval BUILD_DIR ::= $(shell mktemp -d build_XXXXXXXX)) + $(eval BUILD_DIR ::= $(shell mktemp -d build_test_XXXXXXXX)) cd $(BUILD_DIR) \ && conan install ../.. -s "compiler.libcxx=libstdc++11" \ && cmake -D TEST=On ../.. \ diff --git a/test/Run_Test.m b/test/Run_Test.m index 37f818b9ef8fad605a2da08f48cb733d750a328b..a3f607bba65df1456fa1eaa010b693f001f38631 100644 --- a/test/Run_Test.m +++ b/test/Run_Test.m @@ -20,7 +20,7 @@ pkg load caosdb; all_tests = true; all_tests &= moxunit_runtests("-verbose", "test_unittest.m"); -all_tests &= moxunit_runtests("-verbose", "test_caosdb.m"); +all_tests &= moxunit_runtests("-verbose", "test_query_retrieve.m"); if not(all_tests) exit(1); diff --git a/test/test_caosdb_conversion.cpp b/test/test_caosdb_conversion.cpp index fa773ea4b517c08f2fec69aec748d55a7f2fa631..f8d1419bae51577e95ffc1dc4f1b8607fcc9ae96 100644 --- a/test/test_caosdb_conversion.cpp +++ b/test/test_caosdb_conversion.cpp @@ -21,7 +21,6 @@ #include "caosdb/exceptions.h" #include "maoxdb.hpp" #include "mex.h" -// #include "mexproto.h" #include <boost/lexical_cast.hpp> #include <gtest/gtest.h> #include <charconv> @@ -31,72 +30,23 @@ #include <type_traits> #include <vector> +namespace maoxdb { + using std::string; -namespace maoxdb { +namespace ce = caosdb::entity; /////////////////////////////////////////////////////////////////////////////// // 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 << "'."; - } +// Test conversion from native to mx and back +template <typename T> void test_value_native(const std::vector<T> &value_contents) { + for (auto c : value_contents) { + auto v = ce::Value(c); + auto mx_v = mxFromValue(v); + auto vv = valueFromMx(mx_v); + EXPECT_EQ(v, vv); } } @@ -139,64 +89,61 @@ template <> auto toStr(double value) -> std::string { /** * 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_String) { + auto value_contents = std::vector<string>{"", "23", "foo", "CaosDB", "\n\n\n\a\n</>"}; + test_value_native(value_contents); } -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_Bool) { + auto value_contents = std::vector<bool>{true, false}; + test_value_native(value_contents); } -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_Integer) { + auto value_contents = generateValues<long>(); + test_value_native(value_contents); } -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); +TEST(caosdb_conversion, value_Double) { + auto value_contents = generateValues<double>(); + test_value_native(value_contents); } +/** + * Test Entity generation vom mxArray. + */ +TEST(caosdb_conversion, file_entity) { + // clang-format off + auto fields = std::vector<string> + {"role", + "id", + "versionId", + "name", + "description", + "datatype", + "unit", + "value", + "parents", + "properties", + "filepath", + "localpath" + }; + // clang-format on + auto *array = mxCreateStructFromStrings(fields); + mxSetField(array, 0, "role", mxCreateString("FILE")); + mxSetField(array, 0, "id", mxCreateString("-1")); + mxSetField(array, 0, "versionId", mxEmptyCHAR()); + mxSetField(array, 0, "name", mxCreateString("File_1")); + mxSetField(array, 0, "description", mxEmptyCHAR()); + mxSetField(array, 0, "datatype", mxEmptyCHAR()); + mxSetField(array, 0, "unit", mxEmptyCHAR()); + mxSetField(array, 0, "value", mxEmptySparse()); + mxSetField(array, 0, "filepath", mxCreateString("testfile.xyz")); + mxSetField(array, 0, "localpath", mxCreateString("/dev/null")); + mxSetField(array, 0, "parents", mxEmptyCell()); + mxSetField(array, 0, "properties", mxEmptyCell()); + auto entities = entitiesFromMx(array, false, ""); +} /* * The following are treated just like TEXT, at least for the moment: * - REFERENCE diff --git a/test/test_caosdb.m b/test/test_query_retrieve.m similarity index 98% rename from test/test_caosdb.m rename to test/test_query_retrieve.m index cd61886ab21cab1cd95307c6455c142970d004c0..77fb1402a4ad8e3265172fae2a8dc9f74c50412d 100644 --- a/test/test_caosdb.m +++ b/test/test_query_retrieve.m @@ -17,7 +17,7 @@ % along with this program. If not, see <https://www.gnu.org/licenses/>. %% The main function which intitializes the tests. -function test_suite = test_caosdb() +function test_suite = test_query_retrieve() try % assignment of 'localfunctions' is necessary in Matlab >= 2016 test_functions = localfunctions(); catch % no problem; early Matlab versions can use initTestSuite fine diff --git a/test/test_unittest.m b/test/test_unittest.m index 45e1d35a066851a00492ac25421353b8dc19ffa2..33b6fc758466e79ada0c6aca9c43289f55faac74 100644 --- a/test/test_unittest.m +++ b/test/test_unittest.m @@ -37,3 +37,48 @@ function test_local() version = caosdb_exec("--version"); assertEqual(version(1:21), "v0.1 (libcaosdb v0.0."); end + +% Test if conversion between Entity objects and mxArray structures works. +function test_entity_conversion() + % set plain Octave properties + p1 = Property(); + p1.id = "P1_id"; + p1.name = "Prop 1"; + p1.description = ""; + p1.importance = "somewhat"; + p1.set_datatype("DOUBLE"); + p1.unit = "Mt"; + p1.value = []; + + p2 = Property(); + p2.id = "P2_id"; + p2.name = "Prop 2"; + p2.description = ""; + p2.importance = "very"; + p2.set_datatype("BOOLEAN"); + p2.value = 1; + + par = Parent(); + par.id = "-1"; + par.name = "Parent 1"; + par.description = "self-inheritance"; + + e = Entity(); + e.role = "Record"; + e.id = "-1"; + e.name = "Test Record"; + + % merge objects + e.set_properties({p1, p2}); + e.set_parents({par}); + + % convert forwards and backwards + e_struct = e.to_struct(); + e_clone = Entity(e_struct); + + % Make sure that the conversion was lossless + assertEqual(e_clone.id, e.id); + assertEqual(numel(e.get_parents()), 1); + assertEqual(numel(e.get_properties()), 2); + assertTrue(isequal(struct(e), struct(e_clone))); +end diff --git a/test/test_utilities.cpp b/test/test_utilities.cpp index 72a453b8998c8cc358a6ed843e96f1e7c6eefb9c..57fb2571d76cf1fb6b49681abba1bcf5460a534f 100644 --- a/test/test_utilities.cpp +++ b/test/test_utilities.cpp @@ -21,7 +21,6 @@ #include "caosdb/exceptions.h" #include "maoxdb.hpp" #include "mex.h" -// #include "mexproto.h" #include <gtest/gtest.h> #include <limits> #include <string> @@ -40,33 +39,65 @@ 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(mxGetScalarValue<UINT64_T>(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(mxGetScalarValue<INT64_T>(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(mxGetScalarValue<double>(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(mxGetScalarValue<bool>(scalar), value); } void test_scalar_empty() { mxArray *scalar = mxEmptyDOUBLE(); EXPECT_TRUE(mxIsDouble(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); + + scalar = mxEmptyUINT64(); + EXPECT_TRUE(mxIsUint64(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); + + scalar = mxEmptyINT64(); + EXPECT_TRUE(mxIsInt64(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); + + scalar = mxEmptyLOGICAL(); + EXPECT_TRUE(mxIsLogical(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); + + scalar = mxEmptyCHAR(); + EXPECT_TRUE(mxIsChar(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); + + scalar = mxEmptySTRING(); + EXPECT_TRUE(mxIsChar(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); + EXPECT_EQ(mxGetNumberOfElements(scalar), 0); + EXPECT_EQ(mxGetNumberOfDimensions(scalar), 2); + + scalar = mxEmptySparse(); + EXPECT_TRUE(mxIsSparse(scalar)); + EXPECT_TRUE(mxIsEmpty(scalar)); EXPECT_EQ(mxGetNumberOfElements(scalar), 0); } @@ -116,6 +147,15 @@ TEST(utilities, scalar_arrays) { TEST(utilities, empty_array) { test_scalar_empty(); } +TEST(utilities, empty_cellstring) { + auto empty = mxEmptyCell(); + auto empty_strings = mxCellToStrings(empty); + EXPECT_TRUE(empty_strings.empty()); + + empty = mxEmptyStruct(); + EXPECT_EQ(mxGetNumberOfElements(empty), 0); +} + /** * Test exception handling */