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
  */