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